Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-protocol): the 422 issues[] descends invalid_key / invalid_element, so a rejected record key arrives with the rule it broke - #8817
Conversation
…422 issues[] (#8783) zodIssuesToMetadataIssues expanded invalid_union only, so a z.record/z.map key rejection reached Studio as the bare wrapper "Invalid key in record" while the rule it broke sat one level down in issue.issues — the shape #4971 named, and the one both packages/spec walks have descended since #5389. ObjectSchema.fields is a constrained-key record carrying the snake_case rule and object is in the builtin registry, so this was the commonest authoring mistake on the most-authored metadata type. The descent is additive and matches what the two spec walks were measured to emit for the card's own repro: the wrapper entry stays at index 0, the detail joins it on the same slot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
…alks + changeset (#8783) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
…tainer-issue-descent
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
…tainer-issue-descent
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8783
What was wrong
Zod raises a
z.record/z.mapkey rejection asinvalid_keyand az.mapelement rejection asinvalid_element, and in both cases the issue's ownmessageis a bare wrapper —"Invalid key in record"— with the real diagnosis one level down inissue.issues. That is structurally theinvalid_unionshape #4971 named: the prescription is produced, then dropped by a walk that reads only the top level.Both
packages/specwalks learned to descend those codes in #5389.zodIssuesToMetadataIssues— the walk behindsaveMetaItem's422 INVALID_METADATA(#5364) and the read path's diagnostics (#5598) — expandedinvalid_uniononly, so it stopped at the wrapper. Three walks over onesafeParse, two of them reaching the prescription and the Studio-facing one not.Reachable from ordinary authored metadata, not synthetic.
ObjectSchema.fieldsis a record whose KEY schema carries the snake_case rule (spec/src/data/object.zod.ts:1654), andobjectis in the builtingetMetadataTypeSchemaregistry (spec/src/kernel/metadata-type-schemas.ts:87). So the commonest authoring mistake on the most-authored metadata type — writingfirstNamefor a field key, exactly what an agent coming from JS naming writes — produced:The author was told a key was invalid and never told what a valid one looks like, so the next move was to guess. The declared message existed and was correct; it just did not reach anyone. The same save now answers:
The shape, and why it is this shape
Additive — matched to the walks that already worked, not chosen. The card's assumption was measured before anything was written, over the card's own repro:
Both keep the wrapper and add the detail, so this envelope does the same: the wrapper stays at index 0 — it is the only entry naming the slot the client sent, and Studio's designer keys on it — and the detail joins it on the same path. No entry that shipped before is removed or renumbered.
Targeted, not a widened walk. Only the two container codes open the descent; an
issuesarray hanging off any other code is still ignored (pinned),invalid_unionstill expands through the unchanged ranking (pinned), and the nesting bound now covers both descents at the same depth of 3. Container issues are deliberately not ranked the way union branches are: a union's branches are competing candidates, so they are selected between; a container has one inner schema, so every issue it raised is a true statement about the value and dropping any would drop a real diagnosis.The verdict is unchanged in every case — this moves what a refusal says, never whether it is one. Every rejection pin asserts
code+status+ nothing persisted alongside the message.Scope
⛔ No export was added and nothing was promoted from
packages/spec. Rewiring this third copy onto a shared export is #8660's option 2, a public-surface decision under #4001, and is not implied here. The code set is declared module-locally inprotocol.ts, exactly as the ranking and the limits already were. #8660'sunion-branch-policy.cross-package-parity.test.ts§1 — none of the seven policy symbols reachable from any public@objectstack/specentry point — is untouched and still green.That file's header recorded container descent as a deliberate exclusion ("this package's copy expands
invalid_uniononly"). This change makes that statement false, so the header is corrected and a §5 is added comparing the container descent across all three walks over three fixtures. §5 is kept separate from §2's corpus because §3 asserts that every §2 fixture really produces aninvalid_union— a container fixture there would force that vacuity guard to be loosened.Verification
Reverse verification, direction predicted in writing before it ran, then measured: take the container descent back out of
collectMetadataIssuesand 7 of the 14 new pins go red, 7 stay green — the predicted seven by name. The seven that stay green are the ones asserting facts about zod's raw issues or about shapes that are deliberately not descended, which is what makes the red half meaningful. The fix was committed first, so restoring it was agit checkoutof the path out of the branch — a commit that really exists, never a stash;git status --porcelainwas read back clean afterwards (noMM).Gate union re-run at final HEAD
97b0bc414—origin/mainmerged in a second time (the base had moved eight commits, three of them into this same file: #8747auditMetaItem, #8671diffMetaItem, #8786searchAll/renderTitle). The region was re-derived from the merged tree rather than trusted: every hunk of this diff sits in lines 503–790, and the nearest other landing site isauditMetaItemat:6187. Closure rebuilt after the merge; tree clean, nothing committed after the run.check:nul-bytes,check:cross-package-test-inputs,check:durability-log-level,check:filter-alias-parity,check:changeset-gate-self-tests,check:objectui-changeset,check:engine-double-contract,check:query-options-erasure,check:where-matcher— all PASScheck-adr-0087-registration,check-changeset-no-major,check-empty-changeset(--base origin/main) — all PASScheck:type-check-coveragePASS;check:type-check-debt(--self-test+--re-measure) PASS after rebuilding the closure — 33 entries re-measured, 1926 raw errors, none above its ceiling. Measured for this package specifically, since it hides its tests from tsc and a cleanpnpm typecheckwould prove nothing:@objectstack/metadata-protocolappears in neither the above-ceiling list nor the surplus list, so it measured exactly its recorded 63 — the +363-line test file added none. The one surplus reported is@objectstack/lint(-1), pre-existing onmainand untouched here.pnpm --filter @objectstack/metadata-protocol test— 95 files / 1407 tests passed (92/1387 before the merge; the merge brought three new test files)@objectstack/objectql209 files / 3664 tests,@objectstack/rest118 files / 1948 tests — all passedcheck:where-matcherwas run because the dispatch flagged it as firing on diff content rather than paths; it passes, and this diff adds no matcher.Generated by Claude Code