docs(spec): a form section / page component gates visibility only — say so, and tell disabled where it belongs (#7887) - #8199
Conversation
…t in the rejection (#7887) `FormSectionSchema` / `PageComponentSchema` gate visibility only; editability lives on fields (maintainer ruling, 2026-08-12). No slot and no alias row is added — the rejection now carries a guidance string naming the field-level `readonly` / `readonlyWhen` pair instead of refusing bare. Filed as `VISIBILITY_ONLY_STRICT_OPTIONS` on those two shapes rather than in the shared `VISIBILITY_STRICT_OPTIONS`: the third consumer, `FormFieldSchema`, answers `disabled` through its own rename row, and a guidanceSet consumes a key before the rename channel is ever reached. 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:
|
#7887) `check:api-surface` (repo-wide TypeScript Type Check job) went red: exporting `VISIBILITY_ONLY_STRICT_OPTIONS` from `shared/visibility.ts` put it in the public barrel and moved the package's API surface. Moved to a new `shared/editability-boundary.ts`, which the barrel deliberately does not re-export — the same posture as `strict-object.ts` and `alias-probe.ts`. `StrictObjectOptions`, the const's own type, is not public either, so a published value of that type is one no consumer could annotate. The public API surface and `export-origins` now both read unchanged, which is also the card's own claim one level out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016YBUGvukaeVu9DjKdsHJa9
|
Pushed Took option 2 (keep it package-internal), not the snapshot regeneration. The barrel does re-export What tipped it past "slightly better fit":
( The PR body's placement section now covers both decisions — which table the guidance goes in, and why the table is internal — and the changeset gained a sentence saying the public surface does not move. Re-ran after the change: full Taking the process note: Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31641136083 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
Queue-failure receipt — requeuing ONCE, and this is the record of why. Queue build 31641136083 ejected this PR. Signature: Triaged before requeuing, not after:
That leaves the third branch of the lane's queue-triage rule — cause not established as either mine or a known flake — whose action is one requeue, receipted here. The commitment that makes a single requeue honest: if the same signature ejects this PR again, I will not requeue a second time. I will file it as a flaky/broken-test finding against the owning lane and leave this PR out of the queue until that is resolved. Two consecutive identical ejections is evidence about Also notified the Generated by Claude Code |
Fixes #7887
The ruling being implemented
Maintainer ruling of 2026-08-12 (comment 5271005260), operative sentence, verbatim:
So this PR adds no key and no alias row. It documents the boundary on both shapes and gives the existing rejection a guidance string naming where the key does belong.
Before, on a section:
After:
It points at
readonlyWhenand never atdisabledWhen— verified onorigin/main, not inherited from the card:data/field.zod.tsrenamesdisabledtoreadonlyand its own comment records that "a field hasreadonlyWhen, notdisabledWhen" (#7832). A prescription namingdisabledWhenwould hand the author their next rejection. One pin asserts the prescription text containsreadonlyWhenand another asserts it never containsdisabledWhen.Where the guidance landed, and why not the obvious place
VISIBILITY_STRICT_OPTIONSis shared by three shapes:FormSectionSchema,PageComponentSchema, andFormFieldSchema. Filing the boundary family in that shared table is one line shorter and is wrong in three separate ways, all of them measured rather than argued (I ran the counterfactual — the set hoisted into the shared table — and reverted it):strictUnknownKeyErrorconsults exactguidance, thenguidanceSets, thenaliases, and a set matchcontinues past the rename channel.FormFieldSchemaanswersdisabledthrough its ownaliases: { disabled: 'readonly' }row, andreadonlyis real there. Measured on the counterfactual: the sentence Did you meandisabled→readonly? disappears, and a field author is redirected away from the surface where the key they want actually lives.alias-integrity.test.tsgoes red — the [finding] alias-integrity cannot see an alias row made unreachable by a guidanceSet — a vis-shaped alias on VISIBILITY_STRICT_OPTIONS surfaces would be dead code with no gate saying so #7889 reachability check, which landed this morning:alias-integrity.test.ts'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 check goes red too —readonlyis a declared key onFormFieldSchema, so a shared set listing it is a dead member on that surface by construction.The existing comment at
view.zod.ts'sFormFieldSchemastrict-options block already rejected shared-table filing for the alias case, in the same direction ("filed there it would name a key two of its three surfaces do not accept"). This is that rule read from the other end, and the comment is extended to say so rather than contradicted.So: a new
VISIBILITY_ONLY_STRICT_OPTIONS(the shared options plus the boundary set) is taken by the two shapes that gate visibility and nothing else.FormFieldSchemakeeps the bare options. The prescription text is written once, filed on exactly two surfaces.Set membership covers
disabled,disabledWhen,readonly,readOnly,readonlyWhen,editable. None of them matchesVISIBILITY_KEY_PATTERN(/vis|conceal|hidden|show.?when/i), so the two sets on the table never contend and declaration order is not load-bearing — pinned, so it cannot quietly become load-bearing later.…and why the table is package-internal
The options table lives in a new
packages/spec/src/shared/editability-boundary.tsthat theshared/index.tsbarrel deliberately does not re-export — the same posture asstrict-object.tsandalias-probe.ts, the machinery it belongs to.It was first written into
shared/visibility.ts, which the barrel does re-export wholesale, andcheck:api-surfacecaught that (+ VISIBILITY_ONLY_STRICT_OPTIONS (const), 1 added / 0 breaking) — a repo-wide gate that a package-scopedtypecheckdoes not reach. Two fixes were available: regenerate the api-surface snapshot, or keep it internal. Internal is right here, and not merely because it is tidier:StrictObjectOptions, the const's own type, is not public (it lives in the unbarrelledstrict-object.ts). A published value of an unpublished type is an export no consumer can annotate or usefully extend.check:api-surfacenow reports "public API surface + factory signatures unchanged ✓", andexport-originsis byte-identical tomain— this PR touches no committed generated artifact.VISIBILITY_STRICT_OPTIONSbeing public is a pre-existing accident ofvisibility.tscarrying the runtimenormalizeVisibleWhenhelper; not touched here.Acceptance invariance
The lane's admission criterion, evidenced five ways:
visibleOn/fields) and a representative page component (type/id/label/properties/className/visibility) both still parse, and the ADR-0089 fold still produces the same output keys.FormFieldSchema's message is byte-for-byte what it was — captured before the change and after, identical.pnpm --filter @objectstack/spec buildrunsgen:schema, which rewritesauthorable-surface.base.jsonand the JSON-schema artifacts; the tree was clean afterwards. Nothing about the accepted surface moved for it to record.check:api-surfaceabove.New pins —
packages/spec/src/shared/editability-boundary.test.tsEach asserts on a real
safeParseerror, never on the options table (a row filed in a table nothing consults is the dead-entry shapealias-integrity.test.tsexists for, and only reading the message back cannot pass that way):disabledon section/component reaches the boundary text, rendered as the template bulletguidanceentries — same text, three copies of the paragraphreadonly: trueandreadonlyWhendisabledWhenvisibleWhenis named and really parses on both shapesFormFieldSchemastill emits the rename and NOT the boundary textVISIBILITY_KEY_PATTERNvisible-when-alias-guidance.test.ts§3's assertions are unchanged and green by construction — this card added no disabled-ish key, which is exactly what those pins measure. Only that block's prose moved: it saiddisabled"stays uncurated", which is no longer true for the two shapes the ruling covers (SelectOptionSchema, out of the ruling's scope, stays bare). The test name now reads "so no alias row is filed", which is the claim the assertions actually make.Changeset
patch, confirmed rather than inherited. The criterion is whether the prose reaches a consumer; measured:dist.d.tshover — YES. The boundary paragraphs are JSDoc on the two exported schema consts,FormSectionSchemaandPageComponentSchema.content/docs/references/ui/view.mdxis built bybuild-docs.tsfrom the file-level doc block plus per-property.describe()text; a schema's own JSDoc is not rendered. Grepping the page for existingFormSectionSchemadocblock text ("Closed under ADR-0089 D3a") returns nothing, while a per-key.describe()string hits. No top-level.describe()was added for it: it would not reach that page either, and it would moveauthorable-surface.base.jsonfor no consumer gain.One reachable consumer is enough, and no metadata that parses today parses differently, so
patchis right. No public export is added, so there is nothing additive for the bump to cover.Scope
data/object.zod.tsis untouched and no part of thevisible/showWhensymmetry question is implemented — that is #7816's, per the cross-seat note there.SelectOptionSchemais also untouched: the ruling names two shapes, and a per-optiondisabledis a genuinely different question (a disabled select option is a real UI concept, unlike a read-only section). Recorded as an observation for the PM to route, not fixed here.Verification
Re-run in full after the api-surface fix:
pnpm --filter @objectstack/spec test— 386 files / 10218 tests, green.pnpm --filter @objectstack/spec typecheck— green.pnpm --filter @objectstack/spec check:api-surface— "public API surface + factory signatures unchanged ✓" (after a full dts build;OS_SKIP_DTS=1is not valid for this gate, per AGENTS.md §9).pnpm --filter @objectstack/spec check:export-origins— current, 5021 exports, artifact byte-identical tomain.alias-integrity/strict-object/visible-when-alias-guidance/ui/view/ui/page— green.git checkoutand re-run green.check:nul-bytes,check:adr-anchors,check:changeset-gate-self-tests,check:docs-audit-scope,check:merge-driver,check:release-body,check:spec-parsed-alias,check:i18n,@objectstack/lint check:doc-formula-expressions— all exit 0.Generated by Claude Code