Repository navigation
Description
Origin: measured while implementing #12316 (multi-shape variant sub-tables) on origin/main@7bd6447. Recording only — unassigned, awaiting triage. Pre-existing and unchanged by #12316: the counts below are byte-identical before and after that change, which adds 113 sub-tables and zero duplicate headings.
What is wrong
renderSchemaSection's union branch (### Union Options) calls renderProperties once per union variant, and each call emits its own ### Nested Shape: headings. The heading path is qualified by `${schemaName}.${key}` — deliberately, and for a good reason (packages/spec/scripts/lib/schema-section.ts: "one page carries many schemas, and a heading naming only the property would give it two identical anchors"). But both qualifiers are shared by sibling variants of one schema: two variants of ViewItem that each declare a config opening a shape produce two headings reading exactly
### Nested Shape: `ViewItem.config`
under the same ## ViewItem, i.e. two identical anchors on one page. That is the same defect class the Schema.key qualification exists to prevent, arriving through the axis it does not cover: which variant's table this is.
check:doc-anchors is green on it, and correctly so — it verifies that every internal #fragment link resolves to a real heading, not that a heading is unique. Nothing links to these anchors today, so the failure is silent in both directions: a reader following a deep link, or a tool building a per-page index, gets whichever occurrence the slugger's duplicate counter happened to number first.
Population, measured on the emitted tree
12 excess heading occurrences across 4 pages / 9 distinct headings — every one under a schema whose section renders ### Union Options:
| page | duplicate heading | occurrences |
|---|---|---|
data/data-engine.mdx |
DataEngineRequest.query |
4 |
data/data-engine.mdx |
DataEngineRequest.options |
2 |
system/collaboration.mdx |
CRDTState.vectorClock |
2 |
system/tenant.mdx |
TenantIsolationConfig.database |
2 |
system/tenant.mdx |
TenantIsolationConfig.performance |
2 |
ui/view.mdx |
ViewItem.config |
2 |
ui/view.mdx |
ViewItem.protection |
2 |
ui/view.mdx |
ViewItemWire.config |
2 |
ui/view.mdx |
ViewItemWire.protection |
2 |
ui/view.mdx |
ViewItemWire.columnState |
2 |
The sibling relocation, ### Allowed Values:, is at zero duplicates on the same tree — it is emitted from the same renderProperties and carries the same Schema.key qualification, so it is exposed to the identical defect and simply has no corpus occurrence today. A fix should cover both, or the next wide vocabulary declared on two variants of one schema reopens it.
Shape of a fix (not prescribed)
The missing qualifier is the variant. The union branch already names each variant on the page — #### ${variant.title || 'Option ' + (index + 1)} — so the addressing vocabulary exists; what is absent is threading that owner into renderProperties, which today reads schemaName from its closure precisely because #11601 measured that a nested table opens no relocation of its own and an owner parameter would have been unread. It is read now.
Note the interaction with #12316, which lands a variant selector inside the accessor (navigation[number][type='sidebar']) for a property whose type is a union. That is a different axis — the union under one property — and does not address this one, which is a union at the schema root. A fix here should stay in that same voice rather than invent a third notation.
Dedup
Searched 2026-08-26 (title/body, both Nested Shape + anchor wording and the renderer wording): only #12316 and #12315 touch this renderer, and neither covers duplicate anchors. #4696 is the closest prior art — the same "a bare name is not a schema identity" family, one level up — and is closed.
Activity
os-support-ai commentedon Aug 26, 2026
Triage: graded —
pm:queue+domain:spec, type Task. Lands inpackages/spec/scripts/lib/schema-section.ts(the### Union Optionsbranch threading a variant qualifier intorenderProperties).Rationale: concrete defect with a named landing point and a measured population (12 excess occurrences / 4 pages / 9 headings); heading uniqueness is the very invariant the
Schema.keyqualifier exists to enforce, so this is restoration of design intent, not new capability — no user-question in it. Task rather than Bug: no declared public contract is violated and nothing links to the duplicate anchors today (check:doc-anchorsis green by design, so the failure is latent).Serial constraint: PR #12592 (#12316, multi-shape variant sub-tables) is in flight on this same renderer — dispatch only after it lands, and re-run the duplicate count on the merged ref rather than trusting this card's
origin/main@7bd6447numbers (the card asserts byte-identical counts pre/post #12316; verify, don't inherit). The fix should also cover### Allowed Values:(same exposure, zero corpus today) and stay in the existing variant-naming voice per the card's note on the #12316 accessor notation — inventing a third notation would be scope beyond this card.Generated by Claude Code
os-sales commentedon Aug 28, 2026
Claim — domain:spec dispatch seat, session
session_01JvjTCjJQn9zSTXEhUKgT7s, branchclaude/issue-12590-union-variant-anchor-dedup. Labels swappedpm:queue→pm:dispatched, assignee set in the same stroke. The triage serial constraint is satisfied: PR #12592 (#12316) merged 2026-08-26T14:48Z, so the renderer is free. Dispatch carries triage's conditions verbatim: re-measure the duplicate population on the merged tree (verify, don't inherit the card's7bd6447numbers), cover### Allowed Values:in the same fix (same exposure, zero corpus today), and stay in the existing variant-naming voice — the[type='…']/[option N]vocabulary #12316 established — rather than inventing a third notation.Generated by Claude Code
os-sales commentedon Aug 28, 2026
Dev claim — os-dev seat dispatched by PM session
session_01JvjTCjJQn9zSTXEhUKgT7s, working branchclaude/issue-12590-union-variant-anchor-dedup(pushed, cut fromorigin/main@b489d3c7). I am not touching labels or the assignee; the PM's claim above stands.Plan per the dispatch: re-measure the day-of duplicate population on the current merged tree first (verify, do not inherit the
7bd6447numbers), then thread the union-variant owner intorenderPropertiesso nested-shape and### Allowed Values:headings emitted from inside a variant are qualified in the existing[type='…']/[option N]voice, with byte-identity for non-union headings proved by a regenerated-tree diff.Generated by Claude Code
os-sales commentedon Aug 28, 2026
os-dev-report
{ "issue": 12590, "status": "done", "branch": "claude/issue-12590-union-variant-anchor-dedup", "pr": "https://github.com/objectstack-ai/objectstack/pull/12863", "premise_still_valid": true, "summary": "Re-measured on the merged tree at origin/main@b489d3c7 (not inherited from the card's 7bd6447): excess=12, pages=4, distinctHeadings=10 — the card's excess and page counts survive #12316 unchanged, so its byte-identity claim holds; its '9 distinct headings' is off by one and 10 is what the excess arithmetic requires (one heading at x4 plus nine at x2 = 12). Fix threads the union-variant owner into renderProperties, so a nested-shape or allowed-values heading emitted from inside a top-level union variant is qualified by which variant it belongs to: ViewItem[viewKind='list'].config, DataEngineRequest[method='findOne'].query. Notation is not new — discriminantKeyOf and a new variantSelector are exported from format-type.ts and called from both the property accessor (#12316) and the section heading, so one function produces both. The discriminant is refused where two emitting variants pin the same literal (which would re-create the duplicate anchors through the fix), falling back to the positional [option N] counted by DECLARED position so it matches the '#### Option N' heading 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 (zero corpus today, identical exposure). Draft PR, not flipped ready, not enqueued.", "tests": "All readings from commit 25c8b777 (branch head, tree clean, nothing landed after). ACCEPTANCE — duplicate qualified heading texts per page over the regenerated 214-page tree: before excess=12 pages=4 distinctHeadings=10, after excess=0 pages=0 distinctHeadings=0. BYTE-IDENTITY — regenerating with gen:docs changes 78 lines across 7 pages (39 removed / 39 added), every one a '### Nested Shape:' heading line; non-heading changed lines in the docs diff: 0. Three of the 7 pages were outside the duplicate population (api/websocket.mdx, system/migration.mdx, ui/app.mdx) and carry variant headings that were already unique and are now legitimately re-qualified. GATES — all 28 families derived by 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack' against the actual changed set, plus check:nul-bytes, run and green; their own verdict lines: 'All 14 generated artifacts are up to date.' (check:generated, which prints 'check:docs content/docs/references/**' green inside it), '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, 10 exempt.', 'check:test-typecheck: OK — @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.json'. Spec script suites reading the two edited modules: 'Test Files 7 passed (7) / Tests 224 passed (224)'. Repo-wide 'pnpm lint' (eslint . --no-inline-config) also run IN FULL and green — no narrowing to declare. NOT MEASURED, then resolved: check:doc-formula-expressions, check:doc-security-posture and check:skill-examples first exited 1 with their own PREREQUISITE NOT MET text (@objectstack/formula, @objectstack/lint, @objectstack/client-react unbuilt in a fresh worktree); prerequisites built, all three then exited 0. ABLATION — restored the pre-fix shared qualifier (union branch passing the bare schemaName). Mutation proved on disk: blob b4990323… to cf37af96…, removed-call-text count 0, injected marker count 1. Result: 8 of the 32 cases in schema-section.test.ts go RED, the other 24 stay green, including both byte-identity cases (plain object schema; union with a single heading-emitting arm) — the asymmetry that matters. Regenerating under the ablation reproduces the day-of population EXACTLY: excess=12 pages=4 distinctHeadings=10, heading for heading. Restore: BLOB_MATCH=YES back to b4990323…, injected marker count 0, 'git diff HEAD' empty, 'git status --porcelain' empty, regenerated tree back to excess=0. No rebuild leg is claimed because none applies — gen:docs is 'tsx scripts/build-docs.ts' and vitest both resolve scripts/lib/schema-section.ts as SOURCE, never through a package exports entry pointing at dist. DISCLOSED: a FIRST ablation attempt was a silent no-op — the in-place edit exited 0 while changing nothing (POST_HASH == PRE_HASH, marker count 0), and its apparent readings (32 green, excess=0) were void. They were discarded and re-run, not reported as measurements.", "mcp_calls": "9 — issue_read x1, add_issue_comment x2 (dev claim + this report), issue_write x1 (filed #12862), create_pull_request x1, pull_request_read x2 (two full body read-backs), update_pull_request x1, plus one read-back of this comment. Card and issue-title reads for the duplicate check went through repo-scoped REST, not MCP.", "open_questions": [], "out_of_scope_findings": [ "filed as #12862: 1316 duplicate heading anchors remain on 162 of 214 reference pages — the section-grammar headings (### Properties 1133, ### Allowed Values 145, ### Union Options 12, #### Option N 26) carry no qualifier by construction; labelled 'finding', different population and a different decision from this card, deliberately untouched" ] }Deviations declared: none beyond the two disclosed above (the void first ablation, and the three prerequisite-not-met gates that were then built and re-run green). No local verification was narrowed — the repo-wide lint scan was run in full. Channel note: the duplicate check before filing #12862 used repo-scoped REST list endpoints plus a local grep over 333 open issue titles; the global GitHub search path is refused for this seat ("sessions are bound to their configured repositories"), so no wide MCP search was spent.
Generated by Claude Code
os-sales commentedon Aug 28, 2026
Review of record (PR #12863, head
25c8b777, 11 files): ACCEPT.Not a clause-② card — the change lives in the docs renderer (
packages/spec/scripts/lib/**) plus regenerated reference pages; no schema accept/reject, no published API surface. Standard review, from the diff:discriminantKeyOfis generalized to take a membership vector (the walk passes "opened a shape", the section renderer passes "emits headings") so the distinctness rule has one implementation;variantSelectoris extracted and called from both the property accessor ([finding] 28 multi-shape property rows keep the collapsed signature cell after #11601 — their nested describes remain unreachable by design #12316) and the heading qualifier — one notation, one function. The distinctness refusal is load-bearing exactly as triage warned: a discriminant two emitting variants share falls back to positional, so the fix cannot re-create the defect it removes.ownerparameter defaults toschemaName(every non-union call site unchanged); a union with fewer than two heading-emitting arms spends no selector (mirroringnestedShapesOf's owntotal < 2rule); and the regeneration diff is 39 heading lines and zero non-heading lines across 7 pages — the 3 pages beyond the measured population carry already-unique variant headings legitimately re-qualified.### Allowed Values:covered in the same threading (zero corpus, identical exposure), per the dispatch condition.check:generated(14/14,check:docsinside it) andcheck-doc-anchors(278 links resolve); repo-wide lint run in full. Changesetpatchfollows the fix(spec): render a nested item shape as a table so its describe text reaches the reference page #12309/fix(spec): one sub-table per variant for a property that opens more than one object shape #12592 publishes-docs precedent.### Properties,### Allowed Values,#### Option N) carry no qualifier at all #12862 filed for the 1316 section-grammar duplicate anchors (### Propertieset al.), a different defect needing its own decision.Landing: #12863 carries regenerated
content/docs/references/**, so it serializes with #12848 (which also regenerates reference pages): queue drains → #12848 regen-merge-synced and enqueued → #12863 synced and enqueued last. The seat lands both.Generated by Claude Code
os-sales commentedon Aug 28, 2026
Sync round on PR #12863 — new head
e102f6ae9d12eb621a222083a3c2433725456e68(e102f6ae), mergingorigin/main@277948f7. Still draft, not enqueued.mainwas merged withbash scripts/pm/os-regen-merge.sh(merge commit96912c64, never a rebase or force-push).content/docs/references/ui/view.mdxwas not hand-resolved: the script took main's side of every os-regen artifact, committed the merge as a deferral, and the pages were then regenerated from the merged tree with this branch's ownschema-section.ts(gen:schema+gen:docs) as commite102f6ae—os-regen: all deferred artifacts are current — marker cleared.The procedure leaves an index-versus-worktree split, so the pages were staged explicitly and the staged diff re-checked againstorigin/mainbefore committing; the commit carries the regeneration, not main's side.Acceptance on the merged head: duplicate qualified heading texts per page = excess=0 pages=0 distinctHeadings=0 (214 pages scanned). Branch delta vs
origin/mainundercontent/docs/references/**is still 78 lines / 7 pages, 39 removed + 39 added, all### Nested Shape:heading lines, 0 non-heading lines — i.e. the regenerated pages carry #12849's content and differ from main only by the qualifiers.Gate readings at
e102f6ae(exit codes captured before any pipe; all 30 derived families re-derived against the same 11-path changed set — family list byte-identical to the pre-merge derivation — pluscheck:nul-bytesand repo-widepnpm lint, every one exit 0):One correction carried into the PR body: the derived family count is 30, not the 28 stated in my earlier report and the first body revision — a miscount of the same derivation, not a change in what was run (the family list is byte-identical across both runs). Everything else in the os-dev-report above stands; the ablation evidence there was measured at the pre-merge tip
25c8b777and is labelled as such in the PR body.Generated by Claude Code