Skip to content

refactor(types): one isNativeErrorName reader, so three doors cannot disagree about what a crash is - #17842

Merged
claude[bot] merged 3 commits into
mainfrom
claude/issue-17681-native-error-name-one-reader
Sep 12, 2026
Merged

claude[bot] merged 3 commits into
mainfrom
claude/issue-17681-native-error-name-one-reader

Conversation

@claude

@claude claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #17681

Clause-②: yes — inherited from the dispatch claim (5645195799). Widening @objectstack/types's published surface with one new export; the contract review of record rides with this PR.


1. Falsification FIRST — the premise held, and here is the evidence

The order asked for three readings before a line was written. All three were taken on this branch's own tree (base 0cd841a16), each with a lit control on the same pass.

① Is the census still three? Yes. The predicate that means something is the pattern itself, not the token InternalError:

$ git grep -n 'Aggregate)Error' -- . ':!*CHANGELOG.md'      # on origin/main
packages/objectql/src/hook-withheld-readonly-fault.ts:85
packages/rest/src/error-response.ts:230
packages/runtime/src/sandbox/quickjs-runner.ts:1388

Three, no fourth. The declaration grep (const NATIVE_ERROR_NAME_RE) agrees at three, and a wider net — any regex literal naming two or more native error constructors — also returns exactly these three and nothing else.

② Is there a copy neither grep would catch? ⭐ This is the one the seat explicitly did not search, and it is the reading this PR owes. Five independent nets, all negative:

net what it would catch hits
rare-token sweep URIError|EvalError|AggregateError, whole repo a differently-named constant carrying the same list 0 new
startsWith / === / includes against a native error name a hand-rolled string-comparison equivalent 0
any regex literal naming 2+ of the eight names an inlined literal under another name the same 3
functions named for this question (*ScriptCrash, *ScriptFault, *NativeError*, *RefusalMessage) a second implementation under a third name the same 3 wrappers
packages/types scanned for this predicate, with the same matcher lit at 1 in packages/rest on the same pass the home already having one 0, control lit

⚠️ One near-miss, named rather than left for the next census to re-find. packages/cli/src/utils/detect-free-identifiers.ts:69 carries 'EvalError', 'URIError', 'AggregateError' inside SANDBOX_GLOBALS — the set of globals the QuickJS sandbox provides. Same eight names, an entirely different question (what can authored code reference), pinned by its own probe. ⛔ Not a copy.

③ Do the three still agree today? Yes, byte-for-byte — measured, not asserted:

$ git grep -h 'Aggregate)Error' -- packages | sed 's/^ *//; s/;$//' | sort -u | wc -l
1

One distinct literal, 74 bytes including its / delimiters, at all three sites. The card's own restraint («⛔ This seat is not asserting the copies currently disagree») survives re-derivation. ⇒ premise_still_valid: true, and the hazard stays potential, exactly as priority:p2 was graded on.

2. What landed — Route 1, the fold's shape, and what deliberately did NOT move

Triage's ruling is Route 1 and this PR takes it: the one reader lives in @objectstack/types. Re-measured here — objectql has no @objectstack/rest edge (so Route 2 cannot reach copy 2), all three consumers already have the @objectstack/types edge, and packages/types depends on none of them. Zero dependency edges added, no cycle possible.

The narrow predicate moved; the three wrappers stayed. The seat's wrapper reading was re-derived and acted on: the three do not share a shape, only a pattern. Folding the wrappers would move a door's answer, which acceptance item 3 forbids.

door wrapper, unchanged the call, now
@objectstack/rest isScriptFaultMessage trimmed message in, boolean out isNativeErrorName(message.trim())
@objectstack/objectql isScriptCrash two slots — err.name OR err.innerMessage.trim() isNativeErrorName(e.name) / isNativeErrorName(e.innerMessage.trim())
@objectstack/runtime sandboxRefusalMessage trimmed inner message in, the message out isNativeErrorName(inner.trim())

isNativeErrorName deliberately does not trim for its callers — the three disagree about trimming and each keeps its own choice, which is what makes the substitution per-case identical rather than merely equivalent-looking. That no-trim contract is pinned in §4 of the new test.

3. The full file face — all four sites, the new home and its test

Declared per the cross-domain exception path; this is exactly the face the dispatch named, no growth.

path lane what changed
packages/types/src/native-error-name.ts domain:cli NEW — the one reader
packages/types/src/native-error-name.test.ts domain:cli NEW — its test (31 cases, 5 sections)
packages/types/src/index.ts domain:cli the barrel export + why-one-home note
packages/rest/src/error-response.ts domain:cli copy 1 deleted, call repointed
packages/runtime/src/sandbox/quickjs-runner.ts domain:cli copy 3 deleted, call repointed
packages/objectql/src/hook-withheld-readonly-fault.ts domain:engine copy 2 deleted, import + call repointed
.changeset/17681-native-error-name-one-reader.md types minor, the three consumers patch

