Commit 85e29b8
fix(types): operatorFacingErrorText answers through the one driver-fault cut (#21482)
Fixes #21418
Clause-②: no
## What this changes
`operatorFacingErrorText`
(`packages/types/src/driver-error-classification.ts`) now returns cut
text by construction. Its single exit passes the answer through
`redactStatementFromMessage`, the one driver-fault cut in
`packages/types/src/driver-fault-redaction.ts`. That module is unedited
(empty diff), and no copy of the cut is made. No caller is edited.
This carries out the maintainer's ruling A on #21385 (`5950942037`),
quoted verbatim:
> **A: one cutter for every log face.**
> 平台任何一层的日志都不带调用方绑定值;一把刀、一处规则,覆盖所有日志面
It also carries out triage's ruling `5954287249`: "the helper returns
cut text by construction", with ⛔ no cut repeated at the callers.
The walk decides which rule the cut runs under. It goes by what the walk
knows about the text, not by what the text looks like:
- **Text reached below the raw-path sentence** is the fault of a
statement the driver itself sent. It is cut with `{ statementSent: true
}`. That is the same argument the driver's own raw-terminal log line
passes, so the operator's stored record equals the text of that line.
- **Every other answer** asks the shared leak predicate, as the engine's
own log line does. That covers an undeclared throw, a declared envelope
the walk does not unwrap, and the fallback channel. A driver dump is
cut. Anything else comes back unchanged, empty text included.
What survives:
- **In the returned text:** the dialect's own diagnostic, minus the
value slots the cut's templates own.
- **On the thrown value:** nothing is touched. Its `code`, `status`,
class and `cause` reach every classifier unchanged. For example,
`classifyIndexFailure` still answers `conflict`, and
`isMissingTableError` reads the same chain.
## Caller census (for the raise rule in `5954287249`)
The tree has **eight** caller files with **15** call sites. Triage and
the dispatch said "seven", but the list they gave has eight files. I
re-derived the list two ways:
- `git grep -ln` at `24db8a1c`, with the defining module and `index.ts`
as the control;
- the new enumeration pin's AST scan.
| Caller | Sites | Binds | Reading (from source) |
|---|---|---|---|
| `packages/cli/src/commands/db/clean.ts` | 1 | nothing | Two constant
statements, no parameters. |
| `packages/metadata-protocol/src/migrations/partial-index-probe.ts` | 2
| identifiers only | Unique-index DDL that its three callers compose
from platform table and column names and constant state literals. No
parameters. A unique index built over duplicate stored rows can carry a
stored value in MySQL's own duplicate-entry diagnostic. The cut's
templates own that slot. |
| `packages/metadata-protocol/src/migrations/read-probe.ts` | 2 |
identifiers only | The catalog statements inline the probed platform
table name, behind the probeable-name check. The fallback is the
caller's constant probe. |
| `packages/metadata-protocol/src/migrations/runtime-index-preflight.ts`
| 2 | identifiers only | A constant liveness statement, plus
duplicate-group reads over platform tables. No parameters. |
| `packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts` |
5 | **values, at 2 of the 5 sites** | See the breakdown below this
table. |
| `packages/metadata/src/migrations/drop-projection-tables.ts` | 1 |
identifiers only | A constant table list. |
| `packages/metadata/src/migrations/migrate-env-id-to-project-id.ts` | 1
| identifiers only | A constant table list. Its column probe binds table
and column names, but it swallows its own errors before any helper call.
|
|
`packages/metadata/src/migrations/migrate-project-id-to-environment-id.ts`
| 1 | identifiers only | Same shape as the row above. |
What the five `seed-tenancy-backfill.ts` sites bind:
- **Split probe:** binds the platform's global-tenant constant.
- **Organization probe:** binds nothing.
- **Collision probe:** binds nothing. It inlines object and field names
read from stored rows, behind an identifier check.
- **Stamp:** binds the organization id that the migration read from the
organization table.
- **Counter merge:**
- its first statement binds object and field names and the global-tenant
constant;
- the statements after it bind the organization id, counter values, a
key hash and the sequence scope.
**Reading for the seat:**
- **No caller binds a value taken from a request.**
- One caller, `seed-tenancy-backfill.ts`, binds stored values the
migration read itself, at its stamp and counter-merge sites.
- One of those values can come from a request caller's data. The
sequence scope is a rendered autonumber prefix, which a field-scoped
format renders from a record's field value, so a request caller
originally wrote it.
- What was pinned: the stamp site's own binding. The fixture composes
the dialect text from the statement and parameters the site really sent,
in knex's inlined shape. The organization id is the sentinel.
- Not measured: a real-driver run at that site.
- Applying the raise rule is the seat's call.
## Pins
- **The helper** (`driver-error-classification.operator-text.test.ts`,
new `[#21418]` block). A synthetic sentinel is bound into a raw
statement in four dialect shapes:
- a listed verb;
- a statement opening with a verb the predicate does not list, over a
diagnostic it does not recognise;
- a value inlined in both the statement and MySQL's diagnostic;
- a value inlined only in PostgreSQL's diagnostic.
Further cases cover a re-wrapped envelope, the depth bound, a string
`cause` and an undeclared driver dump. In every case:
- the fixture's `cause` is first shown to carry the sentinel;
- the answer carries none of it, and keeps the diagnostic;
- the answer equals the cutter's own answer for that text.
Two more cases complete the block. One pins that the thrown value is
untouched (code, status, class, `cause`, stack). A CONTROL case pins
that non-dump text comes back byte-identical.
- **The real producer** (`driver-sql`,
`sql-driver-16657-operator-facing-cause-text.test.ts`). It runs a real
`SqlDriver.execute()` refusal with the sentinel bound through knex, on a
statement the predicate cannot read. Three things are asserted:
- the `cause` carries the sentinel;
- the helper's answer does not;
- the answer equals the tail of the driver's own raw-terminal log line.
- **Every caller's carriers:**
- `metadata-protocol` and `metadata`'s
`raw-exec-operator-detail-16657.test.ts`, which have new `[#21418]`
blocks;
- the new
`packages/cli/src/commands/db/clean.operator-text-21418.test.ts`, which
runs the real oclif command and stays in the `unit` tier.
Each case scans the returned result and every recorded log line, message
and meta, for the sentinel. It also asserts that the diagnostic and the
site's verdict survive (`unreadable`, `conflict`, `absent`, `error`, the
exit code). One field is read separately: the backfill's receipt
declares the `organizationId` it adopted, by design and not through the
helper.
- **The enumeration pin**
(`driver-error-classification.callers.test.ts`, the `test:repo` project,
on its already-declared `packages/**/*.ts` radius). It freezes the eight
callers with their call-site counts. Each caller names its sentinel pin
file, and the test checks that file exists, imports the caller and binds
the sentinel. A positive control and a renamed-import check close its
two blind spots.
## Reverse verification
The fix was committed first (`24db8a1c`). Each mutation went through
`scripts/ablation-replace.mjs`:
- the anchor hit 1 → 0 and the blob changed;
- `@objectstack/types` was rebuilt;
- `ablation-dist-preflight` found the marker in 2 built files.
Each restore was proven: blob == HEAD and `git diff HEAD` empty. The
restore leg rebuilt, both markers were absent from all 12 built files,
and the tree was clean.
- **First attempt void.** The DTS build refused both mutations because
each left a binding unused. No pin ran. Both legs were re-run with
mutations that keep the binding read.
- **Leg A: the call to the cutter reverted, nothing else.**
| Package | Red / total |
|---|---|
| types | 12 / 22 |
| metadata-protocol | 17 / 25 |
| metadata | 6 / 11 |
| cli | 1 / 2 |
| driver-sql | 1 / 3 |
Every new sentinel case went red. Only the `[the fixture]` cases and the
CONTROL case stayed green. The `#16657` cases that now assert the cut
answer also went red.
- **Leg B: the cut kept, but what the walk knows dropped (predicate
only).** Red counts:
| Package | Red / total | Which cases |
|---|---|---|
| types | 2 / 22 | The unlisted-verb cell, and the re-wrapped and
depth-bound case built on it. |
| driver-sql | 1 / 3 | The real-producer leg. |
| metadata | 1 / 11 | A dialect text the predicate does not read as a
dump, so its statement came back whole. |
| metadata-protocol | 0 / 25 | Its fixtures open with listed verbs. |
| cli | 0 / 2 | Its fixtures open with listed verbs. |
So the walk's knowledge is load-bearing exactly where the predicate is
blind.
- **Restore:** 22/22, 25/25, 11/11, 2/2 and 3/3.
## Verification (at `24db8a1c` unless stated)
- **`@objectstack/types`:**
- `test`: 705 passed;
- `test:repo`: 11 passed;
- `typecheck`: exit 0. `--listFiles` includes both edited test files.
- **`@objectstack/metadata-protocol`:**
- The full suite ran at `89871414`: 3,091 passed and 3 failed, all 3 in
the new block. The cause was a fixture that read the receipt's declared
organization field as a carrier. It is fixed in `24db8a1c`.
- At `24db8a1c`: that file passes 25/25, and `typecheck` exits 0.
- **`@objectstack/metadata`:** the full suite passed 840 (56 files), and
`typecheck` exits 0.
- **`@objectstack/cli`:**
- The `unit` tier ran the new pin together with
`test/vitest-tiers-partition.test.ts`: 24 passed. The `integration` tier
is declared to CI.
- The full `typecheck` is NOT MEASURED, because the 60-package closure
was not built.
- A focused `tsc` over the new file found 0 errors in it. The 3 errors
it reported are in `clean.ts`, all missing declarations of unbuilt
workspace dependencies.
- **`@objectstack/driver-sql`:**
- the producer pin: 3/3;
- the `#21385` refusal-line pin: 16 passed, 2 live skips;
- the diagnostic value probe: 2 live skips (this PR changes only
comments there);
- `typecheck`: exit 0;
- live PostgreSQL and MySQL: NOT MEASURED here.
- **`objectql` and `rest`:** each edited test file passes, 13/13 and
4/4.
- **`qa/dogfood`:** this PR changes only a comment there, so the suite
is NOT MEASURED.
- **Gates:** `dispatch-gates.mjs --commands` derived 73 at `24db8a1c`.
- 69 ran and exited 0.
- `check-engine-split-ratio` first refused on the shallow checkout. It
exited 0 after the prescribed `--shallow-since` fetch.
- 4 are NOT MEASURED, each with prerequisite exit 3:
`check:dual-build-cjs-loads` (it needs every package's `dist`), and
`check:i18n`, `check:i18n-coverage` and `check:i18n-walk-parity` (they
need the built CLI). As a scoped check instead, the `types` dist loads
under both `require()` and `import`.
- The `--ran` reconciliation: 73 accounted for, 0 UNRUN.
- **Lint, by proven narrowing at `24db8a1c`:**
- eslint's own config, through `isPathIgnored`, ignores none of the 11
touched TS files;
- `--format json` counts 11 files, with 0 errors and 0 warnings;
- `eslint.config.mjs` enables no type-aware linting (no
`parserOptions.project`), and its only load-time reads are two baseline
JSON files this diff does not touch. So the diff cannot move any
untouched file's verdict.
The full `pnpm lint` is CI's.
## Stale prose carried from #21385's ACCEPT (`5957209724`), each
verified against the tree
| Position | Verdict |
|---|---|
| `operatorFacingErrorText`'s docblock ("the driver writes the
statement…") | stale, corrected |
| `metadata-protocol` `raw-exec-operator-detail-16657.test.ts`, raw
envelope copy | stale, corrected |
| `metadata` `raw-exec-operator-detail-16657.test.ts`, raw envelope copy
| stale, corrected |
| `rest` `package-door-16019-raw-statement-fault-code.test.ts`, raw
envelope copy | stale, corrected; its
`looksLikeInternalErrorLeak(COMPOSED) === false` assertion holds for the
new sentence |
| `types` `driver-error-classification.operator-text.test.ts`, raw and
read-exit copies (2) | stale, corrected |
| `objectql` `engine-find-missing-table-log-level.test.ts`, read-exit
copy | stale, corrected |
| `packages/qa/dogfood/test/raw-statement-fault-redaction.test.ts`
header | stale, corrected (the driver's line no longer writes the
statement) |
| `driver-sql` `sql-driver-diagnostic-value-probe.test.ts` rationale |
stale, corrected at its three places (the redactor's home and the "does
not depend" reason) |
All six envelope copies are now byte-equal to the producer's two
sentences. I measured this by evaluating each copy against
`sql-driver.ts`. The same stale premise was also in the headers of the
`types` operator-text test and the `metadata-protocol` test, and both
are corrected. These edits change test and comment text only.
## Acceptance notes
- **The census count:** eight files, not seven (see above).
- **A boundary, not filed.** An undeclared throw is cut under the shared
leak predicate, so an undeclared dump that the predicate cannot read
would come back whole. No producer reaches a caller that way today:
- every caller runs raw statements through `IDataDriver.execute`;
- the SQL drivers declare the raw-path envelope, `driver-sql` directly
and `driver-turso` through the same composition.
Carrier: none.
- **Comment drift, not filed.** The `@objectstack/types` entry in
`scripts/cross-package-test-inputs.mjs` describes
`driver-error-classification.callers.test.ts` as the
`isMissingTableError` gate only. The enumeration pin added here rides
the same declared radius. Carrier: none.
- **Engine-package test paths for the seat to declare:**
- test-only edits in
`packages/metadata-protocol/src/migrations/raw-exec-operator-detail-16657.test.ts`,
`packages/metadata/src/migrations/raw-exec-operator-detail-16657.test.ts`
and `packages/objectql/src/engine-find-missing-table-log-level.test.ts`;
- in `packages/drivers/driver-sql/src/`,
`sql-driver-diagnostic-value-probe.test.ts` (comment only) and
`sql-driver-16657-operator-facing-cause-text.test.ts`. The second is
outside the claim's listed surface: it is the real-producer leg.
There is no source edit in any `domain:engine` package.
## Patch round 1 (`9f5fba42f8`): the `os db clean` pin no longer pays
oclif's load inside its clocked window
*Added by the `domain:cli` seat from the dev's patch-round report on
#21418; the measurements are the dev's, on the shared 4-vCPU container.*
**The red:** on `24db8a1c`, Test Core (6/6) failed because
`clean.operator-text-21418.test.ts` › "the failure line names the file
and the dialect diagnostic, and carries no sentinel" timed out at
vitest's default 5000 ms.
**Where the time went.** Phase timers ran in a throwaway copy, which was
deleted and never committed. The command's own run took 11–119 ms. The
rest of the case was oclif's `Config.load`, which the case paid inside
its clocked window by handing `DbClean.run` a `{ root }`: 99.3–99.7% of
the case. On a built package with no `oclif.manifest.json`, that load
imports every command module.
| load | `Config.load` | the command's own run | the case on `24db8a1c`
|
|:--|--:|--:|:--|
| idle, 5 runs | 3214–3681 ms | 11–13 ms | 3427–3829 ms, passes |
| 8 busy loops, 3 runs | 8709–9712 ms | 29–57 ms | timed out 3 of 3 |
| 24 busy loops, 3 runs | 26017–36325 ms | 56–119 ms | timed out 3 of 3
|
**The fix (test file only).** The Config loads once at module scope and
is passed to `DbClean.run`, per AGENTS.md's rule "Clocked windows
measure behaviour, never loading" and the in-package precedent
`src/commands/datasource/envelope-unwrap.test.ts`. The assertions are
unchanged. There is no skip, retry, quarantine or timeout change, and
the file stays in the `unit` tier.
**Before / after, interleaved pairs, same command and load:**
| load | BEFORE (`24db8a1c`) | AFTER (`9f5fba42f8`) |
|:--|:--|:--|
| idle, 3 pairs | 3478–3556 ms, pass | 12–15 ms, pass |
| 8 busy loops, 3 pairs | 5021–5972 ms, 3 of 3 timed out | 24–44 ms, 3
of 3 pass |
| 24 busy loops, 4 pairs | 5068–5488 ms, 4 of 4 timed out | 100–300 ms,
4 of 4 pass |
The load moved into collection, which is not clocked. vitest's import
phase for the file went from 6.2–6.8 s to 9.6–10.4 s idle, and from
42.7–63.1 s to 64.9–94.2 s at 24 busy loops.
**Gates at `9f5fba42f8`.** The 73 families re-derived with no paths are
identical to round 0, and all 73 exit 0. `check:i18n*` and
`check:dual-build-cjs-loads` were NOT MEASURED in round 0; they were
measured green this round.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 15b29d3 commit 85e29b8
12 files changed
Lines changed: 1016 additions & 60 deletions
File tree
- .changeset
- packages
- cli/src/commands/db
- drivers/driver-sql/src
- metadata-protocol/src/migrations
- metadata/src/migrations
- objectql/src
- qa/dogfood/test
- rest/src
- types/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
Lines changed: 199 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
Lines changed: 33 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
84 | 117 | | |
85 | 118 | | |
86 | 119 | | |
| |||
Lines changed: 13 additions & 10 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
| |||
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
50 | 53 | | |
51 | 54 | | |
52 | 55 | | |
| |||
114 | 117 | | |
115 | 118 | | |
116 | 119 | | |
117 | | - | |
118 | | - | |
119 | | - | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
120 | 123 | | |
121 | 124 | | |
122 | 125 | | |
| |||
0 commit comments