fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064) - #14363

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing
Sep 2, 2026
Merged

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064)#14363
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14064

The card asked one question first, and triage made it binding: is this page the only MIXED file routed to merge=os-regen, or is there a batch? The census is below, it is decisive, and it changed which remedy is correct.

1. The census — every path routed to merge=os-regen, classified

18 patterns, 337 tracked files. (The count is independently confirmed by the driver's own self-test: entryForPath agrees with git check-attr on all 337 routed file(s).)

Classification method. Not "does it look generated" and not the presence of a banner — both are readable off the file and neither answers the question the driver poses. The question is: after the driver keeps one side whole and drops the other, can the generator restore what was dropped? So each row is classified by what its gen: script writes:

  • GENERATED-ONLY — the generator renders the file's entire content in memory and writes it (writeFileSync(path, rendered), the createSinkemit()/flush() pair, or writeShards()), reading the file back only to compare for staleness. Nothing on disk survives into the output, so every byte is re-derivable and a whole-side deferral loses nothing.
  • MIXED — the generator reads the target file as input and rewrites only part of it. The rest is carried over from disk, is derived from nothing, and cannot be restored.
Routed patternFilesClassEvidence
packages/spec/spec-changes.json1GENERATED-ONLYbuild-spec-changes.ts:134writeFileSync(SNAPSHOT, next)
packages/spec/liveness/state-counts.md1GENERATED-ONLYliveness/build-state-counts.mts:135 writes rendered whole; banner emitted by the generator
packages/spec/authorable-surface/**14GENERATED-ONLYbuild-schemas.ts, JSON shards written whole
packages/spec/authorable-surface.base.json1GENERATED-ONLYbuild-schemas.ts:2172 serialize-and-write
packages/spec/authorable-defaults/**13GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/json-schema.manifest/**14GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/api-surface/**17GENERATED-ONLYbuild-api-surface.ts, JSON shards
packages/spec/api-surface-signatures.json1GENERATED-ONLYbuild-api-surface.ts:164writeFileSync(SIG_SNAPSHOT, sigStr)
packages/spec/src/meta-spelling/meta-url-data.generated.ts1GENERATED-ONLYbuild-meta-url-spelling.ts:160 writes next; the read at :139 is a staleness compare only
packages/spec/export-origins/**17GENERATED-ONLYbuild-export-origins.ts:372writeShards(ORIGINS_DIR, shardTexts) — writes whole, removes stale
packages/spec/declaration-map/**14GENERATED-ONLYbuild-declaration-map.ts, JSON shards
docs/protocol-upgrade-guide.md1GENERATED-ONLYbuild-upgrade-guide.ts:123writeFileSync(GUIDE, next); the read at :113 is the --check compare
docs/audits/2026-07-...strictness-ledger.counts.md1GENERATED-ONLYbuild-strictness-ledger-counts.mts:43writeFileSync(countsPath, rendered)
content/docs/references/**229GENERATED-ONLYbuild-docs.tsemit() per page + manageDir(DOCS_ROOT) — the sink owns the tree and deletes strays. The 14 category index.mdx pages carry no banner but are built line-by-line in the generator (mdx += ..., :875-:895)
content/docs/permissions/system-context.mdx1MIXEDgen:system-context-census is check-system-context-census.mjs --fix, which reads the page and does writeFileSync(join(ROOT, PAGE), text) where text is the on-disk page with anchor line numbers substituted. It is the only routed generator that takes its own target as input.
skills/*/references/_index.md9GENERATED-ONLYbuild-skill-references.ts:379emit(..., generateIndex(...))
skills/objectstack-ui/contracts/react-blocks.contract.json1GENERATED-ONLYbuild-react-blocks-contract.ts:136emit(OUT_JSON, ...)
skills/objectstack-ui/references/react-blocks.md1GENERATED-ONLYbuild-react-blocks-contract.ts:170emit(OUT_MD, ...)

Result: 336 GENERATED-ONLY, 1 MIXED. There is no batch. The card's own "whether this page is the only one in that shape is unmeasured" is now measured, and the answer is yes.

2. Two further measurements, both of which changed the answer

2a. The generated half is NOT a separable table, so splitting the page is not available

Triage recorded a third shape it explicitly did not advocate: split the generated anchor table into its own file and route only that. Measured against the page: the 145 anchors sit on 83 distinct lines spread across 10 sections, from page line 49 to 238, with 14 gaps — 6 in "Which isSystem this page is about", 4 in "How the flag is set", 19/16/21/25/33/4 in the six table subsections, 13 in "What isSystem does not do", 4 in "Known rough edges". They are citations woven through hand-written prose, not a block. There is no anchor table to move out; "splitting" would mean extracting 83 individual lines from ten prose sections. Shape C is off the table on evidence, not on cost.

2b. 24 of the last 25 main commits to this page change nothing but anchor numbers

This is the reading that decides between the remaining two shapes, and it did not exist when triage set its rule. Classifying each of the last 25 main commits to the page by comparing the parent and the commit after blanking every anchor's line number (using the repo's one anchor reader, so CONTINUATION and RANGE_END anchors are counted — a naive path.ts:NNNN regex reads 24 of the 25 as prose-changing and is wrong):

anchors-only: 24 prose-touching: 1 total: 25
PROSE 73c846687 fix(plugin-security): resolve the org-admin permission set per organization

The single prose commit is the 18-line block from the near-miss the card is about. So:

3. The shape chosen: B, at the driver, and why not A

Shape A (move the row to NOT_DRIVER_MANAGED) buys the prose back by handing a page that conflicts roughly hourly to a human who cannot resolve it correctly — the merged tree's correct anchors are on neither side (#13625: five conflicted anchors resolve to 4408/5771/6019/6382/6575 against branch 4407/5770/… and main 4284/5647/…). It gives back 24-in-25 of a measured win to address a 1-in-25 case. Triage's "only one page ⇒ A is cheap and sufficient" was the right rule for the data it had; the anchors-only/prose split above is new data, and it inverts the cost.

Shape B, implemented at the driver rather than at the census gate. A prose-integrity check in check-system-context-census.mjs cannot work: the gate has no reference for what prose should be there, because prose is derived from nothing. The driver, by contrast, is handed %O %A %B and is the only actor in the system that can see both sides. So:

  • scripts/regen-artifacts.mjs — the row declares mixed: 'line-anchors'. New field, documented in the table's contract header: the row is not generated whole, and the value names the equivalence under which deferring is lossless. Keyed by name so the table keeps its "constants and pure functions" shape.
  • scripts/doc-line-anchors.mjs — new export blankAnchorLineNumbers(rawText), plus start/end offsets on each anchor so a caller can rewrite one in place. It lives here because this module is already declared "the ONE reader" of these anchors; a second three-line regex would be a second definition of "anchor" that drifts from the gate's.
  • scripts/git-merge-regen.mjs — before deferring a mixed row, deferralIsLossless() asks whether the incoming side changed anything but the generated half. Two shapes are provably lossless: theirs differs from the ancestor only in anchors (the 24-in-25 case), or theirs and ours agree once anchors are blanked. Everything else is unsafe, including an unreadable side or an unrecognised comparator name — the default has to be unsafe, because the failure this closes is invisible.
    • When unsafe, the driver text-merges in place (git merge-file into %A). Clean merge ⇒ both sides' prose survives, the path is still marked pending, and the mandatory regeneration re-derives the anchors. That is exactly the union a human produced by hand on the merge that found this defect. Conflict ⇒ markers, exit 1, and a message telling the resolver to take either side's anchors and re-run the generator.

The 24-in-25 case is untouched: it defers to OURS exactly as before.

4. The rationale that was falsified, corrected in both places

.gitattributes argued that deferring is safe becausecheck-system-context-census.mjs re-derives the census from the tree on every PR. That sentence is true, and its domain is half the risk surface: the gate re-derives the census and the anchors, and re-derives no prose. Both .gitattributes and the row's comment in scripts/regen-artifacts.mjs now state the correction, carry the ten-gates-green measurement, and record that the routing itself remains correct.

5. This driver has form — three times the same shape

Triage asked for this paragraph, and the census is what earns it.

Three separate cards, one shape: the driver makes a silent choice, and the gates are green on the wrong side of it. The common root is not any one of these bugs but the assumption that "regenerable" is a property of a path rather than of a difference. mixed is the first place that assumption is written down as something checkable rather than assumed, and the driver now fails closed against it.

6. Regression cover

check:merge-driver gains two legs:

Ablation (the guard removed, mutation and restoration both proved on disk):

HEAD blob: a0074c6d6eba32f6094711b82b8c2f1be798f8f0
marker before: 1 marker after: 0
mutated blob: d86589870acdae69d7d75ad186e8cc5763b8b794 (differs, so the mutation landed)
ABLATED_EXIT=1
✗ self-test (mixed): incoming PROSE was dropped with no conflict and no marker.
✗ merge driver wiring is inconsistent — see above.
restore: on-disk blob a0074c6d6eba32f6094711b82b8c2f1be798f8f0 == HEAD blob, `git diff HEAD` empty

The new test reproduces #14064 on the unfixed driver and goes green on the fixed one. No dist/ is involved — these are plain .mjs scripts loaded from source, so there is no build leg to stale.

7. Gates run — at 081b60c8c, the head of this branch

Derived with node scripts/pm/dispatch-gates.mjs --commandsafter merging origin/main (the first derivation warned STALE, naming scripts/pm/dispatch-gates.mjs itself as changed in the range). All exit codes captured by redirect-then-capture, never through a pipe.

CommandExit
node scripts/check-ci-filter-parity.mjs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjs3 — PREREQUISITE NOT MET, i.e. NOT MEASURED (its own words: "Nothing was measured: this gate exited before parsing a single summary line"). Not a finding.
node scripts/pm/bare-root-worklist.mjs --self-test0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:cross-package-test-inputs0
pnpm check:entry-guard0
pnpm check:merge-driver0
pnpm check:parse-guard0
pnpm check:pnpm-filter-targets0
pnpm check:ratchet-remedy-authority0
pnpm check:watch-hint-literal0
pnpm check:pm-dispatch-gatesNOT RUN — declared narrowing, see below

Beyond the derivation, because scripts/doc-line-anchors.mjs is imported by the census gate and the derivation does not map that edge:

CommandExitVerdict line
pnpm check:system-context-census0OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read — identical to the card's green control
pnpm check:doc-anchors0self-test + live, both clean
pnpm check:nul-bytes0plus a direct control-byte scan of all four changed files: clean
pnpm check:declared-population-live0156 of 200 famil(ies) declare a path population

Reconciliationnode scripts/pm/dispatch-gates.mjs --ran ran.list (mode present on this base):

Run reconciliation — 16 derived, 15 run, 0 NOT-MEASURED, 1 UNRUN.
UNRUN (1): pnpm check:pm-dispatch-gates [absent from the run record]

The declared narrowing.check:pm-dispatch-gates exceeded this container's ~10-minute foreground ceiling twice (SIGTERM, exit 143, at 10m00s and 9m20s, producing zero bytes of output both times). It entered the derived list by change KIND (scripts/**), not by path: this PR does not touch scripts/pm/dispatch-gates.mjs, so nothing here can change its behaviour. lint.yml runs it on this PR unconditionally, which is where its result will come from.

8. Notes


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064) - #14363

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing
Sep 2, 2026
Merged

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064)#14363
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14064

The card asked one question first, and triage made it binding: is this page the only MIXED file routed to merge=os-regen, or is there a batch? The census is below, it is decisive, and it changed which remedy is correct.

1. The census — every path routed to merge=os-regen, classified

18 patterns, 337 tracked files. (The count is independently confirmed by the driver's own self-test: entryForPath agrees with git check-attr on all 337 routed file(s).)

Classification method. Not "does it look generated" and not the presence of a banner — both are readable off the file and neither answers the question the driver poses. The question is: after the driver keeps one side whole and drops the other, can the generator restore what was dropped? So each row is classified by what its gen: script writes:

  • GENERATED-ONLY — the generator renders the file's entire content in memory and writes it (writeFileSync(path, rendered), the createSinkemit()/flush() pair, or writeShards()), reading the file back only to compare for staleness. Nothing on disk survives into the output, so every byte is re-derivable and a whole-side deferral loses nothing.
  • MIXED — the generator reads the target file as input and rewrites only part of it. The rest is carried over from disk, is derived from nothing, and cannot be restored.
Routed patternFilesClassEvidence
packages/spec/spec-changes.json1GENERATED-ONLYbuild-spec-changes.ts:134writeFileSync(SNAPSHOT, next)
packages/spec/liveness/state-counts.md1GENERATED-ONLYliveness/build-state-counts.mts:135 writes rendered whole; banner emitted by the generator
packages/spec/authorable-surface/**14GENERATED-ONLYbuild-schemas.ts, JSON shards written whole
packages/spec/authorable-surface.base.json1GENERATED-ONLYbuild-schemas.ts:2172 serialize-and-write
packages/spec/authorable-defaults/**13GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/json-schema.manifest/**14GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/api-surface/**17GENERATED-ONLYbuild-api-surface.ts, JSON shards
packages/spec/api-surface-signatures.json1GENERATED-ONLYbuild-api-surface.ts:164writeFileSync(SIG_SNAPSHOT, sigStr)
packages/spec/src/meta-spelling/meta-url-data.generated.ts1GENERATED-ONLYbuild-meta-url-spelling.ts:160 writes next; the read at :139 is a staleness compare only
packages/spec/export-origins/**17GENERATED-ONLYbuild-export-origins.ts:372writeShards(ORIGINS_DIR, shardTexts) — writes whole, removes stale
packages/spec/declaration-map/**14GENERATED-ONLYbuild-declaration-map.ts, JSON shards
docs/protocol-upgrade-guide.md1GENERATED-ONLYbuild-upgrade-guide.ts:123writeFileSync(GUIDE, next); the read at :113 is the --check compare
docs/audits/2026-07-...strictness-ledger.counts.md1GENERATED-ONLYbuild-strictness-ledger-counts.mts:43writeFileSync(countsPath, rendered)
content/docs/references/**229GENERATED-ONLYbuild-docs.tsemit() per page + manageDir(DOCS_ROOT) — the sink owns the tree and deletes strays. The 14 category index.mdx pages carry no banner but are built line-by-line in the generator (mdx += ..., :875-:895)
content/docs/permissions/system-context.mdx1MIXEDgen:system-context-census is check-system-context-census.mjs --fix, which reads the page and does writeFileSync(join(ROOT, PAGE), text) where text is the on-disk page with anchor line numbers substituted. It is the only routed generator that takes its own target as input.
skills/*/references/_index.md9GENERATED-ONLYbuild-skill-references.ts:379emit(..., generateIndex(...))
skills/objectstack-ui/contracts/react-blocks.contract.json1GENERATED-ONLYbuild-react-blocks-contract.ts:136emit(OUT_JSON, ...)
skills/objectstack-ui/references/react-blocks.md1GENERATED-ONLYbuild-react-blocks-contract.ts:170emit(OUT_MD, ...)

Result: 336 GENERATED-ONLY, 1 MIXED. There is no batch. The card's own "whether this page is the only one in that shape is unmeasured" is now measured, and the answer is yes.

2. Two further measurements, both of which changed the answer

2a. The generated half is NOT a separable table, so splitting the page is not available

Triage recorded a third shape it explicitly did not advocate: split the generated anchor table into its own file and route only that. Measured against the page: the 145 anchors sit on 83 distinct lines spread across 10 sections, from page line 49 to 238, with 14 gaps — 6 in "Which isSystem this page is about", 4 in "How the flag is set", 19/16/21/25/33/4 in the six table subsections, 13 in "What isSystem does not do", 4 in "Known rough edges". They are citations woven through hand-written prose, not a block. There is no anchor table to move out; "splitting" would mean extracting 83 individual lines from ten prose sections. Shape C is off the table on evidence, not on cost.

2b. 24 of the last 25 main commits to this page change nothing but anchor numbers

This is the reading that decides between the remaining two shapes, and it did not exist when triage set its rule. Classifying each of the last 25 main commits to the page by comparing the parent and the commit after blanking every anchor's line number (using the repo's one anchor reader, so CONTINUATION and RANGE_END anchors are counted — a naive path.ts:NNNN regex reads 24 of the 25 as prose-changing and is wrong):

anchors-only: 24 prose-touching: 1 total: 25
PROSE 73c846687 fix(plugin-security): resolve the org-admin permission set per organization

The single prose commit is the 18-line block from the near-miss the card is about. So:

3. The shape chosen: B, at the driver, and why not A

Shape A (move the row to NOT_DRIVER_MANAGED) buys the prose back by handing a page that conflicts roughly hourly to a human who cannot resolve it correctly — the merged tree's correct anchors are on neither side (#13625: five conflicted anchors resolve to 4408/5771/6019/6382/6575 against branch 4407/5770/… and main 4284/5647/…). It gives back 24-in-25 of a measured win to address a 1-in-25 case. Triage's "only one page ⇒ A is cheap and sufficient" was the right rule for the data it had; the anchors-only/prose split above is new data, and it inverts the cost.

Shape B, implemented at the driver rather than at the census gate. A prose-integrity check in check-system-context-census.mjs cannot work: the gate has no reference for what prose should be there, because prose is derived from nothing. The driver, by contrast, is handed %O %A %B and is the only actor in the system that can see both sides. So:

  • scripts/regen-artifacts.mjs — the row declares mixed: 'line-anchors'. New field, documented in the table's contract header: the row is not generated whole, and the value names the equivalence under which deferring is lossless. Keyed by name so the table keeps its "constants and pure functions" shape.
  • scripts/doc-line-anchors.mjs — new export blankAnchorLineNumbers(rawText), plus start/end offsets on each anchor so a caller can rewrite one in place. It lives here because this module is already declared "the ONE reader" of these anchors; a second three-line regex would be a second definition of "anchor" that drifts from the gate's.
  • scripts/git-merge-regen.mjs — before deferring a mixed row, deferralIsLossless() asks whether the incoming side changed anything but the generated half. Two shapes are provably lossless: theirs differs from the ancestor only in anchors (the 24-in-25 case), or theirs and ours agree once anchors are blanked. Everything else is unsafe, including an unreadable side or an unrecognised comparator name — the default has to be unsafe, because the failure this closes is invisible.
    • When unsafe, the driver text-merges in place (git merge-file into %A). Clean merge ⇒ both sides' prose survives, the path is still marked pending, and the mandatory regeneration re-derives the anchors. That is exactly the union a human produced by hand on the merge that found this defect. Conflict ⇒ markers, exit 1, and a message telling the resolver to take either side's anchors and re-run the generator.

The 24-in-25 case is untouched: it defers to OURS exactly as before.

4. The rationale that was falsified, corrected in both places

.gitattributes argued that deferring is safe becausecheck-system-context-census.mjs re-derives the census from the tree on every PR. That sentence is true, and its domain is half the risk surface: the gate re-derives the census and the anchors, and re-derives no prose. Both .gitattributes and the row's comment in scripts/regen-artifacts.mjs now state the correction, carry the ten-gates-green measurement, and record that the routing itself remains correct.

5. This driver has form — three times the same shape

Triage asked for this paragraph, and the census is what earns it.

Three separate cards, one shape: the driver makes a silent choice, and the gates are green on the wrong side of it. The common root is not any one of these bugs but the assumption that "regenerable" is a property of a path rather than of a difference. mixed is the first place that assumption is written down as something checkable rather than assumed, and the driver now fails closed against it.

6. Regression cover

check:merge-driver gains two legs:

Ablation (the guard removed, mutation and restoration both proved on disk):

HEAD blob: a0074c6d6eba32f6094711b82b8c2f1be798f8f0
marker before: 1 marker after: 0
mutated blob: d86589870acdae69d7d75ad186e8cc5763b8b794 (differs, so the mutation landed)
ABLATED_EXIT=1
✗ self-test (mixed): incoming PROSE was dropped with no conflict and no marker.
✗ merge driver wiring is inconsistent — see above.
restore: on-disk blob a0074c6d6eba32f6094711b82b8c2f1be798f8f0 == HEAD blob, `git diff HEAD` empty

The new test reproduces #14064 on the unfixed driver and goes green on the fixed one. No dist/ is involved — these are plain .mjs scripts loaded from source, so there is no build leg to stale.

7. Gates run — at 081b60c8c, the head of this branch

Derived with node scripts/pm/dispatch-gates.mjs --commandsafter merging origin/main (the first derivation warned STALE, naming scripts/pm/dispatch-gates.mjs itself as changed in the range). All exit codes captured by redirect-then-capture, never through a pipe.

CommandExit
node scripts/check-ci-filter-parity.mjs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjs3 — PREREQUISITE NOT MET, i.e. NOT MEASURED (its own words: "Nothing was measured: this gate exited before parsing a single summary line"). Not a finding.
node scripts/pm/bare-root-worklist.mjs --self-test0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:cross-package-test-inputs0
pnpm check:entry-guard0
pnpm check:merge-driver0
pnpm check:parse-guard0
pnpm check:pnpm-filter-targets0
pnpm check:ratchet-remedy-authority0
pnpm check:watch-hint-literal0
pnpm check:pm-dispatch-gatesNOT RUN — declared narrowing, see below

Beyond the derivation, because scripts/doc-line-anchors.mjs is imported by the census gate and the derivation does not map that edge:

CommandExitVerdict line
pnpm check:system-context-census0OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read — identical to the card's green control
pnpm check:doc-anchors0self-test + live, both clean
pnpm check:nul-bytes0plus a direct control-byte scan of all four changed files: clean
pnpm check:declared-population-live0156 of 200 famil(ies) declare a path population

Reconciliationnode scripts/pm/dispatch-gates.mjs --ran ran.list (mode present on this base):

Run reconciliation — 16 derived, 15 run, 0 NOT-MEASURED, 1 UNRUN.
UNRUN (1): pnpm check:pm-dispatch-gates [absent from the run record]

The declared narrowing.check:pm-dispatch-gates exceeded this container's ~10-minute foreground ceiling twice (SIGTERM, exit 143, at 10m00s and 9m20s, producing zero bytes of output both times). It entered the derived list by change KIND (scripts/**), not by path: this PR does not touch scripts/pm/dispatch-gates.mjs, so nothing here can change its behaviour. lint.yml runs it on this PR unconditionally, which is where its result will come from.

8. Notes


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064) - #14363

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing
Sep 2, 2026
Merged

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064)#14363
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14064

