Repository navigation
Commit d956910
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 #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.
- **Leg A: the helper ignores the flag.**
- Mutated: objectql 18 red / 300 green, and dogfood 6 red / 4 green.
- Red: every position-1 case. Green: the position-2 pins, every control,
the #21274 boundary suite and the lifecycle suite.
- In dogfood the six unlisted rows went red, and the three listed
controls and the Archiver stayed green.
- The marker was absent from all 14 built files.
- Restored: 318/318 and 10/10, with the marker present in 4 built files.
- **Leg B: the sweep's catch skips the helper.**
- Mutated: objectql 4 red / 314 green, and dogfood 1 red (the Archiver)
/ 9 green.
- The DTS emit failed on the now-unused import (TS6133). The JS bundles
built and carried the mutation, so the dogfood red shows it reached the
consumed artifact, and the marker was absent from all 8 JS files.
- Restored: 318/318 and 10/10, with the marker present and the tree
clean.
## 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](https://claude.ai/code/session_017xfMoEjKUuSh2xYB8sCozp)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8963dbf commit d956910
6 files changed
Lines changed: 935 additions & 10 deletions
File tree
- .changeset
- packages
- objectql/src
- lifecycle
- qa/dogfood/test
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
0 commit comments