This repository was archived by the owner on Oct 26, 2026. It is now read-only.
Repository navigation
Commit 072b7e8
feat(plugin-form): record:line_items publishes every key of its 17.6.0 spec row, each by measurement (objectui#11536) (#11557)
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](https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 806f327 commit 072b7e8
5 files changed
Lines changed: 690 additions & 40 deletions
File tree
- .changeset
- apps/console/src/__tests__
- packages/plugin-form/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
286 | 286 | | |
287 | 287 | | |
288 | 288 | | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
289 | 298 | | |
290 | 299 | | |
291 | 300 | | |
| |||
549 | 558 | | |
550 | 559 | | |
551 | 560 | | |
552 | | - | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
553 | 565 | | |
554 | 566 | | |
555 | 567 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
648 | 648 | | |
649 | 649 | | |
650 | 650 | | |
651 | | - | |
652 | | - | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
653 | 654 | | |
654 | 655 | | |
655 | 656 | | |
| |||
750 | 751 | | |
751 | 752 | | |
752 | 753 | | |
753 | | - | |
| 754 | + | |
| 755 | + | |
754 | 756 | | |
755 | 757 | | |
756 | 758 | | |
| |||
890 | 892 | | |
891 | 893 | | |
892 | 894 | | |
893 | | - | |
| 895 | + | |
894 | 896 | | |
895 | | - | |
| 897 | + | |
896 | 898 | | |
897 | 899 | | |
898 | 900 | | |
| |||
1535 | 1537 | | |
1536 | 1538 | | |
1537 | 1539 | | |
1538 | | - | |
1539 | | - | |
1540 | | - | |
1541 | | - | |
1542 | | - | |
1543 | | - | |
1544 | | - | |
1545 | | - | |
1546 | | - | |
1547 | | - | |
1548 | | - | |
1549 | | - | |
1550 | | - | |
1551 | | - | |
1552 | | - | |
1553 | | - | |
1554 | | - | |
1555 | | - | |
1556 | | - | |
1557 | | - | |
| 1540 | + | |
| 1541 | + | |
| 1542 | + | |
| 1543 | + | |
| 1544 | + | |
| 1545 | + | |
| 1546 | + | |
| 1547 | + | |
| 1548 | + | |
1558 | 1549 | | |
1559 | 1550 | | |
1560 | 1551 | | |
| |||
3327 | 3318 | | |
3328 | 3319 | | |
3329 | 3320 | | |
| 3321 | + | |
| 3322 | + | |
| 3323 | + | |
| 3324 | + | |
| 3325 | + | |
| 3326 | + | |
| 3327 | + | |
| 3328 | + | |
| 3329 | + | |
| 3330 | + | |
| 3331 | + | |
| 3332 | + | |
| 3333 | + | |
| 3334 | + | |
| 3335 | + | |
| 3336 | + | |
3330 | 3337 | | |
3331 | 3338 | | |
3332 | 3339 | | |
| |||
3526 | 3533 | | |
3527 | 3534 | | |
3528 | 3535 | | |
3529 | | - | |
3530 | | - | |
3531 | | - | |
3532 | | - | |
3533 | | - | |
3534 | | - | |
3535 | | - | |
3536 | | - | |
3537 | | - | |
3538 | | - | |
3539 | | - | |
3540 | | - | |
| 3536 | + | |
| 3537 | + | |
| 3538 | + | |
| 3539 | + | |
| 3540 | + | |
| 3541 | + | |
3541 | 3542 | | |
3542 | 3543 | | |
3543 | 3544 | | |
| |||
4363 | 4364 | | |
4364 | 4365 | | |
4365 | 4366 | | |
| 4367 | + | |
| 4368 | + | |
| 4369 | + | |
| 4370 | + | |
| 4371 | + | |
| 4372 | + | |
| 4373 | + | |
| 4374 | + | |
4366 | 4375 | | |
4367 | | - | |
| 4376 | + | |
4368 | 4377 | | |
4369 | 4378 | | |
4370 | 4379 | | |
| |||
5992 | 6001 | | |
5993 | 6002 | | |
5994 | 6003 | | |
5995 | | - | |
| 6004 | + | |
5996 | 6005 | | |
5997 | 6006 | | |
5998 | 6007 | | |
| |||
0 commit comments