Uh oh!
There was an error while loading. Please reload this page.
fix(devx): defer a MIXED page to one side only when that provably loses nothing (#14064) - #14363
Merged
Merged
Conversation
…xed-file-regen-routing
baozhoutao
marked this pull request as ready for review
September 2, 2026 03:56
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, classified18 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:writeFileSync(path, rendered), thecreateSinkemit()/flush()pair, orwriteShards()), 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.packages/spec/spec-changes.jsonbuild-spec-changes.ts:134writeFileSync(SNAPSHOT, next)packages/spec/liveness/state-counts.mdliveness/build-state-counts.mts:135writesrenderedwhole; banner emitted by the generatorpackages/spec/authorable-surface/**build-schemas.ts, JSON shards written wholepackages/spec/authorable-surface.base.jsonbuild-schemas.ts:2172serialize-and-writepackages/spec/authorable-defaults/**build-schemas.ts, JSON shardspackages/spec/json-schema.manifest/**build-schemas.ts, JSON shardspackages/spec/api-surface/**build-api-surface.ts, JSON shardspackages/spec/api-surface-signatures.jsonbuild-api-surface.ts:164writeFileSync(SIG_SNAPSHOT, sigStr)packages/spec/src/meta-spelling/meta-url-data.generated.tsbuild-meta-url-spelling.ts:160writesnext; the read at:139is a staleness compare onlypackages/spec/export-origins/**build-export-origins.ts:372writeShards(ORIGINS_DIR, shardTexts)— writes whole, removes stalepackages/spec/declaration-map/**build-declaration-map.ts, JSON shardsdocs/protocol-upgrade-guide.mdbuild-upgrade-guide.ts:123writeFileSync(GUIDE, next); the read at:113is the--checkcomparedocs/audits/2026-07-...strictness-ledger.counts.mdbuild-strictness-ledger-counts.mts:43writeFileSync(countsPath, rendered)content/docs/references/**build-docs.tsemit()per page +manageDir(DOCS_ROOT)— the sink owns the tree and deletes strays. The 14 categoryindex.mdxpages carry no banner but are built line-by-line in the generator (mdx += ...,:875-:895)content/docs/permissions/system-context.mdxgen:system-context-censusischeck-system-context-census.mjs --fix, which reads the page and doeswriteFileSync(join(ROOT, PAGE), text)wheretextis 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.mdbuild-skill-references.ts:379emit(..., generateIndex(...))skills/objectstack-ui/contracts/react-blocks.contract.jsonbuild-react-blocks-contract.ts:136emit(OUT_JSON, ...)skills/objectstack-ui/references/react-blocks.mdbuild-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
isSystemthis page is about", 4 in "How the flag is set", 19/16/21/25/33/4 in the six table subsections, 13 in "WhatisSystemdoes 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:NNNNregex reads 24 of the 25 as prose-changing and is wrong):The single prose commit is the 18-line block from the near-miss the card is about. So:
content/docs/permissions/system-context.mdxis a generated anchor table that 12 of the last 20 main commits rewrite — and it is the one such artifact with nomerge=os-regenrouting #13646's win;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.mjscannot 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 %Band is the only actor in the system that can see both sides. So:scripts/regen-artifacts.mjs— the row declaresmixed: '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 exportblankAnchorLineNumbers(rawText), plusstart/endoffsets 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 amixedrow,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.git merge-fileinto%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
.gitattributesargued that deferring is safe becausecheck-system-context-census.mjsre-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.gitattributesand the row's comment inscripts/regen-artifacts.mjsnow 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.
os-regen-merge.shstep 2 unconditionally checked out main's side of every regen path, reverting a branch's own committed hand-deletions.gen:schema在 merge 未 commit 时运行,会把 authorable-surface 锚点倒退回旧 merge-base —— 生成器写入、门全绿、静默撤销 main 的锚点推进 #5370 —gen:schemarun before the merge was committed walked authorable-surface anchors back to the old merge-base: generator writes, gates green, main's advance silently undone.merge=os-regen— the driver keeps one side whole and drops the other's hand-written prose, and the census gate cannot see the loss #14064 — the driver keeps OURS whole and drops THEIRS whole on a MIXED page: prose deleted, ten doc gates green.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.
mixedis 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-drivergains two legs:reconcileMixedComparators()— everymixedrow names a comparator that exists and discriminates. The second half is load-bearing: a comparator that collapsed everything to one value would report every deferral lossless and silently restore this exact defect with every gate green, so each comparator is run against a pair it must call equal and a pair it must call different.endToEndMixed()— a realgit mergein a temp repo, both limbs: an anchors-only incoming change must still defer to OURS (provingcontent/docs/permissions/system-context.mdxis a generated anchor table that 12 of the last 20 main commits rewrite — and it is the one such artifact with nomerge=os-regenrouting #13646's win is intact), and an incoming prose change must not vanish silently.Ablation (the guard removed, mutation and restoration both proved on disk):
The new test reproduces #14064 on the unfixed driver and goes green on the fixed one. No
dist/is involved — these are plain.mjsscripts loaded from source, so there is no build leg to stale.7. Gates run — at
081b60c8c, the head of this branchDerived with
node scripts/pm/dispatch-gates.mjs --commandsafter mergingorigin/main(the first derivation warned STALE, namingscripts/pm/dispatch-gates.mjsitself as changed in the range). All exit codes captured by redirect-then-capture, never through a pipe.node scripts/check-ci-filter-parity.mjsnode scripts/check-cross-package-test-inputs.mjsnode scripts/check-shard-attestation.mjsnode scripts/check-test-completeness.mjsnode scripts/pm/bare-root-worklist.mjs --self-testpnpm check:agent-test-spellingpnpm check:bash32-floorpnpm check:cli-command-idspnpm check:cross-package-test-inputspnpm check:entry-guardpnpm check:merge-driverpnpm check:parse-guardpnpm check:pnpm-filter-targetspnpm check:ratchet-remedy-authoritypnpm check:watch-hint-literalpnpm check:pm-dispatch-gatesBeyond the derivation, because
scripts/doc-line-anchors.mjsis imported by the census gate and the derivation does not map that edge:pnpm check:system-context-censusOK — 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 controlpnpm check:doc-anchorspnpm check:nul-bytespnpm check:declared-population-live156 of 200 famil(ies) declare a path populationReconciliation —
node scripts/pm/dispatch-gates.mjs --ran ran.list(mode present on this base):The declared narrowing.
check:pm-dispatch-gatesexceeded 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 touchscripts/pm/dispatch-gates.mjs, so nothing here can change its behaviour.lint.ymlruns it on this PR unconditionally, which is where its result will come from.8. Notes
.gitattributesand three rootscripts/*.mjs— under the private root manifest. Nothing is published from any package, which is the textbookskip-changesetcase; the label is applied on this PR.scripts/check-system-context-census.mjs, so theROOT_DIR_WATCH_HINTSdeclaration is not a prerequisite here. Checked:03edd9ceis not an ancestor oforigin/mainas of this branch's merge, and this PR does not conflict with it.Generated by Claude Code