feat(plugin-form): record:line_items publishes every key of its 17.6.0 spec row, each by measurement (objectui#11536) - #11557
Conversation
…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>
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs read: card objectui#11536 (body and all 6 comments, the seat ACCEPT ① Derived judgmentsAccept-set widening, The ruling's bar, "never declare a key the renderer does not read", holds for all ten. Each read site is confirmed in
Pin file Member pins. Ledger on the merged head. Exactly one
Nothing else in the public surface moves. The diff touches no renderer, no README, no committed manifest JSON (none in the objectui tree names Check-runs on the head: 43 total, 40 success, 3 skipped ( ② Semver level
③ Boundary flagsDev report
Implemented-by: VERDICT: PASS |
|
Generated by Claude Code |
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 reportingunknown-prop. The renderer does not change: it already read every one of them.What changed
packages/plugin-form/src/index.tsx: theline_itemsregistration declares the ten keys of the 17.6.0RecordLineItemsPropsrow it did not publish (parentObject,parentId,recordId,title,readonly,minRows,maxRows,filter,sort,limit).filterandsortaretype: 'array'withof: 'object';titleis'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.packages/plugin-form/src/__tests__/lineItemsDeclaredInputs-11536.test.tsx(30 tests): the measurement every declaration rests on, plus the member pins forcolumns,dataSource,filterandsort.apps/console/src/__tests__/registry-inputs-spec-parity.test.ts: strikes everyOWED TO objectui#11536entry (the ten unpublished keys and the two owed member pins), registers the four member pins inMEMBER_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-formminor.The measurement, key by key
Method. A read in
LineItemsPanelis not proof on its own:SchemaRendererhoists thepropertiesbag onto the node, and the block wraps the panel inElementDataSourceGate. So each key is authored the way the spec authors it, as{ type: 'record:line_items', properties: { … } }. It is rendered through the realSchemaRendererand 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.parentObjectparentObjectconst inLineItemsPanel(the key, else the page record'sobjectName), used bysavefor the rollup gate andbuildMasterDetailEditBatch's parent leg{ 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 legparentIdparentIdconst (the key, elserecordId, else the page record'srecordId) →load(find(childObject, { $filter: { [relationshipField]: parentId } })) andsave$filter: { po: 'p1' }; a new line is saved withpo: 'p1'; CONTROL: with no parent id, nofindand the "Save the record first" staterecordIdparentIdconst, second in order$filter: { po: 'r9' }; outranks the page record (page-1); loses toparentIdtitleCardTitlechild: the key, else theform.lineItems.titledefaultreadonlyallow_add/allow_deleteingridField, and thereadonlyprop onLineItemsField(GridFielddraws its read-only table)minRowsgridField.min_rows→GridField'sminRows(removeRowguard and Remove buttondisabled)minRows: 2both Remove row buttons are disabled; no blank lines are added; CONTROL: enabledmaxRowsgridField.max_rows→GridField'smaxRows(addRow/duplicateRowguards, ghost row, Add and Duplicatedisabled)maxRows: 2, Add line and Duplicate row are disabled and the entry row is gone (2 inputs, not 3); CONTROL: enabled, 3 inputsfilteruseResolvedFilter(schema.filter, …)→toFilterNodeSafely→mergeFilterNodes(parentScope, …)inload$filter: ['and', ['po', '=', 'p1'], [['billable', 'equals', true]]]; CONTROL{ po: 'p1' }sortconvertSortToQueryParams(schema.sort)→$orderbyinload$orderby: { amount: 'desc' }; CONTROL: no$orderbylimitresolveRowLimit(schema.limit, DEFAULT_LINE_ITEMS_LIMIT)→$topinload$top: 25; CONTROL$top: 500Keys 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 thechildObjectnote under Acceptance notes.filter/sort/limitare TOP-LEVEL reads, not onlydataSourcemembers (PM hypothesis 2, measured). With no binding,useElementDataSourceSchemareturns 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 nodataSourceon the node. With a binding, the gate composes them first: the panel's ownfilteris ANDed with the binding's and the view's, and the panel's ownsort/limitoutrank the view's while losing to the binding's own (pinned in thedataSourceblock of the pin file).Member pins
record:line_items.columnslabelor elsename;typepicks the cell control and a typeless member is a text cell, with no hydration from the child object;defaultHiddenhides a member,requiredoutranks it and marks the header;nameis the field Save writesrecord:line_items.dataSourceobjectlands onchildObjectand outranks an authored one;viewcontributes filter (behind the parent scope), sort and page size; an unresolvable view reports and loads nothing; panelsort/limitoutrank the view's, bindingsort/limitoutrank the panel's; bindingfilteris ANDed with the panel's; a view'scolumnsare not readrecord:line_items.filterViewFilterRulemembers reach$filterin 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 criterionrecord:line_items.sort{ field, order }members reach$orderbyin authored order; a member with nofieldis dropped while siblings survive; anorderother thandescreads ascending; no usable member sends no$orderbyfilterandsortentered the array-armed population by being declared, and were pinned in the same change, soMEMBER_PIN_EXEMPTION_CEILINGgoes to 0 rather than staying at 2.PM hypotheses, as measured
c9e2fe3) andindex.tsxreverted to the base6158e4c93,record:line_items publishes every top-level key its spec props schema declaresreceived exactlyparentObject, parentId, recordId, title, readonly, minRows, maxRows, filter, sort, limit.dataSource-only: all three are read top-level (above).memberPinsnamescolumnsanddataSourceonmain: confirmed, and used.OWED TO objectui#11536entry is left in any ledger. The owner stays inOBJECTUI_11111_OWNERSwith a per-owner total of 0, the shape objectui#8652 and objectui#8649 left when they were struck.@object-ui/plugin-formto itssrc, so the parity test reads source. The closure build was still needed, fortype-check.Reverse validation (one-off, no file kept)
The fix was committed first (
c9e2fe3). Thenpackages/plugin-form/src/index.tsxwas replaced with the base blob under atrap … EXIT INT TERMrestore. 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 andevery member pin names a key that is still array/object-armed on a covered block, which receivedrecord:line_items.filterandrecord:line_items.sort. Restored withgit checkout HEAD -- …, checked by blob hash equal toHEAD:packages/plugin-form/src/index.tsx(ef2d497e) and an emptygit 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.unpublishedKeys12 → 2, removing objectui#11536's 10; objectui#11168's 1 and objectui#11068's 1 remain.memberPins2 → 0.'objectui#11536'12 → 0.MEMBER_PIN_EXEMPTION_CEILING2 → 0.Whichever of this PR and objectui#11068 lands second recomputes
unpublishedKeysfrom 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.tsxwent red under this change. Its sampler fills every declared input, and the newly declaredparentId/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 inSAMPLE_BY_INPUT, beside the six defaults that table already restates for the same reason. It also annotates the docblock sentence "NO public block declares arecordIdinput", which this change made false. 13 / 13 green after the fix.Verification (HEAD
390a65f)pnpm exec vitest runon the pin file, the oldLineItemsPanel.elementDataSourcetest, the parity test and the 11 console tests that read registryinputs(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)Test Files 14 passed (14)·Tests 499 passed (499)pnpm exec vitest run packages/plugin-form/(atc9e2fe3; the second commit touches no plugin-form file)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 intsconfig.test.json's program per--listFiles)tsc --noEmit && tsc -p tsconfig.test.jsoncleanpnpm --filter @object-ui/console run type-check(console closure built)pnpm check:sdui-registration-pins(after a consolevite build)All 14 registration(s) a sideEffects array promises are present in the built consolepnpm check:element-data-source-declarationOK — 14 gate-consuming file(s) checkedpnpm check:component-surface-parityrecord:line_itemsrowpnpm check:control-bytesOK (scanned 10153 tracked text file(s))pnpm check:new-line-citations0 new citation(s)pnpm check:changeset-claims8067-component-input-member-kind.md), read, still truepnpm check:pending-changeset-literalsNo test source names a pending changesetnode scripts/check-changeset-presence.mjsdeclares 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-collisionsLint is narrowed to the four changed TS files (
eslint --no-inline-config --format json), a measurement rather than a skip:eslint.config.jsapplies to every**/*.{ts,tsx}.no-explicit-any/react-refresh/only-export-components, the kinds the neighbouring code already carries).parserOptions.project/projectService), and no rule undereslint-rules/reads another file. So this diff cannot move a verdict on an untouched file.The repo-wide
pnpm lintis 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)
childObjectis declaredrequired: true, while the 17.6.0 row declares it optional. The row's own description says why: the component-leveldataSourcebinding can supply it. Measured once through the realmanifestFromConfigs+validateTreepair: a node that binds the object throughdataSourceand omitschildObjectgets amissing-required-properror onchildObject.RecordLineItemsProps.safeParseaccepts the same props, and the panel renders such a node (thedataSourcepin rows). This is not changed here, because it is not one of the ten ruled keys. It is in the report for the seat.packages/types/src/zod/public-blocks.zod.tsstill saysrecord:line_itemshas 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.ViewFilterRuleoperator reaches$filteras 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