Skip to content

fix(objectql): the raw-statement door and the lifecycle sweep cut a driver fault by construction (#21345) - #21384

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21345-fault-redaction-residue
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21345-fault-redaction-residue

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21345

Clause-②: no

Security handling: this body, the commits, the changeset and the test names carry classes and positions only. Every pin plants a synthetic sentinel and asserts it is ABSENT; no measured log line is copied anywhere.

What this changes

The engine-boundary cut from #21274 left two positions. Both now take the same helper, redactPropagatedDriverFault in packages/objectql/src/driver-fault-redaction.ts. There is no second redaction and no copy of the cut, and the frozen verb list in packages/types/src/error-leak.ts is untouched.

  1. The raw-statement door (ObjectQL.execute). The helper cut message and stack only when the shared leak predicate recognised the text as a driver dump, and the predicate reads a statement by four leading verbs. The door now passes { statementSent: true }: it handed the driver a statement, so it knows by construction that one may lead every message on the fault's chain. The cut then runs without asking the predicate. Same split, same structural cut at the separator, same value templates, same property rules. A statement whose leading verb is not one of the four leaves the bare marker, with no verb kept.
  2. The lifecycle sweep (lifecycle-service.ts). The sweep's per-object catch routes the fault through the same helper before it is pushed to report.errors or logged at WARN. Faults from the engine doors this loop also uses arrive already cut, and the helper hands those back unchanged.

Measured baseline (before the fix)

Probe: ObjectQL.execute over a real SqlDriver, one synthetic sentinel bound into the statement, an extractor printing only the carrier paths that hold it. Measured on better-sqlite3 13, live PostgreSQL 16 and live MySQL 8.0 at base 23365eaed.

dialect statement class carriers holding the sentinel
sqlite common-table-expression form, diagnostic matching no phrasing cause.message, cause.stack, default inspect
sqlite dialect upsert verb, diagnostic matching no phrasing cause.message, cause.stack, default inspect
pg common-table-expression form, value in the diagnostic cause.message, cause.stack, default inspect
pg dialect merge verb, value in the diagnostic cause.message, cause.stack, default inspect
mysql common-table-expression form, diagnostic matching no phrasing cause.message, cause.stack, default inspect
mysql dialect upsert verb, value in the diagnostic cause.message, cause.stack, default inspect
all three CONTROL: listed verb none

In every row the envelope's own message was composed, String() and JSON were clean, and mysql2's sql was already cut by the unconditional property rule. The engine's logger received nothing on this door, because the execute door writes no engine log line. After the fix, every row reads "none", with the class, code, status and cause.code unchanged.

Zone 2 hypotheses

  • H1 confirmed, with one amendment. The positions and the residue are as stated, and the residue reproduces on all three dialects. The amendment: there is no engine WARN line on the execute door. The only log line on that path is driver-sql's own raw-terminal line (see "Out of scope" below).
  • H2 partly falsified. The engine does not know the text the message carries. knex prefixes the statement COMPILED with the bound values inlined (better-sqlite3, mysql2), so removing the sent text would match nothing and leave the values in place. What the door does know is that a statement leads the message, and the structural cut needs nothing more. So the helper takes the door's knowledge as its input, { statementSent: true }, in one place. The engine door is the cleaner site: it covers every driver behind it, and the triage direction's second clause holds there too, because the propagated fault's cause leaves carrying no statement in any field. driver-sql is not edited.
  • H3 confirmed. The per-site table is below.
  • H4 confirmed. The REST answer (mapDataError status, code and field) and the missing-table classifier are pinned equal raw-versus-cut on every new shape. The [security] A driver error on an auth-table write reaches the auth library's logger unredacted: the server log carries the statement's bound values (credential material among them), while the engine's own line is redacted #21274 suites stay green: driver-fault-boundary-redaction.test.ts with its byte-identical WARN-line controls, the full objectql suite, and the plugin-auth carrier pin against the rebuilt objectql dist. That pin's SQLite cell passed 5/5; its PostgreSQL and MySQL cells were a named skip, because the live servers had been stopped by then.

Per-site table: direct-driver calls in lifecycle-service.ts

site call what it binds status
Archiver, cold schema sync cold.syncSchema identifiers only (DDL) redacted now, through the sweep's catch
Archiver, hot page read hot.find the cutoff and tenant ids redacted now, through the catch. On driver-sql, the read terminal already composes its message
Archiver, cold copy cold.upsert, per row the archived row's values redacted now: the card's position
Archiver, hot delete hot.bulkDelete the copied ids redacted now, through the catch
Archiver, cold prune cold.deleteMany a cutoff redacted now, through the catch
Rotator driver.rotateShards identifiers and a clock redacted now, through the catch
space reclaim, after the loop driver.reclaimSpace nothing (a pragma or vacuum) already safe: it binds no caller value
governance row count driver.count(name) with no filter nothing already safe: it binds no caller value, and a driver-sql read fault is a composed envelope
governance snapshot, tenant scan, reference audit engine doors (engine.find) — already safe: the engine boundary cuts them

Pins

  • packages/objectql/src/driver-fault-redaction-residue.test.ts holds 48 cases:
    • The six raw-path shapes above, mirrored property for property. Each pins non-vacuity (the cause carries the sentinel, and the predicate does not recognise it), no carrier (own properties including hidden ones, the cause chain, default and hidden inspect, String, JSON), class, codes, non-enumerable cause, diagnostic, REST answer and idempotence.
    • The door through ObjectQL.execute, with no sentinel in the engine logger.
    • The Archiver: three dialect cold-write faults and one hot-delete fault. Neither the sweep's WARN line nor its report.errors entry carries the sentinel, both keep the diagnostic, and nothing is hot-deleted.
    • Controls: a listed-verb raw fault is cut byte-identically with and without the flag, a non-dump error leaves as the same reference, and a non-driver sweep failure is reported word for word.
  • packages/qa/dogfood/test/raw-statement-fault-redaction.test.ts holds 10 cases on real stores:
    • Per cell, two unlisted-verb statements and a listed-verb control, through ObjectQL.execute over a real SqlDriver. SQLite always runs. The PostgreSQL and MySQL cells run when OS_TEST_POSTGRES_URL / OS_TEST_MYSQL_URL are set, as a named skip otherwise, each in a schema or database derived from the file name.
    • The Archiver over two real SQLite stores, where a trigger makes the cold store refuse the copy.

Reverse verification

Both legs were run with the fix committed, the mutation applied through scripts/ablation-replace.mjs (anchor hit 1 to 0, blob changed, restored to the HEAD blob with an empty git diff HEAD), objectql rebuilt, and scripts/ablation-dist-preflight.mjs reading dist/ before each colour was believed.

Verification (final head)

Final head: e036911f5b. The full suites ran at 69ba7adfbd, the merge of origin/main f39760864c. The one later commit touches only the two new test files, which were re-run there, as were both typechecks.

  • pnpm --filter @objectstack/objectql test: 363 files, 7325 tests passed. test:repo: 1 file, 5 tests passed.
  • pnpm --filter @objectstack/objectql typecheck: exit 0, test layer included (check:test-typecheck OK, no new debt).
  • pnpm --filter @objectstack/dogfood typecheck: exit 0 (its include is test/**/*).
  • pnpm --filter @objectstack/dogfood test, against freshly built dists with both live URLs set: 167 files passed and 1 skipped; 1379 tests passed and 3 skipped. The new file alone: 10/10 on SQLite, live PostgreSQL 16 and live MySQL 8.0.
  • Gates: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 70 at e036911f5b. All 70 were run, every one exited 0, and --ran reconciles 70/70.
    • check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3, nothing measured). The 8 packages it named were then built (a full turbo cache hit), and it re-ran with exit 0.
    • Also run: pnpm check:live-db-isolation PASS (it sees the new live file), node scripts/check-nul-bytes.mjs exit 0, and a control-byte self-scan of every touched file with no hit.
  • ESLint, narrowed to the 5 touched TS files and measured as follows:
    • Population, read from eslint.config.mjs: packages/**/*.{ts,tsx,mts,cts}. JSON output: 5 files, 0 errors, 0 warnings.
    • Invariance: the config enables no type-aware linting (no parserOptions.project), so this diff cannot move any untouched file's verdict.
    • The repo-wide pnpm lint is CI's.

