Uh oh!
There was an error while loading. Please reload this page.
feat(spec,lint): a layout section can reference a declared field group instead of copying its members - #13897
Conversation
📓 Docs Drift CheckThis PR changes 2 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0d078180136b64c20ded84ff1f7e4cde8b199d12 && git checkout 0d078180136b64c20ded84ff1f7e4cde8b199d12
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin db9c460cf39211709e99a8db3d755d9d44188b4f a61844e50caa6b3431cc4194eab55bb97dcdfe67 && git checkout -B drift-repro db9c460cf39211709e99a8db3d755d9d44188b4f && git merge --no-ff a61844e50caa6b3431cc4194eab55bb97dcdfe67
node scripts/docs-audit/affected-docs.mjs --json db9c460cf39211709e99a8db3d755d9d44188b4f
|
…ction-group-reference
… page's merged content
huangyiirene
commented
Aug 31, 2026
Correction + re-verification at |
Uh oh!
There was an error while loading. Please reload this page.
…g set (#13804) Contract-review increment on PR #14196 (director-seat conditional PASS, comment 5494985273). Two changes, exactly as ruled: 1. The changeset for @objectstack/service-datasource moves patch -> minor. The diff adds public API — the `datasourceConnectivityChanged` and `ConnectivityBearingFields` exports, the public `DatasourceConnectionService.reconnect` method, and the published `DatasourceAdminServiceConfig.reregisterPool` member — which is additive widening = minor by repo convention (#13897 is the same shape). `patch` under-reported the surface movement. 2. `schemaMode` joins the connectivity-bearing field set. It was found during this card's premise verification and reported as a fork rather than added unilaterally; the review ruled it IN in the same stroke. It is really read at three sites on the connect path — the `canConnect` policy gate, `toSpec` -> `factory.create` (driver construction), and `registerDatasourceDef` (the write gate's def) — and it is patchable by `updateDatasource`, so omitting it left a schemaMode-only save persisting the new record while all three kept the OLD value until restart: a narrower instance of the stale-pool defect this card fixes. One comparator line, plus the exported field slice, plus one comparator pin. The module docblock now states the resolution instead of carrying the fork as an open question. The label-only reverse control (same driver instance, zero factory calls, zero evictions) stays green, which is what shows the set widened by exactly one member rather than into "rebuild on everything". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
Part of #13855
Clause-②: yes — path limb
packages/spec/src/ui/**plus a content limb that widens the accept set on two authoring surfaces. Draft, withneeds:contract-reviewattached in the same stroke as creation.Maintainer ruling 2026-08-31 on #13855, option B, verbatim: 「直接处理b」. The renderer half is tracked separately; this PR is the spec half plus the reference diagnostics the ruling routes the existence check through.
The gap this closes
ADR-0085 makes
fieldGroups+Field.groupthe canonical grouping, assembled in exactly one place —deriveFieldGroupLayout(ADR-0085 section 5). The two layout escape hatches had no way back to it: a custom record page'srecord:detailsproperties.sectionsand a view-levelform.sectionseach enumerated their members by hand. An author who reached for either had to hand-copy the same membership fact a second and third time, with nothing linking the copies to the declaration — so every field added to the object afterwards made them quietly staler. Measured on a real app: three disagreeing groupings of one object, with the detail page missing two fields the form showed.A section may now name the group instead, on both surfaces:
Members (every visible field whose
Field.grouppoints at the key, in field-declaration order) and the group's own presentation (label, icon, description,collapse,visibleWhen, and the drop when a group has no visible members) all come fromderiveFieldGroupLayout. Nothing is re-implemented in section land — the section schema resolves nothing and assembles nothing.Both surfaces, one design — the structural comparison
The ruling made the second surface conditional on the two section vocabularies being symmetric enough. Measured, they are:
record:detailssectionFormSectionSchemafields(was required)fields(was required)name,label,description,icon,columnsname,label,description,columnscollapsible+defaultCollapsed(no defaults)collapsible+collapsed(both.default(false))visibleWhenplus the deprecatedvisibleOnhideEmpty,showBorder,headerColorpaneThe vocabularies differ in their leaves, but the rule over them is identical, so it is declared once in
packages/spec/src/shared/section-group-reference.tsand each surface passes in its own key names. No record-page-only landing, no named remainder on this axis.The mixing rule — designed and pinned, offered for review
groupandfieldsare mutually exclusive. A section declaring NEITHER is refused — before this change that was unrepresentable, becausefieldswas required, so the refusal is what keeps the old guarantee (no section reaches a renderer without a member source) afterfieldsgoes optional. Presence, never non-emptiness:fields: []parsed before and still parses.name,label,description,icon, the collapse pair, andvisibleWhen/visibleOnare refused besidegroup, each refusal naming thefieldGroupsentry that owns the key. The surface keys the group says nothing about ride alongside unchanged:columns,pane,hideEmpty,showBorder,headerColor.visibleWhenandcollapse, andderiveFieldGroupLayoutpasses both through to the section it derives — exactly the two things Wizard view v1 — declaration-and-refusal tightening of FormViewSchema type:'wizard' (Card S, ruled shape of #13622) #13704 refused on a step one day earlier. Acceptinggroupthere would deliver that behaviour through the object's declaration while the step-key refusals reported clean.Two judgement calls in that list are the ones worth a reviewer's attention:
labelbesidegroupputs group presentation back in section land, which is the one thing ADR-0085 section 5 single-sources; and it re-creates a second writable spelling of the fact this whole form exists to stop copying. It is also the reversible direction — refusing now and allowing overrides later is additive, while shipping overrides and withdrawing them is breaking. If contract review prefers the override reading, this is the line to change.collapsible/collapsedon the form surface are refused only when writtentrue. They carry.default(false), so by the time an object-level refinement runs an authoredfalseis indistinguishable from the default — the same asymmetry Wizard view v1 — declaration-and-refusal tightening of FormViewSchema type:'wizard' (Card S, ruled shape of #13622) #13704 records for the wizard step keys, and it costs nothing, sincefalsedeclares exactly what a group withcollapse: 'none'delivers.Existence: reference diagnostics, not a new channel
The key names something on a DIFFERENT schema, so the spec door deliberately answers only the grammar — the
UserFilterFieldSchema.fieldprecedent ("must exist — checked by reference diagnostics"). Existence lands in@objectstack/lint, in the two rules that already own field-existence on these exact surfaces:page-section-group-unknowninvalidate-page-field-bindings(the reference-integrity suite member for pages)form-section-group-unknowninvalidate-form-layout, reading both the canonicalsectionsand the legacygroupsbucketBoth resolve against one shared index (
indexObjectFieldGroups) so the two surfaces cannot grow two answers that happen to agree, and the page-side extraction readsCOMPONENT_FIELD_SPECS[...].nestedSections— the same descriptor table the field check walks, so a component that grows sections is covered by both questions in one edit.Severity:
warning, matching the family. Both host rules declarewarningfor a dangling reference because the consumer degrades rather than fails, and theerrorlimb invalidate-page-field-bindingsis reserved for a reference that reaches a QUERY, where the empty result is indistinguishable from "there is no data". A section that does not render is visible and touches no query, so it sits with its siblings. The hint lists the object's declared groups, and says what to do when the object declares none at all.Type-surface note for consumers
fieldsbecomes optional on both section shapes — that is what makesgroupthe other way to declare the same fact — soz.infernow types it as possibly undefined. A consumer readingsection.fieldsunconditionally must handle the reference form. Every in-repo reader already guards it (validate-form-layout,validate-translatable-sections, the CLI i18n extractor, the form-view refinement's own optional chain). No authored metadata changes shape and nothing that parsed before stops parsing.Also single-sourced here: the group-key grammar is now
FIELD_GROUP_KEY_PATTERN, declared beside the derivation and read by the declaring surface (ObjectFieldGroupSchema.key) and both referencing surfaces, so a key one accepts can never be a key another refuses.Verification
Everything below at final commit
4cb9ddba6, exit codes captured before any pipe.Suites
pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2— EXIT=0,Test Files 445 passed | 1 skipped (446),Tests 11962 passed | 1 skipped (11963)pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2— EXIT=0,Test Files 87 passed (87),Tests 2409 passed | 5 skipped (2414)pnpm --filter @objectstack/spec --filter @objectstack/lint run typecheck— EXIT=0 (spec's test layer compiles undertsconfig.test.json; the new test file is inside that program, confirmed by--listFiles)pnpm lint(repo-wideeslint . --no-inline-config) — EXIT=0New pins. 34 in
packages/spec/src/ui/section-group-reference.test.ts(both doors:ComponentPropsMap['record:details']andFormViewSchema), 11 across the two lint rule test files. Accept pins carry the weight the refusals cannot: the key must SURVIVE the parse on both surfaces (on the form surface, through a.transform, the view-level.superRefine, and the legacygroupsfold), the enumerated form is pinned unchanged includingfields: [], and every surface key that is NOT refused is pinned as still accepted — a refusal list written one key too wide is indistinguishable from a correct one until exactly that parses. Controls: an unknown sibling key is still refused on both surfaces, and a near-miss spelling of the new key is steered togroup.Reverse verification, from the committed state, both legs source-resolved (the pins import the mutated modules by relative path, not through
dist/), each mutation proved on disk by anchor and injected-text grep counts plus a blob-hash change, each restore proved by blob-hash equality against the HEAD blob and an emptygit diff HEAD:group/fieldsmutual-exclusivity refusal:Tests 2 failed | 32 passed(the two exclusivity pins, one per surface) — restored16e68e091a7dto16e68e091a7dTests 7 failed | 45 passedacross both lint rule files — restored6779ac6870c6to6779ac6870c6Gate family, derived by
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackfrom the real change set (44 families, 49 runnable commands) — all run:check-dev-prereqsandcheck:dual-build-cjs-loads("PREREQUISITE NOT MET ... nothing was measured"),check:type-check-debt(refuses--re-measurewithout the built closure, by design),@objectstack/lint check:doc-formula-expressions(prerequisite:@objectstack/formulabuilt),check:react-declaration-parity(no objectui manifest in this repo), andcheck-test-completenesspluscheck-half-states, which both exit 3 saying nothing was read. Reported as unread instruments, never as green.check:docs(regeneratedcontent/docs/references/{data/object,ui/component,ui/view}.mdx), the export-surface freshness pin (gen:export-origins+gen:api-surfacefor the newFIELD_GROUP_KEY_PATTERNexport), andcheck-system-context-census(pure line rot from the docblock this PR adds toobject.zod.ts, repaired with the gate's own--fix). Nothing generated was hand-edited.Named remainders
deriveFieldGroupLayout, so a section referencing one renders nothing. That is inherited ADR-0085 behaviour, not introduced here, and it is deliberately NOT a finding: unlike the existence question it is data-dependent and has no closed oracle in a static pass.record:*component cannot be authored on akind: 'react'page (recordContextFindingrefuses it upstream), so there is no third surface needing this check.Generated by Claude Code