Skip to content

feat(plugin-form): record:line_items publishes every key of its 17.6.0 spec row, each by measurement (objectui#11536) - #11557

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11536-line-items-declare-or-refuse
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-11536-line-items-declare-or-refuse

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11536

Clause-②: yes

Why yes: the ten keys are declared on the block's registry inputs, which widens a published surface. The SDUI manifest, the JSX intrinsics and the page validator now accept them instead of reporting unknown-prop. The renderer does not change: it already read every one of them.

What changed

  • packages/plugin-form/src/index.tsx: the line_items registration declares the ten keys of the 17.6.0 RecordLineItemsProps row it did not publish (parentObject, parentId, recordId, title, readonly, minRows, maxRows, filter, sort, limit). filter and sort are type: 'array' with of: 'object'; title is 'string' only, because the row declares a plain string and the panel draws it as a React child. Each input carries a description of what the panel does with it.
  • New pin file packages/plugin-form/src/__tests__/lineItemsDeclaredInputs-11536.test.tsx (30 tests): the measurement every declaration rests on, plus the member pins for columns, dataSource, filter and sort.
  • apps/console/src/__tests__/registry-inputs-spec-parity.test.ts: strikes every OWED TO objectui#11536 entry (the ten unpublished keys and the two owed member pins), registers the four member pins in MEMBER_PINS, and lowers the caps those entries counted in (see "Shared cap line" below).
  • apps/console/src/__tests__/record-block-record-reach.test.tsx: a file outside the claim's surface, added this round because this change turned it red (see "Deviation" below).
  • .changeset/11536-line-items-published-inputs.md: @object-ui/plugin-form minor.

The measurement, key by key

Method. A read in LineItemsPanel is not proof on its own: SchemaRenderer hoists the properties bag onto the node, and the block wraps the panel in ElementDataSourceGate. So each key is authored the way the spec authors it, as { type: 'record:line_items', properties: { … } }. It is rendered through the real SchemaRenderer and this package's own registration, and asserted at what the key does: the query the adapter receives, the batch Save sends, or the DOM. Every row has a control beside it that differs only by that key. Read sites are named by symbol, per AGENTS.md #11.

key read site measured effect (pin-file row) declared
parentObject parentObject const in LineItemsPanel (the key, else the page record's objectName), used by save for the rollup gate and buildMasterDetailEditBatch's parent leg names the parent leg of the Save batch: { object: 'purchase_order', action: 'update', id: 'p1', data: { total_amount: 5 } }; outranks the page record's object; without it and with no page record, the batch has no parent leg yes
parentId parentId const (the key, else recordId, else the page record's recordId) → load (find(childObject, { $filter: { [relationshipField]: parentId } })) and save $filter: { po: 'p1' }; a new line is saved with po: 'p1'; CONTROL: with no parent id, no find and the "Save the record first" state yes
recordId same parentId const, second in order $filter: { po: 'r9' }; outranks the page record (page-1); loses to parentId yes
title CardTitle child: the key, else the form.lineItems.title default heading reads "Order lines"; CONTROL "Line Items" yes
readonly Save button gate, allow_add / allow_delete in gridField, and the readonly prop on LineItemsField (GridField draws its read-only table) no Save, no Add line / Duplicate row / Remove row, read-only table, no inputs; CONTROL: all present yes
minRows gridField.min_rows → GridField's minRows (removeRow guard and Remove button disabled) with 2 lines and minRows: 2 both Remove row buttons are disabled; no blank lines are added; CONTROL: enabled yes
maxRows gridField.max_rows → GridField's maxRows (addRow / duplicateRow guards, ghost row, Add and Duplicate disabled) with 2 lines and maxRows: 2, Add line and Duplicate row are disabled and the entry row is gone (2 inputs, not 3); CONTROL: enabled, 3 inputs yes
filter useResolvedFilter(schema.filter, …) → toFilterNodeSafely → mergeFilterNodes(parentScope, …) in load $filter: ['and', ['po', '=', 'p1'], [['billable', 'equals', true]]]; CONTROL { po: 'p1' } yes
sort convertSortToQueryParams(schema.sort) → $orderby in load $orderby: { amount: 'desc' }; CONTROL: no $orderby yes
limit resolveRowLimit(schema.limit, DEFAULT_LINE_ITEMS_LIMIT) → $top in load $top: 25; CONTROL $top: 500 yes

Keys left out: none. The row has fifteen keys. The other five (childObject, relationshipField, columns, amountField, totalField) were declared before this change and are not re-judged here, apart from the childObject note under Acceptance notes.

filter / sort / limit are TOP-LEVEL reads, not only dataSource members (PM hypothesis 2, measured). With no binding, useElementDataSourceSchema returns the schema by reference ("BY REFERENCE when there is nothing to apply") and the panel reads the three keys itself, which is what the rows above assert with no dataSource on the node. With a binding, the gate composes them first: the panel's own filter is ANDed with the binding's and the view's, and the panel's own sort / limit outrank the view's while losing to the binding's own (pinned in the dataSource block of the pin file).

Member pins

id what the pin asserts
record:line_items.columns one column per member in authored order, headed by label or else name; type picks the cell control and a typeless member is a text cell, with no hydration from the child object; defaultHidden hides a member, required outranks it and marks the header; name is the field Save writes
record:line_items.dataSource object lands on childObject and outranks an authored one; view contributes filter (behind the parent scope), sort and page size; an unresolvable view reports and loads nothing; panel sort / limit outrank the view's, binding sort / limit outrank the panel's; binding filter is ANDed with the panel's; a view's columns are not read
record:line_items.filter ViewFilterRule members reach $filter in order as one group ANDed behind the parent scope, operator word unchanged; a member naming the relationship field does not replace the scope; [] is no criterion
record:line_items.sort { field, order } members reach $orderby in authored order; a member with no field is dropped while siblings survive; an order other than desc reads ascending; no usable member sends no $orderby

filter and sort entered the array-armed population by being declared, and were pinned in the same change, so MEMBER_PIN_EXEMPTION_CEILING goes to 0 rather than staying at 2.

PM hypotheses, as measured

  1. The ten keys: confirmed by the gate's own output. With the fix committed (c9e2fe3) and index.tsx reverted to the base 6158e4c93, record:line_items publishes every top-level key its spec props schema declares received exactly parentObject, parentId, recordId, title, readonly, minRows, maxRows, filter, sort, limit.
  2. Top-level versus dataSource-only: all three are read top-level (above).
  3. memberPins names columns and dataSource on main: confirmed, and used.
  4. End state: no OWED TO objectui#11536 entry is left in any ledger. The owner stays in OBJECTUI_11111_OWNERS with a per-owner total of 0, the shape objectui#8652 and objectui#8649 left when they were struck.
  • The brief's note that the parity test "reads built packages" did not hold. The console's vite config aliases @object-ui/plugin-form to its src, so the parity test reads source. The closure build was still needed, for type-check.

Reverse validation (one-off, no file kept)

The fix was committed first (c9e2fe3). Then packages/plugin-form/src/index.tsx was replaced with the base blob under a trap … EXIT INT TERM restore. The mutation on disk: grep -c "name: 'parentObject'" 0, against 1 at HEAD. The parity test then went red, the expected direction: 2 failed / 253 passed. The two reds were the ten unpublished keys above and every member pin names a key that is still array/object-armed on a covered block, which received record:line_items.filter and record:line_items.sort. Restored with git checkout HEAD -- …, checked by blob hash equal to HEAD:packages/plugin-form/src/index.tsx (ef2d497e) and an empty git diff HEAD. With the fix: 255 / 255 green.

Shared cap line (serial note for objectui#11068)

Changed in registry-inputs-spec-parity.test.ts:

  • OBJECTUI_11111_LEDGER_CAPS.unpublishedKeys 12 → 2, removing objectui#11536's 10; objectui#11168's 1 and objectui#11068's 1 remain.
  • memberPins 2 → 0.
  • The per-owner totals: 'objectui#11536' 12 → 0.
  • MEMBER_PIN_EXEMPTION_CEILING 2 → 0.

Whichever of this PR and objectui#11068 lands second recomputes unpublishedKeys from the ledger. After both, it reads 1, which is objectui#11168's.

Deviation: one file outside the claim's surface

apps/console/src/__tests__/record-block-record-reach.test.tsx went red under this change. Its sampler fills every declared input, and the newly declared parentId / recordId (sampled 'x') outrank the bound record. That is the declared behaviour, but it detached the panel from the record, so the probe reported "the bound record does not reach its output". The fix restates the renderer default, the key absent, for the three override keys in SAMPLE_BY_INPUT, beside the six defaults that table already restates for the same reason. It also annotates the docblock sentence "NO public block declares a recordId input", which this change made false. 13 / 13 green after the fix.

Verification (HEAD 390a65f)

command exit verdict line
pnpm exec vitest run on the pin file, the old LineItemsPanel.elementDataSource test, the parity test and the 11 console tests that read registry inputs (public-contract, sdui-manifest-build-11403, sdui-intrinsics-compile-11075, component-input-union-specimens, record-block-record-reach, public-block-binding-reach, element-data-source-input-injection, preview-samples-registry-resolvable, html-tier-manifest, i18nLabelInputsManifest-10993, filterContextTokensSweep-10666) 0 Test Files 14 passed (14) · Tests 499 passed (499)
pnpm exec vitest run packages/plugin-form/ (at c9e2fe3; the second commit touches no plugin-form file) 0 Test Files 162 passed (162) · Tests 1878 passed · 1 skipped (1879)
pnpm --filter @object-ui/plugin-form run type-check (closure built; the new test file is in tsconfig.test.json's program per --listFiles) 0 tsc --noEmit && tsc -p tsconfig.test.json clean
pnpm --filter @object-ui/console run type-check (console closure built) 0 clean
pnpm check:sdui-registration-pins (after a console vite build) 0 All 14 registration(s) a sideEffects array promises are present in the built console
pnpm check:element-data-source-declaration 0 OK — 14 gate-consuming file(s) checked
pnpm check:component-surface-parity 0 report-only; no record:line_items row
pnpm check:control-bytes 0 OK (scanned 10153 tracked text file(s))
pnpm check:new-line-citations 0 0 new citation(s)
pnpm check:changeset-claims 0 report-only; one pending body names the parity file (8067-component-input-member-kind.md), read, still true
pnpm check:pending-changeset-literals 0 No test source names a pending changeset
node scripts/check-changeset-presence.mjs 0 declares 1 changeset(s)
check:unreferenced-sources, check:spec-symbols, check:handler-key-reads, check-test-path-roots, check-vi-mock-override-shape, check-vi-mock-specifiers, check-vi-mock-inherit, check-lint-coverage, check-type-check-coverage, check-changeset-fixed, check-changeset-no-major, check-registry-bare-name-collisions 0 each OK

Lint is narrowed to the four changed TS files (eslint --no-inline-config --format json), a measurement rather than a skip:

  • Population: the root eslint.config.js applies to every **/*.{ts,tsx}.
  • Count: the JSON output holds 4 files, 0 errors and 33 warnings (no-explicit-any / react-refresh/only-export-components, the kinds the neighbouring code already carries).
  • Invariance: the config enables no type-aware linting (no parserOptions.project / projectService), and no rule under eslint-rules/ reads another file. So this diff cannot move a verdict on an untouched file.

The repo-wide pnpm lint is CI's.

Docs: no page lists this block's inputs. The plugin-form README and the docs table name the block and its fallback only, and the manifest is generated from the registration, so no doc changes.

Acceptance notes (not filed here)

  1. childObject is declared required: true, while the 17.6.0 row declares it optional. The row's own description says why: the component-level dataSource binding can supply it. Measured once through the real manifestFromConfigs + validateTree pair: a node that binds the object through dataSource and omits childObject gets a missing-required-prop error on childObject. RecordLineItemsProps.safeParse accepts the same props, and the panel renders such a node (the dataSource pin rows). This is not changed here, because it is not one of the ten ruled keys. It is in the report for the seat.
  2. packages/types/src/zod/public-blocks.zod.ts still says record:line_items has no spec row "on purpose". 17.6.0 carries the row, so the sentence went stale at the bump. Carrier: objectui#10872, which holds that arming.
  3. A ViewFilterRule operator reaches $filter as written, so a word outside the spec's operator enum is forwarded too. The spec's authoring gate refuses such a word. Observation only.

Generated by Claude Code

claude added 2 commits October 3, 2026 14:14
…0 spec row, each by measurement

The ten keys the spec row adds over the registration (parentObject,
parentId, recordId, title, readonly, minRows, maxRows, filter, sort, limit)
are each authored in the spec's { type, properties } form through the real
SchemaRenderer and this registration, beside a control without the key, and
every one moves the panel, so every one is declared.

The console parity gate's bookings for this block are struck: the ten
unpublished-key entries and the two owed member pins. columns, dataSource,
filter and sort get member pins in the new test file; the unpublishedKeys cap
goes 12 -> 2, the memberPins cap and the member-pin exemption ceiling 2 -> 0.

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
…t overrides unset

record:line_items now declares parentId, recordId and parentObject, and the
probe fills every declared input. Each of the three outranks the record the
page shows, so a sampled value detached the panel from the bound record and
the probe reported "the bound record does not reach its output" for a block
that reaches it. The renderer default is the key absent, and that is what the
sampler restates, beside the six it already restates for the same reason.

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
…fuse

Conflict in registry-inputs-spec-parity.test.ts, resolved by recomputing the
caps from the ledger rather than taking a side: main's build struck
objectui#11068's booking and this branch struck objectui#11536's, so
unpublishedKeys counts objectui#11168's one entry alone (1), and both owners'
per-owner totals read 0.

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 1 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/8067-component-input-member-kind.md

  • names apps/console/src/__tests__/registry-inputs-spec-parity.test.ts → apps/console/src/__tests__/registry-inputs-spec-parity.test.ts — edited by this change

    A registration's type: 'array' said a value was a list and stopped there, so a member that drifted from @objectstack/spec was invisible to every layer that reads a declaration. page:header.actions is the measured cost: the contract declares z.array(z.string()) ("Action IDs"), the renderer read the members as ActionDef objects, and the repo-wide parity gate in apps/console/src/__tests__/registry-inputs-spec-parity.test.ts stayed green for the whole life of the drift because both sides carried the key and neither could say what was inside it. What settled it was a maintainer ruling, not a test — and even after the fix, "these are ids" survived only as English in the registration's description.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with d0c0c7fe9 (merge-base with origin/main): 4 file(s) changed outside .changeset/, read against 2084 pending declaration(s) that publish a body (2718 pending in total). · run

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3309.1 KB 3330.4 KB
Main entry chunk (gzip) 150.3 KB 350 KB
Entry file index-3H_H6aNj.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) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
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) 572.97KB 137.34KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 231.08KB 64.07KB
fields (index.js) 261.62KB 66.20KB
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) 83.60KB 22.89KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 142.26KB 38.44KB
plugin-designer (index.js) 231.28KB 48.76KB
plugin-detail (index.js) 245.43KB 64.53KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 175.57KB 45.31KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 235.05KB 64.60KB
plugin-kanban (index.js) 49.59KB 15.58KB
plugin-list (index.js) 116.46KB 28.93KB
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.25KB 2.04KB
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.16KB 1.96KB
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) 0.20KB 0.18KB
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.07KB 2.39KB
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: a322ba447f747f21b51ad74abf129af03aa786f7
Local-runs: none

Inputs read: card objectui#11536 (body and all 6 comments, the seat ACCEPT 5970449592 included as a claim under test), PR objectui#11557 (body, 5-file list, net diff against main d0c0c7fe9), the 43 check-runs on the head, @objectstack/spec 17.6.0 from npm (RecordLineItemsProps in dist/ui/index.d.ts), and the head's tree through git show (no checkout). Reviewed at 2026-10-03, rendered by a subagent of the seat session named below.

① Derived judgments

Accept-set widening, record:line_items registry inputs (+10 keys). The SDUI manifest, the JSX intrinsics and validateTree now accept parentObject, parentId, recordId, title, readonly, minRows, maxRows, filter, sort and limit instead of reporting unknown-prop. Right. Each type arm matches the 17.6.0 row key for key: four ZodOptional ZodString keys declared string; readonly ZodBoolean declared boolean; minRows / maxRows / limit ZodNumber declared number; filter (ZodArray of the ViewFilterRule object) and sort (ZodArray of { field, order }) declared type: 'array', of: 'object'. title is a plain ZodString in the row, so 'string' alone (no I18nLabel arm) is the correct arm. The row's other five keys are unchanged in the diff.

The ruling's bar, "never declare a key the renderer does not read", holds for all ten. Each read site is confirmed in LineItemsPanel.tsx at the head, independently of the dev's table:

  • parentObject: const parentObject = schema.parentObject || record?.objectName, consumed by save (canRollup) and buildMasterDetailEditBatch. Right.
  • parentId / recordId: const parentId = schema.parentId || schema.recordId || record?.recordId, consumed by load ({ [schema.relationshipField]: parentId } as the $filter scope), by save, and by the !parentId branch that draws the save-record-first state. Precedence as declared. Right.
  • title: CardTitle child schema.title || t('form.lineItems.title'). Right.
  • readonly: the Save button is rendered only under !schema.readonly; gridField.allow_add / allow_delete are !schema.readonly; LineItemsField receives readonly={schema.readonly}. Right.
  • minRows / maxRows: gridField.min_rows / max_rows, consumed in GridField as cfg.min_rows ?? 0 / cfg.max_rows (the removeRow guard that returns while the row count is at or below minRows, the addRow / duplicateRow guards that return once the row count reaches maxRows, the hasGhost entry row, and the matching disabled attributes). The description's "adds no blank lines to reach the number" and "trailing blank entry row is not drawn" both follow from that code. Right.
  • filter: useResolvedFilter(schema.filter, filterScope) then toFilterNodeSafely then mergeFilterNodes(parentScope, listFilterNode) in load, so the authored filter is ANDed behind the parent scope and never replaces it. Right.
  • sort: convertSortToQueryParams(schema.sort) into $orderby; normalizeSortEntries drops a member with no field and reads any order other than desc as asc, exactly as the member pin states. Right.
  • limit: resolveRowLimit(schema.limit, DEFAULT_LINE_ITEMS_LIMIT) into $top, with isUsableRowLimit (positive integer) and describeRefusedRowLimit writing one console.warn from an effect; DEFAULT_LINE_ITEMS_LIMIT = 500. The description's "must be a positive integer, otherwise ignored with a console warning, default 500" is exact. Right.

filter / sort / limit are top-level reads, not only dataSource members. The pin file authors them in properties with no dataSource on the node and asserts the query; RECORD_LINE_ITEMS_DATA_SOURCE maps filter: true, sort: true, limit: 'limit', so with a binding the gate composes onto the same keys. Declaring them is therefore a declaration of keys the panel reads. Right.

Pin file lineItemsDeclaredInputs-11536.test.tsx. Every row goes through the real SchemaRenderer and the package's own ../index registration in the { type, properties } form, with a control beside it, asserted at the query, the Save batch or the DOM. That is the measurement shape the ruling asked for, not a source read. Right.

Member pins. MEMBER_PINS gains record:line_items.columns, .dataSource, .filter and .sort, each pointing at the pin file, which carries one describe block per pin. filter and sort entered the array-armed population by this diff and were pinned in it, so MEMBER_PIN_EXEMPTION_CEILING 2 to 0 (not 2 to 2) is the correct movement. The dataSource pin's claim that the key is injected by Registry.register through ELEMENT_DATA_SOURCE_INPUT and is absent from the spec row is confirmed in packages/core/src/registry/Registry.ts and the 17.6.0 d.ts. Right.

Ledger on the merged head. Exactly one owedEntries call remains in the parity file (action:button undoable, objectui#11168); zero OWED TO objectui#11536 strings. OBJECTUI_11111_LEDGER_CAPS.unpublishedKeys 11 (on main, after objectui#11068's strike) to 1, memberPins 2 to 0, per-owner 'objectui#11536' 12 to 0, and MEMBER_PIN_EXEMPTION_CEILING to 0. The merge conflict was resolved by recomputing from the ledger, not by picking a side, and the cap assertion the booked entries are not exactly the cap is green on the head (Test shards). The owner stays in OBJECTUI_11111_OWNERS / OBJECTUI_11111_BOOKINGS with a zero total, the shape objectui#8652 and objectui#8649 left behind. Right.

record-block-record-reach.test.tsx. The sampler's sampleFor fills every declared input, so the newly declared parentId / recordId would have been sampled 'x' and detached the panel from the bound record. Restating the three override keys as undefined in SAMPLE_BY_INPUT is the right repair: every panel read of them is an || chain, so a key present with undefined behaves as the renderer default. One nit, no action: the comment says "the key ABSENT", while schema[input.name] = sampleFor(input) writes the key with an undefined value; equivalent at every read site. The docblock annotation ("NO public block declares a recordId input" went false) is correct. Right.

Nothing else in the public surface moves. The diff touches no renderer, no README, no committed manifest JSON (none in the objectui tree names record:line_items), and no governed path (none of .claude/**, docs/adr/**, skills/**, AGENTS.md, CLAUDE.md). Governed Surface Queue Guard: success.

Check-runs on the head: 43 total, 40 success, 3 skipped (Test (coverage), the coverage shard matrix and dependabot, each conditional), 0 failure. Test shards 1 to 8, Type Check, Lint, Build & E2E, Test (dist pins), Changeset Declaration / Bump Policy / Fixed Group Check / Claim Re-read, Spec Main Shape Gate and Line Citation Gate are all success.

② Semver level

  • .changeset/11536-line-items-published-inputs.md declares '@object-ui/plugin-form': minor. The PR body's Clause-②: yes (no arm) requires at least minor; minor on a fixed-group package is the correct level for a widened accept set, and objectui's no-major rule holds (Changeset Bump Policy and Changeset Fixed Group Check success). Not skip-changeset: the diff edits packages/plugin-form/src/index.tsx, published source of a released package. Right.
  • Changeset prose against the diff: each per-key sentence matches the registration description and the confirmed read site; "default 500" is DEFAULT_LINE_ITEMS_LIMIT; "each read as a top-level key as well as through a dataSource binding" is what the pin file and RECORD_LINE_ITEMS_DATA_SOURCE show; "Nothing changes at runtime" holds, the diff touches no renderer. Right.
  • Clause-②: line: yes, at line start, with no arm, the correct declaration for a widening that breaks nothing. Right.
  • Pending changeset re-read (8067-component-input-member-kind.md, bot comment 5970433109): its paragraph says the parity gate stayed green through the page:header.actions drift because type: 'array' could not say what was inside. This diff adds four member pins and changes nothing about how the gate reads a type: 'array' arm, so the paragraph is still true. No correction needed. Right.

③ Boundary flags

Dev report 5970200796 and patch-round report 5970436508, open_questions: [] in both; every deviation and out-of-scope finding answered here:

  1. Deviation, record-block-record-reach.test.tsx joined the surface: answered under ①; accepted, the seat's amendment of the claim stands.
  2. Deviation, the parity test reads src through the console vite alias, not built packages: informational, no contract effect.
  3. Deviation, commit trailers use the model-free pair: matches objectui's AGENTS.md rule on agent commits; no contract effect.
  4. Deviation, the plugin-form full suite ran at c9e2fe3 and the final commit touches only a console test; patch-round build timeout then a cached re-run: the eight Test shards on a322ba447 cover both. Answered.
  5. Deviation, no labels written, PR assignee set through the relay: ordinary; no contract effect.
  6. Out-of-scope finding 1, childObject registered required: true while the 17.6.0 row declares it optional, so validateTree reports missing-required-prop on a node that binds its object through dataSource and omits childObject while RecordLineItemsProps.safeParse accepts it: confirmed real from the head tree (the registration line is unchanged in the diff; the parity file has no required-parity test, so no gate holds it). Not one of the ten ruled keys and not this card's ask, so it does not move this verdict. ESCALATED to the seat: it needs a carrier (a card, or a ruling that required: true stays by design), since the page validator currently refuses a node the spec accepts.
  7. Out-of-scope finding 2, the stale "no spec row on purpose" sentence in packages/types/src/zod/public-blocks.zod.ts: carrier objectui#10872 named by the dev and the seat. Routed, not escalated.
  8. Out-of-scope finding 3, a ViewFilterRule operator reaches $filter as written: observation; the spec's authoring gate refuses a word outside the enum, and this diff declares the member kind, not the operator set. No action.
  9. Serial note for objectui#11068: both strikes stand on the merged head and the recomputed caps are checked by the parity test's own assertion. Closed.

Implemented-by: claude/issue-11536-line-items-declare-or-refuse
Reviewed-by: session_01FjqrwXPfSMkSfkKYDSRkN2

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

needs:contract-review removed: an at-tier PASS is on record for this head · domain:ui seat 1 · session_01FjqrwXPfSMkSfkKYDSRkN2 · 2026-10-03T15:32Z.

  • Record: 5970604204 on this PR. Served-tier: CONTRACT_REVIEW_TIER, Head-sha: a322ba447f747f21b51ad74abf129af03aa786f7, Local-runs: none, Implemented-by: claude/issue-11536-line-items-declare-or-refuse / Reviewed-by: session_01FjqrwXPfSMkSfkKYDSRkN2, PASS.
  • The PR head at this act is a322ba447, the head the record judged.
  • Pre-landing checks: every check on the head is success or an expected skip (40 / 3). check-governed-merges --pr objectstack-ai/objectui#11557: NOT governed.
  • Landing in this act: ready, then auto-merge.
  • The record's escalated item ③6: childObject is registered required: true while the 17.6.0 row makes it optional, so the page validator refuses a dataSource-bound node the spec accepts. This seat is the carrier.
    • So far it was measured through manifestFromConfigs + validateTree and confirmed from the tree. ⛔ It has not yet been measured at a public door, so under the filing gate it is not a card yet.
    • Next step: one reading at the authoring door (objectui validate or the JSX page compile) on a dataSource-bound record:line_items node without childObject. If that door refuses the node, the seat files it as a class (b) card.
    • Recorded on the seat post.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 15:34
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 15:34
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit 072b7e8 Oct 3, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11536-line-items-declare-or-refuse branch October 3, 2026 15:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants