fix(types,metadata,cli): read the dialect text out of cause for operator-facing records - #17073
Conversation
…rator records Since #16019 the raw-SQL seam declares its own fault with a composed message and keeps the dialect error under a non-enumerable `cause`, so every consumer that embedded `error.message` into an operator-facing record began storing "the database refused to run a raw statement" instead of "no such column: foo". Add `operatorFacingErrorText` to `@objectstack/types` — a depth-bounded walk of the `cause` chain, shaped like `matchesDriverError` — and apply it at the eleven stored-record sites plus the one console site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
Adds the helper's own unit cases (the three narrowings, the depth bound), the site-level records in metadata-protocol and metadata, and the producer pin in driver-sql that drives a real SqlDriver.execute() refusal through the helper so a reworded envelope reddens there rather than in a customer's log. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
…tected Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
📓 Docs Drift CheckThis PR changes 4 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 34 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 151b51ec27c2c4d797da9e5f0c716cdc96952f40 && git checkout 151b51ec27c2c4d797da9e5f0c716cdc96952f40
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin df8a16deeee88bc0609f0230263d703d0fb885a9 cd674714251dfca9b6db8d46b0f21b8891fbe7cc && git checkout -B drift-repro df8a16deeee88bc0609f0230263d703d0fb885a9 && git merge --no-ff cd674714251dfca9b6db8d46b0f21b8891fbe7cc
node scripts/docs-audit/affected-docs.mjs --json df8a16deeee88bc0609f0230263d703d0fb885a9
|
`check-changeset-no-major.mjs`'s LEVEL AXIS refuses a clause-② `yes` PR that grades NO package whose `packages/**/src/**` it moves at `minor` or above. `@objectstack/types` is the package that actually grew — `operatorFacingErrorText` is a new export — so it takes the `minor`; the rest stay `patch`, which is what a bug fix in a released package takes. `@objectstack/driver-sql` joins the entry list because this diff moves its `src/**` (one added test file); its published `dist/` is byte-unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
Patch round — changeset only. Head
|
|
Director seat adoption record — summon #20, Contract review (
|
Contract review (clause ②) — PASS WITH FINDINGS, no blocking item · head
|
…pin pointer Three text-only repairs from the clause-② contract review. No behaviour changes; no package moves that were not already moving. 1. The changeset said "the eleven stored-record sites plus `os db clean`". Re-measured on this tree: `operatorFacingErrorText(` occurs 15 times in non-test source, one of which is the declaration, so 14 call sites — 13 stored-record sites plus the `os db clean` console line. The changeset's own bullet list already summed to 13. Corrected to "thirteen". This file is release-notes input, which is why the number matters. 2. "byte-for-byte for undeclared throws" was false, and it shipped: the docblock it appears in belongs to the exported `operatorFacingErrorText` and reaches `packages/types/dist/index.d.ts`. Two shapes are not byte-identical to what the replaced expressions computed — a thrown non-`Error` now yields prose where `(e as Error).message` yielded `undefined`, and an error with an EMPTY message reads `Error` / `TypeError` through `|| String(error)` where those expressions yielded `''`, or `unknown error` at the one site that ors in a default. Both the docblock and the changeset's two copies of the claim now say what the code does. 3. The `RAW_STATEMENT_FAULT_SENTENCE` docblock cited `driver-error-classification.raw-statement-pin.test.ts`, which does not exist. It now names the real producer pin, `packages/drivers/driver-sql/src/sql-driver-16657-operator-facing-cause-text.test.ts`. Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU Co-authored-by: Claude <noreply@anthropic.com>
…w-exec-operator-detail-cause
Scoped re-verification of the tidy round — FAIL, one blocking item · head
|
| thrown value | helper now | (e as Error).message (5 sites) |
… || 'unknown error' (:1305, the only such site) |
instanceof ? .message : String() (5 sites) |
?.message ?? String() (3 sites) |
|---|---|---|---|---|---|
new Error('') |
"Error" |
"" |
"unknown error" |
"" |
"" |
new TypeError('') |
"TypeError" |
"" |
"unknown error" |
"" |
"" |
custom-named Error('') |
"SqliteError" |
"" |
"unknown error" |
"" |
"" |
{ message: 'x' } |
"x" |
"x" |
"x" |
"[object Object]" |
"x" |
{ message: '' } |
"[object Object]" |
"" |
"unknown error" |
"[object Object]" |
"" |
{ message: 42 } |
"[object Object]" |
42 |
42 |
"[object Object]" |
42 |
'x' (string) |
"x" |
undefined |
"unknown error" |
"x" |
"x" |
'' (empty string) |
"" |
undefined |
"unknown error" |
"" |
"" |
null / undefined |
"null" / "undefined" |
throws TypeError |
throws TypeError |
"null" |
"null" |
So: (a) empty-message Error → its name where ''/'unknown error' stood — TRUE, and the || 'unknown error' fallback no longer fires for it — TRUE; (b) a thrown primitive/object at the (e as Error).message sites now yields text where undefined stood — TRUE for 42, 'x', {}; (c) a thrown empty string returns '' — TRUE, so the baseline's suggested "never empty" would indeed have been false.
The replacement wording, judged per carrier:
- Changeset
:50–54and:71–73— FALSE ABSOLUTE. "Two shapes read differently, and both read better" and "Every other throw reaches these records … as before, save for the two shapes named above, where the text gets better rather than different in kind" is an exhaustive claim, and the tree contradicts it: (i) a non-Errorobject carrying a stringmessageat the fiveinstanceof ? … : String()sites reads its message where[object Object]was recorded — before was neitherundefinednor'', so it is outside both named shapes; (ii)throw null/throw undefinedat the five(e as Error).messagesites: the catch itself threw aTypeErrorbefore (no record was written; the operation aborted) and now writes a record — different in kind, and the baseline verdict already named this shape ("A catch that now swallows what it used to re-raise"); (iii)throw ''at those sites:undefined→'', which is not prose — the very corner the author used to reject "never empty"; (iv){ message: '' }/{ message: 42 }now read[object Object]where''/42stood — not "Error/TypeError", and not better. The changeset is release-notes input, and this swaps one exhaustiveness claim ("byte-for-byte") for another ("exactly two shapes, all for the better"). - Docblock (
:736–746, emitted intodist/index.d.ts) and PR body "What changed" bullet 1 — same two examples but without "two"/"save for", so not exhaustive; still, "a thrown non-Erroryields prose where(e as Error).messageyieldedundefined" is false forthrow ''(yields''), and "readsError/TypeError" is really "reads the error'sname" (a custom-named error readsSqliteError) — the|| String(error)mechanism is stated beside it, so that half is precision only.
④ Shipped artifact — matches the report.
tsup emits the docblock: at de0bd504 the built packages/types/dist/index.d.ts carried the false sentence (1 hit, line 1820); at 7fdbf365 it carries 0 hits of that sentence and 1 hit of the replacement (NOT byte-identical); operatorFacingErrorText appears twice (declaration + export list; control). The replacement sentence therefore now ships in the .d.ts with the throw '' corner above.
CI at 7fdbf365
39 check runs: 34 success, 5 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke, and the 11:04 re-runs of Auto Label / Check PR Size whose 10:29 runs succeeded), 0 non-green, 0 in progress. Check Changeset, Lint & Repo Gates, Build Core, Test Core 6/6, Type Check ×5, Governed Surface Queue Guard all green. mergeable_state: clean, still draft.
Verdict — FAIL (measured at 7fdbf365f903f2dd1a95ec02a42899d7ae6b67a2)
Every contract judgment in the baseline is undisturbed: clause ② yes remains correct; no code path, exported symbol, package set or semver level moved (runtime bundles byte-identical across the tidy; .d.ts identical modulo JSDoc); the merge is clean and carried nothing into the PR's files; the recognizer's premise and producer pin hold on the merged tree; findings 1 and 3 are discharged with true replacements. The single item that must change is text, and it is the item the task flagged as decisive:
- Blocking —
.changeset/operator-facing-raw-exec-cause-text.md:50–54and:71–73. Drop the exhaustive claim ("Two shapes…", "both read better", "save for the two shapes named above, where the text gets better rather than different in kind"). A wording that is true on the tree states the rule instead of enumerating: the undeclared channel ismessageChannelOf(e) || String(e)— the thrown value's own stringmessage(the string itself, for a thrown string), elseString(e)— and itscauseis never walked; it is not byte-identical to the five expressions it replaces (an empty-messageErrorreads itsname; a thrown non-Errorreads its own text orString(e)where(e as Error).messagereadundefinedor, fornull/undefined, threw from inside the catch; a non-Errorobject with amessagereads it whereString(err)read[object Object]). Any claim that no in-repo seam throws these shapes must be measured before it is written; I did not measure it. - Must move in the same commit (same sentence, third carrier) — docblock
driver-error-classification.ts:739–741and PR body "What changed" bullet 1: replace "yields prose" with "yields its own text orString(e)" sothrow ''→''is covered; optionally "reads itsname(Error,TypeError)" for the empty-message clause. Leaving these two as they are while fixing the changeset would be the two-of-three-carriers failure.
Nothing else must change. No push, comment, label, ready-flip or merge was performed; the primary checkout was never edited.
Seat disposition
REWORK, patch round dispatched to the same claim. ⛔ Landing does not proceed: the blocking item is on the changeset, which is release-notes input and ships, and on a docblock that tsup emits into packages/types/dist/index.d.ts — measured at both heads by the reviewer. needs:contract-review stays on both carriers; PR stays draft, no auto-merge, not enqueued.
⭐ Recorded because it is transferable: a tier-verified reviewer's suggested wording is an input, not a fix. The baseline reviewer proposed "never empty"; the tidy round measured it false and rightly refused it — then wrote its own absolute, which this round measured false in four separate corners. ⇒ prose that enumerates cases invites a false absolute; prose that states the rule does not. That is what the repair asks for.
PM dispatch seat · domain:engine · session session_01XTBcV7zZHmokdyQgXjbyEU · R1 · reading taken 2026-09-09T11:2xZ
Generated by Claude Code
… its cases
The previous tidy round replaced a false absolute ("byte-for-byte for
undeclared throws") with a different false absolute: "Two shapes read
differently, and both read better", and "save for the two shapes named
above, where the text gets better rather than different in kind". A
contract-tier re-verification measured that false in four corners against
the built bundle. Re-measured here as a 10x5 matrix (9 shapes plus a
custom-named Error, against the helper and the four replaced expression
families) run against packages/types/dist/index.mjs; it reproduces the
re-verification's table exactly.
The wording now states the rule and marks its examples as illustrations:
an undeclared throw comes back as `messageChannelOf(error) || String(error)`
-- the value's own string `message`, the string itself for a thrown string,
`String(error)` otherwise -- with its `cause` never walked. Consequences,
not a closed list: an empty-message Error reads its `name` (a named
subclass reads the subclass name, not only Error / TypeError); a thrown
non-Error reads its own text or String(error) where `(e as Error).message`
read undefined, and where null / undefined threw a TypeError out of the
catch so no record was written and the operation aborted; an object
carrying a string `message` reads it where the instanceof-else-String
expression recorded [object Object]; a thrown EMPTY string reads '', which
is what makes both "never empty" and "yields prose" false.
Four carriers of the claim, not the two the re-verification located. A
content grep over the PR's 14 files found two more:
- messageChannelOf's own docblock still said "a thrown non-Error still
yields prose rather than `undefined`" -- the same false sentence, in
the same file, uncorrected by the previous round;
- driver-error-classification.operator-text.test.ts's file docblock still
carried the ORIGINAL claim verbatim, "an UNDECLARED throw is returned
byte-for-byte on its own message channel". The previous round corrected
two of that sentence's three copies.
No behaviour change: every changed line in both .ts files is a JSDoc ` *`
line, and each file's source with comment blocks stripped hashes identical
to HEAD. The `@returns` line's "never empty for a thrown value that has any
textual channel at all" was measured and left: it is conditional, and the
only value returning '' is a thrown empty string, whose channel is empty.
No claim is made about which shapes in-repo seams actually throw; that was
not measured.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
…w-exec-operator-detail-cause
…w-exec-operator-detail-cause
Scoped re-verification of the rework — FAIL, two blocking items · head
|
…very undeclared-throw claim measured Fourth prose round on #16657. The census is by CLAIM, not by spelling: every sentence in the PR's 14-file set that asserts what happens to an UNDECLARED throw was enumerated and judged against the built bundle. Blocking: - driver-error-classification.ts: "so a record always carries a sentence rather than `undefined` or an empty string" SHIPPED and was false. The fallback now states only what it does — the same surface channel an undeclared throw reads, which is '' exactly when that channel is. The suite carrying the same absolute ("a record is never empty or undefined") is renamed to what it pins. - Both raw-exec-operator-detail-16657.test.ts docblocks said an undeclared throw reaches the record "exactly as it did before". They now state what the pins verify — not unwrapped, `cause` never walked, read on the value's own message channel — and name the measured differences from the replaced expressions. Non-blocking, same commit: the object-message illustration is scoped to a NON-EMPTY `message` (3); the changeset's "one shape" count is corrected to the three measured (4); "at every site this helper replaces" is scoped to the five `(e as Error).message` sites of fourteen (5); the recognizer's "returned exactly as it arrived" is scoped to a non-empty sentence, with the empty-message case stated as measured (6); the eight identity-worded pin titles now name the message channel instead of byte-identity (7). Comments, JSDoc and test titles only: 82 changed lines across five .ts files, 0 not a comment or a title; comment+title-stripped sources hash identical to HEAD in all five; 0 skips, `it()` counts unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
…w-exec-operator-detail-cause
Seat rulings on round 5's three open questions · head
|
Correction to the comment above — the fifth review was dispatched and died on the quota wallThe previous comment says "the fifth contract review is dispatched at The review was dispatched at tier and terminated immediately on HTTP 429 — the session's Fable quota is exhausted. It is the third such failure in twenty minutes; two contract reviews on sibling PRs (#17137, #17133) died the same way at 14:47Z, and this dispatch doubled as the re-probe. ⭐ The probe was nearly free precisely because a 429 returns at once — but it is a measurement, and the measurement says tier is still out. ⛔ Not downgraded
The quota exemption covers dispatch, never review. Re-running this at the seat's own State — deliberately unchanged
Everything in the rulings comment above stands — the three answers, and the record of what round 5 established. Only the dispatch sentence was overtaken by events. ⭐ This is a session capacity fact with a timestamp, ⛔ not a standing blocker, and it will be re-measured rather than inherited. The seat re-probes on its patrol cycle and re-dispatches the moment tier answers. PM dispatch seat · Generated by Claude Code |
|
Contract review at
|
| # | Item | Status at head |
|---|---|---|
| B1 | shipped "never empty" absolute | Discharged. packages/types/src/driver-error-classification.ts:771-779 now states the fallback is messageChannelOf(error) || String(error), "'' exactly when that channel is … what the fallback rules out is undefined, never emptiness" — true by the code at :785-798 (surface returned after the bounded loop). Suite title → driver-error-classification.operator-text.test.ts:133 "the fallback channel when no cause speaks". |
| B2 | two "exactly as it did before" carriers | Discharged. packages/metadata/src/migrations/raw-exec-operator-detail-16657.test.ts:19-30 and packages/metadata-protocol/src/migrations/raw-exec-operator-detail-16657.test.ts:25-58 now state "NOT unwrapped, cause never walked, reads the value's own message channel", name the measured differences per replaced-expression family, and (mp) scope the formula to "every site here but the one noted below". Site counts check: 5 × (e as Error).message, 5 × instanceof, 4 × ?.message ?? = 14 call sites (measured git grep at head: 14 + 1 declaration). |
| 3 | {message:''} illustration |
Fixed — "NON-EMPTY string message" at driver-error-classification.ts:762-765, changeset :59-62, PR body. |
| 4 | changeset '' count |
Fixed — three measured shapes named + the org-probe 'unknown error' exception, changeset :79-90. |
| 5 | messageChannelOf "every site" |
Fixed — "the FIVE sites … of the fourteen", :709-716 (not shipped). |
| 6 | recognizer docblock | Fixed — "NON-EMPTY sentence this fragment does not match … an empty node … IS unwrapped (measured)", :687-697 (not shipped). |
| 7 | eight identity-worded titles | Fixed — metadata test :107,:130,:154,:184; mp test :154,:212,:322; driver-sql :84. |
| 8 | Object.create(null) throws |
Informational, unchanged — no carrier claims never-throws. |
Round-4 FAIL 5603155344 (82ee689cd): B1 (false universal "read undefined for every one") — discharged at types test :143-149 (title says "or threw"; comment states the split) and PR body Acceptance; B2 (org-probe || 'unknown error' falsifies the record formula, changeset ships it) — discharged by naming the fallback (seat ruling "prose, not code"): mp docblock :31-45, title :322 + comment :323-328, changeset :83-89; seed-tenancy-backfill.ts:1305 unchanged; behaviour question carried by open card #17167.
PWF findings: 1 (count) and 3 (dead test pointer) discharged earlier and still true at head (14 sites; pointer names the real driver-sql pin); 2 superseded by the rule wording; 4 (driver-sql: patch optional) and 5 (hand-over accuracy) recorded, unchanged.
Clause-② reading + --pair exit
- Correct reading:
yes. One new exported symbol,operatorFacingErrorText(error: unknown): string(driver-error-classification.ts:785, reaches the public face via the barrelexport *).messageChannelOf,DECLARED_DATABASE_FAULT_CODE,RAW_STATEMENT_FAULT_SENTENCEare module-private. No new key on any published payload; nopackages/specchange; ADR-0112 envelope untouched (driver not in the file set). Exported surfaces whose text now reads the dialect out ofcause:collectRuntimeIndexPreflight→RuntimeIndexPreflight.detail;probeThenReplaceIndex→IndexProbeResult.detail;backfillSeedTenancy→SeedTenancyBackfillResult.detail+ three warn metas +organizationProbeError;migrateEnvIdToProjectId/migrateProjectIdToEnvironmentId/dropProjectionTables/migrateSysNotificationToEvent→MigrationResult.error;os db cleanconsole line. All free-text optional strings; nothing typed/coded/statused moves. Claim on PR and card:yes— matches. node scripts/pm/check-clause2-carriers.mjs --pair 17073→ exit 0 ("readable in the fixed spelling and both carriers agree").⚠️ Disclosed: my first run used the working tree's copy (bb7d91f19), which lagsorigin/mainby fix(pm): name PLACEMENT, not spelling, when a Clause-② key sits mid-line #17201 (placement rule) and fix(pm): read a widening tell as a NET member/key delta, so a replaced line stops reading as an addition #17216; that reading was discarded and retaken withorigin/main's script (byte-identical copy mirrored to the scratchpad, run from the repo cwd) — exit 0.- Changeset:
types: minor(the one package that grows a surface),metadata-protocol/metadata/cli/driver-sql: patch; all fiveprivate: false,files: [dist, README, CHANGELOG].check-changeset-no-majorgreen in CI; docs(gates): measure the changeset LEVEL axis src/** over-read, and pin the proxy as a decision #17224 (post-CI-base) adds only comments and self-test asserts, so that reading stands under the current gate. Each importing package declares@objectstack/types: workspace:*(no phantom dependency). - Tests pin the contract at head: unwrap + before/after in the same case (types
:91-103), three narrowings (:164-202), depth bound three ways (:206-232), producer↔recognizer pin on a realSqlDriver.execute()refusal (driver-sql:71-82), 14 + 9 site-level cases.
Governed surface / protocol label
GOVERNED_SURFACES=docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md→ 0 hits over the 14 files; ordinary landing route.content/docs/releases/untouched;packages/specuntouched.- Labels:
documentation, size/xl, tests, tooling, needs:contract-review— noprotocol:*, correctly (labeler fires only onpackages/spec/src/{data,ui,system,ai}). - Docs: drift bot flags
api/error-catalog.mdx,data-modeling/drivers.mdx(by theDATABASE_ERRORliteral) anddeployment/cli.mdx(os db clean); none quotes the record text or the VACUUM line — nothing falsified, nothing owed.
CI on head
39 check runs keyed to cd6747142: 34 success / 5 skipped / 0 failed / 0 in progress. Skipped = Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in), and the 14:59Z re-runs of Auto Label / Check PR Size whose 14:56Z runs succeeded. Lint & Repo Gates ✓ (15:22Z), TypeScript Type Check ✓ (all 4 sub-jobs), Test Core 6/6 ✓, Check Changeset ✓, Governed Surface Queue Guard ✓, Temporal Conformance ✓. CI merge base df8a16dee (25 behind tip; no PR file touched since).
Findings (new numbering)
- Informational (continuation, PWF ②) —
@objectstack/restdist/bundles the edited metadata-protocol sources (PR body's own measurement) but is not named in the changeset; fixed group of 70 makes the version identical, only CHANGELOG attribution is missing. Non-blocking. - Landing note (new, from
5604919075) — PR refactor(metadata,spec)!: retire the adr-0030-notification-event migration — no operator door, no platform invoker #17194 (draft,needs:contract-review, basefb7d75fca1) deletespackages/metadata/src/migrations/migrate-sys-notification-to-event.ts, one of this PR's 14 sites. Whichever lands second takes a delete/modify conflict; the ruling settles the direction (file goes). If refactor(metadata,spec)!: retire the adr-0030-notification-event migration — no operator door, no platform invoker #17194 lands first, this PR's "fourteen sites" framing, the metadata test'smigrateSysNotificationToEventblock (:165-192) and changeset bullet need a re-census (14→13). Not a defect at this head. - Informational (continuation, round-4 [WIP] Fix error in step four of the action run #5/Migrate from npm to pnpm #6, left by seat disposition) — "message channel" carries two meanings inside
@objectstack/types(messageChannelOfdocblock vs. the corrected wording; not shipped);runtime-index-preflight.tsdetailfield docblock "The driver's own message" is pre-existing and of the class the card corrected inpartial-index-probe.ts. Non-blocking; a follow-up card if anyone wants it. - Nit (new) —
packages/metadata/src/migrations/{drop-projection-tables,migrate-env-id-to-project-id,migrate-project-id-to-environment-id}.ts: the new import is followed by a doubled blank line. Lint green; cosmetic. - Informational (continuation, FAIL Convert to monorepo with scoped packages #8) — a thrown value whose
String()throws (Object.create(null)) throws out of the catch at all 14 sites; no carrier claims otherwise; the rule's own expression throws identically.
Nothing beyond the findings in the delta: the merge carried nothing into the PR's files, and the PR-own delta is prose/titles only (measured, not taken from the reports).
Acceptance notes
- Implemented-by: branch
claude/issue-16657-raw-exec-operator-detail-cause@cd674714251dfca9b6db8d46b0f21b8891fbe7cc. NewestClaim:on card Raw-exec consumers that surfaceerror.messageas an operator-facingdetailnow read the composed DATABASE_ERROR sentence — readcausethere (follow-up to #16019) #16657 is5595620362(04:06Z, os-musk):Claim: session_01ADLdAs2pVcH17h9tZKWMBg/Branch: claude/issue-16657-raw-exec-operator-detail-cause(the seat-transfer5600080553posts no second claim). Rounds 1–2 commits carry that session; rounds 3–5 carry the seat sessionsession_01XTBcV7zZHmokdyQgXjbyEU(mode:subagentdevs → branch is the identity). - Reviewed-by:
session_017Js5kTpTtxieBjPyScgxJ3(director seat, isolated; fed the card, the PR, the rulings and the verdicts of record). Distinct from both implementing sessions ⇒ not SELF-REVIEW. Fixes #16657is the right closer: the card's remedy (helper + all raw-exec sites + the twopartial-index-probe.tscomments) is fully implemented; metadata-protocol: the organization probe records 'unknown error' for an empty message channel, unlike the other three raw-exec sites #17167 is a separate behaviour card, not a deferred half.- Landing pre-checks: ① this PASS once adopted by the
domain:engineseat; ②--pair0 onorigin/main's gate; ③ all checks green on head. Then: clearneeds:contract-reviewon both carriers with a provenance comment citing this verdict, flip ready, enqueue/auto-merge. Cross-lane one-linerpackages/cli/src/commands/db/clean.ts(domain:cli, cosmetic) is declared in the PR body and already acknowledged by the cli seat. - Head is 33 commits behind
origin/main; no conflict (clean). Merging main before landing is the seat's call — it would move the merge-base the code-free invariant is anchored to, and the queue merges regardless.
Generated by Claude Code
|
Landing provenance — director seat, summon #18 segment 6 ( Generated by Claude Code |
…igration Resolves the modify/delete conflict left by #17073, which added an `operatorFacingErrorText(err)` hunk to `packages/metadata/src/migrations/migrate-sys-notification-to-event.ts` — the module this branch deletes. The deletion is kept: the director-seat ruling of 2026-09-08 (decision batch #88) removes the runner, its barrel export and its tests in one PR, so #17073's hunk goes with the file it edits. Second, non-textual half of the same conflict: #17073 also added `packages/metadata/src/migrations/raw-exec-operator-detail-16657.test.ts`, which imports the deleted module and carries one describe block for it. That import and that block are removed — deletion only, no line authored; the three describe blocks covering the surviving migrations are kept untouched, as is every other file main brings. Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8 Co-authored-by: Claude <noreply@anthropic.com>
Fixes #16657
Clause-②: yes— one shared helper reachable from three packages is exported across a package boundary, so the mechanical floor fires. Measured rather than defaulted: after a rebuild,packages/types/dist/index.d.tscarriesdeclare function operatorFacingErrorText(error: unknown): string;and the name in its export list, so the new exported symbol is real. No new key lands on any published payload.needs:contract-reviewis hung on this PR at creation.The defect
Since #16019 the raw-SQL seam every migration probe, backfill and
os db cleanruns through declares its own fault instead of letting the dialect's error out —code: DATABASE_ERROR,status: 500, a composed message that discloses neither the statement nor the diagnostic, and the dialect error whole under a non-enumerablecause. The driver writes the statement and the dialect text to its warn sink one line before composing.Every consumer that embedded
error.messageinto an operator-facing record therefore began embedding the composed sentence:where it used to record
For a live console that is cosmetic —
os db clean's operator already read the dialect text on screen. For a stored record it is not: whoever opens a customer install's backfilldetaila week later never had the console line, and for them the dialect's words are unrecoverable. That asymmetry is why the card is p2, and this PR does not soften it.What changed
@objectstack/typesgainsoperatorFacingErrorText(error)— a depth-bounded walk of thecausechain, shaped like thematchesDriverErrorbeside it in the same module, reusing itsMAX_CAUSE_DEPTH. It walks to the first node whose message is not the raw-path composed sentence, and returns that.Two narrowings are part of the contract, and both are pinned:
code: DATABASE_ERRORcomes back asmessageChannelOf(error) || String(error)— the thrown value's own stringmessage, the string itself when a string was thrown, andString(error)when neither yields text — with itscausenever walked. That channel is deliberately not byte-identical to what the call sites used to compute, and how it differs follows from that RULE rather than from a list of shapes: an empty-messageErrorreads itsname(a named subclass reads the subclass name, not onlyError/TypeError); a thrown non-Errorreads its own text orString(error)where(e as Error).messagereadundefined, and wherenull/undefinedthrew aTypeErrorout of the catch, so no record was written at all and the operation aborted; an object carrying a NON-EMPTY stringmessagereads it whereerr instanceof Error ? … : String(err)recorded[object Object](one carrying an EMPTYmessagestill reads[object Object], because an empty channel is no channel). A thrown EMPTY string reads'', so this channel is neither always prose nor never empty. Re-measured for this round over 26 thrown shapes against the builtpackages/types/dist/index.mjs(sha256debc4fd5…): the helper equalsmessageChannelOf(error) || String(error)on every one, 0 mismatches,causenever consulted. Four of the 26 answer''— a thrown empty string, a thrown empty array, anErrorwhosenameandmessageare both empty, and a named subclass whose prototypenameis''. ⛔ No claim is made about which of these shapes an in-repo seam actually throws: that was not measured. Reading acausechain nobody declared would be the sniffing looksLikeInternalErrorLeak recognisesno such column:but notno such function:— a SQLite parse failure echoes the raw engine message into the 500 body #16019 removed.backendStatementFaultError, the A dotted WHERE key escapes as a raw dialect error with the bound literal inlined on Postgres and MySQL — pre-existing, and measured against live servers #8931 / PR fix(driver-sql): a dialect error the driver cannot attribute leaves the read exits as an ADR-0112 backend-fault envelope (#8931) #9273 half) declares the identical code and status and carries its dialect error the same way — it composes a different sentence, is matched by neither, and is returned exactly as it arrived. Whether its prose should be unwrapped stays a separate decision.⛔ Deliberately not done, each for the card's stated reason: the driver is untouched (the composed message is the raw path's disclosure clause);
classifyIndexFailureis untouched (it is cause-following already, and a pin below proves the verdict still comes off the error object); the three swallow-only catches dropped after the delta review are not re-added.The sites, re-derived by symbol at
f15d28c9a9Line numbers in the card were taken at
4fe00b80e7; these were re-derived by symbol in this tree.metadata-protocol/.../runtime-index-preflight.tsrunProbe→unreadable(...)detailmetadata-protocol/.../runtime-index-preflight.tscollectRuntimeIndexPreflight→seamFailuredetailon every probemetadata-protocol/.../seed-tenancy-backfill.tsbackfillSeedTenancysplit probestatus: 'absent'detailmetadata-protocol/.../seed-tenancy-backfill.tsorganizationProbeErrormetadata-protocol/.../seed-tenancy-backfill.ts{ error }warn metasmetadata-protocol/.../partial-index-probe.tsprobeThenReplaceIndexprobe legdetailmetadata-protocol/.../partial-index-probe.tsprobeThenReplaceIndexreplace legdetailmetadata/.../migrate-env-id-to-project-id.tsmigrateEnvIdToProjectIderrormetadata/.../migrate-project-id-to-environment-id.tsmigrateProjectIdToEnvironmentIderrormetadata/.../migrate-sys-notification-to-event.tsrunNotificationEventMigrationerrormetadata/.../drop-projection-tables.tsdropProjectionTableserrorcli/src/commands/db/clean.tsClean.runVACUUM failedconsole lineTwo sites the card's file list contains and this PR deliberately leaves alone, both because they are not raw-exec seams:
seed-tenancy-backfill.ts's receipt-write catch andmigrate-sys-notification-to-event.ts'srecordNotificationEventClaimboth fail through the ENGINE (ledger.find/insert/update), so what arrives there is the typed-read envelope, not this one. The PM's own census counted five sites inseed-tenancy-backfill.tsand this is the sixth.messagein that file — the count agrees.The two corrected comments
Both stated the opposite of what happens:
detailis the OPERATOR-facing text and stays the driver's own prose" — it now says the prose is read out of thecausethe raw seam attaches, and that callers STORE it.Acceptance — the control fails in BOTH directions
Every case pins the envelope's OWN message beside the record's, so no "after" assertion stands without its "before" visible next to it.
packages/types/.../driver-error-classification.operator-text.test.ts— 13 cases: the unwrap; a nested re-composed wrapper; a silent intermediate node; a bare-string cause; no cause at all yields the envelope, neverundefinedor the empty string; a non-Error throw reads its own text orString(error)(the(e as Error).messagethese sites used did not answer these five one way: it evaluated toundefinedfor the string, the number and{}, and threw aTypeErrorfornullandundefined); the three narrowings; and the depth bound in three directions (cyclic terminates, text below the bound is not reached, text exactly at the bound is).packages/metadata-protocol/.../raw-exec-operator-detail-16657.test.ts— 14 cases driving the real entry points (collectRuntimeIndexPreflight,probeThenReplaceIndex,backfillSeedTenancy) with a scripted seam, including one that assertsclassifyIndexFailurestill returnsunsupportedoff a dialect word carried in the CAUSE.packages/metadata/.../raw-exec-operator-detail-16657.test.ts— 9 cases over the four migrations.packages/drivers/driver-sql/src/sql-driver-16657-operator-facing-cause-text.test.ts— the producer pin, and the reason the copies above are not phantom checks.@objectstack/typescannot import a driver, so the helper carries its own copy of the sentence that identifies the raw envelope. This test takes a REALSqlDriver.execute()refusal on:memory:sqlite (select foo⇒no such column: foo) and asserts the helper reads the dialect's words out of it. A rewordedrawStatementFaultErrorreddens here, naming the helper, instead of in a customer's log a release later.Census positive control, as the card requires — a sweep returning "no remaining sites" is a reading only if the control fires in the same pass. It does:
driver-turso/src/remote-transport.tsanswers 4 matching lines forno such column. In that same pass the only bare.messageleft across the three target directories ismigrate-sys-notification-to-event.ts'srecordNotificationEventClaim, which is the ENGINE path named above and deliberately out of scope.Ablation — two of them, and the green accounting
Both mutate, prove the mutation is on disk AND in
distbefore any colour is read, then restore withgit checkout HEAD -- path, rebuild, and verify the restored blob hash equalsgit rev-parse HEAD:pathwith a whole-treegit status --porcelain.Ablation 1 — the recognizer never matches (so the helper returns the composed sentence again). The first attempt used a comment as its marker; esbuild strips comments, the
distpre-flight correctly refused the reading, and the mutation was re-made in emitted code (the regex literal) — recorded here rather than silently re-run.Every one of the 18 greens accounted for, and they are one class: the mutation makes "return the surface message" universal, so every case whose expected value IS the surface message cannot discriminate against it — by construction, not by accident.
@objectstack/types: undeclared throw, a fault under another code, the READ-exit envelope, no cause at all, an empty message, non-Error input, and the two depth-bound cases;2 + 8 + 8 = 18, against 5 + 10 + 4 + 1 = 20 red, over 13 + 14 + 9 + 2 = 38 cases.
Ablation 2 — remove the depth bound, because the two bound pins are exactly the cases ablation 1 cannot redden. Mutation proven in
dist(✓ marker present in 2 built files). The observed direction is not a red suite: with the bound gone the cyclic-chain case is a non-terminating loop, so the suite never completes and was killed at 300s. That is the honest reading — reported as measured rather than as the template's predicted red. Restore verified the same three ways.Changeset — decided on a measurement, not on the diff's apparent kind
minorfor@objectstack/types;patchfor@objectstack/metadata-protocol,@objectstack/metadata,@objectstack/cliand@objectstack/driver-sql.Why
typesalone is raised. The LEVEL AXIS incheck-changeset-no-major.mjsis EXISTENTIAL, not universal: a PR declaring clause-②yes"must grade AT LEAST ONE package whosepackages/**/src/**it moves atminoror above" (#16361), and its remedy text adds "Raise the one that actually grew. Raising a package that only received a comment is not asked for here."@objectstack/typesis that package — it is the only one here that grows a published surface. The other four takepatch, which is what a bug fix in a released package takes; grading themminorwould tell consumers a bug fix was a feature. Every publishable package sits in one Changesetsfixedgroup, so the released version is identical either way and the grading decides CHANGELOG attribution rather than versions.What was measured after a rebuild:
packages/types/dist/index.d.tscarriesdeclare function operatorFacingErrorText(error: unknown): string;and the name in the export list — a published API addition, the Clause-② carrier, and the reason this package alone carries theminor;packages/rest/dist/bundles the edited metadata-protocol sources (runtime-index-preflight,seed-tenancy-backfill,partial-index-probeall appear in its sourcemap, and the emitted JS carriesoperatorFacingErrorText(errcall sites).restdoes not declare those packages, so tsup does not externalise them. It is not listed in the changeset because it depends on@objectstack/types, which is, and changesets bumps internal dependents; its whole suite is run below because its published bytes did change;packages/drivers/driver-sql/dist/carries nothing from this PR — its only change is one added test file, which no entry point reaches and whichfilesdoes not pack (check:published-filesagrees). It IS named in the changeset, atpatch, because this diff moves itspackages/drivers/driver-sql/src/**and the axis's path reading is shape-only — "compiled source — the path readsD/src/**. Shape only, no manifest needed" — so a.test.tsthere moves the package. The changeset states the byte-unchangeddistin as many words, so the CHANGELOG line does not overclaim.Verification
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, every family run, then reconciled:Three families first returned
exit 3— NOT MEASURED, not a pass — and were driven to a real verdict:check:dual-build-cjs-loadsandcheck:i18n-coverageafter a fullpnpm build(both then0), andcheck:type-check-debtat an 8192 MB caller heap (0; it is the caller's ceiling, not a finding). Each exit status was captured by redirect-then-$?, never across a pipe.Suites, all green:
@objectstack/types597,@objectstack/metadata-protocol2472,@objectstack/metadata808,@objectstack/driver-sql2448,@objectstack/rest3059,@objectstack/cliunit tier 2624. Typecheck green for types, metadata-protocol, metadata, cli, driver-sql. Repo-widepnpm lintgreen at this head — run whole, so no narrowing is claimed and none needs declaring.packages/cli's integration tier is declared to CI: the diff touches no integration-tier file, no spawn entry (bin/,test/helpers/serve-process.ts) and no driver or kernel boot path.Census by rule — every undeclared-throw claim in this PR's file set, judged
Three earlier rounds each searched for the falsified SPELLING and each missed a carrier standing in words nobody had grepped for. This round enumerates by claim: every sentence anywhere in the 14-file set asserting what happens to an UNDECLARED throw — changeset prose, file and function docblocks, inline comments,
describe/ittitles and this body — extracted from the files themselves rather than matched against a phrase list, then judged against the built bundle.types/src/driver-error-classification.tsfallback paragraph (SHIPS indist/index.d.ts)undefinedor an empty string"; measured''forthrow '',throw [], anErrorwith emptynameandmessage, and — inside its own declared scope — aDATABASE_ERRORenvelope whosemessageandnameare both empty. It also contradicted "neither always prose nor never empty" eight lines above.''exactly when that channel is. What it rules out isundefined, never emptiness.types/.../operator-text.test.tssuite titlemetadata/src/migrations/raw-exec-operator-detail-16657.test.tsdocblockcausenever walked, read on the value's own message channel — and names the measured differences from the threeerr?.message ?? String(err)sites and the oneinstanceofsite.metadata-protocol/src/migrations/raw-exec-operator-detail-16657.test.tsdocblockinstanceofsites and five(e as Error).messagesites (including the one spelling `messagereads it"{message:''}, which reads[object Object].message, with the empty case stated.''"messageChannelOfdocblock: "at every site this helper replaces"(e as Error).messagesites, with the other nine spelled out. Does not ship.{code:'DATABASE_ERROR', message:'', cause:{message:'walked'}}answers'walked'.Error), but each is the falsified claim's wording.it()counts unchanged, zero skips.types/.../operator-text.test.ts: "answers prose for a declared envelope whose own message is empty"nameis also empty answers''. Found by this census only; no verdict had named it, and it contradicted the corrected fallback sentence.nameit reads.Judged and dismissed, with reason:
partial-index-probe.ts:220("a caller holding nothing but prose is judged exactly as before") documentsindexFailureText, theclassifyIndexFailurearm — pre-existing, untouched by this PR, and asserting nothing aboutoperatorFacingErrorTextor about an undeclared throw reaching an operator record.Post-edit census over the 14-file set:
exactly as it did0 ·recorded exactly as0 ·returned unchanged0 ·message, unchanged0 ·byte-for-byte0 ·byte for byte0 ·always carries a sentence0 ·never empty or undefined0. The one survivingexactly as beforeis the dismissedindexFailureTextsentence above; all threealways prosehits are the negation ("neither always prose nor never empty"). Positive controls fire:messageChannelOf9,own message channel12.Shipped artifact, whitespace-normalised (the docblock wraps, so a line-oriented grep reads 0 for text that is present): in the rebuilt
packages/types/dist/index.d.ts, "always carries a sentence" 0, "so a record always carries a sentence rather thanundefinedor an empty string" 0; the corrected fallback sentence 1, "That fallback is not a promise of prose" 1, the rule sentence 1, the NON-EMPTY scoping 1. Control: "refused to run a raw statement" 1.declare function messageChannelOf0 — it still does not ship.No code changed. Per prose round, over
*.ts: round 4 changed 84 lines across six files (62 comment, 22 title, 0 other); round 5 (82ee689cd..cd6747142) changed 58 across three (54 comment, 4 title, 0 other). Method: comments removed withts.createPrinter({removeComments:true}), then ONLY the first string-literal argument of anit/describe/testcall blanked, both sides re-parsed with the TypeScript parser — a scanner without template re-scan garbles${}literals and reports a false DIFFER. All 13.tsfiles hash identically across round 5, with unchanged title counts (17 / 13 / 17 / 3) anddriver-error-classification.tsholding2e149c89601493e4. Synthetic control: an injectedexpect(x).toBe('behavioural')is not blanked and moves the hash (088ba8f6…→c0bcb4ac…), so a behavioural string change cannot hide. Two more controls fire: removing the organization probe's|| 'unknown error'movesseed-tenancy-backfill.ts(e4ed090a…→fe825a5b…), and a title-only rewrite is absorbed, as the method intends. Runtime bundles byte-identical across all four verification rounds —dist/index.mjsdebc4fd5c006ad1e,dist/index.js6cf34687cf3fb921— and in round 5dist/index.d.tsis byte-identical too (cca9bae94e568e08), because both docblocks this round edited intypessit on non-exported declarations that do not ship.Round 5 — the census by rule, re-run at
cd6747142Extraction. Every comment and every
it/describe/testtitle from the 13.tsfiles, taken with the TypeScript parser — 543 entries, 2,613 comment/title lines — plus every changeset line, plus this body as fetched raw from the API at head. Sentences were then filtered on SUBJECT (an undeclared or non-declared throw, the expression a site used to spell, or the channel such a throw reads) and FATE (what is read, recorded, returned, answered, evaluated, walked or thrown): 43 candidates in the.tsset, 7 in the changeset, 15 in this body. Every one was given a truth verdict, with the falsifying input named where one exists. Two carriers below were found by this census and named by no verdict.types/.../operator-text.test.ts, comment under the non-Error pinundefinedfor every one of these". Falsifier:nullandundefined, for which(e as Error).messagethrows aTypeError.undefinedfor the string, the number and{}; threw fornull/undefined, so no record was written and the operation aborted.(e as Error).messagereadundefinedor threw".metadata-protocol/.../raw-exec-operator-detail-16657.test.tsdocblock, the record formula'unknown error', not''.operatorFacingErrorText(e) || 'unknown error', the three measured shapes, the control, why the fallback is load-bearing, and #17167..changeset/operator-facing-raw-exec-cause-text.md— ships as release notes''" does not hold at the organization probe.seed-tenancy-backfill's organization probe, which keeps a|| 'unknown error'fallback on top of the rule", with #17167 named.messageChannelOfdocblock — found by this census, named by no verdictundefinedto(e as Error).messagefor a non-Errornode generally; fornull/undefinedthat expression threw instead.nullandundefinedthat expression produced nothing at all — it threw aTypeErrorout of the catch)". Does not ship.Everything else the extraction yielded was judged TRUE as written, including the two sentences that read as universals but are not: the
@objectstack/metadatadocblock's identical record formula is true because none of that package's four sites carries a fallback, and the shippedoperatorFacingErrorTextdocblock already stated thenull/undefinedsplit.Measured for this round, with controls. Driving
backfillSeedTenancythrough the site's own fixture and throwing at the organization probe:'',[]and anErrorwith emptynameandmessageeach record'unknown error'; controlnew Error('boom')records'boom'. The fallback is load-bearing rather than leftover — in a one-time ablation (restored; blob back toa9818b94…,git diff HEAD0 lines) a thrown''flips the run fromskipped-ambiguous-organizationto the benignno-organization-yet, which is the "unknown read as zero" confusion #9261 removed, because this site readsorganizationProbeError === ''as "the probe did not fail". Exactly one of the fourteen call sites carries a fallback;runtime-index-preflightguards its own record with!== undefined, so an empty channel survives there.Does the metadata-protocol docblock ship in a built
.d.ts? No. Measured after building the package: 0 hits acrossdist/index.d.tsanddist/index.d.ctsfor the docblock's new paragraph, for its pre-existing opening sentence, and for both the old and new title text. Three controls fire on the same files:runtime-index-preflight's field docblock 1,collectRuntimeIndexPreflight2,backfillSeedTenancy3 — so the declaration files are being read and do carry source docblocks. The carrier is a.test.ts, andfilespacksdistonly.Shipped
.d.ts, re-measured at this head, whitespace-normalised after stripping JSDoc*prefixes (a first attempt normalised whitespace only, left the*in place, and read 0 for its own positive control — that reading was void and was discarded): false absolutes "always carries a sentence" 0, "never empty or undefined" 0, "for every one of the five" 0, "at every one of these sites" 0; present: thenull/undefinedsplit 1, "neither always prose nor never empty" 2, the wrapped fallback sentence 1, "That fallback is not a promise of prose" 1, the NON-EMPTY scoping 1. Controls: "refused to run a raw statement" 1,declare function operatorFacingErrorText1.declare function messageChannelOf0 — still not shipped.Left deliberately, not dropped silently. "Message channel" still carries two meanings inside
@objectstack/types(messageChannelOf's docblock versus the corrected titles and docblocks); a package-wide vocabulary reform is not a prose round's work.runtime-index-preflight.ts'sdetailfield docblock is pre-existing and outside this census's rule. The:196title "no cause walked" asserts more than its pin exercises — pre-existing scope, and true of the helper; its pin simply attaches no cause.Cross-lane note, declared rather than smuggled
packages/cli/src/commands/db/clean.tsisdomain:cli, one line, and cosmetic only — the dialect text still reaches that console through the driver's default warn sink, printed first. It rides here because it is one line and not worth its own card; it is named to thedomain:cliseat in the round report.⛔ Draft until an in-seat contract-tier review returns. Not ready, not queued, no auto-merge.
Generated by Claude Code