The card asked one question first, and triage made it binding: is this page the only MIXED file routed to merge=os-regen, or is there a batch? The census is below, it is decisive, and it changed which remedy is correct.

1. The census — every path routed to merge=os-regen, classified

18 patterns, 337 tracked files. (The count is independently confirmed by the driver's own self-test: entryForPath agrees with git check-attr on all 337 routed file(s).)

Classification method. Not "does it look generated" and not the presence of a banner — both are readable off the file and neither answers the question the driver poses. The question is: after the driver keeps one side whole and drops the other, can the generator restore what was dropped? So each row is classified by what its gen: script writes:

  • GENERATED-ONLY — the generator renders the file's entire content in memory and writes it (writeFileSync(path, rendered), the createSinkemit()/flush() pair, or writeShards()), reading the file back only to compare for staleness. Nothing on disk survives into the output, so every byte is re-derivable and a whole-side deferral loses nothing.
  • MIXED — the generator reads the target file as input and rewrites only part of it. The rest is carried over from disk, is derived from nothing, and cannot be restored.
Routed patternFilesClassEvidence
packages/spec/spec-changes.json1GENERATED-ONLYbuild-spec-changes.ts:134writeFileSync(SNAPSHOT, next)
packages/spec/liveness/state-counts.md1GENERATED-ONLYliveness/build-state-counts.mts:135 writes rendered whole; banner emitted by the generator
packages/spec/authorable-surface/**14GENERATED-ONLYbuild-schemas.ts, JSON shards written whole
packages/spec/authorable-surface.base.json1GENERATED-ONLYbuild-schemas.ts:2172 serialize-and-write
packages/spec/authorable-defaults/**13GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/json-schema.manifest/**14GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/api-surface/**17GENERATED-ONLYbuild-api-surface.ts, JSON shards
packages/spec/api-surface-signatures.json1GENERATED-ONLYbuild-api-surface.ts:164writeFileSync(SIG_SNAPSHOT, sigStr)
packages/spec/src/meta-spelling/meta-url-data.generated.ts1GENERATED-ONLYbuild-meta-url-spelling.ts:160 writes next; the read at :139 is a staleness compare only
packages/spec/export-origins/**17GENERATED-ONLYbuild-export-origins.ts:372writeShards(ORIGINS_DIR, shardTexts) — writes whole, removes stale
packages/spec/declaration-map/**14GENERATED-ONLYbuild-declaration-map.ts, JSON shards
docs/protocol-upgrade-guide.md1GENERATED-ONLYbuild-upgrade-guide.ts:123writeFileSync(GUIDE, next); the read at :113 is the --check compare
docs/audits/2026-07-...strictness-ledger.counts.md1GENERATED-ONLYbuild-strictness-ledger-counts.mts:43writeFileSync(countsPath, rendered)
content/docs/references/**229GENERATED-ONLYbuild-docs.tsemit() per page + manageDir(DOCS_ROOT) — the sink owns the tree and deletes strays. The 14 category index.mdx pages carry no banner but are built line-by-line in the generator (mdx += ..., :875-:895)
content/docs/permissions/system-context.mdx1MIXEDgen:system-context-census is check-system-context-census.mjs --fix, which reads the page and does writeFileSync(join(ROOT, PAGE), text) where text is the on-disk page with anchor line numbers substituted. It is the only routed generator that takes its own target as input.
skills/*/references/_index.md9GENERATED-ONLYbuild-skill-references.ts:379emit(..., generateIndex(...))
skills/objectstack-ui/contracts/react-blocks.contract.json1GENERATED-ONLYbuild-react-blocks-contract.ts:136emit(OUT_JSON, ...)
skills/objectstack-ui/references/react-blocks.md1GENERATED-ONLYbuild-react-blocks-contract.ts:170emit(OUT_MD, ...)

Result: 336 GENERATED-ONLY, 1 MIXED. There is no batch. The card's own "whether this page is the only one in that shape is unmeasured" is now measured, and the answer is yes.

2. Two further measurements, both of which changed the answer

2a. The generated half is NOT a separable table, so splitting the page is not available

Triage recorded a third shape it explicitly did not advocate: split the generated anchor table into its own file and route only that. Measured against the page: the 145 anchors sit on 83 distinct lines spread across 10 sections, from page line 49 to 238, with 14 gaps — 6 in "Which isSystem this page is about", 4 in "How the flag is set", 19/16/21/25/33/4 in the six table subsections, 13 in "What isSystem does not do", 4 in "Known rough edges". They are citations woven through hand-written prose, not a block. There is no anchor table to move out; "splitting" would mean extracting 83 individual lines from ten prose sections. Shape C is off the table on evidence, not on cost.

2b. 24 of the last 25 main commits to this page change nothing but anchor numbers

This is the reading that decides between the remaining two shapes, and it did not exist when triage set its rule. Classifying each of the last 25 main commits to the page by comparing the parent and the commit after blanking every anchor's line number (using the repo's one anchor reader, so CONTINUATION and RANGE_END anchors are counted — a naive path.ts:NNNN regex reads 24 of the 25 as prose-changing and is wrong):

anchors-only: 24 prose-touching: 1 total: 25
PROSE 73c846687 fix(plugin-security): resolve the org-admin permission set per organization

The single prose commit is the 18-line block from the near-miss the card is about. So:

3. The shape chosen: B, at the driver, and why not A

Shape A (move the row to NOT_DRIVER_MANAGED) buys the prose back by handing a page that conflicts roughly hourly to a human who cannot resolve it correctly — the merged tree's correct anchors are on neither side (#13625: five conflicted anchors resolve to 4408/5771/6019/6382/6575 against branch 4407/5770/… and main 4284/5647/…). It gives back 24-in-25 of a measured win to address a 1-in-25 case. Triage's "only one page ⇒ A is cheap and sufficient" was the right rule for the data it had; the anchors-only/prose split above is new data, and it inverts the cost.

Shape B, implemented at the driver rather than at the census gate. A prose-integrity check in check-system-context-census.mjs cannot work: the gate has no reference for what prose should be there, because prose is derived from nothing. The driver, by contrast, is handed %O %A %B and is the only actor in the system that can see both sides. So:

  • scripts/regen-artifacts.mjs — the row declares mixed: 'line-anchors'. New field, documented in the table's contract header: the row is not generated whole, and the value names the equivalence under which deferring is lossless. Keyed by name so the table keeps its "constants and pure functions" shape.
  • scripts/doc-line-anchors.mjs — new export blankAnchorLineNumbers(rawText), plus start/end offsets on each anchor so a caller can rewrite one in place. It lives here because this module is already declared "the ONE reader" of these anchors; a second three-line regex would be a second definition of "anchor" that drifts from the gate's.
  • scripts/git-merge-regen.mjs — before deferring a mixed row, deferralIsLossless() asks whether the incoming side changed anything but the generated half. Two shapes are provably lossless: theirs differs from the ancestor only in anchors (the 24-in-25 case), or theirs and ours agree once anchors are blanked. Everything else is unsafe, including an unreadable side or an unrecognised comparator name — the default has to be unsafe, because the failure this closes is invisible.
    • When unsafe, the driver text-merges in place (git merge-file into %A). Clean merge ⇒ both sides' prose survives, the path is still marked pending, and the mandatory regeneration re-derives the anchors. That is exactly the union a human produced by hand on the merge that found this defect. Conflict ⇒ markers, exit 1, and a message telling the resolver to take either side's anchors and re-run the generator.

The 24-in-25 case is untouched: it defers to OURS exactly as before.

4. The rationale that was falsified, corrected in both places

.gitattributes argued that deferring is safe becausecheck-system-context-census.mjs re-derives the census from the tree on every PR. That sentence is true, and its domain is half the risk surface: the gate re-derives the census and the anchors, and re-derives no prose. Both .gitattributes and the row's comment in scripts/regen-artifacts.mjs now state the correction, carry the ten-gates-green measurement, and record that the routing itself remains correct.

5. This driver has form — three times the same shape

Triage asked for this paragraph, and the census is what earns it.

Three separate cards, one shape: the driver makes a silent choice, and the gates are green on the wrong side of it. The common root is not any one of these bugs but the assumption that "regenerable" is a property of a path rather than of a difference. mixed is the first place that assumption is written down as something checkable rather than assumed, and the driver now fails closed against it.

6. Regression cover

check:merge-driver gains two legs:

Ablation (the guard removed, mutation and restoration both proved on disk):

HEAD blob: a0074c6d6eba32f6094711b82b8c2f1be798f8f0
marker before: 1 marker after: 0
mutated blob: d86589870acdae69d7d75ad186e8cc5763b8b794 (differs, so the mutation landed)
ABLATED_EXIT=1
✗ self-test (mixed): incoming PROSE was dropped with no conflict and no marker.
✗ merge driver wiring is inconsistent — see above.
restore: on-disk blob a0074c6d6eba32f6094711b82b8c2f1be798f8f0 == HEAD blob, `git diff HEAD` empty

The new test reproduces #14064 on the unfixed driver and goes green on the fixed one. No dist/ is involved — these are plain .mjs scripts loaded from source, so there is no build leg to stale.

7. Gates run — at 081b60c8c, the head of this branch

Derived with node scripts/pm/dispatch-gates.mjs --commandsafter merging origin/main (the first derivation warned STALE, naming scripts/pm/dispatch-gates.mjs itself as changed in the range). All exit codes captured by redirect-then-capture, never through a pipe.

CommandExit
node scripts/check-ci-filter-parity.mjs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjs3 — PREREQUISITE NOT MET, i.e. NOT MEASURED (its own words: "Nothing was measured: this gate exited before parsing a single summary line"). Not a finding.
node scripts/pm/bare-root-worklist.mjs --self-test0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:cross-package-test-inputs0
pnpm check:entry-guard0
pnpm check:merge-driver0
pnpm check:parse-guard0
pnpm check:pnpm-filter-targets0
pnpm check:ratchet-remedy-authority0
pnpm check:watch-hint-literal0
pnpm check:pm-dispatch-gatesNOT RUN — declared narrowing, see below

Beyond the derivation, because scripts/doc-line-anchors.mjs is imported by the census gate and the derivation does not map that edge:

CommandExitVerdict line
pnpm check:system-context-census0OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read — identical to the card's green control
pnpm check:doc-anchors0self-test + live, both clean
pnpm check:nul-bytes0plus a direct control-byte scan of all four changed files: clean
pnpm check:declared-population-live0156 of 200 famil(ies) declare a path population

Reconciliationnode scripts/pm/dispatch-gates.mjs --ran ran.list (mode present on this base):

Run reconciliation — 16 derived, 15 run, 0 NOT-MEASURED, 1 UNRUN.
UNRUN (1): pnpm check:pm-dispatch-gates [absent from the run record]

The declared narrowing.check:pm-dispatch-gates exceeded this container's ~10-minute foreground ceiling twice (SIGTERM, exit 143, at 10m00s and 9m20s, producing zero bytes of output both times). It entered the derived list by change KIND (scripts/**), not by path: this PR does not touch scripts/pm/dispatch-gates.mjs, so nothing here can change its behaviour. lint.yml runs it on this PR unconditionally, which is where its result will come from.

8. Notes


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064) - #14363

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing
Sep 2, 2026
Merged

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064)#14363
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14064

The card asked one question first, and triage made it binding: is this page the only MIXED file routed to merge=os-regen, or is there a batch? The census is below, it is decisive, and it changed which remedy is correct.

