Skip to content

fix(objectql)!: a string-typed field refuses a plain object or an array instead of storing its JSON text - #29223

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-0whijd
Nov 2, 2026
Merged

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #29133

Clause-②: no (narrowing) — measured: on base fea2dbb7 a plain object or an array on a text / textarea field was admitted by validateRecord on insert and on update; on head it is refused invalid_type on every member of BOUNDED_STRING_FIELD_TYPES, on insert, update and multi-update. Nothing is widened.
Metadata-protocol: none — measured: no schema, key, accepted metadata value or export changes; only a record write carrying a value of the wrong shape is refused.
Skills-docs: none — no skill or doc teaches writing an object or an array into a string field, and the refusal reuses the existing catalog sentence.

维护者速读

  • 改了什么:文本类字段(text、textarea、email、url 等 12 种)收到对象或数组时,现在直接拒收(invalid_type),不再把它转成 JSON 文本存进去。
  • 为什么改:之前创建、PATCH、屏幕流恢复时给文本字段传对象或数组都返回 201/200,库里存的是一段 JSON 文本,数据悄悄变脏。
  • 风险与回滚:是收窄,原来能写进去的错形状值现在会被拒;数字和布尔值仍按原样转成字符串,不受影响。回滚即 revert 本 PR。
  • 先看:packages/objectql/src/validation/record-validator.ts 字符串分支新增的那段;record-validator.text-shape.test.ts;operator-object-write-value.test.ts 的两处改动。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/validation/record-validator.text-shape.test.ts,预期 107 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

  • packages/objectql/src/validation/record-validator.ts, the BOUNDED_STRING_FIELD_TYPES arm: a plain object or an array is refused before any bound is judged, with the validator's existing invalid_type code and the existing ADR-0104 invalid_value_shape sentence (detail expected a string, received an object / an array). No new error code, no new message key, no packages/spec edit. It is a shape test, so it runs ahead of shapeOnly and a readonly field's value is judged the same way; the ReadonlyValueScope docblock lists it.
  • A filter-operator object keeps its own earlier sentence (the operator rule runs first). Numbers, booleans, null, a missing value, json and the structured-JSON types are unchanged.
  • Changeset .changeset/29133-text-field-refuses-object-array.md (@objectstack/objectql patch, BREAKING banner, ADR-0087 not-required (no-migration-prescription)).

Tests

  • New packages/objectql/src/validation/record-validator.text-shape.test.ts (107 cases): every member of the spec set × object / array / empty object / empty array × insert / update refuses with code + constraint.type + detail; the message names field, type and shape; the readonly scope refuses; the operator sentence still wins; strings, numbers, booleans, null and json still pass; the engine's insert, update and multi-update answer VALIDATION_FAILED and the driver is never written.
  • Forced test edit: operator-object-write-value.test.ts, two pins in "what the rule deliberately leaves alone" (admits an UNDECLARED $-spelling, admits a plain non-operator object) wrote { $inn: [...] } / { a: 1 } into the text field title and asserted admission. That is the coercion this card removes. Both pins move to the free-form select field stage_free already in that schema, so they keep asserting what they state (the operator rule is derived from the vocabulary, not from the $ sigil) without depending on the text arm.

Acceptance notes

  • A number or a boolean on a string field keeps its string reading, as the card's scope says. Not measured exhaustively across seeds and fixtures, so it was left alone rather than narrowed; every consumer suite below stayed green with it.
  • Out of scope, noted not filed: a free-form select with no options still admits a plain object (the pin moved above shows it); same class as this card, different arm.

Verification

All readings at head a50b8dce unless named.

Premise on base fea2dbb

A scratch validateRecord probe (deleted after): {"nested":true}, ["a","b"], 42, true on text + textarea — ADMIT on insert and update, all eight.

Suites
  • @objectstack/objectql test: Test Files 528 passed (528), Tests 10233 passed; typecheck exit 0 (lock VERDICT command-exit 0).
  • @objectstack/metadata-protocol test: Test Files 297 passed, 4 skipped (301); Tests 22062 passed, 26 skipped.
  • @objectstack/service-automation test (screen-flow resume): Test Files 257 passed (257); Tests 3309 passed.
  • @objectstack/dogfood (full battery, narrowing arm): Test Files 386 passed, 3 skipped (389); Tests 3195 passed, 28 skipped (3223); lock VERDICT command-exit 0, after building the @objectstack/dogfood^... closure. No failure.
  • eslint over packages/objectql (--no-inline-config): exit 0.
