Uh oh!
There was an error while loading. Please reload this page.
fix(devx): durability log-level matcher reads the callee, and stops accepting a spelling that prints nothing - #9750
Conversation
…ps accepting a spelling that prints nothing `loggerLevel()` required the callee to be a plain property access, so `(logger.error ?? logger.warn)(...)` — a call on a parenthesized expression — collected no levels and the catch was reported as `catch swallows the failure with no log at all`, the harshest verdict in the file, on code that is loud at runtime. The dangerous half was the repair that report invited. The one fallback spelling the matcher DID accept is `logger.error?.(...)`, which prints nothing at all against a sink that has no `error` — and `error` is declared optional on exactly the sinks that use the idiom. So the gate's cheapest satisfaction converted a loud degradation into a silent one. - resolve the callee structurally: parentheses, `??`/`||`, a ternary, `.call`/`.apply`/`.bind`, non-null assertions, `logger['error']`, and a same-file `const` holding a fallback are followed to whatever they end at. - an optional CALL (`?.(`) no longer counts as loud: it is the author's own statement that the call may not print, and the sink it holds still has `warn`. Optionality on the RECEIVER (`logger?.error(...)`) is deliberately not judged. - "I could not read this call" is its own verdict, `unreadable-report`, instead of being folded into `silent-swallow`. - six seams the tightened rule found — plugin-audit x3, plugin-email, plugin-security x2, all on sinks whose `error` is declared optional — now reach for `error` and fall back to `warn` instead of to silence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
…lity report One per repaired package. Each asserts the MESSAGE lands at `warn` — not merely that nothing threw, which the silent `logger?.error?.(…)` version satisfied too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
…its report needs `error` is optional on this sink, so a durability report that reaches for it needs somewhere to fall back to — and this interface declared no `warn` at all, while its sibling `ReadAuditLogger` in the same package always has. The type, not the call site, was the thing missing the channel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
…he shape census Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
📓 Docs Drift Check7 anchor(s) derived from 3 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 19 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 1342285e6f3e432284e660f43ce084dd86e357d3 && git checkout 1342285e6f3e432284e660f43ce084dd86e357d3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2065e31d518a1dd363825b2de5ed1207b04291c7 3a6a4b20a86214e7f5d9a2201db117b184d0a83c && git checkout -B drift-repro 2065e31d518a1dd363825b2de5ed1207b04291c7 && git merge --no-ff 3a6a4b20a86214e7f5d9a2201db117b184d0a83c
node scripts/docs-audit/affected-docs.mjs --json 2065e31d518a1dd363825b2de5ed1207b04291c7 |
✅ PM ACCEPT — #9657 / PR #9750 · the harmful gradient is closed, demonstrated in both directionsVerified independently: 11 files ⭐ H1 — six shape families, not three, and the census overturned the shape of the fix3,308 log-emitting calls across
The That count is what killed option 1: five spellings written by authors with no reason to know a gate exists. Documenting a blind spot does not stop the sixth spelling. And option 3 would have added a third staleness-prone vocabulary for nothing option 2 lacks. ⭐ The gradient is closed, and the distinction that closes it is exact
That single sentence is the whole card. And receiver optionality ( Ruling 1 discharged and demonstrated, two-direction ablation against main's actual matcher (extracted with
Plus the runtime proof against a ⭐ Two things the card did not anticipate, and the second is a new axis(1) The tightening found six REAL seams, not collateral — plugin-audit ×3, plugin-email, plugin-security ×2 — every one on a sink whose (2) Part of this is a TYPE-DECLARATION defect, not a matcher defect.
That is the finding you glimpsed before the interruption, and it landed exactly where you predicted: the sink type is a fourth axis for the #9165 family, alongside the call shape. The And the distinct verdict landedPer H1's instruction at that count, "I could not recognise this callee" is now H2 — confirmed, with the caveat that mattersTransitive helper-following works, and #9665's sites are green for the right reason — but only because the helper body spells an unconditional The verification discipline
Ruling on the open question: B — carded, not now, and you were right not to take it.A leaves a live foot-gun: the contract still permits a silence the gate must then catch site by site. C is falsified today — hosts do inject reduced sinks ( B is the contract-first version, and your authoring argument is the deciding one: an AI writing a new durability report reads the interface, sees And you were right that it does not belong in this PR: drawing the population of "logger-ish interfaces" is precisely the fuzzy-scope problem this file's own header warns produces a gate people disable, and pricing that is my call. Filing it, and cross-linking it to #9747 as the sink-type axis. #9748Two sibling Verdict: ACCEPT. Arming once the nine running gates converge. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#9657
loggerLevel()required a call's callee to be a plain property access.(this.logger.error ?? this.logger.warn)(msg, meta)is a call on a parenthesized expression, so it collected no levels and the catch was reported ascatch swallows the failure with no log at all— the harshest verdict in the file, on code that is loud at runtime.The dangerous half is the repair that report invited, and it is what this PR is really about.
Option taken, and why
Option 2, extended — resolve the callee structurally, AND stop accepting the one spelling that prints nothing. Option 2 alone was ruled out on the card, correctly: widening the matcher fixes the false red and leaves the trap.
How the harmful gradient is closed
The trap was:
logger.error?.(…)was the cheapest way to turn the false red green, and against a sink with noerrorit emits nothing. So the gate's cheapest satisfaction converted a loud degradation into a silent one.An optional call (
?.() no longer counts towardloud. That is not a heuristic — it reads the author's own optionality marker:?.(is a statement that this call may not print, on a sink that is still holding awarn. The fallback of??is another log; the fallback of?.is silence. The cheapest way to green is now(l.error ?? l.warn)(…)or a named helper, both correct code.⛔ Optionality on the receiver (
logger?.error(…)) is deliberately NOT judged: it says "there may be no sink at all", and then no better level exists to fall back to. Judging it too would have flagged 22 further in-catchcalls with no remedy to offer any of them.H1 — the census: six shape families, not three
Every log-emitting call under
packages/(non-test.ts), classified by callee shape. 3,308 calls, 658 of them inside acatch:logger.error(…)and its?.variants(logger.error ?? logger.warn)(…)(logger.error ?? logger.warn).call(logger, …)((c.warn ?? c.error))?.(…)l.error?.bind(l) ?? l.warn.bind(l)stored in aconst, called through itwarn(…)/info(…)/log(…)(same-file or imported helper)The card named three shapes; there are six, and the fourth was the interesting one —
db-job-adapter.ts:239and two trigger packages build the fallback with.bind(), store it in aconst, and call through the local. Two of those catches reason explicitly about the durability class in their comments. So the answer is not "teach it three more spellings": the shapes exist becauseerroris optional on these sinks and every author invents their own way to say "error if you have one", and that set is still growing.Accordingly this PR also does what H1 said to do at that count: "I could not recognise this callee" is now its own verdict,
unreadable-report, instead of being folded intosilent-swallow. It still fails the gate — the checker cannot prove the seam is loud — but it accuses the right thing, and its remedy text explicitly rules out?.rather than inviting it. It fires on nothing today; it is what stops the seventh shape from repeating this card. (Filed as one of the six instances in #9165's disposition; this is the false-red direction of that meta-shape.)H2 — the helper indirection does survive, confirmed on
mainnode scripts/check-durability-degradation-log-level.mjs --listonorigin/main:#9665's report is accurate: transitive following works, and its two sites are green for the right reason. But it is green because the helper's body spells
if (this.logger.error) this.logger.error(…)— an unconditional call the matcher reads. The helper is a sanctioned shape, not a sufficient answer, which is why option 1 was not enough.H3 — the gradient, demonstrated in both halves
Runtime (
node, against the sink shapeSqlDriver.logger/SweepLogger/ProjectionLoggerall declare):The gate, two-direction ablation on the real repo,
main's matcher vs this one:main's matcher(this.logger.error ?? this.logger.warn)(…)catch swallows the failure with no log at allloud (error@8264)logDurabilityFailure's body replaced withthis.logger.error?.(msg, meta)reaches error only CONDITIONALLYThe first row reproduces the card's false red exactly. The second reproduces the trap #9665's dev measured, and shows it closed. Both mutations were reverted; the tree is byte-identical to its commit.
What the tightening found: six real seams, and a missing type member
Making
?.(not-loud turned six currently-green seams red. They are not collateral damage — every one is on a sink whoseerroris declared optional, which is the same reason the fallback idiom exists:Repaired in the same defect class, each locally, using the shape #9665 already landed: reach for
error, fall back towarn, never to silence. Message, consequence and fix are identical on both channels; only the level degrades, and only when the sink cannot do better.plugin-audit— the CRUD, auth-event and read-audit lost-row reportsplugin-email— the strandedsys_emailrowplugin-security— both permission-set metadata backfill failuresA type-declaration defect fell out of this, and it may be the more interesting half.
AuthEventAuditLoggerdeclarederror?anddebug?and nowarnat all — so at that site there was no fallback channel to reach for, and the typechecker said so. Its siblingReadAuditLogger, in the same package, has always declaredwarn?. The omission is the outlier, and it means part of what reads as a matcher problem is a sink-type problem: an optionalerrorwith no declared alternative is a contract that permits silence. Addedwarn?, mirroring the sibling exactly — purely additive, so no existing sink stops satisfying the interface.⛔ Not swept: the seven
(a ?? b)(…)sites the card names, per its ruling — none was red and none is now. Two siblinglogger?.error?.(…)summary reports that no catch guards, so no gate can see them, are filed unassigned as #9748 rather than fixed here.Verification
Union re-run after the final commit, on
3a6a4b20a:check:durability-log-level— ✓ 29 seams all loud/rethrowing/propagating; ✓ 66 read seams, byte-identical tomain(the read-seam rule is passed nounreadablesink, so its census and verdicts do not move)--self-test— ✓ 51 log-level cases (16 new) + ✓ 35 read-seam cases. Every new passing case pinsexpectSeams: 1so it cannot pass vacuously, and every new flagging case pinsexpectKinds— a boolean "did it flag" cannot tell a right verdict from a wrong one, which is this whole cardtypecheckclean on all threeexpected [] to have a length of 1, i.e. the old code emitted nothing — exit 1. Restored from HEAD, byte-exactcheck:nul-bytes,check:cross-package-test-inputs,check:test-source-alias,check:type-source-resolution,check:engine-double-contract,check:where-matcher,check:query-options-erasure,check:type-check-coverage,check:empty-changeset, andcheck:i18n(which first refused for lack of a built CLI — "nothing was checked" is not a pass; built@objectstack/cliand re-ran for a real answer)Generated by Claude Code