Acceptance notes and deviations

  • File surface. The claim said "Pins in objectql". The real-driver pin lives in packages/qa/dogfood/test/, because objectql has no driver-sql dependency and dogfood is where this repo pairs an objectql unit pin with a real-driver pin. Dogfood is private, so the changeset is unaffected.
  • Local gate scope. The dispatch asked for the whole Lint & Repo Gates script set locally. The agent definition rules that enumeration CI's, and says it wins on a conflict. The derived union, plus the named extras above, ran locally, and CI runs the full set.
  • Temporary live servers. The PostgreSQL and MySQL instances ran under a temporary directory, were stopped by recorded PID, and were removed.

Out of scope (reported, not edited)

  • A third position: driver-sql's own server-log line for a refused raw statement. rawStatementFault writes the statement and the dialect's message to the driver logger before composing the envelope.
    • Measured carrying the sentinel on all three dialects, for listed and unlisted verbs alike, wherever the dialect inlines the value.
    • The read terminal and the unresolvable-filter refusal do the same by their documented design ("kept server-side for an operator").
    • This is outside this card's two positions and inside driver-sql, which this dispatch stops at, so the dev report raises it for the seat and the maintainer. The dogfood pin silences that logger and names the position in its header.

Generated by Claude Code

claude added 5 commits October 2, 2026 08:11
…river fault by construction

