Skip to content

Commit 806f327

Browse files
test(plugin-form): object-form fields fixtures name fields as strings; the { name } row pins a stored read the declared member refuses (objectui#11550) (#11560)
Part of #11550 Clause-②: no Executes item 1 of triage ruling `5969880008` on the code side: the `{ name }` entry in `object-form` `fields` (and the `object-master-detail-form` hand-off) is retired from the authoring faces this PR owns. Item 2, the section "shape 3" inline runtime entry, is not touched. The governed half, the page-builder guide, is a separate draft PR from branch `claude/issue-11550-guide-fields-names-only`. The two land independently, in either order: neither reads the other's files, and the guide example passes every door with or without this PR. Session: `https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2` (os-dev, `mode:subagent` of the PM seat; identity = this branch). ## What changes | File | Before | After | |---|---|---| | `fieldSecurityPayload.test.tsx` | `FIELDS` = seven `{ name, label }` entries | seven field names. Every dropped `label` equalled the object field's own, and the form never read it | | `systemManagedPayload.test.tsx` | three `{ name, label }` entries | three names | | `drawerFirstLoadWindow-10190.test.tsx` | the "flat fields" shape was two `{ name }` entries | `['title', 'note']`. The shape exists to drive the flat (no `sections`) path, not the entry | | `objectFormFieldsMembers-8071.test.tsx`, row 6 | "tolerates the `{ name }` spelling — recorded as drift, not a second contract" | turned around: the `ObjectFormSchema` zod mirror refuses the entry, with exactly one issue (`invalid_type` at `fields.0`); a bare name is the accepted control; and a STORED entry still draws. The docblock paragraph that recorded the tolerance now records the retirement | | `topLevelFieldsWarnCoverage-8847.test.tsx` | "`{ name }` is tolerated as the same member as the bare name" | relabelled: the master-detail hand-off keeps the STORED read; not an authoring shape. Same assertion | | `.changeset/11550-object-form-fields-names-only.md` | none | empty frontmatter: test-only, no package released | `drawerFirstLoadWindow-10190` is a fixture the claim's list did not name. It was found by re-grepping current `main`, and it is in the claimed class (a fixture that authors `{ name }` field entries). ## The read stays, as stored-value tolerance `SimpleObjectForm`'s field loop in `ObjectForm.tsx` (the `(fieldName as any).name` arm) and `buildFlatFields` in `flatFields.ts` still draw a `{ name }` entry. The ruling allows it ("A stored `{ name }` entry may stay readable, as objectui#11533 kept stored rules readable"), and measurement shows no stricter door this repo could own: | Door, measured on `c476be0` (throwaway probe, deleted, never committed) | `fields: [{ name: ... }]` | `fields: ['...']` | |---|---|---| | `ObjectFormSchema` zod mirror (declared `string[]`) | refused, `invalid_type` at `fields.0` | accepted | | authored node via `safeValidateSchema` (the guide's example as written before PR B) | accepted | accepted | | authored node via `StrictAnyComponentSchema` | accepted | accepted | The authored node's `properties` bag is `@objectstack/spec`'s `ComponentPropsMap['object-form']` row by reference (objectui#11276), and that row types `fields` members as unknown today. objectui does not narrow a spec row. Triage notes that objectstack-ai/objectstack#21464 types the member as names once its `.objectui-sha` covers this landing. So the authoring door that would refuse the entry is upstream's; this side stops teaching it. ## The ruling's three pins - **A string `fields` entry draws as before.** Rows 1 to 3 of `objectFormFieldsMembers-8071` and the master-detail member rows are unchanged and green. The respelled fixtures exercise the string path on the default, modal and drawer arms. - **The guide's `os:check` example parses with names only.** That is PR B; `pnpm check:skill-examples` is green there. - **The section shape-3 pins stay green.** `submitTargetRefusal.test.tsx` and `sectionFields.test.ts` are untouched and green. ## Ablation (one-shot, from the committed state, restored) Both legs ran through `ablation-replace.mjs` (anchor must hit once, landing proven by count and blob hash) inside a script with an EXIT/INT/TERM restore trap. The tests import `../ObjectForm` and `@object-ui/types/zod`, which the root vitest config aliases to source, so neither leg needed a rebuild. - **Leg A, the read.** In `ObjectForm.tsx`, `(fieldName as any).name` was replaced by `undefined`: anchor 1 to 0, blob `2ae1112530f5` to `5e6d5e79185d`. `objectFormFieldsMembers-8071` + `topLevelFieldsWarnCoverage-8847` gave `Tests 2 failed | 13 passed (15)`, exactly the two stored-read rows. Restored: blob equals HEAD, `git diff HEAD` empty. - **Leg B, the declared side.** In the mirror, `fields: z.array(z.string())` was widened to `z.array(z.unknown())`: anchor 1 to 0, blob `360b2e65b15e` to `7aeed09ef049`. The same two files gave `Tests 1 failed | 14 passed (15)`, row 6 only (`expected [] to deeply equal [ { code: 'invalid_type', … } ]`). Restored the same way. ## Verification (at `12d4fb1`) - `pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-form^...' build`: exit 0. - `pnpm --filter @object-ui/plugin-form type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`): exit 0. `--listFilesOnly` on the test config lists all five edited test files, among 162 test files. - `pnpm exec vitest run packages/plugin-form/`: `Test Files 162 passed (162)`, `Tests 1859 passed | 1 skipped (1860)`. - The five edited files plus the two shape-3 pins: 104 of 104 passed before the edit (BASE `c476be0`) and after it. - Gates, each exit 0: `check:control-bytes`, `check:new-line-citations` (0 new), `check-changeset-presence` (one empty-frontmatter changeset), `check:changeset-claims`, `check-changeset-no-major`, `check-changeset-fixed`, `check:pending-changeset-literals`, `check:test-path-roots`, `check:vi-mock-specifiers`, `check-vi-mock-override-shape`. `check-governed-queue-guard.mjs --test` on all six paths: NOT GOVERNED. - ESLint, narrowed to the five edited files: 0 errors (25 `no-explicit-any` warnings, none added). Population: all five are inside eslint's own config (`--print-config` resolves each), and `--format json` reports 5 files. Invariance: `eslint.config.js` enables no type-aware linting and no rule under `eslint-rules/` reads the filesystem, so this diff cannot move a verdict on an untouched file. The full `pnpm lint` is CI's. ## Acceptance notes - **Authoring faces outside this claim, not edited, reported to the seat.** The published registration descriptions in `packages/plugin-form/src/index.tsx` still say "`{ name }` is tolerated" for the `fields` input of `object-form`, `view:form` and `embeddable-form`, and "(`{ name }` tolerated)" for `object-master-detail-form`. The console warning text in `warnUnresolvedTopLevelField` (`sectionFields.ts`) says "(`{ name }` is tolerated)". These still teach the entry. `index.tsx` is held by objectui#11536 in flight, so it is not edited here. The respelling is to drop each parenthetical, with a changeset, since `src/` text ships. - `FlatFieldsContext.fields` in `flatFields.ts` documents the `{ name }` read. It is not exported from the package entry, and it describes the read that stays, so it is left as it is. - objectui#11536 (`index.tsx`, the console parity test) and objectui#11512 (`MasterDetailForm.i18nLabels.test.tsx`) touch none of these files. --- _Generated by [Claude Code](https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent b34cc14 commit 806f327

6 files changed

Lines changed: 35 additions & 24 deletions
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
Test-only change in `@object-ui/plugin-form`; no package is released and the renderer is unchanged (objectui#11550). The `object-form` `fields` fixtures name their fields as strings: `fieldSecurityPayload`, `systemManagedPayload` and the flat drawer shape in `drawerFirstLoadWindow-10190` used `{ name, label }` entries whose `label` the form never read. The `{ name }` row in `objectFormFieldsMembers-8071` no longer pins a tolerated spelling. It now pins that the `ObjectFormSchema` mirror refuses the entry at `fields.0` while a STORED one still draws, and the master-detail hand-off row in `topLevelFieldsWarnCoverage-8847` is relabelled as the same stored read.

‎packages/plugin-form/src/__tests__/drawerFirstLoadWindow-10190.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ function heldRead() {
7373
}
7474

7575
const SHAPES = [
76-
['flat fields', { fields: [{ name: 'title' }, { name: 'note' }] }],
76+
['flat fields', { fields: ['title', 'note'] }],
7777
['sections', { sections: [{ name: 'basics', label: 'Basics', fields: ['title', 'note'] }] }],
7878
] as const;
7979

‎packages/plugin-form/src/__tests__/objectFormFieldsMembers-8071.test.tsx‎

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,30 @@
4949
* site changed — the ablation objectui#8071 ran on this file removed the
5050
* `typeof fieldName === 'string'` name resolution and reddened rows 1, 3 and 5.
5151
*
52-
* The `{ name }` spelling in row 5 is recorded as the renderer's tolerance, not
53-
* as a second contract (AGENTS.md #0.1): it is what `buildFlatFields` documents
54-
* ("field names or objects carrying one") and what the loop's
55-
* `(fieldName as any).name` fallback does. Pinned so that a future edit removing
56-
* it is a decision rather than an accident, and so the contrast with the
57-
* REJECTED `{ field }` spelling above it cannot quietly invert.
52+
* Row 6 is the `{ name }` entry, and objectui#11550 TURNED IT AROUND (triage
53+
* ruling 5969880008). It used to be pinned as a tolerated spelling, which every
54+
* authoring face then taught. It is retired from authoring: the declared member
55+
* is `string[]` (`ObjectFormSchema.fields`, whose zod mirror refuses the entry
56+
* at `fields.0`), and the guide and the fixtures name fields as strings. What
57+
* stays is the READ: the loop's `(fieldName as any).name` and `buildFlatFields`
58+
* still draw an entry STORED that way, as objectui#11533 kept stored rules
59+
* readable. So the row pins both halves, and the `{ field }` contrast above it
60+
* stays exactly as it was.
5861
*/
5962

6063
import { describe, it, expect, vi } from 'vitest';
6164
import { render, waitFor } from '@testing-library/react';
6265
import React from 'react';
6366
import { registerAllFields } from '@object-ui/fields';
67+
import { ObjectFormSchema as ObjectFormMirror } from '@object-ui/types/zod';
6468
import { ObjectForm } from '../ObjectForm';
6569

6670
registerAllFields();
6771

72+
/** `{ code, path }` of what a parse refused, so row 6 names WHICH entry was refused and HOW. */
73+
const refusal = (result: { success: boolean; error?: { issues: Array<{ code: string; path: PropertyKey[] }> } }) =>
74+
result.success ? [] : result.error!.issues.map((issue) => ({ code: issue.code, path: issue.path.join('.') }));
75+
6876
const OBJECT_SCHEMA = {
6977
name: 'invoice',
7078
fields: {
@@ -162,7 +170,15 @@ describe('object-form `fields` members are BARE FIELD NAMES (objectui#8071)', ()
162170
).toEqual(['Note']);
163171
});
164172

165-
it('6. tolerates the `{ name }` spelling — recorded as drift, not a second contract', async () => {
173+
it('6. a STORED `{ name }` entry still draws, but the declared member refuses it (objectui#11550)', async () => {
174+
// The authoring half: the mirror of the declared `string[]` refuses the
175+
// entry, and ONLY the entry. The bare name beside it is the control.
176+
const node = { type: 'object-form', objectName: 'invoice', mode: 'create' } as const;
177+
expect(refusal(ObjectFormMirror.safeParse({ ...node, fields: [{ name: 'note' }] }))).toEqual([
178+
{ code: 'invalid_type', path: 'fields.0' },
179+
]);
180+
expect(ObjectFormMirror.safeParse({ ...node, fields: ['note'] }).success).toBe(true);
181+
// The read half: metadata already stored with the entry keeps drawing.
166182
expect(await renderedFieldLabels({ fields: [{ name: 'note' }] })).toEqual(['Note']);
167183
});
168184
});

‎packages/plugin-form/src/__tests__/topLevelFieldsWarnCoverage-8847.test.tsx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@
5050
* untyped stub. A DETAIL column name is the case this composition invites,
5151
* because one node declares two field vocabularies. It renders nothing in
5252
* the parent, while the line grid keeps that column;
53-
* - `{ name }` is tolerated as the same member as the bare name;
53+
* - a STORED `{ name }` member still draws as its bare name. It is not an
54+
* authoring shape: objectui#11550 retired it from every authoring face, and
55+
* `objectFormFieldsMembers-8071.test.tsx` row 6 pins the declared refusal.
56+
* This row pins only that the hand-off keeps the stored read;
5457
* - ⚠️ the key bounds what is DRAWN, not what the PARENT leg of the atomic
5558
* batch WRITES: a seeded parent field it does not list is still written.
5659
* That is the ruled behaviour (objectui#11114), not a defect: the
@@ -251,7 +254,7 @@ describe('object-master-detail-form `fields` — the member shape (objectui#8071
251254
).toBeGreaterThan(0);
252255
});
253256

254-
it('`{ name }` is tolerated as the same member as the bare name', async () => {
257+
it('a STORED `{ name }` member still draws as its bare name — read only, not authoring (objectui#11550)', async () => {
255258
const { container } = await mountMembers({ fields: [{ name: 'note' }, 'status'] });
256259
expect(parentFields(container)).toEqual(['note', 'status']);
257260
});

‎packages/plugin-form/src/fieldSecurityPayload.test.tsx‎

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,15 +127,7 @@ const DENIED = ['score', 'adjusted_score'] as const;
127127
*/
128128
const UNCHANGED_ALLOWED = { indicator_name: 'Revenue', target_value: 100, weight: 20 };
129129

130-
const FIELDS = [
131-
{ name: 'sheet', label: 'Sheet' },
132-
{ name: 'indicator_name', label: 'Indicator' },
133-
{ name: 'target_value', label: 'Target' },
134-
{ name: 'weight', label: 'Weight' },
135-
{ name: 'actual_value', label: 'Actual' },
136-
{ name: 'score', label: 'Score' },
137-
{ name: 'adjusted_score', label: 'Adjusted' },
138-
];
130+
const FIELDS = ['sheet', 'indicator_name', 'target_value', 'weight', 'actual_value', 'score', 'adjusted_score'];
139131

140132
const makeDataSource = () => {
141133
const update = vi.fn(async (_o: string, _id: string, d: any) => ({ id: 'LINE1', ...d }));

‎packages/plugin-form/src/systemManagedPayload.test.tsx‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,7 @@ describe('form write payloads — server-owned columns (objectui#10108)', () =>
214214
// Business fields only — no ownership or audit column is declared
215215
// in any section, which is what makes an emitted one an INJECTION
216216
// rather than a displayed field being echoed.
217-
fields: [
218-
{ name: 'plan_indicator', label: 'Indicator' },
219-
{ name: 'actual_value', label: 'Actual' },
220-
{ name: 'remark', label: 'Remark' },
221-
],
217+
fields: ['plan_indicator', 'actual_value', 'remark'],
222218
} as any}
223219
dataSource={ds}
224220
/>,

0 commit comments

Comments
 (0)