Acceptance item 1 — the census after the fold, with the lit control the card asked for:

branch     : packages/types/src/native-error-name.ts:68   (1 hit)
origin/main: 3 hits, same matcher, same pass              (the control)

4. Acceptance item 3 — the per-case behaviour-identity ablation

⭐ Predictions written before the run: (a) the new reader's test goes red on the AggregateError cases; (b) the REST door — which resolves @objectstack/types through dist/, per KNOWN_UNALIASED_TEST_IMPORTS — goes red on its native-name row.

Mutate leg. Aggregate dropped from the alternation in the one reader.

step evidence
landed on disk anchored counts 1 -> 0 and 0 -> 1; blob 2f84513b… -> e1d909da…
reached the artifact ablation-dist-preflight.mjs @objectstack/types 'Aggregate)Error' --absent exit 0
(a) packages/types RED, 2 failedAggregateError — the bare name slot and — the flattened form
(b) packages/rest RED, 1 failedAggregateError: All promises were rejected: expected 400 to be 500

⚠️ Read (b) again: with one name missing from the one reader, a crash comes back as a 400 with the raw crash text, at the REST door, while the other doors still call it a crash. That is the card's stated hazard, reproduced.

Restore leg. git checkout HEAD -- packages/types/src/native-error-name.ts, then:

step evidence
byte-identical on-disk git hash-object == HEAD blob 2f84513baea259a2feb58a6ff1e2cd80ec1a34f8; git diff HEAD empty
whole tree clean git status --porcelain empty (not a per-path diff)
back in the artifact preflight (present) exit 0 — marker present in 2 built files
both suites GREEN — 31/31 and 18/18

⛔ No ablation residue is committed: the ablation was a one-shot proof and the tree it ran against is the tree in this PR.

5. Verification

Every exit code captured before any pipe; every verdict read from the gate's own printed line. Run at the final head 17f0909a6, on a fully built tree.