Position 1: ObjectQL.execute declares that it sent a raw statement, so the
boundary cut runs on every message of the fault's chain without waiting for
the shared leak predicate to recognise the statement's leading verb. The
predicate's frozen list is untouched.

Position 2: the lifecycle sweep's per-object catch routes the fault through
the same boundary helper before it is reported or logged, so a direct-driver
fault from the Archiver's cold or hot calls is cut like an engine door's.

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
…sentinel

The objectql pins hold the raw-statement door and the lifecycle sweep over
shapes mirroring the measured raw-path and cold-write faults; the dogfood pin
drives a real SqlDriver (SQLite always, PostgreSQL and MySQL where their URLs
are set) and the Archiver over two real SQLite stores. Controls: class and
codes survive, a statement the predicate already recognised is cut exactly
as before, a non-dump error leaves unchanged.

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
… real dispatch contract

Claude-Session: https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 17 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 43e928dd4c0be18b5a10430239371332f2ee47f4 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d6226d4bbf94f0607f711c3d4f989fa708696015 — the merge of head e036911f5bc4fc37796f8fd870abe38bae80d9db into base 43e928dd4c0be18b5a10430239371332f2ee47f4, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d6226d4bbf94f0607f711c3d4f989fa708696015 && git checkout d6226d4bbf94f0607f711c3d4f989fa708696015
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 43e928dd4c0be18b5a10430239371332f2ee47f4 e036911f5bc4fc37796f8fd870abe38bae80d9db && git checkout -B drift-repro 43e928dd4c0be18b5a10430239371332f2ee47f4 && git merge --no-ff e036911f5bc4fc37796f8fd870abe38bae80d9db

