Skip to content

refactor(plugin-form,types): the master-detail form's details entry is the spec's closed entry, by reference (objectui#11396) - #11624

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11396-master-detail-details-entry
Oct 5, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11396-master-detail-details-entry

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #11396
Clause-②: no — the published MasterDetailDetailConfig keeps its accept set: the derived type is Equal to 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/spec 17.6.0 judges ComponentPropsMap['object-master-detail-form'].details as an array of CLOSED entries (objectstack-ai/objectstack#21215, a29a0ea555): 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, and the console parity row still said the spec row is z.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: MasterDetailDetailConfig was an interface with eleven hand-written members. It is now a type DERIVED from the spec's entry — the element type of ObjectMasterDetailFormProps['details'], which is the spec's z.input authoring 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 (GridColumn from @object-ui/fields, InlineMode from ./deriveMasterDetail) are gone from this file; every other line is unchanged. The amountField → grid totalField hand-off is untouched (the same six-key grid object, pinned by row 2 of the members test).
  • packages/plugin-form/src/index.tsx: the details registration goes from { name, type: 'array', required: true } to the same plus of: 'object' and a description. ⚠️ This file is outside the claim's file surface; it is here because the dispatch asked for the of decision and the declaration lives on the registration, not on the parity row. The required: true is not touched (Acceptance note 1).
  • packages/plugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsx: the header no longer says the spec row is z.array(z.unknown()); the compile-time block gains two rows — derivedFromSpec (the config Equals the spec entry with sortField omitted, on 17.6.0 and on objectstack main alike: the Omit subtracts the key whether the entry declares it as a string or as a retiredKey() tombstone) and columnsAreTheGridColumn (a columns entry Equals GridColumn) — and keeps memberList (the eleven keys spelled out) and the @ts-expect-error row on a written sortField. Patch round 1 (999aecb6) dropped a third row, specStillDeclaresSortField, which pinned the 17.6.0 shape and was red against objectstack main in the Spec Main Shape Gate. The runtime rows are unchanged.
  • packages/types/src/__tests__/object-master-detail-form-details-entry-11396.test.ts (new; 7 it blocks, 19 tests: six rows on each of two faces, six cross-door rows, one non-vacuity row): a type-level row that the block's properties.details entry Equals the spec's entry; on the tolerant face (safeValidateSchema, which objectui validate runs) and on the strict authoring face, for a node in the spec's { type, properties } form: { childObject } accepted (control); an undeclared entry key refused at properties.details.0 with one unrecognized_keys naming it; no childObject refused with invalid_type at that member; a bare-string column refused at properties.details.0.columns.0; a column with an undeclared key refused there naming it; sortField ACCEPTED (the fork, recorded so that closing it is a deliberate flip). Plus one verdict across the two doors: for each probe, safeValidateSchema and the spec's own props row agree on success and on the issue codes at the same paths under properties.
  • apps/console/src/__tests__/registry-inputs-spec-parity.test.ts: the object-master-detail-form.details member-pin prose. Every true sentence is kept, including the camelCase grid sentence objectui#11610 wrote; the last two sentences (the z.array(z.unknown()) row, the five-of-eleven count, "declares no of") are replaced by the closed entry, the derivation, the fork and the of.
  • .changeset/11396-master-detail-details-entry.md: empty frontmatter, with a body saying why nothing is released.

Premise readings (this worktree, base 9dfaca65, which is origin/main at PR time)

  • After pnpm install, packages/types and packages/plugin-form both resolve @objectstack/spec at 17.6.0; both declare ^17.6.0.
  • ObjectMasterDetailFormPropsSchema.shape.details, read off the installed schema: an OPTIONAL array of a z.object whose catchall is never. The entry's keys, in the schema's own order: childObject (string, required), then relationshipField, columns, formFields, inlineMode, amountField, sortField, totalField, title, minRows, maxRows, addLabel, each optional. {} is refused with invalid_type at childObject; { childObject: 'x', bogus: 1 } is refused with unrecognized_keys naming bogus.
  • columns: an array of a strict object whose key set equals that of InlineGridColumnSchema from @objectstack/spec/data, and whose z.input type Equals GridColumn (which IS that InlineGridColumn), judged by tsc. ⚠️ Runtime identity does not hold across the published entry points: the entry's column element is not === InlineGridColumnSchema as @objectstack/spec/data exports it, and @objectstack/spec/ui's own InlineGridColumnSchema export is not === the data one either — the ui bundle carries its own instance. The card's "by identity" is a statement about objectstack's source. Nothing here depends on identity (Acceptance note 2).
  • sortField is NOT retired at 17.6.0: { childObject: 'x', sortField: 'a' } parses with success and the value kept. objectstack main has retired it since — a retiredKey() tombstone on the entry (spec(ui): retire sortField from the object-master-detail-form detail entry; the pinned console stopped reading it at objectui 0a3e5409f, 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-ancestor answers 0 for that commit against objectstack origin/main and 1 against the @objectstack/spec@17.6.0 tag, with a29a0ea555 as 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.
  • inlineMode is the enum grid | form, the same two values InlineMode declares; formFields is string[]; title and addLabel are plain strings (not I18nLabel); minRows / maxRows are numbers. So the derived members are the hand-written members' types.
  • ObjectMasterDetailFormPropsParsed is exported alongside ObjectMasterDetailFormProps (the z.input face). The authoring face is the one callers write, so it is the one derived from; the parsed face differs only inside a column (readonlyWhen / requiredWhen normalised to Expression envelopes).
  • packages/types needed no edit: ObjectMasterDetailFormBlockSchema's properties is propsBag(... 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 absorbed

The spec entry declares sortField; MasterDetailForm reads no such member (objectui#11070 round 9 retired the authored override — the line-position field is DERIVED from the child object by deriveDetail — and row 2c of the members test pins that a written one reaches nothing); MasterDetailDetailConfig leaves it off; objectui validate ACCEPTS it, because @object-ui/types takes the row by reference. Per the seat ruling this is reported with its measurement, not widened or narrowed here:

  • widening the TypeScript face to the full spec entry would declare, on the authoring face, a key the runtime does not honour;
  • narrowing the zod face would be a consumer-side departure from the spec row the mirror exists to take by reference.

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/spec release that carries it. One row reds at that bump and says what to do: the sortField ACCEPTED rows in the new types pin. It is a RUNTIME row on purpose: the Spec Main Shape Gate compiles this repository against objectstack main, so a compile-time row pinning the 17.6.0 shape is red there today — which is what patch round 1 corrected.

The of decision

The parity file's own rule (member declarations are derived from single-kind member contracts): a key is declared with of when the contract accepts exactly ONE coarse member kind in its member position, and left alone when it accepts several. On 17.5.0 the details member position accepted every kind (z.unknown()), so no of was derivable. On 17.6.0 it accepts object alone — the string, number, boolean and array probes are refused with invalid_type at the entry — so of: 'object' is derivable, and the registration now declares it. The gate's member direction witnesses it against the spec row (the object-master-detail-form.details:of=object judgement is witnessed, and the single-kind row reads contract accepts {object}; the parity run below is green with the declaration in place). Precedent: record:details.sections declares of: 'object' for its strict spec entry. object-kanban.columns does 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 OldMasterDetailDetailConfig and asserted Equal of OLD and NEW = true, beside a non-vacuity row asserting Equal of OLD and NEW-minus-title is false. 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 --listFiles names masterDetailDetailsMembers-8071.test.tsx, MasterDetailForm.tsx and the spec's dist/ui/index.d.ts). That is the whole of the Clause-②: no.

Tests and the ablation

  • pnpm exec vitest run packages/plugin-form/ apps/console/src/__tests__/registry-inputs-spec-parity.test.ts --maxWorkers=2 (under os-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.
  • The three touched test files by name (under the lock): 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); building plugin-tree and test-support and rerunning gave the green above.
  • Ablation (the fix committed first, as a4ac1f38; objectstack's scripts/ablation-replace.mjs in WRAP mode over MasterDetailForm.tsx, child command tsc -p tsconfig.test.json --noEmit):
    • A first attempt was a NO-OP by the tool's own evidence and is reported as such: anchor '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.
    • The real leg: anchor 'sortField' → replacement 'addLabel' (the derivation now omits addLabel instead). anchor 1 -> 0, blob 2fa79a3713f6 -> 165e0f7923a5, mutation proven on disk. The child went RED with five errors: the derivedFromSpec row and the memberList row (Type 'true' is not assignable to type 'false'), the retiredSortField row (Unused '@ts-expect-error' directive), and two renderer reads of d.addLabel in 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 status clean. The rows and the renderer both hang off the derivation.
    • Resolution path: the test program compiles ../MasterDetailForm from source and reads the spec from its installed .d.ts; no dist/ of this package is in the path, so no dist preflight applies.

Gate readings, at head a4ac1f38

  • eslint --no-inline-config --format json over the five touched TypeScript files: 5 files, 0 errors. Warnings: 0 on the parity file and on the new pin; on MasterDetailForm.tsx, the members test and index.tsx every warning is no-explicit-any on a pre-existing any line, react-hooks/exhaustive-deps or react-refresh/only-export-components on lines this diff does not touch.
    • This is a declared narrowing of pnpm lint. Population: eslint.config.js lints **/*.{ts,tsx}; the changeset .md is outside it. File count: 5, from the JSON output. Invariance: the config sets no parserOptions.project and no projectService, 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 checkers, exit 0 each: 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.
  • Exit 0 each: check:spec-symbols, check:spec-floors (first refused no-artifact on the unbuilt plugin-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 --test over the six paths: NOT GOVERNED.
  • NOT MEASURED, left to CI: the full pnpm test, the tree-wide pnpm lint, check:published-dist.

Acceptance notes

  1. details is optional on the spec row and required on the registration and the TypeScript face. The spec's details is z.ZodOptional; the registration declares required: true (kept as it was), MasterDetailFormSchema.details is required, and the renderer itself tolerates absence (schema.details || []). objectui#11605 records the mechanism by which a required: true the spec row does not carry refuses a node at the page compile ("its only requiredness check is input.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.
  2. Two instances of InlineGridColumnSchema across @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.
  3. The registration's new description is authoring help for the manifest and the designer; the member SHAPE is carried by of and by the pins, not by that prose. It names the two things the spec's description cannot tell an author: amountField is the child column summed and totalField the parent field it is saved to, and sortField is read by nothing here.

Patch round 1 — 999aecb6 (seat ruling 5986267745)

The Spec Main Shape Gate was red on a4ac1f38 with one attributed diagnostic: masterDetailDetailsMembers-8071.test.tsx, TS2322, compiled against @objectstack/spec built from objectstack ebfe658c7241. Confirmed: objectstack main carries sortField: retiredKey(MASTER_DETAIL_DETAIL_SORT_FIELD_RETIRED) on the detail entry (retiredKey() is z.never().optional()), from objectstack-ai/objectstack#21589 / PR #21632 / 6ec54f00, which is an ancestor of origin/main and not of the @objectstack/spec@17.6.0 tag (control: a29a0ea555 is an ancestor of the tag). The row specStillDeclaresSortField pinned the 17.6.0 shape and failed there.

  • Rows: specStillDeclaresSortField dropped. derivedFromSpec, columnsAreTheGridColumn, memberList and the @ts-expect-error row stay, and hold on both shapes (measured below). The forward tripwire is the runtime sortField ACCEPTED pin in packages/types, renamed to say so; it flips at the bump.
  • Sentences: the MasterDetailForm.tsx docblock, 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 objectstack main, is unreleased after 17.6.0, and that the fork closes at objectui's bump to the first release carrying it.
  • Local reproduction of the gate, with the spec built from objectstack main e83c9f615441 in a scratch worktree (packed with npm pack, injected with scripts/spec-main-shape-gate.mjs inject; no dependency substitution), TURBO_FORCE=true and a scratch --cache-dir, under os-verify-lock:
    • RED (declared narrowing: turbo filtered to @object-ui/plugin-form, the attributed test file checked out from a4ac1f38 while HEAD held the fix): turbo run type-check --filter=@object-ui/plugin-form --concurrency=2 --continue exited 2; the gate's report printed 1 diagnostic(s) in 1 objectui file(s) — the members test, TS2322 Type 'true' is not assignable to type 'false'. Restore proven: blob == HEAD, git diff HEAD empty.
    • GREEN (the gate's full step, on 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.
  • Back on 17.6.0 (reinstalled; 0 inject markers in the store): plugin-form closure rebuilt, pnpm --filter @object-ui/plugin-form run type-check exit 0, the three touched test files 3 passed, 283 tests, pnpm --filter @object-ui/console run type-check exit 0.
  • Clause-②: no re-measured on 999aecb6: the pre-PR interface at 9dfaca65, byte-copied, Equals the derived type (tsc -p tsconfig.test.json exit 0, temp file deleted before push).
  • Ablation redone on 999aecb6 (anchor 'sortField' → 'addLabel'): mutation landed (anchor 1 -> 0, blob changed), five errors — derivedFromSpec, memberList, the unused @ts-expect-error, two renderer reads of d.addLabel — restore proven (blob == HEAD, git diff HEAD empty).
  • Gates on 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/main has not moved since this branch was fast-forwarded to 9dfaca65, so the merge before pushing was a no-op in both rounds.


Generated by Claude Code

… 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>
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3314.4 KB 3330.4 KB
Main entry chunk (gzip) 151.6 KB 350 KB
Entry file index-BW7i11CH.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.22KB 6.37KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 573.73KB 137.60KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 232.57KB 64.51KB
fields (index.js) 262.75KB 66.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 39.47KB 11.25KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 143.54KB 38.79KB
plugin-designer (index.js) 231.41KB 48.84KB
plugin-detail (index.js) 245.97KB 64.67KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.02KB 45.85KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 235.92KB 64.87KB
plugin-kanban (index.js) 50.06KB 15.74KB
plugin-list (index.js) 116.72KB 29.10KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.36KB 2.51KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…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>
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3314.5 KB 3330.4 KB
Main entry chunk (gzip) 151.6 KB 350 KB
Entry file index-CcyPbuHe.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.22KB 6.37KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 573.73KB 137.60KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 232.57KB 64.51KB
fields (index.js) 262.75KB 66.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 39.47KB 11.25KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 143.54KB 38.79KB
plugin-designer (index.js) 231.41KB 48.84KB
plugin-detail (index.js) 245.97KB 64.67KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 235.92KB 64.87KB
plugin-kanban (index.js) 50.06KB 15.74KB
plugin-list (index.js) 116.72KB 29.10KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.36KB 2.51KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 999aecb65710d0f57cdc326fbc65524ff22721ef
Local-runs: none

Head under review: the build commit a4ac1f38 plus patch round 1 999aecb6, on main at 9dfaca65 (still origin/main at this write, so the merge is a no-op). Inputs: card #11396 (its body and its seven comments: the triage grade 5938428578 and its correction 5938547712, the hold release 5982107569, the claim 5982160146, the dev report 5986159424, the seat ruling 5986267745, the round-1 report 5986486723), PR #11624 (body, file list, net diff against main), the check-runs on the head, and — for the premise only — objectstack's @objectstack/spec@17.6.0 tag and origin/main, read with git show (no node_modules/@objectstack/spec is installed in the sibling checkout). Nothing was checked out, built, run or re-run: the dev's local readings quoted below are reports, the check-runs are the verdicts.

Check-runs on the head (each reading over REST, commits/{sha}/check-runs):

  • 2026-10-05T01:23Z — 42 runs: 11 in progress (the eight test shards, Build Docs, Type Check, Spec Main Shape Gate), 28 success, 3 skipped.
  • 2026-10-05T01:26Z — 10 in progress (Build Docs completed, success).
  • 2026-10-05T01:30Z — 8 in progress (Type Check and shard 5 completed, success).
  • 2026-10-05T01:32Z — 2 in progress (shards 1 and 4); Spec Main Shape Gate completed success after a thirteen-minute run.
  • 2026-10-05T01:35Z — 43 runs (the 43rd is the aggregate Test job, created once the last shard finished; success), 0 in progress: 40 success, 3 skipped. The three skips are the expected ones: Test (coverage shard N/4) and Test (coverage) are push-only by their if: in ci.yml, and dependabot is gated on the dependabot actor. No failure, no cancellation, nothing neutral. The verdict below rests on this reading and on no in-progress run.

① Derived judgments

1. MasterDetailDetailConfig, hand-written interface to a type derived from the spec entry — RIGHT.

  • A true reference, not a restatement: the only hand-written fact about the shape is the one omitted key. The type is the Omit on sortField of the element of ObjectMasterDetailFormProps['details'] (NonNullable first, then the element), imported type-only from @objectstack/spec/ui. At the tag ObjectMasterDetailFormProps is the z.input face (ADR-0122: the bare name is the author state), so the derivation is on the authoring face, the one a caller writes. The two imports only the old interface used (GridColumn, InlineMode) are gone; nothing else in the file moves.
  • FROM = TO is credible from the diff and the 17.6.0 source, not only from the dev's one-off compile. The 17.6.0 entry (masterDetailDetailEntry() in the spec's ui/component.zod.ts at the tag) is strictObject(...) over exactly twelve keys: childObject: z.string() required; relationshipField, amountField, sortField, totalField, title, addLabel each z.string().optional(); formFields z.array(z.string()).optional(); inlineMode z.enum(['grid', 'form']).optional(); minRows / maxRows z.number().optional(); columns z.array(InlineGridColumnSchema).optional(). The old interface's eleven members are those twelve minus sortField, type for type: InlineMode is 'grid' | 'form' in deriveMasterDetail.ts; GridColumn in @object-ui/fields IS InlineGridColumn, which the tag defines as z.input of InlineGridColumnSchema; title and addLabel are plain strings on both sides. The wrappers add nothing to the type: lazySchema and closedObject / strictObject are type-identity (T in, T out; the alias table is parse-time guidance, not a key), and no exactOptionalPropertyTypes is set in the tree, so key?: string and zod's key?: string | undefined are the same member. The compiled witness on the head is the green Type Check run: plugin-form's type-check is tsc --noEmit plus tsc -p tsconfig.test.json, whose include names src/**/*.test.tsx, so the memberList row (exactly the eleven keys) and derivedFromSpec sit in a compiled program, not a phantom one. The accept set of the published type does not move.

2. Patch round 1 — RIGHT. specStillDeclaresSortField is gone from the net diff; what remains (derivedFromSpec, columnsAreTheGridColumn, memberList, the @ts-expect-error row on a written sortField) holds on both shapes by construction: the Omit subtracts the key whether the entry declares it as z.string().optional() (the tag) or as retiredKey(...) (origin/main, where retiredKey is z.never(...).optional()), and keyof after the Omit is the same eleven either way. Premise re-read in the objectstack checkout: 6ec54f00 (objectstack-ai/objectstack#21589, landed by PR #21632) is an ancestor of origin/main (e83c9f61, the commit the dev's reproduction built) and NOT of the @objectstack/spec@17.6.0 tag; the control a29a0ea555 is an ancestor of both; no spec tag later than 17.6.0 exists in the checkout. So "landed on objectstack main, unreleased after 17.6.0" is true. The reading of record is the Spec Main Shape Gate on this head: success, a pull_request trigger with no paths: filter (the seat's "red on every PR once merged" concern was real and is cleared). The dev's red-then-green reproduction is consistent with it and is a report. The runtime ACCEPTED pin in packages/types is an adequate forward tripwire: at the bump that carries the tombstone, { childObject, sortField } is refused on both faces, the row reds, and its comment names the flip and the Omit drop; it is a runtime row on purpose, so the Spec Main Shape Gate is not the one that reds. One note for whoever takes that bump, not a finding today: the row's remedy names two edits, while five prose sites will read stale at that moment — the MasterDetailForm.tsx docblock, the members-test header, the types-pin header, the registration description and the parity row. The corrected sentences are true at the head: docblock, members-test header, types-pin header, registration description, parity row, changeset body and PR body all say the same three things (landed on main at #21589, unreleased after 17.6.0, closes at objectui's bump to the first release carrying it), and each is what the objectstack checkout says.

3. The sortField fork — reported, not absorbed; the omission is consistent with the rules. Measured on all four faces and stated in the PR: the tag declares it; MasterDetailForm reads no d.sortField (the grid is handed entry.sortField, the derived one; row 2c of the members test pins that a written one reaches nothing); the TypeScript face omits it; both zod faces accept it, pinned ACCEPTED so that closing it is a deliberate flip. Neither direction was taken silently: widening the TypeScript face would re-declare a key objectui#11070 round 9 retired and the renderer does not honour; narrowing the zod faces would make @object-ui/types depart from the row it takes by reference. Relative to the previously published TypeScript face the omission moves nothing (the member has been gone since #11070, shipped then as yes (narrowing)); relative to the spec it is a stated, pinned, one-key fork that the spec itself is closing. That is the seat ruling's shape — a fork with its measurement — and it keeps #0 / #0.1 (the spec stays the contract; objectui adds no alias and no fallback).

4. The registration in packages/plugin-form/src/index.tsx (details gains of: 'object' and a description; required: true stays) — RIGHT; the deviation is accepted. What of changes, reader by reader (the readers git grep finds are packages/sdui-parser — validate.ts, codegen.ts, the manifest canonicaliser — and the console parity gate; plugin-designer reads none):

  • the parity gate's member direction (judgeMembers) gains one judgement, object-master-detail-form.details:of=object, witnessed against the 17.6.0 row (string, number, boolean and array probes are refused for their kind at the entry; {} is refused one level down at childObject, which the gate folds as "accepts the kind"), and the pin member declarations are derived from single-kind member contracts holds because the contract now accepts exactly {object}; the console project runs inside the sharded pnpm test, green;
  • the page compile (validateTree, checkMemberTypes) now emits ONE member-type-mismatch diagnostic at severity warning for a details array holding a non-object member — a warning, not a refusal, and for a value the spec refuses at parse anyway;
  • the codegen (memberTsType) narrows the generated authoring type of details from an array of unknown to an array of string-keyed records of unknown — again only onto values the contract already refuses; this lands when objectstack regenerates its manifest at the next .objectui-sha bump, which is routine there;
  • objectui check and objectui validate judge through safeValidateSchema, which reads the spec row, not of.
    So no reader accepts anything the spec refuses or refuses anything the spec accepts. The deviation is justified by the repository's own derivation rule rather than by the dispatch order (not an input here): ComponentInput.of is declared where the member contract accepts exactly ONE coarse kind, and the parity file pins both halves (a stale MULTI_KIND_MEMBER_CONTRACTS entry "must be deleted once the contract collapses to one kind"); on 17.5.0 details accepted every kind, on 17.6.0 it accepts object alone, so the key became derivable and its only home is the registration. record:details.sections is the standing precedent (of: 'object' on a strict spec entry). The description is authoring help and carries no shape (the dev's Acceptance note 3); its sentences are true at the head. required: true is untouched and is Acceptance note 1 (③ below).

5. The console parity row object-master-detail-form.details — RIGHT. Compared byte for byte against main: the first 1909 characters are identical (every sentence up to "wrapped only to record the props it is handed", the objectui#11610 camelCase-grid sentence included), the closing "New file (objectui#8071 slice 18)." is kept, and only the two sentences the bump made false are replaced (the z.array(z.unknown()) row, "five of the eleven", "declares no of"). Each new sentence against 17.6.0: closed entries, childObject required, columns the inline grid column, an undeclared member key refused by name — the tag's strictObject; "@object-ui/types takes that row by reference" — ObjectMasterDetailFormBlockSchema.properties is propsBag(stripImportedDefaults(SpecObjectMasterDetailFormPropsSchema)); "both published faces" — the tolerant face safeValidateSchema (what objectui validate and objectui check call) and StrictAnyComponentSchema, both pinned in the new types test; "held Equal ... with ONE stated fork" — the derivedFromSpec row, equal to the entry minus sortField exactly as the sentence goes on to say; the retirement sentence — the objectstack checkout as in ①.2; "declares of: 'object'" — the head's registration.

6. The tests — they bear weight, and the ablation reading is consistent with the diff.

  • derivedFromSpec pins the EXPORT's shape against the spec entry minus the fork, whatever definition stands behind the export; memberList spells the eleven keys, so a key the spec adds or drops is a red row, never a silent widening — the "never silently widen" rule, mechanised; columnsAreTheGridColumn ties the entry's column to GridColumn through the derivation, so a drift between @object-ui/fields' column and the spec's reds here. All three compile in plugin-form's test program (Type Check green).
  • The new packages/types pin (7 it blocks, 19 tests): a type-level Equal of the mirror's properties.details element to the spec's, with a non-vacuity row, compiled by packages/types' tsc -p tsconfig.test.json (its include names src/**/*.test.ts); six runtime rows on each of two faces at the spec's own paths and codes; six cross-door rows comparing safeValidateSchema with the spec row itself; one non-vacuity row over the probe table. These red on a hand-mirrored details, on a type-changing derivation in types, and at the bump (the ACCEPTED row). The test shards are green.
  • Ablation (dev report, both rounds): anchoring 'sortField' to 'addLabel' in the derivation moves the blob, and the child reds with five diagnostics — derivedFromSpec and memberList (true not assignable to false), the now-unused @ts-expect-error, and two d.addLabel renderer reads — exactly what the diff predicts (one line of MasterDetailLines reads d.addLabel twice). The first attempt was a tool-refused no-op and is reported as such. The Equal row can go red.

7. Sentences the PR makes false — none left unjudged. git grep at the head: no docblock, README or docs page describes the master-detail details as z.array(z.unknown()), unjudged or hand-mirrored. The remaining z.array(z.unknown()) mentions in the parity file belong to other keys' rows and are true of those rows. packages/plugin-form/README.md lists MasterDetailDetailConfig among the exports and describes no entry shape; content/docs/plugins/plugin-form.mdx describes none; packages/types/src/zod/README.md says the public blocks' properties are the spec rows by reference, which is true. The spec's own 17.6.0 docblock cites objectui's MasterDetailDetailConfig at the pin 31971ff1e28f — objectstack's text, not this PR's. One near-miss noted, not a finding: a comment in apps/console/src/__tests__/public-block-binding-reach.test.tsx says the declared TYPE array "carries no information about the ENTRY"; the type is still array, and of: 'object' still cannot supply childObject, so the sample stays keyed by name for the reason the comment gives.

② Semver level

Clause-②: no, arm none — read with readClause2Line from the PR body: kind: declared, value: no, arm: null. The changeset .changeset/11396-master-detail-details-entry.md has empty frontmatter and a body saying why nothing is released. This matches the diff under the fixed-group, no-major policy:

③ Boundary flags

Dev deviations, round 0:

  1. File surface grew by packages/plugin-form/src/index.tsx — ACCEPTED (①.4): the of declaration is derivable only since 17.6.0 and lives nowhere but the registration; declared in the PR body and in both reports.
  2. The first ablation attempt was a tool-refused no-op (the replacement still contained the anchor) — ACCEPTED as reported; the reading rests on the second leg alone.
  3. Commit trailers are the model-free pair AGENTS.md prescribes, not the harness reminder's model-named line — ACCEPTED: objectstack AGENTS.md names that pair as the rule and the harness trailer as a reporting exemption; both commits on the head carry the pair and no model identifier.

Dev deviations, round 1:
4. The red leg a declared narrowing (turbo filtered to plugin-form, the attributed file at a4ac1f38) with the green leg the gate's full step — ACCEPTED; the gate's own run on the head is the verdict either way.
5. objectstack main advanced from ebfe658c to e83c9f61 between the seat's reading and the reproduction — ACCEPTED; 6ec54f00 is an ancestor of e83c9f61 (checked here), so the shape compiled against is the retired one.

open_questions: empty in both reports; nothing to answer or escalate.

Out-of-scope findings, judged against the filing gate (classes a, b, c each owing a measured reach:; otherwise Acceptance notes):

  • Acceptance note 1 — details is optional on the spec row, required: true on the registration, required on MasterDetailFormSchema, and tolerated absent by the renderer (schema.details || []). Class (b) candidate: the page compile's missing-required-prop branch is severity error, so a spec-valid node without details would be refused there; reach: NOT measured (no page-compile or os validate probe in the record, and this review runs none). Per the gate, no reach means no card: the Acceptance note stands. Escalated to the seat as a one-probe filing: one validateTree / os validate run on a node carrying objectName and no details converts it into a (b) filing with its Seam: line, or shows it unreachable. Not this card, and keeping required: true here was the correct scope boundary: dropping it moves what the page compile refuses, a measured change of its own (the registrations still declare objectName required where the dataSource binding can supply it, so the page compile refuses bound object-grid / object-kanban / object-metric nodes (the objectui#11569 family's closing card) #11605 family did exactly that for objectName, each with a minor changeset).
  • Acceptance note 2 — two InlineGridColumnSchema instances across @objectstack/spec's published entry points (ui carries its own; the entry's column element is not === data's). An observation with no consumer: nothing in objectui pins cross-entry-point identity, the key sets and types are equal, and the card's "by identity" is a statement about objectstack's source. Per the gate (a read-only observation does not file): Acceptance note, no card. If objectstack wants it, it is objectstack's bundle shape to read.
  • Acceptance note 3 — the description is authoring help, not shape: agreed (①.4).

The merge: base main at 9dfaca65 is the merge-base and is still origin/main at this write, so no merge commit was needed in either round; GitHub reports mergeable: true, mergeable_state: clean; the PR is a draft with two bot comments (the bundle reports) and no review record before this one. Governed Surface Queue Guard is green and the six paths are outside every governed surface; this record is owed by the lane (domain:spec), not by Tier S. The serial note in the PR body (objectui#11170 in flight, no shared file) is the seat's own and was not re-read here. The manifest objectstack regenerates at its next .objectui-sha bump will carry the new of and description; that regeneration is routine for the bump and is not a finding on this PR.

Implemented-by: claude/issue-11396-master-detail-details-entry
Reviewed-by: session_01CPvhwGcirXqBGEdPSb72TZ

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants