mirror of
https://github.com/paperclipai/paperclip-docs.git
synced 2026-11-07 06:16:07 -10:00
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]>
78 lines
2.8 KiB
JavaScript
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);
|