1. The census — every path routed to merge=os-regen, classified

18 patterns, 337 tracked files. (The count is independently confirmed by the driver's own self-test: entryForPath agrees with git check-attr on all 337 routed file(s).)

Classification method. Not "does it look generated" and not the presence of a banner — both are readable off the file and neither answers the question the driver poses. The question is: after the driver keeps one side whole and drops the other, can the generator restore what was dropped? So each row is classified by what its gen: script writes:

  • GENERATED-ONLY — the generator renders the file's entire content in memory and writes it (writeFileSync(path, rendered), the createSinkemit()/flush() pair, or writeShards()), reading the file back only to compare for staleness. Nothing on disk survives into the output, so every byte is re-derivable and a whole-side deferral loses nothing.
  • MIXED — the generator reads the target file as input and rewrites only part of it. The rest is carried over from disk, is derived from nothing, and cannot be restored.
Routed patternFilesClassEvidence
packages/spec/spec-changes.json1GENERATED-ONLYbuild-spec-changes.ts:134writeFileSync(SNAPSHOT, next)
packages/spec/liveness/state-counts.md1GENERATED-ONLYliveness/build-state-counts.mts:135 writes rendered whole; banner emitted by the generator
packages/spec/authorable-surface/**14GENERATED-ONLYbuild-schemas.ts, JSON shards written whole
packages/spec/authorable-surface.base.json1GENERATED-ONLYbuild-schemas.ts:2172 serialize-and-write
packages/spec/authorable-defaults/**13GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/json-schema.manifest/**14GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/api-surface/**17GENERATED-ONLYbuild-api-surface.ts, JSON shards
packages/spec/api-surface-signatures.json1GENERATED-ONLYbuild-api-surface.ts:164writeFileSync(SIG_SNAPSHOT, sigStr)
packages/spec/src/meta-spelling/meta-url-data.generated.ts1GENERATED-ONLYbuild-meta-url-spelling.ts:160 writes next; the read at :139 is a staleness compare only
packages/spec/export-origins/**17GENERATED-ONLYbuild-export-origins.ts:372writeShards(ORIGINS_DIR, shardTexts) — writes whole, removes stale
packages/spec/declaration-map/**14GENERATED-ONLYbuild-declaration-map.ts, JSON shards
docs/protocol-upgrade-guide.md1GENERATED-ONLYbuild-upgrade-guide.ts:123writeFileSync(GUIDE, next); the read at :113 is the --check compare
docs/audits/2026-07-...strictness-ledger.counts.md1GENERATED-ONLYbuild-strictness-ledger-counts.mts:43writeFileSync(countsPath, rendered)
content/docs/references/**229GENERATED-ONLYbuild-docs.tsemit() per page + manageDir(DOCS_ROOT) — the sink owns the tree and deletes strays. The 14 category index.mdx pages carry no banner but are built line-by-line in the generator (mdx += ..., :875-:895)
content/docs/permissions/system-context.mdx1MIXEDgen:system-context-census is check-system-context-census.mjs --fix, which reads the page and does writeFileSync(join(ROOT, PAGE), text) where text is the on-disk page with anchor line numbers substituted. It is the only routed generator that takes its own target as input.
skills/*/references/_index.md9GENERATED-ONLYbuild-skill-references.ts:379emit(..., generateIndex(...))
skills/objectstack-ui/contracts/react-blocks.contract.json1GENERATED-ONLYbuild-react-blocks-contract.ts:136emit(OUT_JSON, ...)
skills/objectstack-ui/references/react-blocks.md1GENERATED-ONLYbuild-react-blocks-contract.ts:170emit(OUT_MD, ...)

Result: 336 GENERATED-ONLY, 1 MIXED. There is no batch. The card's own "whether this page is the only one in that shape is unmeasured" is now measured, and the answer is yes.

2. Two further measurements, both of which changed the answer

2a. The generated half is NOT a separable table, so splitting the page is not available

Triage recorded a third shape it explicitly did not advocate: split the generated anchor table into its own file and route only that. Measured against the page: the 145 anchors sit on 83 distinct lines spread across 10 sections, from page line 49 to 238, with 14 gaps — 6 in "Which isSystem this page is about", 4 in "How the flag is set", 19/16/21/25/33/4 in the six table subsections, 13 in "What isSystem does not do", 4 in "Known rough edges". They are citations woven through hand-written prose, not a block. There is no anchor table to move out; "splitting" would mean extracting 83 individual lines from ten prose sections. Shape C is off the table on evidence, not on cost.

2b. 24 of the last 25 main commits to this page change nothing but anchor numbers

This is the reading that decides between the remaining two shapes, and it did not exist when triage set its rule. Classifying each of the last 25 main commits to the page by comparing the parent and the commit after blanking every anchor's line number (using the repo's one anchor reader, so CONTINUATION and RANGE_END anchors are counted — a naive path.ts:NNNN regex reads 24 of the 25 as prose-changing and is wrong):

anchors-only: 24 prose-touching: 1 total: 25
PROSE 73c846687 fix(plugin-security): resolve the org-admin permission set per organization

The single prose commit is the 18-line block from the near-miss the card is about. So:

3. The shape chosen: B, at the driver, and why not A

Shape A (move the row to NOT_DRIVER_MANAGED) buys the prose back by handing a page that conflicts roughly hourly to a human who cannot resolve it correctly — the merged tree's correct anchors are on neither side (#13625: five conflicted anchors resolve to 4408/5771/6019/6382/6575 against branch 4407/5770/… and main 4284/5647/…). It gives back 24-in-25 of a measured win to address a 1-in-25 case. Triage's "only one page ⇒ A is cheap and sufficient" was the right rule for the data it had; the anchors-only/prose split above is new data, and it inverts the cost.

Shape B, implemented at the driver rather than at the census gate. A prose-integrity check in check-system-context-census.mjs cannot work: the gate has no reference for what prose should be there, because prose is derived from nothing. The driver, by contrast, is handed %O %A %B and is the only actor in the system that can see both sides. So:

  • scripts/regen-artifacts.mjs — the row declares mixed: 'line-anchors'. New field, documented in the table's contract header: the row is not generated whole, and the value names the equivalence under which deferring is lossless. Keyed by name so the table keeps its "constants and pure functions" shape.
  • scripts/doc-line-anchors.mjs — new export blankAnchorLineNumbers(rawText), plus start/end offsets on each anchor so a caller can rewrite one in place. It lives here because this module is already declared "the ONE reader" of these anchors; a second three-line regex would be a second definition of "anchor" that drifts from the gate's.
  • scripts/git-merge-regen.mjs — before deferring a mixed row, deferralIsLossless() asks whether the incoming side changed anything but the generated half. Two shapes are provably lossless: theirs differs from the ancestor only in anchors (the 24-in-25 case), or theirs and ours agree once anchors are blanked. Everything else is unsafe, including an unreadable side or an unrecognised comparator name — the default has to be unsafe, because the failure this closes is invisible.
    • When unsafe, the driver text-merges in place (git merge-file into %A). Clean merge ⇒ both sides' prose survives, the path is still marked pending, and the mandatory regeneration re-derives the anchors. That is exactly the union a human produced by hand on the merge that found this defect. Conflict ⇒ markers, exit 1, and a message telling the resolver to take either side's anchors and re-run the generator.

The 24-in-25 case is untouched: it defers to OURS exactly as before.

4. The rationale that was falsified, corrected in both places

.gitattributes argued that deferring is safe becausecheck-system-context-census.mjs re-derives the census from the tree on every PR. That sentence is true, and its domain is half the risk surface: the gate re-derives the census and the anchors, and re-derives no prose. Both .gitattributes and the row's comment in scripts/regen-artifacts.mjs now state the correction, carry the ten-gates-green measurement, and record that the routing itself remains correct.

5. This driver has form — three times the same shape

Triage asked for this paragraph, and the census is what earns it.

Three separate cards, one shape: the driver makes a silent choice, and the gates are green on the wrong side of it. The common root is not any one of these bugs but the assumption that "regenerable" is a property of a path rather than of a difference. mixed is the first place that assumption is written down as something checkable rather than assumed, and the driver now fails closed against it.

6. Regression cover

check:merge-driver gains two legs:

Ablation (the guard removed, mutation and restoration both proved on disk):

HEAD blob: a0074c6d6eba32f6094711b82b8c2f1be798f8f0
marker before: 1 marker after: 0
mutated blob: d86589870acdae69d7d75ad186e8cc5763b8b794 (differs, so the mutation landed)
ABLATED_EXIT=1
✗ self-test (mixed): incoming PROSE was dropped with no conflict and no marker.
✗ merge driver wiring is inconsistent — see above.
restore: on-disk blob a0074c6d6eba32f6094711b82b8c2f1be798f8f0 == HEAD blob, `git diff HEAD` empty

The new test reproduces #14064 on the unfixed driver and goes green on the fixed one. No dist/ is involved — these are plain .mjs scripts loaded from source, so there is no build leg to stale.

7. Gates run — at 081b60c8c, the head of this branch

Derived with node scripts/pm/dispatch-gates.mjs --commandsafter merging origin/main (the first derivation warned STALE, naming scripts/pm/dispatch-gates.mjs itself as changed in the range). All exit codes captured by redirect-then-capture, never through a pipe.

CommandExit
node scripts/check-ci-filter-parity.mjs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjs3 — PREREQUISITE NOT MET, i.e. NOT MEASURED (its own words: "Nothing was measured: this gate exited before parsing a single summary line"). Not a finding.
node scripts/pm/bare-root-worklist.mjs --self-test0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:cross-package-test-inputs0
pnpm check:entry-guard0
pnpm check:merge-driver0
pnpm check:parse-guard0
pnpm check:pnpm-filter-targets0
pnpm check:ratchet-remedy-authority0
pnpm check:watch-hint-literal0
pnpm check:pm-dispatch-gatesNOT RUN — declared narrowing, see below

Beyond the derivation, because scripts/doc-line-anchors.mjs is imported by the census gate and the derivation does not map that edge:

CommandExitVerdict line
pnpm check:system-context-census0OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read — identical to the card's green control
pnpm check:doc-anchors0self-test + live, both clean
pnpm check:nul-bytes0plus a direct control-byte scan of all four changed files: clean
pnpm check:declared-population-live0156 of 200 famil(ies) declare a path population

Reconciliationnode scripts/pm/dispatch-gates.mjs --ran ran.list (mode present on this base):

Run reconciliation — 16 derived, 15 run, 0 NOT-MEASURED, 1 UNRUN.
UNRUN (1): pnpm check:pm-dispatch-gates [absent from the run record]

The declared narrowing.check:pm-dispatch-gates exceeded this container's ~10-minute foreground ceiling twice (SIGTERM, exit 143, at 10m00s and 9m20s, producing zero bytes of output both times). It entered the derived list by change KIND (scripts/**), not by path: this PR does not touch scripts/pm/dispatch-gates.mjs, so nothing here can change its behaviour. lint.yml runs it on this PR unconditionally, which is where its result will come from.

8. Notes


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064) - #14363

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing
Sep 2, 2026
Merged

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064)#14363
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14064

The card asked one question first, and triage made it binding: is this page the only MIXED file routed to merge=os-regen, or is there a batch? The census is below, it is decisive, and it changed which remedy is correct.

