Skip to content

feat(data): signature and qrcode join the bounded-string family end to end - #12119

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-11875-signature-qrcode-bounded-text-family
Aug 25, 2026
Merged

feat(data): signature and qrcode join the bounded-string family end to end#12119
os-warren merged 1 commit into
mainfrom
claude/issue-11875-signature-qrcode-bounded-text-family

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#11875

Maintainer ruling 2026-08-25 (issue comment 5406510997), option 1: the write seam gains signature / qrcode's declared maxLength, 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. secret and color are 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

  1. Authoring (packages/spec/src/data/field.zod.ts) — both types join BOUNDED_STRING_FIELD_TYPES, so Field.signature({ maxLength: 64 }) — refused at the authoring seam since the spec: maxLength is authorable on every field type and validated as no more than a number — maxLength: 0 and maxLength: 12.5 parse 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 at field.zod.ts refuses maxLength outside 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. The maxLength.describe() and both authoring forms (field.form.ts, object.form.ts) move with the set in the same stroke.
  2. Write seam (packages/objectql/src/validation/record-validator.ts) — the max_length / min_length branch now reads the spec's BOUNDED_STRING_FIELD_TYPES instead of a hand-copied ten-type list (also discharging the follow-up the spec: maxLength is authorable on every field type and validated as no more than a number — maxLength: 0 and maxLength: 12.5 parse cleanly #11566 seat noted in its report: the validator and the schema read one constant now). A declared bound on signature / qrcode refuses over-long values with a field-named ADR-0112 max_length envelope. secret / color stay outside, pinned by a control test.
  3. Storage (packages/drivers/driver-sql/src/sql-driver.ts) — both types move from the catch-all's varchar(255) into the TEXT family, in createColumn and its varcharColumnChars mirror together (their agreement is pinned). Both driver-sql: richtext is emitted as varchar(255) while its markdown/html siblings 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 (createColumn region + mirror); the driver-sql: a declared field named id / created_at / updated_at is silently discarded by initObjects — declared type, length and constraints all ignored with no diagnostic #12015 band 8600–9130 is 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), reading information_schema.COLUMNS via a separate query, never the emitted DDL:

  • Post-change: body_sig / body_qr read back text (PG: data_type=text; MySQL: DATA_TYPE=text); a 1000-char data:image/png;base64,… value is accepted and round-trips byte-identically on both dialects. Control in the same session: the same value into c_color (still varchar(255)) is refused BY THE SERVER (22001 / ER_DATA_TOO_LONG) — the cell provably enforces widths.
  • Pre-change (ablation red leg, below): the same data-URI write is refused value too long for type character varying(255) (PG 22001) and Data 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/qrcode with maxLength: 64 lands varchar(64) in the physical catalog (PG character varying(64), MySQL varchar(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 posture code has 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 against maxLength: 64 refused with { code: 'max_length', constraint: { maxLength: 64, actual: 100 } }; exactly 64 accepted; 65 refused; enforced on update as well as insert; min_length travels with the branch; a bound-less signature/qrcode accepts an ordinary data-URI; secret/color carve-out controls stay unenforced.

The formerly "STILL-OPEN half" test cases are extended into the closed shape, not deletedsql-driver-11794-richtext-text-family.test.ts keeps the same measurement (same data-URI, same dialects) with the direction flipped, plus the new live keyed-and-bounded case.

Test evidence

  • spec: 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, PG Asia/Shanghai, MySQL +08:00): Test Files 139 passed (139) / Tests 2833 passed | 1 skipped (2834).
  • pnpm typecheck for the three packages: turbo Tasks: 14 successful (exit 0).
  • Repo-wide 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 == HEAD blob + clean porcelain; both consuming suites import their subject from source — record-validator.test.ts via ./record-validator.js, the driver test via ../src/index.js — so no dist leg applies)

  • Write seam: reverting the branch to the pre-change hand list (anchors: set-read 1→0, hand-list 0→1) turned exactly the predicted 6 red / 4 green in the new block — refusals vanish, acceptances stay.
  • Storage: dropping both case labels from createColumn + 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 5311cee259 by node 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 — including check-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:generated proved 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

minor for @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-existing signature/qrcode column stays varchar(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 for richtext.

Out of scope


Generated by Claude Code

…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
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-sql, @objectstack/objectql, @objectstack/spec, touching 6 documentable anchor(s).

28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 399ecad58033006635b69d645b4df1d2091c98e5.

1 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: 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.

Coarse fallback — 129 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 399ecad58033006635b69d645b4df1d2091c98e5packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9d8562895fb7a794b90143cc5928ed4000d04ad7 — the merge of head 5311cee259d8d7c7f615958cbd58b1fa1fb16c8f into base 399ecad58033006635b69d645b4df1d2091c98e5, 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 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

⚠️ 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 399ecad58033006635b69d645b4df1d2091c98e5 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-warren
os-warren marked this pull request as ready for review August 25, 2026 10:27
@os-warren
os-warren added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit a11c1a5Aug 25, 2026
33 checks passed
@os-warren
os-warren deleted the claude/issue-11875-signature-qrcode-bounded-text-family branch August 25, 2026 10:47
os-litant pushed a commit that referenced this pull request Aug 25, 2026
…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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:datasize/mteststooling

Projects

None yet

2 participants

@os-warren@claude