Uh oh!
There was an error while loading. Please reload this page.
feat(data): signature and qrcode join the bounded-string family end to end - #12119
Conversation
…o end (#11875) Maintainer ruling 2026-08-25, option 1. Three seams move together: - spec: both types join BOUNDED_STRING_FIELD_TYPES, so maxLength/minLength become authorable on them; the out-of-set refusal message now enumerates the set itself instead of a hand-written copy (the #12017 two-copies shape), and both authoring forms show the key for the same set. - objectql: the record-validator's max_length/min_length branch reads the spec's set instead of a hand-copied ten-type list — a declared bound on signature/qrcode refuses over-long values with a field-named ADR-0112 envelope, boundary included (64 accepted, 65 refused at maxLength: 64). - driver-sql: both types move from the catch-all's varchar(255) into the TEXT family under exactly the #11794 invariant — TEXT is permitted because the write seam now enforces the declared bound. Measured on live MySQL 8.0.46 (STRICT_TRANS_TABLES) and Postgres 16: the 1000-char data-URI that was refused ER_DATA_TOO_LONG / 22001 lands as text per information_schema.COLUMNS and round-trips byte-identically; a keyed, bounded column is varchar(maxLength) with the server refusing exactly one char past the bound (#11374 semantics unchanged). secret and color are explicitly NOT covered (opaque sys_secret ref per ADR-0100; short by construction). Existing tables are untouched — the additive sync never rewrites a column's type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6HFzyH98W1YaQXhJUJt6o
📓 Docs Drift CheckThis PR changes 3 package(s): 28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 1 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 129 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 9d8562895fb7a794b90143cc5928ed4000d04ad7 && git checkout 9d8562895fb7a794b90143cc5928ed4000d04ad7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 399ecad58033006635b69d645b4df1d2091c98e5 5311cee259d8d7c7f615958cbd58b1fa1fb16c8f && git checkout -B drift-repro 399ecad58033006635b69d645b4df1d2091c98e5 && git merge --no-ff 5311cee259d8d7c7f615958cbd58b1fa1fb16c8f
node scripts/docs-audit/affected-docs.mjs --json 399ecad58033006635b69d645b4df1d2091c98e5
|
Uh oh!
There was an error while loading. Please reload this page.
…1949-minlength-defect-pair Semantic merge, both intents stacked: minLength rows/describe/superRefine message and tests move with the set exactly as #12119 moved maxLength's — forms at the twelve, message enumerates the set itself (the #12017 two-copies lesson), wrongTypes gains 'color', accept pin covers signature/qrcode. Generated artifacts deferred to the regeneration commit per the os-regen procedure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NDGG54XF5gbTLdQzCtnaVV
Fixes#11875
Maintainer ruling 2026-08-25 (issue comment 5406510997), option 1: the write seam gains
signature/qrcode's declaredmaxLength, and then both move into the TEXT family under exactly the invariant #11794 established — a TEXT column is permitted precisely because the seam enforces the declared bound.secretandcolorare explicitly NOT covered by the ruling and do not travel (#11875 records why). The minLength twin defect remains tracked in #11949, which is not addressed here; an interaction note was left there.The three seams, in order
packages/spec/src/data/field.zod.ts) — both types joinBOUNDED_STRING_FIELD_TYPES, soField.signature({ maxLength: 64 })— refused at the authoring seam since the spec:maxLengthis authorable on every field type and validated as no more than a number —maxLength: 0andmaxLength: 12.5parse cleanly #11566 tightening — now parses. The card's premise sentence ("maxLength is admitted on every field type") was stale, exactly as the claim comment warned: re-measured on this tree, the superRefine atfield.zod.tsrefusesmaxLengthoutside the set, so option 1 required the spec edit (clause ② path limb). The refusal message used to hand-enumerate the ten types in prose — a second copy of the list, the driver-sql's varchar-sizing type list and spec's BOUNDED_STRING_FIELD_TYPES must now agree, and nothing pins them — the same three-lists-disagree defect #11566 was filed for, one layer down #12017 two-copies family — and now enumerates the set itself, so it can never drift again. ThemaxLength.describe()and both authoring forms (field.form.ts,object.form.ts) move with the set in the same stroke.packages/objectql/src/validation/record-validator.ts) — themax_length/min_lengthbranch now reads the spec'sBOUNDED_STRING_FIELD_TYPESinstead of a hand-copied ten-type list (also discharging the follow-up the spec:maxLengthis authorable on every field type and validated as no more than a number —maxLength: 0andmaxLength: 12.5parse cleanly #11566 seat noted in its report: the validator and the schema read one constant now). A declared bound onsignature/qrcoderefuses over-long values with a field-named ADR-0112max_lengthenvelope.secret/colorstay outside, pinned by a control test.packages/drivers/driver-sql/src/sql-driver.ts) — both types move from the catch-all'svarchar(255)into the TEXT family, increateColumnand itsvarcharColumnCharsmirror together (their agreement is pinned). Both driver-sql:richtextis emitted as varchar(255) while itsmarkdown/htmlsiblings get TEXT — a rich-text body is capped at 255 characters #11794 comment blocks that recorded the deliberate hold-back are rewritten to record the closure. Diff stays inside the claimed band (createColumnregion + mirror); the driver-sql: a declared field namedid/created_at/updated_atis silently discarded by initObjects — declared type, length and constraints all ignored with no diagnostic #12015 band8600–9130is untouched.Measurements (all at head
5311cee259, the only commit)Live physical defect, re-measured both directions on Postgres 16 and MySQL 8.0.46 (
STRICT_TRANS_TABLES), readinginformation_schema.COLUMNSvia a separate query, never the emitted DDL:body_sig/body_qrread backtext(PG:data_type=text; MySQL:DATA_TYPE=text); a 1000-chardata:image/png;base64,…value is accepted and round-trips byte-identically on both dialects. Control in the same session: the same value intoc_color(stillvarchar(255)) is refused BY THE SERVER (22001/ER_DATA_TOO_LONG) — the cell provably enforces widths.value too long for type character varying(255)(PG 22001) andData too long for column 'body_qr'(MySQL ER_DATA_TOO_LONG) — the card's live defect reproduced on this tree, not recalled.#11374 keyed-and-bounded interaction, proved live on both dialects: a keyed
signature/qrcodewithmaxLength: 64landsvarchar(64)in the physical catalog (PGcharacter varying(64), MySQLvarchar(64)); the boundary value (64 chars) is accepted, one past it (65) is refused by the server. The declared bound's enforcement never diverges by shape: the write seam holds it in every corner (validator, boundary-pinned both directions), the index only decides whether the column ALSO enforces it — the same posturecodehas today. The four-corner mirror semantics (unkeyed/keyed × bounded/unbounded) are pinned for both new members.Write seam, boundary included, both directions (
record-validator.test.ts, +10 tests): 100-char value againstmaxLength: 64refused with{ code: 'max_length', constraint: { maxLength: 64, actual: 100 } }; exactly 64 accepted; 65 refused; enforced on update as well as insert;min_lengthtravels with the branch; a bound-less signature/qrcode accepts an ordinary data-URI;secret/colorcarve-out controls stay unenforced.The formerly "STILL-OPEN half" test cases are extended into the closed shape, not deleted —
sql-driver-11794-richtext-text-family.test.tskeeps the same measurement (same data-URI, same dialects) with the direction flipped, plus the new live keyed-and-bounded case.Test evidence
Test Files 425 passed (425)/Tests 11322 passed (11322); objectql:232 passed/4127 passed; driver-sql full suite with BOTH live servers attached under CI's skewed-zone configuration (TZ=America/New_York, PGAsia/Shanghai, MySQL+08:00):Test Files 139 passed (139)/Tests 2833 passed | 1 skipped (2834).pnpm typecheckfor the three packages: turboTasks: 14 successful(exit 0).pnpm lint(eslint . --no-inline-config): exit 0, full run, no narrowing.Ablations (both from the committed state, predictions written before the runs, mutations proved on disk by anchored grep counts, restores verified by
git hash-object==HEADblob + clean porcelain; both consuming suites import their subject from source —record-validator.test.tsvia./record-validator.js, the driver test via../src/index.js— so no dist leg applies)caselabels fromcreateColumn+ mirror (anchors:case 'signature':2→0,case 'qrcode':2→0, markers 2) turned 9 red / 2 green across SQLite + both live cells, per-test as predicted — including the SQLite round-trip staying green (SQLite enforces no varchar width, which is exactly why the live cells exist) and the live server refusing the data-URI with the card's original error codes. First take of this leg was VOID (the replacement swallowed the block-opening brace — transform error, readout discarded) and was redone with a brace-preserving mutation.Gates
Union derived at head
5311cee259bynode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(provenance line names this repo and commit; changeset derived by the script from the merge base, not hand-fed). All 40 matched families ran locally, every exit code captured pre-pipe, all 0 — includingcheck-adr-0087-registration("this PR adds no declared-breaking changeset"),check-changeset-no-major,check:driver-conformance("45 covered cell(s)"),check:i18n("9 package(s) — all bundles in sync"). Convention-triggered families for test-file and form-module edits also all 0:check:query-options-erasure,check:type-check-coverage,check:type-check-debt --re-measure(on the fully built closure),check:engine-double-contract,check:where-matcher,check:cross-package-test-inputs,check:nul-bytes("no raw ASCII control bytes"). Spec generated artifacts:check:generatedproved exactly one stale (gen:docs, the.describe()change) and regenerated only it; the four files PR #12003 fences (api-surface/api.json,authorable-surface/api.json,export-origins/api.json,json-schema.manifest/api.json) are untouched — verified by porcelain after regeneration.Changeset
minorfor@objectstack/spec,@objectstack/objectql,@objectstack/driver-sql— capability addition at the authoring seam plus behaviour change at write/storage seams, matching the tier the dispatch set; not declared-breaking (nothing an author could previously hold is removed: the newly-refused writes require a newly-declarable bound), so no ADR-0087 disposition marker is owed, confirmed by the gate's own verdict line. Existing tables are untouched: a pre-existingsignature/qrcodecolumn staysvarchar(255)(the additive sync never rewrites column types), so the data-URI refusal persists on old columns until an operator migrates them — same posture #11794 shipped forrichtext.Out of scope
textcanonical column's exact field-type set omitssignature/qrcode, which the driver now emits as TEXT #12117 (finding):type-compat.ts'stextcanonical exact set omitssignature/qrcode, which this PR makes the driver's own emitted shape for them.minLengthstill has the pre-#11566 defect pair — validated as no more than a number, authorable on every field type #11949 (minLength twin defect, pre-existing) remains open and untouched here; interaction note left on the card.Generated by Claude Code