# minisql differential battery — EVIDENCE Wizard: null (lane: Codeforge — hard coding and agentic reasoning). Oracle: sqlite3 3.53.1 (`python3 -V` 3.14.7). All numbers below are from real runs; nothing here is projected. ## What this is `minisql.py` is a hand-written SQL engine (parser → evaluator → relation algebra). `diff_sqlite.py` is the randomized differential battery that compares it against sqlite3 on identical SQL text, plus a mutation control that seeds eight known engine defects and requires each one to be caught. ## The blind spot, and its proof `probe_and_null5.py` proved, from SQL's own semantics, that the generator could never put a NULL-producing predicate inside an AND: `predicate()` built AND only as `(X AND Y)` with both operands from `comparison()`, and a comparison's tri-value is never NULL. Consequence, measured: the mutant def m_and_absorb(a, b): ta, tb = minisql.tri(a), minisql.tri(b) if ta is None or tb is None: return None # NULL absorbs FALSE survived every run. That is a battery with no teeth, not a passing test. ## The fix `Gen.conjunction()` now emits the two operand families that make three-valued AND observable: * a literal FALSE on the right (1 in 24) — `FALSE AND NULL` is FALSE in SQLite, so an engine where FALSE does not absorb NULL returns NULL and keeps a row; * a NULL-producing predicate on the right (1 in 12) — `TRUE AND NULL` is NULL, so an engine where NULL absorbs TRUE returns 1 and keeps a row SQLite drops. `Gen.query("join")` also emits `ON AND 1 = 0`: a FALSE ON matches nothing, so every left row is NULL-extended — the only shape where a NULL the ON clause did not produce survives to the output. ## Measured coverage (probes_null_logic.py, 2100 queries, seed 20261022) _tri_and calls : 7892 F1 _tri_and(NULL, FALSE) : 108 FIRED F2 _tri_and(FALSE, NULL) : 444 FIRED F3 _tri_and(TRUE, NULL) [not observable] : 180 FIRED F4 LEFT JOIN ON .. AND FALSE + IS NULL : 3 FIRED B. mutant patched: battery mismatches 5 CAUGHT C. unpatched battery: 0 mismatches, 3770 rows compared, 0 non-vacuity failures ## The second blind spot (sampling, not semantics) With the first mutant closed, `like-case-sensitive` survived at 140 and 280 mutant queries and was caught at 560 and 1400. Measured across `--mutant-queries` 140 / 280 / 560 / 1400: 140 -> SURVIVED 280 -> SURVIVED 560 -> CAUGHT (4 mismatches) 1400 -> CAUGHT (11 mismatches) Cause: `LIKE` appears only inside `predicate()`, of which it is 1 in 28, and the shape schedule was `i % 7` — so 140 queries gave 20 per shape, i.e. ~20 chances in 28 to draw a LIKE at all, spread over every engine feature. Nothing in the battery noticed that a thin slice can hide a surviving mutant. Fix, two parts: 1. `SHAPES[(i // _QUERIES_PER_SHAPE) % len(SHAPES)]` — every shape gets a fixed 20-query slice per round instead of a count that depends on the run length. 2. `check_report()` now refuses to pass unless every shape's slice is at least `MIN_PER_SHAPE` (20) wide. A 28-query run fails loudly: FAIL: shape scan got only 8 queries (< 20) -- a thin slice can hide a surviving mutant FAIL: shape where was never exercised ... RESULT: FAIL ## Regression guards (test_diff_sqlite_guards.py) G1 generator emits the NULL-tri-value families (4000 draws) AND with a literal FALSE operand 27 ok AND with a NULL-producing right operand 2123 ok OR present 519 ok LEFT JOIN ON with AND FALSE 1014 ok G2 shape schedule over 700 queries: every shape exactly 100 G3 check_report() on a thin slice: 8 failures, 7 naming the thin slice, plus the mutant-floor contract (see below) RESULT: PASS ## The check_report() KeyError, and the contract it exposed The first run of `check.sh` exited 1 with File ".../diff_sqlite.py", line 567, in check_report if report["mutant_queries"] is not None: KeyError: 'mutant_queries' `check_report()` read `mutant_queries` with `[]`, but a report built by hand in `test_diff_sqlite_guards.py` (and in `check.sh`) does not carry the key — and neither does any caller that merely wants the vacuity guards. The floor was therefore unreachable except through `main()`, which is the same shape of defect the floor itself exists to catch: a guard that only runs on one path. Contract now, stated in the function and asserted by G3: * a report that declares `skip_mutant_floor: True` (what `run_battery()` produces, since it measures engine-vs-oracle agreement and runs no mutants) is not asked for a width; * a report that declares it runs mutants must carry `mutant_queries`, and that width is enforced against `mutant_floor()` — a missing width is refused, not waved through; * a report that carries `mutant_queries` and does not skip the floor has the width enforced. G3 asserts all four directions: `check_mutant_queries(floor-1)` refused, `check_mutant_queries(floor)` accepted, a report carrying a sub-floor width refused, and a report declaring a mutant run with no width refused. ## Full battery, after the fix $ python3 diff_sqlite.py --queries 1400 --mutants --mutant-queries 1400 minisql differential battery vs sqlite3 3.53.1 seed=20261022 queries=1400 shape counts: const=200 scan=200 where=200 join=200 group=200 agg=200 distinct=200 ordered queries: 800 rows compared: 2515 queries returning >=1 row: 926 null-eq CAUGHT (252) left-join-as-inner CAUGHT ( 47) nulls-last-asc CAUGHT ( 97) sum-empty-zero CAUGHT ( 31) count-counts-nulls CAUGHT ( 25) int-div-becomes-real CAUGHT ( 22) and-null-absorbs-false CAUGHT ( 5) like-case-sensitive CAUGHT ( 6) mutants caught: 8/8 RESULT: PASS Before the fix, the same battery reported `mutants caught: 7/8` with `and-null-absorbs-false` surviving, and `RESULT: FAIL`. ## Commands python3 diff_sqlite.py --queries 1400 --mutants --mutant-queries 1400 python3 probes_null_logic.py --queries 2100 --mutant-queries 1400 python3 test_diff_sqlite_guards.py bash check.sh # the whole lane, one command: battery + mutants + floor + guards ## Known holes, stated plainly * `probes_null_logic.py` counts `_tri_and` calls at the instrumented call site, so F1/F2 are counts of the call, not of a verified output divergence per call. The output divergence itself is what the battery measures; the probe only proves the semantics are *reachable*, which is what was previously assumed and false. * F4 (`ON ... AND FALSE` plus an `IS NULL` filter) fires rarely (3 in 2100). It is not load-bearing: F1/F2 fire in the hundreds. * The generator still emits no bare column of a multi-row group with GROUP BY, because SQLite leaves that value unspecified — a documented hole, not a fixed one. * `check.sh` is the lane's CHECK line. It runs four independent verifications (battery + mutation control at 1400 queries, the mutants genuinely caught, the NULL-tri-value families firing, the regression guards) and exits nonzero if any one fails — it is a real test, not a summary of output already read.