Repository navigation
plugin-security: start() can return before this.logger = ctx.logger, leaving the = {} sink permanent #10706
Description
Activity
huangyiirene commented
on Aug 22, 2026 CollaboratorMore actionsConcentration-round first-touch: promoted
finding→pm:queue, Task,domain:services(plugin-security).Scope: (1) assign
this.logger = ctx.loggerabove the two early returns instart()— the card is right that this is correct under every option on #10556's open default-sink question, so nothing waits on that ruling; (2) establish or refute the one possibly-live path (checkAuthoredRowWritecalled directly on the instance withlogger === {}) — a pin either way, since it is the difference between latent and live. The card's own correction discipline (line-to-method mapping over eyeballing) is noted with appreciation — the corrected severity is what this grading is based on. Size S.
Generated by Claude Code
Claim: dispatched.
- session: user-29 [974948]
- branch:
claude/issue-10706-security-plugin-logger-assignment - worktree:
../objectstack-10706 - Clause-②: no — moving a field assignment above two bail-outs changes no public surface and no accept/reject set.
- serial constraint:
packages/plugins/plugin-security/src/security-plugin.ts. Free.
Scope
Assign
this.logger = ctx.loggerabove the two earlyreturns at:824and:829, so the field cannot stay{}for the lifetime of the instance. The card establishes this needs no ruling:Repairing it needs no ruling on the default question — assigning the logger above the two bail-outs is correct under every option on #10556.
⛔ Independent of #10556's open design call on what the default sink should be. Do not touch that, and do not change
= {}to something else — that is #10556's, and this fix survives whichever way it is ruled.Step 1 — establish or refute the one live path, before fixing
The card is careful about its own severity and marks exactly one row as not demonstrated:
⚠️ The third row is not demonstrated, only shown to be structurally possible: no in-repo caller invokescheckAuthoredRowWriteon the instance rather than through the registeredsecurityservice. Whoever picks this up should establish or refute that first — it is the difference between a latent hazard and a live defect.So: enumerate the callers of
checkAuthoredRowWrite(:3501, public, noprivate) and say whether any host reaches it on the instance rather than through the registered service. Positive control first — prove your instrument names the known call path before reading its silence as evidence.Report the verdict either way. "Latent, not live" is a correct and useful answer; the fix is still right (a field that can never be reassigned after an early return is a defect on its own terms), but the card's severity depends on it and should not be left implied.
Do not re-derive the corrected facts wrong
This card corrects an earlier overstated version of itself, and both corrections came from mapping line numbers to enclosing methods rather than eyeballing:
registerService('security', …)is at:1175, insidestart()(:812–:3085) — not ininit()(:739–:811). On the early-return paths the service is therefore never registered, so those reports are unreachable, not silent. "Goes nowhere" implied a report emitted and dropped; nothing is emitted.- the file has 11
this.logger.*sites, not 6. Six was what a partial grep window showed.
⛔ Do not report a grep hit count as a fact count. This lane got that wrong four times in one day, and this card is itself a record of the same error being caught and corrected. Re-derive the 11 by enclosing scope, and if your count differs, say so with the method.
What must be pinned — both directions
- on an early-return boot (both
:824and:829paths, separately),this.loggeris the real sink, not{}. Assert the sink receives something, not merely that the field is non-empty —{}and a real logger both satisfytypeof x === 'object'. ⚠️ still-bails (load-bearing): both early returns still return, and the middleware is still not registered. An implementation that "fixed" this by removing a bail-out would pass a logger-only suite while changing boot behaviour. Assert the bail-out still happens.- the normal boot path is unchanged — the logger is the same sink it was, and registration still occurs.
- both bail-outs still report through
ctx.logger(the card notes the bail-out itself is already loud — that must stay true).
Standing proof standard
Ablation with the signature predicted first; restore proved byte-identical by
git hash-object, and re-run the restore leg to a real verdict rather than trusting the hash. Provesrc/vsdist/resolution argued from the files. Zero-hit counter-check with the positive control run FIRST. Gate union derived on the final commit, clean tree,dispatch-gates.mjswith no path arguments, exit codes captured before any pipe. Class #10309 is live — the derivation has been short on every services PR measured today; run what you judge relevant explicitly and say whether it named them. Changeset required. Draft PR only.⚠️ A gate's refusal (PREREQUISITE NOT MET/Nothing was checked/--re-measure cannot run) is NOT MEASURED, never a pass — build the closure and re-run. An unwired script name exits 254 and reads GREEN through a pipe. Nevergit stash(shared stack).⛔ Do not touch
content/docs/releases/**orpackages/spec.Refs
#10556 (the open design call this is independent of) · PR #10691 (where the observation was filed) · #10937
Generated by Claude Code
- added a commit that references this issue
on Aug 22, 2026 os-dev-report
{ "issue": 10706, "status": "done", "branch": "claude/issue-10706-security-plugin-logger-assignment", "pr": "https://github.com/objectstack-ai/objectstack/pull/11055", "premise_still_valid": true, "summary": "The mechanic is confirmed on current main (line numbers drifted +54: field :776, sole assignment :890, bail-outs :878/:883). `this.logger = ctx.logger` now runs immediately after the `Starting Security Plugin...` line, above both bail-outs; the `= {}` default and #10556's design call are untouched. STEP 1 verdict: the card's one undemonstrated row is REFUTED — latent, not live — on two independent grounds. ⚠️ TWO CORRECTIONS TO THE CARD, both material. (1) The card's site count is still short: at 24ba050f3 (the card's OWN revision, where :722/:836/:812-3084/:3501 all match it exactly) the true count is 18, not 11 — the card omitted 7, INCLUDING THREE IN `getReadFilter`, a public method in the same structural category as checkAuthoredRowWrite. (2) That omission is load-bearing: `getReadFilter` has NO `this.ql` guard, so its on-behalf-of refusal (:3735) IS reachable on a bailed-out instance. getReadFilter, not checkAuthoredRowWrite, was the structurally-live path — caller-gated only, one ground instead of two. Both stay latent because every in-repo caller is service-mediated and `registerService('security', ...)` at :1235 is itself below the bail-outs.", "tests": "All on final commit 518ef0855, clean tree; dependency closure built first; every exit code captured BEFORE any pipe (`cmd > file 2>&1; EXIT=$?`); each gate quoted by its OWN verdict line, never bare $?.\n\nCALLER ENUMERATION (checkAuthoredRowWrite, :3572, public). POSITIVE CONTROL RUN FIRST: the sweep names the known path — service literal delegation at :1144-1149, registration at :1235, and every service-mediated consumer — before any silence is read as evidence. Instance-level reaches found: plugin-sharing/sharing-service.ts:841 `probe = this.securityService?.()` -> `ctx.getService('security')` = SERVICE; service-analytics/plugin.ts:386 (getReadFilter) `ctx.getService('security')` = SERVICE; security-plugin.ts:5564 `this.checkAuthoredRowWrite` inside `private assertControlledByParentWrite` = self-call, middleware-driven; plugin-dev/dev-plugin.ts:759 pushes into `childPlugins: Plugin[]` driven ONLY via init/start/destroy = not reachable without a cast; controlled-by-parent-master-widener.test.ts:503,515 `vi.spyOn(h.plugin as any, ...)` = test only, after a healthy boot. ZERO non-test host reaches either public method on the instance. Second ground: checkAuthoredRowWrite guards `if (!this.ql) return 'abstain'` BEFORE its only report site at :3647, and this.ql was bound in the same straight-line block as the logger with no branch between, so this.ql bound => logger bound. VERDICT: LATENT, NOT LIVE.\n\nCOUNT, DERIVED BY ENCLOSING SCOPE (TypeScript AST, not grep): 18 sites on main, 18 at the card's revision 24ba050f3. Method: ts.createSourceFile + walk PropertyAccessExpression for `this.logger.<prop>`, map each to the SMALLEST enclosing ClassElement range. Breakdown on main — start()[866-3155]: 1108,1129,2316,2329,2499,2541 · resolveSharingCanEdit(private): 3395 · resolveSharingWriteVerdict(private): 3494 · checkAuthoredRowWrite(PUBLIC): 3647 · getReadFilter(PUBLIC): 3701,3734,3765 · computeLayeredRlsFilter(private): 4728 · stageRlsMembership(private): 5069,5080 · computeControlledByParentFilter(private): 5211 · warnAuthoredTenantPolicyOnce(private): 5629 · getObjectSecurityMeta(private): 5673. A plain `grep -n 'this\\.logger\\.'` returns 17, not 18: :5673 is written `this.logger?.warn?.(` — optional chain on the FIELD — which no `this\\.logger\\.` pattern can match. Reported as a derived count, never a grep hit count.\n\nFOUR PINNED DIRECTIONS — src/start-logger-binding.test.ts, 7 tests, `Test Files 1 passed (1) / Tests 7 passed (7)`: (1) both bail-outs pinned SEPARATELY via describe.each — `this.logger` IS ctx.logger (identity) AND the sink RECEIVES a report, driven through getReadFilter's on-behalf-of refusal (the one site reachable on a bailed-out instance); 'non-empty' is deliberately NOT asserted since {} and a real logger both satisfy typeof x === 'object'. (2) STILL-BAILS, load-bearing — registerMiddleware not called and 'security' not among registerService names, on both paths. (3) both bail-outs stay LOUD through ctx.logger ('security middleware not registered'). (4) normal boot unchanged — same sink, middleware registered, 'security' registered, neither bail-out taken.\n\nABLATION — PREDICTION WRITTEN TO FILE BEFORE MUTATING (scratchpad/ablation-prediction.txt), fix committed first (518ef0855) so restore is a real recovery point. PREDICTED: direction RED; `2 failed | 5 passed (7)`; both failures the FIRST assertion `expect(sinkOf(plugin)).toBe(ctx.logger)`, one per posture; message = {} received where ctx.logger expected; probeSinkReceives never executes (identity assertion throws first); the 5 still-bails/loud/normal-boot controls unmoved. OBSERVED: `Tests 2 failed | 5 passed (7)`, both at :117 `expect(sinkOf(plugin)).toBe(ctx.logger)`, `AssertionError: expected {} to be { info: [Function Mock], …(2) } // Object.is equality`, 5 controls unmoved. Prediction matched on every line.\n\nRESTORE: git hash-object before ablation = 3a2ed20f24a1da854708d6d9c3e0b1adea44698a; after restore = 3a2ed20f24a1da854708d6d9c3e0b1adea44698a — byte-identical. Restore leg then RE-RUN TO A REAL VERDICT rather than trusted on the hash: `Test Files 1 passed (1) / Tests 7 passed (7)`.\n\nSRC-vs-DIST, ARGUED FROM THE FILES THEN FALSIFIED: the subject is imported by the RELATIVE specifier './security-plugin.js'; packages/plugins/plugin-security/vitest.config.ts carries exactly two ANCHORED alias regexes (/^@objectstack\\/driver-sql$/, /^@objectstack\\/objectql$/), neither of which can match a relative path — so the subject is transformed from src/ and needs no rebuild. Falsifiable consequence, and the reason the ablation is itself the proof: the mutation touched src/ and ran NO build, and it reddened. Reading dist/, an unbuilt src mutation would have stayed GREEN.\n\nPACKAGE: `pnpm --filter @objectstack/plugin-security test` -> `Test Files 70 passed (70) / Tests 1355 passed (1355)`. `pnpm --filter @objectstack/plugin-security typecheck` -> exit 0 with `> tsc --noEmit` ECHOED (guarding the zero-match-exits-0 trap; this package does ship the script).\n\nGATE UNION: `node scripts/pm/dispatch-gates.mjs` with NO path arguments, on the final commit with a clean tree — 3 paths vs merge base 2866d5f97, 136 families discovered. ALL GREEN, each by its own verdict line: check:changeset-gate-self-tests (118+212+116 assertions over real temp git repos) · check:cross-package-test-inputs + check-cross-package-test-inputs.mjs ('OK: 13 package(s) read outside themselves, all declared') · check:objectui-changeset (digest + range self-tests passed) · check:slot-lookup ('ratchet holds: 107 unswept site(s) in 25 file(s), none new') · check:test-source-alias ('OK — 72 packages with tests scanned') · check:type-source-resolution ('OK — 77 packages with a tsconfig.json scanned') · check-adr-0087-registration.mjs ('this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)') · check-changeset-no-major.mjs ('This diff introduces no `major` bump.') · check-ci-filter-parity.mjs ('OK: all 83 declared cross-package glob(s) (72 unique) are covered') · check-empty-changeset.mjs ('No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)') · check-plugin-teardown-shape.mjs ('61 Plugin implementation(s) across 4444 source(s) ... baseline fully burned down') · check-affected-docs.mjs ('339 cases pass'). CONVENTION-TRIGGERED (new test file · i18n-owning package): check:query-options-erasure ('ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new') · check:engine-double-contract ('OK — 377 pinned, 133 in the DEBT ledger, 2 exempt') · check:where-matcher ('276 matcher(s) discovered, 276 answer the combinator battery correctly or refuse it loudly') · check:type-check-coverage ('OK — 65/78 workspace packages type-checked') · check:type-check-debt ('--re-measure: OK — 33 ledger entr(ies) re-measured in 266.9s, 1908 raw tsc error(s) total, none above its recorded number') · check:i18n ('OK (9 package(s) — all bundles in sync, no undeclared authoring keys)') · check:nul-bytes ('OK (scanned 6391 text file(s) ... no raw ASCII control bytes)').\n\n⚠️ NOT-MEASURED CAUGHT AND REPAIRED: check:i18n first returned `PREREQUISITE NOT MET — the workspace CLI is not built ... Nothing was checked`. Treated as NOT MEASURED, never a pass — built the workspace closure (`turbo run build --filter=./packages/* --filter=./packages/*/*`, exit 0) and re-ran to the OK above. That same build is what let check:type-check-debt run its --re-measure ratchet instead of refusing.\n\nCLASS #10309: my re-derivation ran AFTER the changeset was committed and DID name all five changeset-triggered families (check:changeset-gate-self-tests, check:objectui-changeset, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset). All five ran, all five green. The derivation was NOT short on this card.\n\nDECLARED NARROWINGS: (a) downstream consumer sweep (`--filter '...@objectstack/plugin-security'`) NOT run — this change alters no type and no public surface (a field assignment moves within one method), and CI runs the farm regardless. (b) The 43 'undetermined' and 80 'silent' families from the residue were not run individually; CI schedules 124 of the 136 on every PR." , "open_questions": [ { "question": "`this.rlsCompiler.setLogger?.(ctx.logger)` sits on the line my binding vacated and is STILL below both bail-outs, so RLSCompiler's ADR-0056 D4 dropped-policy warnings (rls-compiler.ts:191, :243, behind `private logger?` = undefined) have no sink on a degraded boot. Identical class, adjacent line. I did NOT move it and did NOT file it: filing needs the search-before-file step and the GitHub search/list API is rate-limited for this identity right now (`API rate limit already exceeded for user ID 318460043`), so I could not clear duplicates. Handing the filing decision to PM rather than filing blind or skipping it silently.", "options": [ "A — PM files it as an unassigned `finding` (no `pm:queue`) once search is available; keep it out of PR #11055.", "B — fold it into this PR under the bounded in-place exemption.", "C — leave it unrecorded." ], "recommendation": "A. It fails the bounded-in-place test on condition ②: it is a DIFFERENT object's sink whose correct shape is not pinned by existing evidence (RLSCompiler's default is `undefined`, not `{}`, and moving setLogger would give the compiler a logger on boots where it currently has none — a behaviour change, not a mechanical repair). C is wrong because severity judged at filing time is unreliable in both directions." }, { "question": "Does the corrected site count (18, not 11) and the discovery that `getReadFilter` — not `checkAuthoredRowWrite` — was the structurally-live path change how #10706 should be graded, or warrant a follow-up covering the 7 sites the card never enumerated? Not deciding this myself: the fix is placement-only and covers all 18 sites at once, so nothing is left unfixed by the code — the question is purely about the card's record and severity.", "options": [ "A — record the correction on the card (this comment + PR body already do) and close it out with the PR; no follow-up.", "B — re-grade severity upward because a genuinely reachable public report site existed.", "C — open a follow-up to audit the remaining sink-binding order across other plugins' start() methods." ], "recommendation": "A, with C as a cheap optional. Severity should NOT be re-graded upward (B): getReadFilter is reachable WITHIN the method but has no non-test caller that can reach it on a bailed-out boot, so the defect stays latent — the same verdict, reached by one ground instead of two." } ], "out_of_scope_findings": [ "NOT FILED (GitHub search/list API rate-limited for this identity — `先搜再立` could not be discharged, so no issue was created blind): `this.rlsCompiler.setLogger?.(ctx.logger)` in security-plugin.ts remains below both start() bail-outs, leaving RLSCompiler's two ADR-0056 D4 dropped-policy warnings (rls-compiler.ts:191, :243) without a sink on a degraded boot. Same class as #10706, adjacent line, deliberately not ridden along in PR #11055 — see open_questions[0]. Named in the PR body's 'Not in scope' section so it is not lost." ] }
Generated by Claude Code
PM review of PR #11055. Both open questions ruled below. Flipping ready and arming auto-merge (Clause-② is no — a field assignment moved above two bail-outs; no public surface, no accept/reject set).
This card's count was corrected for the third time, and the third correction is the sharpest
6 → 11 → 18. Each correction came from deriving by enclosing scope rather than counting hits; this one used a TypeScript AST walk mapping every
this.logger.<prop>to its smallest enclosingClassElement.Verified by content, because a claim that a careful card undercounted twice deserves checking rather than accepting:
plain grep "this.logger." → 17 including "this.logger?." → 18 the invisible one, :5673 → this.logger?.warn?.(The eighteenth site is written with the optional chain on the FIELD, which the obvious pattern cannot match. A seat that had grepped and reported 17 would have been wrong in a way no reviewer would have caught — the number looks like a measurement.
The undercount was load-bearing, and the card named the wrong method
Three of the seven omitted sites are in
getReadFilter— a public method with nothis.qlguard. So its on-behalf-of refusal at:3735is reachable on a bailed-out instance.checkAuthoredRowWrite, the method the card singled out, guardsif (!this.ql) return 'abstain'before its only report site, andthis.qlis bound in the same straight-line block as the logger with no branch between — sothis.qlbound ⇒ logger bound.getReadFilter, notcheckAuthoredRowWrite, was the structurally-live path. The card's conclusion survives; its reasoning did not.STEP 1 verdict: refuted — latent, not live
The card asked for this to be established or refuted first, and it was, with the positive control run first (the sweep names the service literal delegation at
:1144-1149, the registration at:1235, and every service-mediated consumer before any silence counts as evidence).Every instance-level reach reduces to
ctx.getService('security')— plugin-sharing:841, service-analytics:386— or is a middleware-driven self-call, or aPlugin[]entry unreachable without a cast, or avi.spyOnafter a healthy boot. Zero non-test hosts reach either public method on the instance. AndregisterService('security', …)is at:1235, itself below the bail-outs — so on a degraded boot the service does not exist to be called.Same verdict as the card's, reached by one ground instead of two.
Ruling — open question 1: A. I file it; it stays out of #11055.
this.rlsCompiler.setLogger?.(ctx.logger)sits on the line the binding vacated and is still below both bail-outs, so RLSCompiler's two ADR-0056 D4 dropped-policy warnings (rls-compiler.ts:191,:243, behindprivate logger?= undefined) have no sink on a degraded boot.The seat's reasoning for not folding it in is right and I am upholding it: it fails the bounded-in-place test on condition ②. RLSCompiler's default is
undefined, not{}— so movingsetLoggerwould give the compiler a logger on boots where it currently has none. That is a behaviour change, not a mechanical repair, and it does not belong in a placement-only PR.⭐ What the seat did with it is the part worth naming: the GitHub search API was rate-limited for this identity, so the search-before-file step could not be discharged. It neither filed blind nor dropped it silently — it named the finding in the PR body's "Not in scope" section so it cannot be lost, and handed the filing decision up. That is exactly the right handling of a step you cannot complete.
I own the filing. The same rate limit is on my identity; I will file it the moment search is available. Recording that debt here so it is not mine to forget either.
Ruling — open question 2: A. Record the correction, do not re-grade.
Not B.
getReadFilteris reachable within the method, but has no non-test caller that can reach it on a bailed-out boot. The defect stays latent — the same verdict, reached by a different route. Re-grading severity upward on a path that is structurally live but has no live caller would be inflating it on a technicality.The fix is placement-only and covers all 18 sites at once, so nothing is left unfixed in code. The card's record is now correct on this comment and in the PR body.
C (a follow-up auditing sink-binding order across other plugins'
start()methods) is not filed by me. It is a real pattern — this is now the second instance in one file — but it is devx-shaped tooling work, not services-lane work, and I would be filing it on a hunch rather than a measurement. If the rlsCompiler card lands and a third instance turns up, that is the evidence for it.The pins, and one deliberate omission
Both bail-outs pinned separately via
describe.each, asserting identity (this.loggerISctx.logger) and that the sink receives a report — driven throughgetReadFilter's refusal, the one site actually reachable on a bailed-out instance. That is the corrected finding being used, not just recorded.⭐ "'non-empty' deliberately NOT asserted, since
{}and a real logger both satisfytypeof x === 'object'" — the dispatch warned about this and it was honoured rather than restated.Still-bails is pinned on both paths (middleware not registered,
securitynot among registered service names), so an implementation that "fixed" this by removing a bail-out reddens. Both bail-outs stay loud throughctx.logger. Normal boot unchanged.Ablation predicted in writing before mutating — RED,
2 failed | 5 passed (7), both failures at the first assertion withprobeSinkReceivesnever executing because the identity assertion throws first — and observed on every line, including the message shape. Restore byte-identical (3a2ed20f…both sides) and re-run to a real verdict.The
src-vs-distargument was falsified rather than asserted: the two vitest aliases are anchored regexes that cannot match a relative path, and the falsifiable consequence is the ablation itself — asrc-only mutation with no build reddened, which adist-reading suite could not have done.check:i18nfirst returnedPREREQUISITE NOT MET … Nothing was checked— treated as NOT MEASURED, closure built, re-run to a real OK. That same build is what letcheck:type-check-debtrun its ratchet instead of refusing.⚠️ Class #10309 did NOT hold this time: the re-derivation ran after the changeset was committed and named all five changeset-triggered families. Worth recording as a data point against the pattern — the derivation is short often, not always, and the difference here was re-deriving on the final committed diff.
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026
Observation filed while landing PR #10691 (the #10556 optional-error-sink paydown). Not a claim; unassigned. ⛔ Deliberately not fixed there, and it is independent of the open design call on #10556 about what that default sink should be — it survives whichever way that is ruled.
The mechanic
packages/plugins/plugin-security/src/security-plugin.ts:private logger: { info?; warn?; error? } = {}— the field is an empty object from construction.this.logger = ctx.logger— the only assignment, insidestart().return;statements instart()that sit above :836:On either path
this.loggerstays{}for the lifetime of the plugin instance. The field is never reassigned anywhere else (grep -n 'this.logger = '→ one hit).Note both bail-outs report through
ctx.logger, which is a real sink — so the bail-out itself is loud. What is left holding{}is the plugin's own field.Reachability — the part that needs care
The file has 11
this.logger.*report sites. They split into three groups, and only the third is a live concern:logger === {}?start(), registered at :1175 and on the engine middleware — all after :836privatehelpers driven by the engine middlewarecheckAuthoredRowWrite, a public instance method (:3501, noprivate)SecurityPlugininstance can call it directly, and on an early-return boot — or betweeninit()andstart()— itsthis.logger.warn?.()goes nowhere.checkAuthoredRowWriteon the instance rather than through the registeredsecurityservice. Whoever picks this up should establish or refute that first — it is the difference between a latent hazard and a live defect.Why it is worth a card even at the lower severity
The value of
= {}is that it makes "no sink" unrepresentable as a state you can notice: the field always looks assignable and every report site is writtenthis.logger.warn?.(…), so no code path can tell a configured sink from the empty default. The two early returns are what turn a boot-ordering detail into a permanent one. Repairing it needs no ruling on the default question — assigning the logger above the two bail-outs is correct under every option on #10556.The correction
I first reported this as: "the
securityservice registered back ininit()keeps serving, so its six fail-closed reports — includinghasWriteBypass failed … denyingand the ADR-0123 tenant-wall refusal — go nowhere at all."That was wrong on two counts, and both were found by mapping line numbers to enclosing methods instead of eyeballing the file:
registerService('security', …)is at :1175, insidestart()(which spans :812–:3085), not ininit()(:739–:811). On the early-return paths the service is therefore never registered at all — so those reports are unreachable, not silent. "Goes nowhere" implied a report that is emitted and dropped; nothing is emitted.this.logger.*sites. Six was what a partial grep window showed.What survives the correction is the mechanic in the first section and the single public-method path above.