1. The census — every path routed to merge=os-regen, classified

18 patterns, 337 tracked files. (The count is independently confirmed by the driver's own self-test: entryForPath agrees with git check-attr on all 337 routed file(s).)

Classification method. Not "does it look generated" and not the presence of a banner — both are readable off the file and neither answers the question the driver poses. The question is: after the driver keeps one side whole and drops the other, can the generator restore what was dropped? So each row is classified by what its gen: script writes:

  • GENERATED-ONLY — the generator renders the file's entire content in memory and writes it (writeFileSync(path, rendered), the createSinkemit()/flush() pair, or writeShards()), reading the file back only to compare for staleness. Nothing on disk survives into the output, so every byte is re-derivable and a whole-side deferral loses nothing.
  • MIXED — the generator reads the target file as input and rewrites only part of it. The rest is carried over from disk, is derived from nothing, and cannot be restored.
Routed patternFilesClassEvidence
packages/spec/spec-changes.json1GENERATED-ONLYbuild-spec-changes.ts:134writeFileSync(SNAPSHOT, next)
packages/spec/liveness/state-counts.md1GENERATED-ONLYliveness/build-state-counts.mts:135 writes rendered whole; banner emitted by the generator
packages/spec/authorable-surface/**14GENERATED-ONLYbuild-schemas.ts, JSON shards written whole
packages/spec/authorable-surface.base.json1GENERATED-ONLYbuild-schemas.ts:2172 serialize-and-write
packages/spec/authorable-defaults/**13GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/json-schema.manifest/**14GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/api-surface/**17GENERATED-ONLYbuild-api-surface.ts, JSON shards
packages/spec/api-surface-signatures.json1GENERATED-ONLYbuild-api-surface.ts:164writeFileSync(SIG_SNAPSHOT, sigStr)
packages/spec/src/meta-spelling/meta-url-data.generated.ts1GENERATED-ONLYbuild-meta-url-spelling.ts:160 writes next; the read at :139 is a staleness compare only
packages/spec/export-origins/**17GENERATED-ONLYbuild-export-origins.ts:372writeShards(ORIGINS_DIR, shardTexts) — writes whole, removes stale
packages/spec/declaration-map/**14GENERATED-ONLYbuild-declaration-map.ts, JSON shards
docs/protocol-upgrade-guide.md1GENERATED-ONLYbuild-upgrade-guide.ts:123writeFileSync(GUIDE, next); the read at :113 is the --check compare
docs/audits/2026-07-...strictness-ledger.counts.md1GENERATED-ONLYbuild-strictness-ledger-counts.mts:43writeFileSync(countsPath, rendered)
content/docs/references/**229GENERATED-ONLYbuild-docs.tsemit() per page + manageDir(DOCS_ROOT) — the sink owns the tree and deletes strays. The 14 category index.mdx pages carry no banner but are built line-by-line in the generator (mdx += ..., :875-:895)
content/docs/permissions/system-context.mdx1MIXEDgen:system-context-census is check-system-context-census.mjs --fix, which reads the page and does writeFileSync(join(ROOT, PAGE), text) where text is the on-disk page with anchor line numbers substituted. It is the only routed generator that takes its own target as input.
skills/*/references/_index.md9GENERATED-ONLYbuild-skill-references.ts:379emit(..., generateIndex(...))
skills/objectstack-ui/contracts/react-blocks.contract.json1GENERATED-ONLYbuild-react-blocks-contract.ts:136emit(OUT_JSON, ...)
skills/objectstack-ui/references/react-blocks.md1GENERATED-ONLYbuild-react-blocks-contract.ts:170emit(OUT_MD, ...)

Result: 336 GENERATED-ONLY, 1 MIXED. There is no batch. The card's own "whether this page is the only one in that shape is unmeasured" is now measured, and the answer is yes.

2. Two further measurements, both of which changed the answer

2a. The generated half is NOT a separable table, so splitting the page is not available

Triage recorded a third shape it explicitly did not advocate: split the generated anchor table into its own file and route only that. Measured against the page: the 145 anchors sit on 83 distinct lines spread across 10 sections, from page line 49 to 238, with 14 gaps — 6 in "Which isSystem this page is about", 4 in "How the flag is set", 19/16/21/25/33/4 in the six table subsections, 13 in "What isSystem does not do", 4 in "Known rough edges". They are citations woven through hand-written prose, not a block. There is no anchor table to move out; "splitting" would mean extracting 83 individual lines from ten prose sections. Shape C is off the table on evidence, not on cost.

2b. 24 of the last 25 main commits to this page change nothing but anchor numbers

This is the reading that decides between the remaining two shapes, and it did not exist when triage set its rule. Classifying each of the last 25 main commits to the page by comparing the parent and the commit after blanking every anchor's line number (using the repo's one anchor reader, so CONTINUATION and RANGE_END anchors are counted — a naive path.ts:NNNN regex reads 24 of the 25 as prose-changing and is wrong):

anchors-only: 24 prose-touching: 1 total: 25
PROSE 73c846687 fix(plugin-security): resolve the org-admin permission set per organization

The single prose commit is the 18-line block from the near-miss the card is about. So:

3. The shape chosen: B, at the driver, and why not A

Shape A (move the row to NOT_DRIVER_MANAGED) buys the prose back by handing a page that conflicts roughly hourly to a human who cannot resolve it correctly — the merged tree's correct anchors are on neither side (#13625: five conflicted anchors resolve to 4408/5771/6019/6382/6575 against branch 4407/5770/… and main 4284/5647/…). It gives back 24-in-25 of a measured win to address a 1-in-25 case. Triage's "only one page ⇒ A is cheap and sufficient" was the right rule for the data it had; the anchors-only/prose split above is new data, and it inverts the cost.

Shape B, implemented at the driver rather than at the census gate. A prose-integrity check in check-system-context-census.mjs cannot work: the gate has no reference for what prose should be there, because prose is derived from nothing. The driver, by contrast, is handed %O %A %B and is the only actor in the system that can see both sides. So:

  • scripts/regen-artifacts.mjs — the row declares mixed: 'line-anchors'. New field, documented in the table's contract header: the row is not generated whole, and the value names the equivalence under which deferring is lossless. Keyed by name so the table keeps its "constants and pure functions" shape.
  • scripts/doc-line-anchors.mjs — new export blankAnchorLineNumbers(rawText), plus start/end offsets on each anchor so a caller can rewrite one in place. It lives here because this module is already declared "the ONE reader" of these anchors; a second three-line regex would be a second definition of "anchor" that drifts from the gate's.
  • scripts/git-merge-regen.mjs — before deferring a mixed row, deferralIsLossless() asks whether the incoming side changed anything but the generated half. Two shapes are provably lossless: theirs differs from the ancestor only in anchors (the 24-in-25 case), or theirs and ours agree once anchors are blanked. Everything else is unsafe, including an unreadable side or an unrecognised comparator name — the default has to be unsafe, because the failure this closes is invisible.
    • When unsafe, the driver text-merges in place (git merge-file into %A). Clean merge ⇒ both sides' prose survives, the path is still marked pending, and the mandatory regeneration re-derives the anchors. That is exactly the union a human produced by hand on the merge that found this defect. Conflict ⇒ markers, exit 1, and a message telling the resolver to take either side's anchors and re-run the generator.

The 24-in-25 case is untouched: it defers to OURS exactly as before.

4. The rationale that was falsified, corrected in both places

.gitattributes argued that deferring is safe becausecheck-system-context-census.mjs re-derives the census from the tree on every PR. That sentence is true, and its domain is half the risk surface: the gate re-derives the census and the anchors, and re-derives no prose. Both .gitattributes and the row's comment in scripts/regen-artifacts.mjs now state the correction, carry the ten-gates-green measurement, and record that the routing itself remains correct.

5. This driver has form — three times the same shape

Triage asked for this paragraph, and the census is what earns it.

Three separate cards, one shape: the driver makes a silent choice, and the gates are green on the wrong side of it. The common root is not any one of these bugs but the assumption that "regenerable" is a property of a path rather than of a difference. mixed is the first place that assumption is written down as something checkable rather than assumed, and the driver now fails closed against it.

6. Regression cover

check:merge-driver gains two legs:

Ablation (the guard removed, mutation and restoration both proved on disk):

HEAD blob: a0074c6d6eba32f6094711b82b8c2f1be798f8f0
marker before: 1 marker after: 0
mutated blob: d86589870acdae69d7d75ad186e8cc5763b8b794 (differs, so the mutation landed)
ABLATED_EXIT=1
✗ self-test (mixed): incoming PROSE was dropped with no conflict and no marker.
✗ merge driver wiring is inconsistent — see above.
restore: on-disk blob a0074c6d6eba32f6094711b82b8c2f1be798f8f0 == HEAD blob, `git diff HEAD` empty

The new test reproduces #14064 on the unfixed driver and goes green on the fixed one. No dist/ is involved — these are plain .mjs scripts loaded from source, so there is no build leg to stale.

7. Gates run — at 081b60c8c, the head of this branch

Derived with node scripts/pm/dispatch-gates.mjs --commandsafter merging origin/main (the first derivation warned STALE, naming scripts/pm/dispatch-gates.mjs itself as changed in the range). All exit codes captured by redirect-then-capture, never through a pipe.

CommandExit
node scripts/check-ci-filter-parity.mjs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjs3 — PREREQUISITE NOT MET, i.e. NOT MEASURED (its own words: "Nothing was measured: this gate exited before parsing a single summary line"). Not a finding.
node scripts/pm/bare-root-worklist.mjs --self-test0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:cross-package-test-inputs0
pnpm check:entry-guard0
pnpm check:merge-driver0
pnpm check:parse-guard0
pnpm check:pnpm-filter-targets0
pnpm check:ratchet-remedy-authority0
pnpm check:watch-hint-literal0
pnpm check:pm-dispatch-gatesNOT RUN — declared narrowing, see below

Beyond the derivation, because scripts/doc-line-anchors.mjs is imported by the census gate and the derivation does not map that edge:

CommandExitVerdict line
pnpm check:system-context-census0OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read — identical to the card's green control
pnpm check:doc-anchors0self-test + live, both clean
pnpm check:nul-bytes0plus a direct control-byte scan of all four changed files: clean
pnpm check:declared-population-live0156 of 200 famil(ies) declare a path population

Reconciliationnode scripts/pm/dispatch-gates.mjs --ran ran.list (mode present on this base):

Run reconciliation — 16 derived, 15 run, 0 NOT-MEASURED, 1 UNRUN.
UNRUN (1): pnpm check:pm-dispatch-gates [absent from the run record]

The declared narrowing.check:pm-dispatch-gates exceeded this container's ~10-minute foreground ceiling twice (SIGTERM, exit 143, at 10m00s and 9m20s, producing zero bytes of output both times). It entered the derived list by change KIND (scripts/**), not by path: this PR does not touch scripts/pm/dispatch-gates.mjs, so nothing here can change its behaviour. lint.yml runs it on this PR unconditionally, which is where its result will come from.

8. Notes


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064) - #14363

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing
Sep 2, 2026
Merged

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064)#14363
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14064

The card asked one question first, and triage made it binding: is this page the only MIXED file routed to merge=os-regen, or is there a batch? The census is below, it is decisive, and it changed which remedy is correct.

1. The census — every path routed to merge=os-regen, classified

