Skip to content

Commit db7c9ea

Browse files
committed
chore(role-word): skip the generated protocol-upgrade docs tree, as the gate already skips references
content/docs/protocol-upgrade/ is the upgrade guide's docs pages, written by gen:upgrade-guide from the ADR-0087 registries and gitignored. Its fix site is the registry .ts, which this markdown gate cannot reach, the same reason content/docs/references/ is skipped. CI never generates the pages, so reading them made a local run judge a tree CI never sees: after a docs build the gate reported 99 NEW uses (17.md 56, 18.md 43) while CI was green on the same commit. The self-test's existence pin now accepts an untracked tree that the register of untracked generator output (NOT_DRIVER_MANAGED in regen-artifacts.mjs) declares, and a new case requires every such tree under a ROOT to be skipped. A new program-level battery runs the gate over a fixture page inside each skipped tree (exit 0, nothing read), the same page one level up (NEW use, exit 1), and in a sibling directory sharing the prefix (NEW use, exit 1). Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
1 parent bf6942b commit db7c9ea

1 file changed

Lines changed: 127 additions & 14 deletions

File tree

‎scripts/check-role-word.mjs‎

Lines changed: 127 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848
// merely unimplemented.
4949
//
5050
// Scope: content/docs (hand-written; its `references/` tree is generated from
51-
// spec and excluded BY PATH — the spec source is the fix site there) and
51+
// spec and its `protocol-upgrade/` tree from the ADR-0087 registries, both
52+
// excluded BY PATH — the generator's source is the fix site there) and
5253
// skills/, walked whole, `references/` included: that tree is published catalog
5354
// content, not a generated one. See the SKIP_SUBTREES docblock below for why the
5455
// exclusion is a path and not a directory name. File and directory NAMES count
@@ -63,6 +64,7 @@ import {
6364
import { tmpdir } from 'node:os';
6465
import { join, relative, dirname } from 'node:path';
6566
import { fileURLToPath } from 'node:url';
67+
import { NOT_DRIVER_MANAGED } from './regen-artifacts.mjs';
6668

6769
const ROOTS = ['content/docs', 'skills'];
6870

@@ -122,15 +124,37 @@ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist']);
122124
// justifies the entry above is what decides it rather than weighing against it:
123125
// the reserved word emitted into a page by its generator is a REAL finding whose
124126
// remedy is that generator. Naming the generated tree is how the gate is told
125-
// where to send such a finding, and `content/docs/references/` is the only tree
126-
// under these roots whose fix site this gate cannot otherwise reach.
127+
// where to send such a finding, and exactly two trees under these roots have a
128+
// fix site this gate cannot otherwise reach:
129+
//
130+
// content/docs/references/ generated from spec by
131+
// packages/spec/scripts/build-docs.ts; the
132+
// fix site is the Zod source
133+
// content/docs/protocol-upgrade/ the protocol upgrade guide, one page per
134+
// protocol major plus an index, written by
135+
// `gen:upgrade-guide` from the ADR-0087
136+
// conversion and migration registries; the
137+
// fix site is the registry `.ts`
138+
//
139+
// The second tree is also UNTRACKED: gitignored, written on demand by the
140+
// `@objectstack/docs` build, never committed, so CI's checkout never holds it.
141+
// Read, it made a local run judge a tree CI never sees — a docs build reddened
142+
// this gate with findings whose remedy lives in a `.ts` file this markdown gate
143+
// does not read, while CI stayed green over the same commit. Skipping it is the
144+
// fix-site argument above, applied unchanged, and it makes a local verdict equal
145+
// CI's. Whether the published guide's prose should itself meet ADR-0090 D3 is a
146+
// question for the registry, not one this exclusion settles.
127147
//
128148
// The self-test pins both directions from the WALK's own output rather than as a
129149
// file count. A count has to be re-typed whenever a page is added, and neither
130150
// failure it must catch is a statement a count can make: "the scan reaches no
131-
// published reference page at all", and "the scan descended into the generated
132-
// tree".
133-
const SKIP_SUBTREES = new Set(['content/docs/references']);
151+
// published reference page at all", and "the scan descended into a generated
152+
// tree". An untracked tree leaves that walk pin vacuous on a fresh checkout, so
153+
// the self-test also holds this set against `NOT_DRIVER_MANAGED` in
154+
// scripts/regen-artifacts.mjs — the one register of untracked generator output —
155+
// and runs this gate as a child process over a fixture page inside each skipped
156+
// tree, with the same page outside it as the control.
157+
const SKIP_SUBTREES = new Set(['content/docs/references', 'content/docs/protocol-upgrade']);
134158
const EXTENSIONS = new Set(['.mdx', '.md']);
135159
const BASELINE_PATH = 'scripts/role-word-baseline.json';
136160
const WORD = /\brole(?:s)?\b/gi;
@@ -1258,11 +1282,12 @@ let selfTestReachedVerdict = false;
12581282
// must not red. A battery BELOW its floor means cases stopped running; the
12591283
// remedy is to find what stopped registering.
12601284
const SELF_TEST_BATTERIES = Object.freeze({
1261-
'The scan population: which `references/` the skip means (#15061)': 5,
1285+
'The scan population: which `references/` the skip means (#15061)': 7,
12621286
'The ratchet-remedy authority convention (#8435)': 4,
12631287
'The NEW-use message names the relocation case (#14659)': 7,
12641288
'The green body reports what was READ (#9910)': 5,
12651289
'A missing ROOT is REFUSED, per root (#9932)': 8,
1290+
'Every skipped subtree, at the PROGRAM level': 7,
12661291
'The dispatch-gates declaration (#9964\'s pattern)': 4,
12671292
'The vendor-wire fence exemption (#10533)': 6,
12681293
'Every way the MARKING could stop bounding the exemption': 15,
@@ -1275,7 +1300,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
12751300

12761301
// DELETING an entry silences that battery's floor exactly as effectively as
12771302
// zeroing it, so the roster's own size is pinned too.
1278-
const SELF_TEST_BATTERY_FLOOR = 13;
1303+
const SELF_TEST_BATTERY_FLOOR = 14;
12791304

12801305
// The key an assertion is filed under when no battery is open. It is not a
12811306
// declared battery, so it reds by the same set difference rather than silently
@@ -1345,12 +1370,38 @@ function selfTest() {
13451370
+ 'the three surviving entries meet at every depth and under every root, and `references` '
13461371
+ 'never did. A fourth entry has to be stated rather than appended',
13471372
[...SKIP_DIRS].every((d) => INSTALLED_OR_GENERATED.has(d)));
1348-
expect('#15061 — every SKIP_SUBTREES entry lies under a configured ROOT and EXISTS: a path '
1349-
+ 'excluding a tree no root reaches, or one that has since moved, is dead configuration that '
1350-
+ 'reads as coverage',
1373+
// The UNTRACKED half of SKIP_SUBTREES, read from the register rather than
1374+
// re-spelled here: `NOT_DRIVER_MANAGED`'s `untracked: true` entries, which
1375+
// `check:merge-driver` holds against git and against their generator's name,
1376+
// narrowed to the ones under a ROOT. Such a tree exists only after its
1377+
// generator ran, so existence cannot be what proves the exclusion live; the
1378+
// register entry naming its generator is.
1379+
const underRoot = (p) => ROOTS.some((r) => p === r || p.startsWith(`${r}/`));
1380+
const untrackedUnderRoots = NOT_DRIVER_MANAGED
1381+
.filter((e) => e.untracked && e.path.endsWith('/**'))
1382+
.map((e) => e.path.slice(0, -'/**'.length))
1383+
.filter(underRoot);
1384+
const skippedReached = walkedRel.filter(
1385+
(f) => [...SKIP_SUBTREES].some((p) => f.startsWith(`${p}/`)),
1386+
);
1387+
expect('the walk reaches NO page under ANY SKIP_SUBTREES entry, content/docs/protocol-upgrade/ '
1388+
+ 'included whenever a local docs build has written it. On a fresh checkout that tree is '
1389+
+ 'absent and this case cannot fail for it, which is why the register case below and the '
1390+
+ 'program-level battery exist',
1391+
skippedReached.length === 0);
1392+
expect('#15061 — every SKIP_SUBTREES entry lies under a configured ROOT and EXISTS, or is an '
1393+
+ 'UNTRACKED generated tree the register declares (written on demand, so absent from a fresh '
1394+
+ 'checkout): a path excluding a tree no root reaches, or one that has since moved, is dead '
1395+
+ 'configuration that reads as coverage',
13511396
[...SKIP_SUBTREES].every(
1352-
(p) => ROOTS.some((r) => p === r || p.startsWith(`${r}/`)) && existsSync(p),
1397+
(p) => underRoot(p) && (existsSync(p) || untrackedUnderRoots.includes(p)),
13531398
));
1399+
expect('every untracked generated tree the register places under a ROOT is in SKIP_SUBTREES, '
1400+
+ 'and the register names at least one, so a renamed flag cannot pass this by matching '
1401+
+ 'nothing. CI never generates such a tree, so reading it makes a local run judge pages CI '
1402+
+ 'never sees, with findings whose fix site is their generator. Dropping the '
1403+
+ 'content/docs/protocol-upgrade entry reds HERE on every checkout, pages on disk or not',
1404+
untrackedUnderRoots.length > 0 && untrackedUnderRoots.every((p) => SKIP_SUBTREES.has(p)));
13541405

13551406
// ── The ratchet-remedy authority convention (#8435) ────────────────────────
13561407
//
@@ -1625,6 +1676,65 @@ function selfTest() {
16251676
rmSync(sandbox, { recursive: true, force: true });
16261677
}
16271678

1679+
// ── Every skipped subtree, at the PROGRAM level ────────────────────────────
1680+
//
1681+
// The scan-population cases prove the SET says the right thing; these prove
1682+
// the program honours it, by building a tree and running this file in it as a
1683+
// child, the discipline of the missing-ROOT legs above. It is also the only
1684+
// evidence an untracked tree leaves on a fresh checkout, where the walk pin
1685+
// has none of its pages to see.
1686+
//
1687+
// The trees are built from SKIP_SUBTREES and the register's untracked trees
1688+
// together, never re-spelled: an entry dropped from the set is still built
1689+
// here, so its page is COUNTED and the skip leg reds by exit status.
1690+
//
1691+
// Per tree, three legs that differ only in where the page sits:
1692+
// inside the page inside the skipped tree: exit 0 with zero files READ,
1693+
// so the green came from the walk never opening it, not from an
1694+
// exemption or a baseline (no baseline exists in these trees);
1695+
// outside the same page one level up, in the hand-written tree: a NEW use,
1696+
// exit 1, because a gate that skipped everything would pass `inside`;
1697+
// sibling the same page in a sibling directory whose name merely STARTS
1698+
// with the skipped one's: a NEW use too, because the exclusion is
1699+
// a path and never a string prefix.
1700+
battery('Every skipped subtree, at the PROGRAM level');
1701+
const SKIP_FIXTURE_PAGE = 'fixture-page.md';
1702+
const SKIP_FIXTURE = '# Approvals\n\nEvery approver holds a role, said the prose.\n';
1703+
expect('the skipped-subtree fixture CARRIES the reserved word, so a green inside a skipped tree '
1704+
+ 'is the skip working and not an empty page',
1705+
countMatches(SKIP_FIXTURE) === 1);
1706+
const skipSandbox = mkdtempSync(join(tmpdir(), 'check-role-word-skipped-'));
1707+
try {
1708+
const nothingRead = scanClause(ROOTS.map((root) => ({ root, files: 0 })));
1709+
const skipTrees = [...new Set([...SKIP_SUBTREES, ...untrackedUnderRoots])];
1710+
skipTrees.forEach((tree, i) => {
1711+
const buildSkipTree = (leg, pageDir) => {
1712+
const dir = join(skipSandbox, `${i}-${leg}`);
1713+
for (const r of ROOTS) mkdirSync(join(dir, r), { recursive: true });
1714+
mkdirSync(join(dir, pageDir), { recursive: true });
1715+
writeFileSync(join(dir, pageDir, SKIP_FIXTURE_PAGE), SKIP_FIXTURE);
1716+
return dir;
1717+
};
1718+
const inside = runIn(buildSkipTree('inside', tree));
1719+
expect(`${tree}/ — a page inside it is SKIPPED: exit 0 with nothing read, where the same `
1720+
+ 'page counted would be a NEW use and exit 1',
1721+
inside.status === 0 && inside.out.includes(nothingRead));
1722+
const outsidePage = `${dirname(tree)}/${SKIP_FIXTURE_PAGE}`;
1723+
const outside = runIn(buildSkipTree('outside', dirname(tree)));
1724+
expect(`${tree}/ — the same page one level up, outside it, still FIRES as a NEW use `
1725+
+ '(exit 1), so the green above came from this exclusion and not from a gate that skips '
1726+
+ 'everything',
1727+
outside.status === 1 && outside.out.includes(`${outsidePage}: NEW use of the reserved word`));
1728+
const siblingPage = `${tree}-draft/${SKIP_FIXTURE_PAGE}`;
1729+
const sibling = runIn(buildSkipTree('sibling', `${tree}-draft`));
1730+
expect(`${tree}/ — the same page in a sibling directory whose name only STARTS with it still `
1731+
+ 'FIRES as a NEW use (exit 1): the exclusion is a path, never a string prefix',
1732+
sibling.status === 1 && sibling.out.includes(`${siblingPage}: NEW use of the reserved word`));
1733+
});
1734+
} finally {
1735+
rmSync(skipSandbox, { recursive: true, force: true });
1736+
}
1737+
16281738
// ── The dispatch-gates declaration (#9964's pattern) ──────────────────────
16291739
//
16301740
// Enforcement cannot hold any of these: the declaration is read by another
@@ -2260,9 +2370,12 @@ function selfTest() {
22602370
console.log(
22612371
`OK self-test: the walk reaches ${walkedRel.length} markdown file(s) across the roots, `
22622372
+ `${publishedRefs.length} of them published reference pages under skills/, and `
2263-
+ `${generatedRefs.length} under the generated content/docs/references/ tree — both `
2373+
+ `${skippedReached.length} under the generated subtrees (${[...SKIP_SUBTREES].join(', ')}) — both `
22642374
+ 'directions pinned from the WALK, never from a typed count, so a skip that empties the '
2265-
+ 'published half and one that swallows the generated half each name themselves. '
2375+
+ 'published half and one that swallows the generated half each name themselves. Every '
2376+
+ 'skipped subtree is also held against the register of untracked generator output, and '
2377+
+ 'driven through a real child process: a page inside it is never read, while the same page '
2378+
+ 'outside it, or in a sibling directory sharing its prefix, still counts. '
22662379
+ 'The NEW-use remedy marks baseline expansion as maintainer-only, the predicate '
22672380
+ 'rejects an unmarked offer, the ratchet-DOWN remedy stays the author\'s own, and that '
22682381
+ 'same remedy NAMES the pure-relocation case, quotes a ratchet-DOWN row the gate really '

0 commit comments

Comments
 (0)