Skip to content

fix(spec): qualify a union variant's nested-shape and allowed-values headings by which variant they belong to - #12863

Merged
os-sales merged 5 commits into
mainfrom
claude/issue-12590-union-variant-anchor-dedup
Aug 28, 2026
Merged

fix(spec): qualify a union variant's nested-shape and allowed-values headings by which variant they belong to#12863
os-sales merged 5 commits into
mainfrom
claude/issue-12590-union-variant-anchor-dedup

Conversation

@os-sales

@os-sales os-sales commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Fixes #12590

renderSchemaSection's ### Union Options branch calls renderProperties once per variant, and both halves of the Schema.key qualifier those headings carry are shared by every sibling variant of one schema. Two ViewItem variants each declaring a shape-opening config therefore emitted ### Nested Shape: \ViewItem.config`` twice — two identical anchors on one page, on the very heading whose qualifier exists to prevent exactly that.

Day-of population (re-measured, not inherited)

The card measured origin/main@7bd6447. PR #12592 (#12316) has since merged on this same renderer, so the population was re-measured on the then-current merged tree, origin/main@b489d3c7, by counting identical ### Nested Shape: / ### Allowed Values: heading texts per emitted page under content/docs/references/**:

excess=12  pages=4  distinctHeadings=10   (214 pages scanned)
page count heading
data/data-engine.mdx x4 ### Nested Shape: `DataEngineRequest.query`
data/data-engine.mdx x2 ### Nested Shape: `DataEngineRequest.options`
system/collaboration.mdx x2 ### Nested Shape: `CRDTState.vectorClock`
system/tenant.mdx x2 ### Nested Shape: `TenantIsolationConfig.database`
system/tenant.mdx x2 ### Nested Shape: `TenantIsolationConfig.performance`
ui/view.mdx x2 ### Nested Shape: `ViewItem.config`
ui/view.mdx x2 ### Nested Shape: `ViewItem.protection`
ui/view.mdx x2 ### Nested Shape: `ViewItemWire.config`
ui/view.mdx x2 ### Nested Shape: `ViewItemWire.protection`
ui/view.mdx x2 ### Nested Shape: `ViewItemWire.columnState`

The card's excess (12) and page (4) counts survive #12316 unchanged, which is what its byte-identity claim asserted. Its "9 distinct headings" does not: the measured value is 10, and 10 is the count the excess arithmetic requires (one heading at x4 contributes 3, nine headings at x2 contribute 1 each = 12). Reported as a correction to the card, not as a change in the defect.

Zero pages collide on ### Allowed Values: today — 24 such headings exist across the tree, none duplicated. That half is covered anyway; see below.

The qualifier, and why it invents nothing

The heading's owner is now the schema name plus the variant selector, in the accessor grammar #12316 established and the page already prints:

before   ### Nested Shape: `ViewItem.config`          (twice on ui/view.mdx)
after    ### Nested Shape: `ViewItem[viewKind='list'].config`
         ### Nested Shape: `ViewItem[viewKind='form'].config`

before   ### Nested Shape: `DataEngineRequest.query`  (four times on data/data-engine.mdx)
after    ### Nested Shape: `DataEngineRequest[method='find'].query`
         ### Nested Shape: `DataEngineRequest[method='findOne'].query`
         ### Nested Shape: `DataEngineRequest[method='count'].query`
         ### Nested Shape: `DataEngineRequest[method='aggregate'].query`

The spelling is not re-implemented here. format-type.ts's discriminantKeyOf and a new variantSelector are exported and called from both positions, so the segment stamped into a property accessor (navigation[number][type='sidebar']) and the segment qualifying a section heading are produced by one function. A page carries one variant notation, not two.

Three properties of that grammar are load-bearing and are pinned:

  • A discriminant is only usable when every heading-emitting variant pins a DISTINCT literal. discriminantKeyOf already refuses a key two variants pin to the same value; qualifying with such a key would re-create the duplicate anchors through the fix itself. Where it refuses, the fallback is [option N].
  • [option N] counts DECLARED position, including arms that emit no headings — so the number is checkable against the #### Option N heading three lines above it.
  • A union with fewer than two heading-emitting arms spends no selector, mirroring the walk's own "fewer than two shapes" rule. That is what keeps everything outside the duplicate population byte-identical.

### Allowed Values: is covered by the same threading. It has zero corpus today and is the identical exposure — one qualifier shared by every sibling variant — so closing only the half with a measured population would leave the next wide vocabulary declared on two variants to reopen it.

