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
46 changes: 46 additions & 0 deletions build/check-doc-drift.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2236,6 +2236,51 @@ for (const rel of trackedMd.filter((p) => RUNNABLE_DOC_PREFIXES.some((pre) => p.
});
}

// ── check 23 (F-450 c): the scope-limit canon and the per-pin table agree both ways ──
// doc-lib's CLI_PIN_FACTS carries a `scope` paraphrase per scope-limited list
// command (keyed by catalog id, ONE home, read by report); the canon is prose —
// one `###` subsection per such command under "Scope-limited domains" in
// setup's index-scope-notes.md, headed by the command's canonical path (the
// id with its colons as spaces), which report's `scope.path` points at. A
// table entry with no subsection is a pointer into nothing; a subsection with
// no entry is a limit the report never names. Both are read out of source
// text (check 22's precedent — the build lane may not import the plugin), and
// a reader that no longer matches fails loudly rather than sweeping nothing.
const SCOPE_NOTES_REL = "plugins/gs-superadmin/skills/setup/references/index-scope-notes.md";
const pinBlock = /export const CLI_PIN_FACTS = Object\.freeze\(\{[\s\S]*?\n\}\);/.exec(read(DOC_LIB_REL));
const scopeEntryRe = /"([a-z0-9-]+(?::[a-z0-9-]+)+)": Object\.freeze\(\{[^}]*?\bscope: "/g;
const tableScopePaths = pinBlock ? [...pinBlock[0].matchAll(scopeEntryRe)].map((m) => m[1].split(":").join(" ")) : [];
if (!pinBlock || tableScopePaths.length < 1) {
fail(`check 23: could not read the scope entries out of ${DOC_LIB_REL}'s CLI_PIN_FACTS (got ${tableScopePaths.length}) — the table moved or its shape changed; fix the reader, never the sweep`);
}
// The notes are read through the file's one fence grammar (fenceRanges, as
// check 22 does): a fenced example carrying a heading-shaped line is neither
// canon nor a section terminator. The section ends at the next level-1 OR
// level-2 heading (review round: a later `# Appendix` with `###` subsections
// was being read as canon).
const scopeNotesLines = read(SCOPE_NOTES_REL).split(/\r?\n/);
const scopeFenced = fenceRanges(SCOPE_NOTES_REL, scopeNotesLines, "an unreadable fence hides its headings from check 23");
const scopeInFence = (i) => scopeFenced.some(([a, b]) => i > a && i < b);
const scopeSectionStart = scopeNotesLines.findIndex((l, i) => !scopeInFence(i) && /^## Scope-limited domains\b/.test(l));
let scopeSectionEnd = scopeNotesLines.length;
if (scopeSectionStart >= 0) for (let i = scopeSectionStart + 1; i < scopeNotesLines.length; i++) if (!scopeInFence(i) && /^#{1,2} /.test(scopeNotesLines[i])) { scopeSectionEnd = i; break; }
const notesScopePaths = scopeSectionStart < 0 ? [] : scopeNotesLines.slice(scopeSectionStart + 1, scopeSectionEnd).filter((l, k) => !scopeInFence(scopeSectionStart + 1 + k) && /^### /.test(l)).map((l) => l.slice(4).trim());
if (scopeSectionStart < 0) fail(`check 23: ${SCOPE_NOTES_REL} has no "## Scope-limited domains" section — the canon report's scope.path points at is gone (F-450 c)`);
// Both comparison loops run only when BOTH readers succeeded: fail() counts
// rather than throws, and a reader failure followed by the sweep printed one
// spurious "drop the subsection" per canon subsection — the opposite of "fix
// the reader, never the sweep" (review round).
if (tableScopePaths.length && scopeSectionStart >= 0) {
for (const p of tableScopePaths) {
if (!notesScopePaths.includes(p))
fail(`check 23: doc-lib's CLI_PIN_FACTS carries a scope entry for "${p}" but ${SCOPE_NOTES_REL} has no "### ${p}" subsection under "Scope-limited domains" — report's scope.path would point at nothing; add the canon subsection (F-450 c)`);
}
for (const p of notesScopePaths) {
if (!tableScopePaths.includes(p))
fail(`check 23: ${SCOPE_NOTES_REL} carries a "### ${p}" subsection under "Scope-limited domains" but doc-lib's CLI_PIN_FACTS has no scope entry for its command — report would never name that limit; add the table entry or drop the subsection (F-450 c)`);
}
}

// The fail() gate sits HERE, after the last check, and must stay last (F-258).
// It used to sit immediately after check 12 — with four checks and the pass line
// below it — so any fail() raised further down printed its message and then let the
Expand All @@ -2258,6 +2303,7 @@ const passLine =
`${sweep9Read} of ${sweep9.length} files swept for portability-duplicate definitions, ` +
`${lcCalls} localeCompare call sites locale-pinned, ` +
`check 22: ${runnableLinesSwept} runnable line(s) in ${runnableDocsSwept} skill/template doc(s) name none of the ${laneFolders.length} default domain folders literally, ` +
`check 23: ${tableScopePaths.length} scope-limit entries in CLI_PIN_FACTS match the ${notesScopePaths.length} canon subsections both ways, ` +
`${scriptRowsNamed} of ${scriptFiles.length} shipped scripts named in the MAINTAINERS Scripts table (rest are shared libs), ` +
`${placeholderFences} placeholder fence(s) in ${pluginRootSkillsSeen.size} skill(s) (setup's pinned link fence; every other skill fence addresses scripts through ${WORKSPACE_LINK}), ` +
`${check13Fences} hand-written doc fence(s) free of bash line continuations and ` +
Expand Down
6 changes: 6 additions & 0 deletions build/check-stale-facts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,12 @@ const FACT_CARRIERS = [
// installed package's auth module; the version stamp beside it forces
// a re-read of that file when the pin moves.
{ re: /\/\/ v(\d+\.\d+\.\d+) package's dist\/core\/auth\/index\.js/, expect: meta.cliVersion, what: "auth-death literal audit version" },
// The per-pin CLI facts table (F-450): what the installed CLI does that
// its catalog does not declare — not-enumerable-bare sublists and scope
// limits — applied only while the catalog carries this exact version.
// The stamp is the table's self-retirement, and this pin forces the hand
// re-verification of every entry at each adoption.
{ re: /export const CLI_PIN_FACTS = Object\.freeze\(\{\s*cliVersion: "(\d+\.\d+\.\d+)"/, expect: meta.cliVersion, what: "per-pin CLI facts table version stamp (CLI_PIN_FACTS)" },
],
},
];
Expand Down
47 changes: 47 additions & 0 deletions build/test-check-doc-drift.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,53 @@ try {
} finally { snap.restore(); }
}

// ── check 23 · the scope-limit canon and the per-pin table agree both ways (F-450 c) ─
{
const NOTES = "plugins/gs-superadmin/skills/setup/references/index-scope-notes.md";
const DOCLIB = "plugins/gs-superadmin/scripts/doc-lib.mjs";
const snap = snapshotFiles([at(NOTES), at(DOCLIB)]);
try {
mutate(NOTES, (s) => s.replace("### journey surveys list", "### journey surveys listing"));
const res = run();
check("check 23a: a table entry whose canon subsection is missing (heading respelled) goes red naming the command and the file",
res.status === 1 && /check 23: .*scope entry for "journey surveys list" but .*index-scope-notes\.md has no "### journey surveys list" subsection/.test(res.stderr) && /"### journey surveys listing" subsection .* has no scope entry/.test(res.stderr), res.stderr);
} finally { snap.restore(); }
try {
mutate(NOTES, (s) => s.replace("## Recovering list-invisible email templates", "### connectors chains\n\nA limit nobody tabled.\n\n## Recovering list-invisible email templates"));
const res = run();
check("check 23b: a canon subsection with no table entry goes red naming the subsection",
res.status === 1 && /check 23: .*"### connectors chains" subsection under "Scope-limited domains" but doc-lib's CLI_PIN_FACTS has no scope entry/.test(res.stderr), res.stderr);
} finally { snap.restore(); }
try {
mutate(NOTES, (s) => s.replace("## Recovering list-invisible email templates", "## Recovering\n\n### journey surveys list\n\n## Recovering list-invisible email templates").replace("### journey surveys list\n\nHardcodes", "### journey surveys list (moved)\n\nHardcodes"));
const res = run();
check("check 23c: a subsection outside the Scope-limited section does not count as the canon",
res.status === 1 && /has no "### journey surveys list" subsection/.test(res.stderr), res.stderr);
} finally { snap.restore(); }
try {
mutate(DOCLIB, (s) => s.replace("export const CLI_PIN_FACTS = Object.freeze({", "export const CLI_PIN_FACTS = Object.freeze(Object.assign({}, {"));
const res = run();
check("check 23d: the table's opener changing shape goes red at the reader, not silently as an empty sweep — and ONLY at the reader (no spurious per-subsection failures)",
res.status === 1 && /check 23: could not read the scope entries out of/.test(res.stderr) && (res.stderr.match(/check 23:/g) ?? []).length === 1, res.stderr);
} finally { snap.restore(); }
try {
mutate(NOTES, (s) => s.replace("### journey surveys list", "```markdown\n### journey fenceprobe list\n```\n\n### journey surveys list"));
const res = run();
check("check 23f: a heading-shaped line inside a fenced example is neither canon nor a terminator (the file's fence grammar)", res.status === 0, res.stderr);
} finally { snap.restore(); }
try {
mutate(NOTES, (s) => s.replace("## Recovering list-invisible email templates", "# Appendix\n\n### journey appendix list\n\n## Recovering list-invisible email templates"));
const res = run();
check("check 23g: a level-1 heading ends the section — a ### under a later # Appendix is not canon", res.status === 0, res.stderr);
} finally { snap.restore(); }
try {
mutate(NOTES, (s) => s.replace("## Scope-limited domains — paging cannot fix these", "## Scope limits — paging cannot fix these"));
const res = run();
check("check 23e: the section heading itself renamed goes red naming the file (the anchor is read, F-439 class)",
res.status === 1 && /check 23: .*index-scope-notes\.md has no "## Scope-limited domains" section/.test(res.stderr), res.stderr);
} finally { snap.restore(); }
}

// ── check 11: guard-residuals anchor drift ─────────────────────────────────
{
const snap = snapshotFiles([at(PLUGIN_README)]);
Expand Down
32 changes: 30 additions & 2 deletions build/test-check-stale-facts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ function runRig(files) {
// …plus the auth-death literal's audit stamp (F-458), same home.
writeFileSync(
join(rig, "plugins", "gs-superadmin", "scripts", "doc-lib.mjs"),
`// strict writers went 29 → 0; POST reads 59 → 0\n// read actions in the v${PIN} catalog audited\n` +
`// the sentence every auth-path throw in the\n// v${PIN} package's dist/core/auth/index.js ends with\n`,
docLibStub(PIN),
);
// Coverage floor (F-101): the checker requires ≥40 tracked .md files with
// some nested — filler carries no version/count/flag tokens.
Expand All @@ -107,6 +106,35 @@ function runRig(files) {
}
}

// The doc-lib carrier stub, parameterized on the version its CLI_PIN_FACTS
// stamp cites — the F-450 case below re-writes it with a stale stamp only.
function docLibStub(pinFactsVersion, pin = PIN) {
return (
`// strict writers went 29 → 0; POST reads 59 → 0\n// read actions in the v${pin} catalog audited\n` +
`// the sentence every auth-path throw in the\n// v${pin} package's dist/core/auth/index.js ends with\n` +
`export const CLI_PIN_FACTS = Object.freeze({\n cliVersion: "${pinFactsVersion}",\n commands: Object.freeze({}),\n});\n`
);
}

// ── 0. F-450: the per-pin CLI facts table's version stamp is a carrier ──────
// A table stamped for another pin is not applied by its readers (self-
// retiring), so the stamp must trip here at adoption, by name, while the
// other doc-lib carriers stay green.
{
// The stamp is a bare version (the const's own literal), so the stale form
// is STALE without its v.
const res = runRig({ "plugins/gs-superadmin/scripts/doc-lib.mjs": docLibStub(STALE.replace(/^v/, "")) });
ok("stale CLI_PIN_FACTS stamp fails", res.status === 1);
ok("stale CLI_PIN_FACTS failure names the table and the pin", res.stderr.includes("CLI_PIN_FACTS") && res.stderr.includes(PIN));
ok("the other doc-lib carriers stay green under a stale pin-facts stamp", !res.stderr.includes("read-verb allowlist") && !res.stderr.includes("auth-death"));
// A table that moved or lost its stamp is an unmatched pattern — itself a
// failure (silently unmatched prose is how the F-280 carriers went stale).
const gone = runRig({ "plugins/gs-superadmin/scripts/doc-lib.mjs": docLibStub(PIN).replace("export const CLI_PIN_FACTS", "const PIN_FACTS_MOVED") });
ok("a missing CLI_PIN_FACTS stamp fails as an unmatched carrier pattern", gone.status === 1 && gone.stderr.includes("CLI_PIN_FACTS") && gone.stderr.includes("no longer pinned"));
const fresh = runRig({ "plugins/gs-superadmin/scripts/doc-lib.mjs": docLibStub(PIN) });
ok("current CLI_PIN_FACTS stamp passes", fresh.status === 0);
}

// ── 1. Stale citation in bus PROSE: still fails ──────────────────────────────
{
const res = runRig({ "dev/FEEDBACK.md": `The pin is ${STALE} and that is a claim.\n` });
Expand Down
Loading