Ablation

scripts/ablation-replace.mjs replaced the guard if (isPlainObjectValue(value) || Array.isArray(value)) { with a never-true condition (anchor 1 → 0, replacement 0 → 1, blob 002f842a → 40e5174d). The new file ran red: Test Files 1 failed, Tests 100 failed | 7 passed (107) — every refusal case red with expected [] to have a length of 1 but got +0; the seven green are the leave-alone cases. Restore: blob == HEAD 002f842a, git diff HEAD empty; rerun 107/107 green. The suite imports the validator from src, so no dist leg; ablation-dist-preflight --absent confirmed the built dist never carried the marker.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at a50b8dce derived 61 families; all 61 run, each exit 0. --ran reconciliation: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN. Two first ran exit 3 on container prerequisites and were rerun green: check-plugin-teardown-shape --self-test (after git fetch --depth=1 of its fixture commit) and check:dual-build-cjs-loads (after the dogfood closure build). CI families: read from this PR's check-runs.


Generated by Claude Code

…ay as its value

The string arm of the record validator judged String(value) while the
write carried value, so an object or array on a text-family field passed
and the driver stored its JSON text. Refuse both with the existing
invalid_type code and the ADR-0104 invalid_value_shape sentence.

Claude-Session: https://claude.ai/code/session_01ADHkmRRicnRgy1gLFe46z7
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 3 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via invalid_type (literal, a string literal in validateOne))
  • content/docs/api/error-handling-server.mdx (via invalid_type (literal, a string literal in validateOne))
  • content/docs/automation/flows.mdx (via invalid_type (literal, a string literal in validateOne))
  • content/docs/deployment/cli.mdx (via invalid_type (literal, a string literal in validateOne))
  • content/docs/frontend/cli.mdx (via invalid_type (literal, a string literal in validateOne))
  • content/docs/protocol/objectui/concept.mdx (via invalid_type (literal, a string literal in validateOne))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via invalid_type (literal, a string literal in validateOne))
  • content/docs/releases/v17/17-6.mdx (via invalid_type (literal, a string literal in validateOne))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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 14208f2c504ff6f1529be8a28323a91df53d0562 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from bebbb726f13566c509d5cb45b4754137b6992d6d — the merge of head a50b8dce07a691b25c02837e77af204d0cc7105b into base 14208f2c504ff6f1529be8a28323a91df53d0562, 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 bebbb726f13566c509d5cb45b4754137b6992d6d && git checkout bebbb726f13566c509d5cb45b4754137b6992d6d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 14208f2c504ff6f1529be8a28323a91df53d0562 a50b8dce07a691b25c02837e77af204d0cc7105b && git checkout -B drift-repro 14208f2c504ff6f1529be8a28323a91df53d0562 && git merge --no-ff a50b8dce07a691b25c02837e77af204d0cc7105b

node scripts/docs-audit/affected-docs.mjs --json 14208f2c504ff6f1529be8a28323a91df53d0562

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 14208f2c504ff6f1529be8a28323a91df53d0562 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

Contract-review-claim: session_01ADHkmQjPiXgNoSaEpqeGvY · head a50b8dce07a691b25c02837e77af204d0cc7105b · 2026-11-02T09:48Z

The lane seat runs this PR's review at CONTRACT_REVIEW_TIER (declared Clause-②: no (narrowing)), in an isolated reviewer.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a50b8dce07a691b25c02837e77af204d0cc7105b
Local-runs: none
Scope: 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 + packages/spec/src/data/field.zod.ts (BOUNDED_STRING_FIELD_TYPES), packages/spec/src/data/field-value.zod.ts (MULTI_CAPABLE_TYPES, isMultiValueField), packages/spec/src/system/validation-message.ts (invalid_value_shape), packages/objectql/src/engine.ts (the insert / update / multi-update validator calls), packages/objectql/src/engine-validate.ts, packages/plugins/plugin-audit/src/audit-writers.ts, packages/plugins/plugin-audit/src/auth-event-audit.ts, packages/plugins/plugin-audit/src/action-invocation-audit.ts, packages/plugins/plugin-audit/src/key-material-copy-migration.ts, packages/services/service-automation/src/suspended-run-store.ts, packages/services/service-messaging/src/sql-http-outbox.ts, packages/services/service-storage/src/field-file-service.ts, packages/platform-objects/src/system/migration-flag.ts, packages/plugins/plugin-auth/src/organization-metadata-decode.ts, packages/plugins/plugin-auth/src/objectql-adapter.ts, ui/packages/fields/src/widgets/SignatureField.tsx, ui/packages/fields/src/widgets/QRCodeField.tsx, ui/packages/fields/src/widgets/RichTextField.tsx, .changeset/29133-text-field-refuses-object-array.md (prose face, read for the ② declaration lines)