18 patterns, 337 tracked files. (The count is independently confirmed by the driver's own self-test: entryForPath agrees with git check-attr on all 337 routed file(s).)

Classification method. Not "does it look generated" and not the presence of a banner — both are readable off the file and neither answers the question the driver poses. The question is: after the driver keeps one side whole and drops the other, can the generator restore what was dropped? So each row is classified by what its gen: script writes:

  • GENERATED-ONLY — the generator renders the file's entire content in memory and writes it (writeFileSync(path, rendered), the createSinkemit()/flush() pair, or writeShards()), reading the file back only to compare for staleness. Nothing on disk survives into the output, so every byte is re-derivable and a whole-side deferral loses nothing.
  • MIXED — the generator reads the target file as input and rewrites only part of it. The rest is carried over from disk, is derived from nothing, and cannot be restored.
Routed patternFilesClassEvidence
packages/spec/spec-changes.json1GENERATED-ONLYbuild-spec-changes.ts:134writeFileSync(SNAPSHOT, next)
packages/spec/liveness/state-counts.md1GENERATED-ONLYliveness/build-state-counts.mts:135 writes rendered whole; banner emitted by the generator
packages/spec/authorable-surface/**14GENERATED-ONLYbuild-schemas.ts, JSON shards written whole
packages/spec/authorable-surface.base.json1GENERATED-ONLYbuild-schemas.ts:2172 serialize-and-write
packages/spec/authorable-defaults/**13GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/json-schema.manifest/**14GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/api-surface/**17GENERATED-ONLYbuild-api-surface.ts, JSON shards
packages/spec/api-surface-signatures.json1GENERATED-ONLYbuild-api-surface.ts:164writeFileSync(SIG_SNAPSHOT, sigStr)
packages/spec/src/meta-spelling/meta-url-data.generated.ts1GENERATED-ONLYbuild-meta-url-spelling.ts:160 writes next; the read at :139 is a staleness compare only
packages/spec/export-origins/**17GENERATED-ONLYbuild-export-origins.ts:372writeShards(ORIGINS_DIR, shardTexts) — writes whole, removes stale
packages/spec/declaration-map/**14GENERATED-ONLYbuild-declaration-map.ts, JSON shards
docs/protocol-upgrade-guide.md1GENERATED-ONLYbuild-upgrade-guide.ts:123writeFileSync(GUIDE, next); the read at :113 is the --check compare
docs/audits/2026-07-...strictness-ledger.counts.md1GENERATED-ONLYbuild-strictness-ledger-counts.mts:43writeFileSync(countsPath, rendered)
content/docs/references/**229GENERATED-ONLYbuild-docs.tsemit() per page + manageDir(DOCS_ROOT) — the sink owns the tree and deletes strays. The 14 category index.mdx pages carry no banner but are built line-by-line in the generator (mdx += ..., :875-:895)
content/docs/permissions/system-context.mdx1MIXEDgen:system-context-census is check-system-context-census.mjs --fix, which reads the page and does writeFileSync(join(ROOT, PAGE), text) where text is the on-disk page with anchor line numbers substituted. It is the only routed generator that takes its own target as input.
skills/*/references/_index.md9GENERATED-ONLYbuild-skill-references.ts:379emit(..., generateIndex(...))
skills/objectstack-ui/contracts/react-blocks.contract.json1GENERATED-ONLYbuild-react-blocks-contract.ts:136emit(OUT_JSON, ...)
skills/objectstack-ui/references/react-blocks.md1GENERATED-ONLYbuild-react-blocks-contract.ts:170emit(OUT_MD, ...)

Result: 336 GENERATED-ONLY, 1 MIXED. There is no batch. The card's own "whether this page is the only one in that shape is unmeasured" is now measured, and the answer is yes.

2. Two further measurements, both of which changed the answer

2a. The generated half is NOT a separable table, so splitting the page is not available

Triage recorded a third shape it explicitly did not advocate: split the generated anchor table into its own file and route only that. Measured against the page: the 145 anchors sit on 83 distinct lines spread across 10 sections, from page line 49 to 238, with 14 gaps — 6 in "Which isSystem this page is about", 4 in "How the flag is set", 19/16/21/25/33/4 in the six table subsections, 13 in "What isSystem does not do", 4 in "Known rough edges". They are citations woven through hand-written prose, not a block. There is no anchor table to move out; "splitting" would mean extracting 83 individual lines from ten prose sections. Shape C is off the table on evidence, not on cost.

2b. 24 of the last 25 main commits to this page change nothing but anchor numbers

This is the reading that decides between the remaining two shapes, and it did not exist when triage set its rule. Classifying each of the last 25 main commits to the page by comparing the parent and the commit after blanking every anchor's line number (using the repo's one anchor reader, so CONTINUATION and RANGE_END anchors are counted — a naive path.ts:NNNN regex reads 24 of the 25 as prose-changing and is wrong):

anchors-only: 24 prose-touching: 1 total: 25
PROSE 73c846687 fix(plugin-security): resolve the org-admin permission set per organization

The single prose commit is the 18-line block from the near-miss the card is about. So:

3. The shape chosen: B, at the driver, and why not A

Shape A (move the row to NOT_DRIVER_MANAGED) buys the prose back by handing a page that conflicts roughly hourly to a human who cannot resolve it correctly — the merged tree's correct anchors are on neither side (#13625: five conflicted anchors resolve to 4408/5771/6019/6382/6575 against branch 4407/5770/… and main 4284/5647/…). It gives back 24-in-25 of a measured win to address a 1-in-25 case. Triage's "only one page ⇒ A is cheap and sufficient" was the right rule for the data it had; the anchors-only/prose split above is new data, and it inverts the cost.

Shape B, implemented at the driver rather than at the census gate. A prose-integrity check in check-system-context-census.mjs cannot work: the gate has no reference for what prose should be there, because prose is derived from nothing. The driver, by contrast, is handed %O %A %B and is the only actor in the system that can see both sides. So:

  • scripts/regen-artifacts.mjs — the row declares mixed: 'line-anchors'. New field, documented in the table's contract header: the row is not generated whole, and the value names the equivalence under which deferring is lossless. Keyed by name so the table keeps its "constants and pure functions" shape.
  • scripts/doc-line-anchors.mjs — new export blankAnchorLineNumbers(rawText), plus start/end offsets on each anchor so a caller can rewrite one in place. It lives here because this module is already declared "the ONE reader" of these anchors; a second three-line regex would be a second definition of "anchor" that drifts from the gate's.
  • scripts/git-merge-regen.mjs — before deferring a mixed row, deferralIsLossless() asks whether the incoming side changed anything but the generated half. Two shapes are provably lossless: theirs differs from the ancestor only in anchors (the 24-in-25 case), or theirs and ours agree once anchors are blanked. Everything else is unsafe, including an unreadable side or an unrecognised comparator name — the default has to be unsafe, because the failure this closes is invisible.
    • When unsafe, the driver text-merges in place (git merge-file into %A). Clean merge ⇒ both sides' prose survives, the path is still marked pending, and the mandatory regeneration re-derives the anchors. That is exactly the union a human produced by hand on the merge that found this defect. Conflict ⇒ markers, exit 1, and a message telling the resolver to take either side's anchors and re-run the generator.

The 24-in-25 case is untouched: it defers to OURS exactly as before.

4. The rationale that was falsified, corrected in both places

.gitattributes argued that deferring is safe becausecheck-system-context-census.mjs re-derives the census from the tree on every PR. That sentence is true, and its domain is half the risk surface: the gate re-derives the census and the anchors, and re-derives no prose. Both .gitattributes and the row's comment in scripts/regen-artifacts.mjs now state the correction, carry the ten-gates-green measurement, and record that the routing itself remains correct.

5. This driver has form — three times the same shape

Triage asked for this paragraph, and the census is what earns it.

Three separate cards, one shape: the driver makes a silent choice, and the gates are green on the wrong side of it. The common root is not any one of these bugs but the assumption that "regenerable" is a property of a path rather than of a difference. mixed is the first place that assumption is written down as something checkable rather than assumed, and the driver now fails closed against it.

6. Regression cover

check:merge-driver gains two legs:

Ablation (the guard removed, mutation and restoration both proved on disk):

HEAD blob: a0074c6d6eba32f6094711b82b8c2f1be798f8f0
marker before: 1 marker after: 0
mutated blob: d86589870acdae69d7d75ad186e8cc5763b8b794 (differs, so the mutation landed)
ABLATED_EXIT=1
✗ self-test (mixed): incoming PROSE was dropped with no conflict and no marker.
✗ merge driver wiring is inconsistent — see above.
restore: on-disk blob a0074c6d6eba32f6094711b82b8c2f1be798f8f0 == HEAD blob, `git diff HEAD` empty

The new test reproduces #14064 on the unfixed driver and goes green on the fixed one. No dist/ is involved — these are plain .mjs scripts loaded from source, so there is no build leg to stale.

7. Gates run — at 081b60c8c, the head of this branch

Derived with node scripts/pm/dispatch-gates.mjs --commandsafter merging origin/main (the first derivation warned STALE, naming scripts/pm/dispatch-gates.mjs itself as changed in the range). All exit codes captured by redirect-then-capture, never through a pipe.

CommandExit
node scripts/check-ci-filter-parity.mjs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjs3 — PREREQUISITE NOT MET, i.e. NOT MEASURED (its own words: "Nothing was measured: this gate exited before parsing a single summary line"). Not a finding.
node scripts/pm/bare-root-worklist.mjs --self-test0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:cross-package-test-inputs0
pnpm check:entry-guard0
pnpm check:merge-driver0
pnpm check:parse-guard0
pnpm check:pnpm-filter-targets0
pnpm check:ratchet-remedy-authority0
pnpm check:watch-hint-literal0
pnpm check:pm-dispatch-gatesNOT RUN — declared narrowing, see below

Beyond the derivation, because scripts/doc-line-anchors.mjs is imported by the census gate and the derivation does not map that edge:

CommandExitVerdict line
pnpm check:system-context-census0OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read — identical to the card's green control
pnpm check:doc-anchors0self-test + live, both clean
pnpm check:nul-bytes0plus a direct control-byte scan of all four changed files: clean
pnpm check:declared-population-live0156 of 200 famil(ies) declare a path population

Reconciliationnode scripts/pm/dispatch-gates.mjs --ran ran.list (mode present on this base):

Run reconciliation — 16 derived, 15 run, 0 NOT-MEASURED, 1 UNRUN.
UNRUN (1): pnpm check:pm-dispatch-gates [absent from the run record]

The declared narrowing.check:pm-dispatch-gates exceeded this container's ~10-minute foreground ceiling twice (SIGTERM, exit 143, at 10m00s and 9m20s, producing zero bytes of output both times). It entered the derived list by change KIND (scripts/**), not by path: this PR does not touch scripts/pm/dispatch-gates.mjs, so nothing here can change its behaviour. lint.yml runs it on this PR unconditionally, which is where its result will come from.

8. Notes


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064) - #14363

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing
Sep 2, 2026
Merged

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064)#14363
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14064

The card asked one question first, and triage made it binding: is this page the only MIXED file routed to merge=os-regen, or is there a batch? The census is below, it is decisive, and it changed which remedy is correct.

1. The census — every path routed to merge=os-regen, classified

