fix(types,metadata,cli): read the dialect text out of cause for operator-facing records - #17073
fix(types,metadata,cli): read the dialect text out of cause for operator-facing records#17073os-musk wants to merge 14 commits into
cause for operator-facing records#17073Conversation
…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
…, not "prose" The by-rule census caught one more carrier of the "always prose" half of the claim, in a title no spelling-census would have matched: "answers prose for a declared envelope whose own message is empty". Measured false in general — a declared envelope whose `message` AND `name` are both empty answers '' — and it contradicted the fallback sentence corrected in the previous commit. True of its own pin, which reads `name` = 'Error'; the title now says that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Scoped re-verification, round 4 — FAIL, two blocking items · head
|
| # | Verdict | Measured basis |
|---|---|---|
| 1 fallback paragraph (ships) | TRUE | surface is the value returned after the loop; '' exactly when it is; declared Error with both empty → ''. |
2 describe title |
TRUE | descriptive, claims nothing. |
| 3 metadata docblock | TRUE | the four sites are exactly operatorFacingErrorText(err); Error('') ''→'Error', {message:42} 42→'[object Object]', {message:'x'} at the instanceof site '[object Object]'→'x'. |
| 4 metadata-protocol docblock | before/after statements TRUE; the general record formula FALSE at one of its own sites | finding 2 below. |
| 5 NON-EMPTY scoping (ships) | TRUE | {message:''} → '[object Object]'. |
| 6 changeset count | TRUE as hedged ("the ones measured") | three named shapes all measured ''. |
7 messageChannelOf docblock |
TRUE | removed lines of the base..head diff over the nine site files: (e as Error).message 5, instanceof Error ? … : String() 5, ?.message ?? 4 = 14. |
| 8 recognizer docblock | measured clause TRUE (→ 'walked'); the general clause has a new corner |
finding 3. |
| 9 eight titles | true of their pins (each throws a non-empty Error) |
but "at every one of these sites" is false at one site — finding 2. |
10 :153 title |
TRUE ('Error'); confirmed named by no verdict: round-2 verdict 0 hits for the phrase; round-3's single hit is inside its finding 6 (the recognizer docblock), and round 3 named :133 and :143, not :153. |
|
— the :143 title, also rewritten this round |
clause "where (e as Error).message read undefined" is FALSE for two of its own five pins |
finding 1. |
② Is the by-rule census COMPLETE? — No. One uncorrected carrier (an eleventh) and one site-level exception the census did not reach.
Method (mine, not the author's): every comment and every describe/it/test title extracted from the 14 files with the TypeScript parser (1,597 comment/title lines) plus all 72 changeset lines; the four test files and the changeset read in full; the nine source files concept-filtered (176 hits, all read); the comment adjacent to each of the 14 helper call sites read. I did not grep for the phrases the verdicts listed.
Eleventh carrier — packages/types/src/driver-error-classification.operator-text.test.ts, the comment directly under the :143 pin: "(e as Error).message — the expression this helper replaces at five sites — evaluates to undefined for every one of these." The five values pinned beneath it are 'no such column: foo', 42, undefined, null, {}. Measured: the expression throws a TypeError for null and undefined; it reads undefined only for the other three. Present unchanged since the file was added; not in the author's ten. The same false universal stands in the PR body's Acceptance section ("evaluated to undefined for every one of the five pinned values") and, as a clause, in the :143 title rewritten this round. All three contradict the shipped helper docblock and both site-test docblocks, which say null/undefined "threw a TypeError out of the catch" — a distinction this PR itself made load-bearing.
Site-level exception — packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts, the organization probe: the PR kept organizationProbeError = operatorFacingErrorText(e) || 'unknown error' (the only surviving 'unknown error' in the file). Measured by driving backfillSeedTenancy with the site test's own seamExec fixture and throwing at the ORGANIZATION_TABLE probe: '' → 'unknown error', [] → 'unknown error', Error with name ''/message '' → 'unknown error' (control new Error('boom') → 'boom'; the pinned collision-probe site with throw '' → ''). So at that site, for an empty channel, the record is not messageChannelOf(error) || String(error). Three sentences are false there: the metadata-protocol docblock "the record reads the thrown value's own message channel, messageChannelOf(error) || String(error)" (the docblock names this site's || 'unknown error' only in its BEFORE state); the title "an UNDECLARED refusal reads its own message channel at every one of these sites" (whose pin exercises only the collision-probe site); and the changeset, which ships as release notes, :79-84: "Every other throw now reaches these records through the rule above rather than through the expression each site spelled out … some shapes still record ''".
The dismissal is right. partial-index-probe.ts "judged exactly as before" (:220): present at base (1 hit), 0 +/− lines in the PR's diff to that file, documents indexFailureText — the classifyIndexFailure arm — untouched by the PR. Not a carrier.
Judged and not counted as carriers: the :160 title "leaves an UNDECLARED throw exactly as its message channel reads" (true under the vocabulary the corrected sentences define, "own message channel" = the rule; but the same package's messageChannelOf docblock defines "message channel" as the node's own message, under which new Error('') reads '' and the helper answers 'Error'); runtime-index-preflight.ts detail?: string field docblock "The driver's own message, when the probe could not run" (pre-existing, the same class as the two corrected partial-index-probe comments, asserts nothing specifically about undeclared throws); the @returns conditional (true under its own definition of channel; a contrived corner — an Error whose toString returns '' but whose name is 'Error' answers '').
③ Code-free? — Yes, measured. The count in the PR body is 82; the truth at head is 84.
Diff of the two PR commits over *.ts: 84 changed lines — 62 comment (^[-+]\s*(\*|//)), 22 title (^[-+]\s*(it|describe)\(['"]), 0 other. The PR body's "82" is the count before 82ee689cd added two.
Title-blanking method, verified with the TypeScript parser (not the scanner): comments removed by ts.createPrinter({removeComments:true}); only the first string-literal argument of an it/describe/test call is blanked. driver-error-classification.ts comment-stripped hash identical across the round (2e149c89601493e4); the four test files' comment-stripped-and-title-blanked hashes identical with title counts 17 / 13 / 17 / 3 on both sides (equal to the author's); the comment-stripped line diffs list exactly the 11 title lines and nothing else. Synthetic control: an expect(x).toBe('behavioural') string is not blanked, so a behavioural string change cannot hide. Controls that fire: sql-driver.ts across the same range DIFFER (upstream code), the helper file base..head DIFFER (real code added). Disclosed: my first attempt used ts.createScanner without template re-scan, which garbles ${} template literals and produced a false DIFFER on the metadata-protocol file — discarded for the AST method; the author did not disclose which stripper produced their hashes, but their result agrees with the sound method.
Dist swap: with the 97ef74195 blob (b76ed87d0c) of the helper file, dist/index.mjs debc4fd5c006ad1e and dist/index.js 6cf34687cf3fb921 — byte-identical to head; dist/index.d.ts dbead20aa192520f → cca9bae94e568e08, 14 changed lines, all JSDoc (control). Restored blob f212917603 = HEAD:path; git status --porcelain empty.
④ Shipped artifact — verified by content, whitespace-normalised.
Rebuilt packages/types/dist/index.d.ts (cca9bae94e568e08): exact false absolute 0, "always carries a sentence" 0, "never empty or undefined" 0; corrected fallback sentence 1, "That fallback is not a promise of prose" 1, "rules out is undefined, never emptiness" 1, rule sentence 1, NON-EMPTY scoping 1, EMPTY-message clause 1, "neither always prose nor never empty" 2. declare function messageChannelOf 0 and absent from the 80-name export list (operatorFacingErrorText present); the recognizer and messageChannelOf docblock phrases 0. Control: "refused to run a raw statement" 1; the line-oriented grep of the wrapped fallback sentence reads 0 exactly as the author warned. The .d.ts built from the previous blob carried the absolute (1) — the removal is real.
⑤ Coverage — none lost.
it() counts 13 / 9 / 14 / 2 and describe() 4 / 4 / 3 / 1 unchanged; .skip/.only/.todo/.each/xit/xdescribe 0 in all four (control 2). All four files run green under the new titles in the scratch tree: 13, 2, 9, 14 passed, exit 0. Each renamed pin asserts the record equals the thrown non-empty Error's message — the channel the new titles name. Two titles claim more than their pin exercises: "no cause walked" (metadata-protocol :196; the pin attaches no cause) and "at every one of these sites" (one site exercised). Pre-existing scope; non-blocking.
⑥ Delta — clean, invariant, premise undisturbed.
Merge-base moved 0da638cd9 → 06d38fb92. Change set 1,351 lines, sha256 356e23cf2fbce0eb at a0f5b4612 (vs old base) and at 94001833e (vs new base); diff of the two is empty; head differs only by the one-line rename (f820600246d08a27, still 14 files). git merge-tree --write-tree a0f5b4612 06d38fb92 = b77c10c8… = the merge commit's tree; diff-tree --cc prints the SHA only; ls-files -u 0; conflict markers 0 over the 14 files (control 2). Upstream 0da638cd9..06d38fb92: 4 commits, 0 touch PR files; 9cdffbe36 (#16887) touches sql-driver.ts — 74 hunk lines mention NUMERIC/representation (control), 0 mention the fault composition; rawStatementFaultError's body is byte-identical (md5 6f06a479…) at 0da638cd9 and head; producer sentence sql-driver.ts:1009, recognizer :700, index.ts:50 export; producer pin 2/2 on the merged tree. os-regen-merge.sh reported nothing to regenerate and the diff touches no generated artifact — consistent.
⑦ Gate honesty — acceptable, and corroborated here.
The misread: pnpm --filter @objectstack/types test --concurrency=2 reproduces CACError: Unknown option --concurrency, exit 1, no test ran — reading that as NOT MEASURED is right (an invocation failure before vitest parsed anything; neither pass nor red). Exit 3 is documented PREREQUISITE NOT MET in all six scripts, distinct from a finding's 1. Treating them as NOT MEASURED was acceptable: not claimed as passes, and their inputs are provably unmoved by a comment-and-title diff (runtime bundles byte-identical; .d.ts JSDoc-only). Corroboration, not a claim on the author's behalf: after a full package build in the scratch worktree (exit 0), all six ran to real verdicts — check:dual-build-cjs-loads, check:i18n, check:i18n-coverage, check:i18n-walk-parity, check:lean-entry-closure, check:type-check-debt each exit 0 by redirect-then-$?, 0 PREREQUISITE lines, OK verdict lines present. CI at head: 39 runs, 34 success, 5 skipped, 0 non-green.
Verdict — FAIL (measured at 82ee689cdb2aafb8e4a478ef0afe28b0ed88eb4d)
Everything substantive holds: the rule is TRUE on every shape thrown, the runtime bundles are byte-identical across the round, the shipped .d.ts carries the corrected sentences and none of the struck ones, no pin lost coverage, the merge is clean and carried nothing, the premise and the producer pin hold, the gates were honestly reported and corroborate. What fails is the standard this round was sent to meet — every sentence true, every carrier found — on the axis the PR turns on:
- Blocking — a false universal about undeclared throws stands in three carriers, one of them written this round.
packages/types/src/driver-error-classification.operator-text.test.ts, the comment under the:143pin ("evaluates toundefinedfor every one of these"), the:143title's clause ("where(e as Error).messagereadundefined"), and the PR body's Acceptance sentence ("for every one of the five pinned values"): measured false fornullandundefined— the expression throws aTypeError, which the shipped docblock and both site docblocks say in as many words. Must state the split (readundefinedfor the string, number and{}values; threw fornull/undefined) or drop the universal, in all three. - Blocking — the organization-probe site's
|| 'unknown error'falsifies the record formula, and the changeset ships it. Measured:'',[]and anErrorwith emptynameandmessagethrown at that probe record'unknown error', not''. Must move together: the metadata-protocol docblock's after-state (name the site's surviving fallback), the "at every one of these sites" title, and.changeset/operator-facing-raw-exec-cause-text.md:79-84("through the rule above rather than through the expression each site spelled out … some shapes still record''"). Removing the fallback instead is a code change and the seat's decision, not a prose round's. - Non-blocking — recognizer docblock: "composes a DIFFERENT, NON-EMPTY sentence is returned exactly as it arrived — it speaks at depth 0, so the walk stops on it" is false for a different, non-empty sentence that embeds the fragment (measured: with a cause →
'walked'); the struck wording "does not carry this sentence" was exact on this axis. One clause: "a NON-EMPTY sentence this fragment does not match". Does not ship. - Non-blocking — PR body "82 changed lines": 84 at head.
- Non-blocking — "message channel" carries two meanings in one package (
messageChannelOf's docblock vs the corrected titles/docblocks); the:160title is true only under the second. - Informational —
runtime-index-preflight.tsdetailfield docblock "The driver's own message" is the class the card had corrected inpartial-index-probe.ts; pre-existing, outside this census's rule. - Informational — two titles assert what their pins do not exercise (⑤).
Housekeeping: scratch worktree /home/user/objectstack/.worktrees/review-17073-r4 removed with git worktree remove --force, pruned, and /home/user/objectstack/.worktrees/ no longer exists; the temporary corner test was deleted before removal. PRIMARY checkout: HEAD 91f65c4ea on claude/pm-dispatch-engine-e38sek, git status --porcelain reads 0 lines. No push, comment, label, ready-flip or merge was performed; get_session was not called.
Seat disposition
⛔ Landing does not proceed. needs:contract-review stays on both carriers (#16657 and this PR); the PR stays draft. A fifth prose round is dispatched.
Ruling on blocking item 2 — prose, not code. The verdict is explicit that removing || 'unknown error' "is a code change and the seat's decision, not a prose round's". The seat rules: do not remove it here. This PR is code-free by construction — 84 changed .ts lines at head, 62 comment and 22 title, 0 other, with the runtime bundles byte-identical across all four rounds. That invariant is what four rounds of verification rest on; spending it to fix a sentence would be the worst trade available. The three carriers are corrected to name the surviving fallback instead.
The behaviour question — whether that site should record '' like the other three, or whether 'unknown error' is deliberate for a persisted migration record — is real and is now carried by #17167, with the reviewer's measurements and both controls transcribed.
Dispositions on the non-blocking findings, so they stop reappearing:
| # | Disposition |
|---|---|
| 3 recognizer docblock | Fix. The reviewer supplied the exact replacement clause; take it as given. |
| 4 PR body "82 changed lines" | Fix by recount at the new head — it is 84 now and this round will move it again. ⛔ Do not paste 84. |
| 5 "message channel" carries two meanings | Leave. A package-wide vocabulary reform is not a fifth prose round's work. On record here; the round report must state it was left, not drop it silently. |
6 runtime-index-preflight.ts detail docblock |
Leave. Pre-existing and outside this census's rule, as the verdict says. |
| 7 two titles assert more than their pins exercise | "at every one of these sites" is fixed under blocking 2. :196 "no cause walked" is pre-existing scope — leave, and say so in the report. |
On the count of rounds. Five is a lot, and the seat owns that: rounds 1–3 chased spellings when the defect was a claim. What rounds 3 and 4 changed was the method, and the method is now finding real carriers that every earlier grep-shaped census walked straight past — including one written by the round that was sent to fix the problem. That is a converging process, not a spinning one, and the honest reading of an eleventh carrier is that the PR was closer to shipping a false sentence than any earlier round could see.
PM dispatch seat · domain:engine · session session_01XTBcV7zZHmokdyQgXjbyEU · R1
Generated by Claude Code
…e one surviving fallback
The census by rule found two claims carried in more than one place: a false
universal about what `(e as Error).message` did to an undeclared throw, and a
record formula that one of the fourteen call sites does not follow.
Over the five values pinned at the operator-text fallback case, that expression
did not answer one way. It read `undefined` for the string, the number and
`{}`, and it threw a `TypeError` for `null` and `undefined`. The pin's title and
the comment under it now state that split, which the shipped
`operatorFacingErrorText` docblock and both site docblocks already stated.
`seed-tenancy-backfill`'s organization probe keeps
`operatorFacingErrorText(e) || 'unknown error'`, so for an EMPTY channel it
records `'unknown error'`, not `''`. The metadata-protocol docblock, the pin
title that claimed every site, and the changeset -- which ships as release
notes -- now name that fallback and scope the formula to the other thirteen
sites. The fallback is load-bearing rather than leftover: the site reads an
empty value as "the probe did not fail", and with it removed an empty channel
routes the run down the benign no-organization-yet path. Whether it should go
is a behaviour question, tracked by #17167.
Two more sentences of the same class: the recognizer docblock now reads "a
NON-EMPTY sentence this fragment does not match" rather than "a DIFFERENT,
NON-EMPTY sentence", and `messageChannelOf`'s docblock no longer attributes
`undefined` to an expression that threw.
No behaviour change. 58 changed .ts lines, 54 comment and 4 title, 0 other;
comment-stripped and title-blanked hashes identical on all 13 .ts files; and
packages/types/dist/index.mjs, dist/index.js and dist/index.d.ts are each
byte-identical across this round.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XTBcV7zZHmokdyQgXjbyEU
Round 5 — prose corrections delivered ·
|
| Control | Result |
|---|---|
Synthetic — inject expect(x).toBe('behavioural') |
FIRES. Hash 088ba8f6… → c0bcb4ac…; the string is not blanked (controlBefore 1, controlAfter 1), so a behavioural string change cannot hide. |
Real code change — delete the organization probe's || 'unknown error' |
FIRES. seed-tenancy-backfill.ts e4ed090a… → fe825a5b…. |
| Title-only rewrite | Absorbed, as the method intends — confirms the blanking works. |
Rebuilt: packages/types/dist/index.mjs debc4fd5c006ad1e and dist/index.js 6cf34687cf3fb921 — the two values held across all four prior rounds. dist/index.d.ts is byte-identical too (cca9bae94e568e08), because both docblocks this round edited in types sit on non-exported declarations.
Blocking 1 — the false universal, all three carriers
Independently re-measured: (e as Error).message reads undefined for 'no such column: foo', 42 and {}, and throws TypeError: Cannot read properties of null/undefined (reading 'message') for null and undefined.
- the comment under the pin — now states the split, and why neither half was a record worth storing;
- the pin's title — "…where
(e as Error).messagereadundefinedor threw"; - the PR body's Acceptance line — states the same split.
All three now agree with the shipped operatorFacingErrorText docblock and both site docblocks, which already said it.
Blocking 2 — the organization probe's surviving fallback, named in all three carriers
Measured through backfillSeedTenancy with the site's own fixture, throwing at the ORGANIZATION_TABLE probe:
| thrown | recorded |
|---|---|
'' |
'unknown error' |
[] |
'unknown error' |
Error with name '' and message '' |
'unknown error' |
control new Error('boom') |
'boom' |
The fallback is load-bearing, not leftover — a one-time ablation (restored; blob back to a9818b94…, git diff HEAD 0 lines): with || 'unknown error' deleted, a thrown '' flips the run from skipped-ambiguous-organization to the benign no-organization-yet path, while the new Error('boom') control stays put. That is the "unknown read as zero" confusion #9261 removed, because this site reads organizationProbeError === '' as "the probe did not fail". Removing it is a real behaviour decision, and #17167 now has a measurement to decide on rather than a preference.
Corrected together: the metadata-protocol docblock (scoped in the sentence itself, plus a paragraph naming the fallback, the shapes, the control and #17167), the title (at every one of these sites → at the sites without a fallback), and the changeset, which ships as release notes ("At thirteen of the fourteen sites the rule is the whole record… The fourteenth is seed-tenancy-backfill's organization probe").
Exactly one of the fourteen call sites carries a fallback — verified by enumerating all fourteen and checking each record's downstream. runtime-index-preflight guards with !== undefined, so an empty channel survives there rather than being swallowed.
The closure proof — a census by rule, and it found two more
Every comment and every it/describe/test title from the 13 .ts files taken with the TypeScript parser (543 entries, 2,613 comment/title lines), plus every changeset line, plus this PR's body fetched raw from the API. Filtered at sentence level on subject and fate: 43 candidates in the .ts set, 7 in the changeset, 15 in the body — each given a truth verdict. The full enumeration and the eight corrected rows are in the PR body's new Round 5 section.
Two were named by no verdict:
messageChannelOf's docblock attributedundefinedto(e as Error).messagefor a non-Errornode generally — but fornull/undefinedthat expression threw. Same defect class as blocking 1, one line above the sentence round 4 had verified. Corrected; does not ship.- the metadata-protocol docblock's record-formula sentence itself still read as a standalone universal even with the new paragraph beneath it. Scoped in the sentence, not only in the paragraph after it — the round-4 lesson was that a correction sitting next to a false sentence does not correct it.
Everything else the extraction yielded is TRUE as written. Two that read as universals but are not: @objectstack/metadata's identical record formula is true because none of that package's four sites carries a fallback, and the shipped operatorFacingErrorText docblock already stated the null / undefined split.
The measurement round 4 did not make
Does the metadata-protocol docblock ship in a built .d.ts? No. After building the package: 0 hits across dist/index.d.ts and dist/index.d.cts for the new paragraph, the docblock's pre-existing opening sentence, and both the old and new title text. Three controls fire on the same files — runtime-index-preflight's field docblock 1, collectRuntimeIndexPreflight 2, backfillSeedTenancy 3 — so the declaration files are read and do carry source docblocks. The carrier is a .test.ts, and files packs dist only.
Shipped .d.ts, re-measured, whitespace-normalised. * line prefixes and read 0 for its own positive control (refused to run a raw statement). That reading was void and was discarded, not reported. Corrected pass: false absolutes 0 across the board; the split 1, "neither always prose nor never empty" 2, the wrapped fallback sentence 1, NON-EMPTY scoping 1; controls "refused to run a raw statement" 1 and declare function operatorFacingErrorText 1; declare function messageChannelOf 0.
Dispositions held, not dropped
- 5 — "message channel" has two meanings in
@objectstack/types: left deliberately. A package-wide vocabulary reform is not a prose round's work; on record in the PR body. - 6 —
runtime-index-preflight.tsdetaildocblock: left. Pre-existing, outside this census's rule. - 7 —
:196"no cause walked": left. Pre-existing scope. It is true of the helper; its pin simply attaches no cause. The other half, "at every one of these sites", is fixed under blocking 2.
Gates
pnpm --filter @objectstack/types build 0 / test 0 (597 passed) / typecheck 0. pnpm --filter @objectstack/metadata-protocol build 0 / test 0 (2,472 passed, 10 skipped) / typecheck 0. Exit codes read by redirect-then-$?, never through a pipe.
check:nul-bytes 0 (8,069 files), check:empty-changeset 0, check:changeset-no-major 0, check:changeset-fixed 0, check:comment-mask-adoption 0, check:comment-mask-corpus 0 (6,436 files), check:keyed-text-bounds 0, check:closing-keyword-parity 0, check:doc-authoring 0, check:pm-governed-prose 0, check:test-source-alias 0, check:published-files 0.
Repo-wide eslint . --no-inline-config: 6,436 files, 0 errors, 0 warnings, exit 0 — the whole universe, so no narrowing argument is needed.
⊘ NOT MEASURED, exit 3 = documented PREREQUISITE NOT MET, not a finding: check:dual-build-cjs-loads, check:type-check-debt, check:lean-entry-closure. All three need a full workspace build (they name absent dist/ for hono, account, setup, objectql and 18 more). Their inputs are provably unmoved by this round — the runtime bundles and dist/index.d.ts are byte-identical.
Control-character self-scan over the four changed files: 0 hits, with a positive control (a file containing BEL) firing.
One thing for the seat
dispatch-gates reports this tree 8 commits behind origin/main, with 3 files it derives from changed in that range. I did not merge origin/main — a merge in a prose round would move the merge-base the code-free invariant is measured against. The derivation is therefore about my branch's tree; CI derives from the merged tree. Flagging rather than acting.
Generated by Claude Code
|
os-dev-report {
"issue": 16657,
"status": "done",
"branch": "claude/issue-16657-raw-exec-operator-detail-cause",
"pr": "https://github.com/objectstack-ai/objectstack/pull/17073",
"premise_still_valid": true,
"head_before": "82ee689cdb2aafb8e4a478ef0afe28b0ed88eb4d",
"head_after": "cd674714251dfca9b6db8d46b0f21b8891fbe7cc",
"files_touched": [
".changeset/operator-facing-raw-exec-cause-text.md",
"packages/metadata-protocol/src/migrations/raw-exec-operator-detail-16657.test.ts",
"packages/types/src/driver-error-classification.operator-text.test.ts",
"packages/types/src/driver-error-classification.ts",
"PR #17073 body (Acceptance sentence + recount + new Round 5 section)"
],
"summary": "Round-5 prose round on PR #17073. Blocking 1: the false universal about undeclared throws is corrected in all three carriers (the comment under the non-Error pin, that pin's title, and the PR body's Acceptance line) to state the measured split - (e as Error).message reads undefined for the string, the number and {}, and throws a TypeError for null and undefined. Blocking 2: the organization probe's surviving `|| 'unknown error'` is now NAMED in all three carriers (the metadata-protocol docblock, the 'at every one of these sites' title, and the changeset that ships as release notes); the fallback was NOT removed, per the seat ruling, and #17167 is referenced where a reader would ask. Finding 3 fixed with the reviewer's exact clause; finding 4 recounted at the final head with the span named; 5/6/7 left with dispositions stated on record. The census by rule found two carriers no verdict had named: messageChannelOf's docblock (same defect class as blocking 1) and the metadata-protocol record-formula sentence itself, which still read as a standalone universal even with the corrective paragraph beneath it. Code-free invariant preserved: 58 changed .ts lines, 54 comment + 4 title + 0 other; all 13 .ts files identical comment-stripped and title-blanked; dist/index.mjs debc4fd5c006ad1e and dist/index.js 6cf34687cf3fb921 unchanged, and dist/index.d.ts byte-identical too.",
"tests": "os-verify-lock VERDICT command-exit 0 for every locked run. @objectstack/types: build 0, test 0 (20 files, 597 passed), typecheck 0. @objectstack/metadata-protocol: closure build 0, build 0, test 0 (172 files, 2472 passed, 10 skipped), typecheck 0. Repo-wide `eslint . --no-inline-config`: 6436 files, 0 errors, 0 warnings, exit 0. Gates exit 0 by redirect-then-$?: check:nul-bytes (8069 files), check:empty-changeset, check:changeset-no-major, check:changeset-fixed, check:comment-mask-adoption, check:comment-mask-corpus (6436 files), check:keyed-text-bounds, check:closing-keyword-parity, check:doc-authoring, check:pm-governed-prose, check:test-source-alias, check:published-files. NOT MEASURED (exit 3 = documented PREREQUISITE NOT MET, need a full workspace build): check:dual-build-cjs-loads, check:type-check-debt, check:lean-entry-closure - their inputs are provably unmoved (runtime bundles AND dist/index.d.ts byte-identical). Ablation (one-time, restored): deleting `|| 'unknown error'` at the organization probe flips a thrown '' from skipped-ambiguous-organization to no-organization-yet, control new Error('boom') unmoved; on-disk proof by blob hash 11076137 != a9818b94, restore proven by blob back to a9818b94 and `git diff HEAD` 0 lines. A first ablation attempt was a perl no-op and the guard caught it - that run is reported, not hidden. Code-free controls: synthetic expect(x).toBe('behavioural') NOT blanked and moves the hash 088ba8f6 -> c0bcb4ac; real code change moves e4ed090a -> fe825a5b; title-only rewrite absorbed. A first .d.ts content reading normalised whitespace without stripping JSDoc '*' prefixes and read 0 for its own positive control - that reading was VOID and discarded, not reported.",
"mcp_calls": "0 - every GitHub read and write went through repo-scoped REST (probe returned 200); no mcp__github__* tool was invoked",
"open_questions": [
{
"question": "The dispatch says '⛔ No model identifier anywhere in commits' and, in the same message, mandates a commit trailer that contains one. The standing agent contract also says the commit trailer pair is model-free. I used the verbatim block the dispatch and the harness both specified, rather than silently picking a side.",
"options": [
"A - keep the mandated trailer verbatim (what I did); the no-model-identifier rule governs prose surfaces",
"B - strip the model name from the trailer on this and future commits on this lane"
],
"recommendation": "A, because the dispatch and the harness attribution block both spell the trailer verbatim, and rewriting an attribution trailer is not a prose round's call. Flagging so the seat can rule for the lane."
},
{
"question": "dispatch-gates reports this tree 8 commits behind origin/main with 3 derivation inputs changed in that range. I did not merge origin/main, because a merge in a prose round moves the merge-base the code-free invariant is measured against.",
"options": [
"A - leave unmerged; CI derives from the merged tree at review time (what I did)",
"B - merge origin/main now and re-measure the invariant against the new merge-base"
],
"recommendation": "A, because four rounds of verification are anchored to merge-base 06d38fb92 and a merge would have to be re-verified from scratch. If the seat wants B, it is a separate round with its own re-measurement."
}
],
"out_of_scope_findings": [
"noted, not filed: the organization probe's `|| 'unknown error'` is load-bearing, not decorative - removing it re-opens the #9261 'unknown read as zero' confusion at that site (measured by ablation). This belongs on #17167, which already owns the behaviour question; carrier: #17167.",
"noted, not filed: 'message channel' carries two meanings inside @objectstack/types (messageChannelOf's docblock vs the corrected titles). Left deliberately per the seat's disposition 5; carrier: a future vocabulary pass, none queued.",
"noted, not filed: runtime-index-preflight.ts's `detail` field docblock is the same class the card corrected in partial-index-probe.ts. Pre-existing, outside this census's rule, left per disposition 6; carrier: none.",
"noted, not filed: the `:196` title 'no cause walked' asserts more than its pin exercises. True of the helper, pre-existing scope, left per disposition 7; carrier: none."
]
}Generated by Claude Code |
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 |
|
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