node scripts/docs-audit/affected-docs.mjs --json 43e928dd4c0be18b5a10430239371332f2ee47f4

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 10:07
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit d956910 Oct 2, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21345-fault-redaction-residue branch October 2, 2026 10:34
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ult cut, moved to core (objectstack-ai#21414)

Fixes objectstack-ai#21385

Clause-②: no

Security handling: this body, the commits, the changesets and the test
names carry classes and positions only. Every pin plants a synthetic
sentinel and asserts it is ABSENT. No measured log line is copied
anywhere, and the local probe printed booleans only.

## What this changes

This implements the maintainer's ruling on objectstack-ai#21385 (comment 5950942037,
letter A): one cutter for every log face. The cutter's home is
`@objectstack/types`, per the claim's amendment 1 (5954587444) and
triage's pointer (5954318919). See "Patch round 1" below.

1. **The redaction module moves to `@objectstack/types`.**
`packages/objectql/src/driver-fault-redaction.ts` becomes
`packages/types/src/driver-fault-redaction.ts`. Against `main`, that is
the PR's one git rename (`R092`). The cut is the same code: the same
split, the same structural cut at the separator, the same value
templates and the same property rules.
- The module's one import, `looksLikeInternalErrorLeak`, is now the
package-local `./error-leak.js`. `error-leak.ts` and its frozen list are
not edited.
- The types entry exports four names, listed by name:
`redactBoundStatement`, `redactStatementFromMessage`,
`redactPropagatedDriverFault` and the `DriverFaultOrigin` type.
- The template table and its load-time guard stay package-internal. The
guard's eight cases moved verbatim beside it, to
`packages/types/src/driver-fault-redaction.test.ts`.
- Why `types`: it is the lowest package every face of this family can
reach. `driver-sql`, `objectql` and `core` all depend on it, and the
family's fourth position, `operatorFacingErrorText`, lives inside it
(objectstack-ai#21418). The module header says so.
- There is no copy of the cut, no edit to `error-leak.ts` or
`driver-error-classification.ts`, and no row added to the frozen
predicate list. `packages/core` is not in the diff.
2. **One widening, on the log face.** `redactStatementFromMessage` takes
an optional second argument: the same `{ statementSent: true }` that
`redactPropagatedDriverFault` already took. Without it, the answer is
unchanged.
3. **objectql calls the moved code.** `engine.ts` and
`lifecycle/lifecycle-service.ts` now import from `@objectstack/types`,
with the same arguments as before. Five objectql test files repoint
their import. None of the moved names was on objectql's entries.
4. **driver-sql's five lines call it.** In `sql-driver.ts`, each refusal
line writes the dialect's text through `redactStatementFromMessage(text,
{ statementSent: true })`, imported from `@objectstack/types`. The lines
are:
- the unresolvable WHERE column (`INVALID_FILTER`), on `find` and on
`count`;
   - the read terminal (`DATABASE_ERROR`);
- the raw-statement terminal (`DATABASE_ERROR`), which no longer writes
the sent statement as a separate field;
   - the unresolvable groupBy / aggregation column (`INVALID_FIELD`);
   - the unresolvable listed-distinct column (`INVALID_FIELD`).

`statementSent` holds by construction: each line writes a fault raised
by a statement this driver sent, which is the same knowledge the
engine's raw door passes. The read terminal's cut sits where its text is
computed, so the two debug lines that demote it inside a scope take the
cut too (see Acceptance notes).
5. **`driver-turso`'s remote transport is covered by the same line.**
Its refusals reach the base class's raw terminal. Its transport error
carries no statement (the bare shape), so the line now writes the
engine's diagnostic and nothing of the statement it was sent.
6. **Two envelope sentences are corrected.** The read terminal's and the
raw terminal's composed `DATABASE_ERROR` messages said the statement was
written to the server log. After this change that is not true, so each
now says the diagnostic was written, with the statement and its bound
values cut. Code, status, `cause` and the withheld text are unchanged.
See Acceptance notes, deviation 1.

## Measured: sentinel on each line, before and after

Probe: drive each line through the public driver method on
better-sqlite3, a live PostgreSQL 16 and a live MySQL 8.0.46, with one
synthetic sentinel. An extractor printed only whether the captured line
held the sentinel, and on which field. BEFORE was measured at
`ecb6ca0258` (base), and AFTER at `0cbd86d55e`, with the same extractor.
The committed pins re-measure AFTER at the final head, `992e940fff`.

| line | drive | sqlite before → after | pg before → after | mysql
before → after |
|---|---|---|---|---|
| WHERE column | `find` | present → absent | absent → absent | present →
absent |
| WHERE column | `count` | present → absent | absent → absent | present
→ absent |
| read terminal | missing table (sqlite, mysql); 22P02 value (pg) |
present → absent | present → absent | present → absent |
| raw terminal | bound value | present (dialect field) → absent |
present (dialect field) → absent | present (dialect field) → absent |
| raw terminal | value spelled inline | present (statement field and
dialect field) → absent | same → absent | same → absent |
| groupBy / aggregation column | — | present → absent | absent → absent
| present → absent |
| listed-distinct column | — | present → absent | absent → absent |
present → absent |

After the change, on every row: the lead (code and class of fault,
object and column) is kept, the dialect's own diagnostic is kept, and
the cut's marker stands where the statement was. On the pg read and raw
rows, the 22P02 value slot reads as the value marker. The raw line no
longer carries a `statement:` field.

The `driver-turso` remote transport, with a sentinel spelled inline in
the sent statement: the raw terminal was handed it in the statement and
not in the transport's bare error, and the line carries none of it. No
marker appears there, because the bare error has no statement to cut.

## Zone 2 hypotheses

- **H1 confirmed, with one amendment.** All five lines were driven on
all three dialects. At base they sit at `sql-driver.ts` `:10087`,
`:10215`, `:10284`, `:10960` and `:11253`. The amendment: on PostgreSQL,
the three unresolvable-column lines never carried the sentinel. pg
positions bindings as `$n` before knex formats the message, and its
column diagnostic names identifiers only. Those three pg cells are
recorded in the pin as non-regression cells, not as non-vacuous ones.
The pg read and raw terminals did carry it, through the 22P02 diagnostic
and through the raw statement's own text.
- **H2 superseded in patch round 1.** Round 0 confirmed that
`packages/core` could hold the module. Amendment 1 moved it to
`packages/types`, where it also fits. Measured, the package takes a new
runtime module:
- its `tsup` entry is `src/index.ts` (edge-safe; the module uses no
`node:` builtin);
- its `tsconfig` includes `src/**/*`, and `--listFiles` shows both the
module and its test;
- `check-dts-emitted` runs in its build, and
`check:dual-build-cjs-loads`, `check:lean-entry-closure` and
`check:published-files` all pass;
  - no gate refused it, and none was edited.

The WHOLE module moved, not only the cutter. The two faces and the
property rules are thin wrappers over the module's private split, so
objectql no longer has a wrapper of its own.
- **H3 confirmed.** Each line keeps its lead, plus `(CODE)` on the read
and raw lines, and the dialect's diagnostic. It loses the statement and
any template-owned value. The raw terminal's separately logged
`statement:` field carried an inline sentinel on all three dialects at
base, and it is no longer written.
- **H4 partly falsified.** Every envelope's code, status, `cause` and
withheld text are unchanged. But two composed messages described the log
half ("the statement … written to the server log"), and this change made
them false. They were corrected; see deviation 1. The engine's existing
pins for the two earlier positions (the cards behind PR objectstack-ai#21335 and PR
objectstack-ai#21384) stay green:
- the full objectql suite, which includes
`driver-fault-boundary-redaction.test.ts` and
`driver-fault-redaction-residue.test.ts`;
- the dogfood suite, which includes
`raw-statement-fault-redaction.test.ts`;
  - the plugin-auth carrier pin, 15/15 on three dialects.

## Pins

- **New:
`packages/drivers/driver-sql/src/sql-driver-21385-refusal-log-line-redaction.test.ts`,
46 cases.** It runs on every cell of the driver axis: SQLite always,
live PostgreSQL and MySQL when their URLs are set.
  - Per cell, each of the seven drives has two cases:
- a SENTINEL case: the text handed to the line carried the sentinel as
measured, the written line does not carry it, and the marker is present;
- a CONTROL case: the envelope's code and status, the lead, the
diagnostic, the dialect code and the named object and column.
  - Each cell also has one success control.
  - One case pins the deferred-DDL debug line.
- Non-vacuity is read off what each protected refusal method was HANDED,
through a recording subclass. Every assertion on a line is a boolean, so
a red names the line and never prints it.
- **Flipped: nine existing pins in eight files.** Each of these pinned
the retired design, in which the statement was in the log line:
- in `driver-sql`: `sql-driver-11455-…`, `-11541-…`, `-16019-…`,
`-17639-…`, `-17857-…`, `sql-driver-backend-fault-envelope` (two cases)
and `sql-driver-unresolvable-where-column-refusal` (the positive
control);
- in `driver-turso` (patch round 1):
`turso-driver-16019-remote-raw-statement-fault-envelope`.

Each now asserts the diagnostic and that the statement is absent. The
`driver-sql` ones also assert the marker. The positive controls, and the
turso pin, read the sentinel's presence on what the refusal was handed
(or on the envelope's `cause`) instead of on the log.

## Reverse verification

**The five calls (round 0).** The change was committed first (head
`7445ed5cda`, `sql-driver.ts` blob `ede93583a0f1`). Only the five calls
were reverted: the four identical call sites and the read terminal's
one. This used two nested `scripts/ablation-replace.mjs` legs: anchor x4
→ x0 (blob `ede93583a0f1` → `e4243feaf00e`), then anchor x1 → x0 (blob
`e4243feaf00e` → `a8ea87a6014b`). The driver-sql pin loads
`./sql-driver.js` from source, so no build sits between the mutation and
the run.

- **Mutated: 22 red / 24 green of 46.**
- Red on "the sentinel reached the line": sqlite 7/7, mysql 7/7, pg 3
(read, raw bound, raw inline), and the debug-line case. That is 18.
- Red on "says a statement was cut": the 4 pg cells that were never
handed the sentinel (WHERE find and count, aggregate, distinct).
  - Green: all 21 CONTROL cases and the 3 success controls.
- **Restored.** Proven by the tool twice and by a script trap once: blob
== HEAD, and `git diff HEAD` is empty.

**The turso flip (patch round 1, at `992e940fff`).** Only the
`driver-sql` change this pin depends on was reverted: the raw terminal's
dropped `statement:` field was put back. `driver-turso` reads
`driver-sql` from its dist, so each leg was rebuilt and its dist checked
before the run.

- The first attempt was a no-op. The replacement text contained the
anchor, so `ablation-replace.mjs` refused it (anchor x1 → x1) and ran
nothing. The anchor was changed and the leg re-run.
- **Mutate.** Anchor x1 → x0, blob `ae07547bcd68` → `025ec6208050`. The
on-disk marker count was 1. `driver-sql` was rebuilt, and
`ablation-dist-preflight.mjs` found the marker present in 2 built files.
- The turso pin went 1 red / 3 green; the red is "the sentinel reached
the server log".
- The driver-sql raw pins went 7 red / 46 green: the six raw-terminal
sentinel cases on three cells, and the `16019` raw-line pin.
- **Restore.** Proven by the tool and by a trap: blob == HEAD
`ae07547bcd68`, and `git diff HEAD` is empty. After a rebuild, the
preflight with `--absent` found the marker absent from all 6 built
files. The turso pin went back to 4/4, and the driver-sql raw pins to
53/53.

## Verification

Final head: `992e940fff`. Live servers: PostgreSQL 16 and MySQL 8.0.46,
throwaway instances that were stopped by recorded PID and removed.
`TZ=America/New_York` was set, as the live CI job sets it.

- `@objectstack/types`: `test` 23 files / 696 tests passed; `test:repo`
1 file / 7 tests passed; `typecheck` exit 0.
- `@objectstack/core` (the module left it): `test` 76 files / 2156 tests
passed; `typecheck` exit 0.
- `@objectstack/objectql`: `test` 365 files / 7379 tests passed;
`test:repo` 1 file / 5 tests passed; `typecheck` exit 0, with
`check:test-typecheck` OK.
- `@objectstack/driver-sql`, full suite (`vitest run --maxWorkers=2`),
with both live URLs: 227 files passed; 5472 tests passed and 1 skipped;
0 sentinel occurrences in the run log. `typecheck`: exit 0.
- `@objectstack/driver-turso`: `test` 88 files passed; 2365 tests passed
and 33 skipped. `typecheck`: exit 0.
- `@objectstack/driver-sqlite-wasm`: `test` 36 files / 675 tests passed;
`typecheck` exit 0.
- The `Test Core (1/6)` packages that CI never reached:
- `spec`: `test` 600 files, 17606 passed + 1 todo; `test:repo` 48 files
/ 849 passed;
  - `service-settings` 33 / 591;
  - `cloud-connection` 31 / 401;
  - `service-messaging` 46 / 507;
  - `example-showcase` 31 / 394;
  - `plugin-pinyin-search` 2 / 21;
  - `connector-mcp` 3 / 23;
  - `knowledge-memory` 1 / 8.

  All exited 0.
- Dogfood, against freshly built dists (`turbo run build
--filter=@objectstack/dogfood... …`, 63/63), with both live URLs: `test`
171 files passed and 1 skipped; 1402 tests passed and 3 skipped.
- `typecheck` exit 0, after `pnpm install --frozen-lockfile`. The first
attempt failed on a missing link for `@objectstack/trigger-api`: the
`main` merge had added that dependency, and this worktree's install
predated the merge.
- The plugin-auth carrier pin
(`driver-fault-auth-log-carriers.test.ts`), on three dialects: 15/15.
- The runtime files that read these lines
(`seed-tenancy-autonumber-split`,
`expected-read-refusal-noise.channel-asymmetry`,
`metadata-list-ambient-vs-bare-transaction`,
`first-boot-migration-gate-read`): 4 files, 22 tests passed.
- Gates: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derived 70 at `992e940fff`. All
70 ran with exit 0, and `--ran` reconciles 70 derived / 70 run / 0
NOT-MEASURED / 0 UNRUN.
- `check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET (exit
3) for 8 packages without a dist. After those were built, it exited 0.
- Also run: `pnpm check:live-db-isolation` PASS, and a control-byte
self-scan of the 26 touched files, with no hit.
- ESLint, narrowed to the 23 touched TS files, at `992e940fff`:
- the population read from `eslint.config.mjs` is
`packages/**/*.{ts,tsx,mts,cts}`;
  - `--format json` gives 23 files, 0 errors and 0 warnings;
