Repository navigation
fix(types,plugin-form)!: close FormField's open index signature; a form-view field's immutable locks the control on edit - #28363
Conversation
…r immutable `group` and the section-divider disclosure keys are written by the host, so they move onto package-local hand-off types instead of riding the FormField index signature. A form-view field's `immutable` now locks the control once the record exists, instead of being copied to a key no renderer reads. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibxnmxDxesWCZGKX6jW
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibxnmxDxesWCZGKX6jW
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibxnmxDxesWCZGKX6jW
… each key Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibxnmxDxesWCZGKX6jW
|
domain:spec#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
domain:spec#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: Inputs: card #25923 (body; ruling 6412606977; the item-4 landing record 6448239783 naming both leftovers; spec seat 3's release 6538368518; the seat's claim 6539522597; the dev report 6540384291 with its open question and deviations), PR #28363 (body with its Skills-docs line, the file list of 13 files, the diff of 355 lines against ① Derived judgments
② Semver level
③ Boundary flags
Verdict reasoning. The deferred narrowing lands with its breakage fixed by declaration rather than by casts, pinned both ways; the spec's Implemented-by: VERDICT: PASS |
|
domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D 维护者速读
席位核对 · ACCEPT ·
|
Requested by Tesla · project thread
Fixes #25923
Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: none — no
skills/**page or docs page teachesFormFieldor its index signature (grep -rn FormField skills/is empty); the closed-face ruleskills/objectui/rules/protocol.mdalready states for node types now holds forFormFieldtoo, and the spec's ownimmutabledescription ("editable on create, locked once the record exists") is what the renderer now does.维护者速读
@object-ui/types的FormField去掉了[key: string]: any开放索引签名,拼错或未声明的键在 TS 里直接报错;表单视图字段的immutable: true现在真的生效:编辑已有记录时该控件锁定,新建时仍可编辑。group)都能静默通过;immutable以前只是被抄到运行时字段上,没有任何渲染器读它,编辑表单里照样能改。这是本卡最后两项(席位裁决 A 与 immutable 跟进)。ui/packages/plugin-form/src/sectionFields.ts(immutable 那一行)、ui/packages/plugin-form/src/fieldGroups.ts(两个本地交接类型)、ui/packages/types/src/form.ts。pnpm --dir ui exec vitest run packages/plugin-form/src/sectionFields.spec-parity.test.ts packages/types/src/__tests__/form-field-closed-face-25923.test.ts,预期全绿。What changes
FormFieldis a closed authoring face (seat ruling 6412606977, letter A).ui/packages/types/src/form.tsdrops[key: string]: anyfromFormField. Every declared member is unchanged. The zod mirrorFormFieldSchemaand the strict faceStrictAnyComponentSchemaare unchanged: they already refused undeclared keys. The readers the drop broke are fixed by declaring each key where its producer and reader live (uiAGENTS.md#0.1), never with a cast toanyorRecord<string, unknown>:@object-ui/coreFieldWithDeclaredDefault(the parameter type ofisServerOwnedValue) drops its own[key: string]: unknown. An interface without an index signature is not assignable to a type that has one, so everyFormFieldcall site ofisServerOwnedValuebroke on it: the 5 in componentsform.tsxand the 1 in plugin-formWizardForm.tsxmeasured on 2026-10-26 clear with this one change. The 2 plugin-grid errors measured then do not reproduce:ObjectGrid.tsxno longer importsFormFieldon today'smain. Its test's bare{ name: 'title' }literal is now a typedFormFieldvalue.@object-ui/componentsform.tsx: the field's raw metadata carrier is typedRecord<string, unknown> | FormField(the stashed object field, else the field config itself, which is what the code already did) onRenderFieldProps.field,nativeNumberStepanddependentLookupParents, instead of converting the config through the signature.@object-ui/plugin-form:group, which the host copies from the object field andderiveFieldGroupSectionsreads back, is typed on a package-localFormFieldWithGroup(fieldGroups.ts).ObjectForm's generated field andbuildFlatFieldswrite it through that type. The section-divider row'scollapsible/collapsed/onToggleare typed on a module-localSectionDividerRow, which replaces aRecord<string, unknown>row cast toFormFieldand anas unknown as FormFieldgate row.SectionDividerSource.visibleWhenisFormField['visibleWhen'], notunknown.The immutable follow-up (the work-item-4 landing record 6448239783: "
immutableis declared on the mirror but not rendered by objectui (declared ≠ enforced). Render it, or record a refusal."). It is rendered.normalizeSectionFieldwrites a form-view field'simmutable: trueasdisabledwhen the form edits an existing record (isCreateFormModefalse, the same create-mode questionrequiredasks there). A create form leaves it editable. The old pass-through copy to a runtimeimmutablekey that no renderer read is gone. Thezod-mirror-paritycensus row moves fromcarried, not renderedtotranslated.Spec accept sets: none moves. The diff touches no
packages/specpath and no*.zod.ts. The objectui zod faces are byte-identical, soobjectui validateand the strict face judge every document as before. Only TypeScript consumers that wrote an undeclared key on a freshFormFieldliteral see a change, from their own compiler.Tests
ui/packages/types/src/__tests__/form-field-closed-face-25923.test.ts: fresh-literal@ts-expect-errorrows for a misspelled key (labell) and a host-written key (group), plus the declared spelling compiling. Reverse check: with the signature put back,tsc -p tsconfig.test.jsonreds both rows with TS2578 (unused directive); restored fromHEAD, emptygit diff HEAD.sectionFields.spec-parity.test.tsimmutablerow: an edit form (mode: 'edit',recordId) draws it disabled with noimmutablekey carried; a create form leaves it enabled.flatFields.test.tsparity pin spreads both producers into plain records instead of casting aFormFieldtoRecord<string, unknown>.Acceptance notes
keyField,disclosureandprecisionare still copied bynormalizeSectionFieldonto the runtime field through itsany-typed base, with no reader (the census rows say so). They are not onFormFieldand this PR does not put them there, since declaring keys no renderer reads would advertise them. The copy is a deliberate ADR-0007 pass-through in the mapper's own comment. Whether to keep carrying them or drop the copy is left to the seat.form.tsxreads its row throughfieldProps as any, as before. This PR types the producer side only.Verification
ui type-check, all dependents of @object-ui/types (head 43cf87a)
bash scripts/pm/os-verify-lock.sh -c "cd ui && pnpm exec turbo run type-check --filter='...@object-ui/types' --concurrency=2 --continue"gaveTasks: 105 successful, 105 totalandVERDICT command-exit 0. The first measurement, with only the signature dropped, failed in@object-ui/components(form.tsx824, 1946, 2021, 2117, 2257, 3359, 3824), then@object-ui/plugin-form(fieldGroups.ts369) once components was fixed. The new pin's owntsc -p tsconfig.test.jsonin@object-ui/typesexited 0 on the next head.ui vitest (head 6692b1a)
pnpm exec vitest run --maxWorkers=2 packages/plugin-form/ packages/core/src/validation/ packages/components/src/renderers/form/ packages/types/src/__tests__/form-field-closed-face-25923.test.ts packages/types/src/__tests__/zod-mirror-parity.test.tsunder the lock gaveTest Files 310 passed (310),Tests 3186 passed | 18 skipped (3204),VERDICT command-exit 0.pnpm --dir ui exec vitest run scripts/gave169 passed | 2 skipped,4901 passed | 2 skipped, exit 0.Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 34 commands, all exit 0 (codes captured before any pipe). The changeset gates were re-run after the last changeset edit.--ranreconciliation: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.check-adr-0087-registration:not-required (runtime-interface-only), verified on both interfaces.Generated by Claude Code