You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit fe41dc7
Browse filesBrowse the repository at this point in the historyBrowse files
test(plugin-form): pin object-master-detail-form dataSource, details and fields members (objectui#8071 slice 18) (#11113)
Part of #8071
Clause-②: no — test pins only; no published contract, key, export or
flag changes
「lands after objectui#11086: merge main, then re-measure the three pins
on 17.5.0」
> Seat amendment (objectui#8071 `5892096260`): the 「lands after
objectui#11086」 constraint is relaxed on evidence. `git merge-tree`
against #11086's head auto-merges the ledger, no pinned read site is in
#11086's file list, and the pins survive 17.5.0 per its tarball. This PR
lands on its own green CI; whichever of the two lands second runs these
pins on its own tree.
## What this does
Slice 18 of objectui#8071. It pins the last three `AWAITING_A_PIN` keys,
all on one block: `object-master-detail-form.dataSource`, `.details` and
`.fields`. In the same commit it deletes their `MEMBER_PIN_EXEMPTIONS`
entries and lowers `MEMBER_PIN_EXEMPTION_CEILING` from **4 to 1**. That
closes the block.
`AWAITING_A_PIN` had no entries left after this, and
`apps/console/tsconfig.json` sets `noUnusedLocals`, so the constant is
deleted too. A comment where it stood says why.
`record:related_list.actions` (`NO_READ_SITE_TO_PIN`) is **not
touched**. It holds the ceiling's last unit.
| key | pin file | new or promoted |
|---|---|---|
| `dataSource` |
`packages/plugin-form/src/MasterDetailForm.elementDataSource.test.tsx` |
promoted (objectstack#7121 file), read end to end, then **grown by 5
rows** |
| `details` |
`packages/plugin-form/src/__tests__/masterDetailDetailsMembers-8071.test.tsx`
| **new**, 8 rows |
| `fields` |
`packages/plugin-form/src/__tests__/topLevelFieldsWarnCoverage-8847.test.tsx`
| promoted (objectui#8847 file), read end to end, then **grown by 5
rows** |
## Why `Part of`, not a closing keyword
The card's own text defines the work as turning **every** listed key
into a pin: 「本卡的工作就是把它们一个个变成 pin」. `record:related_list.actions` is on
that list. It has no read site, so no pin can meet the card's criterion.
Its remedy is the enforce-or-remove decision the spec owns, and until
that decision lands the entry stays and the card's population is not
closed.
⚠️ There is a second reason, measured today. The `NO_READ_SITE_TO_PIN`
constant says the entry "belongs to" objectui#7300, "which is open".
**objectui#7300 is closed as `not_planned`** (REST read on 2026-09-29).
So #8071 is now the only open card whose ledger text names this last
entry, and closing it would leave the entry with no live tracker. This
PR does not edit that constant (it is out of scope for this dispatch).
The stale sentence is listed under Acceptance notes.
## H2: the real read site for each key
⚠️ The read sites are in **`packages/plugin-form`**. None is in
`packages/plugin-grid`, which is what the claim's file surface names.
See Deviation below.
| key | where the renderer reads it | what reads each member |
|---|---|---|
| `dataSource` | `MasterDetailFormRenderer` (plugin-form `index.tsx`)
wraps `ElementDataSourceGate` with **no `mapping`**, so the default
applies and only `object` lands, on `objectName`. The gate lives in
`packages/react/src/element-data-source/`. | `object` becomes
`objectName`, unconditionally. From there `MasterDetailForm` uses it
three ways: the parent `ObjectForm`, `deriveDetail`'s FK lookup, and the
batch's parent leg. `view` is resolved: an unresolvable view reports, a
resolving one contributes nothing. `filter` is not mapped, but beside a
`view` it is **merged**, and a malformed one refuses the block. `sort`
and `limit` are not read at all. |
| `details` | `MasterDetailForm` itself: `rawDetails = schema.details`,
the derive effect, `MasterDetailLines`, `sendBatch`, `readLines`. |
`title` becomes the section heading. `columns` go to the grid.
`sortField`, `minRows`, `maxRows`, `addLabel` and `amountField` reach
the grid **renamed**, as `sort_field`, `min_rows`, `max_rows`,
`add_label` and `total_field`. `inlineMode` and `formFields` choose the
display mode and the row form. `childObject` and `relationshipField`
address the batch creates and the edit-mode read. `totalField` receives
the rollup on the parent leg. |
| `fields` | **Not read by `MasterDetailForm`.** The `parentSchema` memo
copies `fields: schema.fields` onto an `object-form`-shaped node
rendered through a directly imported `ObjectForm`, which falls through
to `SimpleObjectForm`. That is the real read site. | Members are bare
parent field names in authored order, and `{ name }` is tolerated. The
`FormFieldSchema` object form resolves to no name and draws the route-1
warning. |
## H3: will the pins survive `@objectstack/spec` 17.5.0? Yes, per the
tarball
I read the 17.5.0 tarball read-only and diffed it against the installed
17.4.0:
- `ObjectMasterDetailFormPropsSchema` is **identical** for `fields` and
`details`. Both are `z.array(z.unknown())` with the same descriptions,
so every member parses on both versions. No pin row parses them anyway,
because the read site is the whole member contract.
- `dataSource` (`ElementDataSourceSchema`) **does change**: `filter`
moves from `FilterConditionSchema` (a MongoDB-style record) to
`z.array(ViewFilterRuleSchema)`. No pin row parses the binding. The new
rows spell `filter` in the 17.5.0 rule-array form, and the one row that
needs a malformed filter uses a rule the **runtime** lowering refuses:
an array value on `equals`, objectui#8557.
- PR #11086's file list touches **none** of the read sites these pins
exercise: `MasterDetailForm.tsx`, `ObjectForm.tsx`, plugin-form
`index.tsx`, `ElementDataSourceGate.tsx`, `element-data-source.ts`,
`filter-converter.ts`, `deriveMasterDetail.ts`, `masterDetailTx.ts`. It
does touch `GridField.tsx`, but only the currency and percent scale
clamp, which these pins do not exercise.
- Trial merge with `git merge-tree` of this branch against PR #11086's
head `174c4a7`: the ledger **auto-merges**, because #11086's hunks and
this branch's hunks do not overlap. The only conflict is
`packages/fields/src/index.tsx`, which this branch does not touch. It is
between #11086 and `main`.
- ⛔ NOT MEASURED: the pins actually **run** on 17.5.0. That happens
after #11086 lands, per the line at the top.
## The ledger, re-derived by TypeScript AST on both trees (not taken
from the order)
| | base `88fbd79` | head |
|---|---:|---:|
| `MEMBER_PIN_EXEMPTIONS` | 4 | **1** |
| `MEMBER_PINS` | 98 | **101** |
| `MEMBER_PIN_EXEMPTION_CEILING` | 4 | **1** |
| `NEWLY_JUDGED_UNPINNED_MEMBERS` | `[]` | `[]` |
| non-property members in either map | `[]` | `[]` |
The sets match exactly. The three keys that left the exemptions are the
three that entered the pins. Nothing entered the exemptions and nothing
left the pins. What remains is `record:related_list.actions =
NO_READ_SITE_TO_PIN`, and 1 equals the ceiling.
## Tests (head `ae719c4`)
- The three pin files together with the ledger: 4 files, **223 passed**.
- `pnpm exec vitest run packages/plugin-form/`: **144 files, 1719
passed, 1 skipped**.
- `pnpm exec vitest run apps/console/`: **131 files, 1475 passed**. The
ledger file alone had 199 passed.
- Type-check. plugin-form: `turbo run type-check` gave 13/13 successful.
`--listFiles` shows the `tsconfig.test.json` leg lists all three pin
files, while the main leg lists 0 of them (it excludes tests). Console:
`tsc --noEmit && tsc -b tsconfig.node.json` exit 0, and `--listFiles`
includes the ledger.
- Lint, narrowed to the changed files with the narrowing measured.
`eslint --no-inline-config --format json` over the 4 changed test files
reports **4 files, 0 errors, 20 warnings**, all `no-explicit-any`.
`eslint.config.js` sets no `parserOptions.project` or `projectService`,
so the lint is not type-aware and this diff cannot change the verdict on
any untouched file. Repo-wide `pnpm lint` is left to CI.
- Gates, each exit 0: `check:control-bytes`, `check:new-line-citations`
(0 new), `check:test-path-roots`, `check:vi-mock-specifiers`,
`check:vi-mock-inherit`, `check:vi-mock-override-shape`,
`check:changeset-claims`, `check:pending-changeset-literals`,
`check:unreferenced-sources`, `check-changeset-presence` (reads the
empty frontmatter as the declaration), `check-changeset-no-major`,
`check-changeset-fixed`, `check-changeset-overwrite`.
- `check:changeset-claims` names
`.changeset/8067-component-input-member-kind.md` because it mentions the
ledger file. I re-read the paragraph: it is a past-tense account of the
`page:header.actions` drift, and this diff does not falsify it. No
correction is owed.
## Ablations
Each leg ran on the committed tree through `ablation-replace.mjs`.
Before any result was read, the tool proved the mutation on disk (anchor
count, blob hash). Each leg restored under the tool's trap, with the
restore proven by blob equal to the HEAD blob and an empty `git diff
HEAD`. The vitest config aliases every `@object-ui/*` import to `src`,
so the mutated source is what ran and no dist preflight applies.
- **A, the gate mechanism.** I re-added
`object-master-detail-form.details` as an exemption and left the ceiling
untouched. Blob `e591038c` became `335d11ab`. Result: **3 failed, 196
passed**. The three are the census partition row, `carries no stale
member-pin exemption`, and `only ratchets DOWN` with "expected 2 to be
less than or equal to 1", which proves the ceiling moved with the list.
- **B, `details`.** I deleted `min_rows: d.minRows,` from the grid
object. Result: **2 failed**, rows 2 and 2b, and the other two pin files
stayed green in the same run. The first run of this leg reddened row 2
only, because `toEqual` treats a key holding `undefined` as absent. Row
2b now asserts the key set too, and the leg was re-run on `ae719c4`.
- **B, `dataSource`.** I changed the gate's `next[objectKey] =
composed.object` to the `??=` spelling. Result: **2 failed**, the
OUTRANKS row and the detail-half row, with neighbours green. The
detail-half row fails at its first wait: the parent form is the old
object `order`, whose only field is `code`, so the bound object's field
never appears.
- **B, `fields`.** I deleted `fields: schema.fields,` from the
`parentSchema` memo. Result: **5 failed**, every row in the file that
authors a non-default `fields`, including the pre-existing route-1
warning row. The bare-name control and the no-`fields` control stayed
green, as did both neighbour files.
## Acceptance notes (not changed here)
- ⚠️ **Handed back as a finding (class b): the `fields` description
versus the parent leg.** The `object-master-detail-form` `fields`
registration calls the key the parent pool for "values, create defaults
and the submitted set". Measured: a seeded parent field that `fields`
does not list is **still written** on the batch's parent leg. With
`fields: ['status']` and `initialValues: { status, memo }`, the parent
leg carries both. The pin records this as behaviour. Fixing it means
changing either the renderer or the declaration, and this card writes
pins only.
- The `NO_READ_SITE_TO_PIN` text ("objectui#7300 … is open") is stale:
#7300 is closed as `not_planned`. Out of scope for this dispatch.
- All four `fields` registration descriptions (`object-form`, `form`,
`embeddable-form`, `object-master-detail-form`) still say the
`FormFieldSchema` object member "is silently skipped". Since
objectui#8738 route 1 it draws a named warning. Wording only; no
carrier.
- The spec's `details` description names five of the twelve members the
renderer reads (`title`, `childObject`, `addLabel`, `columns`,
`relationshipField`). That is this card's own subject, now pinned.
- `masterDetailFormTypeVocabulary.test.tsx` still carries six cross-file
`ObjectForm.tsx` line addresses, which AGENTS.md #11 bans. This diff
does not touch that file, so the opportunistic-repair rule does not
apply. With this block closed, no later slice of this card will touch it
either. No carrier.
- From reading the sources only, not measured at any entry point: the
registration declares `details` as `required: true`, the spec declares
it `.optional()`, and the renderer tolerates it missing (`schema.details
|| []`).
- I also corrected a stale reason in the promoted
`topLevelFieldsWarnCoverage-8847.test.tsx` docblock ("`fields` is
documented as ignored once `sections` is given"), which objectui#9884
had already falsified. The change is limited to that docblock paragraph.
## Deviation: the claim's file surface
The claim lists `packages/plugin-grid/src/__tests__/` "only if a pin
needs a renderer-side fixture there". The dispatch's H2 flagged that
entry as inherited and possibly wrong. Measured: this block renders from
`packages/plugin-form`, so the three pin files live there. Nothing under
`packages/plugin-grid` is touched. The claim's file surface needs
amending; that belongs to the seat, and this PR posts no claim.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
pins: 'The I18nLabel object arm, resolved at the read site: through the real `SchemaRenderer`, a `properties.cancelText` map under a `zh` UI renders the map\'s `zh-CN` entry on the Cancel button, with `en` listed FIRST so an `en` or first-entry fallback fails the row. Before objectui#10935 the map was a raw Button child and threw "Objects are not valid as a React child". The button renders only when the host supplies `onCancel`, so the row adds one beside the document and asserts the host handler still runs. A plain-string control and the nothing-authored control (`Cancel`) stay green, and the first row asserts the mounted documents pass `safeValidateSchema`. The manifest half (a map is not a `type-mismatch`) is `masterDetailFormI18nLabelManifest.test.ts` in this directory (objectui#10935).',
pins: 'The per-element binding\'s members on the block that writes a parent AND its children in one batch. `object` is the only key mapped (the gate\'s default mapping), asserted at the fetch it causes, and it OUTRANKS a flat `objectName` on the same node: `next[objectKey] = composed.object` is unconditional, and the `??=` spelling would keep a rebound form on the old parent with no diagnostic. ⭐ The row only this block can make: the bound object reaches the DETAIL half. `deriveDetail` picks a child\'s FK by the parent object\'s name and the atomic batch names the parent on its first leg, measured on one child object that carries a lookup to BOTH candidate parents, so the derived FK and the parent leg together say which name won. `view` / `filter` / `sort` / `limit` are NOT mapped: a resolving view adds no parent collection query, an unresolvable one REPORTS, and in `edit` mode each child line read stays `{ $filter: { FK: recordId }, $top: 500 }` whatever the binding carries. ⚠️ Unmapped is not unread: beside a named `view` the gate merges the binding\'s `filter` with the view\'s, so a malformed one WITHHOLDS the whole form behind the malformed-filter notice, with a well-formed one as that row\'s control. A binding-free form is the control for the negatives. `dataSource` is absent from `ComponentPropsMap[\'object-master-detail-form\']` (it is the injected `ELEMENT_DATA_SOURCE_INPUT`), so nothing declared constrains the precedence. Pre-existing file (objectstack#7121), promoted after being read end to end and GROWN by five rows, four facts and a control (objectui#8071 slice 18).',
pins: 'Members are detail-collection OBJECTS (`MasterDetailDetailConfig`), and unlike this block\'s parent keys the read site is THIS block: `MasterDetailForm` is the key\'s only reader. Each member renders as its own section in AUTHORED order, headed by `title` (fallback `Line Items`), with `columns` reaching the line grid in authored order. ⭐ The sharp row: five members reach the grid through ONE hand-written object and four are RENAMED on the way (`sortField` to `sort_field`, `minRows` to `min_rows`, `maxRows` to `max_rows`, `addLabel` to `add_label`, `amountField` to `total_field`). That object is pinned as a SORTED KEY SET of exactly six, with two off-list members (one in the grid\'s own snake_case spelling) asserted not forwarded and an all-empty control beside it. `inlineMode: \'form\'` makes a list whose Add opens the full form (labelled `Add` when unset), and in grid mode `formFields` wider than `columns` offers the row form; each arm is the other\'s control. `childObject` and `relationshipField` address the atomic batch (each line a create on the child, linked by `{ $ref: 0 }`) and the edit-mode read (`{ $filter: { FK: recordId }, $top: 500 }` per collection). `totalField` receives the sum of the lines\' `amountField` on the PARENT leg, and a collection without one puts nothing there. With `{ childObject }` alone the FK and the columns are DERIVED from the child object, the one member behaviour the spec\'s description promises, and the batch writes the derived FK. `details: []` is the non-vacuity control: no section, no grid, a one-leg batch. The line grid is the REAL `LineItemsField`, wrapped only to record the props it is handed. The spec row is `z.array(z.unknown())`, whose description names five of the twelve members the renderer reads, and the registration declares no `of`, so the read site is the whole member contract. New file (objectui#8071 slice 18).',
pins: 'Members are bare PARENT field names drawn in AUTHORED order, against a no-`fields` control whose order is the object\'s. A name the parent does not declare is dropped rather than drawn as an untyped stub, measured with a DETAIL column name because one node here declares two field vocabularies, while the line grid keeps that column. `{ name }` is the same member as the bare name. The two pre-existing rows carry the sharpest member fact: the spec `FormFieldSchema` object that is legal in `sections[].fields` resolves to no name here, draws the named route-1 warning (objectui#8738, objectui#8847) and does not render, with a bare name as the firing control. ⚠️ Pinned as BEHAVIOUR and handed back: the key bounds what is DRAWN, not what the parent leg WRITES. A seeded field it does not list is still written, while the registration calls this key the parent pool for "the submitted set". The intersection with `sections` belongs to the `object-master-detail-form.sections` pin and is not re-asserted. `MasterDetailForm` reads none of this itself: the `parentSchema` memo copies the key onto an `object-form`-shaped node rendered through a DIRECTLY imported `<ObjectForm>`, the hand-written carrier that is why the pin is taken at this block. The spec row is `z.array(z.unknown())` and the registration declares no `of`, so the read site is the whole member contract. Pre-existing file (objectui#8847), promoted after being read end to end and GROWN by five rows (objectui#8071 slice 18).',
pins: 'Members are PARENT FIELD NAMES and each member value is that control\'s opening value \u2014 pinned in ONE file with `initialValues`, because this block\'s own registration declares them a pair ("Alternate spelling of `initialValues` the renderer also reads", with new schemas told to prefer `initialValues`) and a pair pinned apart leaves the PRECEDENCE stated nowhere. The precedence is PER MEMBER since objectui#9760: one shared `resolveInitialRecord(schema)` merges `{ ...initialValues, ...initialData }`, so with both authored a member `initialData` says nothing about keeps its `initialValues` value, and an EMPTY `initialData` contributes nothing rather than blanking the parent form. It used to be whole-object (`schema.initialData || schema.initialValues`, where `||` tests the object and `{}` is truthy); objectui#8071 wrote pins only, so it recorded that and handed the defect back, and the maintainer ruling on objectui#9760 repaired it \u2014 the two rows were FLIPPED there, \u26d4 not deleted. \u2b50 The two rows no sibling pin can make are the block\'s own: the seed reaches the PARENT LEG of the atomic batch and nothing else (a member naming a detail column seeds no child row and the posted batch still carries exactly one operation), and in `edit` mode with a `recordId` the fetched record REPLACES both keys wholesale, so a member the record omits opens empty rather than falling back to the seed. Neither key has a read site in this block at all: `MasterDetailForm`\'s `parentSchema` memo copies them key by key onto an `object-form`-shaped object rendered through a DIRECTLY imported `<ObjectForm>` \u2014 no `SchemaRenderer`, no registry lookup \u2014 which is exactly why the pin is taken here, since that hand-written map can drop a key while every declaration still reads correct (the failure `object-form.sections` records twice, objectui#9779 / objectui#9834). A no-keys row renders the same controls empty as the non-vacuity control. Both keys are registered `type: \'object\'` and typed `Record<string, any>`, so every object parses on both declared sides and the read site is the whole member contract. New file (objectui#8071 slice 15).',
0 commit comments