Skip to content

fix(spec): render a nested item shape as a table so its describe text reaches the reference page - #12309

Merged
os-litant merged 3 commits into
mainfrom
claude/issue-11601-nested-describe-rendering
Aug 25, 2026
Merged

fix(spec): render a nested item shape as a table so its describe text reaches the reference page#12309
os-litant merged 3 commits into
mainfrom
claude/issue-11601-nested-describe-rendering

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes#11601

The defect

build-docs.ts renders a property whose type is an inline object as a one-line signature in the Type cell — and that cell has no description column. Every .describe() an author wrote on a key of that shape was therefore unreachable from the reference page: not truncated, not marked, absent.

The card's specimen, content/docs/references/ui/component.mdx:

| **items** | `{ label: string \| Record<string, string>; icon?: string; visibleWhen?: string \| object; value?: string; … }[]` | ✅ | |

An empty Description cell — while packages/spec/src/ui/component.zod.ts:699 gives that item-level visibleWhen a ~600-character contract note whose whole point is that its evaluation environment is not the page-component visibleWhen of the same name.

The loss was invisible from both sides. check:docs compares generated output with committed output, so it is green forever on prose neither side contains — measured on main@0b478e1, adding a .describe() to a nested item key produced a zero-linegen:docs diff.

Population, enumerated before a rendering was chosen

Measured over the 1585 emitted JSON Schema documents, counting property rows the section renderer actually emits (main object table plus union-variant tables), and shapes at the one depth level a cell opens:

readingcount
property rows rendered across the tree8615
rows opening a nested shape1293
... of those, rows whose shape has at least one described key1208
distinct schemas affected566
categories affected13 of 14
described nested keys (the rows this PR adds)7502
authored describe characters that reached no page~473 KB
reach path: direct object child / array item / Record value / union variant735 / 374 / 63 / 36

The population is large enough that a sub-table (rather than expandable rows or inline prose) is the only shape that stays inside the page grammar this renderer already has.

What is rendered now

A property that opens exactly one shape, and whose shape has at least one described key, gets a ### Nested Shape: table directly under the Properties table — the same position, addressing and heading level ### Allowed Values: has used since #6225, so the page gains no second grammar.

