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
form view overlay: a section field the object does not have publishes valid, the record form draws an editable box for it, and what is typed there is dropped on save with a whole-record PATCH #29021
Filing gate: ① a product defect, found beside the clauses (records-forms.form-view-gallery), reach: PUT /api/v1/meta/view and the console record form, measured twice.
Business reason: a form view that names a field the object does not have (a typo, or a field deleted later) publishes as valid, and the record form then shows an editable box for it. A user types into it and saves; the value is silently thrown away, and the save becomes a PATCH of every field of the record, which can overwrite a field another person changed meanwhile. The lint's own message says such a field "is silently skipped and never renders on the form".
Reader who acts: the triage seat, then a domain:ui implementer in ui/packages/plugin-form/src/sectionFields.ts, and the domain:server lane if FORM_FIELD_UNKNOWN should reach the door's _diagnostics. QA suggestion only: area:records, p2.
Duplicate search: fromObjectSchema → 2 hits (#26452, #9019, both closed and unrelated); form view unknown field section → 331 hits, the top ones QA run records or unrelated.
Batch-of: release-18.3.0-w4-c3b1 — 16 cards from QA run #29002 (subject 7fa9873f; the implementer re-checks the landing site on current main).
Reproduction
As admin: PUT /api/v1/meta/view/showcase_task.form?mode=draft with {name:'showcase_task.form', object:'showcase_task', viewKind:'form', isDefault:true, config:{type:'tabbed', data:{provider:'object',object:'showcase_task'}, sections:[{name:'overview',label:'Overview',columns:2,fields:['title','no_such_field']}, …]}} → 200; POST …/showcase_task.form/publish → 200; the read serves _diagnostics: {"valid":true}.
Open /_console/apps/com.example.showcase/showcase_task/record/ID/edit: the Overview tab shows a text box labelled "no_such_field".
Type "ghost" in it and Save.
Expected: no control for an unknown field (and the lint's warning on the door). Actual: the box is drawn; Save sends a PATCH carrying the whole record (title, project, assignee, status, priority, estimate, dates, location, notes…) and no no_such_field key → 200, and "ghost" is gone with no error. A direct PATCH {"no_such_field":"x"} → 400 INVALID_FIELD. Reproduced 2 times on fresh loads.
Mechanism (suspected)
ui/packages/plugin-form/src/sectionFields.tsfromObjectSchema: if (!field) return { name, label: fieldName, type: 'input' } disagrees with packages/lint/src/validate-form-layout.tsFORM_FIELD_UNKNOWN ("never renders").
QA-source: #29002 · records-forms.form-view-gallery · observed beside the clauses
Read fresh. fromObjectSchema in ui/packages/plugin-form/src/sectionFields.ts draws an input for an unknown field, which disagrees with FORM_FIELD_UNKNOWN in packages/lint/src/validate-form-layout.ts, so the primary fix is domain:ui; surfacing the lint in the door's _diagnostics would be a server follow-up. A dropped value plus a whole-record PATCH that can overwrite a concurrent edit is a user-visible defect, p2. It was seen beside the clauses, not as the P1 item's FAIL, so the release rule does not apply.
Claim: PM loop round 11 · 2026-11-02T08:01Z
Session: session_01QsR2jTfpYE3Paxw3g66yLu (card thread, os-dev); dispatched by seat session session_01QsR2jR7ZFkpqmm4U8S1f9y
Account: zhuangjianguo
Branch: claude/project-thread-jfw4ow (platform-assigned to the card thread; it can push only this branch)
Worktree: objectstack-issue-29021
Domain: domain:ui
Seat: domain:ui#4
File surface: ui/packages/plugin-form/src/sectionFields.ts (the unknown-field fallback), any plugin-form renderer that reads it if needed, their tests, and a changeset (stop on breach; explain in the report)
Container & model: S, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: default
Clause-②: no
Metadata-protocol: none
Responsibility: a form view naming a field the object lacks draws an editable box whose value is silently dropped on save | the lint already says such a field never renders | every user of a form view with a stale or mistyped field
Thread-read: 6571480228
Premise: holds (at origin/main, sectionFields.ts:294 returns { name, label: fieldName, type: 'input' } for an unknown field)
Serial constraints cleared: #29072 and #29019 build beside it on other files
Reported 2026-11-02T09:08Z for draft PR #29194 at head 49b88da9.
{
"issue": 29021,
"status": "done",
"branch": "claude/project-thread-jfw4ow",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29194",
"head": "49b88da9",
"session": "session_01QsR2jTfpYE3Paxw3g66yLu",
"premise_still_valid": true,
"summary": "fromObjectSchema (ui/packages/plugin-form/src/sectionFields.ts) now answers undefined when a resolved object schema does not declare the name, normalizeSectionField passes that through and buildSectionFields drops the entry, so the tabbed, wizard, drawer and modal arms draw nothing for an unknown section member, matching FORM_FIELD_UNKNOWN and the default arm's pool. customFields members and self-describing inline entries are still drawn; a form with NO object schema (no data source, host submitHandler owns the write) keeps the bare stub, since there is no object to judge against and its value reaches the host (submitTargetRefusal.test.tsx pins that). A section left with no known field builds no field and each arm keeps its existing empty-section behaviour (default arm: dropped whole, objectFormSectionMembers-8071 row 2). No designer relies on the fallback: Studio's object form designer renders flat top-level fields; the metadata-admin ViewPreview now shows what the record form shows.",
"tests": "new __tests__/unknownSectionField-29021.test.tsx: 4 arms x 2 rows (only the known field drawn; edit+save sends no key for either unknown name) 8/8 green; sectionFields.test.ts adds 5 rows (unknown name builds nothing in both named shapes, no-schema boundary keeps the stub, buildSectionFields drops unknown keeping order, all-unknown section is [], member and inline still drawn). Full plugin-form suite at 49b88da9 under the lock: 110/110 files 872/872 tests + 108/108 files 1547 passed 1 skipped. Ablation via ablation-replace.mjs deleting 'if (ctx.objectSchema != null) return undefined;' (anchor 1->0, blob d6b678291c7c->50ccedea3703): 7 failed / 37 passed of 44 (the 4 draw rows + 3 unit rows); the 4 save rows stay green under ablation because the stub value was already stripped on save. Restored blob == HEAD, git diff HEAD empty. No dist build involved: the tests import plugin-form from source.",
"gates": "dispatch-gates --commands derived 34 at 49b88da9; all 34 ran, all exit 0 (incl. check-sdui-manifest.mjs + --self-test, changeset gates with the PR body as GITHUB_EVENT_PATH, check:type-check-debt under the lock, pnpm --dir ui exec vitest run scripts/ 169 passed 2 skipped). --ran reconciliation: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. turbo type-check @object-ui/plugin-form 48/48. eslint on the 7 touched files exit 0 (0 errors). PR-body gates rerun on the final body: check-changeset-no-major, check-adr-0087-registration, check-closing-keyword-parity, check-partof-closing-keyword all exit 0. CI check-runs: in_progress at report time.",
"line_budget": "8 files, +284 -44 (328 changed lines) vs origin/main merge base; under 3000",
"files_changed": ["ui/packages/plugin-form/src/sectionFields.ts", "ui/packages/plugin-form/src/flatFields.ts", "ui/packages/plugin-form/src/__tests__/unknownSectionField-29021.test.tsx", "ui/packages/plugin-form/src/sectionFields.test.ts", "ui/packages/plugin-form/src/sectionFields.spec-parity.test.ts", "ui/packages/plugin-form/src/__tests__/selectMultipleGroupLabel.test.tsx", "ui/packages/plugin-form/src/sectionFields.blankGate-11262.test.ts", ".changeset/29021-unknown-section-field-draws-nothing.md"],
"deviations": ["flatFields.ts (plugin-form, the one other fromObjectSchema caller) changed to read the new undefined answer; inside the claim's 'any plugin-form renderer that reads its result' fence.", "Two existing tests used an undeclared 'ghost' field as a blank slate (sectionFields.spec-parity.test.ts now declares plain: {type:'number'}); selectMultipleGroupLabel's 'keeps its fallback type' row flipped to pin that no field is built.", "Dev showcase before/after: NOT MEASURED (needs a full workspace build + dev server); the record edit route renders through ObjectForm's tabbed arm, which the render pins drive.", "Session-context asked to assign the PR and request a review via GitHub MCP; skipped per COMMON RULES 3 and 10 (no MCP writes, no assignee or reviewer)."],
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "2 — mcp__github__issue_read get, mcp__github__issue_read get_comments (reads only)",
"api_writes": "2 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, run 41418137832, body read back identical 9227 bytes) and this os-dev-report comment via post-stamped; plus git push (not REST)",
"open_questions": [],
"out_of_scope_findings": [
"class: none (by design) · whole-record PATCH: measured in the plugin-form harness with the fallback restored, typing 'ghost' into the stub and saving sent {title, notes} (every drawn field) on all four arms; with the fix a save with nothing changed sends the same. Cause: dirtyEditPayload (ui/packages/plugin-form/src/sanitize.ts) strips the unknown key, the diff vs the loaded record is empty, and an empty diff deliberately sends the whole payload (documented there). The ghost box can no longer trigger it. carrier: seat — noted, not filed; save path unchanged per ruling",
"class: c · reach: the card's own measurement, PUT /api/v1/meta/view + publish then read serves _diagnostics {valid:true} for a section naming no_such_field · evidence: _diagnostics is the Zod verdict (packages/metadata-protocol/src/metadata-diagnostics.ts); validateFormLayout (FORM_FIELD_UNKNOWN) runs only in the authoring-rule registry (os validate / os lint), not at the door · dedupe: FORM_FIELD_UNKNOWN _diagnostics, validateFormLayout door, form view unknown field publish · carrier: domain:server (seat files)"
]
}
Seat review · 2026-11-02T09:36Z · PR #29194 at 49b88da9: ACCEPT (on the PR; Clause-②: no, no contract review owed). It goes to the merge queue now. Server-side follow-up: #29196.
Landed · 2026-11-02T10:46Z · PR #29194 merged through the merge queue as 67c2e3a6. Record: ACCEPT 6573809087 at 49b88da9 (Clause-②: no: the changed functions are internal to @object-ui/plugin-form, so no contract review is owed).
probe
before (67c2e3a6~1)
now (origin/main)
fromObjectSchema(…): FormField | undefined in ui/packages/plugin-form/src/sectionFields.ts
0
1
if (ctx.objectSchema != null) return undefined; in the same file (no stub control for a name the object lacks)
0
1
The card closed through Fixes. pm:dispatched comes off with this note. With no object schema at all (a form whose host submitHandler owns the write), the bare stub stays, as ruled.
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: records-forms.form-view-gallery
Triage:
domain:ui· p2 ·pm:queue· 2026-11-02T07:22Z⛔ Not a claim.
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y
Claim: PM loop round 11 · 2026-11-02T08:01Z
Session:
session_01QsR2jTfpYE3Paxw3g66yLu(card thread, os-dev); dispatched by seat sessionsession_01QsR2jR7ZFkpqmm4U8S1f9yAccount:
zhuangjianguoBranch:
claude/project-thread-jfw4ow(platform-assigned to the card thread; it can push only this branch)Worktree:
objectstack-issue-29021Domain:
domain:uiSeat:
domain:ui#4File surface:
ui/packages/plugin-form/src/sectionFields.ts(the unknown-field fallback), any plugin-form renderer that reads it if needed, their tests, and a changeset (stop on breach; explain in the report)Container & model: S,
mode:thread,model: default tier (dispatch-gates --tier: no path-derived mandate),effort: defaultClause-②: no
Metadata-protocol: none
Responsibility:
a form view naming a field the object lacks draws an editable box whose value is silently dropped on save | the lint already says such a field never renders | every user of a form view with a stale or mistyped fieldThread-read: 6571480228
Premise: holds (at
origin/main,sectionFields.ts:294returns{ name, label: fieldName, type: 'input' }for an unknown field)Serial constraints cleared: #29072 and #29019 build beside it on other files
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#4 · zhuangjianguo · session_01QsR2jTfpYE3Paxw3g66yLu
os-dev-report
Reported 2026-11-02T09:08Z for draft PR #29194 at head
49b88da9.{ "issue": 29021, "status": "done", "branch": "claude/project-thread-jfw4ow", "pr": "https://github.com/objectstack-ai/objectstack/pull/29194", "head": "49b88da9", "session": "session_01QsR2jTfpYE3Paxw3g66yLu", "premise_still_valid": true, "summary": "fromObjectSchema (ui/packages/plugin-form/src/sectionFields.ts) now answers undefined when a resolved object schema does not declare the name, normalizeSectionField passes that through and buildSectionFields drops the entry, so the tabbed, wizard, drawer and modal arms draw nothing for an unknown section member, matching FORM_FIELD_UNKNOWN and the default arm's pool. customFields members and self-describing inline entries are still drawn; a form with NO object schema (no data source, host submitHandler owns the write) keeps the bare stub, since there is no object to judge against and its value reaches the host (submitTargetRefusal.test.tsx pins that). A section left with no known field builds no field and each arm keeps its existing empty-section behaviour (default arm: dropped whole, objectFormSectionMembers-8071 row 2). No designer relies on the fallback: Studio's object form designer renders flat top-level fields; the metadata-admin ViewPreview now shows what the record form shows.", "tests": "new __tests__/unknownSectionField-29021.test.tsx: 4 arms x 2 rows (only the known field drawn; edit+save sends no key for either unknown name) 8/8 green; sectionFields.test.ts adds 5 rows (unknown name builds nothing in both named shapes, no-schema boundary keeps the stub, buildSectionFields drops unknown keeping order, all-unknown section is [], member and inline still drawn). Full plugin-form suite at 49b88da9 under the lock: 110/110 files 872/872 tests + 108/108 files 1547 passed 1 skipped. Ablation via ablation-replace.mjs deleting 'if (ctx.objectSchema != null) return undefined;' (anchor 1->0, blob d6b678291c7c->50ccedea3703): 7 failed / 37 passed of 44 (the 4 draw rows + 3 unit rows); the 4 save rows stay green under ablation because the stub value was already stripped on save. Restored blob == HEAD, git diff HEAD empty. No dist build involved: the tests import plugin-form from source.", "gates": "dispatch-gates --commands derived 34 at 49b88da9; all 34 ran, all exit 0 (incl. check-sdui-manifest.mjs + --self-test, changeset gates with the PR body as GITHUB_EVENT_PATH, check:type-check-debt under the lock, pnpm --dir ui exec vitest run scripts/ 169 passed 2 skipped). --ran reconciliation: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. turbo type-check @object-ui/plugin-form 48/48. eslint on the 7 touched files exit 0 (0 errors). PR-body gates rerun on the final body: check-changeset-no-major, check-adr-0087-registration, check-closing-keyword-parity, check-partof-closing-keyword all exit 0. CI check-runs: in_progress at report time.", "line_budget": "8 files, +284 -44 (328 changed lines) vs origin/main merge base; under 3000", "files_changed": ["ui/packages/plugin-form/src/sectionFields.ts", "ui/packages/plugin-form/src/flatFields.ts", "ui/packages/plugin-form/src/__tests__/unknownSectionField-29021.test.tsx", "ui/packages/plugin-form/src/sectionFields.test.ts", "ui/packages/plugin-form/src/sectionFields.spec-parity.test.ts", "ui/packages/plugin-form/src/__tests__/selectMultipleGroupLabel.test.tsx", "ui/packages/plugin-form/src/sectionFields.blankGate-11262.test.ts", ".changeset/29021-unknown-section-field-draws-nothing.md"], "deviations": ["flatFields.ts (plugin-form, the one other fromObjectSchema caller) changed to read the new undefined answer; inside the claim's 'any plugin-form renderer that reads its result' fence.", "Two existing tests used an undeclared 'ghost' field as a blank slate (sectionFields.spec-parity.test.ts now declares plain: {type:'number'}); selectMultipleGroupLabel's 'keeps its fallback type' row flipped to pin that no field is built.", "Dev showcase before/after: NOT MEASURED (needs a full workspace build + dev server); the record edit route renders through ObjectForm's tabbed arm, which the render pins drive.", "Session-context asked to assign the PR and request a review via GitHub MCP; skipped per COMMON RULES 3 and 10 (no MCP writes, no assignee or reviewer)."], "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "2 — mcp__github__issue_read get, mcp__github__issue_read get_comments (reads only)", "api_writes": "2 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, run 41418137832, body read back identical 9227 bytes) and this os-dev-report comment via post-stamped; plus git push (not REST)", "open_questions": [], "out_of_scope_findings": [ "class: none (by design) · whole-record PATCH: measured in the plugin-form harness with the fallback restored, typing 'ghost' into the stub and saving sent {title, notes} (every drawn field) on all four arms; with the fix a save with nothing changed sends the same. Cause: dirtyEditPayload (ui/packages/plugin-form/src/sanitize.ts) strips the unknown key, the diff vs the loaded record is empty, and an empty diff deliberately sends the whole payload (documented there). The ghost box can no longer trigger it. carrier: seat — noted, not filed; save path unchanged per ruling", "class: c · reach: the card's own measurement, PUT /api/v1/meta/view + publish then read serves _diagnostics {valid:true} for a section naming no_such_field · evidence: _diagnostics is the Zod verdict (packages/metadata-protocol/src/metadata-diagnostics.ts); validateFormLayout (FORM_FIELD_UNKNOWN) runs only in the authoring-rule registry (os validate / os lint), not at the door · dedupe: FORM_FIELD_UNKNOWN _diagnostics, validateFormLayout door, form view unknown field publish · carrier: domain:server (seat files)" ] }objectstack-fleet commentedon Nov 2, 2026
domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y
Seat review · 2026-11-02T09:36Z · PR #29194 at
49b88da9: ACCEPT (on the PR;Clause-②: no, no contract review owed). It goes to the merge queue now. Server-side follow-up: #29196.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y
Landed · 2026-11-02T10:46Z · PR #29194 merged through the merge queue as
67c2e3a6. Record: ACCEPT 6573809087 at49b88da9(Clause-②: no: the changed functions are internal to@object-ui/plugin-form, so no contract review is owed).67c2e3a6~1)origin/main)fromObjectSchema(…): FormField | undefinedinui/packages/plugin-form/src/sectionFields.tsif (ctx.objectSchema != null) return undefined;in the same file (no stub control for a name the object lacks)The card closed through
Fixes.pm:dispatchedcomes off with this note. With no object schema at all (a form whose hostsubmitHandlerowns the write), the bare stub stays, as ruled.Generated by Claude Code