feat(types): declare dependsOn on BaseFieldMetadata in the spec's field-level shape; widgets read it through the declared type (objectui#6153) - #8609
Conversation
…ield-level shape; widgets read it through the declared type
`BaseFieldMetadata` gains `dependsOn?: FieldDependsOn`, derived by reference
from `@objectstack/spec/data`'s `Field` — an array of controlling field names
or `{ field, param }` entries, never a bare string (the installed spec 17.3.0
answers `invalid_type` to one on a field document). The four option widgets
read it as `field?.dependsOn ?? dependsOnProp` instead of through `as any`;
`LookupField` reads both of its spellings (`depends_on`, then `dependsOn`)
through `LookupFieldMetadata`, keeping the snake_case arm for objectui#7357 to
retire. Docs for select and lookup document `dependsOn` as a metadata key
again; two changesets (`@object-ui/types` minor, `@object-ui/fields` patch).
Pins: type-level membership and shape, the installed spec's two-control
answer, annotated `SelectFieldMetadata` / `LookupFieldMetadata` literals with
no cast, read-site text pins at all five readers, and runtime gate / cascade /
`$filter` scoping through the declared type.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
…endson-declared-member
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…f its receiver (objectui#6153 patch round) `relationalMetaCopySet.derivation.test.ts` re-extracts LookupField's reads by scanning for member accesses off the receiver `fieldMeta`. objectui#6153 moved the cascade read through an annotated alias (`const cascadeMeta: LookupFieldMetadata | undefined = fieldMeta;`), so the scanner reported `dependsOn` / `depends_on` as orphans while LookupField still reads both. The reads are real; the scanner's reach was the gap. `memberReads` now also sweeps every `const NAME(: TYPE)? = receiver;` alias, derived from the consumer's source on each run (no alias is listed here), with a positive control for the two keys, an inline-fixture control pinning what the alias rule reaches and what it must not, and the real consumers' alias census (`['fieldMeta', 'cascadeMeta']` / `['meta']`). The copy-set table is untouched: removing the reader entries would have asserted a read that did not stop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
|
Patch round, head CI's Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Fixes #6153
What
Instance 1 of the card, per the maintainer ruling recorded at comment 5510108593 (director seat, summon #8, 2026-09-02, verbatim 「同意」): A — objectui's metadata face mirrors the spec's field-level
dependsOn.BaseFieldMetadata(packages/types/src/field-types.ts) declaresdependsOn?: FieldDependsOn.FieldDependsOnis DERIVED by reference from@objectstack/spec/data'sField(NonNullableof itsdependsOnmember) rather than restated: an array of controlling field names or{ field, param }entries. It is exported from@object-ui/types.SelectField,MultiSelectField,RadioField,CheckboxesFieldreadfield?.dependsOn ?? dependsOnProp(the typedfieldprop) instead of(config as any)?.dependsOn/ the untypedconfigcarrier;LookupFieldreads both of its spellings through an annotatedLookupFieldMetadataalias (cascadeMeta?.depends_on ?? cascadeMeta?.dependsOn) — BOTH arms kept, precedence unchanged, because dropping the snake_case arm is objectui#7357's job (pm:blockedon this card).content/docs/fields/select.mdxandlookup.mdxdocumentdependsOnas a metadata key again, each with an annotatedtssnippet (the same literals the pins compile);select.mdx's "model each level as a lookup withdepends_on" now namesdependsOn.@object-ui/typesminor (a new declared member plus its export),@object-ui/fieldspatch (read path only; behaviour unchanged).Not touched, by ruling: instance 2 (
options[].description, landed with #6140 / PR #7061); the #4631 camelCase-fallback family inLookupField(displayField,descriptionField,allowCreate,lookupColumns,lookupPageSize,lookupFilters,avatarField);packages/types/src/__tests__/zod-mirror-parity.test.ts(held by PR #8501 and PR #8553 — no ledger row moves, measured below).Zone 2 — the PM's mechanism assumptions, measured
SelectField.tsxread at 98 in the ruling → 101 now;LookupField.tsx"around :300" → 300 to 308 at BASE, 307 to 317 now;CheckboxesField.tsx:43→ 43 at BASE. Every site the ruling names was found.dependsOn/depends_onreads underpackages/fields/src/widgetsare the four option widgets plusLookupField;FieldEditWidget.tsx,toHostProps.ts,OptionsEmptyState.tsxanduseCascadingOptions.tscarry the key only as a PROP. Not four, not six.DependsOnInputis reachable (form.ts, exported) and every reader takes it, but it is NOT the spec's field-level shape. Measured on the installed@objectstack/spec17.3.0 with the two-control probe (FieldSchema.safeParse): a canonical select carryingdependsOn: ['country', { field: 'region', param: 'region_id' }]draws NO issue; the same document plus a bogus key drawsunrecognized_keysnaming it;dependsOn: 'country'(a bare string, whichDependsOnInputadmits) drawsinvalid_typeatdependsOn;depends_on: ['country']drawsunrecognized_keysnamingdepends_on. The ruling's primary clause is "declaresdependsOnin the spec's shape"; declaring the widerDependsOnInputwould let an author write field metadata the publish door refuses and thatLookupFieldsilently ignores. So the member is typed with the spec-derivedFieldDependsOn, which is assignable toDependsOnInputat every reader (pinned). If the seat prefers the literalDependsOnInput, it is a one-line change onfield-types.tsplus the fence pins.src/zod/has no field-types mirror (the ledger's only mention offield-types.tsis a disclaimer in its header), and the instrument —pnpm --filter @object-ui/types type-check, third legtsc -p tsconfig.test.json— is green;--listFilescounts the new pin (1) and the finding(fields):RichTextFieldreads arowskey off field metadata that neitherMarkdownFieldMetadatanorHtmlFieldMetadatadeclares — undeclared-but-consumed, reached through anas any#6140 pin (1) among 612 files.Pins
packages/types/src/__tests__/field-metadata-depends-on-declared-6153.test.ts— type-level:BaseFieldMetadata['dependsOn']is exactlyFieldDependsOn, which is exactly an array ofstring | { field, param? }; select and lookup inherit it; it flows intoDependsOnInput; a bare string is refused (@ts-expect-error); annotatedSelectFieldMetadata(single andmultiple) andLookupFieldMetadata(entry and shorthand) literals compile; the legacydepends_onliteral still compiles until RetireBaseFieldMetadata.depends_on— objectui's snake_case twin of the spec's field-leveldependsOn(enforce-or-remove; objectui#6153 ruling A follow-up) #7357 deletes that line. Runtime: the four-way spec probe above, read-site text pins for all five readers, and a "no reader reachesdependsOnthrough a cast" assertion.packages/fields/src/widgets/SelectField.dependsOnDeclared-6153.test.tsx— an annotated, uncastSelectFieldMetadatagates (select-empty-province), unlocks, prunescaundercountry: cnand keepszj; themultiplearm gates (multiselect-empty-provinces).packages/fields/src/widgets/LookupField.dependsOnDeclared-6153.test.tsx— an annotated, uncastLookupFieldMetadatagates the trigger, scopes$filterbyaccount_idfor the entry shape and byaccountfor the shorthand.examples/app-showcase's cascading-select and invoice-to-contact areform-cascading-select.test.tsx,form-dependent-values.test.tsx,SelectField.cascade.test.tsxandLookupField.dependsOn.test.tsx; all pass unchanged. The live e2ee2e/live/cascading-options.spec.tsneeds a running stack and stays with CI's live lane.Reverse verification (from commit
cbbca66; restore =git checkout HEAD -- PATH, byte-proved)dependsOn?: FieldDependsOn;fromBaseFieldMetadata(src count 1 → 0;pnpm exec tscinpackages/types;dist/field-types.d.tscount 1 → 0):tsc -p tsconfig.test.jsonexit 2, 8 errors, all in the new pin (TS2339 Property 'dependsOn' does not exist on type 'BaseFieldMetadata'and siblings);packages/fieldstsc --noEmitexit 2, 6 errorsTS2551at exactly the six read sites (SelectField 101, MultiSelectField 45, RadioField 43, CheckboxesField 44, LookupField 309 and 317). Restore:git diff HEADempty, blob hash equals HEAD's, dist count back to 1, both checks exit 0.(config as any)?.dependsOnback intoSelectField: the read-site pin fails 2 of 11 ("the read site still exists", "no reader reaches dependsOn through an as any"); restore: blob equals HEAD's, 11 of 11 pass.Gates (local, targeted; exit codes captured before any pipe)
pnpm --filter @object-ui/types type-check(3 legs) — VERDICT command-exit 0 (28 s, shared-box seconds).pnpm --filter @object-ui/fields type-check(2 legs) — first run exit 2 with 113 × TS2307 "Cannot find module '@object-ui/…'" = NOT MEASURED (fresh worktree, no siblingdist/); afterturbo run build --filter='@object-ui/fields^...' --concurrency=2(9 tasks, 51 s) — VERDICT command-exit 0.dependsOn/ cascade family, the finding(fields):RichTextFieldreads arowskey off field metadata that neitherMarkdownFieldMetadatanorHtmlFieldMetadatadeclares — undeclared-but-consumed, reached through anas any#6140 / finding(types/app-shell): 8 more hand-written select-option shapes remain after #6887 — and the derivation gate is structurally blind to the whole class #7014 / 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 pins, the parity ledger's runtime):Test Files 20 passed (20),Tests 252 passed (252).eslintwithout--no-inline-config— that flag is objectstack's, and here it un-silences a pre-existing block-disabled import atindex.ts:1298outside this diff): exit 0, 0 errors, 73 pre-existing warnings, 0 in the new files or on the changed lines.check-control-bytes✅ 6796 files ·check-changeset-presence✅ "8 source file(s) of 2 released package(s) changed, and this change declares 2 changeset(s)" ·check-changeset-no-major✅ ·check-changeset-fixed✅ ·check-changeset-overwrite✅ ·check-doc-fence-languages✅ ·check-spec-symbol-derivation✅ "nothing cites a key its spec symbol does not declare" ·check-unreferenced-sources✅ ·check-type-check-coverage✅ 42/42 ·check-doc-links✅ "Links are valid across 17 scan roots" ·check-doc-expression-carriageexit 0 (report-only) ·check-doc-component-types✅ ·check-doc-example-shared-reader✅ ·check-governed-queue-guard --test✅ NOT GOVERNED (15 paths).check-doc-snippet-types— declared to CI (its--build-filteris the whole workspace). Local proxy, measured: the two newtssnippets extracted from the pages compile--strictagainst the BUILT@object-ui/typesdist/*.d.ts(exit 0), and the same harness turns red (TS2322) whendependsOn: ['country']is replaced by a bare string.pnpm lintfarm,check-designer-field-key-parity,check-handler-key-reads,check-i18n-*, and the remainingcheck:*scripts these paths do not reach.验收备注
noted, not filed:
RadioField/CheckboxesFieldkeepconst config = field as anyforoptions/name— theFieldMetadataunion declares noradio/checkboxesmember (theirtypeliteral is absent), so no declared type exists to read those keys through. Incompleteness, not a defect: the runtime honours the keys. Bearer: A component type has THREE declared surfaces that disagree (TS schema type / registry meta inputs / renderer prop reads), and nothing reconciles or enforces them #4631's three-face reconciliation would meet it; otherwise 承接者:无.SelectField.tsxstill carries(config as any)?.namealthoughconfigis typedSelectFieldMetadata— a dead cast, outside this card's key. 承接者:无.LookupFieldignores a bare-stringdependsOnthat the option widgets honour (core'sresolveDependsOnFieldswraps a string). Unreachable through the declared type (array-only) and consistent with the spec; reachable only from untyped hosts. Bearer: RetireBaseFieldMetadata.depends_on— objectui's snake_case twin of the spec's field-leveldependsOn(enforce-or-remove; objectui#6153 ruling A follow-up) #7357 edits exactly that block when it drops the snake_case arm.LookupField's two-arm precedence is unchanged (depends_onfirst). Moot once RetireBaseFieldMetadata.depends_on— objectui's snake_case twin of the spec's field-leveldependsOn(enforce-or-remove; objectui#6153 ruling A follow-up) #7357 lands.Serial / overlap
None of this PR's files is touched by PR #8501, #8553, #8601 or #8602 (measured from their changed-file lists at claim time). #7357 remains
pm:blockedon this card and is the one that removes the snake_case arm and thedepends_onmember; the pin that keepsdepends_oncompiling is labelled for deletion in that stroke. Related, not addressed here: #4631, #6140, #7061 (instance 2, already merged).Clause-②: yes — adds a declared member to a published type;
needs:contract-reviewapplied.🤖 Generated with Claude Code
https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
Generated by Claude Code