Narrowing face (Clause-②: no (narrowing) at line start of the PR body and of the changeset): reviewFace returns no hits for these paths, so the Scope floor is every non-prose path of the diff, listed first above. PR author is objectstack-fleet[bot]; implementation claim names the branch.

① Derived judgments

  1. Create door narrows — correct. On main the BOUNDED_STRING_FIELD_TYPES arm (record-validator.ts, the string branch) judged s = String(value) against bounds / domain / format and never asked whether value was a string, so { nested: true } and ['a','b'] on any of the twelve types passed ("[object Object]" is 15 chars, "a,b" is 3) and reached the driver as sent. On head the arm returns fail('invalid_type', { type, detail: 'expected a string, received an object|an array' }, 'invalid_value_shape') before any bound. Reached by engine.insert (engine.ts: validateRecordInScope(…, 'insert', 'include')), the REST POST that calls it, a flow create_record, and the dry run / os validate seed walk (engine-validate.ts:548, cli seed-record-values). Pinned for all 12 × {object, array, {}, []} × {insert, update} in the new suite.
  2. Update and multi-update doors narrow — correct. engine.update first pass calls validateRecord(updateSchema, data, 'update') (engine.ts:17846 / 18264) for the single and multi: true branches alike; the new end-to-end case drives both through a recording driver and asserts the driver sees no write. The screen-flow resume (card step 3) lands through the same engine.update; the dev pinned the engine door rather than the HTTP route, which is the right seam since nothing between the route and the validator reads the value.
  3. Readonly scope narrows too — correct and consistent. The check is placed before if (shapeOnly) return null, so a readonly text-family field's value under the 'include' (insert final payload, dry run) and 'only' (update second pass, post-strip) scopes is now refused for a plain object or array where it was admitted. This is one accept-set change the card did not name; it is right by the ReadonlyValueScope contract ("the per-type arms that ask 'is this a value of the declared type at all'"), and the number, boolean, date and multi-value arms already refuse their wrong shape in shapeOnly. The docblock at record-validator.ts:672 is updated to list it.
  4. Nothing else changes — verified. A string, a number and a boolean still take their String(value) reading (pinned); null / undefined return at isMissing before the arm; json and the structured-JSON class never enter the arm; a filter-operator object is still refused one branch earlier with its own sentence (valueMayBeAnObject is false for every string type, so the $in rule runs first; pinned). No string type is in MULTI_CAPABLE_TYPES (select, radio, lookup, user, file, image) or MULTI_OPTION_TYPES, so there is no multiple: true text field whose array this refusal could take away. A Date or other class instance is not a plain object (isPlainObjectValue requires Object.prototype or a null prototype) and keeps its coercion — unchanged, noted for completeness.
  5. Wire surface unchanged — verified. Code invalid_type and message key invalid_value_shape ({{label}} has an invalid {{type}} value: {{detail}}) both exist on main in all four bundles (validation-message.ts:153/202/248/294); only the detail text is new. No packages/spec edit, no export change.
  6. Forced test edit — intent kept. The two #5922 "leaves alone" pins asserted that the operator rule is derived from the operator vocabulary, not the $ sigil, by writing { $inn: … } / { a: 1 } into title (text) and expecting admission. On head that admission no longer exists for text, so leaving them would make them fail for a reason unrelated to the operator rule. Moving them to stage_free (a select with no options, already in that schema, and the one scalar type the 写入载荷里的算子对象:text 型字段不做类型校验,{ title: { $in: [...] } } 原样写进库(number 型会响亮拒绝) #5922 table records as still ADMITTING a plain object) keeps the assertion about the operator rule intact. The comment explains the move and cites the new suite.

② Semver level

Clause-②: no (narrowing) is declared at line start in the PR body and the changeset, with the measurement. Metadata-protocol: none is right: no schema key, accepted metadata value, export or type moves; only a record write of the wrong shape is refused. Per AGENTS.md's Post-Task Checklist level table a non-protocol narrowing ships patch and keeps its BREAKING banner and ADR-0087 disposition: the changeset is '@objectstack/objectql': patch, carries the **BREAKING** banner with before / now / unchanged / remedy, and the adr-0087: not-required (no-migration-prescription) disposition marker, a category check-adr-0087-registration.mjs recognises. Check Changeset is green on this head. Consistent.