18 patterns, 337 tracked files. (The count is independently confirmed by the driver's own self-test: entryForPath agrees with git check-attr on all 337 routed file(s).)

Classification method. Not "does it look generated" and not the presence of a banner — both are readable off the file and neither answers the question the driver poses. The question is: after the driver keeps one side whole and drops the other, can the generator restore what was dropped? So each row is classified by what its gen: script writes:

  • GENERATED-ONLY — the generator renders the file's entire content in memory and writes it (writeFileSync(path, rendered), the createSinkemit()/flush() pair, or writeShards()), reading the file back only to compare for staleness. Nothing on disk survives into the output, so every byte is re-derivable and a whole-side deferral loses nothing.
  • MIXED — the generator reads the target file as input and rewrites only part of it. The rest is carried over from disk, is derived from nothing, and cannot be restored.
Routed patternFilesClassEvidence
packages/spec/spec-changes.json1GENERATED-ONLYbuild-spec-changes.ts:134writeFileSync(SNAPSHOT, next)
packages/spec/liveness/state-counts.md1GENERATED-ONLYliveness/build-state-counts.mts:135 writes rendered whole; banner emitted by the generator
packages/spec/authorable-surface/**14GENERATED-ONLYbuild-schemas.ts, JSON shards written whole
packages/spec/authorable-surface.base.json1GENERATED-ONLYbuild-schemas.ts:2172 serialize-and-write
packages/spec/authorable-defaults/**13GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/json-schema.manifest/**14GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/api-surface/**17GENERATED-ONLYbuild-api-surface.ts, JSON shards
packages/spec/api-surface-signatures.json1GENERATED-ONLYbuild-api-surface.ts:164writeFileSync(SIG_SNAPSHOT, sigStr)
packages/spec/src/meta-spelling/meta-url-data.generated.ts1GENERATED-ONLYbuild-meta-url-spelling.ts:160 writes next; the read at :139 is a staleness compare only
packages/spec/export-origins/**17GENERATED-ONLYbuild-export-origins.ts:372writeShards(ORIGINS_DIR, shardTexts) — writes whole, removes stale
packages/spec/declaration-map/**14GENERATED-ONLYbuild-declaration-map.ts, JSON shards
docs/protocol-upgrade-guide.md1GENERATED-ONLYbuild-upgrade-guide.ts:123writeFileSync(GUIDE, next); the read at :113 is the --check compare
docs/audits/2026-07-...strictness-ledger.counts.md1GENERATED-ONLYbuild-strictness-ledger-counts.mts:43writeFileSync(countsPath, rendered)
content/docs/references/**229GENERATED-ONLYbuild-docs.tsemit() per page + manageDir(DOCS_ROOT) — the sink owns the tree and deletes strays. The 14 category index.mdx pages carry no banner but are built line-by-line in the generator (mdx += ..., :875-:895)
content/docs/permissions/system-context.mdx1MIXEDgen:system-context-census is check-system-context-census.mjs --fix, which reads the page and does writeFileSync(join(ROOT, PAGE), text) where text is the on-disk page with anchor line numbers substituted. It is the only routed generator that takes its own target as input.
skills/*/references/_index.md9GENERATED-ONLYbuild-skill-references.ts:379emit(..., generateIndex(...))
skills/objectstack-ui/contracts/react-blocks.contract.json1GENERATED-ONLYbuild-react-blocks-contract.ts:136emit(OUT_JSON, ...)
skills/objectstack-ui/references/react-blocks.md1GENERATED-ONLYbuild-react-blocks-contract.ts:170emit(OUT_MD, ...)

Result: 336 GENERATED-ONLY, 1 MIXED. There is no batch. The card's own "whether this page is the only one in that shape is unmeasured" is now measured, and the answer is yes.

2. Two further measurements, both of which changed the answer

2a. The generated half is NOT a separable table, so splitting the page is not available

Triage recorded a third shape it explicitly did not advocate: split the generated anchor table into its own file and route only that. Measured against the page: the 145 anchors sit on 83 distinct lines spread across 10 sections, from page line 49 to 238, with 14 gaps — 6 in "Which isSystem this page is about", 4 in "How the flag is set", 19/16/21/25/33/4 in the six table subsections, 13 in "What isSystem does not do", 4 in "Known rough edges". They are citations woven through hand-written prose, not a block. There is no anchor table to move out; "splitting" would mean extracting 83 individual lines from ten prose sections. Shape C is off the table on evidence, not on cost.

2b. 24 of the last 25 main commits to this page change nothing but anchor numbers

This is the reading that decides between the remaining two shapes, and it did not exist when triage set its rule. Classifying each of the last 25 main commits to the page by comparing the parent and the commit after blanking every anchor's line number (using the repo's one anchor reader, so CONTINUATION and RANGE_END anchors are counted — a naive path.ts:NNNN regex reads 24 of the 25 as prose-changing and is wrong):

anchors-only: 24 prose-touching: 1 total: 25
PROSE 73c846687 fix(plugin-security): resolve the org-admin permission set per organization

The single prose commit is the 18-line block from the near-miss the card is about. So:

3. The shape chosen: B, at the driver, and why not A

Shape A (move the row to NOT_DRIVER_MANAGED) buys the prose back by handing a page that conflicts roughly hourly to a human who cannot resolve it correctly — the merged tree's correct anchors are on neither side (#13625: five conflicted anchors resolve to 4408/5771/6019/6382/6575 against branch 4407/5770/… and main 4284/5647/…). It gives back 24-in-25 of a measured win to address a 1-in-25 case. Triage's "only one page ⇒ A is cheap and sufficient" was the right rule for the data it had; the anchors-only/prose split above is new data, and it inverts the cost.

Shape B, implemented at the driver rather than at the census gate. A prose-integrity check in check-system-context-census.mjs cannot work: the gate has no reference for what prose should be there, because prose is derived from nothing. The driver, by contrast, is handed %O %A %B and is the only actor in the system that can see both sides. So:

  • scripts/regen-artifacts.mjs — the row declares mixed: 'line-anchors'. New field, documented in the table's contract header: the row is not generated whole, and the value names the equivalence under which deferring is lossless. Keyed by name so the table keeps its "constants and pure functions" shape.
  • scripts/doc-line-anchors.mjs — new export blankAnchorLineNumbers(rawText), plus start/end offsets on each anchor so a caller can rewrite one in place. It lives here because this module is already declared "the ONE reader" of these anchors; a second three-line regex would be a second definition of "anchor" that drifts from the gate's.
  • scripts/git-merge-regen.mjs — before deferring a mixed row, deferralIsLossless() asks whether the incoming side changed anything but the generated half. Two shapes are provably lossless: theirs differs from the ancestor only in anchors (the 24-in-25 case), or theirs and ours agree once anchors are blanked. Everything else is unsafe, including an unreadable side or an unrecognised comparator name — the default has to be unsafe, because the failure this closes is invisible.
    • When unsafe, the driver text-merges in place (git merge-file into %A). Clean merge ⇒ both sides' prose survives, the path is still marked pending, and the mandatory regeneration re-derives the anchors. That is exactly the union a human produced by hand on the merge that found this defect. Conflict ⇒ markers, exit 1, and a message telling the resolver to take either side's anchors and re-run the generator.

The 24-in-25 case is untouched: it defers to OURS exactly as before.

4. The rationale that was falsified, corrected in both places

.gitattributes argued that deferring is safe becausecheck-system-context-census.mjs re-derives the census from the tree on every PR. That sentence is true, and its domain is half the risk surface: the gate re-derives the census and the anchors, and re-derives no prose. Both .gitattributes and the row's comment in scripts/regen-artifacts.mjs now state the correction, carry the ten-gates-green measurement, and record that the routing itself remains correct.

5. This driver has form — three times the same shape

Triage asked for this paragraph, and the census is what earns it.

Three separate cards, one shape: the driver makes a silent choice, and the gates are green on the wrong side of it. The common root is not any one of these bugs but the assumption that "regenerable" is a property of a path rather than of a difference. mixed is the first place that assumption is written down as something checkable rather than assumed, and the driver now fails closed against it.

6. Regression cover

check:merge-driver gains two legs:

Ablation (the guard removed, mutation and restoration both proved on disk):

HEAD blob: a0074c6d6eba32f6094711b82b8c2f1be798f8f0
marker before: 1 marker after: 0
mutated blob: d86589870acdae69d7d75ad186e8cc5763b8b794 (differs, so the mutation landed)
ABLATED_EXIT=1
✗ self-test (mixed): incoming PROSE was dropped with no conflict and no marker.
✗ merge driver wiring is inconsistent — see above.
restore: on-disk blob a0074c6d6eba32f6094711b82b8c2f1be798f8f0 == HEAD blob, `git diff HEAD` empty

The new test reproduces #14064 on the unfixed driver and goes green on the fixed one. No dist/ is involved — these are plain .mjs scripts loaded from source, so there is no build leg to stale.

7. Gates run — at 081b60c8c, the head of this branch

Derived with node scripts/pm/dispatch-gates.mjs --commandsafter merging origin/main (the first derivation warned STALE, naming scripts/pm/dispatch-gates.mjs itself as changed in the range). All exit codes captured by redirect-then-capture, never through a pipe.

CommandExit
node scripts/check-ci-filter-parity.mjs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjs3 — PREREQUISITE NOT MET, i.e. NOT MEASURED (its own words: "Nothing was measured: this gate exited before parsing a single summary line"). Not a finding.
node scripts/pm/bare-root-worklist.mjs --self-test0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:cross-package-test-inputs0
pnpm check:entry-guard0
pnpm check:merge-driver0
pnpm check:parse-guard0
pnpm check:pnpm-filter-targets0
pnpm check:ratchet-remedy-authority0
pnpm check:watch-hint-literal0
pnpm check:pm-dispatch-gatesNOT RUN — declared narrowing, see below

Beyond the derivation, because scripts/doc-line-anchors.mjs is imported by the census gate and the derivation does not map that edge:

CommandExitVerdict line
pnpm check:system-context-census0OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read — identical to the card's green control
pnpm check:doc-anchors0self-test + live, both clean
pnpm check:nul-bytes0plus a direct control-byte scan of all four changed files: clean
pnpm check:declared-population-live0156 of 200 famil(ies) declare a path population

Reconciliationnode scripts/pm/dispatch-gates.mjs --ran ran.list (mode present on this base):

Run reconciliation — 16 derived, 15 run, 0 NOT-MEASURED, 1 UNRUN.
UNRUN (1): pnpm check:pm-dispatch-gates [absent from the run record]

The declared narrowing.check:pm-dispatch-gates exceeded this container's ~10-minute foreground ceiling twice (SIGTERM, exit 143, at 10m00s and 9m20s, producing zero bytes of output both times). It entered the derived list by change KIND (scripts/**), not by path: this PR does not touch scripts/pm/dispatch-gates.mjs, so nothing here can change its behaviour. lint.yml runs it on this PR unconditionally, which is where its result will come from.

8. Notes


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064) - #14363

Merged
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing
Sep 2, 2026
Merged

fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064)#14363
baozhoutao merged 4 commits into
mainfrom
claude/issue-14064-mixed-file-regen-routing

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#14064

The card asked one question first, and triage made it binding: is this page the only MIXED file routed to merge=os-regen, or is there a batch? The census is below, it is decisive, and it changed which remedy is correct.

1. The census — every path routed to merge=os-regen, classified