Verification

Head is e102f6ae, which merges origin/main@277948f7. Acceptance, byte-identity and the whole gate set were re-run on that merged head; the ablation below was run on the pre-merge tip 25c8b777 and is stated as such.

Acceptance measurement — duplicate qualified heading texts per page over the regenerated 214-page tree, at e102f6ae:

before (origin/main@b489d3c7)   excess=12  pages=4  distinctHeadings=10
after  (e102f6ae)               excess=0   pages=0  distinctHeadings=0

Byte-identity for everything else. This branch's delta against origin/main under content/docs/references/** is 78 lines across 7 pages: 39 removed, 39 added, every one of them a ### Nested Shape: heading line. Non-heading changed lines: 0. That figure is unchanged across the merge, which is the point — the regenerated pages carry main's content and only the heading qualifiers differ. The 3 pages beyond the duplicate population (api/websocket.mdx, system/migration.mdx, ui/app.mdx) carry variant headings that were already unique and are now re-qualified — legitimately, since they are emitted from inside a union variant. No heading outside a union variant moved.

Reverse verification (ablation), run at 25c8b777. The qualifier threading was ablated — the union branch restored to passing the bare schemaName, i.e. the pre-fix shared qualifier:

  • mutation proved on disk: blob b4990323…cf37af96…; occurrences of the removed call text 0, injected marker 1
  • 8 of the 32 cases in schema-section.test.ts go RED, the other 24 stay green — including both byte-identity cases (a plain object schema, and a union with a single heading-emitting arm), which is the asymmetry that matters
  • regenerating under the ablation reproduces the day-of population exactly: excess=12 pages=4 distinctHeadings=10, heading for heading
  • restored with git checkout HEAD scoped to the mutated file (spelled with the two-dash pathspec separator): blob back to b4990323… (BLOB_MATCH=YES), injected marker count 0, git diff HEAD empty, git status --porcelain empty, and the regenerated tree back to excess=0

An earlier ablation attempt was a no-op — the in-place edit reported success while changing nothing (POST_HASH == PRE_HASH, marker count 0). Its readings were discarded, not reported; the numbers above are from the run whose mutation is proved on disk. No rebuild step is claimed for this ablation because none applies: gen:docs is tsx scripts/build-docs.ts and vitest both resolve scripts/lib/schema-section.ts as source, not through a package exports entry pointing at dist.

Gates, at e102f6ae. All 30 families derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack against the actual 11-path changed set, plus check:nul-bytes, run and green. (An earlier revision of this body said 28; that was a miscount of the same derivation, corrected here — the family list itself is byte-identical across both runs.) Their own verdict lines, for the load-bearing ones:

✓ All 14 generated artifacts are up to date.          (check:generated — includes ✓ check:docs  content/docs/references/**)
✅ check-doc-anchors: 278 internal #fragment link(s) across 408 source file(s) all resolve to a real heading
✓ variant/doc gate: 18 discriminated union(s) — 8 governed (every variant mentioned in a bound doc), 10 exempt.
check-nul-bytes: OK (scanned 7153 text file(s) -- 7153 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).
Test Files  7 passed (7) / Tests  224 passed (224)   (the 7 spec script suites reading the two edited modules)

pnpm lint (repo-wide eslint . --no-inline-config) also run in full and green — no narrowing to declare.

Three gates first exited 1 with PREREQUISITE NOT MET (@objectstack/formula, @objectstack/lint and @objectstack/client-react unbuilt in a fresh worktree). Their own text states nothing was measured; the prerequisites were built and all three then exited 0. Recorded here so the first reading is not mistaken for a finding.

Merge with main

main moved under this branch (#12849, #12838, #12837, #12854, #12737), and #12849 also regenerated content/docs/references/ui/view.mdx. Landed via bash scripts/pm/os-regen-merge.sh — merge commit, never a rebase or force-push. Generated pages were not hand-resolved: the script takes main's side of every os-regen artifact, commits the merge (the pre-commit hook records that as a deferral), and the regeneration then runs from the merged tree with this branch's own renderer, as its own commit e102f6ae. The deferral marker cleared on that commit (os-regen: all deferred artifacts are current — marker cleared.). The index-versus-worktree split the procedure leaves behind was staged explicitly and the staged diff re-checked against origin/main before committing, so the commit contains the regeneration rather than main's side.

Notes

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e84bbf6dd37734802027cacd2870347c1a63045fpackageMentionDocs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants