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
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
立卡门类别: ① 产品缺陷 — 具名落点 (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):
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"].
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 maind659c1d; 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).
{
"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."
]
}
Activity
objectstack-fleet commentedon Nov 2, 2026
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
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY
Claim: PM loop round 1 · 2026-11-02T08:31Z
Session:
session_01ADHkmQjPiXgNoSaEpqeGvY(seat); dev = card threadsession_01ADHkmRRicnRgy1gLFe46z7Account:
os-steveBranch:
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:serverSeat:
domain:server#1File 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/objectqlchangeset. ⛔ Nopackages/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: mediumClause-②: no (narrowing) (expected: an object or array written to a text-family field, accepted 201/200 on
main, is refusedinvalid_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, todayThread-read: 6572514209
Premise: holds (seat read on
maind659c1d; 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).
objectstack-fleet commentedon Nov 2, 2026
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.
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY
Landed · 2026-11-02T10:58Z
391216ff8c, through the merge queuecompletedbyFixes #29133pm:dispatchedis removed. Seat record: claim 6572633066, contract review PASS 6574387576, seat ACCEPT 6574427943.textcolumn hands back42, not"42"#29241 (numbers / booleans on text fields judged ≠ stored), records: a free-formselectfield (nooptions) admits a plain object or an array on create and stores it as-is, although a select's stored value is a scalar option value #29244 (free-formselectadmits a plain object; re-home the two 写入载荷里的算子对象:text型字段不做类型校验,{ title: { $in: [...] } }原样写进库(number型会响亮拒绝) #5922 pins). Both await triage.