Skip to content

fix(types): operatorFacingErrorText answers through the one driver-fault cut - #21482

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21418-operator-text-cut
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21418-operator-text-cut

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

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

claude added 3 commits October 2, 2026 21:23
…ult cut

WIP: the helper's single exit passes its answer through
redactStatementFromMessage; text reached below the raw-path sentence is cut
with statementSent, everything else asks the shared leak predicate.

Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-authored-by: Claude <noreply@anthropic.com>
WIP: enumeration pin of operatorFacingErrorText's callers, sentinel pins at
each caller's carriers (cli, metadata-protocol, metadata), the real-producer
leg in driver-sql, and the stale envelope prose corrected.

Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-authored-by: Claude <noreply@anthropic.com>
…ation field apart from the carriers

Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 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
  • 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 — 4 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 100c394f6fd0113377bccdabed6ecf3b04b720da → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 87de8854793865f656d4df3971c03a895b7f83f4 — the merge of head 9f5fba42f8352996539d1967a57c67fd90899395 into base 100c394f6fd0113377bccdabed6ecf3b04b720da, 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 87de8854793865f656d4df3971c03a895b7f83f4 && git checkout 87de8854793865f656d4df3971c03a895b7f83f4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 100c394f6fd0113377bccdabed6ecf3b04b720da 9f5fba42f8352996539d1967a57c67fd90899395 && git checkout -B drift-repro 100c394f6fd0113377bccdabed6ecf3b04b720da && git merge --no-ff 9f5fba42f8352996539d1967a57c67fd90899395

node scripts/docs-audit/affected-docs.mjs --json 100c394f6fd0113377bccdabed6ecf3b04b720da

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

The case loaded the Config inside its clocked window; with the package built
that load imports every command module and was the whole cost of the case,
past vitest's default budget on a loaded shard. It is now paid once during
collection. The assertions are unchanged.

Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 00:15
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit 85e29b8 Oct 3, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21418-operator-text-cut branch October 3, 2026 00:44
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/xl tests tooling

Projects

None yet

2 participants