Skip to content

fix(objectql): measure the value-bearing diagnostic families instead of reading them off a manual (#9160) - #9274

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-9160-value-bearing-diagnostic-probe
Aug 17, 2026
Merged

fix(objectql): measure the value-bearing diagnostic families instead of reading them off a manual (#9160)#9274
os-zhuang merged 1 commit into
mainfrom
claude/issue-9160-value-bearing-diagnostic-probe

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #9160

What this does

#8823 established that a database's diagnostic does not always name only IDENTIFIERS — MySQL's ER_DUP_ENTRY inlines the conflicting VALUE — and redacted that one slot while keeping the index name.

The list it introduced had exactly one entry and no way to notice a second was missing. Nothing measured whether a diagnostic a driver produced carried a value, so the next entry needed the same accident that found the first. The standing rule (packages/types/src/unique-violation.ts) — a dialect's spelling goes in once measured off a thrown error, never from a reading of the manual — correctly stopped the list growing on a guess.

This adds the instrument. sql-driver-diagnostic-value-probe.test.ts plants a canary, raises each candidate family through the driver's own bind path against the live MySQL 8.0 / PostgreSQL 16 services the Temporal Conformance (live PG + MySQL) job already stands up, and asserts of every family — value-bearing or not — where the canary lands: error.message (which ObjectLogger.write serializes, so an exposure) or error.detail (which it does not).

Positive control — mandatory, and it answered

ER_DUP_ENTRY (1062), the one family already known value-bearing and already encoded, is raised FIRST. Reproduced verbatim off live MySQL 8.0.46:

Duplicate entry 'SENSITIVE-CANARY-9160' for key 'probe_9160_mysql.uq_probe_9160_mysql_email'

That is the phrasing the existing single entry matches. Without it a zero anywhere else would be uninterpretable — so its failure message says "the instrument is broken", not "a test failed". Ablation-proved: making the control's second insert non-conflicting produces exactly that red.

What was measured

MySQL 8.0.46 · PostgreSQL 16.13, both configured as CI configures them (PG Asia/Shanghai, MySQL +08:00), canary SENSITIVE-CANARY-9160:

dialect family diagnostic, verbatim canary lands on
mysql 1062 Duplicate entry 'CANARY' for key 'probe.uq' message (already encoded)
mysql 1366 Incorrect integer value: 'CANARY' for column 'age' at row 1 message
mysql 1292 Incorrect datetime value: 'CANARY' for column 'when_at' at row 1 message
mysql 1264 Out of range value for column 'age' at row 1 absent
mysql 1406 Data too long for column 'label' at row 1 absent
mysql 1054 Unknown column 'zzz_nonexistent_field' in 'field list' absent
pg 22P02 invalid input syntax for type integer: "CANARY" message
pg 22007 invalid input syntax for type timestamp with time zone: "CANARY" message
pg 22003 value "99999999999" is out of range for type integer message
pg 23505 duplicate key value violates unique constraint "probe_email_key" detail
pg 23502 null value in column "id" of relation "probe" violates not-null constraint detail
pg 22001 value too long for type character varying(20) absent

Both families the card named as candidates are value-bearing, and both match the manual's phrasing exactly — which the card flagged as an open question and is itself informative, not a null result.

The Postgres result is the sharp one. #8823 recorded that Postgres escapes the unique-violation leak only because its value sits on error.detail, a field the logger never serializes — "coincidence, not a defence". 22P02 / 22007 / 22003 put the caller's value on error.message, the field that IS serialized. The coincidence does not cover them. That is exactly the distinction the dispatch asked to be called out.

Also measured and recorded as negatives worth having: Out of range value for column … (1264) and Data too long for column … (1406) read like value-bearing families and are not — they name the column only. invalid input syntax for type json carries no value on message at all (its token is on detail), so it is deliberately left untouched.

What changed in the redactor

The one-off regex pair became an enumerable VALUE_BEARING_TEMPLATES table, one row per measured family, each citing the live server that produced it. Four rows added — mysql 1366/1292 (column-bound), mysql 1292 (column-less), pg 22P02/22007, pg 22003.

Every identifier-bearing tail is still kept whole. Over-matching deletes the diagnostic an operator came for — the expensive direction #8682 paid to avoid — and the six identifier-only families above are pinned against exactly that regression.

Scope

Per the dispatch brief, this is the measurement half only. The contract inversion (an allowlist of identifier-shaped tails instead of a denylist of value-bearing ones) is not implemented, not prototyped, and the probe is not shaped to make it inevitable. redactBoundStatement still has exactly 3 call sites. No new error code, no packages/spec change, no *.zod.ts, no ledger touched, no public surface widened — the probe deliberately does not import the redactor, which is why it lives in driver-sql and the redactor's fixtures live in objectql with this probe named as their warrant.

Four families beyond the two the card named were encoded because the probe measured them value-bearing and 裁决 #3 makes the recorded output the warrant. Leaving a canary-confirmed leak unfixed while in the file would have forced the probe to either go red or be weakened to not notice it. Flagging it here so it can be trimmed if that reads as over-reach.

Known residue — measured, deliberately NOT closed

When the caller's value itself contains -, the statement cut lands inside it and eats the template head. Families with a right anchor (for key …, for column … at row N) recover via their tail pattern. The two whose value runs to end of message (pg 22P02/22007, mysql 1292's Truncated incorrect … spelling) have no anchor and leave a suffix of the value standing. Closing that needs the cut itself to become template-aware — a change to #8682's contract, so it is reported rather than decided. Filed as a finding; see the report on #9160.

Verification

All at a09f33eb, the final commit.

  • Live probe, MySQL 8.0.46 + PostgreSQL 16.13 stood up locally with CI's own configuration: 13/13 pass, positive control included.
  • Full driver-sql suite against both live servers, wired exactly as the CI job wires it (TZ=America/New_York, both URLs, OS_EXPECT_LIVE_DIALECT_MATRIX=1): 104 files, 2244 tests, all pass.
  • Full objectql suite: 213 files, 3765 tests, all pass (38 in driver-fault-redaction.test.ts, up from 14).
  • Typecheck: both packages clean.
  • Gate union re-derived from the actual changed paths with scripts/pm/dispatch-gates.mjs, all pass: check:changeset-gate-self-tests, check:durability-log-level, check:objectui-changeset, check:test-source-alias, check:type-source-resolution, check:query-options-erasure, check:type-check-coverage, check:engine-double-contract, check:where-matcher, check:nul-bytes, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-engine-split-ratio.

Reverse verification, three legs, each committed-then-ablated-then-restored:

  1. Remove the four new template rows — exactly the 8 new assertions go red, the 30 pre-existing stay green (so existing behaviour is provably unchanged).
  2. Declare ER_TRUNCATED_WRONG_VALUE_FOR_FIELD identifier-only — the probe reds with moved the caller's value from 'absent' to 'message' … this is a new leak. Add the template to VALUE_BEARING_TEMPLATES … and cite this output as the warrant. That is the notification objectql: the value-bearing-diagnostic list in driver-fault-redaction.ts has one entry and no way to notice a second is missing (observation) #9160 asked for, demonstrated firing.
  3. Make the positive control non-conflicting — reds with the positive control raised NO error: this probe is not measuring anything.

Generated by Claude Code

…of reading them off a manual (#9160)

#8823 left a one-entry list of value-bearing dialect templates and no way to
notice a second was missing. This adds the instrument: a live probe that plants
a canary, raises each candidate family through the driver's own bind path
against the MySQL 8.0 / PostgreSQL 16 services the Temporal Conformance job
already stands up, and asserts where the canary lands — error.message (which
ObjectLogger.write serializes) or error.detail (which it does not).

Positive control first: ER_DUP_ENTRY (1062) reproduced verbatim, so a zero
elsewhere is interpretable.

Four more families measured value-bearing on error.message and encoded:
mysql 1366/1292 (Incorrect <type> value: '<v>' for column '<c>' at row N),
mysql 1292 (Truncated incorrect <TYPE> value: '<v>'),
pg 22P02/22007 (invalid input syntax for type <t>: "<v>"),
pg 22003 (value "<v>" is out of range for type <t>).

The Postgres result is the sharp one: #8823 recorded that pg escapes the
unique-violation leak only because its value sits on error.detail, which the
logger never serializes -- "coincidence, not a defence". These three put the
caller's value on error.message, so the coincidence does not cover them.

Six identifier-only families raised by the same probe are pinned untouched, so
over-matching (which deletes the diagnostic an operator came for) is a red too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

11 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)

Coarse fallback — 14 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 origin/mainpackageMentionDocs.

Copy link
Copy Markdown
Contributor Author

PM review — answering the question you flagged, and holding the flip

Not flipping yet: 9 of 23 jobs are still in_progress at a09f33eb (three Test Core shards, three Dogfood Regression Gate shards, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL), Build Core, ESLint, TypeScript Type Check). Zero failures so far, read job-by-job. Temporal Conformance is the one that matters most here — it is the job that owns the live services your probe rides on, so a green there is the difference between "the probe passes" and "the probe passes where it will actually run."

