Skip to content

refactor(spec): one union-branch selection policy, imported by both walks, with a shared-fixture parity pin (#8318) - #8659

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-8318-union-branch-policy-extract
Aug 14, 2026
Merged

refactor(spec): one union-branch selection policy, imported by both walks, with a shared-fixture parity pin (#8318)#8659
os-zhuang merged 3 commits into
mainfrom
claude/issue-8318-union-branch-policy-extract

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#8318

The union-branch selection policy had two sibling implementations inside packages/spec. It now has one, plus the mechanical enforcement the module headers were standing in for.

What moved

src/shared/error-map.zod.ts (the prose renderer, #4971/#5389) and src/api/zod-issues-to-fields.ts (the ADR-0114 D3 wire mapper, #8124) each declared the same policy: isKindMismatchOnly, carriesUnknownKey, selectUnionBranches, CONTAINER_ISSUE_CODES, the depth limit 3 and the branch cap 3. While the mapper lived in @objectstack/rest the fork was forced; #8124 removed that reason, leaving the two one directory apart with nothing but their headers binding them together.

All of it now lives in src/shared/union-branch-policy.ts, imported by both walks.

⛔ Package-internal, as the card requires. The module is in no barrel, and no public export moves: check:api-surface and check:export-origins are both green with a clean tree, and check:generated reports all 13 artifacts up to date — zero ledger drift. A test in the parity file asserts directly that none of the seven policy symbols is reachable from the shared or api entry points, so a future barrel line goes red before the ledger drift reaches a reviewer.

What deliberately did NOT move

The two walks stay separate implementations. The renderer owns the indent, the glyph, the (root) spelling and the trailing "... and N more branches rejected this value" line; the mapper owns the D3 code table and the {field, code, message} shape.

The {selected, omitted} wrinkle is handled, not papered over.selectUnionBranches returns both; the renderer prints omitted, and the mapper destructures selected alone at a commented line. The wire dropping the count is now a decision recorded in code rather than an absence you have to infer — and the parity test asserts the asymmetry in both directions (the prose line is present, no fields[] entry mentions it) instead of normalising it away silently.

The parity test

src/shared/union-branch-policy.parity.test.ts. One safeParse per fixture feeds both walks, and their outputs are compared pair for pair after a normalisation that removes exactly three formatting facts (the indent, the glyph, the (root) spelling) and nothing else.

The corpus covers every rule the card named: kind-mismatch drop, the all-kind-mismatch case, fewest-issues ranking, the unrecognized_keys tie-break, declaration-order determinism, the depth limit, the branch cap, and container descent for both invalid_key and invalid_element (#5389), plus a container nested under a union. A guard case asserts the corpus really produces unions, so the parity assertions cannot pass vacuously.

It is deliberately end-to-end rather than a unit test of the extracted module: a unit test would pass trivially now that there is one implementation. What can still regress is a future author re-inlining a copy, and that is what this catches.

Reverse verification

Prediction stated first: re-forking the cap in the wire mapper should redden only the fixtures that select more than one branch, not the whole file.

Measured, on the committed tree, by slicing the mapper's selection to one branch:

  • 3 red, 25 green — exactly the three multi-branch cases (declaration order breaks a full tie, the branch cap keeps three of five tied branches, both keep exactly the same three branches, cap included). Direction as predicted.
  • The pre-existing spec-side tests — src/api/zod-issues-to-fields.test.ts and src/shared/error-map.test.ts, 42 tests — stayed green under the same re-fork. That is the measurement that says this file is new coverage rather than a restatement of what was already pinned.

Restored with git restore --source=HEAD and proven byte-identical via git hash-object against HEAD (232d5349c8…).

Behaviour

Unchanged for every issue zod produces. The ranking, both limits and the container-code set are byte-identical to what each walk applied before, and the full spec suite passes unmodified.

One deliberate widening, stated plainly rather than buried: the shared policy reads a missing or non-array path as the root — the wire mapper's already-shipped normalisation, now applied to the renderer too. The renderer previously threw a TypeError on such an issue object, so nothing that used to produce output produces different output. No value satisfying the renderer's own ZodIssueMinimal type (whose path is a required array) is affected, and zod never emits one that is not.

Verification

All at 6097b7846 (the head this PR pushes; the union was run after the final commit).

@objectstack/spec suite398 files / 10555 tests passed
@objectstack/rest suite (the wire consumer)115 files / 1903 tests passed
@objectstack/spec typecheckgreen, incl. check:scripts-typecheck + check:test-typecheck (debt ledger unchanged)
check:generatedall 13 artifacts up to date — no ledger drift
check:dual-source-exports, check:exported-anygreen (4912 names / 16 entry points, unchanged)
named gatescheck:doc-formula-expressions, check:error-code-casing, check:merge-driver, check:spec-parsed-alias, check:type-source-resolution, check:nul-bytes — all green
re-derived via dispatch-gates.mjscheck:objectui-changeset, check:query-options-erasure, check:type-check-coverage, check-changeset-no-major, check-empty-changeset — all green

Two gates are red on pristine origin/main as well, verified in a comparison worktree at 189a73225 and already filed by others: #8654 (check-changeset-no-major --self-test control) and #8658 (check-adr-0087-registration input assertion). Both are the post-release-cut drained-.changeset condition, not this diff. check-dev-prereqs needs the whole workspace built (43 of 67 packages unbuilt here) and is a property of the worktree, not of this change.

Synced with origin/main at 189a73225 (clean merge, no packages/spec incoming).


Generated by Claude Code

…alks (#8318)
`shared/error-map.zod.ts` (the prose renderer) and `api/zod-issues-to-fields.ts`
(the ADR-0114 D3 wire mapper) carried the same selection policy as two separate
implementations: kind-mismatch drop, fewest-issues ranking, `unrecognized_keys`
tie-break, declaration-order determinism, depth limit 3, branch cap 3, and the
`invalid_key` / `invalid_element` container codes. #8124 moved the mapper into
this package, so the historical reason for the fork is gone.
The policy now lives in `src/shared/union-branch-policy.ts`, package-internal
and absent from every barrel — no public export moves, and `api-surface/` /
`export-origins/` are untouched. The two walks stay separate: the renderer owns
the prose and the "... and N more branches" line, the mapper owns the D3 code
table and the `{field, code, message}` shape. `selectUnionBranches` returns
`{selected, omitted}`; the mapper destructures `selected` alone at a commented
line, making the wire's omission a recorded decision rather than an absence.
`src/shared/union-branch-policy.parity.test.ts` drives both walks from one
`safeParse` per fixture and compares their outputs pair for pair, over a corpus
covering every rule of the policy plus container descent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5tUwGM3LQoqErTfkvRW7W
@vercel

vercelBot commented Aug 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 14, 2026 12:40pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/permissions/system-context.mdx(via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review August 14, 2026 12:40
@os-zhuang
os-zhuang enabled auto-merge August 14, 2026 12:40
@os-zhuang
os-zhuang added this pull request to the merge queueAug 14, 2026
Merged via the queue into main with commit 44bc51dAug 14, 2026
28 of 29 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8318-union-branch-policy-extract branch August 14, 2026 13:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: union-branch selection policy now has two sibling implementations INSIDE one package (error-map renderer vs the D3 structural mapper)

2 participants

@os-zhuang@claude