CI on a50b8dce: read from the head's check-runs API, not the PR body. Every completed run is success or skipped (path-filtered UI, Dogfood shards, docs); the Test Core shards and the Type Check jobs were still in_progress at the time of my first reads and this record was posted only after all 48 runs completed with none red — see the status line under VERDICT.

③ Boundary flags

  1. Open question — numbers and booleans on a string field: A (keep coercing) or B (narrow in a follow-up)? Answer: A for this PR, as the dev recommends and as the card scopes. But the dev's "nothing measured them as harmful" undersells the residual: the arm judges String(42) while the payload still carries 42, so on the InMemory driver a text column now holds a JS number — the same judged ≠ stored gap this card closes for objects, at a smaller scale (SQL drivers coerce on their own). That is a measurable defect, not a matter of taste, so it should be filed as a card (B) with a seeds / examples / fixtures writer grep as its first step, not left to "only if a card asks". Should-fix, not blocking: the card's acceptance is object / array and every suite stayed green with coercion in place.
  2. Out-of-scope finding — a free-form select with no options admits a plain object. Same class as this card, different arm, and the two moved pins now rest on exactly that admission, so whoever closes it must re-home them. Should be filed rather than "noted, not filed" (carrier: none). Should-fix.
  3. Deviation — premise measured with a validateRecord probe, not an HTTP POST. Accepted: the engine insert / update / multi-update door is pinned end to end and nothing on the REST or screen-flow path reinterprets a field value before the validator.
  4. Deviation — origin/main not merged (22 commits). Verified: git diff fea2dbb7..origin/main -- packages/objectql/src/validation/ is empty; no conflict with the validator.
  5. Author. PR user.login is objectstack-fleet[bot]; no second author to name.

④ Design judgment

