Repository navigation
fix(objectql)!: a string-typed field refuses a plain object or an array instead of storing its JSON text - #29223
Conversation
…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>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 20 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY Contract-review-claim: The lane seat runs this PR's review at |
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY Contract reviewServed-tier: Narrowing face ( ① Derived judgments
② Semver level
CI on ③ Boundary flags
④ Design judgmentDoes the refusal hold against the code it cites? Yes. Who writes objects or arrays into text-family fields today? I enumerated every
Is the "ahead of Does the narrowing take a declared capability away at the consumer, or make the spec's own refusal real? The latter: Implemented-by: VERDICT: PASS Should-fix (non-blocking, file as cards citing this record):
CI status on this head at post time: all 48 check-runs completed, none red (0 pending, 0 failed, head still a50b8dc). |
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY 席位核对 · ACCEPT ·
|
| 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. |
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY Should-fix items of contract review 6574387576 filed as cards, each with a measured reach line on
|
Requested by Steve · project thread
Fixes #29133
Clause-②: no (narrowing) — measured: on base
fea2dbb7a plain object or an array on atext/textareafield was admitted byvalidateRecordon insert and on update; on head it is refusedinvalid_typeon every member ofBOUNDED_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.
维护者速读
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, theBOUNDED_STRING_FIELD_TYPESarm: a plain object or an array is refused before any bound is judged, with the validator's existinginvalid_typecode and the existing ADR-0104invalid_value_shapesentence (detailexpected a string, received an object/an array). No new error code, no new message key, nopackages/specedit. It is a shape test, so it runs ahead ofshapeOnlyand a readonly field's value is judged the same way; theReadonlyValueScopedocblock lists it.null, a missing value,jsonand the structured-JSON types are unchanged..changeset/29133-text-field-refuses-object-array.md(@objectstack/objectqlpatch, BREAKING banner, ADR-0087not-required (no-migration-prescription)).Tests
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 withcode+constraint.type+ detail; the message names field, type and shape; the readonly scope refuses; the operator sentence still wins; strings, numbers, booleans, null andjsonstill pass; the engine's insert, update and multi-update answerVALIDATION_FAILEDand the driver is never written.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 thetextfieldtitleand asserted admission. That is the coercion this card removes. Both pins move to the free-formselectfieldstage_freealready 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
selectwith nooptionsstill admits a plain object (the pin moved above shows it); same class as this card, different arm.Verification
All readings at head
a50b8dceunless named.Premise on base fea2dbb
A scratch
validateRecordprobe (deleted after):{"nested":true},["a","b"],42,trueontext+textarea— ADMIT on insert and update, all eight.Suites
@objectstack/objectqltest: Test Files 528 passed (528), Tests 10233 passed; typecheck exit 0 (lock VERDICT command-exit 0).@objectstack/metadata-protocoltest: Test Files 297 passed, 4 skipped (301); Tests 22062 passed, 26 skipped.@objectstack/service-automationtest (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.packages/objectql(--no-inline-config): exit 0.Ablation
scripts/ablation-replace.mjsreplaced the guardif (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 withexpected [] to have a length of 1 but got +0; the seven green are the leave-alone cases. Restore: blob == HEAD 002f842a,git diff HEADempty; rerun 107/107 green. The suite imports the validator fromsrc, so no dist leg;ablation-dist-preflight --absentconfirmed the built dist never carried the marker.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackata50b8dcederived 61 families; all 61 run, each exit 0.--ranreconciliation: 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(aftergit fetch --depth=1of its fixture commit) andcheck:dual-build-cjs-loads(after the dogfood closure build). CI families: read from this PR's check-runs.Generated by Claude Code