Repository navigation
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
Description
Activity
Triage: lands in
packages/drivers/driver-sql(a pin againstpackages/spec'sBOUNDED_STRING_FIELD_TYPES) ⇒domain:engine,pm:queue, type Task.Rationale: missing guard, not a live defect — the card's own measurement shows the two lists consistent by reasoning today; what is absent is anything that fails when they drift. This is the #11566 three-lists class one layer down. The cheap route is a driver-side pin test importing the spec constant and asserting the relationship (spec's set == union of the driver's two branches) — no cross-package runtime dependency, no fourth hand-maintained list. The card is right that
@objectstack/lintis probably not the home; do not assume otherwise without checking its contract.Dispatch constraints (engine seat):
- Serial: read PR fix(driver-sql): richtext and code take an unbounded TEXT column, restoring the declared Rich Content grouping #11876 first — it moves
richtext/codeinto the driver's text family and the guard's expected partition depends on where those two land. If fix(driver-sql): richtext and code take an unbounded TEXT column, restoring the declared Rich Content grouping #11876 is unmerged at claim time, serialize after it. - Size/model suggestion: S–M; sonnet-eligible if reduced to a pin test, opus if the dev argues for derivation instead.
Generated by Claude Code
- Serial: read PR fix(driver-sql): richtext and code take an unbounded TEXT column, restoring the declared Rich Content grouping #11876 first — it moves
- added a commit that references this issue
on Aug 25, 2026 ⚠️ Re-scope before dispatch — PR #12119 (#11875) has pre-empted a substantial part of this cardPosting this now so whoever picks it up does not measure the pre-#11875 tree. Serial-queued behind #12119, which is enqueued.
What this card said, and what is no longer true
The card's thesis is that nothing binds the driver's varchar-sizing type list to spec's
BOUNDED_STRING_FIELD_TYPES, and it names the #11566 family — "three hand-maintained lists disagreed". Two of the copies this implicates were removed by #12119, not merely updated:copy before #12119 after record-validator.ts'smax_lengthbrancha hand-copied ten-way ||chainreads BOUNDED_STRING_FIELD_TYPES.has(t)field.zod.ts's refusal messagethe ten types hand-written in prose derives them: [...BOUNDED_STRING_FIELD_TYPES].map(…)⇒ The authoring seam and the write seam now read one constant, and the prose copy is gone by construction rather than by being kept in sync. That was the larger half of the "lists disagree" problem, and it was discharged as a side effect of the ruled fix.
What actually remains — this is the card now
Only the driver half, which #12119 did not unify:
- spec's
BOUNDED_STRING_FIELD_TYPES(now twelve members —signatureandqrcodejoined) says which types may declare a bound. sql-driver.ts'svarcharColumnCharsswitch splits that into "gets a varchar" (declaredVarcharLength) vs "gets TEXT unless keyed" (keyableTextLength), and its membership is still hand-maintained.
The card's failure mode is unchanged and still real: spec adds a type to the set, the driver's switch does not learn about it, and the type falls to the catch-all
table.string(name)at knex's 255 — the #11431 defect re-entering through a different door. #12119 is itself the proof that this happens, since adding two members required a hand edit to the driver switch that nothing would have caught if it had been forgotten.Also newly stale in the card body
⚠️ "richtextandcodeare in spec's list but in neither driver branch today; PR fix(driver-sql): richtext and code take an unbounded TEXT column, restoring the declared Rich Content grouping #11876 (open, with triage) moves exactly those two" — fix(driver-sql): richtext and code take an unbounded TEXT column, restoring the declared Rich Content grouping #11876 landed. Both are in the text family now.- The three-row table in the card is a pre-
signature/qrcodehave nomaxLengthenforcement anywhere, so they cannot join the TEXT family — a data-URI signature is refused at 255 chars and the declared bound binds nothing #11875 reading.signatureandqrcodeare now in spec's set and in the driver's TEXT branch. ⛔ Re-measure the three lists onorigin/mainrather than carrying that table forward.
Route note
The card leaves open whether to derive the driver's switch from spec's constant or to pin the two against each other, and flags that
@objectstack/lint's contract forbids depending on a runtime so a lint rule may be the wrong home. That fork stands — but #12119 has now demonstrated the derive direction working twice in the same family (validator and refusal message both read the constant), which is evidence for it rather than against.⚠️ It is still evidence, not a ruling: the driver is a different package with a different dependency posture fromobjectql, and whetherdriver-sqlmay import the constant from@objectstack/specneeds checking rather than assuming.Staying
pm:queue. ⛔ Not dispatchable until #12119 lands — it owns thevarcharColumnCharsregion this card must read.
Generated by Claude Code
- spec's
- added a commit that references this issue
on Aug 25, 2026 Claim: PM loop round 2
Session:session_01LZbWd2jNV1FErXTPSS4Dry
Branch:claude/issue-12017-varchar-type-list-pin
Worktree:objectstack-issue-12017
Domain:domain:engine
File surface:packages/drivers/driver-sql/src/**(the guard and its test). ⛔packages/specis READ-ONLY here — it belongs to the spec seat; importing its constant is fine, editing it is a stop-and-report (stop on breach; explain in the report)
Container & model: M,mode:subagent,model: opus
Clause-②: no — a guard over an existing relationship; it changes no accept/reject behaviour and widens no public surface.⚠️ If the dev's chosen route turns out to change which types get a varchar, that IS clause ② and it must stop and report rather than proceed.
Serial constraints cleared:sql-driver.tsserial slot is free — this card takes it. ⛔ Queued siblings #12121 and #12593 are serial-queued behind it and must not be dispatched until this lands. No open PR touchessql-driver.ts. PR #12119 and PR #11876 both merged (verified below), so the region is quiet.⛔ The blocker on this card is CLEARED — measured, not assumed
Comment
5409198913says: "⛔ Not dispatchable until #12119 lands — it owns thevarcharColumnCharsregion this card must read."PR #12119 merged 2026-08-25T10:47:00Z. ⇒ Dispatchable.
🚨 The card body's central table is STALE. Do NOT carry it forward.
Re-measured by this seat on
origin/mainat claim time. These readings supersede both the card body and the first triage comment, which the second comment already warned about:spec —
BOUNDED_STRING_FIELD_TYPES, live atpackages/spec/src/data/field.zod.ts:134:export const BOUNDED_STRING_FIELD_TYPES: ReadonlySet<string> = new Set([⚠️ Locate it by symbol, ⛔ never by that line number. (My first grep for it returned empty — the pattern broke on the: ReadonlySet<string>annotation sitting between the name and the=. The zero was my instrument, not the tree. Control:FieldSchema= 14 hits in the same file.)driver —
varcharColumnChars's switch, measured in full:branch members → declaredVarcharLength(gets a varchar)string,email,url,phone,password→ keyableTextLength(TEXT unless keyed)text,textarea,html,markdown,richtext,code,signature,qrcode⇒ ⛔ "
richtextandcodeare in neither driver branch" is FALSE — #11876 landed them, and #12119 addedsignature/qrcode. The card's three-row table is a pre-#11875 reading.⭐ The asymmetry the guard has to decide about — measure it before writing the assertion
stringsits in the driver's varchar branch and is not a member of spec's set at all — it is the driver's untyped default (field?.type || 'string'), which is the knex builder name, not a specFieldType. #12131 landed this month on exactly that confusion.So the naive assertion "spec's set == the union of the driver's two branches" is wrong as stated, and writing it that way produces a guard that fails on day one for a reason that is not the defect. ⛔ Do not encode a relationship you have not measured. Derive the true relationship from the tree and state it in the PR body in the form the guard asserts.
The fork — genuinely open, ⛔ not decided here
Pin the two lists against each other (cheap, no cross-package runtime dependency) or derive the driver's switch from spec's constant.
Triage's read, and I agree it is evidence rather than a ruling: #12119 demonstrated the derive direction working twice in the same family (the write-seam validator and the refusal message both read the constant now).
⚠️ Butdriver-sqlis a different package with a different dependency posture fromobjectql— whether it may import from@objectstack/specneeds checking, not assuming. The card also flags that@objectstack/lint's contract forbids depending on a runtime, so a lint rule is probably the wrong home — ⛔ check that contract rather than inheriting the guess.Pick on measurement, and say why in the PR body.
Why this card exists at all — the failure mode, unchanged
spec adds a type to the set → the driver's hand-maintained switch does not learn about it → the type falls to the catch-all
table.string(name)at knex's 255. That is the #11431 defect re-entering through a different door, against a bound the platform now formally accepts.⭐ #12119 is itself the proof: adding two members required a hand edit to the driver switch, and nothing would have caught it if it had been forgotten.
⛔ No divergence exists today. This is a missing guard, not a live defect — do not describe it as one, and a green run proves nothing here. The ablation must show the guard failing when the lists are made to drift.
⛔ Confirm or refute every reading above on the current ref before writing code.
premise_still_valid: falsewith no PR is a legitimate delivery — this card has already had its premise move twice.
Generated by Claude Code
Dev claim (os-dev seat, dispatched by PM round 2)
Session:
session_01LZbWd2jNV1FErXTPSS4Dry
Branch:claude/issue-12017-varchar-type-list-pin(pushed empty at claim time as a write-route probe — push returned exit 0)
Worktree:objectstack-issue-12017
Base:15bf9e859e56862e6ebe7b5c42404de103362457(origin/main, "docs(rest): qualify the three residual ADR-0112 citations to the code axis (#12700)")Read: card body in full plus all three comments (
5405966222,5409198913,5439209193). The body arrived complete — no sanitizer truncation, and it carries no tag-shaped tokens. I am treating comment5439209193's measurements as the current reading and the card body's three-row table as superseded, and I will re-confirm every reading on this base before writing the assertion.File surface I claim:
packages/drivers/driver-sql/src/**(the guard and its test) plus a changeset.packages/specis READ-ONLY on this card — importing its constant is in scope, editing it is a stop-and-report.Route fork (pin vs derive) is open and mine to argue on evidence; I will state the relationship the guard actually asserts, in the form it asserts it, in the PR body. Assignee untouched.
Generated by Claude Code
Generated by Claude Code
- added a commit that references this issue
on Aug 27, 2026 os-dev-report
{ "issue": 12017, "status": "done", "branch": "claude/issue-12017-varchar-type-list-pin", "pr": "https://github.com/objectstack-ai/objectstack/pull/12714", "premise_still_valid": true, "clause2": "no — confirmed, not assumed. No runtime code changed; emitted DDL is byte-identical. The route chosen (pin) changes no accept/reject behaviour and widens no public surface. The DERIVE route would have pre-committed a column shape (TEXT) for every future spec member, which is one of the reasons it was not taken.", "files_changed": [ "packages/drivers/driver-sql/src/sql-driver-12017-bounded-string-spec-parity.test.ts (new, 280 lines)", ".changeset/driver-sql-bounded-string-spec-parity.md (new, patch @objectstack/driver-sql)" ], "spec_readonly_respected": "yes — zero bytes of packages/spec written at any point, ablations included. `git status --porcelain -- packages/spec` verified empty before and after ablation C, which mutates the imported Set IN MEMORY precisely to avoid editing that seat's files.", "relationship_asserted": "Over the spec's FieldType vocabulary: { t in FieldType.options : varcharColumnChars sizes t from the field's own maxLength (i.e. the union of its declaredVarcharLength and keyableTextLength branches) } === BOUNDED_STRING_FIELD_TYPES. Set EQUALITY, not containment — the superset direction is the card's failure mode (a declared bound the column ignores, falling to knex's varchar(255)), the subset direction is sizing code no authored metadata can reach plus, in the TEXT branch, an unbounded column no write seam bounds. Measured on this base both sides are the same 12 types. SCOPING: 'string' is in the driver's varchar branch and in NO spec set because it is not a FieldType at all — FieldType.options opens at 'text' and has no 'string' member (CONFIRMED, refuting nothing in the claim comment); it is the switch's untyped default `field?.type || 'string'`, knex's builder name. A second assertion pins it as that default (a field with no type at all gets the same answer), so the scoping is stated, not omitted.", "route_decision": "PIN, not derive. Both premises the card left open were CHECKED, not inherited: (1) driver-sql MAY import from @objectstack/spec — it already does at runtime as a production dependency, composing STRUCTURED_JSON_TYPES / FILE_REFERENCE_TYPES / MULTI_OPTION_TYPES into JSON_COLUMN_TYPES, whose comment says membership is owned by spec. So derivation is this file's own idiom; that removes the doubt but does not settle the fork. (2) @objectstack/lint IS the wrong home — its README states dependency direction lint -> spec one-way and 'It never depends on a runtime'; driver-sql is a runtime. DECIDING REASON: #12119's two derivations answer the SAME question spec's constant answers ('which types does the write seam bound?'); the driver asks a different one ('which physical column shape?') and BOUNDED_STRING_FIELD_TYPES carries no varchar/TEXT partition. Deriving would invent a default for every future member at the one seam the maintainer has actually ruled on per type (#11794 measured, #11875 ruled). The pin reds when the decision is needed, and also holds the subset direction a derivation cannot.", "summary": "Adds the missing guard binding driver-sql's varchar-sizing type switch to spec's BOUNDED_STRING_FIELD_TYPES — one new test file plus a changeset, no runtime change. The guard identifies each type's branch by PROBING the driver's own dispatch (three probes at a declared bound of 700 separate four outcomes) rather than restating any case list, so it adds no fourth hand-maintained list — which the existing #11794 pin could not avoid (it spells out 36 type names). Card body's three-row table confirmed STALE as the second comment warned; the claim comment's re-measurements were confirmed in full, including the 'string' asymmetry. The card's premise (a missing guard, no live divergence) HOLDS.", "tests": "All at c7ff8127, the head of the branch; every exit code captured BEFORE any pipe (redirect-then-capture), and verdicts quoted are the gates' own printed lines. Dependency closure built first via the shared lock: `pnpm --filter '@objectstack/driver-sql^...' build` -> os-verify-lock VERDICT command-exit 0. GREEN: 3 test files (new guard + sql-driver-11565 + sql-driver-11794) = 12 passed, 3 skipped (skips are opt-in live MySQL/PG cells). tsc --noEmit exit 0 AND PROVEN to cover the new file — package tsconfig has no test exclusion and --listFiles shows the new test among 555 program files (measured, since a typecheck excluding it would be a true statement about nothing). ABLATIONS (direction and exact count predicted in writing BEFORE each mutation; every mutation confirmed on disk by counting BOTH removed and injected text; every restore proven byte-identical via git hash-object vs git rev-parse HEAD:path; each script carries trap ... EXIT INT TERM with absolute paths). No rebuild was needed and this is measured not assumed: the subject is imported by relative source specifier and this package's vitest config aliases @objectstack/spec/* to spec's SOURCE, so no dist sits between mutation and runner. A: rename `case 'qrcode'` out of BOTH switches — predicted RED 2 of 3, OBSERVED exactly that (`- \"qrcode\"` from the set equality; `+ \"qrcode: declared 700, landed varchar(255)\"` from the physical test); control #11565 GREEN (both its sides moved together); #11794 red as predicted, which is why A is not decisive. B: add `case 'secret'` to the varchar branch (a type spec carves out, ADR-0100) — predicted RED 1 of 3, OBSERVED exactly that (`+ \"secret\"`); controls #11565 AND #11794 both GREEN, so this direction is caught by the new guard alone. C (DECISIVE — the card's own failure mode): spec admits a type the driver never learns, created IN MEMORY via BOUNDED_STRING_FIELD_TYPES.add('color') from a temp setup file so packages/spec stayed untouched; setup logged setSize=13 hasColor=true so the drift really existed in-process — predicted RED 2 of 3, OBSERVED exactly that (`- \"color\"` and `+ \"color: declared 700, landed varchar(255)\"`); controls #11565 AND #11794 both GREEN, so a spec-side addition is invisible to both existing pins and visible only to this one.", "gates": { "derived_by": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (script read its own change set from merge base 15bf9e859; 2 paths; asserted repo matched)", "green": [ "check:cross-package-test-inputs — 'OK: 20 package(s) read outside themselves, all declared'", "check:test-source-alias — '72 packages with tests scanned'", "check:engine-double-contract", "check:where-matcher — '307 matcher(s) discovered ... none new'", "check:query-options-erasure", "check:objectql-double-limit", "check:published-files", "check:type-source-resolution", "check:driver-conformance — 'OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt'", "check:nul-bytes — 'scanned 7057 text file(s) ... no raw ASCII control bytes'", "check:page-declaration-shape", "check:slot-lookup", "check:comment-mask-adoption", "check:plugin-teardown-shape", "check-ci-filter-parity", "check-empty-changeset — '1 declaring changeset(s) added'", "check-changeset-no-major", "check-adr-0087-registration", "check:changeset-gate-self-tests", "check:objectui-changeset", "check:pm-half-states", "check:type-check-coverage (structural half — new test file sits inside an existing tsc program, proven by --listFiles)", "tsc --noEmit (@objectstack/driver-sql)" ], "not_measured": [ "check:type-check-debt — its ratchet half needs the WHOLE workspace closure built and refuses outright otherwise. NOT MEASURED, not green. Named rather than counted." ], "red": [] }, "deviations": [ "DECLARED NARROWING: the full @objectstack/driver-sql suite was not run — only the new file and the two neighbouring pins it composes with. No source file changed in this PR, so no existing test's subject moved; CI runs the suite in full regardless.", "DECLARED NARROWING: check:type-check-debt not run (see gates.not_measured) — a refusal-to-measure reported as NOT MEASURED rather than as green.", "NOT MEASURED, recorded so it is not mistaken for a result: the first attempt at ablation C died with `CACError: Unknown option --setupFiles` (vitest 4 has no such CLI flag). That exit 1 never reached the guard body. Re-run through a temporary config extending the package's own; only the re-run is reported.", "CHANNEL SWITCH declared: repo-scoped REST reads and writes work from this seat (issue read 200, PR create 201), but the REST SEARCH endpoint returns 403. The one duplicate check therefore went through a single targeted MCP search_issues, per the sanctioned fallback. It returned a hit, so no control term was needed.", "PR was opened via authenticated REST rather than MCP, which is why mcp_calls is 3." ], "ablations": [ {"id": "A", "mutation": "rename `case 'qrcode'` out of both varcharColumnChars and createColumn", "predicted": "RED, 2 of 3 (set equality + physical); #11565 GREEN", "observed": "RED, 2 of 3; #11565 GREEN; #11794 RED as predicted", "match": true, "restore_proven": "byte-identical, blob 895550f51200488aa86752a304b4888a538460ea"}, {"id": "B", "mutation": "add `case 'secret'` to the varchar branch (subset direction)", "predicted": "RED, 1 of 3; #11565 and #11794 both GREEN", "observed": "RED, 1 of 3 (`+ \"secret\"`); both controls GREEN", "match": true, "restore_proven": "byte-identical, blob 895550f51200488aa86752a304b4888a538460ea"}, {"id": "C", "mutation": "spec-side addition IN MEMORY — BOUNDED_STRING_FIELD_TYPES.add('color'); zero bytes written to packages/spec", "predicted": "RED, 2 of 3; #11565 and #11794 both GREEN", "observed": "RED, 2 of 3; both controls GREEN; setup logged setSize=13 hasColor=true", "match": true, "restore_proven": "scaffold removed, git status --porcelain empty"} ], "mcp_calls": "3 — issue_read(get), add_issue_comment(claim), search_issues(one targeted duplicate check after REST search returned 403). Card comments, PR creation, PR body read-back and this report all went through authenticated REST or the zero-quota unauthenticated read.", "open_questions": [], "out_of_scope_findings": [ "NOT filed — already open as #12514: '[finding] 9 packages/spec gate families declare path populations the tree no longer has — a quiet green on every card, printed but never recorded' (labels tooling, pm:queue, domain:devx). My dispatch-gates run reproduced it verbatim. Confirmed duplicate via the targeted MCP search above; not re-filed." ] }
Generated by Claude Code
ACCEPT — PR #12714
Reviewer of record:
domain:engineseat, sessionsession_01LZbWd2jNV1FErXTPSS4Dry(R2). Verified against the diff, ⛔ not against the report.Checklist
Item Reading Scope 2 files: one new test (279 lines) + one changeset. ⛔ Zero runtime code changed — so clause2: nois verifiable from the file list, not merely asserted.packages/spec⛔ Untouched ✅ — absent from the changed-file list. The fence held even through the ablations: ablation C mutated the imported Set in memory rather than editing that seat's file. Governed Not governed. Governed Surface Queue Guardto confirm on the final head.Changeset patch, argued: notminor(no API, no accept-set change, DDL byte-identical); ⛔ notskip-changeseteither, and the reason given is the right one — the package's published contract becomes a checked invariant, and the CHANGELOG line is what a future reader needs when the guard goes red.CI ⏳ running, zero red so far. Enqueue gated on every check green. ⭐ The route argument refutes my own lean, and it is correct
My claim comment passed on triage's evidence that #12119 demonstrated derive working twice in the same family. The dev checked both open premises instead of inheriting either, and then showed why that evidence does not transfer:
driver-sqlmay import from@objectstack/spec— it already does at runtime, composingSTRUCTURED_JSON_TYPES/FILE_REFERENCE_TYPES/MULTI_OPTION_TYPESintoJSON_COLUMN_TYPES. ⇒ the dependency doubt is dead, but that only removes an obstacle; it does not decide the fork.@objectstack/lintis the wrong home — its README states the direction is one-waylint → specand that it "never depends on a runtime";driver-sqlis a runtime. ⇒ the card's hunch confirmed by reading the contract, as instructed.- The deciding reason: feat(data): signature and qrcode join the bounded-string family end to end #12119's two derivations answer the same question the spec constant answers — "which types does the write seam bound?" The driver asks a different one — "which physical column shape?" — and
BOUNDED_STRING_FIELD_TYPEScarries no varchar/TEXT partition at all. Deriving would therefore have to invent a default (TEXT) for every future member, at the one seam where that choice has consequences the maintainer has ruled on per type (driver-sql:richtextis emitted as varchar(255) while itsmarkdown/htmlsiblings get TEXT — a rich-text body is capped at 255 characters #11794 measured it,signature/qrcodehave nomaxLengthenforcement anywhere, so they cannot join the TEXT family — a data-URI signature is refused at 255 chars and the declared bound binds nothing #11875 ruled it).
⇒ A pin fails loudly at the moment the decision is needed instead of silently supplying an unchosen answer — and, unlike a derivation, it also holds the
⊆direction. ✅ Accepted, and this is the better answer.The assertion is the one I asked for, and the asymmetry is pinned rather than omitted
{ t ∈ FieldType.options : the switch sizes t from the field's own maxLength } === BOUNDED_STRING_FIELD_TYPESSet equality, both directions load-bearing:
⊇is the card's failure mode (a declared bound the column ignores);⊆is sizing code no authored metadata can reach — and in the TEXT branch, an unbounded column no write seam bounds, which is the exact invariant #11794 admits text-family members under.⭐ The
stringtrap I flagged was confirmed and scoped, ⛔ not omitted.FieldType.optionshas no'string'member — and the test asserts that (expect(FieldType.options).not.toContain('string')), turning the scoping into a measurement rather than a convenient boundary. A second pin holds'string'as the untyped default by showing a field with no type at all gets the same answer.⭐ No fourth hand-maintained list — the thing that makes this pin better than its neighbours
Branch membership is read off the driver's own dispatch, not restated: three probes at a declared 700 separate four outcomes from the
(unkeyed, keyed)answer pair. The insight is that the keyed probe is what makestext-familydistinguishable fromnot-sized-from-metadata— unkeyed, both answernull. That conflation is precisely why the existing #11794 pin needed a 36-name hand list, and avoiding it is what lets this file carry none. ⇒ The guard does not become the next copy of the thing it guards.PROBE_CHARS = 700is chosen from the driver's own constants and the reasoning is stated: wider than 255 so the catch-all is distinguishable from an honoured declaration, and inside bothMAX_KEYABLE_VARCHAR_CHARS(768) andMAX_VARCHAR_CHARSso no ceiling nulls the answer and mis-classifies a type.⛔ An unclassifiable answer throws rather than falling into a bucket — "silently filing it under 'not sized' would be this card's own defect committed by its own guard." Exactly right.
Non-vacuity — the part that makes a green run mean anything
This card's whole hazard is that a green run proves nothing (the lists agree today). The file answers that in-run, three ways: the spec set is real; both sizing branches resolved; and the catch-all is reachable — the last being the positive control for the failure mode itself, since a classifier that could never say
default-varchar-255could never report the drift. Plus a live negative control (os12017_not_a_field_type→ 255) and, in the physical test,color— the #11875 carve-out — producing the defect's signature in that very table (declared 700, landed varchar(255)).The physical test asserts the invariant, not the partition — deliberately, so it does not copy the driver's partition and re-create the hand list. Read from
columnInfo()(the PRAGMA), ⛔ not the emitter.Ablations — three, and C is the decisive one
mutation predicted observed controls A rename case 'qrcode'out of both switchesRED 2/3 exact #11565 green; #11794 red ⇒ not decisive B add case 'secret'(a spec carve-out) to the varchar branch — the⊆directionRED 1/3 exact #11565 and #11794 both green C spec admits a type the driver never learns — add('color')in memoryRED 2/3 exact, setup logged setSize=13 hasColor=trueboth existing pins green ⭐ C is the card's own failure mode, and both existing pins stayed green through it. That is the measured proof this guard covers an edge nothing else did — the claim the card makes, established rather than argued. A's non-decisiveness is reported honestly rather than counted as a third win.
Restores proven byte-identical by
git hash-objectvsHEAD:path; no-rebuild leg justified by import form (relative source specifier + this package's vitest alias resolving@objectstack/spec/*to source), measured not assumed.NOT MEASURED / narrowings — correctly declared
check:type-check-debt— its ratchet half needs the whole workspace closure built and refuses; named, ⛔ not counted green.- Full
driver-sqlsuite not run (no source file changed, so no existing test's subject moved); CI runs it regardless. - A first ablation-C attempt died on
CACError: Unknown option --setupFiles— exit 1 that never reached the guard body, reported as NOT MEASURED and re-run properly. ⭐ Same discipline as this lane's timeout-vs-failure distinction. - Out-of-scope finding not re-filed: already open as
hintCoversreads an extensionless module specifier as a filesystem path, so 9packages/specgate families can never be MATCHED to a change set — silent under-derivation on every dispatch #12514, duplicate confirmed by targeted search.
Enqueue condition
⛔ Not enqueued. Bar is every check green — not the required subset. On an all-green head: ready → merge queue. ⛔ Queued siblings #12121 and #12593 stay serial-queued behind this on
sql-driver.tsuntil it lands.
Generated by Claude Code
- added a commit that references this issue
on Sep 17, 2026
Found by the
domain:engineseat's unlock scan while closing #11431. Filed unlabelled and unassigned for triage to grade — ⛔ an execution seat does not grade.What changed to create this
Two things landed a day apart and now depend on each other, with no gate between them:
c49afd0886) —driver-sqlsizes a column fromfield.maxLength, deciding which types get a varchar at all invarcharColumnChars's type switch (packages/drivers/driver-sql/src/sql-driver.ts:13459).maxLengthis authorable on every field type and validated as no more than a number —maxLength: 0andmaxLength: 12.5parse cleanly #11566 (2cc7122245) —packages/specnow decides which types may declaremaxLengthat all, viaBOUNDED_STRING_FIELD_TYPES(packages/spec/src/data/field.zod.ts:1649).Measured on
origin/main, the two lists are related but not identical, and the difference is deliberate on both sides:BOUNDED_STRING_FIELD_TYPEStext, textarea, email, url, phone, password, markdown, html, richtext, codevarcharColumnChars→declaredVarcharLength(varchar-sized)string, email, url, phone, passwordvarcharColumnChars→keyableTextLength(TEXT unless keyed)text, textarea, html, markdownSo today they are consistent by reasoning: spec's list is "may declare a bound", the driver splits that into "gets a varchar" vs "gets TEXT". Nothing asserts that relationship, and the two files are owned by two different seats.
Why this is the #11566 defect one layer down
#11566 was filed precisely because three hand-maintained lists disagreed about where
maxLengthapplies (field.form.ts3 types,object.form.ts9 types,record-validator10 types). That card fixed the authoring half by making spec the single authority. It did not — and could not, from that lane — bind the driver to it.⇒ The failure mode is now: spec adds a type to
BOUNDED_STRING_FIELD_TYPES, the driver's switch does not learn about it, and the type silently falls to the catch-alltable.string(name)at knex's 255 — which is exactly the defect #11431 existed to fix, re-entering through a different door and against a bound the platform now formally accepts.Not claimed
@objectstack/lint's contract forbids depending on a runtime, so a lint rule may not be the right home — worth checking rather than assuming.richtextandcodeare in spec's list but in neither driver branch today; PR fix(driver-sql): richtext and code take an unbounded TEXT column, restoring the declared Rich Content grouping #11876 (open, with triage) moves exactly those two into the text family. Whoever takes this should read that PR first — the shape of the guard depends on where those two land.