### Nested Shape: `PageTabsProps.items[number]`
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **label** | `string \| Record<string, string>` | ✅ | Display label — the default-language string, or an inline locale map … |
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; … }` | optional | Visibility predicate (CEL) — the whole tab (header + panel) is omitted when FALSE … ⚠️ NOT the same environment as page-component `visibleWhen` … |

The heading names the shape with a TypeScript indexed accessoritems[number] for an array element, [string] for a Record value, composed left to right for a wrapper stack. That is a real spelling of the type, not a sigil invented for the docs, which matters in a table that already carries two omission markers ( and … +N more).

Tombstoned keys are rendered in a nested table, unlike in the cell above it: retiredKey() puts the whole [REMOVED] migration prescription in description, and a signature has no column to carry it. format-type.ts says as much where it drops them from { … } — this table is the row such a shape never had.

Four bounds, each measured rather than chosen

  1. One level, matched to the SHAPE_DEPTH_LIMIT budget a cell already spends. A nested table opens no table of its own, so page depth stays a fact about the renderer rather than about how deeply an author happened to nest a schema.
  2. Only where there is text to publish. A shape whose keys carry no describe text keeps its cell — the cell already states the keys and their types, so a table there would restate it in more space. 1208 of the 1293 shape-opening rows qualify.
  3. A union of two or more object shapes keeps its cell. There is no single "the shape of this property" to name; a heading would need a variant index, i.e. a second addressing notation. Same reason formatPropertyType refuses to relocate a vocabulary out of Enum<…>[]. 28 of 1293 rows are in that state.
  4. A nested table does not relocate vocabularies — it elides them the way a { … } summary does (INLINE_ENUM_WIDTH_LIMIT, bare ). It is a second position for those keys, and gen:docs 顶层长枚举仍是单个 6092 字符的表格单元格 —— ### Allowed Values 项目符号只对「整个 schema 是枚举」生效,对「某个属性是枚举」从不生效 #6225's relocation budget is only spendable where the vocabulary's authoritative copy lives. Measured by regenerating without this rule: the 288-member ApiError.code vocabulary was re-listed under every nested error shape — 20,260 bullet lines across the tree, api/metadata.mdx alone +6097 — for a vocabulary already published in full on api/errors.mdx. Every PR that registers an error code regenerates the same 11 reference pages — the ledger is a serialization point, and a PR carrying a new code is in a race it keeps losing #9182 took the count out of this position for a smaller version of the same cost.

The regenerated tree is purely additive

143 files, +14195 / -118 lines. Every one of the 38177 pre-existing lines is still present byte for byte — proved by a line-multiset comparison of the tree at origin/main against the regenerated tree (comm -23 over both sorted: 0 lines present before and absent after). The 118 are re-ordering around the inserted sections, not removal. The ## SchemaName heading multiset is unchanged (identical md5).

Largest movers: ui/view.mdx +1422, api/protocol.mdx +968, data/data-engine.mdx +653, ui/component.mdx +276.

Considered and not taken

Deduplicating identical sub-tables within a page (render once, point later occurrences at the first). Measured: 192 of the 1180 emitted blocks are same-page duplicates and a dedupe would save 2926 of the 14195 added lines — 21%. Rejected because it costs a second addressing notation ("see the shape of X") and breaks the adjacency rule the ### Allowed Values: relocation established: a relocation lands immediately under the table whose cells it completes.

Linking a nested shape to the published schema it structurally matches. 860 of the 1293 occurrences fingerprint-match a schema that has its own section elsewhere, and linking instead of re-tabling would cut the added rows from 7502 to 1435. Rejected for this card: matching on key names plus describe text is not schema identity, and a confident link to the wrong schema is the defect #4696 was filed on. It is a bigger design with its own correctness risk, worth its own card if the tree growth ever becomes a problem.

Verification — all at 044ceb1

Every command below was run through scripts/pm/os-verify-lock.sh; the verdict quoted is the gate's own line, never a bare $?.

commandverdict
pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2Test Files 428 passed | 1 skipped (429) · Tests 11416 passed | 1 skipped (11417)
pnpm --filter @objectstack/spec run check:docs✅ 229 generated files in sync with packages/spec
pnpm --filter @objectstack/spec run check:generated✓ All 14 generated artifacts are up to date.
pnpm --filter @objectstack/spec typecheckcheck:test-typecheck: OK — … 55 file(s) / 263 error(s) held in test-typecheck-debt.json (unchanged, shrink-only)
pnpm lint (eslint . --no-inline-config, whole repo)VERDICT command-exit 0 · held the lock 98s
pnpm --filter @objectstack/spec run check:skill-examples✅ 256 prose examples type-check across 3 surface(s)

Plus the derived gate union for this change set — node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, re-derived from the real diff after the final commit — all exit 0: check:cross-package-test-inputs, check:doc-anchors, check:doc-authoring, check:doc-formula-expressions, check:doc-security-posture, check:docs-audit-scope, check:docs-redirects, check:empty-state, check:liveness, check:merge-driver, check:published-files, check:published-readme-links, check:quick-reference-counts, check:react-page-adapter-contract, check:role-word, check:slot-lookup, check:strictness-ledger, check:test-source-alias, check:type-source-resolution, check:variant-docs, check-ci-filter-parity, check-doc-frontmatter, check-doc-route-spelling, check-docs-section-name, check-plugin-teardown-shape, check-section-landing-index, docs-audit/check-affected-docs, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:type-check-coverage, check:nul-bytes, and the six changeset families.

Two gates were not measured locally and are declared rather than claimed: check:dev-prereqs and check:type-check-debt --re-measure both refuse on an unbuilt workspace closure (60+ packages), and CI builds before it gates.

Reverse verification

Run from the committed state, with a restore trap, and with the mutation proved on disk before anything was read: nestedShapeOf neutered to return null (the pre-fix answer) with every other line of the fix in place.

  • mutation landed: marker occurrences 0 → 1, anchor still 1, git diff --stat 1 insertion.
  • direction: red, as predicted. scripts/nested-shape.test.tsTests 14 failed | 8 passed (22). The 8 survivors are exactly the refusal cases (a leaf, a tombstone, a named/self ref, a cycle, a two-object union) — they answer null either way, and that asymmetry is why the pins are written on both directions.
  • check:docsexit 1, content/docs/references/ is out of date with packages/spec. The card's zero-diff measurement is reversed: the pages now depend on this renderer.
  • restore leg: marker occurrences back to 0, tree clean, Tests 22 passed (22), check:docs back to ✅ 229 generated files in sync.

Changeset

@objectstack/spec: patch — the reference pages are user-visible and gain ~473 KB of authored documentation. Precedent: the two prior build-docs.ts changesets (#11260, #11482) are both spec patches. skip-changeset would have been wrong here: this PR changes what the docs site publishes.


Generated by Claude Code

… reaches the reference page
`build-docs.ts` collapsed an inline object property into a one-line signature
cell that has no description column, so every `.describe()` on a key of that
shape was unreachable from the page. Measured: 1208 property rows across 566
published schemas carry 7502 described nested keys (~473 KB of prose) that
reached no page, and adding such a describe produced a zero-line `gen:docs`
diff.
`nestedShapeOf` (beside `renderType`, so the two agree about what one shape
level is) names the single shape a cell opens and the TypeScript indexed
accessor that selects it. `renderSchemaSection` renders it as a
`### Nested Shape:` table directly under the Properties table, in the position
and grammar `### Allowed Values:` has used since #6225.
Bounded: one level; only where a key carries describe text; never for a union
of two or more object shapes; and a nested table elides vocabularies instead of
relocating them. Regenerated output is purely additive.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDGG54XF5gbTLdQzCtnaVV
@github-actions

github-actionsBot commented Aug 25, 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 c71978ff96e4f32c07830d1323efe65f34391c73packageMentionDocs.

Discharges the os-regen deferral recorded by the merge commit. `main` gained
`SearchAllResponseSchema` / `SearchAllHitSchema` / `CloneDataResponseSchema`
(#12203) while this branch was open, so the pages this branch regenerated at
its fork point described a source the merged tree no longer has — which is what
`check:docs` reported on the merge ref.
Regenerated with `gen:schema && gen:docs` from the merged tree, never by hand.
`api/protocol.mdx` gains the three new schema sections plus one nested-shape
table this branch's renderer produces for the new array-of-object property,
`SearchAllResponse.hits[number]`.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDGG54XF5gbTLdQzCtnaVV
@os-litant
os-litant marked this pull request as ready for review August 25, 2026 19:49
@os-litant
os-litant added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit a84f3eaAug 25, 2026
35 checks passed
@os-litant
os-litant deleted the claude/issue-11601-nested-describe-rendering branch August 25, 2026 20:11
os-litant pushed a commit that referenced this pull request Aug 26, 2026
…rging origin/main
The merge took origin/main's side of every os-regen artifact (per
scripts/pm/os-regen-merge.sh); this commit re-derives them from the merged
tree — gen:schema + gen:docs + gen:strictness-ledger — so main's nested-shape
reference tables (#12309) and this branch's retirement both survive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDGG54XF5gbTLdQzCtnaVV
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

2 participants

@os-litant@claude