docs(spec): each shape names itself in its rejection, and a select option gets the editability boundary (#8202, #8201) - #8339
Conversation
…n a select option (#8202, #8201) Each of the three view/page shapes now names itself in its unknown-key rejection, so the two deliberately contradictory answers to `disabled` (rename on a field, boundary on a section / page component) can be told apart. `SelectOptionSchema` inherits the #7887 ruling with its own prescription: withdraw the option with per-option `visibleWhen`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YBUGvukaeVu9DjKdsHJa9
…ditability boundary (#8202, #8201) Reachability measured rather than inherited: both halves are parse-reachable error strings, #8201's paragraph also reaches `dist/**/*.d.ts` hover, and the generated reference page is measured NOT to render schema-level JSDoc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YBUGvukaeVu9DjKdsHJa9
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
Fixes #8202
Fixes #8201
Two text-face halves finishing the per-shape guidance story PR #8199 started. They land on the same shapes and the same pin file, which is why they ship as one PR. No key is added anywhere, every spelling rejected before is rejected after, and everything that parsed before parses identically — only the sentence an author reads moves.
Half A (#8202) — a rejection now says WHICH shape refused the key
FormFieldSchema/FormSectionSchema/PageComponentSchemashared onesurfacestring, so all three opened their rejection withUnrecognized key(s) on this view/page schema.That was free while the three answered a key identically. Since #8199 they do not:
Those two answers contradict each other by design, and the contradiction is only coherent if the message says which shape the reader is on. Until this PR it did not: an author holding the section answer while looking at a field was being told to move a key that was already in the right place.
Each shape now names itself:
this form field/this form section/this page component.Where the strings landed, and why not the shared table. Per #8199's placement rule, a table shared by three shapes may carry only what is true of all three — it can no more carry one shape's name than it can carry one shape's prescription. So the override is filed at each of the three call sites (
view.zod.tstwice,page.zod.tsonce), andVISIBILITY_STRICT_OPTIONSkeeps the family name it always had. That is the same rule #8199 drew, read from thesurfaceend, and the docblocks on both shared tables now say so.The one hazard that comes with a default nobody uses: a fourth consumer that spreads the shared options and forgets the override inherits the family string silently — the message goes vague rather than wrong, so nothing else fails. Pinned in two places rather than trusted: locally on the four live messages, and repo-wide over every table the
alias-integritywalk constructs.Half B (#8201) —
SelectOptionSchemainherits the #7887 ruling, with its own answerThe 2026-08-12 ruling (a form section / page component gates visibility only; editability lives on fields) named two shapes, so #8199 deliberately left the third — the select option pinned alongside them in
visible-when-alias-guidance.test.ts§3 — bare.It has the boundary now, on the ruling's own premise re-measured for this shape rather than by analogy. Triage measured it on objectui
origin/main@aca27fa: the object-field pipeline that spec'sSelectOptionSchemaactually feeds has zero per-optiondisabledconsumers —packages/fields/src/widgets/SelectField.tsx:161calls root-leveldisabled"the single authority" in its own comment,RadioField.tsx:124reads onlyprops.disabled. A shown-but-unselectable option does exist in objectui's SDUI family (packages/components/src/renderers/form/select.tsx:62), but on that package's own select-option vocabulary inpackages/types— a different shape from this one, so it makes no spec-side key enforced. Declaring one here would ship exactly the ADR-0049 declared-but-unenforced form.Its prescription is not the siblings' text, because the siblings' destination does not exist here: a section redirects the author to "the form field(s) inside it", and an option has no inside. What is true for an option is that it is offered or withheld:
The spelling it points at was checked against
field.zod.tsrather than assumed: per-optionvisibleWhenis the declared key on this shape, and ADR-0068 bindingcurrent_userthere is what makes it the real answer instead of a euphemism for "you cannot". It never namesdisabledWhen(no field surface declares it — #7832), and it points atreadonly/readonlyWhenon the field, both of which really parse there.The decision is not foreclosed. Triage explicitly left real product pull for non-selectable field options open as a maintainer decision that would widen the accepted set. The prose says what the platform honours today and names the route to change it — a spec decision, not a key an author writes. A pin fails a rewrite that hardens it into a permanent refusal.
No key, no alias row — the same red line the mother ruling drew. The set is filed on the option's own table and shares
EDITABILITY_BOUNDARY_KEYSwith the containers (one vocabulary, two answers), so a spelling added to the family is answered on every shape that has no editability semantics.Acceptance invariance
The lane's admission criterion, evidenced in both directions on all four shapes:
disabled,disabledWhen,readonly,readOnly,readonlyWhen,editable) are still rejected on section, page component and select option; pinned per key.['color', 'default', 'label', 'value', 'visibleWhen']) with theExpressionInputSchemafold intact.continues past the rename channel). Asserted directly: no member of the boundary family is one of the option's ten alias keys, andvisible/colourstill produce theirDid you meanpointers.FormFieldSchemastill emits itsdisabled -> readonlyrename and NOT the boundary text — docs(spec): a form section / page component gates visibility only — say so, and telldisabledwhere it belongs (#7887) #8199's invariant, unchanged and re-asserted.pnpm --filter @objectstack/spec build(which runsgen:schemaand rewritesauthorable-surface.base.json) leaves the tree clean;check:api-surfacereports "public API surface + factory signatures unchanged ✓";check:export-originsis current at 5012 exports. Nothing is added to the public barrel —editability-boundary.tsstays package-internal, the same posture docs(spec): a form section / page component gates visibility only — say so, and telldisabledwhere it belongs (#7887) #8199 gave it.Existing assertions modified — four, each justified
ui/view.test.ts(#6416 order pin)this view/page schema->this form fieldui/view.test.ts(full-message shape pin)this form sectionshared/alias-integrity.test.ts(#6619 fold closure)surface === 'this view/page schema'-> the three per-shape names, plusnew Set(...).size === 3toBeGreaterThanOrEqual(3)fails loudly. The existing pin would have gone red and forced this update; it would not have gone quiet. (Only theforloop underneath it would have iterated zero times; the length assertion fires first and stops it, which is exactly why that assertion is there.) The new tripwire in this PR proves the count really is zero. The claim is identical (those three shapes declare through the folded table and carryVISIBILITY_KEY_PATTERN) and the selector is strictly stronger: it now also fails if a shape loses its own name — which it did, untouched, during reverse verification below.shared/visible-when-alias-guidance.test.ts§3SelectOptionSchema"was out of that ruling's scope and stays bare", which Half B makes false. No assertion in §3 changed: all three rows stay green by construction because they measure rejection, and this card adds no key. Confirmed by removing the guidance set and watching §3 stay green while only the new #8201 pins went red.Nothing was loosened. The two
toBe/startsWithpins remain exact-literal pins; the alias-integrity selector gained an assertion rather than dropping one.New pins —
shared/editability-boundary.test.ts§5 and §6surfaceend)no live surface still reports the shared view/page FAMILY name(inalias-integrity.test.ts)disabledon an option reaches the boundary as the template bulletvisibleWhen, and both advertised predicates really parse on the optionreadonly: true/readonlyWhen, and both really parse onFieldSchemadisabledWhenReverse verification (direction predicted before running)
Both fixes were committed first, so each restore came out of a real commit.
surfaceoverride — predicted red in §5, in theview.test.tsshape pin, and in the new repo-wide tripwire. Observed: 6 red, including one I had not predicted and welcome —alias-integrity's [spec] Fold the three hand-written unrecognized_keys error maps intostrictObjectguidance — requires a set-keyed guidance form, and closes the alias-integrity gate's blind spot (#6416 direction 2) #6619 fold pin, which caught the section losing its name through the strengthened selector. Restored, green.guidanceSetsentry — predicted red confined to the new §6 pins, withvisible-when-alias-guidance.test.ts§3 staying green, since it measures rejection and this card adds no key. Observed exactly that: 6 red in §6, §3 green throughout. Restored, green.Changeset
patch, in.changeset/per-shape-guidance-completion.md, confirmed the way this lane does it rather than inherited. The criterion is whether the prose reaches a consumer:unrecognized_keystext an author reads out of a failed parse; every claim is pinned against a realsafeParseerror rather than against the options table. This alone settles it.dist.d.tshover — YES for Half B. The boundary paragraph is JSDoc on the exportedSelectOptionSchemaconst.content/docs/references/data/field.mdxis built from the file-level doc block plus per-property.describe()text. Grepping that page forSelectOptionSchema's existing docblock text ("CRITICAL RULE", "machine identifier that gets stored") returns nothing, while its per-key.describe()string hits at line 262. No.describe()was touched, so neither the page norauthorable-surface.base.jsonmoves.No public export is added, so there is nothing additive for the bump to cover.
Verification
pnpm --filter @objectstack/spec test— 388 files / 10268 tests, green.pnpm --filter @objectstack/spec typecheck— green (incl.check:scripts-typecheck,check:test-typecheck; the shrink-only debt ledger is unmoved).pnpm --filter @objectstack/spec check:api-surface— "public API surface + factory signatures unchanged ✓" (after a real dts build;OS_SKIP_DTS=1is invalid for this gate).pnpm --filter @objectstack/spec check:export-origins— current, 5012 exports.check:adr-anchors,check:changeset-gate-self-tests,check:cross-package-test-inputs,check:docs-audit-scope,check:driver-conformance,check:merge-driver,check:release-body,check:spec-parsed-alias,check:type-source-resolution,check:nul-bytes,check:i18n(after building@objectstack/cli),@objectstack/lint check:doc-formula-expressions(after building@objectstack/formula).scripts/pm/dispatch-gates.mjs), which surfaced four families the dispatch list did not name; all run, all green:check:objectui-changeset,check:query-options-erasure,check:type-check-coverage,scripts/check-changeset-no-major.mjs.scripts/check-dev-prereqs.mjs("The workspace is not built — 1 unmet precondition, not a list of problems"; 66 of 67 packages have nodist/), andcheck:objectui-pin-fresh(.objectui-shalags objectuimain; not in this diff, and that workflow's own header says an ordinary code PR sitting behind objectui "is the normal state of the repo, not a defect" — it blocks only the release lane).Scope
data/object.zod.tsis untouched, and no part of thevisible/showWhensymmetry question is implemented — #8199'svisibleWhenalias family and the shared options table are read, never widened. #7816 is not addressed here; it remains open and owns that question, per the cross-seat note posted there.Generated by Claude Code