- invariance: the config enables no type-aware linting (no
`parserOptions.project`), so this diff cannot move an untouched file's
verdict.
  - The repo-wide `pnpm lint` is CI's.

## Patch round 1

- **Why.** CI on `7445ed5cda` went red in `Test Core (1/6)`, on the
`driver-turso` pin that asserted the statement on the raw terminal's
line. That pin is this PR's, and it is flipped here. The shard's first
attempt had failed earlier, in an unrelated `spec` hook timeout
(objectstack-ai#21421). Separately, the seat moved the cutter's home to
`@objectstack/types` (amendment 1, 5954587444), so that objectstack-ai#21418 can call
it from `operatorFacingErrorText`.
- **What moved.** The module, its guard test and the four named exports
moved from `packages/core` to `packages/types`, and core's export block
was removed. Every importer was repointed, with the module-path strings
and comments that named core. The core `minor` changeset became a types
`minor` one, and the driver-sql and objectql changesets now name
`@objectstack/types`. Net against `main`: one rename, `objectql` →
`types`; `git diff origin/main...HEAD -- packages/core` is empty.
- **The sweep for other pins of the retired line.**
- Command: `git grep -n -E` over the test files of all 18 packages that
depend on `@objectstack/driver-sql`. The pattern covered the five lines'
leads, the `[sql-driver]` prefix, `refused a raw statement` / `refused a
read on` / `could not be resolved on`, `kept server-side`, `statement:
`, and an `includes('DATABASE_ERROR' | 'INVALID_FILTER' |
'INVALID_FIELD')` filter.
- Result: 35 hit lines in 28 files. Read one by one, exactly one is a
pin of the retired content, the `driver-turso` pin above.
- The rest are prose, unrelated words, the objectstack-ai#7929 withheld-filter line,
or line consumers that key on the lead and the diagnostic. The four
runtime consumers among them were run, and are green.
  - No other lane's package needed an edit.
- **Does `dispatch-gates --commands` derive the suites of packages that
depend on a changed package?** Measured: no.
- It maps a file surface to `check:*` gate families. Of its 70 commands
at `992e940fff`, none is a package test suite: the only `--filter`
command is `spec`'s `check:duration-unit-keys`.
- So it derives neither the changed packages' own suites nor their
dependents'. That is why round 0's local runs never reached
`driver-turso`.

## Acceptance notes

- **Deviation 1: two caller-facing envelope messages changed by one
sentence each.** The dispatch said no caller-facing envelope changes.
The read terminal's and the raw terminal's composed messages stated that
the statement was written to the server log, and this change makes that
false. The agent definition requires a released string that a change
makes false to be corrected in that change, and it wins on a conflict.
So each sentence now states what is written: the diagnostic, with the
statement and its bound values cut.
  - The disclosure, code, status and `cause` are unchanged.
- `operatorFacingErrorText`'s copy matches only the leading "refused to
run a raw statement" fragment, which is kept; the producer pin
`sql-driver-16657-…` is green.
- Six hand-built fixture copies in five test files still carry the old
second sentence, in `metadata`, `metadata-protocol`, `rest`, `types` and
`objectql`. They stay green because the matcher keys only on the leading
fragment. They are left as they are: four of the files sit outside this
claim's surface, and editing only the fifth would split the copies.
- **Bounded in-place extension: the read terminal's two debug lines.**
The read terminal computes its dialect text once and writes it on the
warn line or on one of two debug lines (a pre-DDL question, or a table
whose DDL the driver deferred). The cut sits where the text is computed,
so all three take it. It is the same defect class and the same
mechanical call, in the claimed file, with the same gate family. It is
pinned on the deferred-DDL debug line.
- **The turso pin keeps no marker assertion.** The remote transport's
error is bare (no statement in front of the diagnostic), so the cut has
nothing to cut and adds no marker. The pin asserts the diagnostic, the
class of fault, no sentinel and no `statement:` field. Non-vacuity is
read off the sent statement the raw terminal was handed.
- **The process timezone.** Three live-matrix files require the process
zone to differ from UTC. Local runs set `TZ=America/New_York`, as CI's
live job does.

## Out of scope (reported, not edited)

- **`operatorFacingErrorText` (`@objectstack/types`) hands the raw
path's `cause` text back whole.** Measured on SQLite with a synthetic
sentinel bound into a raw statement: the envelope's message carries
none, and the helper's answer carries it. This is objectstack-ai#21418's (the family's
fourth position), which can now call the cutter from the same package.
Its docblock also says the driver writes the statement one line earlier,
which is no longer true.
- **Stale prose outside this surface.** These no longer match the
driver's lines:
- `packages/qa/dogfood/test/raw-statement-fault-redaction.test.ts` (its
header says the driver's raw line writes the statement);
- `sql-driver-diagnostic-value-probe.test.ts`'s rationale for not
importing the redactor (the module paths in its failure messages are
updated here).

---
_Generated by [Claude
Code](https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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

2 participants