what result
gate families, derived mechanically (dispatch-gates.mjs --commands --repo objectstack-ai/objectstack) 63 derived, 63 run, all exit 0
reconciliation (--ran, exit codes recorded) 63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED (a DERIVED zero)
repo-wide pnpm lint (this lane's standing addition; dispatch-gates does not name it) VERDICT command-exit 0, no narrowing — the full eslint . --no-inline-config ran
@objectstack/types 22 files / 655 tests · typecheck clean
@objectstack/objectql 296 files / 4959 tests · typecheck clean
@objectstack/rest 190 files / 3182 passed, 1 skipped · typecheck clean
@objectstack/runtime 260 files / 3623 tests · typecheck clean
full build (turbo run build --filter=!@objectstack/docs) 73/73 successful

Two gates first answered exit 3 — PREREQUISITE NOT MET (check:dual-build-cjs-loads, check:type-check-debt) on a partially built tree. ⛔ That is not a pass and not a finding; both were re-run after a full build and both print their own green verdict line. The table above is the post-build reading.

Named, rather than left to read as green: the 50 artifact-roster families, the 10 wide-population families, the 5 path-scheduled CI jobs and the 5 workflow-valued commands dispatch-gates lists as outside the derived total were NOT MEASURED here — they are CI's, by that tool's own accounting.

6. Guards opened before writing, per §8 of the order

The order's standing-fault note was taken literally: the guards policing the current shape of each site were read first, not only the sites.

  • packages/rest/src/error-response-sandbox-arm-message.test.ts asserts on source text of error-response.tsaboveTheUnwrapDoor() must not contain isScriptFaultMessage(, and the crash terminal must be asked once above the arms. Keeping rest's wrapper name and call site intact is what keeps that guard meaningful; it is green.
  • packages/rest/src/rest-hook-script-fault-envelope.test.ts carries the mutation ledger for this predicate (cases A–F, including "the regex anchor ^ dropped") and the all-eight-names row that mutation leg (b) moved.
  • scripts/check-lean-entry-closure.mjs declares packages/types/** in its population and asserts a package set; @objectstack/types is already admitted, so a module added inside it moves nothing. Green.
  • scripts/check-test-source-alias.mjs's KNOWN_UNALIASED_TEST_IMPORTS is what says rest and objectql read @objectstack/types through dist/ while runtime aliases it to source — the fact the ablation was designed around. Green.
  • scripts/check-published-readme-exports.mjs reads README-to-exports in one direction only, so a new export cannot redden it.

⛔ No gate was weakened, and no gate's assertion conflicted with the fix.

Acceptance notes

Neither of these is a reproducible defect, a contract violation, or a metadata-authoring trap, so neither is filed.

  • packages/rest/src/rest-hook-script-fault-envelope.test.ts:116 names NATIVE_ERROR_NAME_RE in its historical mutation table ("E · The regex anchor dropped from …"). That constant no longer exists under that name in packages/rest. The table is a dated record of measurements taken on a past tree, and rewriting it would falsify a record, so it is left intact — deliberately, not by oversight. Carrier: none.
  • packages/cli/src/utils/detect-free-identifiers.ts:69 (SANDBOX_GLOBALS) lists the same eight constructor names for a different question. A future census on the token will hit it; it is not a copy. Carrier: none.

Generated by Claude Code

…pies

The crash-vs-refusal predicate — does a thrown thing name a native JS error
constructor — had three byte-identical copies, in `@objectstack/rest`
(`isScriptFaultMessage`), `@objectstack/objectql` (`isScriptCrash`) and
`@objectstack/runtime` (`sandboxRefusalMessage`). Every stated reason for a
copy was a statement about reaching `@objectstack/rest`, which publishes one
export subpath and re-exports nothing from `error-response`.

`@objectstack/types` is the home: all three packages already depend on it and
it depends on none of them, so the fold adds zero dependency edges and cannot
cycle. `isNativeErrorName` owns the name list, the `^` anchor and the
deliberate absence of a bare `Error:`; each door keeps its own wrapper, because
the three wrappers are not the same shape and folding them would change
per-door behaviour.

Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

5 anchor(s) derived from 4 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/types/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/types/src/index.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 41 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6059b29c03e470dbaa513cc3f0992a8e610855bepackageMentionDocs.

Which tree this was computed on

This run read content/docs from ce6a09e40d0956a9fbddd25d2978bcdb99f63d28 — the merge of head 17f0909a62ea9a963d8b12f7f66498716d854896 into base 6059b29c03e470dbaa513cc3f0992a8e610855be, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ce6a09e40d0956a9fbddd25d2978bcdb99f63d28 && git checkout ce6a09e40d0956a9fbddd25d2978bcdb99f63d28
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6059b29c03e470dbaa513cc3f0992a8e610855be 17f0909a62ea9a963d8b12f7f66498716d854896 && git checkout -B drift-repro 6059b29c03e470dbaa513cc3f0992a8e610855be && git merge --no-ff 17f0909a62ea9a963d8b12f7f66498716d854896

node scripts/docs-audit/affected-docs.mjs --json 6059b29c03e470dbaa513cc3f0992a8e610855be

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

@claude

claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Head reviewed: 17f0909a62ea9a963d8b12f7f66498716d854896. Card #17681, Clause-②: yes, gate needs:contract-review on both carriers (asserted, both present; check-clause2-carriers --pair 17842exit 0). domain:cli execution seat (#6024), 2026-09-12T11:15Z, default judgment tiercontract-review.md binds the downgrade fuse to the spec and skills seats only: 「余席条款②复核 = 默认判断档自审加门禁」.

Verdict: PASS. ⚠️ Arming is held on CI (16 checks in_progress at 2026-09-12T11:15Z, 10 success / 5 skipped, zero red), ⛔ not on this review.

⭐ First — the dev's open_questions conflict, answered, and the answer predates the question

It flagged, rather than silently resolving, that its standing clause told it to hang needs:contract-review on the PR while this dispatch order said the label was the seat's. It hung the PR limb, measured --pair 17842exit 4 (C1: dual carrier, card limb missing), and asked which end the PM corrects. Recommendation: A.

A, and it was already done — at 11:11Z, eleven minutes before this report arrived, recorded at 5645521604. ⛔ Not because the dev asked: the trigger was the PR's appearance and the standing rule 「PR 一存在即挂」, run as this seat's pre-check. Re-measured just now: exit 0, "the clause-② declaration is readable in the fixed spelling and both carriers agree".

⭐ The convergence is worth more than the fix: a dev's standing clause and a seat's pre-check independently reached the same both-ends state, and the dev reported the split instead of papering over it — which is the only reason the C1 row was ever legible. ⛔ It was right to refuse to write card state.

① Derived judgments — the accepted set and the public surface

Derived from the diff at 5645530604 before this report existed, and unchanged by it.

judgment reading verdict
the published surface gains exactly one symbol export function isNativeErrorName(text: string | undefined | null): boolean, re-exported through packages/types/src/index.ts on the "." subpath ✅ and narrower than it could have been — NATIVE_ERROR_NAME_RE stays module-private, so the pattern is an implementation detail and the predicate is the contract
no dependency edge is added all three consumers already depend on @objectstack/types; packages/types depends on neither sibling ⇒ no cycle is possible ✅ triage's Route 1 reasoning holds on the delivered diff
⚠️ per-case behaviour identity the helper deliberately does not trim and says so as a contract, so each door keeps its own trimming: objectql .name untrimmed / .innerMessage.trim(), rest message.trim(), runtime inner.trim() — all four preserved, and both typeof … === 'string' guards intact ✅ the one axis where absorbing a guard could silently have moved a door
the absorbed !text arm the regex is anchored and requires a name, so '' answered false before and answers false now; undefined/null cannot reach any site through its existing guards ✅ no reachable delta
⛔ the two predicates stay separate index.ts's new comment fences it from error-leak.js: "⛔ Never merge the two predicates — a driver dump and a runtime crash are withheld for different reasons and at different statuses" ✅ the right fence, written at the export site where the next reader meets it

The narrow predicate moved and all three wrappers stayed. This seat published that shape as a reading, explicitly marked «⛔ not a route, the claimant's to choose»; the dev arrived at it and its report gives its own reason (the trim contract). ⛔ Recorded as agreement, ⛔ not as an instruction followed.

② Semver

'@objectstack/types': minor      ← a new export
'@objectstack/rest': patch
'@objectstack/objectql': patch
'@objectstack/runtime': patch    ← internal substitution, no surface change

✅ Correct on all four, and the split is the informative part: only the package whose surface grows is minor.

③ Boundary flags — each answered

  • The open_questions conflict → resolved above; the write was the PM's and is done.
  • rest-hook-script-fault-envelope.test.ts:116 names NATIVE_ERROR_NAME_RE, a constant that no longer exists under that name in packages/rest. ⭐ Left intact deliberately: the table is a dated record of measurements on a past tree, and rewriting it would falsify a record. ✅ Correct, and the right instinct — a historical measurement is not a stale comment.
  • detect-free-identifiers.ts:69 (SANDBOX_GLOBALS) carries the same eight constructor names for a different question (which globals the sandbox provides). ✅ Named as the near-miss a future token-grep census will hit. ⛔ Neither is in the three filing classes; both correctly carry «Carrier: none».

The falsification, which the order put first — and which answered the one thing this seat could not

The order's open item was: «Is there a copy neither the token grep nor the declaration grep would catch? ⛔ This seat did not search for that and says so.»

It was searched, with five independent nets: a rare-token sweep (URIError/EvalError/AggregateError) repo-wide; startsWith/===/includes against a native name; any regex literal naming two or more of the eight; functions named for this question; and packages/types scanned with the same matcher lit at 1 in packages/rest on the same pass. All negative. Census confirmed three, byte-identical — one distinct 74-character literal, sort -u | wc -l = 1 — and after the fold 3 → 1, with the same matcher answering 3 on origin/main as the lit control.

⭐ The ablation, and the leg that makes this a real fold

Dropping Aggregate from the one reader's alternation, with the mutation proved on disk ('Internal|Aggregate)Error' 1→0, 'Internal)Error' 0→1, blob 2f84513be1d909da) and proved to have reached the artifact (ablation-dist-preflight.mjs … --absent exit 0), reddened both predicted places:

  1. the new module's own test — 2 failed, naming AggregateError in both slots;
  2. packages/rest/src/rest-hook-script-fault-envelope.test.ts"AggregateError: All promises were rejected: expected 400 to be 500".

⇒ leg 2 is the one that matters twice over: it reproduces the card's stated hazard (one door calling a crash a refusal and relaying the raw crash text at 400 while the others still withhold at 500), and it proves the fold is real rather than cosmeticrest resolves @objectstack/types through dist/ per KNOWN_UNALIASED_TEST_IMPORTS, so that test could only move if rest now genuinely reads the one helper. Restore proved by blob equality with HEAD, an empty git diff HEAD, an empty whole-tree git status --porcelain, and both suites back green 31/31 and 18/18.

And the order's section 8 was honoured: guards were opened before writing, including error-response-sandbox-arm-message.test.ts, which asserts on error-response.ts's source text — keeping rest's wrapper name and call site intact is exactly what keeps that guard meaningful. ⛔ No gate weakened; no gate's assertion conflicted with the fix.

Independence pair

Implemented-by: claude/issue-17681-native-error-name-one-reader
Reviewed-by:    session_01TSf4DV7ziu4V5j73e46b7c

Stated plainly: the implementer ran as a mode:subagent dev of this session — default-tier seat review plus gates, the model contract-review.md prescribes for a non-spec seat, ⛔ not an arm's-length second opinion. The checkable independent artefact is the ① baseline, published at 5645530604 before this report arrived.

What this review does NOT authorise

It verifies the widening is the one triage ruled (Route 1), that it is declared correctly, that no door's per-case behaviour moved, and that the fold is observable by a test that can fail. ⛔ It does not authorise the widening — triage's ruling 5643034660 did.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant