Skip to content

fix(objectql)!: a string-typed field refuses a Date, a Map, a Set, a class instance or a typed array (#29645) - #29684

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-pcnric
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-pcnric

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #29645

Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no file under skills/** or content/docs/** teaches writing a Date, Map, Set, class instance or typed array into a text-family field (git grep of both trees: zero hits), and the refusal reuses the existing catalog sentence.

维护者速读

  • 改了什么:文本类字段(text、textarea、email、url、phone 等 12 种)收到 Date、Map、Set、类实例或类型化数组时,现在一律以 invalid_type 拒收,与普通对象、数组已有的拒收同一句话;判定改用选项字段(records: an option-family field stores a number or boolean as sent (a single select, or an element of multiselect/checkboxes/tags), though its spec value is a string option code #29562)已在用的 isObjectValue,没有复制新函数。
  • 为什么改:原来的对象判定只认普通对象,其它对象按字符串读法放行:内存驱动把对象原样存下,SQLite 把 Date 存成纪元数字 "1767312000000.0",Map/Set 存成 "{}";email/url/phone 只是碰巧被格式校验拦下。
  • 风险与回滚:是收窄,只影响引擎写入口(流程、钩子、服务端代码);JSON 请求体带不出这些值,REST 调用方无变化。email/url/phone 上这类对象的错误码由格式码变为 invalid_type。旧行不改写。回滚即 revert 本 PR。
  • 先看:packages/objectql/src/validation/record-validator.ts 字符串分支 isObjectValue(value) 那一处;record-validator.text-object-value.test.ts;packages/rest/src/engine-sqlite-text-object-value.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/validation/record-validator.text-object-value.test.ts,预期 140 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

Why

Measured on origin/main @ 7dae3b3c (base) and on this branch (head 2e8ba408), through the engine's insert, update by id and update by predicate, on SQLite (the physical column, read with the driver's query builder) and driver-memory (the engine's read). Twelve types × five values × three doors × two drivers:

value base, 9 types (text, textarea, password, markdown, html, richtext, code, signature, qrcode): SQLite stored base, those 9: memory read base, email / url / phone head, all 12, both drivers, all 3 doors
new Date(...) "1767312000000.0" the Date itself invalid_email / invalid_url / invalid_phone invalid_type
new Map(...) "{}" the Map same format codes invalid_type
new Set(...) "{}" the Set same format codes invalid_type
a class instance { k: 1 } '{"k":1}' the instance same format codes invalid_type
new Uint8Array([1, 2]) '{"0":1,"1":2}' the typed array same format codes invalid_type
plain-string control stored as sent as sent as sent (a valid address, URL, phone) stored as sent

password reads back masked on both sides, so its stored column is the only answer there. The format types refused by accident: String(value) failed their regex. Head refuses the shape first on all twelve, so those three answer invalid_type instead of their format code.

Tests

  • New packages/objectql/src/validation/record-validator.text-object-value.test.ts (140 cases): the spec set's twelve members pinned by name; each type × Date / Map / Set / class instance / typed array × insert / update refuses with code + constraint.type + detail; a per-type plain-string control (a valid address, URL and phone on the three format types); the message names field, type and shape; the readonly scope refuses; a number and a boolean keep records: a number or a boolean written to a text-family field is admitted on its string reading but stored as the raw number/boolean, so a text column hands back 42, not "42" #29241 (written as their string, then judged); the door leaves an object as sent; a json field still holds a Date or a Map; engine insert, insertMany, update by id and update by predicate answer VALIDATION_FAILED and the driver is never written, with a string control that is.
  • New packages/rest/src/engine-sqlite-text-object-value.test.ts (6 cases): the same five values on a real ObjectQL + sqlite SqlDriver, all three doors refused, the physical row absent or unchanged; control: a string stored as sent and a number stored as "42".
  • No existing test changed.

Acceptance notes

  • Reach is the engine doors only (flows, hooks, server code), as the card says; REST over JSON cannot carry these values, so no REST pin is added.
  • The currency arm's isPlainObjectValue (sentence choice only, never the verdict) is untouched and is the helper's one remaining caller.
  • Out of scope, noted not filed: none found.

Verification

All readings on head 2e8ba408, base 7dae3b3c.

Suites
  • @objectstack/objectql test: Test Files 544 passed (544), Tests 10741 passed (10741); lock VERDICT command-exit 0.
  • @objectstack/rest test: Test Files 381 passed (381), Tests 7130 passed, 332 skipped (7462); lock VERDICT command-exit 0.
  • @objectstack/objectql and @objectstack/rest typecheck (both include check:test-typecheck): lock VERDICT command-exit 0.
  • @objectstack/dogfood (full battery, narrowing arm), after building the @objectstack/dogfood^... closure: Test Files 399 passed, 3 skipped (402); Tests 3297 passed, 28 skipped (3325); lock VERDICT command-exit 0. No failure.
  • pnpm lint (eslint . --no-inline-config): exit 0.
Ablation

scripts/ablation-replace.mjs (child mode, trap-armed) put the guard back to if (isPlainObjectValue(value) || Array.isArray(value)) {: anchor 1 → 0, replacement 0 → 1, blob 7b789eee → 61b1bb58. The objectql file (validator imported from src) ran red: Tests 123 failed, 17 passed (140); the 17 green are the controls and leave-alone cases. The objectql build was then run so the rest pin would read the mutation: its JS emitted and its DTS step failed (the mutated line hands unknown to receivedObjectShape), and ablation-dist-preflight found the marker in 4 built files. The rest pin ran red: Tests 5 failed, 1 passed (6), the green one being the control. Restore: blob == HEAD 7b789eee, git diff HEAD empty; rebuild exit 0, ablation-dist-preflight --absent clean on all 14 built files and the tree; the rest pin back to 6/6.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 2e8ba408 derived 60 families; all 60 ran, each exit 0. --ran reconciliation: 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN (pnpm lint recorded outside the derivation). Four first answered a container prerequisite and were rerun green: check-plugin-teardown-shape --self-test exit 3 (after git fetch --depth=1 of its fixture commit), check:dts-closure exit 1 (a dist mid-rebuild by the concurrent dogfood closure build; 225/225 declaration files on rerun), check:type-check-debt exit 3 (verify-lock queue timeout behind the dogfood run; rerun under the lock), check:dual-build-cjs-loads exit 3 (seven unbuilt packages; rerun after the full build). CI families: this PR's check-runs.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic (no packages/spec hunk).
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a, untouched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer brief is Chinese by the repo's rule).
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…class instance or a typed array (#29645)

The BOUNDED_STRING_FIELD_TYPES arm refused a plain object or an array,
but its object test (isPlainObjectValue) is prototype-strict, so any
other object passed on its string reading: driver-memory stored the
object itself, SQLite stored a Date as its epoch number and a Map or a
Set as "{}". The arm now asks the option arms' test (isObjectValue,
#29562), so every object is refused invalid_type with the same
value-shape sentence. A string, a number and a boolean keep #29241's
rule.

Claude-Session: https://claude.ai/code/session_01JQwxbWsjv939FeRpCY83MV
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 20 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 06881565bdced6c949f05d59f0059a3ff7409924 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 187705f6348f215b18c2a95a9f211265cc85c18f — the merge of head 2e8ba4089ac9fdde45d75931093217b2becd88cb into base 06881565bdced6c949f05d59f0059a3ff7409924, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 187705f6348f215b18c2a95a9f211265cc85c18f && git checkout 187705f6348f215b18c2a95a9f211265cc85c18f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 06881565bdced6c949f05d59f0059a3ff7409924 2e8ba4089ac9fdde45d75931093217b2becd88cb && git checkout -B drift-repro 06881565bdced6c949f05d59f0059a3ff7409924 && git merge --no-ff 2e8ba4089ac9fdde45d75931093217b2becd88cb

node scripts/docs-audit/affected-docs.mjs --json 06881565bdced6c949f05d59f0059a3ff7409924

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf

Contract-review-claim: session_01JQwxbXKRzgnVvjzxaYxLUf · head 2e8ba40 · 2026-11-03T01:19Z

Face: Clause-②: no (narrowing) ⇒ CONTRACT_REVIEW_TIER, scope = the diff outside prose (record-validator.ts and its two pins). Served by an independent at-tier subagent; the seat posts its record.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2e8ba4089ac9fdde45d75931093217b2becd88cb
Local-runs: none
Scope: packages/objectql/src/validation/record-validator.ts, packages/objectql/src/validation/record-validator.text-object-value.test.ts, packages/rest/src/engine-sqlite-text-object-value.test.ts + packages/spec/src/data/field-value.zod.ts (STRING_VALUE_TYPES, valueSchemaFor), packages/spec/src/data/field.zod.ts (BOUNDED_STRING_FIELD_TYPES, defaultValue), packages/objectql/src/engine.ts (insert / update doors, resolveNowDefault, normalizeExpressionDefault), packages/objectql/src/engine-validate.ts, packages/core/src/utils/import-coerce.ts, packages/rest/src/import-prepare.ts (xlsxCellToString), packages/core/src/utils/required-emptiness.ts

Inputs read: card #29645 (body, triage 6591873816, claim 6592073199, os-dev-report 6592841327), PR #29684 (body, 4-file list, net diff against main at the head), the head's 45 check-runs. The changeset .changeset/29645-text-field-refuses-any-object.md is read for ② only, not reviewed.

① Derived judgments

  1. Accept-set narrowing, one hunk. validateOne's BOUNDED_STRING_FIELD_TYPES arm replaces isPlainObjectValue(value) || Array.isArray(value) with isObjectValue(value) (the records: an option-family field stores a number or boolean as sent (a single select, or an element of multiselect/checkboxes/tags), though its spec value is a string option code #29562 helper, typeof value === 'object' && value !== null) and names the shape with the existing receivedObjectShape. Net effect on the twelve types (text, textarea, email, url, phone, password, markdown, html, richtext, code, signature, qrcode): a Date, Map, Set, class instance, typed array or any other non-plain object is now refused invalid_type / invalid_value_shape with expected a string, received an object; a plain object and an array keep the refusal they already had, with the identical code, message key and detail. Right — this is exactly the card's Wanted 2 (same helper, no copy), and receivedObjectShape gives the same an object / an array words the old inline ternary did.
  2. Ordering. The test sits where the old one sat: after the required / isMissing gate and the 写入载荷里的算子对象:text 型字段不做类型校验,{ title: { $in: [...] } } 原样写进库(number 型会响亮拒绝) #5922 filter-operator refusal, before if (shapeOnly) return null, so a readonly field's value is judged under validateRecordInScope(..., 'include') and a { $in: [...] } keeps its own sentence. Right; pinned by the readonly case and the "leaves alone" block.
  3. email / url / phone answer changes from invalid_email / invalid_url / invalid_phone to invalid_type for these shapes, because the shape test now runs ahead of the format regex. Right and declared in the changeset (the old code was an accident of String(value) failing the regex, as the live arm's own comment on 写入载荷里的算子对象:text 型字段不做类型校验,{ title: { $in: [...] } } 原样写进库(number 型会响亮拒绝) #5922 already says for the operator-object case).
  4. isPlainObjectValue keeps one caller (the currency arm's sentence choice at the live tree's line 1452) so no unused-helper lint arises; its docblock still describes that role. Right.
  5. normalizeNumericStringValues is untouched at the door; only its "not touched" docblock paragraph is reworded. A number or boolean keeps records: a number or a boolean written to a text-family field is admitted on its string reading but stored as the raw number/boolean, so a text column hands back 42, not "42" #29241 (written as its string, then judged); a Date is left as sent and refused by the arm. Right; pinned (toBe(row) identity and the '42' SQLite control).
  6. Public surface: no export, type, schema key or wire code added or removed; the existing invalid_type code and invalid_value_shape catalog key are reused, so no i18n bundle moves. normalizeBlankTypedRow (the objectql: a multi-value unique column (multiselect, or lookup with multiple: true) cleared to [] on two records refuses the second with 409 UNIQUE_VIOLATION #29343 serial constraint) is not in the diff. Right.
  7. Reach claim ("REST over JSON sees no change"): a JSON body cannot carry these shapes; the XLSX import flattens every cell to a string in xlsxCellToString (packages/rest/src/import-prepare.ts, a Date to its naive wall-clock text) before coerceFieldValue, and the CSV path is strings by construction. So the only doors that change are the engine's insert / insertMany / update by id / update by predicate / validate() dry run (the engine-validate.ts path runs the same validator), from flows, hooks and server code. Right; the pins cover insert, insertMany, both update forms and the readonly scope.
  8. Tests: the objectql pin reads the twelve-member set from @objectstack/spec/data and pins its membership by name, so a future member joins the loop and the name pin reds; the rest pin reads the physical SQLite column through the driver's query builder, the same (driver as any).knex convention rest-data-number-value.test.ts and rest-data-blank-typed-value.test.ts already use. Right.

② Semver level

Changeset .changeset/29645-text-field-refuses-any-object.md: '@objectstack/objectql': patch, title with !, Clause-②: no (narrowing), Metadata-protocol: none, one ADR-0087 marker not-required (no-migration-prescription), a BREAKING banner with Before / Now / Unchanged and the remedy. The diff publishes only @objectstack/objectql (the rest file is a test and publishes nothing). Metadata-protocol none is right: no schema key, accepted metadata value or type moves, and defaultValue literals were already bound by valueSchemaFor (z.string()), so no author-written metadata is newly refused. By AGENTS.md Post-Task Checklist 3 a none kind takes patch, keeping the BREAKING banner and the ADR-0087 disposition: matches. The PR body carries the same two declaration lines; Clause-②: no (narrowing) is the reading #29133's changeset used for the identical class of change and the card's Level line names. Right. The Check Changeset and PR-body guards check-runs on the head are success.

③ Boundary flags

  • open_questions: [] in os-dev-report 6592841327 — nothing to answer.
  • Deviations flagged by the dev: (a) branch name claude/project-thread-pcnric instead of claude/issue-29645-slug — a naming deviation of the card-thread mode, no contract effect; (b) no separate dogfood case although the claim's file surface named one — the recording-driver pin in objectql and the real-SqlDriver pin in rest cover every engine door the card names and the full dogfood battery is reported green, so the reach is pinned; answered, not a gap; (c) harness attribution comment and template headings in the PR body — prose, outside scope.
  • No dev flag names a design doubt. No security_findings, no out_of_scope_findings.
  • Ruling check: the card is a self-declared follow-up S4 of the records: an option-family field stores a number or boolean as sent (a single select, or an element of multiselect/checkboxes/tags), though its spec value is a string option code #29562 contract review; its Wanted 1-3 and Level line are all met by the diff (measurement table in the PR body and both pin headers; refusal via the shared helper; per-type pins plus a per-type plain-string control). No maintainer ruling on the card contradicts the diff.

④ Design judgment

The narrowing read against the published spec on main:

  • valueSchemaFor (packages/spec/src/data/field-value.zod.ts) returns z.string() for every member of STRING_VALUE_TYPES, which contains all twelve BOUNDED_STRING_FIELD_TYPES. The spec's stored value for this class is one string; a Date, Map, Set, class instance or typed array is already refused by the spec's own contract. The record validator admitting them on a String(value) reading was an implementation gap at the consumer, and the diff makes the spec's own refusal real: nothing declared is taken away.
  • I tried to find a declared producer that hands one of these shapes to a text-family field and would now be broken: a defaultValue literal must satisfy valueSchemaFor at parse time (field.zod.ts line 1386), so a Date literal cannot be declared; a CEL default that evaluates to a Date goes through normalizeExpressionDefault -> resolveNowDefault(fieldType, …), which returns the ISO string for any non-date/time type, so a now()-style default on a text field lands as a string, not a Date; the XLSX and CSV imports flatten cells to strings before coercion; REST JSON carries none. The one real reach is server code, hooks and flows (update_record spreads authored fields), where before the change SQLite stored a Date as its epoch number in TEXT and a Map or Set as "{}" — a corrupt stored value, never a capability. The remedy the changeset gives (toISOString(), JSON.stringify, or declare datetime / json) is the correct one.
  • Boundary cases probed: required + object — isEmptyForRequired (core) treats any present non-array value as non-empty, so the object reaches the arm and gets invalid_type, not required; null and a missing key still stop at isMissing; a filter-operator object keeps its 写入载荷里的算子对象:text 型字段不做类型校验,{ title: { $in: [...] } } 原样写进库(number 型会响亮拒绝) #5922 sentence; a json field stays open (z.unknown()), pinned. The currency arm's isPlainObjectValue call is a sentence choice and is rightly untouched.
  • Shape of the change: the string arm now shares one object predicate with the option arms (records: an option-family field stores a number or boolean as sent (a single select, or an element of multiselect/checkboxes/tags), though its spec value is a string option code #29562), so the two families that store a string cannot drift apart again; the typed value is object guard feeds receivedObjectShape(value: object) without a cast.

Findings: no must-fix. No should-fix inside the diff. Out of scope, noted not filed: color and secret are in STRING_VALUE_TYPES but outside BOUNDED_STRING_FIELD_TYPES, so the record validator has no string arm for them at all; the same Date-on-a-string-field question exists there and is neither this card's Wanted nor touched by this diff.

Gates: every check-run on the head has completed; 35 success (Test Core 1-8 and the aggregate, Build Core, all Type Check and Lint families, Test UI, Live E2E UI, Dogfood Regression Gate, Check Changeset, PR-body guards, Governed Surface Queue Guard, Check PR Size, Flag docs affected by code changes), the rest skipped by their path filters; zero failure, zero cancelled. Docs Drift Check lists no page (no hand-written page names the derived anchor); a git grep of content/docs for the text-family value contract finds no page that states a non-string value on a text field is stringified, so no prose is falsified.

Implemented-by: claude/project-thread-pcnric
Reviewed-by: session_01JQwxbXKRzgnVvjzxaYxLUf

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf

维护者速读

  • 改了什么:文本类字段(text、textarea、email、url、phone 等 12 种)收到非普通对象(Date、Map、Set、类实例、类型化数组)时,引擎写入回 invalid_type,不再按原样存储。
  • 为什么改:此前这些值走字符串分支被放行,SQLite 存成纪元数或 JSON 文本、内存驱动存对象;records: an option-family field stores a number or boolean as sent (a single select, or an element of multiselect/checkboxes/tags), though its spec value is a string option code #29562 已对选项字段做了同样的拒绝。
  • 风险与回滚:只拒绝服务端代码、流程与钩子写入的非普通对象;JSON REST 带不出这些值,字符串照旧。回滚即 revert。
  • 先看:packages/objectql/src/validation/record-validator.ts 字符串分支。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/validation/record-validator.text-object-value.test.ts,140 条全绿。
  • 需要你做的:无,席位落地。
  • 席位意见:达档契约复核 PASS(6593176137),无 must-fix,同意落地。

席位核对

ACCEPT · domain:server#2 · head 2e8ba4089ac9fdde45d75931093217b2becd88cb · 2026-11-03T01:41Z · contract review of record at CONTRACT_REVIEW_TIER (narrowing face): PASS 6593176137 on this head, by an independent at-tier subagent adopted by this seat

  1. Shape: draft, base main, Fixes #29645, Clause-②: no (narrowing), Metadata-protocol: none (body and changeset agree; @objectstack/objectql patch, BREAKING banner, one adr-0087 marker). 4 files, +321/-11, not governed (check-governed-merges --pr 29684 0 of 4). The one code hunk swaps the prototype-strict object test for records: an option-family field stores a number or boolean as sent (a single select, or an element of multiselect/checkboxes/tags), though its spec value is a string option code #29562's isObjectValue + receivedObjectShape (no copy); normalizeBlankTypedRow untouched.
  2. Premise measured: dev report 6592841327: on base 9 of 12 text types stored a non-plain object; on head all 360 cells refuse invalid_type, string controls land. Ablation red (123/140, 5/6) and restore proven.
  3. Gates on this head: check-runs pinned to 2e8ba408: 48 total, 35 success, 13 skipped, 0 failed. Dev: 60/60 derived gates exit 0, --ran 0 NOT-MEASURED, pnpm lint 0, dogfood battery VERDICT 0 on this head.
  4. Accepted deviations: no separate dogfood case (the claim named one); the engine-door pins over a recording driver and a real SqlDriver cover every door the card reaches.
  5. Out of scope: Acceptance note from the review: color and secret sit in the spec's STRING_VALUE_TYPES but outside BOUNDED_STRING_FIELD_TYPES, so the validator has no string arm for them; unmeasured, not this card's Wanted, not filed by this record. Skills-docs: not owed.
  6. Landing: not governed, under 3000 lines ⇒ the seat lands it (pr_ready + automerge_enable). At MERGED: landing record on validation(text): a non-plain object (Date, Map, class instance) on a text-family field passes the string arm and is stored as sent #29645 (closed by Fixes), pm:dispatched removed.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 3, 2026 01:42
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 3, 2026 01:42
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 85ea224 Nov 3, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-pcnric branch November 3, 2026 02:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

validation(text): a non-plain object (Date, Map, class instance) on a text-family field passes the string arm and is stored as sent

1 participant