Files
Aron PrinsandClaude Opus 4.7 c29d44a92e Add sync helper scripts and address deferred gotchas
Closes the five deferred items from the prior gotcha audit:

A. Link checker — scripts/sync/lint-links.mjs walks docs/**/*.md and
   verifies every relative markdown/image link resolves to an existing
   file. Skips external URLs, absolute SPA routes (/...), URI schemes
   (mailto:, agent://, etc.), and template placeholders. Found 3 real
   pre-existing broken links to missing screenshots — not fixed in
   this commit, but the tool will now block any new ones.

B. Nav verification — scripts/sync/verify-nav.mjs cross-checks
   site/content.json against the docs filesystem. Dangling entries
   (in nav but missing on disk) are errors; orphans (on disk but not
   in nav) are warnings. Found 1 real orphan (require-board-approval-
   before-spend.md from c0ff148 — never registered in content.json)
   that the user should triage separately.

C. Screenshot registry backfill — scripts/sync/backfill-screenshots.mjs
   scaffolds entries for every PNG under docs/user-guides/screenshots/.
   Run once; registry.json now ships with 274 scaffolded entries
   (137 light + 137 dark). Each has placeholder `depends_on: []` and
   a `_todo` field; humans fill in parent-repo path anchors over time
   to activate staleness detection per-entry. Re-runs preserve any
   entry with non-empty depends_on.

D. content.json edit serialisation — SKILL Phase 5 now requires
   subagents to return a structured `nav_addition` object alongside
   page content rather than editing site/content.json directly. The
   orchestrator merges all nav_additions on the main thread after
   subagents finish. Prevents the parallel-write race where two
   subagents creating new pages clobber each other's nav entries.

E. gh compare truncation — SKILL Phase 2 now explicitly documents
   the GitHub compare endpoint's ~300-file/~300-commit cap and the
   window-splitting fallback (walk intermediate release tags for
   release mode; binary-split commit list for nightly mode).

Also wires the scripts into package.json (sync:lint-links,
sync:verify-nav, sync:check, sync:backfill-screenshots) and into
SKILL Phase 7 (sync:check runs before commit; dangling/broken-link
failures abort the run).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-13 10:37:42 +02:00

78 lines
2.8 KiB
JavaScript

#!/usr/bin/env node
// Verifies site/content.json against the docs filesystem.
// - Every entry in content.json must point at an existing file.
// - Every docs/**/*.md file SHOULD be referenced in content.json (orphans are warnings, not failures).
//
// Use after the /sync-docs skill adds new pages, before commit.
//
// Usage: node scripts/sync/verify-nav.mjs [--json] [--strict]
// --strict: orphans become errors (exit 1) instead of warnings.
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, resolve, relative } from "node:path";
const ROOT = resolve(new URL("../..", import.meta.url).pathname);
const DOCS = join(ROOT, "docs");
const CONTENT = join(ROOT, "site/content.json");
function walkMd(dir, acc = []) {
for (const entry of readdirSync(dir)) {
const p = join(dir, entry);
const s = statSync(p);
if (s.isDirectory()) walkMd(p, acc);
else if (entry.endsWith(".md")) acc.push(relative(ROOT, p));
}
return acc;
}
function collectFiles(node, acc = []) {
if (!node || typeof node !== "object") return acc;
if (Array.isArray(node)) {
for (const c of node) collectFiles(c, acc);
return acc;
}
if (typeof node.file === "string") {
acc.push(node.file);
}
for (const v of Object.values(node)) collectFiles(v, acc);
return acc;
}
// Markdown files that live alongside assets but aren't navigable pages.
const IGNORE_PREFIXES = ["docs/user-guides/screenshots/"];
const content = JSON.parse(readFileSync(CONTENT, "utf8"));
const fsFiles = new Set(
walkMd(DOCS).filter((f) => !IGNORE_PREFIXES.some((p) => f.startsWith(p)))
);
// content.json `file` fields are relative to site/ (e.g. "../docs/foo.md").
// Resolve each against site/ to get a repo-root-relative path.
const navFiles = collectFiles(content).map((f) =>
relative(ROOT, resolve(join(ROOT, "site"), f))
);
const dangling = navFiles.filter((f) => !fsFiles.has(f));
const orphans = [...fsFiles].filter((f) => !navFiles.includes(f));
const json = process.argv.includes("--json");
const strict = process.argv.includes("--strict");
if (json) {
console.log(JSON.stringify({ nav_entries: navFiles.length, fs_files: fsFiles.size, dangling, orphans }, null, 2));
} else {
console.log(`Nav entries: ${navFiles.length} | filesystem md files: ${fsFiles.size}`);
if (dangling.length) {
console.error(`\nDangling nav entries (point to missing files): ${dangling.length}`);
for (const f of dangling) console.error(` ${f}`);
}
if (orphans.length) {
console.warn(`\nOrphan files (exist on disk, not in content.json): ${orphans.length}`);
for (const f of orphans) console.warn(` ${f}`);
}
if (!dangling.length && !orphans.length) console.log("OK — nav and filesystem match.");
}
const failed = dangling.length > 0 || (strict && orphans.length > 0);
process.exit(failed ? 1 : 0);