The four extra families: ⛔ do NOT trim. They are in scope, and trimming would be the error.

You flagged encoding four families beyond the two the card named, and offered to trim. Keep them. 裁决 #3 of the dispatch says it in terms — "If your probe raises a family and records phrasing that shows a value slot, adding that entry is in scope — the recorded output is the warrant." The card named two families as candidates, not as a budget. Six measured value-bearing, six measured identifier-only, all off live servers: that is the warrant the standing rule asks for, and it is the only thing that has ever been allowed to grow that list.

⭐ Your own reason is the stronger one and I want it on the record: leaving a canary-confirmed leak unfixed while the probe watches that file would have forced the probe either to go red or to be weakened so it stops noticing. A measurement instrument that must be blunted to keep the tree green is worse than no instrument. The four rows are what keeps the probe honest.

What this review credits

On #9275 — filed correctly, and I am not reopening the inversion on it

You filed the residue as #9275 rather than fixing it. Correct: all three of its routes change #8682's cut, which is a different contract from the redactor's value list.

⚠️ Worth stating explicitly so nobody later conflates them: #9275 is not the allowlist/denylist inversion question. Triage's grading on #9160 ruled that the inversion "stays unopened unless the probe's measurements show the denylist shape cannot keep up." Your measurements do not show that — they show the opposite, that a denylist plus a probe that notices does keep up for the measured surface. #9275 is about where the cut lands, not about the list's polarity. So the inversion stays closed, per triage, and Fixes #9160 is right: the card as graded was scoped to the measurement probe only, and the probe is delivered.

Holding for the remaining 9 jobs.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectql: the value-bearing-diagnostic list in driver-fault-redaction.ts has one entry and no way to notice a second is missing (observation)

2 participants