Repository navigation
refactor(plugin-form,types): the master-detail form's details entry is the spec's closed entry, by reference (objectui#11396) - #11624
Conversation
… is the spec's closed entry, by reference (objectui#11396) `@objectstack/spec` 17.6.0 judges `ComponentPropsMap['object-master-detail-form'].details` as an array of closed entries (objectstack-ai/objectstack#21215): `childObject` required, `columns` the spec's inline grid column, an undeclared key refused by name. `@object-ui/types` already took the whole props row by reference, so both published zod faces give the spec's verdict on an entry; the TypeScript face in `@object-ui/plugin-form`, `MasterDetailDetailConfig`, restated the entry by hand. - `MasterDetailDetailConfig` is derived from the spec's entry type (its `z.input` authoring face) by a type-only import, minus the one member the renderer does not read: the spec still declares `sortField`, which objectui#11070 round 9 retired here; the omission is stated on the type and pinned, and the spec-side retirement stays objectstack's half. Member for member the type is the shape it was, so the published accept set does not move (empty-frontmatter changeset). - The `object-master-detail-form` registration declares `of: 'object'` for `details`, derived from the now single-kind member contract, and a description. - `masterDetailDetailsMembers-8071.test.tsx` holds the config `Equal` to the spec entry minus `sortField`, pins that the spec still declares `sortField`, and that a `columns` entry is the grid's `GridColumn`; its header no longer says the spec row is `z.array(z.unknown())`. - New `packages/types` pin: both faces refuse an undeclared entry key, a missing `childObject`, a bare-string column and an undeclared column key, accept `{ childObject }` and `sortField`, with one verdict across the two doors. - The console parity row for `object-master-detail-form.details` describes the closed entry, the derivation and the `of`. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
✅ 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
|
…main too; the `sortField` fork is stated as retired upstream (objectui#11396, patch round 1) The `Spec Main Shape Gate` compiles this repository against `@objectstack/spec` built from objectstack `main`, where the detail entry's `sortField` is a `retiredKey()` tombstone (objectstack-ai/objectstack#21589, PR #21632, `6ec54f00`, unreleased after 17.6.0). The compile-time row `specStillDeclaresSortField` pinned the 17.6.0 shape and was red there. - Drop `specStillDeclaresSortField`. `derivedFromSpec`, `columnsAreTheGridColumn` and `memberList` hold on both shapes: the `Omit` subtracts the key whether the entry declares it as a string or as a tombstone. The forward tripwire for the retirement stays the runtime pin in `@object-ui/types`, which records `sortField` ACCEPTED on 17.6.0 and flips at the bump. - Say what is true now in the `MasterDetailForm.tsx` docblock, the members test header, the types pin header and row name, the registration description, the console parity row and the changeset body: the retirement has landed on objectstack `main`, is unreleased after 17.6.0, and the fork closes at objectui's bump to the first release carrying it. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
✅ 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
|
Contract reviewServed-tier: Head under review: the build commit Check-runs on the head (each reading over REST,
① Derived judgments1.
2. Patch round 1 — RIGHT. 3. The 4. The registration in
5. The console parity row 6. The tests — they bear weight, and the ablation reading is consistent with the diff.
7. Sentences the PR makes false — none left unjudged. ② Semver level
③ Boundary flagsDev deviations, round 0:
Dev deviations, round 1:
Out-of-scope findings, judged against the filing gate (classes a, b, c each owing a measured
The merge: base Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #11396
Clause-②: no — the published
MasterDetailDetailConfigkeeps its accept set: the derived type isEqualto the hand-written interface it replaces (a one-off compile below proves the FROM and TO shapes identical, member for member), so only the type's definition moves, and the changeset is the empty-frontmatter declaration.@objectstack/spec17.6.0 judgesComponentPropsMap['object-master-detail-form'].detailsas an array of CLOSED entries (objectstack-ai/objectstack#21215,a29a0ea555):childObjectrequired,columnsthe spec's inline grid column, an undeclared key refused by name.@object-ui/typesalready took the whole props row by reference, so both published zod faces give the spec's verdict on an entry. The TypeScript face in@object-ui/plugin-form,MasterDetailDetailConfig, restated the entry by hand, and the console parity row still said the spec row isz.array(z.unknown()). This PR makes the TypeScript face follow the spec entry by reference, makes the prose true, and pins both.Dispatched under claim
5982160146(domain:spec, size S; a re-dispatch of the same claim). Session:https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ.What changes
packages/plugin-form/src/MasterDetailForm.tsx:MasterDetailDetailConfigwas aninterfacewith eleven hand-written members. It is now a type DERIVED from the spec's entry — the element type ofObjectMasterDetailFormProps['details'], which is the spec'sz.inputauthoring face (what a document writes), imported type-only from@objectstack/spec/ui— with ONE member omitted by name,sortField(the fork, below). The docblock states the fork and where each member lands in the renderer. The two imports only the interface used (GridColumnfrom@object-ui/fields,InlineModefrom./deriveMasterDetail) are gone from this file; every other line is unchanged. TheamountField→ gridtotalFieldhand-off is untouched (the same six-key grid object, pinned by row 2 of the members test).packages/plugin-form/src/index.tsx: thedetailsregistration goes from{ name, type: 'array', required: true }to the same plusof: 'object'and a description.ofdecision and the declaration lives on the registration, not on the parity row. Therequired: trueis not touched (Acceptance note 1).packages/plugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsx: the header no longer says the spec row isz.array(z.unknown()); the compile-time block gains two rows —derivedFromSpec(the configEquals the spec entry withsortFieldomitted, on 17.6.0 and on objectstackmainalike: theOmitsubtracts the key whether the entry declares it as a string or as aretiredKey()tombstone) andcolumnsAreTheGridColumn(acolumnsentryEqualsGridColumn) — and keepsmemberList(the eleven keys spelled out) and the@ts-expect-errorrow on a writtensortField. Patch round 1 (999aecb6) dropped a third row,specStillDeclaresSortField, which pinned the 17.6.0 shape and was red against objectstackmainin theSpec Main Shape Gate. The runtime rows are unchanged.packages/types/src/__tests__/object-master-detail-form-details-entry-11396.test.ts(new; 7itblocks, 19 tests: six rows on each of two faces, six cross-door rows, one non-vacuity row): a type-level row that the block'sproperties.detailsentryEquals the spec's entry; on the tolerant face (safeValidateSchema, whichobjectui validateruns) and on the strict authoring face, for a node in the spec's{ type, properties }form:{ childObject }accepted (control); an undeclared entry key refused atproperties.details.0with oneunrecognized_keysnaming it; nochildObjectrefused withinvalid_typeat that member; a bare-string column refused atproperties.details.0.columns.0; a column with an undeclared key refused there naming it;sortFieldACCEPTED (the fork, recorded so that closing it is a deliberate flip). Plus one verdict across the two doors: for each probe,safeValidateSchemaand the spec's own props row agree on success and on the issue codes at the same paths underproperties.apps/console/src/__tests__/registry-inputs-spec-parity.test.ts: theobject-master-detail-form.detailsmember-pin prose. Every true sentence is kept, including the camelCase grid sentence objectui#11610 wrote; the last two sentences (thez.array(z.unknown())row, the five-of-eleven count, "declares noof") are replaced by the closed entry, the derivation, the fork and theof..changeset/11396-master-detail-details-entry.md: empty frontmatter, with a body saying why nothing is released.Premise readings (this worktree, base
9dfaca65, which isorigin/mainat PR time)pnpm install,packages/typesandpackages/plugin-formboth resolve@objectstack/specat17.6.0; both declare^17.6.0.ObjectMasterDetailFormPropsSchema.shape.details, read off the installed schema: an OPTIONAL array of az.objectwhose catchall isnever. The entry's keys, in the schema's own order:childObject(string, required), thenrelationshipField,columns,formFields,inlineMode,amountField,sortField,totalField,title,minRows,maxRows,addLabel, each optional.{}is refused withinvalid_typeatchildObject;{ childObject: 'x', bogus: 1 }is refused withunrecognized_keysnamingbogus.columns: an array of a strict object whose key set equals that ofInlineGridColumnSchemafrom@objectstack/spec/data, and whosez.inputtypeEqualsGridColumn(which IS thatInlineGridColumn), judged bytsc.===InlineGridColumnSchemaas@objectstack/spec/dataexports it, and@objectstack/spec/ui's ownInlineGridColumnSchemaexport is not===thedataone either — theuibundle carries its own instance. The card's "by identity" is a statement about objectstack's source. Nothing here depends on identity (Acceptance note 2).sortFieldis NOT retired at 17.6.0:{ childObject: 'x', sortField: 'a' }parses with success and the value kept. objectstackmainhas retired it since — aretiredKey()tombstone on the entry (spec(ui): retiresortFieldfrom theobject-master-detail-formdetail entry; the pinned console stopped reading it at objectui0a3e5409f, and the bump that crossed it did not carry the spec half objectstack#21589, PR feat(spec)!: retire the object-master-detail-form detail entry sortField — the console derives the line-position field from the child object (#21589) objectstack#21632,6ec54f00):git merge-base --is-ancestoranswers 0 for that commit against objectstackorigin/mainand 1 against the@objectstack/spec@17.6.0tag, witha29a0ea555as the control answering 0 on the same tag — so the retirement is unreleased after 17.6.0. The fork therefore stands on the installed spec; the runtime pin records it, and the compile-time rows hold on both shapes.inlineModeis the enumgrid | form, the same two valuesInlineModedeclares;formFieldsisstring[];titleandaddLabelare plain strings (notI18nLabel);minRows/maxRowsare numbers. So the derived members are the hand-written members' types.ObjectMasterDetailFormPropsParsedis exported alongsideObjectMasterDetailFormProps(thez.inputface). The authoring face is the one callers write, so it is the one derived from; the parsed face differs only inside a column (readonlyWhen/requiredWhennormalised to Expression envelopes).packages/typesneeded no edit:ObjectMasterDetailFormBlockSchema'spropertiesispropsBag(... stripImportedDefaults(SpecObjectMasterDetailFormPropsSchema)), and the new pin measures a bogus entry key refused on both faces with the spec's own code at the spec's own path.The fork:
sortField, reported and not absorbedThe spec entry declares
sortField;MasterDetailFormreads no such member (objectui#11070 round 9 retired the authored override — the line-position field is DERIVED from the child object byderiveDetail— and row 2c of the members test pins that a written one reaches nothing);MasterDetailDetailConfigleaves it off;objectui validateACCEPTS it, because@object-ui/typestakes the row by reference. Per the seat ruling this is reported with its measurement, not widened or narrowed here:The spec-side retirement has landed on objectstack
main(objectstack-ai/objectstack#21589, PR objectstack-ai/objectstack#21632,6ec54f00) and is unreleased after 17.6.0 (the card's section 3 recorded it as owed on objectstack-ai/objectstack#6017 before it landed). The fork closes at objectui's bump to the first@objectstack/specrelease that carries it. One row reds at that bump and says what to do: thesortFieldACCEPTED rows in the new types pin. It is a RUNTIME row on purpose: theSpec Main Shape Gatecompiles this repository against objectstackmain, so a compile-time row pinning the 17.6.0 shape is red there today — which is what patch round 1 corrected.The
ofdecisionThe parity file's own rule (
member declarations are derived from single-kind member contracts): a key is declared withofwhen the contract accepts exactly ONE coarse member kind in its member position, and left alone when it accepts several. On 17.5.0 thedetailsmember position accepted every kind (z.unknown()), so noofwas derivable. On 17.6.0 it acceptsobjectalone — the string, number, boolean and array probes are refused withinvalid_typeat the entry — soof: 'object'is derivable, and the registration now declares it. The gate's member direction witnesses it against the spec row (theobject-master-detail-form.details:of=objectjudgement iswitnessed, and the single-kind row readscontract accepts {object}; the parity run below is green with the declaration in place). Precedent:record:details.sectionsdeclaresof: 'object'for its strict spec entry.object-kanban.columnsdoes not and is not this card.FROM → TO, proven once
A temporary test file (archived in the dispatch scratchpad, deleted before the commit) copied the HEAD interface byte for byte as
OldMasterDetailDetailConfigand assertedEqualof OLD and NEW= true, beside a non-vacuity row assertingEqualof OLD and NEW-minus-titleisfalse.pnpm --filter @object-ui/plugin-form run type-check(tsc --noEmit && tsc -p tsconfig.test.json) exited 0 with that file in the test program (tsc -p tsconfig.test.json --listFilesnamesmasterDetailDetailsMembers-8071.test.tsx,MasterDetailForm.tsxand the spec'sdist/ui/index.d.ts). That is the whole of theClause-②: no.Tests and the ablation
pnpm exec vitest run packages/plugin-form/ apps/console/src/__tests__/registry-inputs-spec-parity.test.ts --maxWorkers=2(underos-verify-lock):Test Files 166 passed (166),Tests 2190 passed | 1 skipped (2191),VERDICT command-exit 0.pnpm exec vitest run packages/types/ --maxWorkers=2(under the lock):Test Files 358 passed (358),Tests 9519 passed (9519),VERDICT command-exit 0.Test Files 3 passed (3),Tests 283 passed (283).pnpm --filter @object-ui/plugin-form run type-check(both programs):VERDICT command-exit 0.pnpm --filter @object-ui/console run type-check(tsc --noEmit && tsc -b tsconfig.node.json --force, after building the console's dependency closure with pnpm):VERDICT command-exit 0. The first console run was refused on an unbuilt@object-ui/plugin-tree(TS2882, outside the doc-snippets closure); buildingplugin-treeandtest-supportand rerunning gave the green above.a4ac1f38; objectstack'sscripts/ablation-replace.mjsin WRAP mode overMasterDetailForm.tsx, child commandtsc -p tsconfig.test.json --noEmit):'sortField'with replacement'sortField' | 'addLabel'left the anchor count at 1 → 1, so the tool refused the mutation before running the child and restored (blob == HEAD). No reading was taken from it.'sortField'→ replacement'addLabel'(the derivation now omitsaddLabelinstead).anchor 1 -> 0,blob 2fa79a3713f6 -> 165e0f7923a5, mutation proven on disk. The child went RED with five errors: thederivedFromSpecrow and thememberListrow (Type 'true' is not assignable to type 'false'), theretiredSortFieldrow (Unused '@ts-expect-error' directive), and two renderer reads ofd.addLabelin the grid hand-off (Property 'addLabel' does not exist on type 'MasterDetailDetailConfig'). Restore:blob after restore 2fa79a3713f6,blob at HEAD 2fa79a3713f6,git diff HEAD empty,git statusclean. The rows and the renderer both hang off the derivation.../MasterDetailFormfrom source and reads the spec from its installed.d.ts; nodist/of this package is in the path, so no dist preflight applies.Gate readings, at head
a4ac1f38eslint --no-inline-config --format jsonover the five touched TypeScript files: 5 files, 0 errors. Warnings: 0 on the parity file and on the new pin; onMasterDetailForm.tsx, the members test andindex.tsxevery warning isno-explicit-anyon a pre-existinganyline,react-hooks/exhaustive-depsorreact-refresh/only-export-componentson lines this diff does not touch.pnpm lint. Population:eslint.config.jslints**/*.{ts,tsx}; the changeset.mdis outside it. File count: 5, from the JSON output. Invariance: the config sets noparserOptions.projectand noprojectService, so type-aware linting is off and this diff cannot move the verdict on any untouched file. The full run belongs to CI.check:doc-snippets:Semantic phase: 777 of 777 block(s) judged, 0 failed.check:new-line-citations:0 new citation(s).check:control-bytes: OK.changeset:check(fixed and no-major),check:changeset-claims,check:pending-changeset-literals,check-changeset-presence("Every one of them has an EMPTY frontmatter — declared as releasing nothing"),check-changeset-overwrite.check:spec-symbols,check:spec-floors(first refusedno-artifacton the unbuiltplugin-tree; green after the build above),check:installed-pin-claims,check:phantom-deps,check:unused-deps,check:readme-exports,check:test-path-roots,check:component-surface-parity,check:doc-types.check-governed-queue-guard --testover the six paths: NOT GOVERNED.pnpm test, the tree-widepnpm lint,check:published-dist.Acceptance notes
detailsis optional on the spec row and required on the registration and the TypeScript face. The spec'sdetailsisz.ZodOptional; the registration declaresrequired: true(kept as it was),MasterDetailFormSchema.detailsis required, and the renderer itself tolerates absence (schema.details || []). objectui#11605 records the mechanism by which arequired: truethe spec row does not carry refuses a node at the page compile ("its only requiredness check isinput.required && !(input.name in node)"); that card's rule is about binding-supplied keys, which this one is not. Not measured through the page compile here, and not this card. Carrier: none.InlineGridColumnSchemaacross@objectstack/spec's published entry points (Premise readings). Any objectui pin that asserts cross-entry-point identity would be false; none does today, and this PR adds none. Carrier: none.ofand by the pins, not by that prose. It names the two things the spec's description cannot tell an author:amountFieldis the child column summed andtotalFieldthe parent field it is saved to, andsortFieldis read by nothing here.Patch round 1 —
999aecb6(seat ruling5986267745)The
Spec Main Shape Gatewas red ona4ac1f38with one attributed diagnostic:masterDetailDetailsMembers-8071.test.tsx, TS2322, compiled against@objectstack/specbuilt from objectstackebfe658c7241. Confirmed: objectstackmaincarriessortField: retiredKey(MASTER_DETAIL_DETAIL_SORT_FIELD_RETIRED)on the detail entry (retiredKey()isz.never().optional()), from objectstack-ai/objectstack#21589 / PR #21632 /6ec54f00, which is an ancestor oforigin/mainand not of the@objectstack/spec@17.6.0tag (control:a29a0ea555is an ancestor of the tag). The rowspecStillDeclaresSortFieldpinned the 17.6.0 shape and failed there.specStillDeclaresSortFielddropped.derivedFromSpec,columnsAreTheGridColumn,memberListand the@ts-expect-errorrow stay, and hold on both shapes (measured below). The forward tripwire is the runtimesortFieldACCEPTED pin inpackages/types, renamed to say so; it flips at the bump.MasterDetailForm.tsxdocblock, the members-test header, the types-pin header, the registration description, the console parity row and the changeset body now say the retirement landed on objectstackmain, is unreleased after 17.6.0, and that the fork closes at objectui's bump to the first release carrying it.maine83c9f615441in a scratch worktree (packed withnpm pack, injected withscripts/spec-main-shape-gate.mjs inject;no dependency substitution),TURBO_FORCE=trueand a scratch--cache-dir, underos-verify-lock:@object-ui/plugin-form, the attributed test file checked out froma4ac1f38while HEAD held the fix):turbo run type-check --filter=@object-ui/plugin-form --concurrency=2 --continueexited 2; the gate'sreportprinted1 diagnostic(s) in 1 objectui file(s)— the members test, TS2322Type 'true' is not assignable to type 'false'. Restore proven: blob == HEAD,git diff HEADempty.999aecb6):pnpm type-check --continue --concurrency=2—Tasks: 81 successful, 81 total,Cached: 0 cached, 9m26s;report:✅ objectui type-checks against @objectstack/spec at that commit.0inject markers in the store): plugin-form closure rebuilt,pnpm --filter @object-ui/plugin-form run type-checkexit 0, the three touched test files3 passed,283tests,pnpm --filter @object-ui/console run type-checkexit 0.Clause-②: nore-measured on999aecb6: the pre-PR interface at9dfaca65, byte-copied,Equals the derived type (tsc -p tsconfig.test.jsonexit 0, temp file deleted before push).999aecb6(anchor'sortField'→'addLabel'): mutation landed (anchor 1 -> 0, blob changed), five errors —derivedFromSpec,memberList, the unused@ts-expect-error, two renderer reads ofd.addLabel— restore proven (blob == HEAD,git diff HEADempty).999aecb6, exit 0 each: eslint over the five touched TypeScript files (0 errors),check:installed-pin-claims,check:spec-symbols,check:control-bytes,check:new-line-citations(0 new),changeset:check,check:changeset-claims,check:pending-changeset-literals,check-changeset-presence,check-changeset-overwrite.Serial: objectui#11170 was dispatched in this lane during this round and shares no file with this PR.
origin/mainhas not moved since this branch was fast-forwarded to9dfaca65, so the merge before pushing was a no-op in both rounds.Generated by Claude Code