18 patterns, 337 tracked files. (The count is independently confirmed by the driver's own self-test: entryForPath agrees with git check-attr on all 337 routed file(s).)

Classification method. Not "does it look generated" and not the presence of a banner — both are readable off the file and neither answers the question the driver poses. The question is: after the driver keeps one side whole and drops the other, can the generator restore what was dropped? So each row is classified by what its gen: script writes:

  • GENERATED-ONLY — the generator renders the file's entire content in memory and writes it (writeFileSync(path, rendered), the createSinkemit()/flush() pair, or writeShards()), reading the file back only to compare for staleness. Nothing on disk survives into the output, so every byte is re-derivable and a whole-side deferral loses nothing.
  • MIXED — the generator reads the target file as input and rewrites only part of it. The rest is carried over from disk, is derived from nothing, and cannot be restored.
Routed patternFilesClassEvidence
packages/spec/spec-changes.json1GENERATED-ONLYbuild-spec-changes.ts:134writeFileSync(SNAPSHOT, next)
packages/spec/liveness/state-counts.md1GENERATED-ONLYliveness/build-state-counts.mts:135 writes rendered whole; banner emitted by the generator
packages/spec/authorable-surface/**14GENERATED-ONLYbuild-schemas.ts, JSON shards written whole
packages/spec/authorable-surface.base.json1GENERATED-ONLYbuild-schemas.ts:2172 serialize-and-write
packages/spec/authorable-defaults/**13GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/json-schema.manifest/**14GENERATED-ONLYbuild-schemas.ts, JSON shards
packages/spec/api-surface/**17GENERATED-ONLYbuild-api-surface.ts, JSON shards
packages/spec/api-surface-signatures.json1GENERATED-ONLYbuild-api-surface.ts:164writeFileSync(SIG_SNAPSHOT, sigStr)
packages/spec/src/meta-spelling/meta-url-data.generated.ts1GENERATED-ONLYbuild-meta-url-spelling.ts:160 writes next; the read at :139 is a staleness compare only
packages/spec/export-origins/**17GENERATED-ONLYbuild-export-origins.ts:372writeShards(ORIGINS_DIR, shardTexts) — writes whole, removes stale
packages/spec/declaration-map/**14GENERATED-ONLYbuild-declaration-map.ts, JSON shards
docs/protocol-upgrade-guide.md1GENERATED-ONLYbuild-upgrade-guide.ts:123writeFileSync(GUIDE, next); the read at :113 is the --check compare
docs/audits/2026-07-...strictness-ledger.counts.md1GENERATED-ONLYbuild-strictness-ledger-counts.mts:43writeFileSync(countsPath, rendered)
content/docs/references/**229GENERATED-ONLYbuild-docs.tsemit() per page + manageDir(DOCS_ROOT) — the sink owns the tree and deletes strays. The 14 category index.mdx pages carry no banner but are built line-by-line in the generator (mdx += ..., :875-:895)
content/docs/permissions/system-context.mdx1MIXEDgen:system-context-census is check-system-context-census.mjs --fix, which reads the page and does writeFileSync(join(ROOT, PAGE), text) where text is the on-disk page with anchor line numbers substituted. It is the only routed generator that takes its own target as input.
skills/*/references/_index.md9GENERATED-ONLYbuild-skill-references.ts:379emit(..., generateIndex(...))
skills/objectstack-ui/contracts/react-blocks.contract.json1GENERATED-ONLYbuild-react-blocks-contract.ts:136emit(OUT_JSON, ...)
skills/objectstack-ui/references/react-blocks.md1GENERATED-ONLYbuild-react-blocks-contract.ts:170emit(OUT_MD, ...)

Result: 336 GENERATED-ONLY, 1 MIXED. There is no batch. The card's own "whether this page is the only one in that shape is unmeasured" is now measured, and the answer is yes.

2. Two further measurements, both of which changed the answer

2a. The generated half is NOT a separable table, so splitting the page is not available

Triage recorded a third shape it explicitly did not advocate: split the generated anchor table into its own file and route only that. Measured against the page: the 145 anchors sit on 83 distinct lines spread across 10 sections, from page line 49 to 238, with 14 gaps — 6 in "Which isSystem this page is about", 4 in "How the flag is set", 19/16/21/25/33/4 in the six table subsections, 13 in "What isSystem does not do", 4 in "Known rough edges". They are citations woven through hand-written prose, not a block. There is no anchor table to move out; "splitting" would mean extracting 83 individual lines from ten prose sections. Shape C is off the table on evidence, not on cost.

2b. 24 of the last 25 main commits to this page change nothing but anchor numbers

This is the reading that decides between the remaining two shapes, and it did not exist when triage set its rule. Classifying each of the last 25 main commits to the page by comparing the parent and the commit after blanking every anchor's line number (using the repo's one anchor reader, so CONTINUATION and RANGE_END anchors are counted — a naive path.ts:NNNN regex reads 24 of the 25 as prose-changing and is wrong):

anchors-only: 24 prose-touching: 1 total: 25
PROSE 73c846687 fix(plugin-security): resolve the org-admin permission set per organization

The single prose commit is the 18-line block from the near-miss the card is about. So:

3. The shape chosen: B, at the driver, and why not A

Shape A (move the row to NOT_DRIVER_MANAGED) buys the prose back by handing a page that conflicts roughly hourly to a human who cannot resolve it correctly — the merged tree's correct anchors are on neither side (#13625: five conflicted anchors resolve to 4408/5771/6019/6382/6575 against branch 4407/5770/… and main 4284/5647/…). It gives back 24-in-25 of a measured win to address a 1-in-25 case. Triage's "only one page ⇒ A is cheap and sufficient" was the right rule for the data it had; the anchors-only/prose split above is new data, and it inverts the cost.

Shape B, implemented at the driver rather than at the census gate. A prose-integrity check in check-system-context-census.mjs cannot work: the gate has no reference for what prose should be there, because prose is derived from nothing. The driver, by contrast, is handed %O %A %B and is the only actor in the system that can see both sides. So:

  • scripts/regen-artifacts.mjs — the row declares mixed: 'line-anchors'. New field, documented in the table's contract header: the row is not generated whole, and the value names the equivalence under which deferring is lossless. Keyed by name so the table keeps its "constants and pure functions" shape.
  • scripts/doc-line-anchors.mjs — new export blankAnchorLineNumbers(rawText), plus start/end offsets on each anchor so a caller can rewrite one in place. It lives here because this module is already declared "the ONE reader" of these anchors; a second three-line regex would be a second definition of "anchor" that drifts from the gate's.
  • scripts/git-merge-regen.mjs — before deferring a mixed row, deferralIsLossless() asks whether the incoming side changed anything but the generated half. Two shapes are provably lossless: theirs differs from the ancestor only in anchors (the 24-in-25 case), or theirs and ours agree once anchors are blanked. Everything else is unsafe, including an unreadable side or an unrecognised comparator name — the default has to be unsafe, because the failure this closes is invisible.
    • When unsafe, the driver text-merges in place (git merge-file into %A). Clean merge ⇒ both sides' prose survives, the path is still marked pending, and the mandatory regeneration re-derives the anchors. That is exactly the union a human produced by hand on the merge that found this defect. Conflict ⇒ markers, exit 1, and a message telling the resolver to take either side's anchors and re-run the generator.

The 24-in-25 case is untouched: it defers to OURS exactly as before.

4. The rationale that was falsified, corrected in both places

.gitattributes argued that deferring is safe becausecheck-system-context-census.mjs re-derives the census from the tree on every PR. That sentence is true, and its domain is half the risk surface: the gate re-derives the census and the anchors, and re-derives no prose. Both .gitattributes and the row's comment in scripts/regen-artifacts.mjs now state the correction, carry the ten-gates-green measurement, and record that the routing itself remains correct.

5. This driver has form — three times the same shape

Triage asked for this paragraph, and the census is what earns it.

Three separate cards, one shape: the driver makes a silent choice, and the gates are green on the wrong side of it. The common root is not any one of these bugs but the assumption that "regenerable" is a property of a path rather than of a difference. mixed is the first place that assumption is written down as something checkable rather than assumed, and the driver now fails closed against it.

6. Regression cover

check:merge-driver gains two legs:

Ablation (the guard removed, mutation and restoration both proved on disk):

HEAD blob: a0074c6d6eba32f6094711b82b8c2f1be798f8f0
marker before: 1 marker after: 0
mutated blob: d86589870acdae69d7d75ad186e8cc5763b8b794 (differs, so the mutation landed)
ABLATED_EXIT=1
✗ self-test (mixed): incoming PROSE was dropped with no conflict and no marker.
✗ merge driver wiring is inconsistent — see above.
restore: on-disk blob a0074c6d6eba32f6094711b82b8c2f1be798f8f0 == HEAD blob, `git diff HEAD` empty

The new test reproduces #14064 on the unfixed driver and goes green on the fixed one. No dist/ is involved — these are plain .mjs scripts loaded from source, so there is no build leg to stale.

7. Gates run — at 081b60c8c, the head of this branch

Derived with node scripts/pm/dispatch-gates.mjs --commandsafter merging origin/main (the first derivation warned STALE, naming scripts/pm/dispatch-gates.mjs itself as changed in the range). All exit codes captured by redirect-then-capture, never through a pipe.

CommandExit
node scripts/check-ci-filter-parity.mjs0
node scripts/check-cross-package-test-inputs.mjs0
node scripts/check-shard-attestation.mjs0
node scripts/check-test-completeness.mjs3 — PREREQUISITE NOT MET, i.e. NOT MEASURED (its own words: "Nothing was measured: this gate exited before parsing a single summary line"). Not a finding.
node scripts/pm/bare-root-worklist.mjs --self-test0
pnpm check:agent-test-spelling0
pnpm check:bash32-floor0
pnpm check:cli-command-ids0
pnpm check:cross-package-test-inputs0
pnpm check:entry-guard0
pnpm check:merge-driver0
pnpm check:parse-guard0
pnpm check:pnpm-filter-targets0
pnpm check:ratchet-remedy-authority0
pnpm check:watch-hint-literal0
pnpm check:pm-dispatch-gatesNOT RUN — declared narrowing, see below

Beyond the derivation, because scripts/doc-line-anchors.mjs is imported by the census gate and the derivation does not map that edge:

CommandExitVerdict line
pnpm check:system-context-census0OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read — identical to the card's green control
pnpm check:doc-anchors0self-test + live, both clean
pnpm check:nul-bytes0plus a direct control-byte scan of all four changed files: clean
pnpm check:declared-population-live0156 of 200 famil(ies) declare a path population

Reconciliationnode scripts/pm/dispatch-gates.mjs --ran ran.list (mode present on this base):

Run reconciliation — 16 derived, 15 run, 0 NOT-MEASURED, 1 UNRUN.
UNRUN (1): pnpm check:pm-dispatch-gates [absent from the run record]

The declared narrowing.check:pm-dispatch-gates exceeded this container's ~10-minute foreground ceiling twice (SIGTERM, exit 143, at 10m00s and 9m20s, producing zero bytes of output both times). It entered the derived list by change KIND (scripts/**), not by path: this PR does not touch scripts/pm/dispatch-gates.mjs, so nothing here can change its behaviour. lint.yml runs it on this PR unconditionally, which is where its result will come from.

8. Notes


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@baozhoutao@claude