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

@claude claude Bot 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 createSink emit()/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 pattern Files Class Evidence
packages/spec/spec-changes.json 1 GENERATED-ONLY build-spec-changes.ts:134 writeFileSync(SNAPSHOT, next)
packages/spec/liveness/state-counts.md 1 GENERATED-ONLY liveness/build-state-counts.mts:135 writes rendered whole; banner emitted by the generator
packages/spec/authorable-surface/** 14 GENERATED-ONLY build-schemas.ts, JSON shards written whole
packages/spec/authorable-surface.base.json 1 GENERATED-ONLY build-schemas.ts:2172 serialize-and-write
packages/spec/authorable-defaults/** 13 GENERATED-ONLY build-schemas.ts, JSON shards
packages/spec/json-schema.manifest/** 14 GENERATED-ONLY build-schemas.ts, JSON shards
packages/spec/api-surface/** 17 GENERATED-ONLY build-api-surface.ts, JSON shards
packages/spec/api-surface-signatures.json 1 GENERATED-ONLY build-api-surface.ts:164 writeFileSync(SIG_SNAPSHOT, sigStr)
packages/spec/src/meta-spelling/meta-url-data.generated.ts 1 GENERATED-ONLY build-meta-url-spelling.ts:160 writes next; the read at :139 is a staleness compare only
packages/spec/export-origins/** 17 GENERATED-ONLY build-export-origins.ts:372 writeShards(ORIGINS_DIR, shardTexts) — writes whole, removes stale
packages/spec/declaration-map/** 14 GENERATED-ONLY build-declaration-map.ts, JSON shards
docs/protocol-upgrade-guide.md 1 GENERATED-ONLY build-upgrade-guide.ts:123 writeFileSync(GUIDE, next); the read at :113 is the --check compare
docs/audits/2026-07-...strictness-ledger.counts.md 1 GENERATED-ONLY build-strictness-ledger-counts.mts:43 writeFileSync(countsPath, rendered)
content/docs/references/** 229 GENERATED-ONLY build-docs.ts emit() 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.mdx 1 MIXED gen: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.md 9 GENERATED-ONLY build-skill-references.ts:379 emit(..., generateIndex(...))
skills/objectstack-ui/contracts/react-blocks.contract.json 1 GENERATED-ONLY build-react-blocks-contract.ts:136 emit(OUT_JSON, ...)
skills/objectstack-ui/references/react-blocks.md 1 GENERATED-ONLY build-react-blocks-contract.ts:170 emit(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 because check-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 --commands after 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.

Command Exit
node scripts/check-ci-filter-parity.mjs 0
node scripts/check-cross-package-test-inputs.mjs 0
node scripts/check-shard-attestation.mjs 0
node scripts/check-test-completeness.mjs 3 — 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-test 0
pnpm check:agent-test-spelling 0
pnpm check:bash32-floor 0
pnpm check:cli-command-ids 0
pnpm check:cross-package-test-inputs 0
pnpm check:entry-guard 0
pnpm check:merge-driver 0
pnpm check:parse-guard 0
pnpm check:pnpm-filter-targets 0
pnpm check:ratchet-remedy-authority 0
pnpm check:watch-hint-literal 0
pnpm check:pm-dispatch-gates NOT 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:

Command Exit Verdict line
pnpm check:system-context-census 0 OK — 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-anchors 0 self-test + live, both clean
pnpm check:nul-bytes 0 plus a direct control-byte scan of all four changed files: clean
pnpm check:declared-population-live 0 156 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants