From e13f880685b0f02340b7f53627b70b264d3c7764 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:53:53 -0700 Subject: [PATCH] docs: add a warn-only last-walked check for docs-site pages (TASK-020 PR 1) docs/audits/docs-last-walked.json maps each docs-site page to the date it was last walked against prod. Dates only, so public pages carry no walk records. The sidecar starts empty, so every page is listed as having no walk date until the re-walk fills it in. scripts/check-docs-last-walked.js warns when a page has no entry, when its walk date is more than 30 days before the run date, when an entry names no page, or when a date is not a real YYYY-MM-DD. It always exits 0 and is not wired into CI in this PR. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: quire Agent-Model: claude/claude-haiku-5-5/high --- docs/audits/docs-last-walked.json | 1 + .../__tests__/check-docs-last-walked.test.mjs | 55 +++++++++ scripts/check-docs-last-walked.js | 110 ++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 docs/audits/docs-last-walked.json create mode 100644 scripts/__tests__/check-docs-last-walked.test.mjs create mode 100644 scripts/check-docs-last-walked.js diff --git a/docs/audits/docs-last-walked.json b/docs/audits/docs-last-walked.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/docs/audits/docs-last-walked.json @@ -0,0 +1 @@ +{} diff --git a/scripts/__tests__/check-docs-last-walked.test.mjs b/scripts/__tests__/check-docs-last-walked.test.mjs new file mode 100644 index 0000000000..fdbe39ab5f --- /dev/null +++ b/scripts/__tests__/check-docs-last-walked.test.mjs @@ -0,0 +1,55 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { checkLastWalked, parseDate } from '../check-docs-last-walked.js'; + +const RUN = '2026-10-28'; +const PAGE = 'quickstart.mdx'; + +// Age in days of a walk date relative to RUN, built from the same clock the script uses. +const daysBefore = (n) => new Date(Date.parse(`${RUN}T00:00:00Z`) - n * 86400000).toISOString().slice(0, 10); + +test('a page with no sidecar entry warns', () => { + const w = checkLastWalked({ pages: [PAGE], walked: {}, runDate: RUN }); + assert.equal(w.length, 1); + assert.equal(w[0].page, PAGE); + assert.match(w[0].message, /no walk date/); +}); + +test('a walk 29 days before the run date is ok', () => { + const w = checkLastWalked({ pages: [PAGE], walked: { [PAGE]: daysBefore(29) }, runDate: RUN }); + assert.deepEqual(w, []); +}); + +test('a walk exactly 30 days before the run date is still ok', () => { + const w = checkLastWalked({ pages: [PAGE], walked: { [PAGE]: daysBefore(30) }, runDate: RUN }); + assert.deepEqual(w, []); +}); + +test('a walk 31 days before the run date warns and names the age', () => { + const w = checkLastWalked({ pages: [PAGE], walked: { [PAGE]: daysBefore(31) }, runDate: RUN }); + assert.equal(w.length, 1); + assert.match(w[0].message, /31 days before 2026-10-28 \(limit 30\)/); +}); + +test('a sidecar entry that names no page warns', () => { + const w = checkLastWalked({ pages: [PAGE], walked: { 'gone.mdx': RUN, [PAGE]: RUN }, runDate: RUN }); + assert.equal(w.length, 1); + assert.equal(w[0].page, 'gone.mdx'); + assert.match(w[0].message, /not a docs-site page/); +}); + +test('a date that is not a real YYYY-MM-DD warns instead of passing silently', () => { + const w = checkLastWalked({ pages: [PAGE], walked: { [PAGE]: '2026-02-30' }, runDate: RUN }); + assert.equal(w.length, 1); + assert.match(w[0].message, /not a real YYYY-MM-DD/); +}); + +test('parseDate rejects a non-date and accepts a real one', () => { + assert.equal(parseDate('10/28/2026'), null); + assert.equal(parseDate('2026-13-01'), null); + assert.notEqual(parseDate('2026-10-28'), null); +}); + +test('a bad run date throws rather than checking against NaN', () => { + assert.throws(() => checkLastWalked({ pages: [], walked: {}, runDate: 'today' }), /run date must be YYYY-MM-DD/); +}); diff --git a/scripts/check-docs-last-walked.js b/scripts/check-docs-last-walked.js new file mode 100644 index 0000000000..9b8135daa9 --- /dev/null +++ b/scripts/check-docs-last-walked.js @@ -0,0 +1,110 @@ +#!/usr/bin/env node +/** + * Last-walked guard for the docs site (TASK-020). + * + * Every page under `docs-site/` should carry a walk date: the day someone last + * followed its steps against prod. The dates live in a sidecar, + * `docs/audits/docs-last-walked.json`, not in page frontmatter, because public + * pages carry no walk records (Sam, 2026-10-28). The sidecar maps a page path + * (relative to `docs-site/`) to a YYYY-MM-DD date and nothing else. + * + * Warn-only: this script always exits 0. CI wiring comes in a later PR, once + * the dates exist. + * + * Warnings: + * - a page has no entry in the sidecar + * - a page's walk date is more than 30 days before the run date + * - a sidecar entry names no page + * - a sidecar date is not a real YYYY-MM-DD + * + * Usage: node scripts/check-docs-last-walked.js [YYYY-MM-DD] + * The run date defaults to today. Tests pass it explicitly. + */ + +const fs = require('fs'); +const path = require('path'); + +const MAX_AGE_DAYS = 30; +const DAY_MS = 24 * 60 * 60 * 1000; +const DATE_RE = /^\d{4}-\d{2}-\d{2}$/; + +function parseDate(value) { + if (typeof value !== 'string' || !DATE_RE.test(value)) return null; + const ms = Date.parse(`${value}T00:00:00Z`); + if (Number.isNaN(ms) || new Date(ms).toISOString().slice(0, 10) !== value) return null; + return ms; +} + +/** + * Pure check. `pages` is an array of page paths, `walked` is the parsed sidecar + * object, `runDate` is YYYY-MM-DD. Returns an array of warning strings. + */ +function checkLastWalked({ pages, walked, runDate }) { + const runMs = parseDate(runDate); + if (runMs === null) throw new Error(`run date must be YYYY-MM-DD, got "${runDate}"`); + + const warnings = []; + const pageSet = new Set(pages); + + for (const page of pages) { + if (!Object.prototype.hasOwnProperty.call(walked, page)) { + warnings.push({ page, message: `has no walk date in docs/audits/docs-last-walked.json` }); + continue; + } + const walkedMs = parseDate(walked[page]); + if (walkedMs === null) { + warnings.push({ page, message: `walk date "${walked[page]}" is not a real YYYY-MM-DD` }); + continue; + } + const ageDays = Math.round((runMs - walkedMs) / DAY_MS); + if (ageDays > MAX_AGE_DAYS) { + warnings.push({ + page, + message: `last walked ${walked[page]}, ${ageDays} days before ${runDate} (limit ${MAX_AGE_DAYS})`, + }); + } + } + + for (const entry of Object.keys(walked)) { + if (!pageSet.has(entry)) { + warnings.push({ page: entry, message: `is in docs/audits/docs-last-walked.json but is not a docs-site page` }); + } + } + + return warnings; +} + +function listPages(docsDir) { + const out = []; + const walk = (dir) => { + for (const name of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, name.name); + if (name.isDirectory()) walk(full); + else if (name.name.endsWith('.mdx')) out.push(path.relative(docsDir, full).split(path.sep).join('/')); + } + }; + walk(docsDir); + return out.sort(); +} + +function main() { + const repoRoot = path.resolve(__dirname, '..'); + const docsDir = path.join(repoRoot, 'docs-site'); + const sidecarPath = path.join(repoRoot, 'docs', 'audits', 'docs-last-walked.json'); + const runDate = process.argv[2] || new Date().toISOString().slice(0, 10); + + const walked = fs.existsSync(sidecarPath) ? JSON.parse(fs.readFileSync(sidecarPath, 'utf8')) : {}; + const pages = listPages(docsDir); + const warnings = checkLastWalked({ pages, walked, runDate }); + + for (const w of warnings) { + console.warn(`::warning file=docs-site/${w.page}::${w.page} ${w.message}`); + } + console.log( + `${warnings.length} last-walked warning(s) across ${pages.length} docs-site page(s) at ${runDate} (warn-only, exit 0).`, + ); +} + +if (require.main === module) main(); + +module.exports = { checkLastWalked, parseDate, MAX_AGE_DAYS };