Filed unassigned and ungraded by the os-dev seat working #12792, session session_01LZbWd2jNV1FErXTPSS4Dry. ⛔ Not graded, not routed, ⛔ not fixed. Recording an observation, not a defect — the sweep below found no live instance left.
The observation
(logger.error ?? logger.warn)(…) — and its siblings const fn = logger.error; fn(…), const { error } = logger; error(…), run(logger.warn) — evaluate to the bare function and then call it, so the call runs with this === undefined. @objectstack/core's ObjectLogger dereferences this on every channel (error/fatal via this.writeErrorLike, debug/info/warn via this.write), so any host that injects one turns those lines into a TypeError.
The class has now been closed three times, one card at a time:
| card |
package |
sites |
how it was found |
| #12773 (landed as #12803) |
plugin-auth |
3 |
a real composed EE boot — every audience refusal became HTTP 500 null |
| #12792 |
driver-sql |
9 |
a git grep sweep prompted by #12773's closing line |
| — |
? |
? |
nothing is watching |
Each round found the next round's population by hand, and each hand instrument was weaker than the last: #12773's regex could not see the two-step form, which is how that package's own family went from 1 to 3 mid-card; the count on #12792 was published twice as a floor and once had to be corrected in public because grep -c counted a docblock line quoting the shape in prose.
What is measured today
Swept on origin/main at 23843d3f4 with a TypeScript-AST instrument over 5,096 tracked .ts files — comments are not nodes and line breaks are not syntax, so both regex blind spots are closed — for all four shapes:
- live receiver-detach defects outside
driver-sql and the landed plugin-auth fix: 0.
- 2 remaining parenthesized-callee sites are on
console, whose methods are bound in Node and in browsers: packages/rest/src/log.ts:16, packages/cli/src/utils/dev-restart.ts:275.
- 25 remaining two-step sites are the idiomatic options-callback shape — a caller-supplied plain function with a
console default (opts.warn, opts.info) in objectql/registry.ts, spec/data/object.zod.ts, runtime/artifact-reference.ts, cli/utils/console.ts, cli/utils/artifact-boot-migration.ts, service-datasource/sqlite-driver-fallback.ts — not a method lifted off a receiver-sensitive class.
- 0 destructured channels and 0 channels handed on as a bare callback argument, in non-test source.
⚠️ Note the shape of the return path, which is the reason this is worth recording rather than forgetting: those 25 options APIs are typed warn?: (msg: string) = a void return, which invites a caller to write warn: logger.warn at the call site. That spelling detaches at the caller, where no reviewer of the callee will ever see it. Today no in-repo caller does it (measured: 0), and nothing would notice if one started.
Why a gate is plausible and what it would cost
The instrument is small and mechanical: walk each source file, flag ① a CallExpression whose callee is a ParenthesizedExpression resolving to log-channel property reads, ② a local assigned such a read and later called bare, ③ a channel destructured off a sink, ④ a channel passed as a bare argument. #12792's PR checks a version of exactly this into driver-sql as a package-local pin (logger-receiver-detach.test.ts §3), with a control sample proving it fires, which is the working prototype — but it only ever reads one file.
⛔ Not proposed here, deliberately: the false-positive surface is the whole question and it has not been measured. The console and options-callback populations above are both legitimate, and a gate that flags them is a gate people turn off. Any real proposal has to say how it tells "a method off a receiver-sensitive object" apart from "a plain function in an options bag" — probably by anchoring the receiver vocabulary the way DURABILITY_CRITICAL_CALLEES anchors its own, rather than by matching the channel names.
Also unrecorded: the ADR-0120 D4 note is now stale in one direction
The docblock at sql-driver.ts:4290 said check-durability-degradation-log-level.mjs cannot see a call on a parenthesized expression, so the inline form read to it as a false silent-swallow. That is still true of the matcher, and the blind spot is still unfiled as fixed. #12792 measured that it costs nothing in driver-sql today — none of the nine sat inside a catch guarding a DURABILITY_CRITICAL_CALLEES operation, so the checker never discovered them and the audit output is byte-identical before and after the conversion. Recording that here so the next reader does not re-derive it: the matcher limit is real, and it is currently costing zero findings.
Dedup
Searched open issues for a repo-wide guard on this class: no hit (control query on the same session returns #12792, #9609, #5785, #4436, so the empty result is a reading rather than a broken search). #12773 and #12792 are the two per-package fixes and are not duplicates of this — both are scoped to one package and neither proposes a guard.
Generated by Claude Code
Filed unassigned and ungraded by the
os-devseat working #12792, sessionsession_01LZbWd2jNV1FErXTPSS4Dry. ⛔ Not graded, not routed, ⛔ not fixed. Recording an observation, not a defect — the sweep below found no live instance left.The observation
(logger.error ?? logger.warn)(…)— and its siblingsconst fn = logger.error; fn(…),const { error } = logger; error(…),run(logger.warn)— evaluate to the bare function and then call it, so the call runs withthis === undefined.@objectstack/core'sObjectLoggerdereferencesthison every channel (error/fatalviathis.writeErrorLike,debug/info/warnviathis.write), so any host that injects one turns those lines into aTypeError.The class has now been closed three times, one card at a time:
plugin-authHTTP 500 nulldriver-sqlgit grepsweep prompted by #12773's closing lineEach round found the next round's population by hand, and each hand instrument was weaker than the last: #12773's regex could not see the two-step form, which is how that package's own family went from 1 to 3 mid-card; the count on #12792 was published twice as a floor and once had to be corrected in public because
grep -ccounted a docblock line quoting the shape in prose.What is measured today
Swept on
origin/mainat23843d3f4with a TypeScript-AST instrument over 5,096 tracked.tsfiles — comments are not nodes and line breaks are not syntax, so both regex blind spots are closed — for all four shapes:driver-sqland the landedplugin-authfix: 0.console, whose methods are bound in Node and in browsers:packages/rest/src/log.ts:16,packages/cli/src/utils/dev-restart.ts:275.consoledefault (opts.warn,opts.info) inobjectql/registry.ts,spec/data/object.zod.ts,runtime/artifact-reference.ts,cli/utils/console.ts,cli/utils/artifact-boot-migration.ts,service-datasource/sqlite-driver-fallback.ts— not a method lifted off a receiver-sensitive class.warn?: (msg: string) =a void return, which invites a caller to writewarn: logger.warnat the call site. That spelling detaches at the caller, where no reviewer of the callee will ever see it. Today no in-repo caller does it (measured: 0), and nothing would notice if one started.Why a gate is plausible and what it would cost
The instrument is small and mechanical: walk each source file, flag ① a
CallExpressionwhose callee is aParenthesizedExpressionresolving to log-channel property reads, ② a local assigned such a read and later called bare, ③ a channel destructured off a sink, ④ a channel passed as a bare argument. #12792's PR checks a version of exactly this intodriver-sqlas a package-local pin (logger-receiver-detach.test.ts§3), with a control sample proving it fires, which is the working prototype — but it only ever reads one file.⛔ Not proposed here, deliberately: the false-positive surface is the whole question and it has not been measured. The
consoleand options-callback populations above are both legitimate, and a gate that flags them is a gate people turn off. Any real proposal has to say how it tells "a method off a receiver-sensitive object" apart from "a plain function in an options bag" — probably by anchoring the receiver vocabulary the wayDURABILITY_CRITICAL_CALLEESanchors its own, rather than by matching the channel names.Also unrecorded: the ADR-0120 D4 note is now stale in one direction
The docblock at
sql-driver.ts:4290saidcheck-durability-degradation-log-level.mjscannot see a call on a parenthesized expression, so the inline form read to it as a false silent-swallow. That is still true of the matcher, and the blind spot is still unfiled as fixed. #12792 measured that it costs nothing indriver-sqltoday — none of the nine sat inside acatchguarding aDURABILITY_CRITICAL_CALLEESoperation, so the checker never discovered them and the audit output is byte-identical before and after the conversion. Recording that here so the next reader does not re-derive it: the matcher limit is real, and it is currently costing zero findings.Dedup
Searched open issues for a repo-wide guard on this class: no hit (control query on the same session returns #12792, #9609, #5785, #4436, so the empty result is a reading rather than a broken search). #12773 and #12792 are the two per-package fixes and are not duplicates of this — both are scoped to one package and neither proposes a guard.
Generated by Claude Code