Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/audits/docs-last-walked.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
55 changes: 55 additions & 0 deletions scripts/__tests__/check-docs-last-walked.test.mjs
Original file line number Diff line number Diff line change
@@ -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/);
});
110 changes: 110 additions & 0 deletions scripts/check-docs-last-walked.js
Original file line number Diff line number Diff line change
@@ -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 };
Loading