Does the refusal hold against the code it cites? Yes. isPlainObjectValue (#27236) is the right predicate: it refuses {} and null-prototype records, and leaves Date, Buffer, class instances and null alone, so nothing a driver or hook legitimately hands the arm as a non-plain object is caught. Array.isArray covers the card's assignee: ["a","b"]. The order inside the arm is right: the #5922 operator rule runs first (so { $in: … } keeps its sentence and the ADR-0104 admitted-violation sink is never fed for a write about to be refused), then this shape test, then shapeOnly, then bounds / domain / format on a value now known to be a string or scalar. The reused catalog key is the one the operator rule and the ADR-0104 arm already use for "this value's SHAPE is wrong for this type", so no fifth near-duplicate sentence.

Who writes objects or arrays into text-family fields today? I enumerated every Field.text / Field.textarea / Field.code / Field.markdown column in platform-objects, plugins, services, runtime and objectql whose name or description says it holds structured content, and read its writer:

  • sys_audit_log.old_value / new_value / metadata, sys_activity.metadata — safeStringify(...) (audit-writers.ts:2138-2139, 2346; auth-event-audit.ts:386; action-invocation-audit.ts:197); the key-material migration re-serialises via rewriteSerialized → JSON.stringify.
  • sys_automation_run.steps_json / context_json / screen_json — JSON.stringify / serializeStepsBounded (suspended-run-store.ts:750, 994-996, 1049-1050).
  • sys_http_delivery.payload_json / response_body — JSON.stringify(input.payload) and a string body (sql-http-outbox.ts:194, 673).
  • sys_file.metadata / sys_upload_session.metadata — JSON.stringify(request.metadata) (field-file-service.ts:213).
  • sys_migration.details / deviation_detail, sys_migration_journal.detail — JSON.stringify (migration-flag.ts:202, 434; engine.ts:12780, 12882).
  • sys_organization.metadata, sys_oauth_application.metadata and the two oauth resource metadata columns — written by better-auth through the ObjectQL adapter; better-auth declares the field type: "string" and hands over JSON text, which is exactly why organization-metadata-decode.ts exists (decode on READ). The adapter does no stringification of its own and needs none.
  • sys_setting.value / sys_platform_setting.value are Field.json, outside the set; value_enc receives a ciphertext string.
  • Every error / last_error textarea (jobs, import, email, notification delivery, automation run) receives a .message ?? String(err) string.
  • UI: SignatureField emits a data-URL string, QRCodeField and RichTextField are string-typed FieldWidgetComponentProps widgets; no widget hands an object to a text-family field.
    No platform writer passes an object or array to a text-family field expecting stringification, so this is not a regression for shipped writers. Example seeds: the showcase assignee is Field.text, and the dev's os validate / os build gate families and the dogfood battery ran green on head (self-reported; the CI Dogfood shards were path-skipped on this diff).

Is the "ahead of shapeOnly" ordering right? Yes. shapeOnly is set only for def.readonly === true fields under the 'include' and 'only' scopes — payloads the docblock calls FINAL: the insert path after its strip, the dry run, and the update path's second pass after the readonly strip. The update first pass runs 'skip' precisely because a readonly value there may be a legacy stored value about to be dropped, so a whole-record write-back echoing a legacy object in a readonly text column is never judged. No read path calls the validator. A system writer that stamps an object into a readonly text-family column would now be refused on insert; I found none (see the writer list), and this is the same treatment the number, boolean, date and array arms already give a readonly value.

Does the narrowing take a declared capability away at the consumer, or make the spec's own refusal real? The latter: BOUNDED_STRING_FIELD_TYPES is the spec's "stored shape is a string" class (field.zod.ts:153), and nothing in the spec, a skill or a doc teaches writing an object or an array into one; the remedy in the changeset (JSON.stringify on the client, or declare the field json) is the correct one.

Implemented-by: claude/project-thread-0whijd
Reviewed-by: session_01ADHkmQjPiXgNoSaEpqeGvY

VERDICT: PASS

Should-fix (non-blocking, file as cards citing this record):

  1. Numbers and booleans on a text-family field: the arm judges their string reading while the payload still carries the scalar, so an InMemory text column can hold a JS number. Measure writers in seeds / examples / packages/apps / fixtures, then decide A (normalise to the string at the door, as record validator's number arm accepts any value whose Number() is finite, so POST /api/v1/data with a number field [500] answers 201 and driver-sql stores the text '[500]' #20309 does for numeric strings) or B (refuse).
  2. A free-form select with no options admits a plain object ({ a: 1 }); the two #5922 pins moved in this PR now rest on that admission and must be re-homed when it closes.

CI status on this head at post time: all 48 check-runs completed, none red (0 pending, 0 failed, head still a50b8dc).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

席位核对 · ACCEPT · domain:server seat 1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY · 2026-11-02T10:06Z

Head a50b8dce07, report 6574016397 (on #29133), claim 6572633066. Clause-②: no (narrowing) → contract review owed at CONTRACT_REVIEW_TIER: record 6574387576 on this head, PASS, no must-fix.

item reading
Shape Draft, base main, Fixes #29133. 4 files (+234 / −5): record-validator.ts BOUNDED_STRING_FIELD_TYPES arm, new record-validator.text-shape.test.ts (107 cases), two pins moved in operator-object-write-value.test.ts, @objectstack/objectql patch changeset with BREAKING banner and ADR-0087 not-required (no-migration-prescription). Metadata-protocol: none. No packages/spec/**, no new code or key (reuses invalid_type + invalid_value_shape). Not governed.
Rule A plain object or an array on any bounded string type is refused invalid_type on insert, update and multi-update, ahead of shapeOnly; the #5922 operator sentence runs first and keeps its words. Numbers, booleans, null, json and structured types unchanged.
Forced test edit The two #5922 "left alone" pins wrote objects into a text field; they move to the free-form select stage_free and keep asserting the vocabulary rule. Accepted (review ③/④).
Evidence Base fea2dbb7 probe: object / array / number / boolean admitted on text + textarea. Head: CI 48 check-runs completed, none red (read by the reviewer from the check-runs API); the reviewer's writer audit found no platform writer handing an object to a text-family column.
Should-fix Both review should-fix items are filed as cards citing 6574387576 (numbers/booleans judged ≠ stored on text fields; free-form select admits a plain object). Not this PR.
Landing 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY

Should-fix items of contract review 6574387576 filed as cards, each with a measured reach line on origin/main 45e9b2f1 (2026-11-02T10:13Z):

  1. Numbers / booleans on a text-family field judged on their string reading but stored raw → 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.
  2. Free-form select (no options) admits a plain object or an array; the two 写入载荷里的算子对象:text 型字段不做类型校验,{ title: { $in: [...] } } 原样写进库(number 型会响亮拒绝) #5922 pins moved here must be re-homed when it closes → records: a free-form select field (no options) 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.

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

Projects

None yet

1 participant