…ent` is the one spelling (objectui#6951 A1 / objectui#7016)
`TextSchema` declared two spellings for its one content slot: `content`
(read first) and `value` (the fallback limb of
`{schema.content || schema.value}` at renderers/basic/text.tsx:162 and :167),
both declared by objectui#6150, whose docblock called the pair "a dialect,
not a design" and deferred the choice to an ADR-0049 ruling. Maintainer
ruling A1 (2026-09-04): retire `value`, keep `content`, immediately.
The ruling's premise was measured before any edit, on the four roots it
named (examples/, apps/, the examples/ dirs under packages/, content/docs):
776 content-only text nodes, 25 value-only, 0 both -- `content` by 30:1.
- `value?: never` on the interface, `retirementTombstone()` named refusal
on the Zod mirror (message names `content`), `content` docblocks rewritten
- text.tsx renders `{schema.content}` at both arms; the context-menu
renderer's built-in fallback trigger node and plugin-dashboard's three
placeholder text nodes spell `content` so they keep rendering
- every in-repo text node that authored `value` rewritten to `content`:
9 catalog entries, packages/types/examples, 11 doc fences, 3 README samples,
the catalog overlay-trigger test and four component tests
- #6150 pin follows the read site; two component pins that asserted `value`
renders now assert nothing from the retired key reaches the DOM
- new pin text-value-retired-6951: both faces refuse, the union entry
carries the guidance, `content` survives, TextSpanSchema.value control,
tree-scoped catalog absence, renderer read set off disk, tsc legs
- docs row replaced by a retirement note; changeset (types/components minor,
plugin-dashboard patch)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
Fixes #7016
Refs #6951 (A half —
TextSchema; the B half,TreeViewSchema.data, is its own PR and the card stays open until both are onmain)needs:contract-reviewon purpose. This narrows a published validator's accept set (Clause-② yes); the dispatching seat reviews at tier before it lands. Please do not mark ready or enable auto-merge.The ruling
Maintainer ruling A1 (2026-09-04, director record on #6951, batch #25), verbatim: "A1: retire
value, keepcontent… Retirement is immediate, no deprecation window (maintainer 2026-08-27: 「项目在创业阶段,用户也很少,短期不考虑渐进。」). Existingvalue-only documents are not a reason to stage it." — premise-bound: the dev measures thecontent-only /value-only / both distribution first and STOPS ifvalueis the majority. It is not (below), so the retirement is carried out as ruled.Measurement — the ruling's premise, taken before any edit
A
textnode is an object literal whose OWNtypekey is the stringtext(notelement:text, not other schemas carrying avaluemember). Walker: a tokenizer over JSON files, TS/TSX object literals and the fenced code blocks of md/mdx, recording each such object's depth-1 keys and its parent key; declaration contexts (interface/typebodies) excluded; objects classified by KIND from their keys and position —sdui-text(the population the ruling is about),field-descriptor({ value, label, type: "text" }underfields[], wheretypeis a FIELD type andvaluethe field key — the filter-builder catalog entries),ai-sdk-part(output: { type: 'text', value }tool-result envelopes). Tree read atorigin/main=9587fc95(dispatch base, before the merge); re-verified unchanged on the merge-base83c77dc3by grep (below).The four ruling-named roots,
sdui-textkind (examples/,apps/, theexamples/directories underpackages/=packages/types/examples+packages/plugin-charts/examples,content/docs/**; 939 files scanned):content-onlyvalue-onlyexamples/apps/packages/*/examplescontent/docs/**Raw, all kinds, same roots: 947 objects — 776 / 39 / 0 / 132; the 14 extra
value-only are the filter-builder field descriptors (all underfields[], keysvalue,label,type), the 106 extraneitherare form/field descriptors. Verdict on the premise:contentby 30 : 1 — no stop.In-repo tests and fixtures, separately (
packages/**/__tests__,packages/**/*.test.ts*,**/test/; 2314 files):sdui-text634 — 45 / 25 / 0 / 564;ai-sdk-part6 (allvalue);field-descriptor885 (16value). Of the 25sdui-textvalue-only hits: 13 are AI-SDK / MCP tool-result envelopes inplugin-chatbotandapp-shell(unwrapToolResult,isProposalResult,detectReplayOutcome— a different vocabulary, untouched); 7 are realtextnodes and were rewritten (span-children-rendering×3,span-value-fallback×2,undeclared-but-consumed-keys-6150's NODE fixture) or flipped (basic-renderers"value as alias"); 1 keeps authoringvaluedeliberately as the retired-key probe (bindable-text-keys-4795, now asserting nothing reaches the DOM); 4 are left as they are with a reason — a LOCALinterface TextSchemafixture inpackages/react(ComponentRendererProps.reconciliation, its own declaration), a genericCoreSchemaNodeliteral (SchemaNode.reconciliation), thepackages/reactcarriage-map probe pin (SchemaRenderer.bindableTextKeys:155, about the memo, passes unchanged), and a negative fixture for the retiredblocktype (phase2-schemas:329, refused either way).Non-test package sources (
packages/*/src, outside the ruling roots, scanned because a producer of the dialect inside the renderer package would ship a blank): 7 real producers —components/.../overlay/context-menu.tsx:95(the built-in fallback trigger node),plugin-dashboardDashboardGridLayout.tsx:360,DashboardRenderer.tsx:790and:803,legacyRetiredWidget.ts:56(placeholder text nodes whose wording is pinned through the rendered DOM), and three published README samples (components,react,types/src/zod); plus 3 hast / AI-SDK objects that are not SDUI nodes (plugin-markdown/MarkdownImpl.tsx:105,plugin-chatbot/elements/code-block.tsx:53,app-shell/hooks/useChatConversation.ts:590, untouched).Grep cross-checks (single-line forms,
git grep -csummed):content/docs/**fences spellingtext+value: 8 on83c77dc3, 0 on this head;"type": "text"objects in the catalog on base: 701 (the walker's 688sdui-text+ the descriptors);packages/*/srcnon-testtype: 'text', valueon this head: 8 hits, all of them the hast / AI-SDK objects above or comments describing that envelope.The retirement — both faces, and the read
packages/types/src/layout.ts,TextSchemablock only):value?: neverwith the house-form@deprecatedRETIRED docblock;content's docblock no longer says it "wins overvalue" and names the two read sites as{schema.content}. Thevalue?: stringmembers ofTextSpanSchema(:66) andTabsSchema(:414) are other schemas' contracts — untouched, and pinned as the control.packages/types/src/zod/layout.zod.ts,TextSchemaconst only):value: retirementTombstone('RETIRED (objectui#6951) —valueis no longer part of TextSchema; writecontent. …')— one string feeding both the parse-time message and.describe();content's describe no longer references the retired limb.packages/components/src/renderers/basic/text.tsx:162and:167):{schema.content || schema.value}→{schema.content}at BOTH arms.BaseSchemais.passthrough()/ carries[key: string]: any, and the renderer no longer readsvalue, so a deleted member would make an authoredvalueaccepted, unvalidated and rendered as a blank. Pinned live as the "deleted row" control.SchemaNodeSchemavalidates nested nodes againstBaseSchemaCore, notAnyComponentSchema, so the refusal bites at the node's own validator (TextSchema.safeParse) and at the union entry (safeValidateSchemaon a top-leveltextnode), and on the TS face wherever a literal is typed. Atextnode nested under acontent/childrenslot is not deep-validated today — the same reach every tombstone in this package has.Corpus rewrite — every
value-authoringtextnode found aboveNo document authored both keys, so no "content wins" tie-break was needed anywhere; every rewrite is a rename with the string unchanged.
examples/schema-catalog/src/schemas/(9, JSON-aware rename, byte-identical formatting round-trip):components-data-display-kbd/inline-usage.json×2 ·components-feedback-spinner/loading-button.json·components-form-date-picker/date-range-selector.json·components-form-input-otp/verification-form.json·components-overlay-hover-card/basic-hover-card.json·components-overlay-sheet/basic-sheet.json·left-side.json·right-side.jsonexamples/schema-catalog/test/overlay-trigger-mirror-6939.test.tsx×4 ·packages/types/examples/zod-validation-example.ts×1content/docs/(11):core/enhanced-actions.mdx×2 ·guide/architecture.md×2 (thedoc-snippet: fragmentcontrast pair —Hello, John!andHello, ${user.name}!; the second was an expression site PR docs: teachcontentas text's expression channel, notvalue(29 occurrences, not 23) #7114 / docs: expressions/architecture guides teachtext.valuefor expressions — switch all 23 occurrences tocontent, the ruled sole evaluation channel (objectstack#13670 option 2) #7015 left) ·guide/layout.md×2 ·guide/schema-rendering.md×5;components/basic/text.mdxrowvalue?: string; // Alias for contentreplaced by a retirement note quoting the refusal;components/basic/span.mdxcomment no longer citestext'scontentovervalueprecedencepackages/components/README.md:95·packages/react/README.md:29·packages/types/src/zod/README.md:272context-menu.tsx:95;plugin-dashboard×4 (wording unchanged, still pinned by thelegacyRetiredsuites through the DOM)packages/types/src/base.ts:477example,packages/types/src/overlay.ts:598read-site quote,span-value-fallback.test.tsxprecedence rationale dated to before this card#7015 status checked: landed as PR #7114 (29 expression occurrences); the 11 doc sites above are plain-string
valueand were outside its scope.Pins
content/docs/componentspages #6150 pins (undeclared-but-consumed-keys-6150.test.ts): theTextSchema.contentcase follows the read site (readText: '{schema.content}') and the shared NODE fixture spellscontent; the file had no case assertingvalueaccepted — the acceptance lived in the renderer pins below.basic-renderers"should support value property as alias" → nothing from the retired key reaches the DOM;bindable-text-keys-4795"text.valueis still not evaluated" → neither evaluated nor read back).packages/types/src/__tests__/text-value-retired-6951.test.ts(18 tests): zod half — three authored values refused at pathvaluewithinvalid_type/expected: never, the message namescontentand equals the.describe(), the same node refused throughsafeValidateSchemawith the guidance inside theinvalid_unionenvelope, key still DECLARED; inside the boundary —contentparses and survives,{ type: "text" }parses,content: 42still refused,TextSpanSchema.value(thelayout.ts:66sibling) still accepted and survives, an undeclared key still rides passthrough; corpus — tree-scoped walk of every JSON underexamples/schema-catalog/src/schemasandpackages/types/examples(descriptor arrays skipped by position; non-vacuity floor 500 nodes) asserting notextnode spellsvalueand each parses; renderer —text.tsxread set off disk (schema.valueabsent,{schema.content}×2) and thecontext-menuproducer spelling; TS half —@ts-expect-errorlegs on the member type, the fresh literal and a widened value,TextSpanSchemacontrol compiles.?: never/retirementTombstone()pair does not drift (undefinedmeetsundefined); ablation leg B shows the existing ledger reddeninglayout.zod.ts#TextSchema(TS2322) the moment only one face moves, so the lockstep is already enforced without a row. ⛔MIRRORS/EXCLUSIONSuntouched.Verification (exit codes captured before any pipe; heavy runs through the shared verify lock; every reading on the head named)
pnpm --filter '@object-ui/plugin-dashboard^...' --filter '@object-ui/components^...' --filter @object-ui/types build(lock)9587fc95+diffVERDICT command-exit 0 · held the lock 98spnpm exec vitest run --maxWorkers=2 packages/types/ packages/components/ packages/plugin-dashboard/ packages/react/src/__tests__/SchemaRenderer.bindableTextKeys.test.tsx examples/schema-catalog/test/(lock)588c0c8f(pre-merge)Test Files 1 failed, 471 passed (472) · Tests 1 failed, 7103 passed (7104)— the one failure was the new pin's own walker counting the 14 filter-builder descriptors; fixed in2c188f09pnpm --filter @object-ui/types build && pnpm exec vitest run --maxWorkers=2 packages/types/ packages/components/ packages/plugin-dashboard/ packages/react/src/__tests__/SchemaRenderer.bindableTextKeys.test.tsx examples/schema-catalog/test/overlay-trigger-mirror-6939.test.tsx scripts/__tests__/check-doc-component-types.test.ts scripts/__tests__/doc-version-claims.test.ts(lock)2c188f09(post-merge)Test Files 449 passed (449) · Tests 5160 passed (5160) · VERDICT command-exit 0 · held the lock 542spnpm exec vitest run --maxWorkers=2 scripts/__tests__/check-doc-snippet-types.test.ts scripts/__tests__/check-readme-exports.test.ts scripts/__tests__/check-doc-links.test.ts(lock)2c188f09Test Files 3 passed (3) · Tests 278 passed (278)pnpm --filter @object-ui/types type-check(tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json)2c188f09, re-run ona80deb15and on the final headd4de1ca4pnpm --filter @object-ui/components type-check·pnpm --filter @object-ui/plugin-dashboard type-check2c188f09(components re-run ond4de1ca4)pnpm --filter @object-ui/types build && pnpm exec vitest run --maxWorkers=2 packages/types/ packages/components/src/__tests__/basic-renderers.test.tsx …bindable-text-keys-4795… …span-value-fallback… …span-children-rendering…(lock; after mergingorigin/main=3e377c93, so #7736'slayout-default-jsdoc-7361pin runs on the mergedlayout.ts)d4de1ca4(final head)Test Files 126 passed (126) · Tests 2132 passed (2132) · VERDICT command-exit 0 · held the lock 33spnpm --filter @object-ui/types lint·@object-ui/components lint·@object-ui/plugin-dashboard lint·@object-ui/example-schema-catalog lint(eacheslint ., the population CI's Lint job runs)588c0c8f+diff0 errorseach (268 / 924 / 426 pre-existing warnings, none on a touched line)node scripts/check-changeset-presence.mjsd4de1ca4✅ 16 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)node scripts/check-changeset-fixed.mjs·check-changeset-no-major.mjs·check-changeset-overwrite.mjs2c188f09✅ All workspace packages are in the changeset fixed group·✅ No changeset declares a major bump·✅ No pre-existing changeset was modified or deletednode scripts/check-control-bytes.mjsa80deb15✅ check-control-bytes: OK (scanned 6318 tracked text file(s))pnpm check:spec-symbolsa80deb15✅ spec member citations: no comment cites a key its spec symbol does not declarepnpm check:doc-types·pnpm check:doc-fences·node scripts/check-doc-links.mjs2c188f09✅ Every documented component type is registered·✅ check:doc-fences — every TypeScript block in 227 document(s) is fenced ts/tsx/typescript …·Links are valid across 17 scan rootspnpm check:unreferenced-sources·check:i18n-keys·check:i18n-drift·check:element-data-source-declaration·check:action-forward-parity·check:designer-field-key-parity·check:phantom-deps·check:self-import·check:side-effects-array·check:entry-guard·check:icon-record-names·check:skills-paths·check:shell-escape-residue·check:docs-route-closure2c188f09lint.yml/ci.ymlrun that read a path in this diff; derived by hand from.github/workflows/*— the objectstackdispatch-gates.mjsderivation is objectstack-only and does not answer for this tree)node scripts/pm/check-governed-merges.mjs --test …(objectstack, on the final 35-path list atd4de1ca4)0 of 35 path(s) hit the register … NOT governedNOT MEASURED (precondition exits, not verdicts):
pnpm check:doc-snippets→ exit 2PRECONDITION NOT MET(its--build-filterclosure is 26 packages with..., effectively the whole workspace; not built on this shared box);pnpm check:skill-examples→ exit 2 (same closure; this diff touches noskills/**file, and the onlyskills/fences spellingtext.valuearejsonccounter-examples, which that gate parses and does not schema-validate);pnpm check:readme-exports→ exit 1population COLLAPSED — run pnpm build first;pnpm check:sdui-registration-pins→ exit 2No console build to weigh. CI runs all four on the farm.Narrowed measurement standing in for
check:doc-snippets: the six touched non-fragmentts/tsxfences (content/docs/core/enhanced-actions.mdx:309,:399;content/docs/guide/schema-rendering.md:21;packages/components/README.md:86;packages/react/README.md:26;packages/types/src/zod/README.md:266;architecture.md:398is a declareddoc-snippet: fragmentand is not compiled by the gate either) were extracted from the merge-base83c77dc3and from this branch and compiled withtsc --strict --noEmit(jsx: react-jsx,pathsderived from each workspace package'sexports[*].types, against the dist built above). Diagnostic sets are IDENTICAL before and after: five fences clean; the zod README fence reports the same pre-existingTS2304 Cannot find name 'CardSchema'on both sides (no import in that fence on base either). This diff moves no fence verdict.Ablation (red-first evidence; each leg mutate → prove on disk by blob hash and anchor counts → run →
git checkout HEAD -- ABSOLUTE PATH→ prove restored by blob hash == HEAD blob;trapon EXIT INT TERM; finalgit diff HEADempty)valueback toz.string().optional(); anchorsvalue: retirementTombstone(1→0, marker 0→1): the pin file → exit 1, 6 failed / 12 passed — the three refusal cases, the guidance case, thesafeValidateSchemacase and the DECLARED case; every inside-the-boundary, corpus, renderer and TS-erased leg stays green (that asymmetry is the point).value?: never→value?: string; anchors 1→0 / 0→1):tsc -p tsconfig.test.json→ exit 2 —TS2578 Unused '@ts-expect-error'on exactly the three directives (:300,:317,:337) pluszod-mirror-parity.test.ts(1522): TS2322 '"layout.zod.ts#TextSchema"' is not assignable to type 'never'— the existing ledger catching the one-face drift.{schema.content}→{schema.content || schema.value}at both arms; anchors 2→0 / 0→2): pin +basic-renderers+bindable-text-keys-4795+undeclared-but-consumed-keys-6150→ exit 1, 4 failed / 119 passed — the two flipped component pins, finding(types): 13 top-level schema keys that component renderers genuinely READ are declared by no shipped type — measured across all 76content/docs/componentspages #6150's read-site pin, and the off-disk read-set pin.git diff HEADempty,git statusclean.ADR-0087 disposition marker — the gate's own reading
Grep of
scripts/check-changeset-*.mjs(4 scripts:fixed,no-major,overwrite,presence),scripts/__tests__/*,.github/workflows/*and.changeset/README.mdforadr-0087/disposition: no gate in this repository reads a changeset or PR-body marker — the only hits are a prose entry inscripts/__tests__/doc-version-claims.test.ts(quoting a spec tombstone) and a comment inchangelog.yml. objectstack'scheck:adr-0087-registrationhas no objectui counterpart. The four changeset gates ran (table above), all green. No marker was added — copying one from a sibling PR would be the deviation the ruling forbids; if the reviewing seat wants a marker, say which gate should read it.Deviations from the dispatch's file surface (declared, not silent)
origin/mainmerged twice before opening (83c77dc3, then3e377c93after the PM's notice that docs(types): align two layout@defaultdocblocks with the renderer fallbacks #7736 touchedlayout.tsin another region).context-menu.tsx(a producer inside@object-ui/components— "nothing else in the package moves" was read as the renderer read site, and a fallback trigger that would render blank is the retirement's own consequence); threeplugin-dashboardsource files carrying four producer sites (none of their tests — the wording pins pass through the DOM unchanged); three READMEs; four component test files whose fixtures authored the retired key; two comment-only docblocks inpackages/types/src/base.ts/overlay.ts. The changeset therefore also names@object-ui/plugin-dashboard: patch(the presence gate requires the declaration; behaviour is unchanged).type: "text"but are nottextnodes.check:doc-snippets/check:skill-examples/check:readme-exports/check:sdui-registration-pinsNOT MEASURED locally (above), differential fence measurement supplied.Out of scope — observed, not touched
skills/objectui/guides/schema-expressions.md:47,:479,:485teach{ "type": "text", "value": … }as counter-examples with prose "valueis read but never templated"; after this retirement the key is refused outright, so that prose is stale.skills/**is a governed surface (human merge) — handed back on the report, not edited here.schema.content || schema.valueread, in files this PR does not otherwise touch:packages/components/src/renderers/basic/span.tsx:131,examples/schema-catalog/test/catalog-authored-key-6805-6806.test.tsx:64,catalog-gallery-render.test.tsx:68,packages/react/src/__tests__/SchemaRenderer.bindableTextKeys.test.tsx:147(its pin is about the carriage map and passes unchanged).TreeViewSchema.data) — its own PR, as the ruling requires.Session:
https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s🤖 Generated with Claude Code
https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
Generated by Claude Code