Repository navigation
fix(plugin-security): the batched seed existence read's own failure reaches the author with no logger injected - #19281
Conversation
…lure through reportThroughSink `seed-name-lookup.ts` reported the batched existence read's failure and its truncation through a doubly-optional `logger?.warn?.(...)`: a caller that injected no sink got no output at all — the read failed, the oracle switched to the slow per-item path, and no human was told. This is the sixth instance of that shape and it takes the ONE derivation already landed for it rather than a sixth hand-written copy. Delivery only: the wording, the structured meta and the two named causes are axis-specific and stay at the call site, and a read that answered is as silent as it always was. The sink is handed over only once it carries a callable `warn` — the same `typeof` question `reportThroughSink` asks, moved one frame up because `SeedLookupLogger.warn` is optional where `CollisionReportSink.warn` is not. Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF Co-authored-by: Claude <noreply@anthropic.com>
…geset The `[#18091]` UNREADABLE ROWS control in `bootstrap-declared-capabilities.test.ts` is the harness this repair was measured in: with no logger injected it captured exactly ONE author-visible line while the batched existence read that failed first stayed silent. Both sites now deliver through the shared sink, so the control reads TWO — selected by content, because the read fails before the pass summarises. Claude-Session: https://claude.ai/code/session_01AhQASwqJr2Z7XfGWUdvnbF Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 15 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 2530f4cbc71bd8e5657bd04115f3543f1aaeff58 && git checkout 2530f4cbc71bd8e5657bd04115f3543f1aaeff58
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e6a03e649154252740b8e3534804e7abc453fb5c 9830ddcc8d38e5f4e50a752ab115da9c55b6fc30 && git checkout -B drift-repro e6a03e649154252740b8e3534804e7abc453fb5c && git merge --no-ff 9830ddcc8d38e5f4e50a752ab115da9c55b6fc30
node scripts/docs-audit/affected-docs.mjs --json e6a03e649154252740b8e3534804e7abc453fb5c |
Fixes #18570
Clause-②: no
What this changes — one site, the delivery only
packages/plugins/plugin-security/src/seed-name-lookup.tsreported the batched existence read's own failure through a doubly-optionallogger?.warn?.(…). That call evaluates to NOTHING when the caller injected no sink: the read failed, the oracle silently degraded to the per-item read, and no human was told.It now reports through
reportThroughSink— the single derivation #18091 landed for exactly this rule, so this sixth instance of the shape (#17516 · #18023 · #18091's five sites) does not become a sixth hand-written copy.⛔ Exactly one site. The triage seat bounded the caliber in as many words and it is not widened here:
No other
logger?.warn?.(spelling is touched, in this package or anywhere else.permission-set-projection.ts,bootstrap-declared-permissions.tsandPermissionSeedOutcomeare untouched — #18571 owns that surface.Delivery only. The wording, the structured meta (
object,names,rowBudget,organization) and the two named causes —unreadableandtruncated— are axis-specific and stay at the call site, which is the splitseed-refusal-sink.tsdocuments. A read that ANSWERED is as silent as it always was, on every channel.Why the call site asks
typeofrather than handing the parameter straight over:SeedLookupLogger.warnis OPTIONAL whereCollisionReportSink.warnis not, so the host is handed over only once it really carries a callablewarn— the same questionreportThroughSinkitself asks, moved one frame up so the declared types stay honest instead of being asserted past. ⛔seed-refusal-sink.tsis not edited.The evidence bar — a DRIVEN, differential reading, because that is how the card was graded
New suite
seed-name-lookup-degradation-audible.test.ts, five cases. Case 1 is the card's own shape: an unreadable-database pass with no logger argument at all, run beside a lit control in the same run —reportCapabilityRowsUnreadable, an already-repaired #18091 site that reaches the console with no sink. A zero from the site under repair is a reading only while that control fires.The other four are what keeps case 1 from being satisfiable by an unconditional
console.warn:truncatedcause travels the same way,rowBudgetand allwarnis not callable reaches the author without throwingReverse verification — the ablation, run from the committed state
The fix was committed first, then
seed-name-lookup.tsalone was restored to the merge base on disk (proved landed:reportThroughSink(count 1 → 0, blob4d7c35fe→83629f98), the suite re-run, and the file restored toHEAD(proved:git diff HEADempty, on-disk blob back to4d7c35fe).Ablated run — 3 of 5 red, and the failure lines are the measurement:
expected [] to have a length of 1).TypeError: logger?.warn is not a functionthrown out ofbuildExistingByName— so the old spelling did not merely mute a sink-less host, it aborted the seeding pass for a host that declaredwarnand shipped something else.?.()guardsnull/undefined, never a non-callable.One file outside the declared surface, named here with its evidence
The dispatch's declared file surface was
seed-name-lookup.ts+ its test. One more file had to move, and it is this card's own harness rather than a second repair:bootstrap-declared-capabilities.test.ts→[#18091] UNREADABLE ROWS: prints with NO LOGGER INJECTEDassertedexpect(cap.seen).toHaveLength(1). That ONE is the measurement #18570 was filed on. With both sites now audible the same pass prints two, so the pin readstoHaveLength(2)and selects each line by CONTENT (the read fails before the pass summarises, so the #18570 line arrives first). Every content assertion the case already made is preserved, now against its own line. ⛔ No source file of that seeder is touched, and no other assertion in it moves.Measured before editing it: across 24 open PRs and 325 changed-file rows,
seed-name-lookup.tsis held by 0 andbootstrap-declared-capabilities.test.tsby 0, with a same-package firing control — PR #18319 does holdpackages/plugins/plugin-security/src/suggested-audience-bindings.test.ts— so both zeros are readings, not a dead scan.What was measured
Exit codes captured before any pipe, in every case.
pnpm --filter @objectstack/plugin-security test— exit 0,Test Files 115 passed (115) · Tests 2233 passed (2233). (One pre-existing case went red on the first run — the control above — and that red is the finding, repaired as described.)pnpm --filter @objectstack/plugin-security typecheck— exit 0. Three legs, read off the package's own script rather than assumed:tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json && pnpm check:test-typecheck. The third reports0 file(s) / 0 error(s) / 0 pinned signature(s); this package carries notest-typecheck-debt.json.pnpm --filter '@objectstack/plugin-security^...' build— exit 0 (dependency closure), and later the whole./packages/*+./packages/*/*closure,72 successful, 72 total.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands⇒ 60 commands. All 60 run, all exit 0. Reconciled with--ran:60 derived famil(ies) accounted for — 60 run, 0 NOT-MEASURED (a DERIVED zero — all 60 recorded an exit code and none of them is 3), 0 UNRUN.check:dual-build-cjs-loads,check:i18n,check:type-check-debt— each naming a built-output prerequisite. The closure was built and all three re-run to exit 0. ⛔ None was reported as a pass on its 3.pnpm lint(eslint . --no-inline-config, the repo-wide run) — exit 0. Run whole; no narrowing claimed.src/index.tsre-exports nothing fromseed-name-lookup.ts, and the builtdist/index.d.tscarriesSeedLookupLogger0 times against a positive control ofSecurityPlugin22 times. ⇒Clause-②: nostill holds.Two verdict lines worth carrying out of logs that die with the worktree:
check:engine-double-contract—UNRECOGNISED [engine-double-contract]: 9 construct(s) in packages, examples declare a scanned verb (delete, update, findOne) alongside engine siblings, and this gate could not read the implementation -- so they are in NEITHER the pinned population nor the ledger. 213 further construct(s) are SCOPED OUT by a stated criterion and are not counted here. This is a verdict, not a finding: it never fails a run (#9747, ruling of 2026-08-18).None of the nine is a file in this diff.check-changeset-no-major --base origin/main, verbatim:⇒ that axis is measured on this PR by CI, off the
Clause-②: noline above.Changeset
patchon@objectstack/plugin-security. Decided by reading, not habit: the package declares noprivateand shipsfiles: ["dist", "README.md", "CHANGELOG.md"], andseed-name-lookup.tscompiles into thatdist— so this publishes.Acceptance notes
logger?.warn?.(…)does not protect a host whosewarnis present but not callable; it throws. That is a property of the spelling, so every remaining site in the class carries it, and each of those sites belongs to its own card (triage's bound above). Recorded here rather than filed because the class already has an owner:reportThroughSinkis the repair, and the remaining sites are already enumerated on the sibling cards. 承接者: the seat that dispatches the next site in this class.SeedLookupLoggerstill declaresinfo?, which nothing inseed-name-lookup.tsreads. Dead declaration, not a defect, and out of scope for a Clause-②nocard. 承接者: 无.Generated by Claude Code