Skip to content

Commit 2cd4c54

Browse files
Trumpclaude
andauthored
feat(docs): generate system-context.mdx's declared counts instead of hand-typing them (#17043)
`check-system-context-census.mjs --fix` regenerates every DECLARED_COUNTS sentence (the headline, the decomposition table, the plugin-sharing share, the ruling quote) straight from a fresh census, reusing the exact pattern/value pair the check already runs -- as a write instead of a comparison. Anchors are unaffected: they still have nothing mechanical to repair (symbol anchors encode no position), so `--fix` refuses loudly on a sentence it can no longer parse rather than writing a partial page. This removes the failure class rather than detecting it one gate at a time: two branches independently (and correctly, for their own tree) bumping the same sentence to the same number text-merge clean with no conflict, and the merged total is neither side's -- which is the failure mode this change responds to. The fix is to stop hand-retyping the number at all. Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6a7910a commit 2cd4c54

3 files changed

Lines changed: 227 additions & 29 deletions

File tree

content/docs/permissions/system-context.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,23 @@ node scripts/isystem-census.mjs # the summary below
310310
node scripts/isystem-census.mjs --json # every site, its kind and its package
311311
```
312312

313+
**Every number below marked ✅, plus the four sentences elsewhere on this page
314+
that restate the same figures (the opening headline, the plugin-sharing share,
315+
the #4707 ruling quote), is *generated*, never hand-maintained.** `pnpm
316+
gen:system-context-census` (`node scripts/check-system-context-census.mjs
317+
--fix`) re-derives the census and rewrites every declared count that has
318+
drifted from it — the exact computation the check below already runs, applied
319+
as a write instead of a comparison. ⛔ **Never hand-retype one of these
320+
digits.** Two branches each independently — and correctly, for their own
321+
tree — bumping the same sentence to the same number is exactly how this page
322+
once went silently wrong: the edits are textually identical, git merges them
323+
clean with no conflict, and the merged total is neither side's number
324+
(#16919). Run the generator instead, against the tree you actually want
325+
counted, and it fails loudly rather than writing a partial page if a sentence
326+
no longer parses. `--fix` still cannot add or drop a **row** — a site that
327+
arrived or vanished is a human's editorial call, same as always — it only
328+
keeps the aggregate counts in lockstep with whatever rows exist.
329+
313330
**⛔ Not a `grep`.** A text scan is where a census *starts* and it cannot be where
314331
one ends: it returns prose inside comments and strings, the three unrelated
315332
metadata fields, and the `isSystemObject` / `isSystemObjectName` /

scripts/check-system-context-census.mjs

Lines changed: 199 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*
88
* node scripts/check-system-context-census.mjs
99
* node scripts/check-system-context-census.mjs --self-test
10-
* node scripts/check-system-context-census.mjs --fix # nothing to repair -- see below
10+
* node scripts/check-system-context-census.mjs --fix # regenerates declared COUNTS; anchors still need a human -- see below
1111
*
1212
* That page declares itself "the authority" for every platform behaviour keyed off
1313
* `ExecutionContext.isSystem`, and says it is "built by census over the whole repo,
@@ -252,7 +252,7 @@ const SELF_TEST_BATTERIES = Object.freeze({
252252
'the same criterion, behaviourally, on one page': 3,
253253
'⛔ and the half that must NOT have moved: the contract still reds': 2,
254254
'absence is loud': 1,
255-
'⛔ --fix rewrites NOTHING, and says so': 2,
255+
'⛔ --fix regenerates declared COUNTS only, never anchors, and says so': 8,
256256
'⭐ THE RULED RED-FIRST PAIR: a symbol rename REDS, a pure line move does NOT': 5,
257257
'⭐ the precision this trades away, pinned so nobody rediscovers it as a bug': 2,
258258
'the refusal has to SHOW its work (both counts, the symbol, the file)': 2,
@@ -1075,6 +1075,73 @@ export const DECLARED_COUNTS = [
10751075
},
10761076
];
10771077

1078+
/**
1079+
* The WRITE half of `DECLARED_COUNTS` (#16919 direction 1).
1080+
*
1081+
* `evaluate()`'s COUNTS check (below) already computes, for every declared
1082+
* sentence, exactly the value it should hold — `declared.value(census, page)`
1083+
* — and only ever uses it to compare. Two branches independently hand-typing
1084+
* the same freshly-computed number into the same sentence text-merge clean and
1085+
* silently wrong on the SUM: main and a sibling PR each bumped the same seven
1086+
* sentences 106 → 107 for two different new reads, and the merged tree held
1087+
* 108. The fix in that shape is never "type the right number" — it is "stop
1088+
* typing it": this function performs the identical `pattern`/`value` lookup
1089+
* COUNTS already runs, and writes the result back instead of only comparing.
1090+
*
1091+
* Pure — a text in, a text out, plus what changed and what could not be
1092+
* derived. The caller decides whether to persist `text` or to refuse on a
1093+
* non-empty `errors`; nothing here touches the filesystem, so `--self-test`
1094+
* can drive it on a fixture exactly as it drives `evaluate()`.
1095+
*
1096+
* ⛔ Deliberately narrow, same rule as `check:generated --fix` elsewhere in
1097+
* this repo (see AGENTS.md, "Touched `packages/spec`?"): only a MISMATCHED
1098+
* count is rewritten, so a page where every count already agrees with the
1099+
* census comes back byte-identical — idempotent by construction, not merely
1100+
* in practice (`--self-test`'s IDEMPOTENCE case proves it on the real page).
1101+
* `UNENFORCED_TEXT_COUNTS` is deliberately NOT in scope: those six rows count
1102+
* whole-corpus TEXT, not the elevation contract, and are unenforced by design
1103+
* (see that export) — regenerating them would silently start enforcing a
1104+
* population this gate has already measured and rejected as noise.
1105+
*
1106+
* @param {{ pageText: string, census: object, declaredCounts?: typeof DECLARED_COUNTS }} args
1107+
* @returns {{ text: string, rewrites: {id: string, from: string, to: string}[], errors: string[] }}
1108+
*/
1109+
export function regenerateDeclaredCounts({ pageText, census, declaredCounts = DECLARED_COUNTS }) {
1110+
let text = pageText;
1111+
const rewrites = [];
1112+
const errors = [];
1113+
for (const declared of declaredCounts) {
1114+
// `d` adds match INDICES (Node >=16) so only the captured digits are
1115+
// spliced out -- never the surrounding sentence, which stays hand-written
1116+
// prose and must survive byte-for-byte on every run that changes nothing.
1117+
const flags = declared.pattern.flags.includes('d') ? declared.pattern.flags : `${declared.pattern.flags}d`;
1118+
const re = new RegExp(declared.pattern.source, flags);
1119+
const match = re.exec(text);
1120+
if (!match) {
1121+
errors.push(
1122+
`[count-pattern-unmatched] the page no longer carries the \`${declared.id}\` sentence ` +
1123+
`(${declared.why}) -- nothing was rewritten for it. Update the pattern together with the wording.`
1124+
);
1125+
continue;
1126+
}
1127+
const actual = declared.value(census, text);
1128+
if (!Number.isInteger(actual) || actual < 0) {
1129+
errors.push(
1130+
`[count-underivable] \`${declared.id}\` could not be derived (${declared.why}) -- the page ` +
1131+
'structure it reads is gone; nothing was rewritten for it.'
1132+
);
1133+
continue;
1134+
}
1135+
const stated = match[1];
1136+
const replacement = String(actual);
1137+
if (stated === replacement) continue;
1138+
const [start, end] = match.indices[1];
1139+
text = text.slice(0, start) + replacement + text.slice(end);
1140+
rewrites.push({ id: declared.id, from: stated, to: replacement });
1141+
}
1142+
return { text, rewrites, errors };
1143+
}
1144+
10781145
/**
10791146
* ⛔ The six numbers this gate deliberately does NOT hold to the census, listed
10801147
* here so that stays a decision instead of an omission.
@@ -1518,25 +1585,28 @@ function readFileAt(root) {
15181585
}
15191586

15201587
/**
1521-
* ⛔ `--fix` has nothing to repair, and says so instead of exiting 0 in silence.
1588+
* ⛔ ANCHORS have nothing to repair, and `--fix` says so instead of exiting 0 in
1589+
* silence about that half.
15221590
*
15231591
* Before #15921 this rewrote line numbers after a pure shift, which was the
15241592
* common repair and a real one. Symbol anchors encode no position, so the shift
15251593
* that repair existed for cannot happen: an edit above a site moves nothing this
1526-
* page writes. Every red is now a human edit -- a rename, an arrived read, a
1527-
* vanished one -- and none of them is mechanically derivable from the tree.
1594+
* page writes. A remaining anchor red is a human edit -- a rename, an arrived
1595+
* read, a vanished one -- and none of them is mechanically derivable from the
1596+
* tree. COUNTS are the other half, and `run()` handles those separately below
1597+
* (#16919) -- this function is deliberately scoped to what stays a human edit.
15281598
*
15291599
* ⭐ The flag stays RECOGNISED on purpose. `gen:system-context-census` and
15301600
* `scripts/regen-artifacts.mjs` both name it, and a flag that silently became a
15311601
* no-op would leave both reading as a working regeneration path. This prints what
15321602
* it did not do, then returns the ordinary verdict.
15331603
*/
1534-
function reportNoFix() {
1604+
function reportNoAnchorFix() {
15351605
process.stdout.write(
1536-
'check-system-context-census --fix: nothing to rewrite — this page carries no line numbers.\n' +
1537-
' Anchors are `path#symbol` (scripts/symbol-anchors.mjs), so an unrelated edit above a site\n' +
1538-
' cannot rot one and there is no mechanical repair to apply. A red below is a human edit:\n' +
1539-
' a renamed symbol, a read that arrived, or a read that vanished.\n'
1606+
'check-system-context-census --fix: anchors have nothing to rewrite — this page carries no line\n' +
1607+
' numbers. Anchors are `path#symbol` (scripts/symbol-anchors.mjs), so an unrelated edit above a\n' +
1608+
' site cannot rot one and there is no mechanical repair to apply there. A remaining anchor red is\n' +
1609+
' a human edit: a renamed symbol, a read that arrived, or a read that vanished (add its row).\n'
15401610
);
15411611
}
15421612

@@ -1549,7 +1619,6 @@ function run({ fix = false } = {}) {
15491619
process.stderr.write(`::error::[unreadable-page] ${PAGE} could not be read -- ${error.message}\n`);
15501620
return 1;
15511621
}
1552-
if (fix) reportNoFix();
15531622

15541623
const census = runCensus({ root: ROOT });
15551624
/* RESOLUTION is the shared resolver's, run once, over this gate's corpus
@@ -1563,14 +1632,49 @@ function run({ fix = false } = {}) {
15631632
return 1;
15641633
}
15651634

1635+
if (fix) {
1636+
// ── COUNTS: the one half of `--fix` that IS mechanical (#16919 direction 1) ──
1637+
//
1638+
// Reuses the exact `DECLARED_COUNTS` computation the COUNTS check below
1639+
// runs, as a write instead of a comparison -- see `regenerateDeclaredCounts`.
1640+
// A count that cannot be derived or located refuses loudly (`errors`) rather
1641+
// than writing a partial page: a generator that writes six of seven numbers
1642+
// and silently skips the seventh is worse than one that writes none.
1643+
const { text, rewrites, errors } = regenerateDeclaredCounts({ pageText, census });
1644+
if (errors.length > 0) {
1645+
for (const error of errors) process.stderr.write(`::error::${error}\n`);
1646+
process.stderr.write(
1647+
`\ncheck-system-context-census --fix: ${errors.length} declared count(s) could not be ` +
1648+
'regenerated -- the page wording changed out from under the pattern that reads it. Fix the ' +
1649+
'wording and the pattern together (by hand), then re-run --fix.\n'
1650+
);
1651+
return 1;
1652+
}
1653+
if (rewrites.length > 0) {
1654+
writeFileSync(join(ROOT, PAGE), text, 'utf8');
1655+
pageText = text;
1656+
process.stdout.write(
1657+
`check-system-context-census --fix: regenerated ${rewrites.length} declared count(s) from the ` +
1658+
`census: ${rewrites.map((r) => `${r.id} ${r.from}->${r.to}`).join(', ')}.\n`
1659+
);
1660+
} else {
1661+
process.stdout.write(
1662+
'check-system-context-census --fix: every declared count already matches the census -- nothing ' +
1663+
'to rewrite there.\n'
1664+
);
1665+
}
1666+
reportNoAnchorFix();
1667+
}
1668+
15661669
const { problems, stats } = evaluate({ pageText, census, readFile, sweep });
15671670
for (const problem of problems) process.stderr.write(`::error::${problem}\n`);
15681671
if (problems.length > 0) {
15691672
process.stderr.write(
15701673
`\ncheck-system-context-census: ${problems.length} problem(s) over ${stats.anchors} anchors ` +
15711674
`and ${stats.sites} census sites.\n` +
1572-
'Re-run the census with `node scripts/isystem-census.mjs --json`. ⛔ There is no mechanical ' +
1573-
'repair: every red here is a human edit.\n' +
1675+
'Re-run the census with `node scripts/isystem-census.mjs --json`. A `[declared-count]` mismatch ' +
1676+
'is mechanical -- `pnpm gen:system-context-census` (`--fix`) rewrites it from the census. ' +
1677+
'⛔ Everything else here is a human edit.\n' +
15741678
`\nThe anchor grammar:\n ${ANCHOR_GRAMMAR}\n`
15751679
);
15761680
return 1;
@@ -2192,14 +2296,17 @@ function selfTest() {
21922296
const noAnchors = run('---\ntitle: x\n---\n\nnothing here.\n');
21932297
t('ABSENCE: a page with no anchors refuses', noAnchors.problems.some((p) => p.startsWith('[no-anchors]')));
21942298

2195-
// ── ⛔ --fix rewrites NOTHING, and says so ──────────────────────────────────
2299+
// ── ⛔ --fix regenerates declared COUNTS only, never anchors, and says so ───
21962300
//
2197-
// ⭐ The failure this pins is the QUIET one. `gen:system-context-census` and
2198-
// `scripts/regen-artifacts.mjs` both invoke `--fix`; a flag that silently became
2199-
// a no-op leaves both of them reading as a working regeneration path, and the
2200-
// first person to hit a red would run it, see exit 0, and conclude the red was
2201-
// spurious.
2202-
battery('⛔ --fix rewrites NOTHING, and says so');
2301+
// ⭐ Two failures this pins, in opposite directions. `gen:system-context-census`
2302+
// and `scripts/regen-artifacts.mjs` both invoke `--fix`; a flag that silently
2303+
// went back to being a no-op (the pre-#16919 shape) leaves both reading as a
2304+
// working regeneration path while every count red still needs a human. And a
2305+
// `--fix` that goes the OTHER way -- resurrecting the retired line-shift
2306+
// repair, or writing the page on a count it could not actually derive -- is
2307+
// exactly the "mechanical repair" this gate explicitly does not offer for
2308+
// anchors. Both directions are pinned on the SAME source read.
2309+
battery('⛔ --fix regenerates declared COUNTS only, never anchors, and says so');
22032310
let ownSourceForFix = null;
22042311
try {
22052312
ownSourceForFix = readFileSync(join(ROOT, 'scripts/check-system-context-census.mjs'), 'utf8');
@@ -2208,16 +2315,83 @@ function selfTest() {
22082315
}
22092316
if (ownSourceForFix !== null) {
22102317
t(
2211-
'--fix: no anchor-rewriting code survives -- nothing in this gate writes the page',
2212-
!/writeFileSync\(\s*join\(ROOT, PAGE\)/.test(ownSourceForFix) && !/\bfixAnchors\b/.test(ownSourceForFix),
2213-
'a rewriter is back in this file'
2318+
'--fix: no anchor line-shift repair survives -- symbol anchors still encode no position to fix',
2319+
!/\bfixAnchors\b/.test(ownSourceForFix),
2320+
'the retired line-shift repair is back in this file'
2321+
);
2322+
t(
2323+
'--fix: the count-regenerating function is exported, not inlined where nothing else can reach it',
2324+
/export function regenerateDeclaredCounts\(/.test(ownSourceForFix)
22142325
);
22152326
t(
2216-
'--fix: the flag is still RECOGNISED and still explains itself, so the wiring cannot go quiet',
2217-
/argv\.includes\('--fix'\)/.test(ownSourceForFix) && /nothing to rewrite/.test(ownSourceForFix)
2327+
'--fix: the write is gated on there being something to write -- a no-op run must not touch the page',
2328+
/if \(rewrites\.length > 0\) \{/.test(ownSourceForFix) &&
2329+
/writeFileSync\(\s*join\(ROOT, PAGE\)/.test(ownSourceForFix)
2330+
);
2331+
t(
2332+
'--fix: the flag is still RECOGNISED and explains BOTH halves, so the wiring cannot go quiet',
2333+
/argv\.includes\('--fix'\)/.test(ownSourceForFix) &&
2334+
/anchors have nothing to rewrite/.test(ownSourceForFix) &&
2335+
/regenerated \$\{rewrites\.length\} declared count/.test(ownSourceForFix)
22182336
);
22192337
}
22202338

2339+
// ⭐ Behavioural, not textual: drive `regenerateDeclaredCounts` itself on the
2340+
// same fixtures the 'counts' battery already uses to pin the CHECK, so the
2341+
// WRITE can never define "correct" any differently than the comparison does.
2342+
const mismatched = fixturePage() + '\nit is a single boolean read at **7\ndistinct sites across 1 packages**.\n';
2343+
const regenerated = regenerateDeclaredCounts({ pageText: mismatched, census: FIXTURE_CENSUS, declaredCounts: FIXTURE_COUNTS });
2344+
t(
2345+
'--fix REGENERATES: a mismatched declared count is rewritten to the census value, and reported',
2346+
regenerated.errors.length === 0 &&
2347+
regenerated.rewrites.length === 1 &&
2348+
regenerated.rewrites[0].id === 'headline-sites' &&
2349+
regenerated.rewrites[0].from === '7' &&
2350+
regenerated.rewrites[0].to === '1',
2351+
JSON.stringify(regenerated.rewrites)
2352+
);
2353+
t(
2354+
'--fix REGENERATES: the rewritten text carries the new digit and NOT the old one, surrounding prose untouched',
2355+
regenerated.text.includes('read at **1\ndistinct sites') && !regenerated.text.includes('**7\ndistinct sites'),
2356+
regenerated.text
2357+
);
2358+
t(
2359+
'⭐ IDEMPOTENCE: a page that already agrees with the census comes back BYTE-IDENTICAL, zero rewrites',
2360+
(() => {
2361+
const already = fixturePage() + '\nit is a single boolean read at **1\ndistinct sites across 1 packages**.\n';
2362+
const first = regenerateDeclaredCounts({ pageText: already, census: FIXTURE_CENSUS, declaredCounts: FIXTURE_COUNTS });
2363+
const second = regenerateDeclaredCounts({ pageText: first.text, census: FIXTURE_CENSUS, declaredCounts: FIXTURE_COUNTS });
2364+
return (
2365+
first.rewrites.length === 0 &&
2366+
first.text === already &&
2367+
second.rewrites.length === 0 &&
2368+
second.text === first.text
2369+
);
2370+
})()
2371+
);
2372+
t(
2373+
'--fix REFUSES rather than writes a partial page: an unmatched or underivable count surfaces as an error',
2374+
(() => {
2375+
const unmatched = regenerateDeclaredCounts({
2376+
pageText: fixturePage(),
2377+
census: FIXTURE_CENSUS,
2378+
declaredCounts: FIXTURE_COUNTS,
2379+
});
2380+
const underivable = regenerateDeclaredCounts({
2381+
pageText: fixturePage() + '\nhelper at `pkg/a.ts#isSystemObjectName`\n',
2382+
census: FIXTURE_CENSUS,
2383+
declaredCounts: [
2384+
{ id: 'x', pattern: /helper at `pkg\/a\.ts#(\w+)`/, value: () => carryOnwardRowCount('gone'), why: 'fixture' },
2385+
],
2386+
});
2387+
return (
2388+
unmatched.errors.some((e) => e.startsWith('[count-pattern-unmatched]')) &&
2389+
unmatched.text === fixturePage() &&
2390+
underivable.errors.some((e) => e.startsWith('[count-underivable]'))
2391+
);
2392+
})()
2393+
);
2394+
22212395
// ── ⭐ THE RULED RED-FIRST PAIR, on a real tree ─────────────────────────────
22222396
//
22232397
// The two headline behaviours the migration was ruled on, proved rather than

scripts/regen-artifacts.mjs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,17 @@ export const REGEN_ARTIFACTS = Object.freeze([
282282
// nothing on its page a regeneration cannot restore.
283283
//
284284
// No `readsDist`/`readsSchemaTree`: the census is an AST walk over `src/`, so a
285-
// merged tree is the whole prerequisite. `gen` cannot launder a POPULATION change
286-
// either — `--fix` re-anchors a pure shift and REFUSES when a site arrived or
287-
// vanished, leaving the page untouched and the gate red (measured: exit 1, zero
288-
// anchors rewritten, `[declared-count] ruling-sites says 109, the census says 110`).
285+
// merged tree is the whole prerequisite. `gen` still cannot launder a POPULATION
286+
// change into a row nobody wrote: a site that arrived or vanished needs a human
287+
// to add or drop its table row, and `--fix` does not touch table rows. #16919
288+
// narrowed what stays a REFUSAL, though — `--fix` now regenerates every
289+
// `DECLARED_COUNTS` sentence (the headline, the decomposition table, the ruling
290+
// quote, …) straight from the census, the same computation the COUNTS check
291+
// already runs, applied as a write instead of a comparison. So a merge that
292+
// silently produced a wrong SUM (the shape #16919 was filed over — two branches
293+
// each correctly bump the same sentence, text-merge clean, and the merged total
294+
// is neither side's number) is repaired by running the generator on the merged
295+
// tree, not by a human re-deriving and re-typing the number by hand.
289296
{
290297
path: 'content/docs/permissions/system-context.mdx',
291298
gen: 'gen:system-context-census',

0 commit comments

Comments
 (0)