Skip to content

records: a text field accepts an object or an array on create, PATCH and screen-flow resume and stores its JSON text (201/200), although the type's stored shape is a string #29133

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbZx1yZXiTrFXKbSdFZ

立卡门类别: ① 产品缺陷 — 具名落点 (packages/objectql/src/validation/record-validator.ts, the BOUNDED_STRING_FIELD_TYPES branch) + 复现.
reach: 公开入口实测一次错误结果 — POST /api/v1/data/showcase_task with title: {"nested":true} answers 201 and stores the string {"nested":true} (stock showcase boot at 7fa9873, reproduced twice on two records).
业务理由: an integration or an AI-authored client that sends the wrong shape (a lookup object, a multi-select array) into a text field gets a success and a corrupted record: the title or assignee now reads as JSON text in every list, search and report, and nothing tells the caller. The number arm of the same validator refuses the wrong shape since #20309; the string arm does not. Not doing it: wrong-shaped writes keep landing silently and are found later by a person reading garbage.
动手的读者: the domain:server seat (objectql); triage first touch confirms the lane, then os-dev.
查重: text field accepts object stores JSON → 158 hits, record validator text arm → 266, string field object value 200 stored → 429, text field object value stored stringified → 11 (all incl. closed): none is this. Nearest: #20309 (closed, the number arm's twin). Not among the wave-4 cards.

QA-source: #29124 · automation.screen-flow-roundtrip · observed beside the clauses

What happens. The string branch of the record validator begins "A string type's stored shape is a string", then reads const s = typeof value === 'string' ? value : String(value) and checks only bounds, domain and format. A non-string value is never refused as invalid_type, and the driver stores its JSON text.

Reproduction (stock showcase, admin):

  1. POST /api/v1/data/showcase_task {"project":"‹project id›","title":{"nested":true},"assignee":["a","b"]} → 201. Re-read: title = string {"nested":true}, assignee = string ["a","b"].
  2. PATCH /api/v1/data/showcase_task/‹id› {"assignee":{"a":1}} → 200, stored {"a":1}.
  3. The same through a screen flow: resume showcase_reassign_wizard with {"inputs":{"new_assignee":{"a":1}}} → 200, the run completes and the task's assignee becomes {"a":1}. (The screen's text is a widget hint by design, spec: a flow screen field cannot express a numeric bound, help text, or a lookup target — three intents that degrade into prose in the reference app #17306; the gap is the record write.)
  • Expected: 400 VALIDATION_FAILED with an invalid_type entry on title / assignee, as the number arm answers for a non-number.
  • Actual: 201/200 and the JSON text is stored.

Full evidence: QA run record #29124.


Generated by Claude Code

Activity

  1. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:serverDomain 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.field-type-constraints

    Triage: domain:server · p2 · pm:queue · 2026-11-02T08:24Z

    ⛔ Not a claim.


    Generated by Claude Code

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

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

    Claim: PM loop round 1 · 2026-11-02T08:31Z
    Session: session_01ADHkmQjPiXgNoSaEpqeGvY (seat); dev = card thread session_01ADHkmRRicnRgy1gLFe46z7
    Account: os-steve
    Branch: claude/project-thread-0whijd (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own
    Domain: domain:server
    Seat: domain:server#1
    File surface: the string arm of the record validator in packages/objectql/src/validation/record-validator.ts (BOUNDED_STRING_FIELD_TYPES), its tests and a @objectstack/objectql changeset. ⛔ No packages/spec/**, no new error code, no new gate. (stop on breach; explain in the report)
    Container & model: S, mode:thread, model as the thread was opened, effort: medium
    Clause-②: no (narrowing) (expected: an object or array written to a text-family field, accepted 201/200 on main, is refused invalid_type; the dev measures)
    Metadata-protocol: none (expected)
    Responsibility: the code is the record validator of @objectstack/objectql · the platform path is a create/PATCH/screen-flow resume storing JSON text in a string field · reached by every API, integration or AI client that sends the wrong shape, today
    Thread-read: 6572514209
    Premise: holds (seat read on main d659c1d; the dev re-measures on base first)
    Serial constraints cleared: No open PR touches record-validator.ts (seat read over the open claude/ PRs, 08:26Z).

    Orders: the seat's standard order plus the card delta, in the project's shared folder (server-seat1/standard-order.md, server-seat1/29133-delta.md).

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-steve · session_01ADHkmRRicnRgy1gLFe46z7
    os-dev-report

    {
      "issue": 29133,
      "status": "done",
      "branch": "claude/project-thread-0whijd",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29223",
      "session": "session_01ADHkmRRicnRgy1gLFe46z7",
      "premise_still_valid": true,
      "summary": "The BOUNDED_STRING_FIELD_TYPES arm of the record validator now refuses a plain object or an array with the existing invalid_type code and the existing ADR-0104 invalid_value_shape sentence (detail 'expected a string, received an object|an array'), ahead of shapeOnly so readonly values are judged too. Premise re-measured on base fea2dbb7: object, array, number and boolean on text/textarea were all admitted on insert and update. Numbers and booleans keep their string reading (out of scope); null/undefined, json and the filter-operator sentence are unchanged. No packages/spec edit, no new code or key, no new gate.",
      "tests": "Head a50b8dce. New record-validator.text-shape.test.ts 107/107. @objectstack/objectql test: Test Files 528 passed (528), Tests 10233 passed; typecheck exit 0. @objectstack/metadata-protocol: Test Files 297 passed, 4 skipped (301), Tests 22062 passed, 26 skipped. @objectstack/service-automation: Test Files 257 passed (257), Tests 3309 passed. Ablation via ablation-replace.mjs (guard replaced by a never-true condition, anchor 1->0, blob 002f842a->40e5174d): Tests 100 failed | 7 passed (107), refusal cases red 'expected [] to have a length of 1 but got +0'; restored blob == HEAD, git diff HEAD empty; rerun 107/107 green. Suite imports src, dist preflight --absent clean. eslint packages/objectql --no-inline-config exit 0.",
      "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at a50b8dce: 61 derived, 61 run, all exit 0; --ran: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN. check-plugin-teardown-shape --self-test and check:dual-build-cjs-loads first exited 3 (container prerequisites) and were rerun green after the fixture fetch / closure build. check-changeset-no-major offline --event on the PR body: LEVEL AXIS ok, exit 0. CI check-runs: in_progress at report time.",
      "dogfood_battery": "a50b8dce · os-verify-lock VERDICT command-exit 0 · Test Files 386 passed | 3 skipped (389) · Tests 3195 passed | 28 skipped (3223) · no failures",
      "mcp_calls": "3 — mcp__github__issue_read get_comments x2, mcp__github__pull_request_read get x1; no write tools",
      "api_writes": "2 — fleet-write relay pr_create (PR #29223, run 41423240815); this os-dev-report comment via post-stamped.mjs. Plus git push of the branch (not a REST write).",
      "open_questions": [
        {
          "question": "Numbers and booleans on a text-family field keep coercing (String(42)). Narrow them too?",
          "options": [
            "A keep as is: the card scoped them out and no exhaustive grep of seeds/examples/hotcrm fixtures was done",
            "B refuse in a follow-up card after measuring writers in seeds, examples, packages/apps and fixtures"
          ],
          "recommendation": "A now, B only if a card asks: every consumer suite including the dogfood battery stayed green with them admitted, so nothing here measured them as harmful."
        }
      ],
      "out_of_scope_findings": [
        "class: a · reach: validateRecord on a select field with no options admits a plain object ({ a: 1 }) and the operator-object test pins that admission (now on stage_free) · evidence: operator-object-write-value.test.ts 'admits a plain non-operator object' · dedupe words: select no options plain object admitted, free-form select shape",
        "carrier: none · noted, not filed"
      ],
      "files_changed": [
        ".changeset/29133-text-field-refuses-object-array.md",
        "packages/objectql/src/validation/record-validator.ts",
        "packages/objectql/src/validation/record-validator.text-shape.test.ts",
        "packages/objectql/src/validation/operator-object-write-value.test.ts"
      ],
      "line_budget": "+234 / -5 over 4 files (record-validator.ts +24/-3, new test +180, operator test +7/-2, changeset +22)",
      "deviations": [
        "Premise measured with a validateRecord probe on base, not an HTTP POST against a booted showcase; the engine-level insert/update/multi-update door is pinned in the new test instead.",
        "First ablation attempt was aborted before its red run because the planted mutation overlapped the consumer closure build; restored (blob == HEAD), dist preflight --absent showed the marker never reached dist, then the ablation was rerun cleanly after the build finished.",
        "pnpm lint is repo-wide (no objectql lint script): ran eslint over packages/objectql with --no-inline-config instead, exit 0.",
        "Did not merge origin/main: 22 commits moved, none touches record-validator.ts."
      ]
    }

    Stamped 2026-11-02T09:46Z.

  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

    Landed · 2026-11-02T10:58Z

    Probe Reading
    PR #29223 MERGED 2026-11-02T10:58:29Z, merge commit 391216ff8c, through the merge queue
    #29133 closed completed by Fixes #29133

    pm:dispatched is removed. Seat record: claim 6572633066, contract review PASS 6574387576, seat ACCEPT 6574427943.

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