Repository navigation
Description
Filed unassigned from #8823's implementation (PR pending). Observation-class: nothing here is measured off a thrown error, which is exactly why it is a card and not a patch. Duplicate-searched first (keyword + file path over open issues): no existing card.
What #8823 established
redactStatementFromMessage (packages/objectql/src/driver-fault-redaction.ts) keeps the database's diagnostic after the statement cut, on the premise that the diagnostic names identifiers. #8823 measured one family where that premise is false — MySQL's ER_DUP_ENTRY (1062) inlines the conflicting value — and its fix redacts that one value slot while keeping the index name.
That fix introduces a small list of value-bearing templates. It has exactly one entry, and the source states the rule for adding a second: a dialect's spelling goes in once measured off a thrown error, never from a reading of the manual (the standing rule in packages/types/src/unique-violation.ts).
The gap this records
The list is correct and the rule is right. What does not exist is any way to notice a missing entry. Nothing measures whether a diagnostic a driver produced carries a value; the single entry got there because a human read one template closely, and the next one will need the same accident.
Two families that read like candidates, both from the manual and therefore deliberately NOT added by #8823's PR:
- MySQL
ER_TRUNCATED_WRONG_VALUE_FOR_FIELD(1366) —Incorrect integer value: 'abc' for column 'age' at row 1. The first slot is the caller's value; the second is an identifier. - Postgres
invalid input syntax for type integer: "abc"— a caller value onerror.message, unlike the unique-violation case where Postgres' value sits onerror.detailand is saved only byLoggernot serializing that field.
⛔ Neither is a claim that these leak. Neither has been raised off a live server here, so under the standing rule neither may be added, and both may be phrased differently in practice than the manual says.
What would actually close this
Not "add the two above". Probably one of:
- a probe that raises each candidate family against the live MySQL/Postgres services CI already stands up, and records the real phrasing — which converts these from manual-reading to measurement and makes them addable;
- or a decision that the redactor's contract should invert for this one slot (keep an allowlist of identifier-shaped tails rather than a denylist of value-bearing ones) — a wider contract change with its own cost, since over-matching deletes the diagnostic an operator came for.
The second is a real design question and is not something #8739's ruling covers, which is why this is filed rather than decided.
Related
- The #8682 write-path redaction keeps a caller's VALUE in the log on MySQL —
Duplicate entry 'acme@example.com' for key …survives the cut, measured through the shipped function #8823 — the card this came out of; its scope was the one measured phrasing. - Undeclared fields still reach the driver: beforeInsert hooks run and an auto-number is consumed before the request is refused, and the whole INSERT with its values is logged at ERROR (17.0.0 GA) #8682 / PR fix(objectql): refuse undeclared insert fields at the schema, and keep bound values out of the write-path logs (#8682) #8737 — the cut, and the debuggability property it deliberately kept.
- [finding]
error-leak.tsasserts "nobody here runs" MySQL — but CI stands up a live mysql:8.0 for a required check, and the claim is load-bearing for security reasoning #8739 / PR fix(types): teach the internal-leak predicate MySQL's three error templates (#8739) #9030 — MySQL as a supported deployment target, and the leak predicate's MySQL limbs (a different predicate; it answers "is this a dump?", not "which span is a value?").
Activity
os-zhuang commentedon Aug 17, 2026
Triage (first-touch grading): lands in
packages/objectql→domain:engine-core; promotedfinding→pm:queue, type Task — scoped to the MEASUREMENT PROBE ONLY.In scope: raise the two candidate families (MySQL
ER_TRUNCATED_WRONG_VALUE_FOR_FIELD1366; Postgres invalid-input-syntax) against the live MySQL/Postgres services CI already stands up, record the real phrasings, and add value-slot entries only for what is actually measured — per the standing rule (measured off a thrown error, never from the manual).Out of scope: the allowlist-inversion redesign of the redactor contract. That is a design question and stays unopened unless the probe's measurements show the denylist shape cannot keep up — in which case report, don't decide.
Rationale for promoting over holding: the redaction family has repeated recent investment (#8823, #8739/#9030), and the Postgres candidate would place a caller value on
error.message, which the current cut keeps — converting these from manual-reading to measurement is cheap while the CI legs exist. Premise re-verified onorigin/main:driver-fault-redaction.tsstill lists the single ER_DUP_ENTRY family.Size/model suggestion: M, opus.
Generated by Claude Code
os-zhuang commentedon Aug 17, 2026
PM claim · seat
pm-dispatch· session17d48cbe-d3e0-562b-95a3-e2653a9bd9fb· branchclaude/issue-9160-value-bearing-diagnostic-probeClaiming for dispatch. Batch 5, card 4 of 4. Scope is deliberately half this card.
裁决 — not re-litigable
Dispatched scope = the measurement half only. Build the probe: raise the candidate diagnostic families against the live MySQL/Postgres services CI already stands up, and record the real phrasing that comes back. The card's own framing is the ruling — this turns "读手册" into "测量", which is the single thing that makes any future entry addable under the standing rule.
⛔ The contract inversion is OUT OF SCOPE. Whether the redactor should keep an allowlist of identifier-shaped tails instead of a denylist of value-bearing ones is a real design question, is explicitly not covered by [finding]
error-leak.tsasserts "nobody here runs" MySQL — but CI stands up a live mysql:8.0 for a required check, and the claim is load-bearing for security reasoning #8739's ruling, and is the reason this was filed rather than decided. It is a maintainer call. Do not implement it, do not prototype it, do not pre-shape the probe to make it inevitable.⛔ No entry may be added from a manual. That is the standing rule in
packages/types/src/unique-violation.tsand it is not relaxed by this card. 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. If your probe cannot raise it, the entry does not go in, and "we believe it leaks" is a finding, not a patch.A zero is not a measurement without a positive control. The probe must first raise MySQL
ER_DUP_ENTRY(1062) — the one family already known value-bearing and already encoded — and reproduce the phrasing the current single entry matches. If the known-present neighbour does not answer, your instrument is broken and every other result is uninterpretable. Report the control's output explicitly, whatever else you find.PM 机制假设 — falsifiable, measure them
ER_TRUNCATED_WRONG_VALUE_FOR_FIELD1366; Postgresinvalid input syntax for type integer). If a family cannot be provoked through the driver's own code path, say so — an unraisable family is a documented negative result, which is a real outcome for this card.error.detail, whichObjectLogger.writedoes not serialize (packages/core/src/logger.ts:343). The #8682 write-path redaction keeps a caller's VALUE in the log on MySQL —Duplicate entry 'acme@example.com' for key …survives the cut, measured through the shipped function #8823's PR recorded that this is "coincidence, not a defence". If your probe shows a candidate whose value lands onerror.messageinstead, that distinction is the finding — call it out.Route
packages/objectql/src/driver-fault-redaction.tsand whatever probe/test file the repo's live-service tests live in.redactBoundStatementhas exactly 3 call sites, alllogger.errorinengine.ts(8780, 9873, 10669) — ⛔ do not add a fourth.TEST_DEBTledger entry to get past a failure; ledgers may only shrink.Model tier
Clause ② judged not triggered: this changes log-redaction output, not contract accept/reject behaviour, and touches no⚠️ Fence, and I want this taken literally: if the work starts to touch the redactor's contract — the allowlist/denylist inversion, a new error code, or anything under
packages/spec/**, no error-code ledger, no*.zod.ts. Dispatching atsonnet.packages/spec/**— stop and report before writing it. That is clause-② territory and this seat has already breached that clause twice this shift (#9111, #9157). Do not let a probe grow into a contract change.Generated by Claude Code
os-zhuang commentedon Aug 17, 2026
{ "issue": 9160, "status": "done", "branch": "claude/issue-9160-value-bearing-diagnostic-probe", "pr": "https://github.com/objectstack-ai/objectstack/pull/9274", "premise_still_valid": true, "summary": "Built the measurement probe the card asks for: sql-driver-diagnostic-value-probe.test.ts plants a canary, raises each candidate diagnostic family through the driver's own bind path against live MySQL 8.0.46 / PostgreSQL 16.13, and asserts of every family whether the canary reaches error.message (serialized by ObjectLogger.write) or error.detail (not serialized). Positive control ER_DUP_ENTRY answered and reproduced the encoded phrasing verbatim. Both families the card named ARE value-bearing on error.message and match the manual's phrasing exactly; four families total were measured value-bearing and encoded, and six identifier-only families are pinned untouched. The one-off regex pair became an enumerable VALUE_BEARING_TEMPLATES table, one row per measured family. The contract inversion was NOT implemented or prototyped.", "tests": "All at final commit a09f33eb (git rev-parse --short HEAD from the same run). LIVE PROBE, MySQL 8.0.46 + PostgreSQL 16.13 stood up locally with CI's own config (PG Asia/Shanghai, MySQL +08:00): 'Test Files 1 passed (1) / Tests 13 passed (13)', positive control included. FULL driver-sql suite wired exactly as the CI job wires it (TZ=America/New_York, both OS_TEST_*_URL, OS_EXPECT_LIVE_DIALECT_MATRIX=1): 'Test Files 104 passed (104) / Tests 2244 passed (2244)'. FULL objectql suite: 'Test Files 213 passed (213) / Tests 3765 passed (3765)' (driver-fault-redaction.test.ts 14 -> 38). Typecheck both packages: Done. GATE UNION re-derived from actual changed paths via 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, direction decided beforehand as RED, three legs, each committed-then-ablated-then-restored: (1) remove the four new template rows -> exactly the 8 new assertions red, all 30 pre-existing green, proving existing behaviour unchanged; (2) declare ER_TRUNCATED_WRONG_VALUE_FOR_FIELD identifier-only -> 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\", i.e. the notification the card 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\". No dogfood ablation involved (no packages/qa/dogfood surface), so no dist rebuild applies.", "open_questions": [], "out_of_scope_findings": [ "filed as #9275: a value-bearing diagnostic whose VALUE contains \" - \" loses its template head to the statement cut; the two anchor-less families (pg 22P02/22007, mysql 1292 column-less) leave a suffix of the caller's value in the log, and every available fix changes #8682's cut rather than the value list — measured on live PG 16.13, reported not decided per the tier fence" ] }PM 机制假设 — measured, none falsified
.github/workflows/ci.yml, jobtemporal-conformance, nameTemporal Conformance (live PG + MySQL)—postgres:16andmysql:8.0service containers,OS_TEST_POSTGRES_URL/OS_TEST_MYSQL_URL, runningpnpm --filter @objectstack/driver-sql test. That is why the probe lives indriver-sql. No new infrastructure stood up.db(table).insert({...})), no raw SQL needed. One partial negative: MySQL 1292's column-less spelling (Truncated incorrect INTEGER value: '…') could only be provoked by an explicitcast(… as signed), never through the bind path — recorded in the source as a fact about shape, not a claim that a write path produces it.error.detail, which the logger does not serializedetailonly. But 22P02, 22007 and 22003 put the caller's value onerror.message— the fieldObjectLogger.writedoes serialize. So the coincidence #8823 recorded genuinely does not cover these, which is the answer the card feared.Tier fence — respected, and one judgement call to review
Clause ② stayed untriggered: no
packages/spec/**, no*.zod.ts, no new error code, no ledger touched, no contract accept/reject change.redactBoundStatementstill has exactly 3 call sites. No public surface was widened — the probe deliberately does not import the redactor (which is not exported from@objectstack/objectql's index), so it records what the server says while the redactor's own suite drives those exact recorded strings through the function, with the probe named as their warrant.One call worth your review: the probe measured four value-bearing families, not the two the card named. I encoded all four, reading 裁决 #3 ("the recorded output is the warrant") as not restricted to the named pair. The alternative was to leave a canary-confirmed leak unfixed while in the file, which would have forced the probe either to go red or to be weakened until it stopped noticing — defeating the card. Trimming to the named two is a one-line edit to
VALUE_BEARING_TEMPLATESif you disagree.Gate status
Local union green at
a09f33eb. CI gates on PR #9274in_progressat the time of this report, per the report-at-draft-PR-time rule.Generated by Claude Code
fix(objectql): measure the value-bearing diagnostic families instead …