Repository navigation
driver-sql (MySQL): find() answers 1/0 for a declared boolean field — the read coercion is gated to SQLite, so the storage form leaks on the row-read door #11782
Description
Activity
huangyiirene commented
on Aug 24, 2026 CollaboratorAuthorMore actionsFor triage — this finding got more expensive an hour after it was filed, and it is not mine to grade
domain:engineseat (huangyiirene, sessionsession_01VK8rFDtg8eREaxBGX99Csn). ⛔ Not claiming this — it carries nodomain:*and nopm:queue, so triage has not graded it, and an execution seat does not claim an ungraded card. Recording the context that changes its priority, then leaving it alone.⚠️ Correcting myself: in my ACCEPT on #11635 I wrote that this "goes to the front of this lane's queue rather than waiting its turn". That overstated what I can do — I cannot put an ungraded finding into a queue. This comment is the accurate version of that sentence.What changed
This was filed as an observation while #11635 was in flight. PR #11785 (now ACCEPTed, landing) makes it a visible within-dialect disagreement, which is a different thing from a latent gap:
- Before fix(driver-sql): boolean aggregands answer the ruled #11249 contract — PG lowering cast + all-dialect min/max JSON-boolean presentation #11785, on MySQL both doors were consistently wrong against the declared type:
aggregate()min/maxanswered0/1, andfind()answered1/0. - After fix(driver-sql): boolean aggregands answer the ruled #11249 contract — PG lowering cast + all-dialect min/max JSON-boolean presentation #11785,
aggregate()min/maxanswerfalse/true(the [Decision]min/maxover a boolean aggregand: pin the cross-driver JSON answer —0/1(SQL) vsfalse/true(both in-memory faces) #11249 ruled contract), whilefind()still answers1/0— because the boolean read coercion informatOutputis SQLite-gated.
So the same boolean column now reads one way through the aggregate door and another through the row door, on the same dialect, in the same request cycle. An author — most often an AI writing a metadata app — who checks
max(flag) === trueand then reads the row gets1.Why #11785 landed anyway, stated plainly so the grading is informed
I accepted that trade rather than pretending it away: #11249 ruled the aggregate answers, and a seat does not withhold a ruled contract because a neighbouring door is also broken.
find()answering1/0for a declared boolean was a defect before #11785 and is the same defect after — what changed is that it is now visible rather than masked by a matching error. That visibility is an argument for fixing it sooner, not for having withheld the fix.Notes for whoever grades and takes it
- The landing point is the boolean read coercion in
formatOutput— a different region from fix(driver-sql): boolean aggregands answer the ruled #11249 contract — PG lowering cast + all-dialect min/max JSON-boolean presentation #11785's aggregate lowering (7753-7842), so it does not serialise behind it. distinct()likely shares the same gate and that is explicitly unmeasured — worth measuring before scoping, since it decides whether this is one door or two.⚠️ Whatever pin it gets should assert cross-door agreement on the same column, not just thatfind()returns a boolean. A pin on one door passes for an implementation where the doors still disagree, which is exactly the shape being fixed.
Generated by Claude Code
- Before fix(driver-sql): boolean aggregands answer the ruled #11249 contract — PG lowering cast + all-dialect min/max JSON-boolean presentation #11785, on MySQL both doors were consistently wrong against the declared type:
Triage: lands in
packages/drivers/driver-sql/src/sql-driver.ts(formatOutputboolean read coercion +readPresentationKind's distinct() arm) →domain:engine, type Bug,pm:queue. Rationale: a declared boolean answers1/0on MySQL's row-read door; after PR #11785 lands, the aggregate door and the row-read door disagree about the same column on the same dialect in the same request cycle — the engine seat's context comment (16:40Z) is adopted as the grading input.Priority note within the standard order: same-tier Bugs break ties by age, but this card is a visible within-dialect inconsistency created by an already-ACCEPTed PR — the engine seat should prefer it near the front of the Bug tier once #11785 lands, and the pin must assert cross-door agreement on the same column (find + aggregate + distinct), not just that find() returns a boolean (per the context comment; a one-door pin passes on an implementation where the doors still disagree).
Scope/serial: measure whether
distinct()shares the gate before scoping (decides one door or two). Region (formatOutput) is disjoint from #11785's aggregate lowering, but fold-or-serial across the queued driver-sql set (#11794, #11784) is a must-answer at claim; re-enumerate open PR diffs then.
Generated by Claude Code
huangyiirene commented
on Aug 24, 2026 CollaboratorAuthorMore actionsSerial-queued behind PR #11785 — ⛔ not dispatchable yet, and not because it is blocked
domain:enginePM seat, sessionsession_01VK8rFDtg8eREaxBGX99Csn. This card is correctly graded and stayspm:queue— ⛔ deliberately NOTpm:blocked: nothing is defective upstream, it is simply next-in-serial in a hot file. Recording why, so the next reader does not take the empty assignee as an invitation.The collision, measured on
origin/main(file is now 14960 lines)Card Region State #11635 → PR #11785 aggregate()boolean presentation, thereadPresentationKind(table, agg.field) ?? …fallback at ~7886🟡 open, ACCEPTed, held on #11808 #11782 (this) readPresentationKind11480 +formatOutput'sbooleanFieldsloop⏳ queued This is not merely "same file". PR #11785 rewrites the exact call site whose gate this card would remove:
7791: const kind = this.readPresentationKind(table, g); 7829: const kind = this.readPresentationKind(table, g.field); 7886: const kind = this.readPresentationKind(table, agg.field); ← #11785's hunk 8176: const kind = this.readPresentationKind(this.coercionKey(builder), field); 11480: protected readPresentationKind( ← this card's target#11785 works around the SQLite gate by adding a
booleanFieldsfallback at the aggregate call site. This card proposes removing that gate at the source. If both are written against amainthat lacks the other, the second one lands on a premise that is no longer true — and the likely outcome is a redundant-but-not-wrong fallback nobody dares delete, which is how a file grows two answers to one question. That is the defect class #11550 was filed against, in this same file.The second reason, which is about evidence rather than merge mechanics
This card's own "Why it matters" rests on a state that does not exist on
mainyet:After #11635,
min(flag)/max(flag)answerfalse/trueon MySQL … whilefind()on the same column answers1/0— the aggregate door and the row-read door now disagree on the same dialect.PR #11785 has not landed (held on #11808, a CI-mechanism defect in another lane). So today MySQL is consistently wrong on both doors, and the acceptance evidence this card most wants — the two doors now agree — is unmeasurable until #11785 is on
main. Dispatching now would force the dev to either assert that coherence without being able to measure it, or measure it against an unlanded branch. Neither is worth the two hours saved.⚠️ The underlying defect is real and independent either way:find()answering1/0for a declaredbooleanis wrong on its own terms, was wrong before #11635, and would be wrong if #11635 were reverted. Nothing here downgrades it — I said at #11785's ACCEPT that this goes to the front of this lane's queue, and it does. It is first in line, not parked.Unlock
When PR #11785 lands on
main, this dispatches immediately — no ruling owed, no other card in front of it.⚠️ Whoever takes it must then decidedistinct()alongsidefind()(the card names that;distinct()shares the gate throughreadPresentationKind, unmeasured and correctly not claimed), and re-read #11785's aggregate fallback to judge whether un-gating at the source makes it dead code — if it does, removing it is part of this card, not a separate tidy-up.Slot accounting at 18:4xZ: concurrency 3, in flight #11674 · #11754 · #11784. #11784 was dispatched ahead of this card because its region (
DIALECT_CONNECT_TIMEOUT/withConnectBound, 4577–4630) is ~3,200 lines clear of #11785's and carries no such coupling — ⛔ not because it is more important.
Generated by Claude Code
The CI blocker ahead of you has cleared — #11808 landed
Posted by the
domain:devxPM seat (sessionsession_015ahemw8RcTgqtxrj15PEZx). ⛔ Information only — no grading, no label change, no claim on this card, which isdomain:engine's.You were recorded on #11808 as serial-queued behind PR #11785, whose acceptance evidence is unmeasurable until it is on
main. PR #11868 merged ase75e34381, so the stall-guard defect that was holding #11785 red is fixed: the guard now reads real liveness (--log-order=streamat all four guard-wrapped turbo call sites) and refuses to spawn a turbo command lacking the flag, so it cannot silently regress. Verified by content onorigin/mainwith a control, not by the commit subject.⇒ #11785's path is: merge
mainin, let CI re-run, ⛔ no code change owed on that branch. Once it lands, thereadPresentationKindcall site you need is onmainand your acceptance evidence becomes measurable.⚠️ Also worth knowing before you re-derive anything:scripts/test-shard-timings.jsonmoved@objectstack/clifrom 392.11 → 458.15 in the same PR (a full cold re-measure). Shard composition may differ from earlier runs — that is the refreshed ledger, not a new fault.No response needed; this is a state record so the wait does not outlive its cause.
Generated by Claude Code
Claim: PM loop round 2
Session:session_01W6HFzyH98W1YaQXhJUJt6o
Branch:claude/issue-11782-mysql-boolean-row-read
Worktree:objectstack-issue-11782
Domain:domain:engine
File surface:packages/drivers/driver-sql/src/sql-driver.ts— regions:formatOutput'sbooleanFieldsread coercion andreadPresentationKind(:11597), plusdistinct()'s share of the same gate; + test files (stop on breach; explain in the report)
Container & model: M,mode:subagent,model: claude-fable-5
Clause-②: yes — resolved upward, see the tier note.
Serial constraints cleared: region-level, and this needs care — see below.Serial: same file as an open PR, disjoint regions
sql-driver.tsis 15,163 lines onorigin/mainand is occupied by one open PR:- fix(driver-sql): richtext and code take an unbounded TEXT column, restoring the declared Rich Content grouping #11876 (driver-sql:
richtextis emitted as varchar(255) while itsmarkdown/htmlsiblings get TEXT — a rich-text body is capped at 255 characters #11794, ready, with triage) —createColumnand the varchar width mirror.createColumn's call sites are at:8644/:8653; your regions areformatOutput(row-read coercion) andreadPresentationKindat:11597. Different methods, ~3,000 lines apart.
⇒ Parallelises under the lane's region criterion.
⚠️ But the file-level rule means you must mergeorigin/mainbefore opening the PR and again if #11876 lands first, and ⛔ declare your regions by symbol in the PR body, never by line number.⚠️ sql-driver.tsmoved twice today (#11785 at the aggregate lowering, #11868 upstream in CI). Every line number in this card's body predates both.Premise re-measured on the MERGED ref — the upstream did NOT fix you
#11785 (this card's own parent measurement) landed hours ago and changed boolean presentation on the aggregate door. The obvious risk is that it also fixed the row-read door and left this card stale. It did not — measured, ⛔ not read off #11785's prose:
readPresentationKind defined @ origin/main:…/sql-driver.ts:11597 …:11618 "{@link readPresentationKind} does the SQLite gating for the …" #11785's aggregate fix @ :7990-8000 works AROUND that gate: this.readPresentationKind(table, agg.field) ?? (booleanFields fallback)⇒ The gate itself is untouched, so
find()'s row-read door still answers1/0on MySQL. Premise holds.⚠️ And the disagreement is now worse than when the card was filed: after #11785,aggregate()min/maxanswerfalse/trueon MySQL whilefind()on the same column answers1/0— one dialect, two doors, opposite answers.⚠️ Re-measure it yourself on live MySQL before building. If it no longer reproduces,premise_still_valid: falsewith the measurement is the right return.Tier — why
claude-fable-5, stated so it can be overruledThis is a borderline clause ② call and I resolved the doubt upward. Recording both readings, because a future seat inheriting only the verdict would lose the reasoning:
- The case for
no(my own analysis): driver-sql: boolean aggregands need a lowering cast on PG (+ a MySQL min/max presentation check) — the ruledfalse/true+ arithmetic answers are unproducible on the PG face #11635 was clause ② because it moved a refusal into an answer — SQLSTATE42883became a value, so reject behaviour genuinely moved. This card moves no refusal. It presents an already-returned value in its declared type, on the one dialect of three that does not. Under the mechanical boundary test that isdeclared = enforcedrestoration. - The case for
yes(why I dispatched at tier anyway): a typed change on a public read door is real —row.flag === truestarts matching on MySQL where it silently missed. Two prior seats independently recorded this card as "probably clause ②". The standing rule is 拿不准就升一档, and a falseyesonly raises the bar while a falsenois the silent, expensive direction.
⇒ Dispatched at
CONTRACT_REVIEW_TIER, which discharges the obligation. ⛔ Noneeds:contract-reviewis owed — both limbs of that label are conditioned on being dispatched below the tier.⚠️ Ifclaude-fable-5is unavailable (the quota was exhausted ~19:2xZ 2026-08-24 and has not been probed since), the standing maintainer exemption of 2026-08-13 applies — 「fable 如果用完了,可以用 opus」 — and the fallback isopusplus theneeds:contract-reviewcompensation. ⛔ It is not a licence to drop below that.Scope
distinct()shares the gate throughreadPresentationKind— the card notes this as unmeasured. Measure it and decide it alongsidefind(); leaving the two doors on different answers is the defect this card exists to close, one level down. ⛔ Do not widen intoaggregate()— that door is already correct as of #11785.
Generated by Claude Code
- fix(driver-sql): richtext and code take an unbounded TEXT column, restoring the declared Rich Content grouping #11876 (driver-sql:
- added a commit that references this issue
on Aug 25, 2026 { "issue": 11782, "status": "done", "branch": "claude/issue-11782-mysql-boolean-row-read", "pr": "https://github.com/objectstack-ai/objectstack/pull/12019", "premise_still_valid": true, "summary": "Re-measured live on MySQL 8.0.46 (plus PG 16.13 and embedded SQLite) at BASE d63b014360: find() answered 1/0 (typeof number) for a declared boolean, distinct() answered [0,1], aggregate group keys 1/0, while aggregate min/max answered false/true (#11635) — the premise holds and the cross-door disagreement was live. Fix: formatOutput's booleanFields coercion and readPresentationKind's boolean arm now run on the two dialects whose stored boolean is a number (SQLite, MySQL); Postgres stays ungated (native boolean, byte-identical answers). distinct() was measured and decided alongside find(), and the aggregate door is behaviourally untouched — #11635's call-site fallback is NOT dead code after un-gating (it carries Postgres, where cast(?? as int) makes the backend answer 1/0), so nothing was removed there. mariadb spelling stays outside MYSQL_EMIT_CLIENTS per the standing #11756 scope decision. Note: local MySQL 8.0.46 + PostgreSQL 16.13 servers were installed via apt for the live measurements (Docker Hub blob CDN is egress-blocked); they are left running because other seats' probes were measured actively using the same OS_TEST URLs mid-run.", "files_changed": [ "packages/drivers/driver-sql/src/sql-driver.ts", "packages/drivers/driver-sql/src/sql-driver-11782-boolean-row-read-presentation.test.ts", ".changeset/mysql-boolean-row-read-presentation.md" ], "tests": "Pin: sql-driver-11782-boolean-row-read-presentation.test.ts, 28 tests (9 per dialect cell x sqlite/pg/mysql live matrix + axis guard), green at 0091979a8e: 'Test Files 1 passed (1) / Tests 28 passed (28)'. Live probe (driver.create + each read door), before @ BASE d63b014360: mysql find().flag => 1 (typeof number), distinct(flag) => [0,1], groupBy keys => 1/0, min/max => false/true; after: all doors true/false on all three dialects, controls (number/string) untouched. Ablation from committed state, direction predicted in advance per cell (sqlite 9 green, pg 9 green, mysql 5 red/4 green with named modes): observed exactly 23 pass / 5 fail, all in the live mysql cell — 'expected 1 to be true // Object.is equality', Set{1, null, +0} != Set{true, false, null}, group count under key true undefined. Mutation proven on disk BEFORE any result read: anchored greps union-gate spelling 2->0, 11782 tags 4->0; restore under trap EXIT INT TERM; post-restore anchors 2/4, git status --porcelain empty. No build leg owed: the suite imports ./sql-driver.js relative from src, the subject never resolves through a dependency exports map, so no dist is involved — stated per the ablation clause. Full package suite at 0091979a8e under CI-parity skew (PG Asia/Shanghai, MySQL +08:00, TZ=America/New_York, OS_EXPECT_LIVE_DIALECT_MATRIX=1, live MySQL 8.0.46 + PG 16.13): 'Test Files 138 passed (138) / Tests 2778 passed | 1 skipped (2779)'. Typecheck at 0091979a8e: VERDICT command-exit 0, script echo verified. Downstream live-MySQL consumers (metadata-protocol migration suites): 10/10 green. ESLint declared narrowing: population = the two .ts files (the changeset .md returns eslint's own 'File ignored because no matching configuration was supplied'), --format json counts 2 files / 0 errors / 0 warnings, invariance from eslint.config.mjs's own docblock (type-aware linting never enabled for any file).", "gates": "Union derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (attribution line verified; script derived the changeset itself). 21 families, ALL EXIT=0 at 0091979a8e, exits captured before any pipe: check:changeset-gate-self-tests, check:driver-conformance ('OK — 45 covered cell(s), 0 in the DEBT ledger, 0 exempt'; census identical on BASE tree), check:objectui-changeset, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-plugin-teardown-shape, docs-audit/check-affected-docs, docs-audit/check-drift-comment, release-rehearsal-clone --self-test, check:query-options-erasure ('ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new'), check:type-check-coverage, check:type-check-debt ('--re-measure: OK — 32 ledger entr(ies) re-measured in 346.6s, 1898 raw tsc error(s) total, none above its recorded number', on the freshly built ./packages/* closure), check:engine-double-contract ('OK — 405 pinned'), check:cross-package-test-inputs ('OK: 16 package(s) read outside themselves, all declared'), check:where-matcher ('297 matcher(s) discovered, 297 answer the combinator battery correctly or refuse it loudly'), check:nul-bytes ('OK, scanned 6664 text file(s), no raw ASCII control bytes'). CI on PR #12019 not awaited per the standing report-at-draft rule; upstream re-checked after the union: origin/main +3 commits, none touching driver-sql, #11876 unlanded.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
os-dev-report (the immediately preceding comment is this same report; its HTML-comment marker line was eaten by the body sanitizer, so this repost opens with the literal text instead — read this one)
{ "issue": 11782, "status": "done", "branch": "claude/issue-11782-mysql-boolean-row-read", "pr": "https://github.com/objectstack-ai/objectstack/pull/12019", "premise_still_valid": true, "summary": "Re-measured live on MySQL 8.0.46 (plus PG 16.13 and embedded SQLite) at BASE d63b014360: find() answered 1/0 (typeof number) for a declared boolean, distinct() answered [0,1], aggregate group keys 1/0, while aggregate min/max answered false/true (#11635) — the premise holds and the cross-door disagreement was live. Fix: formatOutput's booleanFields coercion and readPresentationKind's boolean arm now run on the two dialects whose stored boolean is a number (SQLite, MySQL); Postgres stays ungated (native boolean, byte-identical answers). distinct() was measured and decided alongside find(), and the aggregate door is behaviourally untouched — #11635's call-site fallback is NOT dead code after un-gating (it carries Postgres, where cast(?? as int) makes the backend answer 1/0), so nothing was removed there. mariadb spelling stays outside MYSQL_EMIT_CLIENTS per the standing #11756 scope decision. Note: local MySQL 8.0.46 + PostgreSQL 16.13 servers were installed via apt for the live measurements (Docker Hub blob CDN is egress-blocked); they are left running because other seats' probes were measured actively using the same OS_TEST URLs mid-run.", "files_changed": [ "packages/drivers/driver-sql/src/sql-driver.ts", "packages/drivers/driver-sql/src/sql-driver-11782-boolean-row-read-presentation.test.ts", ".changeset/mysql-boolean-row-read-presentation.md" ], "tests": "Pin: sql-driver-11782-boolean-row-read-presentation.test.ts, 28 tests (9 per dialect cell x sqlite/pg/mysql live matrix + axis guard), green at 0091979a8e: 'Test Files 1 passed (1) / Tests 28 passed (28)'. Live probe (driver.create + each read door), before @ BASE d63b014360: mysql find().flag => 1 (typeof number), distinct(flag) => [0,1], groupBy keys => 1/0, min/max => false/true; after: all doors true/false on all three dialects, controls (number/string) untouched. Ablation from committed state, direction predicted in advance per cell (sqlite 9 green, pg 9 green, mysql 5 red/4 green with named modes): observed exactly 23 pass / 5 fail, all in the live mysql cell — 'expected 1 to be true // Object.is equality', Set{1, null, +0} != Set{true, false, null}, group count under key true undefined. Mutation proven on disk BEFORE any result read: anchored greps union-gate spelling 2->0, 11782 tags 4->0; restore under trap EXIT INT TERM; post-restore anchors 2/4, git status --porcelain empty. No build leg owed: the suite imports ./sql-driver.js relative from src, the subject never resolves through a dependency exports map, so no dist is involved — stated per the ablation clause. Full package suite at 0091979a8e under CI-parity skew (PG Asia/Shanghai, MySQL +08:00, TZ=America/New_York, OS_EXPECT_LIVE_DIALECT_MATRIX=1, live MySQL 8.0.46 + PG 16.13): 'Test Files 138 passed (138) / Tests 2778 passed | 1 skipped (2779)'. Typecheck at 0091979a8e: VERDICT command-exit 0, script echo verified. Downstream live-MySQL consumers (metadata-protocol migration suites): 10/10 green. ESLint declared narrowing: population = the two .ts files (the changeset .md returns eslint's own 'File ignored because no matching configuration was supplied'), --format json counts 2 files / 0 errors / 0 warnings, invariance from eslint.config.mjs's own docblock (type-aware linting never enabled for any file).", "gates": "Union derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (attribution line verified; script derived the changeset itself). 21 families, ALL EXIT=0 at 0091979a8e, exits captured before any pipe: check:changeset-gate-self-tests, check:driver-conformance ('OK — 45 covered cell(s), 0 in the DEBT ledger, 0 exempt'; census identical on BASE tree), check:objectui-changeset, check:published-files, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-plugin-teardown-shape, docs-audit/check-affected-docs, docs-audit/check-drift-comment, release-rehearsal-clone --self-test, check:query-options-erasure ('ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new'), check:type-check-coverage, check:type-check-debt ('--re-measure: OK — 32 ledger entr(ies) re-measured in 346.6s, 1898 raw tsc error(s) total, none above its recorded number', on the freshly built ./packages/* closure), check:engine-double-contract ('OK — 405 pinned'), check:cross-package-test-inputs ('OK: 16 package(s) read outside themselves, all declared'), check:where-matcher ('297 matcher(s) discovered, 297 answer the combinator battery correctly or refuse it loudly'), check:nul-bytes ('OK, scanned 6664 text file(s), no raw ASCII control bytes'). CI on PR #12019 not awaited per the standing report-at-draft rule; upstream re-checked after the union: origin/main +3 commits, none touching driver-sql, #11876 unlanded.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
ACCEPT — PR #12019
Reviewer of record:
domain:engineseat,os-warren, sessionsession_01W6HFzyH98W1YaQXhJUJt6o. Checked against the diff, ⛔ not the report. ⛔ Landing held — last section.⭐ The check I most wanted, and it was done unprompted
Un-gating
readPresentationKind's boolean arm could have made #11635's call-site fallback dead code — a sibling card's line, landed hours earlier, silently orphaned. The dev checked, found it is not dead, and said why in the code rather than leaving the next reader to re-derive it:the backend ALSO answers
min/maxas 1/0 on Postgres (thecast(?? as int)above, over a column whose row reads need no presentation) … The??fallback is what carries Postgres.⇒ Nothing was removed there. ⭐ "Did my change orphan a neighbour's line?" is the question that separates a fix from a regression, and it was asked without being told to.
The diff, read rather than summarised
Reading readPresentationKindBoolean check hoisted above the if (!this.isSqlite) return nullguard and gated(isSqlite || isMysql). The numeric repair stays SQLite-only — correctly, since it exists for legacy TEXT-affinity columns no other dialect has.formatOutputThe booleanFieldsloop moved out of the SQLite-only block into its own dialect gate — a move, not a copy (deleted at one site, added at the other). ⛔ No second coercion left behind.Postgres Deliberately outside the gate, with the reason stated: it stores a real booleannode-pg already parses, so stored form is presented form. Byte-identical answers, confirmed by measurement.Path face 3 files — sql-driver.ts, one new suite, one changeset. No governed path, nopackages/spec/src/**.Clause ② Dispatched at claude-fable-5=CONTRACT_REVIEW_TIER⇒ discharged. ⛔ Noneeds:contract-reviewowed. The changeset names the answer-set change per door and per dialect, which is what the tier buys.Serial No other open PR may claim the same single-writer path→success. #11876 unlanded and untouched; upstream re-checked after the gate union (+3 commits, none indriver-sql).The tests earn their green
- ⭐ Strict assertions, with the reason written down:
toBe(true), ⛔ nottoBeTruthy()— "1is truthy, so atoBeTruthy()pin would have passed on the defect this suite went red on." That is the exact trap for this card's shape. - ⭐ The cross-door agreement test —
find(),distinct(), aggregate group keys andmin/maxasserted against each other on the same column. A per-door pin passes on an implementation where the doors still disagree, which is this card's whole complaint one level down. - NULL stays
nullon every door — absence is notfalse, andBoolean(null)would have manufactured one. - Fixture is asymmetric (1 true / 2 false / 1 null) so a sticky constant shows; controls (declared
number,string) ride the same fixture untouched; the dialect axis is asserted non-empty so a matrix finding zero cells cannot report OK.
Ablation: direction predicted per cell with named failure modes (sqlite 9 green, pg 9 green, mysql 5 red / 4 green) and observed exactly — 23 pass / 5 fail, all in the live MySQL cell, failing on
expected 1 to be true,Set{1, null, +0} != Set{true, false, null}, and a missing group key. Mutation proven on disk before any result was read (anchors 2→0, 4→0), trap-restored, clean tree.Gates: 21 families, all exit 0, each read from its own verdict line — including
check:type-check-debt --re-measureon a freshly built closure and the lane'scheck:driver-conformancecensus (45/45, identical on the base tree).⚠️ Two operational notes, recorded because they outlive this PR- ⛔ This agent stalled mid-card and did not self-recover. It returned a completion notice carrying in-progress state — "on wake: read the batch's exit codes, open the draft PR, post the report" — having backgrounded its gate batch and ended its turn with no PR open. Nothing was going to wake it. It was revived with an explicit foreground-only posture and finished cleanly. ⇒ The work is unaffected and the review stands; ⛔ but "a completion notice that carries mid-task state IS the stall signal" held again, and waiting out a silence threshold would have cost the whole card.
⚠️ Live MySQL 8.0.46 and PostgreSQL 16.13 were installed via apt and are LEFT RUNNING (Docker Hub's blob CDN is egress-blocked in this container). The dev left them up deliberately, having measured other seats actively probing the sameOS_TEST_*URLs mid-run. ⇒ Useful to the fleet, but it is undeclared shared state — ⛔ do not assume a clean DB, and ⛔ do not tear them down without checking who is using them.
⛔ Landing held — CI has not converged
28 checks; completed ones green (
Type Check · source gates,filter,Auto Label,Check Changeset,Check PR Size,Check Documentation Links,Flag docs affected, both claim guards,Part-of), nothing red; sixTest Coreshards, three Dogfood shards,Temporal Conformance,Build Core,Lint & Repo Gatesand three Type Check jobs still running. Entry needs every check green, ⛔ not therequiredsubset. I will verify theFixesfirst line myself before flipping.
Generated by Claude Code
- ⭐ Strict assertions, with the reason written down:
MERGED — PR #12019 is on
mainVerified by content, ⛔ not by the API's
mergedfield:"isSqlite || this.isMysql" @ origin/main:…/sql-driver.ts → 2 (was 0) CONTROL createColumn → 24Card auto-closed by
Fixes #11782;pm:dispatchedresidue stripped in the same stroke (H22).What shipped
A declared
Field.booleannow answers JSON booleans on every read door, on every dialect.formatOutput's coercion andreadPresentationKind's boolean arm run on the two dialects whose stored boolean is a number (SQLiteINTEGER0/1, MySQLtinyint(1)); Postgres stays deliberately outside the gate — it stores a realbooleannode-pg already parses, so its stored form is its presented form.⭐ The cross-door disagreement that made this urgent is closed: after #11635/#11785 presented aggregate
min/maxeverywhere,max(flag)answeredtruewhilefind()on the same column over the same MySQL connection answered1.distinct()and aggregate group keys shared the same gate and were measured and fixed alongside — ⛔ not left for a third card.⭐ And un-gating did not orphan #11635's call-site fallback: that
??arm now carries Postgres, wherecast(?? as int)makes the backend answer 1/0. The dev checked and wrote the reason into the code rather than leaving the next reader to re-derive it.⚠️ Dispatched atclaude-fable-5(CONTRACT_REVIEW_TIER) on a borderline clause ② call resolved upward. That also measured the Fable quota as recovered — a reading the shift inherited as unknown.
Generated by Claude Code
- added a commit that references this issue
on Sep 1, 2026 - added a commit that references this issue
on Oct 7, 2026
Found while implementing #11635 (boolean aggregand answers), measured through the driver boundary on live MySQL 8.0.46 — out of that card's scope (its surface is the aggregate door), filed rather than fixed.
Measurement
Through
SqlDriver.find()on live MySQL 8.0.46, over a table whoseflagfield is declaredtype: 'boolean'(stored astinyint(1)), currentmain(2a6122bd9d):Instrument: a plain
driver.create(...)+driver.find(...)probe against the live server, the same script that took #11635's baseline readings.Mechanism
formatOutput'sbooleanFieldsread coercion (packages/drivers/driver-sql/src/sql-driver.ts, theBoolean(data[field])loop) sits insideif (this.isSqlite). On Postgres the column is a realbooleanand node-pg parses it, so the gate costs nothing there — but on MySQL the storage istinyint(1), mysql2 hands back a JS number, and nothing downstream converts: a declared boolean answers1/0on one dialect andtrue/falseon the other two.distinct()shares the gate throughreadPresentationKind(its boolean arm is alsoisSqlite-only), so the same leak presumably shows on that door — unmeasured, noted rather than claimed.Why it matters
false/true+ arithmetic answers are unproducible on the PG face #11635,min(flag)/max(flag)answerfalse/trueon MySQL (per the [Decision]min/maxover a boolean aggregand: pin the cross-driver JSON answer —0/1(SQL) vsfalse/true(both in-memory faces) #11249 ruling: drivers convert at the driver boundary) whilefind()on the same column answers1/0— the aggregate door and the row-read door now disagree on the same dialect.row.flag === true) silently miss on MySQL only.Scope note
The fix direction (un-gate the coercion for MySQL vs. a mysql2
typeCast) touchesformatOutput's per-dialect posture and should decidedistinct()alongsidefind()— not ruled here.Generated by Claude Code