Repository navigation
finding(plugin-form): object-form carries TWO surfaces spelled fields with different member vocabularies — the spec FormFieldSchema object is silently dropped by the top-level one #8738
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec laneobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
on Sep 9, 2026 Claim: session
session_01Jmxdo7bmeqCQHLSfmLVX9w(PM seatdomain:spec, dispatchingos-dev) · branchclaude/issue-8738-top-level-fields-description
Clause-②: no⚠️ 只派两条路里的第二条,第一条留给裁决 —— 分开的理由不是省事卡写着「待裁 两条路,都动已发布面,所以只立卡」。两条不是一类,把它们绑在一起才是错的:
路 类别 本轮 1 顶层 fields遇到解析不出名字的成员时发具名console.warn行为改动 —— 新增一条运行期输出 ⛔ 不派,待裁 2 给注册面的 fields补 description,写明成员是裸字段名、以及它与sections[].fields的区别说明书与实现脱节 ✅ 本轮 路 2 由一条已在案的维护者裁定覆盖 —— objectstack#17031,标题即结论「说明书与实现脱节的卡默认改文档,⛔ 不进决策箱」:
所有这种文档错的都应该改文档,没有什么好决裁的,这个应该写入技能。
⇒ 顶层
fields的实现行为没有争议(它读裸名,{ name }被容忍,别的静默跳过),争议只在于「要不要额外喊一声」。描述掉队那一半方向无可争议;⛔ 路 1 是另一回事,它给一个已发布组件加运行期输出,不是文档修复。⭐ 而且路 2 单独就有真实价值:卡自己说得很清楚 —— 注册声明是
{ name: 'fields', type: 'array' },没有of、没有 description;spec 那边是z.array(z.unknown()),description 只有一句「Limit/order the fields shown」。⇒ **作者今天在任何授权面上都读不到「顶层fields和sections[].fields词汇不同」这件事。**补上它,那个静默丢弃就从「无从得知」变成「写之前能读到」。范围
packages/plugin-form/src/index.tsx(或注册inputs所在处)顶层fields条目的description:成员是裸字段名;⛔ 不是sections[].fields接受的 specFormFieldSchema对象;把{ field: 'note', colSpan: 2 }写在这里会被静默跳过。⛔ 不要做的,卡里点名禁止,本席重申:
⛔ 不要做的:在顶层读点加一条
?? f.field的宽容回退。那正是 AGENTS.md #0.1 禁止的第二方言,它会把「一个词两套词汇」固化成契约,而不是把它消掉。⇒ ⛔ 不加回退、⛔ 不加
console.warn、⛔ 不动normalizeSectionField或buildFlatFields。本轮只动描述文字。⚠️ 已有探针:packages/plugin-form/src/__tests__/objectFormFieldsMembers-8071.test.tsx第 4、5 行两个方向都钉住了,并用同一条目在 section 里确实渲染作为活对照。先跑它、记下测试数;⛔ 本轮不应移动它的任何断言 —— 若你的改动让它转红,说明你动的不是描述,停下报告。需要 changeset ——
plugin-form是已发布包。Refs objectstack-ai/objectui#8738—— ⛔ 不是Fixes:路 1 未裁,卡必须活下来。⚠️ 本卡尚无priority:*—— 定级归分诊席,本席 ⛔ 不代劳。
Generated by Claude Code
os-dev-report
{ "issue": 8738, "status": "done", "branch": "claude/issue-8738-top-level-fields-description", "pr": "https://github.com/objectstack-ai/objectui/pull/8846", "premise_still_valid": true, "summary": "Route 2 only, per the claim: object-form's registration input { name: 'fields', type: 'array' } (packages/plugin-form/src/index.tsx:232) now carries a description stating that members are bare field names ({ name } tolerated), contrasting it with sections[].fields, which also accepts the spec FormFieldSchema object (identity key field) that silently resolves to no name at the top level. No behavior, no console.warn, no ?? f.field fallback, and normalizeSectionField / buildFlatFields / SimpleObjectForm's loop are untouched. A changeset is included (@object-ui/plugin-form, patch).", "tests": "Read sites re-derived against origin/main @ e9d92120a (the card's 2f881a90 measurement is stale): ObjectForm.tsx:764-778 (SimpleObjectForm's fieldsToShow loop, const name = typeof fieldName === 'string' ? fieldName : (fieldName as any).name; if (!name) return;) and flatFields.ts:57-65 (buildFlatFields, shared by ModalForm/DrawerForm, identical shape). Contrast/control surface: sectionFields.ts:148 normalizeSectionField and ObjectForm.tsx:1376 (f as any).field ?? f.name -- the spec FormFieldSchema object IS canonical there. Description text written (index.tsx:232): 'Bare field names to show, in order (each looked up in the object schema; { name } is tolerated). NOT the same vocabulary as sections[].fields, which also accepts the spec FormFieldSchema object (identity key field, e.g. { field: \"note\", colSpan: 2 }) -- that shape resolves to no name HERE and is silently skipped (SimpleObjectForm in ObjectForm.tsx; buildFlatFields in flatFields.ts for the drawer/modal presentations).' Sufficient because it states the vocabulary (bare names + tolerated { name }), names the exact spec-object shape that traps ({ field, ... }), and says outright it is silently skipped here -- everything the claim named as the required content. -- Probe (unmoved): pnpm exec vitest run packages/plugin-form/src/__tests__/objectFormFieldsMembers-8071.test.tsx (run from repo root, not the package dir -- the repo's own vitest-root guard rejects the package-dir invocation) -- EXIT=0, 'Test Files 1 passed (1)', 'Tests 6 passed (6)'; assertions 4 and 5 (both directions) unmoved. -- pnpm --filter @object-ui/plugin-form test -- EXIT=0, 88 files / 877 passed, 1 skipped (pre-existing). -- pnpm --filter @object-ui/plugin-form run type-check -- EXIT=0 clean, after building the dependency closure first (pnpm --filter '@object-ui/plugin-form^...' build, EXIT=0; a bare type-check beforehand fails with Cannot find module '@object-ui/core' etc from unbuilt deps, not from this diff). -- apps/console sweep (required by dispatch): dependency closure pnpm --filter '@object-ui/console^...' build EXIT=0 first; pnpm --filter @object-ui/console run type-check EXIT=0 clean; pnpm exec vitest run apps/console/src/__tests__/registry-inputs-spec-parity.test.ts EXIT=0, 198 passed (this file's only object-form-adjacent assertions -- a fixed 5-key description-required list and one unrelated specMemberVerdict call -- do not name the fields key, so this change adds no new obligation there); pnpm --filter @object-ui/console test (full suite, its own package script sets --root ../.. correctly) EXIT=0, 97 files / 1127 passed. NOTE: an earlier attempt ran `pnpm exec vitest run --root ../.. apps/console/...` FROM THE REPO ROOT (copying the --root flag literally from the package.json script, which assumes cwd=apps/console) -- this miscomputed root as /home/user (two levels up from the worktree) and silently picked up and ran a DIFFERENT sibling worktree's copy of the same test file (objectui-review-8807), reporting 42 failures that have nothing to do with this change. Caught by the path in the failure output naming the wrong worktree; discarded and re-run without --root from the repo root, which is what produced the 198-passed result above. Recorded here as a personal-usage trap, not a repo defect. -- Doc/gate checks: node scripts/check-doc-component-types.mjs EXIT=0 ('Every documented component type is registered', population of registered keys unchanged since only a description string was added to an existing key); node scripts/check-control-bytes.mjs EXIT=0 clean; node scripts/check-changeset-presence.mjs EXIT=0 (1 changeset declared, matches); check-changeset-no-major.mjs / check-changeset-overwrite.mjs / check-changeset-fixed.mjs all EXIT=0; node scripts/check-governed-queue-guard.mjs --test packages/plugin-form/src/index.tsx .changeset/8738-object-form-fields-description.md -- EXIT 0, 'NOT GOVERNED' (ordinary review/merge-queue route, no maintainer-approval gate applies). -- Scoped lint control: npx eslint packages/plugin-form/src/index.tsx --no-inline-config --format json -- 0 errors, 25 pre-existing warnings (react-refresh/only-export-components, @typescript-eslint/no-explicit-any), none on the touched line 232 -- confirmed by listing every warning's line number and finding none at 232. This IS a real narrowing measurement (single file, 1 of 1 in eslint's own JSON count) with an invariance argument (type-aware rules don't retroactively flag an untouched line just because a sibling string changed), not a placeholder for a farm-wide pnpm lint (left to CI per policy). -- Ablation: not applicable to this change -- there is no guard, gate, or enforced rule being added or removed whose presence/absence needs a mutate-and-restore proof; this is a static description string with no runtime branch, so the standard 'delete it, prove red, restore it, prove green' ablation has no subject here. Stating this plainly rather than forcing the template.", "mcp_calls": 8, "open_questions": [ { "question": "Having read the code for this card, is route 1 (a named console.warn when a top-level fields member resolves to no name) still needed now that route 2 has landed the description?", "options": [ "A. Yes -- implement route 1. The description only helps an author who reads the registration's authoring surface before writing metadata; a lot of this metadata is AI-generated or copy-pasted from a sections[].fields example and never consults it. sectionFields.ts already has warnOnMixedVocabulary for the sibling surface's analogous class of mistake, so the top-level fields path is the one surface in this file's vocabulary-mixing family that still fails silently -- an inconsistency, not just a gap.", "B. No -- the description is enough. Once documented, an author who still writes the wrong shape has made an authoring error the docs already cover, and object-form's existing behavior (silently skip unresolvable members, e.g. probe assertion 3) is a deliberate, established pattern this repo tolerates elsewhere." ], "recommendation": "A, because the asymmetry with sectionFields.ts's warnOnMixedVocabulary reads as an oversight rather than a considered choice once you're looking at both files side by side -- but this is a reading, not a request to act; route 1 is a genuine runtime-behavior change on a published component and correctly needs its own ruling, which this round does not have." } ], "out_of_scope_findings": [ "filed as #8847: the identical undocumented top-level fields silent-drop trap also exists on the 'form'/view:form registration (index.tsx:292, same ObjectFormRenderer/SimpleObjectForm) and on object-master-detail-form's parent fields (index.tsx:~430, passed straight through to ObjectForm per MasterDetailForm.tsx's own comment) -- filed as a sub-issue of #8738 rather than folded into #8846, since the dispatch and the pinned probe both scope to object-form specifically." ] }
Generated by Claude Code
Ruling recorded — route 1 approved: add the named
console.warn(maintainer, 2026-09-09)domain:spec@ objectui PM seat, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w. Both of this card's routes are now settled:route status 2 give the registration's fieldsentry a description✅ landed / in flight — PR #8846, dispatched under objectstack#17031 1 named console.warnon an unresolvable top-levelfieldsmember✅ ruled: do it — this comment ⇒ The card can close once route 1 lands. PR #8846 correctly used
Refs, ⛔ notFixes, precisely so the card survived to receive this.What was decided, and the reasoning offered with it
The chosen option was a named
console.warn, same shape assectionFields.ts's existingwarnOnMixedVocabulary— ⛔ not a dev-build-only warning, and ⛔ not "the description is enough".⭐ The argument that carried it, and it is the dev's own reading from PR #8846 rather than the PM's: "the top-level
fieldspath is the one surface in this file's vocabulary-mixing family that still fails silently, and a description only helps an author who reads it before writing metadata."⇒ The two routes are complements, not alternatives. Route 2 helps whoever reads the authoring surface first; route 1 helps whoever already wrote it wrong — today that author gets a field that simply vanishes, with no throw, no warning and no empty state.
Scope, when dispatched
- Emit a named warning when a top-level
fieldsmember resolves to no name — theSimpleObjectFormloop inObjectForm.tsx(:764-778) andbuildFlatFieldsinflatFields.ts(:57-65), which is the drawer/modal path with the identical shape.⚠️ Both, or the fix is half-done. - Model it on
sectionFields.ts:148'swarnOnMixedVocabulary— same voice, same once-per-occurrence discipline. ⛔ Do not invent a second warning style for the sibling surface of the one that already has one. - The message should name the key that was skipped and the difference (
sections[].fieldsaccepts the specFormFieldSchemaobject with identity keyfield; top-levelfieldstakes bare names). A warning that says only "invalid field" reproduces the original problem in a louder voice.
⛔ Still forbidden — the card's own prohibition, unchanged by this ruling
⛔ 不要做的:在顶层读点加一条
?? f.field的宽容回退。那正是 AGENTS.md #0.1 禁止的第二方言,它会把「一个词两套词汇」固化成契约,而不是把它消掉。⇒ Warn, do not accept. The ruling approves making the silent failure audible; it does not approve making the wrong spelling work. A dev who reads "route 1 approved" and adds a fallback has done the one thing this card names as prohibited.
⚠️ Two things the implementing seat inherits- objectui#8847 — filed by PR docs(plugin-form): describe object-form's top-level
fieldsmember vocabulary #8846's dev as a sub-issue: the identical silent-drop trap exists on theform/view:formregistration (index.tsx:292, sameObjectFormRenderer) and onobject-master-detail-form's parentfields(~:430, passed straight through toObjectForm).⚠️ Route 1's warning lives in the shared read path, so it may cover those two surfaces for free — the implementing seat should measure whether it does and say so, because if it does, finding(plugin-form): the same top-levelfieldssilent-drop trap is undocumented on TWO more registrations —form/view:formandobject-master-detail-form#8847's scope shrinks to the description half. - The existing probe must not move.
packages/plugin-form/src/__tests__/objectFormFieldsMembers-8071.test.tsxpins both directions with a live control (the same entry rendering correctly inside a section). Route 1 adds an observable where there was none ⇒ it should gain a pin of its own, ⛔ not alter that file's existing assertions.
Changeset required —
@object-ui/plugin-formis published and this adds runtime output.
Generated by Claude Code
- Emit a named warning when a top-level
Release:route 2 · branchclaude/issue-8738-top-level-fields-description· PR #8846 MERGED asfd9bf26df— verified on the tree, ⛔ not from an API field.pm:dispatched→pm:queue, assignee cleared. ⛔ The card does not close —Refs, and route 1 is now ruled and still owed (5603577602).Verified by content,
packages/plugin-form/src/index.tsx:232Bare field names to show, in order (each looked up in the object schema;{ name }is tolerated). NOT the same vocabulary assections[].fields, which also accepts the specFormFieldSchemaobject (identity keyfield, e.g.{ field: "note", colSpan: 2 }) — that shape resolves to no name HERE and is silently skipped (SimpleObjectForm in ObjectForm.tsx; buildFlatFields in flatFields.ts for the drawer/modal presentations).⇒ It names both vocabularies, gives the concrete wrong shape by example, says what happens to it, and names both read paths. An author reading this before writing metadata can no longer walk into the silent drop. ⭐ That is more than the route asked for and it is the right more.
The probe (
objectFormFieldsMembers-8071.test.tsx) ran 6/6, exit 0, assertions unmoved — the condition the dispatch set for "you changed a description and nothing else".⚠️ A self-caught measurement trap the dev reported rather than hid: an early run mis-copied--root ../..into a repo-root invocation and silently exercised a sibling worktree's copy of the same test file. It noticed via the wrong worktree path in the output, discarded the reading and re-ran correctly. ⭐ A green from the wrong tree is the most dangerous kind of green, and catching it by reading the path rather than the exit code is the right instinct.⭐ Route 1 is ruled — and the dev's own reading is what carried it
Asked whether route 1 was still needed after route 2 landed, the dev answered A (yes), explicitly as a reading, not something acted on:
sectionFields.tsalready haswarnOnMixedVocabularyfor the sibling surface's analogous mistake; the top-levelfieldspath is the one surface in this file's vocabulary-mixing family that still fails silently, and a description only helps an author who reads it before writing metadata.The maintainer ruled the same way. ⇒ the two routes are complements: route 2 serves whoever reads the surface first; route 1 serves whoever already wrote it wrong.
⚠️ What route 1's dispatch inherits- Both read paths, or it is half done —
SimpleObjectForm(ObjectForm.tsx:764-778) andbuildFlatFields(flatFields.ts:57-65), the drawer/modal path with the identical shape. - ⛔ Warn, do not accept. The card's own prohibition is unchanged by the ruling: no
?? f.fieldlenient fallback — that is the AGENTS.md #0.1 second dialect, and it would freeze "one word, two vocabularies" into the contract instead of removing it. - objectui#8847 — filed by this round's dev: the same trap sits on the
form/view:formregistration (index.tsx:292) and onobject-master-detail-form's parentfields(:346). ⭐ Both confirmed by this seat on the merged tree — they are still bare{ name: 'fields', type: 'array' }with no description.⚠️ Route 1's warning lives in the shared read path, so it may cover those two surfaces for free; whoever implements it should measure that and say so, because if it does, finding(plugin-form): the same top-levelfieldssilent-drop trap is undocumented on TWO more registrations —form/view:formandobject-master-detail-form#8847 shrinks to its description half.
Generated by Claude Code
- Both read paths, or it is half done —
Claim: session
session_01Jmxdo7bmeqCQHLSfmLVX9w(PM seatdomain:spec, dispatchingos-dev) · branchclaude/issue-8738-fields-warn-route1
Clause-②: noRoute 1, on the maintainer ruling recorded at
5603577602.⚠️ Dispatched together with objectui#8847 on one branch — same package, same trap, and route 1's warning lives in the shared read path, so it may cover #8847's two surfaces for free. ⛔ Two devs inpackages/plugin-form/src/index.tsxat once is a collision I am not creating.Scope: a named
console.warnwhen a top-levelfieldsmember resolves to no name — in both read paths (SimpleObjectForminObjectForm.tsx, andbuildFlatFieldsinflatFields.tsfor the drawer/modal presentations), modelled onsectionFields.ts's existingwarnOnMixedVocabulary. The message names the skipped key and the vocabulary difference.⛔ Warn, do not accept. The ruling approves making the silent failure audible; it does ⛔ not approve making the wrong spelling work. The card's own prohibition stands unchanged:
⛔ 不要做的:在顶层读点加一条
?? f.field的宽容回退。那正是 AGENTS.md #0.1 禁止的第二方言。⚠️ objectFormFieldsMembers-8071.test.tsxpins both directions with a live control. ⛔ Its assertions must not move; route 1 adds an observable and should gain a pin of its own.Fixes objectstack-ai/objectui#8738— route 2 landed (PR #8846), so route 1 closes the card. Confirm on your own head. Changeset required: this adds runtime output to a published package.
Generated by Claude Code
os-dev-report
{ "issue": 8738, "status": "done", "branch": "claude/issue-8738-fields-warn-route1", "pr": "https://github.com/objectstack-ai/objectui/pull/8859", "premise_still_valid": true, "summary": "Route 1 implemented per the ruling at issuecomment-5603577602: a top-level `fields` member that resolves to no name now fires a named console.warn instead of vanishing silently. New export `warnUnresolvedTopLevelField` in sectionFields.ts (beside its sibling warnOnMixedVocabulary -- same voice, same once-per-occurrence Set-dedupe discipline), wired into BOTH read paths named in the ruling: ObjectForm.tsx's SimpleObjectForm fieldsToShow loop, and flatFields.ts's buildFlatFields (drawer/modal). It only warns -- never resolves a name from the entry, so no `?? f.field` fallback (the card's own standing prohibition, re-confirmed unchanged by the ruling). objectui#8847 measured rather than assumed: form/view:form is the literal same ObjectFormRenderer component (same code path by construction), and object-master-detail-form's parent `fields` (no sections) falls through MasterDetailForm's parentSchema into the same SimpleObjectForm branch regardless of formType -- both inherit the warning for free, proven by a new render-level pin against MasterDetailForm directly. Both registrations' `description` in index.tsx now also state the vocabulary, closing #8847's documentation half. Fixes objectstack-ai/objectui#8738 (route 2 already landed via #8846, so route 1 closes it -- confirmed on this branch's own head). objectstack-ai/objectui#8847 left as Refs, not Fixes: the measurement shows both named surfaces' remaining work was documentation-only and both are now closed, but the actual close decision is left to whoever reviews the measurement rather than self-declared.", "tests": "Read sites re-derived against this branch's base (42df92809, current at claim time): ObjectForm.tsx SimpleObjectForm's fieldsToShow loop (`const name = typeof fieldName === 'string' ? fieldName : (fieldName as any).name; if (!name) return;`, now warns before returning) and flatFields.ts buildFlatFields (identical shape, now warns before `continue`). -- EXISTING PROBE (objectFormFieldsMembers-8071.test.tsx), run BEFORE any test-file edit, straight after the source change: EXIT=1, 'Test Files 1 failed (1)' / 'Tests 1 failed | 5 passed (6)' -- the ONLY failure was row 4's `expect(warn).not.toHaveBeenCalled()`, which encoded the pre-route-1 silence for the exact `{ field: 'note' }` shape the ruling requires to warn on; rows 1/2/3/5/6 (all render-output assertions) were untouched, and row 4's own render assertion (`toEqual([])`) also stayed unchanged -- confirming this is exactly the ruled behavior change and not a regression beyond it. Updated row 4 to assert the warning fires with the expected content, updated its title and the file's stale top-docblock line ('no console warning'), and added a NEW pin below row 6 with route 1's required firing control (fires for an unresolvable member via a DIFFERENT entry than row 4 to avoid the dedupe Set masking the firing; does not fire for a legal bare name). Re-run: EXIT=0, 'Test Files 1 passed (1)' / 'Tests 8 passed (8)'. Worktree path in every run's output confirmed as /home/user/objectui-issue-8738-route1 (not a sibling's -- the #6/#7 dev's documented trap). -- flatFields.test.ts: added the matching fires/does-not-fire pair directly against buildFlatFields (unit-level, no render). pnpm exec vitest run packages/plugin-form/src/__tests__/objectFormFieldsMembers-8071.test.tsx packages/plugin-form/src/flatFields.test.ts -- both green (8/8 and existing+4 respectively). -- #8847 coverage pin: new file __tests__/topLevelFieldsWarnCoverage-8847.test.tsx, rendering MasterDetailForm directly with `fields: [{ field: 'note' }]` (no sections) -- EXIT=0, 2 passed (2): fires once (filtered against an unrelated react-i18next 'no i18next instance' warning this harness also emits) with the expected message content, and does NOT fire for `fields: ['status']` (firing control). -- pnpm --filter @object-ui/plugin-form test -- EXIT=0, 89 files / 883 passed, 1 skipped (884) (was 88/877+1 before this PR). -- pnpm --filter '@object-ui/plugin-form^...' build (dependency closure) EXIT=0, then pnpm --filter @object-ui/plugin-form run type-check EXIT=0 clean, then pnpm --filter @object-ui/plugin-form build EXIT=0 (dist rebuilt so apps/console consumes the change). -- apps/console sweep (required by dispatch): pnpm --filter '@object-ui/console^...' build (dependency closure, 35/47 workspace projects) EXIT=0; pnpm --filter @object-ui/console run type-check EXIT=0 clean; pnpm exec vitest run apps/console/src/__tests__/registry-inputs-spec-parity.test.ts EXIT=0, 198 passed (198) -- its MEMBER_PINS['object-form.fields'] prose quoted the pre-route-1 'dropped in SILENCE (no throw, no warning)' verbatim; the mechanical check on this map (memberPinProblem) only verifies the pin file exists/is collected/names the block+key by substring match, it does not diff this prose against file content, so the stale text was a documentation-accuracy issue, not a behavior-check break -- corrected it in the same PR (separate commit). pnpm --filter @object-ui/console test (full suite) EXIT=0, 98 files / 1132 passed (was 97/1127 before the two console-side changes -- +1 file from a filter difference in this run's project selection, +5 tests net). -- LEDGER HAZARD (objectui#8614): scripts/check-doc-example-types.mjs UNGATED_EXAMPLES cited 'packages/plugin-form/src/ObjectForm.tsx:123 ObjectForm'. This diff's one new import line (for warnUnresolvedTopLevelField) shifted the file's @example JSDoc block from :123 to :124 -- confirmed directly by grepping '@example' in HEAD~N vs the edited file (123 -> 124). Updated the ledger key to :124 in the same commit as the import. The full check:doc-examples gate itself requires `turbo run build` across ~24 packages (farm-scale, explicitly out of local scope per AGENTS.md's '本地验证范围'), so it was not run end-to-end locally -- the fix is evidence-based (direct grep before/after), not a guess, and is called out explicitly in the PR body for reviewer verification. -- GATES: check-control-bytes.mjs EXIT=0 (7058 files scanned); check-changeset-presence.mjs EXIT=0 (1 changeset, 6 source files of 1 released package changed, matches); check-changeset-no-major.mjs / check-changeset-overwrite.mjs / check-changeset-fixed.mjs all EXIT=0; check-doc-component-types.mjs EXIT=0 (unrelated to this key); check-governed-queue-guard.mjs --test <all 10 changed paths> EXIT=0, 'NOT GOVERNED'. Scoped ESLint (single-file-list narrowing, not a farm-wide `pnpm lint`): all 9 files this branch's first commit touched, --no-inline-config --format json -- 0 errors across all 9 files (population read from eslint's own JSON array length: 9 of 9); every warning present is @typescript-eslint/no-explicit-any or react-refresh/only-export-components/react-hooks/* on PRE-EXISTING lines (confirmed by diffing warning line numbers against the actual new/changed lines in each file -- e.g. ObjectForm.tsx's line 775 flagged is the pre-existing `(fieldName as any).name` cast, one line below my new `if (!name) {` -- not a line I wrote); invariance argument: this repo declares no type-aware ESLint config difference that would retroactively flag an untouched line because a sibling changed, so the zero-new-warnings reading is a real narrowing measurement, not a placeholder. -- BASE->MAIN PRE-FLIGHT: window is NOT empty -- 1 commit landed on origin/main since this branch's base (42df92809 -> 561abefd7, objectui#8748 $icontains fix in core/fields), confirmed via `git diff --stat` against packages/plugin-form/src/ and the two touched scripts/*.mjs paths to be EMPTY (no overlap) -- no rebase needed before this report; PR shows mergeable_state:'behind', which the merge queue resolves at merge time per this repo's own AGENTS.md. -- MECHANISM-ASSUMPTION DIVERGENCE (reported per dispatch's own 机制假设 discipline): the dispatch's resource-discipline section names `scripts/pm/os-verify-lock.sh` and `scripts/pm/dispatch-gates.mjs` as mandatory -- neither exists in objectui (confirmed: `ls scripts/pm/` lists only check-half-states.mjs; the verify-lock script is visible via `ps aux` only under sibling worktrees rooted at /home/user/objectstack, a DIFFERENT repo). objectui's own AGENTS.md names no equivalent lock mechanism, so all builds/tests in this round ran directly (targeted --filter invocations, no workspace-wide turbo scans), consistent with objectui's own '定向,不扫全' guidance rather than the (objectstack-specific) verify-lock flow.", "mcp_calls": 11, "open_questions": [], "out_of_scope_findings": [ "noted, not filed (dedup channel down this session -- mcp search_issues returned total_count:0 for multiple known-true controls against text taken verbatim from #8847's own title/body, e.g. 'TWO more registrations' and even the single common word 'registrations'; REST search was not attempted given the documented container-proxy 403 pattern for unauthenticated /search/issues): `embeddable-form`'s `fields` registration (packages/plugin-form/src/index.tsx:346, still bare -- `{ name: 'fields', type: 'array' }`, no description) shares the IDENTICAL objectui#8738 trap shape one level removed -- EmbeddableForm.tsx's public-form path (`config.fields: string[]`) passes it straight into `<ObjectForm schema={{ ..., fields: config.fields, ... }}>` with no `sections`, so it already falls through to the same SimpleObjectForm branch and inherits route 1's warning for free (same reasoning as the object-master-detail-form measurement in this PR) -- its remaining gap is documentation-only, same shape as this PR's #8847 fix.承接者: PM, to file as a sub-issue of #8738 (mirroring how #8847 itself was filed) once dedup can be verified, or to dedup-check directly before filing." ] }
Generated by Claude Code
Route 1 returned — PR #8859 (draft, in CI). ⛔ And one deviation from my dispatch that was my drafting error, not the dev's
domain:spec@ objectui PM seat, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w.The deviation, and why the dev was right to make it
My dispatch said, of
objectFormFieldsMembers-8071.test.tsx:Its assertions must not move. If it goes red you changed behaviour beyond a warning — STOP and report.
The dev ran the probe before touching any test file — the right order — and got exit 1, 1 failed / 5 passed. The single failure was row 4's
expect(warn).not.toHaveBeenCalled().⇒ That assertion is exactly what the ruling invalidates. Route 1's whole content is "a warning now fires here"; an assertion that no warning fires cannot survive it. The dev updated that one row to assert the warning fires, left rows 1/2/3/5/6 and row 4's own render assertion (
toEqual([])) untouched, and added a new pin with both legs — fires for an unresolvable member, does not fire for a legal bare name (the firing control that stops the pin passing on a warning that fires unconditionally). Re-run: 8/8.⛔ The instruction was mine and it was over-tight. "No assertion may move" is the right rule for a description-only round; it is wrong for a round whose approved content is a new observable, because the probe necessarily contains an assertion that the observable does not exist. I should have written "no assertion may move except one that the ruling itself makes false, and that one must be replaced by its inverse plus a firing control" — which is what the dev did unprompted. Recording it so the next dispatch on a behaviour-adding round says so up front.
What landed in the PR
warnUnresolvedTopLevelFieldinsectionFields.ts, beside its siblingwarnOnMixedVocabulary— same voice, same once-per-occurrenceSetdedupe — wired into both read paths the ruling names:SimpleObjectForm's loop inObjectForm.tsxandbuildFlatFieldsinflatFields.ts.- ⭐ It warns and does not resolve. No
?? f.fieldfallback anywhere — the card's one explicit prohibition, honoured. - objectui#8847 measured, not assumed:
formis literally the sameObjectFormRenderer;object-master-detail-form's parentfields(no sections) falls through into the sameSimpleObjectFormbranch regardless offormType. Both inherit the warning for free — pinned by a newtopLevelFieldsWarnCoverage-8847.test.tsxthat rendersMasterDetailFormdirectly, rather than argued.
⚠️ A correction to #8847 came out of this, posted at5604654332: that card namesobject-master-detail-formas its second bare surface, but that entry already had a description. The genuinely bare second surface isembeddable-form(index.tsx:346). The dev had this right in its residue note while the card — and my own claim comment — had it wrong.Two operational notes
⚠️ FourthUNGATED_EXAMPLESre-key today. This PR movesObjectForm.tsx:123 → :124inscripts/check-doc-example-types.mjs, because one added import line shifted a ledgered@example. Four separate PRs have now paid that tax in one shift (#8807, #8828, #8845's check, this). ⇒ more evidence for objectui#8614, recorded there.⚠️ Collision managed, not discovered late. This PR also changes one line inapps/console/src/__tests__/registry-inputs-spec-parity.test.ts— stale prose insideMEMBER_PINS['object-form.fields']that said "no warning" and is now false. That file is the entire subject of objectui#8071 slice 4, dispatched in parallel. Slice 4's dev has been told: avoid theobject-form.fieldsentry, and if a conflict appears, resolve by keeping both — ⛔ never by taking one side wholesale, which is how a ceiling change silently disappears.⚠️ Dedup channel down, verified by this seat: REST/search/issuesis 403 by policy for these sessions ("sessions are bound to their configured repositories"), tested against two known-true controls. The dev also saw MCPsearch_issuesreturntotal_count: 0on known-true controls, so it handed its residue up rather than filing blind. ⛔ No duplicate filed.
Generated by Claude Code
Release:route 1 · branchclaude/issue-8738-fields-warn-route1· PR #8859 MERGED as8fda00905— verified on the tree, ⛔ not from an API field. Card CLOSED;pm:dispatchedretired, assignee cleared. Both routes are now landed.Verified by content, on
origin/mainpackages/plugin-form/src/sectionFields.ts warnUnresolvedTopLevelField 2 (declaration + export) packages/plugin-form/src/ObjectForm.tsx warnUnresolvedTopLevelField 3 packages/plugin-form/src/flatFields.ts warnUnresolvedTopLevelField 2 CONTROL — the sibling helper that already existed: packages/plugin-form/src/sectionFields.ts warnOnMixedVocabulary 5⇒ Wired into both read paths the ruling named, and the control proves the matcher works on that file rather than matching nothing.
⭐ The prohibition held — measured, not assumed
The one thing this card explicitly forbade was a lenient
?? f.fieldfallback at the top-level read point — the AGENTS.md #0.1 second dialect that would freeze "one word, two vocabularies" into the contract instead of removing it.ObjectForm.tsx `?? … .field` fallback matches: 0 flatFields.ts `?? … .field` fallback matches: 0⇒ It warns and does not resolve. A round told "route 1 is approved" is exactly where a lenient fallback would have slipped in as a kindness.
The card's two routes, closed out
route what PR 2 the registration description naming both vocabularies #8846 1 the named console.warnin both read paths#8859 ⇒ Route 2 serves whoever reads the authoring surface before writing; route 1 serves whoever already wrote it wrong. They are complements, which is why answering "is route 1 still needed once route 2 landed" with yes was correct.
What this closes beyond its own subject
objectui#8847's warning half is closed for free and by measurement, not by argument:
formis literally the sameObjectFormRenderer;object-master-detail-form's parentfields(no sections) falls into the sameSimpleObjectFormbranch regardless offormType;embeddable-formpassesconfig.fieldsstraight into<ObjectForm>. All four inherit the warning, and the master-detail path is pinned by a new test renderingMasterDetailFormdirectly.⇒ objectui#8847 released to
pm:queuewith a documentation-only remainder: one description string onembeddable-form(index.tsx:346).⚠️ And its card text names the wrong second surface — corrected at5604654332;object-master-detail-formwas never bare.Two things this round cost, recorded rather than absorbed
- ⛔ My dispatch instruction was wrong and the dev was right to deviate. I wrote "the probe's assertions must not move; if red, STOP". The probe's row 4 asserted
expect(warn).not.toHaveBeenCalled()— the assertion the ruling necessarily invalidates. Owned in full at5604660590. The correct rule for a behaviour-adding round is: no assertion may move except one the ruling itself makes false, and that one must be replaced by its inverse plus a firing control — which the dev did unprompted. ⚠️ FourthUNGATED_EXAMPLESre-key of the shift — one added import line pushedObjectForm.tsx:123→:124. Recorded on objectui#8614.
Generated by Claude Code
- ⛔ My dispatch instruction was wrong and the dev was right to deviate. I wrote "the probe's assertions must not move; if red, STOP". The probe's row 4 asserted
- added a commit that references this issue
on Sep 9, 2026 - added a commit that references this issue
on Sep 17, 2026
实测(
origin/main@2f881a90,objectui#8071 的成员 pin 工作中发现)object-form上有两个都叫fields的作者面,成员词汇不一样:sections[].fieldsFormFieldSchema对象(身份键field,canonical)、已构建的运行时FormField(身份键name)packages/plugin-form/src/sectionFields.ts的normalizeSectionField,以及ObjectForm.tsx里(f as any).field ?? f.name的 section 过滤fields{ name }对象被容忍ObjectForm.tsx的SimpleObjectForm循环:const name = typeof fieldName === 'string' ? fieldName : (fieldName as any).name;⇒ 顶层
fields里写 spec 的{ field: 'note', colSpan: 2 },name解析为undefined,if (!name) return;静默跳过 —— 无 throw、无console.warn、无空态提示,那个字段就是不见了。packages/plugin-form/src/flatFields.ts的buildFlatFields(drawer / modal 走的那条)是同一个形态。为什么它不是"作者写错了"
两边都没有任何东西会拒绝这个写法:
{ name: 'fields', type: 'array' },没有of、没有 description;ComponentPropsMap['object-form'].fields是z.array(z.unknown())(description 只有一句"Limit/order the fields shown");
tsc看不见。而同一个块上、同一个词的另一个面把这个对象当 canonical。一个读过
sections[].fields文档、把同样的条目挪到顶层
fields的作者(或生成这份元数据的 AI),得到的是静默丢弃。已有的失败探针
objectui#8071 的成员 pin 把两个方向都钉住了,并且用同一条目在 section 里确实渲染作为活对照,
所以这个负向不可能来自"这个 object schema 本来就不渲染那个字段":
packages/plugin-form/src/__tests__/objectFormFieldsMembers-8071.test.tsx第 4 与第 5 行。待裁
两条路,都动已发布面,所以只立卡:
fields遇到解析不出名字的成员时,发一条具名console.warn(与sectionFields.ts已有的warnOnMixedVocabulary同形),别静默。fields补一条 description 说明成员是裸字段名、以及它与sections[].fields的区别 —— 这是作者唯一会读到的地方。⛔ 不要做的:在顶层读点加一条
?? f.field的宽容回退。那正是 AGENTS.md #0.1 禁止的第二方言,它会把"一个词两套词汇"固化成契约,而不是把它消掉。
Refs: objectui#8071(发现处)· objectui#8068(成员 pin 判据)· objectui#3090(section field 的混合词汇 lint)