Uh oh!
There was an error while loading. Please reload this page.
test(metadata-protocol): pin the THIRD union-branch policy copy against @objectstack/spec (#8660) - #8782
Merged
Merged
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckNo hand-written docs reference the 0 changed package(s). ✅ |
This was referenced Aug 15, 2026
…ted root namespace import (#8660)
hotlong
marked this pull request as ready for review
August 15, 2026 02:03
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#8660
The union-branch selection policy has three implementations. #8318 (PR #8659) consolidated the two inside
packages/specand pinned them against each other. The third —zodIssuesToMetadataIssuesinpackages/metadata-protocol/src/protocol.ts, whose own header calls itself "the THIRD copy" — was structurally out of that consolidation's reach, because the shared module is deliberately not a public export (#4001). This adds the mechanical parity enforcement it was missing.Option 1 exactly as ruled: one new test file plus a changeset. No production line changes, no new export, and
protocol.tsis not touched.What it does
packages/metadata-protocol/src/union-branch-policy.cross-package-parity.test.tsdrives both sides through their public surfaces over one shared fixture corpus:formatZodIssue@objectstack/spec(root barrel)zodIssuesToFields@objectstack/spec/apizodIssuesToMetadataIssuessaveMetaItem's 422 (#5364) and the read path's diagnostics (#5598)One
safeParseper fixture feeds all three, and their outputs are compared as ordered(path, message)pairs after a normalisation that removes exactly three formatting facts — the renderer's indent, its glyph, and its(root)spelling of the empty path — and nothing else.The two halves are genuinely independent, and §1 proves it structurally
A parity test whose halves share a code path proves nothing. §1 asserts that not one of the seven policy symbols is reachable from any public entry point of
@objectstack/spec(root,/api,/shared), soprotocol.tscannot be importing the shared module even if a future author wanted it to — its own copy is the only thing it can run. Measured on the built artifact: all three entry points export none of them.§1 is also the tripwire for the other outcome. If the policy is ever promoted to a public export — the option this card ruled out of scope — §1 goes red and says the next move is to rewire this package onto the shared module, not to leave a third copy behind a green parity test.
A companion case asserts all three imports are really functions, so the comparison cannot be green because both walks emitted nothing.
The corpus
One fixture per element of the policy the card names, each carrying a hand-authored
expectedPaths— a third statement of the rule, so both sides drifting the same way still fails, and so each fixture's name is checkable rather than decorative:z.union([z.string(), z.number()])handed an object: nothing selectedunrecognized_keystie-break§3 guards against vacuity: every fixture must really produce an
invalid_union, six must reach below the top-level issue, and every named policy element must appear in some fixture's rule string.Reverse verification — the test fails when the copies diverge
Predictions stated before each run; both ablations were made on the spec side, which is the direction the card describes (a spec-side tweak reaching two consumers and silently not the third).
metadata-protocolresolves@objectstack/specthroughexportstodist/, so each ablation was rebuilt and proved present in the artifact withscripts/ablation-dist-preflight.mjsbefore the run was allowed to mean anything.A — invert the
unrecognized_keystie-break. Predicted: exactly one fixture reddens, the tie-break one; the others are decided by count, by there being one informative branch, or by a uniform rank. Measured: 1 failed, 16 passed, and the one failure is the predicted case, with the spec side selecting the loose branch and this package the strict one.B — cap at 2 instead of 3. Predicted: the two fixtures that select three branches, plus §4's omission-line assertion (its count moves from 2 to 3). Measured: 3 failed, 14 passed, exactly those three.
Restored both times with
git checkout HEAD -- packages/spec/src/shared/union-branch-policy.ts, proven byte-identical (git hash-object=53fc1a1164= the HEAD blob), rebuilt, and the marker proven absent from all 181 built files with--absentbefore re-running green.What is deliberately NOT compared, and why
invalid_key/invalid_element, 休眠:invalid_key/invalid_element把真实 issue 挂在issue.issues上,union 家族的三个消费者一个都不下降 #5389). The spec walks descend those; this package's copy expandsinvalid_uniononly. That is a difference in reach, not in the ranking, and it is not what union 分支里的 unknown-key 处方永远到不了作者:zodIssuesToFields只映射顶层 issue,失败的 union 只剩Invalid input#5014 bound together — so the corpus stays where all three make the same claim. Measured and filed separately as metadata-protocol's 422 issues[] does not descend invalid_key / invalid_element containers, so a record-key rejection arrives as the bare "Invalid key in record" while the CLI and the wire both show the prescription #8783; it is not addressed here.codevocabulary. Raw zod codes here, the ADR-0114 catalog on the wire (元数据保存的 422 也丢掉 union 分支处方:一个 view 保存失败只回一条path:"" message:"Invalid input",Studio 无字段可高亮 #5364 records why they are not aligned). §4 asserts the divergence in place rather than normalising it away, so it stays a decision instead of decaying into an accident.The
distcaveat, stated in the file itself@objectstack/metadata-protocolis one of the 61 packages registered as still resolving a workspace dep throughdist/inscripts/check-test-source-alias.mjs, so the spec half of every assertion is the built artifact. That is the honest reading for a cross-package pin — it is what this consumer links against, andturbo.jsonhastestdependsOn^build— but it means perturbingpackages/spec/srcalone will not redden this file. The module header says so explicitly, so the next person to attempt an ablation does not lose the lap.Changeset
patch, deliberately not a skipped changeset, on the same reasoning aslegacy-unique-guard-attribution(merged earlier today for a test-only PR): the 422 envelope this pins is a published contract of@objectstack/metadata-protocol, and a CHANGELOG reader should be able to see when its verdict acquired mechanical protection against drifting from the spec's. No production line changes and every assertion is green onmainbefore this lands, so the floor is right rather than absent.Verification
All at
d8f950e3e, the first round's head; the gate union was run after that round's final commit, on the fully rebuilt workspace. The patch round below re-ran everything at the current head — read that table for the state of this PR as it stands.@objectstack/metadata-protocolsuitescripts/pm/dispatch-gates.mjsagainst the real changed paths)check:nul-bytes,check:cross-package-test-inputs,check:durability-log-level,check:objectui-changeset,check:changeset-gate-self-tests,check:empty-changeset,check:query-options-erasure,check:type-check-coverage,check-adr-0087-registration,check-changeset-no-major— all greencheck:test-source-alias(added by me, not in the derived list: this diff adds cross-package imports to a test)check:type-check-debt(the ratchet half,--re-measureon the built workspace)On the type-check question specifically:
packages/metadata-protocol/tsconfig.jsondoes not exclude**/*.test.ts— itsincludeissrc/**/*, so the new file is inside the program. What the package lacks is atypecheckscript at all: it sits in the DEBT ledger at 63 frozen raw errors, andcheck:type-check-debt --re-measureis what actually runstscover it. Re-measured with this file present: exactly 63, zero of them naming it, and no surplus recorded against this package — so the file type-checks clean rather than hiding inside an allowance.Synced with
origin/mainat84cb121eb(clean merge; the merge brought newpackages/speccommits, so the closure was rebuilt before the final test run rather than reporting on the pre-merge artifact).Patch round — §1 kept its claim, the restricted import went
The first push tripped the repo's
no-restricted-importsrule: §1 read the root barrel asimport * as SpecRoot from '@objectstack/spec', and a static namespace binding on the root keeps all fifteen domain namespaces reachable — the ~1.2GB RSS regression that rule exists to prevent.eslint . --no-inline-configmeans there is no per-site opt-out, by design.§1 was not weakened. It still asserts that none of the seven policy symbols is reachable from any public entry point, root included. What changed is how the export surface is read: the three entry points are now loaded through
await import(...)inside the case, and a helper reduces each namespace to the handful of names §1 asks about and lets the object go. Verified against the real linter rather than assumed — the rule'spaths/importNamesform does not match anImportExpression, and the dynamic spelling lints clean.That is not an evasion dressed up as a fix. The file already loads the root module for
formatZodIssue, so nothing extra is pulled in; what the dynamic form drops is precisely the long-lived namespace binding the rule targets, which makes the new spelling strictly less retentive than the one it replaces while making the identical claim about the identical surface.Proof that §1 can still fail (prediction first, as before): adding
export { selectUnionBranches } from './union-branch-policy';topackages/spec/src/shared/index.tsshould make the symbol reachable from@objectstack/spec/sharedonly — the root barrel does not re-export that barrel — so exactly one of §1's three cases should redden.Measured, after rebuilding
@objectstack/specand confirming reachability on the built artifact directly ('selectUnionBranches' in mod: rootfalse,/apifalse,/sharedtrue):/sharedcase, failing withexpected [ 'selectUnionBranches' ] to deeply equal []. Direction as predicted.git checkout HEAD -- packages/spec/src/shared/index.ts, proven byte-identical (git hash-object=f400f4b514= the HEAD blob), rebuilt, and reachability re-probed asfalsebefore re-running green.Note on the ablation marker: grepping
distforselectUnionBranchesproves nothing here — the symbol is bundled as an internal either way. Reachability had to be probed through the module namespace, which is the thing §1 actually asserts.Verification (patch round)
All at
b4706c135, the head this pushes; every gate below was re-run after the final commit, ratchets included.pnpm lint(eslint . --no-inline-config, repo-wide)@objectstack/metadata-protocolsuitecheck:type-check-debt(ratchet,--re-measure)tsc --noEmitover the packagecheck:nul-bytes,check:cross-package-test-inputs,check:test-source-alias,check:query-options-erasure,check:type-check-coverage,check:empty-changesetorigin/mainre-checked at push time: still84cb121eb, already merged — the merge is current, not stale.Generated by Claude Code