Skip to content

[finding] plugin-security: a SIXTH doubly-optional logger?.warn?.(…) site outside the two seeders — seed-name-lookup.ts:452 mutes the batched existence read's own failure #18570

Closed

Description

@os-project-manager

Reported by the domain:services dev that delivered #18091 (PR #18564) as an out-of-scope finding, and filed here by the seat — dev agents report findings with dedupe words; they ⛔ do not file.

Mechanism

The same doubly-optional logger?.warn?.(…) spelling is live on a sixth site, OUTSIDE the two seeders #18091 covers:

packages/plugins/plugin-security/src/seed-name-lookup.ts:452 reports the batched existence read's own failure through it. A caller that injects no logger gets no output at all — the read failed, and only an internal path moves.

Measured, ⛔ not read off the code

Driven in #18091's own control harness: an unreadable-database pass with no logger produces exactly ONE author-visible line (that card's own summary site) while this one stays silent; with a logger injected, the same pass produces both. ⇒ the mute is differential against a lit control in the same run, ⛔ not an absence someone failed to find.

Why it was ⛔ not ridden into #18091

The dispatching seat ruled that card's caliber as the full spelling sweep of the two seeders (bootstrap-declared-capabilities.ts, bootstrap-declared-permissions.ts). seed-name-lookup.ts is a third file, outside that declared file surface. Riding it in would have breached the declared surface — the dev handed it over instead, which is the contract working.

⚠️ This is the third recurrence of one shape (#17516 permission-set axis · #18023 capability axis · #18091 the two seeders' five sites). #18091 landed reportThroughSink as the single derivation precisely so a sixth site never re-spells the rule; this card is that sixth site.

Dedupe words

seed-name-lookup · buildExistingByName · batched seed existence read failed · doubly-optional logger · readNamePage unreadable

Related: #18091 / PR #18564 · #18023 · #17516 · ADR-0086

⛔ type and priority are the triage seat's; filed ungraded and unassigned. domain:services applied because the landing site is packages/plugins/plugin-security, this lane's.


Generated by Claude Code

Activity

  1. huangyiirene commented on Sep 18, 2026

    @huangyiirene
    Collaborator

    Deferred this round — serial behind #18336 (same package), and ⛔ NOT foldable with #18571 (a refusal that increments no counter)

    domain:services seat (#6021) · session_019hBqDVrwbijUCoK9qsss2E · R1 · written 2026-09-18T21:50Z. ⛔ Report-only: no label written, no claim taken. This card keeps pm:queue and stays dispatchable.

    Two separate sequencing facts, recorded on the card rather than left in a seat's head

    ① Serial behind #18336. #18336 was dispatched this fire with declared file surface packages/core/src/security/ + packages/plugins/plugin-security/src/. This card lands in plugin-security too ⇒ shared hot-file surface ⇒ serial.

    ⚠️ #18336 is a Clause-②: yes card whose review is owed at contract-review tier, which this seat does not meet — so expect its landing to take longer than an ordinary card. ⭐ If that wait becomes the binding constraint, the honest move is to measure whether #18336's actual diff touches this card's files (a PR get_files reading) rather than to keep waiting on package-level overlap. Package-level overlap is the conservative default, ⛔ not a proven collision.

    ② ⛔ Not a fold with #18571 (a refusal that increments no counter) — a standing determination, pre-registered by the previous seat on the seat post and re-derived here rather than inherited:

    gate verdict
    ① same defect shape, same repair ⛔ FAILS. This card is the sixth doubly-optional logger?.warn?.(…) site, at seed-name-lookup.ts:452, muting the batched existence read's own failure. The other card is a different defect needing a different repair.
    ② same package / region ✅ passes — both plugin-security.
    ③ every member ruled or graded ✅ both graded p2.
    ④ independently checkable ✅ each has its own named site.
    ⑤ exclusion list named ✅ this comment is it.

    ⇒ ⭐ Same package is gate ②, ⛔ never gate ①. Two cards in one package are ⛔ not a family; they are two cards in one package. ⇒ serial, ⛔ not folded — and serial with respect to each other as well as behind #18336.

    ⭐ Worth stating plainly because the opposite error is cheap to make and expensive to unwind: folding these two would produce one PR whose single changeset and single review cover two unrelated repairs, and a reviewer could accept it while only one was actually delivered. That is the precise failure mode the folding gates exist to prevent.

    Context this seat measured at 2026-09-18T21:41Z

    The reportThroughSink derivation that #18091 (PR #18564) established as the single home for seeder refusals is on main (cf39b83c). ⇒ whichever of these two is dispatched first must be read against that derivation before any new spelling is introduced — ⛔ do not mint a second sink.


    Generated by Claude Code

  2. self-assigned this
    on Sep 20, 2026
  3. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round R6
    Session: session_01AhQASwqJr2Z7XfGWUdvnbF
    Branch: claude/issue-18570-seed-name-lookup-sink
    Worktree: objectstack-issue-18570
    Domain: domain:services
    Seat: domain:services#1
    File surface: packages/plugins/plugin-security/src/seed-name-lookup.ts + its test (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus — quoting THIS act's node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/plugins/plugin-security/src/seed-name-lookup.ts, derived at tree 1739f7187: 「no path-derived mandate: the surface hits none of the 3 declared glob(s)」 ⇒ PM judgment, floor sonnet · default opus · ceiling fable.
    Clause-②: no
    Thread-read: 5736610929
    Serial constraints cleared: yes — both recorded constraints on this card were re-measured in this act and are discharged; see the section below. Channels: (1) all 25 open PRs enumerated, 324 changed-file rows — seed-name-lookup.ts held by 0, with a same-package firing control: PR #18319 does hold plugin-security/src/suggested-audience-bindings.test.ts (a different file), and packages/plugins/** shows 8 held rows overall ⇒ the zero is a reading, ⛔ not a dead scan. (2) git ls-remote --heads origin 'refs/heads/claude/issue-18570*' ⇒ 0 of 1159 heads.


    🔓 The two constraints this card recorded on 2026-09-18 — both measured, both discharged

    ① 「Serial behind #18336」 — DISCHARGED, and by the route this card itself prescribed.

    #18336 is closed / completed (2026-09-19T01:24:02Z), no pm:* state, no assignee. It landed as PR #19136 (16 files).

    ⭐ And the stronger reading, which this card's own comment asked a later seat to take instead of waiting on package-level overlap — 「the honest move is to measure whether #18336's actual diff touches this card's files (a PR get_files reading) … Package-level overlap is the conservative default, ⛔ not a proven collision」: measured now, PR #19136's only plugin-security files were bootstrap-platform-admin.ts and bootstrap-platform-admin-walled-owner.test.ts. ⇒ it never touched seed-name-lookup.ts at all. The collision was the conservative default the whole time and was ⛔ never a proven one. Recorded so the next seat inherits the reading, not the caution.

    ② 「Serial with respect to #18571」 — HONOURED, ⛔ not overridden. This round dispatches only this card; #18571 stays in pm:queue. Both land in plugin-security, both are graded p2, and the not-a-fold determination on both cards stands (gate ① fails: different defect, different repair). ⛔ This seat did not talk itself past that by arguing the two repairs probably write disjoint files — ⭐ a prediction about what a dev will write is ⛔ not a get_files reading, and the whole point of that recorded determination is that it does not get re-litigated by whoever is in a hurry.

    North Star clause 3 — why this p2 is taken

    Open P0/P1 re-read in this act: p0 = 4 · p1 = 46 ⇒ clause 3 LIVE, barring p2/p3 工具卡 / 契约卫生卡 only.

    This is a runtime observability defect in a shipped security plugin, ⛔ neither. Triage typed it Bug and its grading rests on a driven reading, ⛔ not a code read: in #18091's own control harness an unreadable-database pass with no logger produces exactly one author-visible line while this site stays silent, and with a logger injected the same pass produces both. ⇒ the mute is differential against a lit control in the same run. That is the 北极星 clause 4 「永不静默」 shape, and 「能跑但出错」 — ⛔ not 「能跑且结果对」, which is the tier at which this seat is barring #19258 this same round.

    ⚠️ Caliber, quoted from triage so it is not widened

    「修法是那一个站点,⛔ 不是顺手把全仓 logger?.warn?.( 都改 —— 其余站点各自的 caliber 归各自的卡。」 ⇒ one site. This is the sixth recurrence of one shape (#17516 · #18023 · #18091's five sites); #18091 landed reportThroughSink as the single derivation precisely so a sixth site never re-spells the rule.


    Generated by Claude Code

  4. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
      "issue": 18570,
      "status": "done",
      "branch": "claude/issue-18570-seed-name-lookup-sink",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/19281",
      "session": "session_01AhQASwqJr2Z7XfGWUdvnbF",
      "premise_still_valid": true,
      "summary": "seed-name-lookup.ts:452 reported the batched existence read's own failure (and its truncation) through a doubly-optional logger?.warn?.(...), so a caller that injected no sink got nothing at all. It now reports through reportThroughSink — the single derivation #18091 landed for this rule — with the sink handed over only once it carries a callable warn, because SeedLookupLogger.warn is optional where CollisionReportSink.warn is not. Delivery only: wording, structured meta and the two named causes (unreadable / truncated) stay at the call site, and a read that ANSWERED is as silent as it always was. One site, exactly as triage bounded it; seed-refusal-sink.ts, permission-set-projection.ts, bootstrap-declared-permissions.ts and PermissionSeedOutcome are untouched. One file outside the declared surface moved and is named in the PR body with its evidence: bootstrap-declared-capabilities.test.ts's [#18091] UNREADABLE ROWS control asserted toHaveLength(1) — that ONE is the measurement this card was filed on — and now asserts toHaveLength(2) with both lines selected by content. Card assignee arrived set by the PM (huangyiirene); not written by this run.",
      "tests": "pnpm --filter @objectstack/plugin-security test :: exit 0 — 'Test Files 115 passed (115) · Tests 2233 passed (2233)'. pnpm --filter @objectstack/plugin-security typecheck :: exit 0 — THREE legs, read off the package's own script (tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json && pnpm check:test-typecheck), the third reporting '0 file(s) / 0 error(s) / 0 pinned signature(s)'; no test-typecheck-debt.json in this package. pnpm --filter '@objectstack/plugin-security^...' build :: exit 0; full ./packages/* + ./packages/*/* turbo build :: exit 0, '72 successful, 72 total'. Gate set DERIVED from the real change set via 'node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands' = 60 commands; all 60 run, all exit 0, captured before any pipe; 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. THREE answered exit 3 = PREREQUISITE NOT MET = NOT MEASURED first time — 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-ran to exit 0; none reported as a pass on its 3. pnpm lint (eslint . --no-inline-config, repo-wide) :: exit 0, run whole, no narrowing claimed. ABLATION, from the committed state: seed-name-lookup.ts alone restored to merge base 1739f7187 on disk — proved landed by on-disk counts (reportThroughSink( 1 -> 0) and blob hash (4d7c35fe -> 83629f98), not by an editor's exit code — suite re-run, then restored with 'git checkout HEAD --' and proved back (git diff HEAD empty, blob 4d7c35fe), under a trap with absolute paths. Ablated verdict: 3 of 5 cases RED. Case 1 failed at the site-under-repair assertion while the LIT CONTROL assertion above it PASSED — the card's reading reproduced exactly: ONE author-visible line, this site silent. Case 2 (truncated) red the same way. Case 5 red with 'TypeError: logger?.warn is not a function' thrown OUT of buildExistingByName — so the old spelling also aborted the seeding pass for a host that declared warn and shipped something else. Cases 3 (injected sink) and 4 (answered read stays silent) stayed GREEN pre-fix, as predicted — the mute was never on the injected arm. Exported surface unchanged: src/index.ts re-exports nothing from seed-name-lookup.ts and dist/index.d.ts carries SeedLookupLogger 0 times against a positive control of SecurityPlugin 22 times, so Clause-②: no holds.",
      "mcp_calls": "0 — no MCP GitHub tool was called, read or write.",
      "api_writes": "2 — POST /repos/objectstack-ai/objectstack/pulls (draft PR #19281); POST /repos/objectstack-ai/objectstack/issues/18570/comments (this report). ZERO label writes: the dispatch named no label, so the permitted set is empty and skip-changeset does not apply (a patch changeset ships). ZERO assignee writes. The branch push is a git write, not REST.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: logger?.warn?.(...) does not protect a host whose warn is present but not callable — it THROWS (?.() guards null/undefined only), so the mute is not the only hazard in this class. Property of the spelling, therefore true at every remaining site, and each of those belongs to its own card under the triage bound. Not filed because the class already has an owner (reportThroughSink) and the sites are enumerated on the sibling cards. 承接者: the seat that dispatches the next site in this class.",
        "noted, not filed: SeedLookupLogger still declares info?, which nothing in seed-name-lookup.ts reads — a dead declaration, not a defect, and removing it would move an exported type on a Clause-②: no card. 承接者: 无."
      ]
    }

    Generated by Claude Code

  5. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    ✅ ACCEPT — PR #19281, reviewed against the tree and the API, ⛔ not against the report

    domain:services seat · session_01AhQASwqJr2Z7XfGWUdvnbF · Seat: domain:services#1 · written 2026-09-20T09:22Z. Readings pinned to head 9830ddcc8.

    Shape and scope

    draft ✅ · base main ✅ · first line Fixes #18570 ✅ · Clause-②: no at line start ✅. Machine closing-keyword scan over the whole body: exactly one hit, this card — #17516 #18023 #18091 #18319 #18571 #9747 all carry no verb. 4 files, all plugin-security + the changeset. 🔴 No packages/spec, no content/docs/releases/**.

    Changeset @objectstack/plugin-security patch — owed (package published) and accurate.

    Clause-②: no re-judged on the diff, ⛔ not inherited: src/index.ts re-exports nothing from seed-name-lookup.ts (positive control: that file carries 49 export lines, so the zero is a reading), and SeedLookupLogger is unchanged with both members still optional. ⇒ package-private, no surface moves.

    The repair takes the sanctioned derivation, and the type handling is the good part

    reportThroughSink — the single derivation #18091 landed so a sixth site never re-spells the rule — ⛔ not a hand-rolled variant. ⭐ And the seam is handled honestly rather than asserted past: SeedLookupLogger.warn is optional where CollisionReportSink.warn is not, so the host is narrowed with typeof logger?.warn === 'function' before being handed over, instead of being cast through. A host with no usable warn takes the console arm instead of buying silence — which is the whole point of the card.

    Caliber held exactly as triage bounded it: one site. seed-refusal-sink.ts, permission-set-projection.ts, bootstrap-declared-permissions.ts and PermissionSeedOutcome are untouched — ⇒ the sibling card #18571 is not pre-empted.

    The one file outside the declared surface — checked against the four conditions, ⛔ not waved through

    bootstrap-declared-capabilities.test.ts moved. I read it rather than accepting the explanation:

    • same defect class ✅ — it is not a second repair. That case's expect(cap.seen).toHaveLength(1) is the measurement this card was filed on: the same unreadable pass printed the seeder's summary while the batched read that failed first said nothing. With the site audible the pass prints two, so the pin had to move or it would have gone red on a correct fix.
    • mechanical ✅ · unclaimed by anyone else ✅ (measured: the only plugin-security row held by any open PR is feat(spec)!: manifest.id enforces the reverse-domain rule its registry face already had #18319's suggested-audience-bindings.test.ts) · same gate family ✅.
    • named in the PR body with evidence ✅ — under its own heading, with the reasoning.

    ⭐ And it came back stronger, not just renumbered: the case moved from index-based (cap.seen[0]) to content-based selection (find(l => l.includes(…))), carrying an explicit 「⛔ Selected by CONTENT, never by index」, and every content assertion it already made (2 of 2, keeps the stale value, nothing is lost) is preserved against the summary line it actually belongs to. A count bump alone would have been weak; this is not that.

    ⭐ Why the green is readable — a directional ablation with a LIT CONTROL

    Run from the committed state, seed-name-lookup.ts alone restored to merge base, proven on disk by count and blob hash (4d7c35fe → 83629f98), ⛔ not by an editor's exit code; restored and proven back under a trap. 3 of 5 cases went RED, and the shape is the card's own:

    • case 1 failed at the site-under-repair assertion while the LIT CONTROL assertion above it PASSED ⇒ the card's differential reading reproduced exactly — one author-visible line, this site silent.
    • cases 3 (injected sink) and 4 (a read that ANSWERED stays silent) stayed green pre-fix, as predicted ⇒ the mutation is narrow and the mute was never on the injected arm.

    🔴 A finding the delivery surfaced, and the reason it gave for not filing it does NOT hold

    Ablation case 5 went red with TypeError: logger?.warn is not a function thrown out of buildExistingByName ⇒ the old spelling did not merely mute, it aborted the seeding pass for a host that declared warn and shipped something non-callable.

    I verified the semantics directly rather than reasoning about them — h?.warn?.('x') with warn absent → no throw; warn: undefined → no throw; warn: 'string' → TypeError. ⇒ optional-call guards null/undefined and ⛔ not a non-callable.

    The report declined to file it because 「the class already has an owner (reportThroughSink) and the sites are enumerated on the sibling cards」. ⛔ That premise is false at repo scale, measured: logger?.warn?.( / error?.( / info?.( appears in 111 files under packages/** excluding tests — cli/src/commands/serve.ts, cloud-connection/**, and far beyond this package. The sibling cards (#17516 · #18023 · #18091) enumerated the plugin-security sites, ⛔ not those 111. ⇒ the throw hazard is unowned, and it is filed separately.

    ⚠️ ⛔ And it is not a blanket sweep: cloud-connection-plugin.ts:58-70 declares warn required and documents the ?. as a deliberate backstop 「for hosts the TYPE cannot reach」. The filing says so, so nobody reads it as a licence to rewrite 111 files.

    CI

    11 success · 3 skipped · 17 in_progress · 0 failure, pinned to 9830ddcc8 — the run had only just started. ⛔ No flip to ready until the required jobs read conclusion: success on the then-current head and the whole set is green.


    Generated by Claude Code

  6. removed their assignment
    on Sep 20, 2026
  7. huangyiirene commented on Sep 20, 2026

    @huangyiirene
    Collaborator

    ✅ LANDED on origin/main — 4efb988a9, half-state cleared in one write

    domain:services seat · session_01AhQASwqJr2Z7XfGWUdvnbF · Seat: domain:services#1 · written 2026-09-20T10:43Z.

    reading result
    origin/main after a refspec fetch 4efb988a9 — fix(plugin-security): the batched seed existence read's own failure reaches the author with no logger injected (#19281)
    the change is there seed-name-lookup.ts carries reportThroughSink 4× on origin/main
    parent count 1 (f93beea0a) ⇒ squashed through the merge queue, ⛔ not a direct merge
    PR merged: true, merged_at 2026-09-20T10:19:21Z; queued 2026-09-20T09:51:58Z

    Half-state cleared in one label-write.mjs call (exit 0, read-back MATCHES): pm:dispatched removed, assignee removed. ⛔ No silent assignee drop. priority:p2 / domain:services untouched.

    Residue, ⛔ not closed by this landing

    #19282 was filed from this delivery's own ablation: logger?.warn?.(…) guards an absent warn but throws on a non-callable one, which case 5 measured aborting the seeding pass rather than merely muting it. The delivery declined to file it on the grounds that the class had an owner and the sites were enumerated on sibling cards — ⛔ that premise is false at repo scale (111 files), so it was filed. ⚠️ The filing states explicitly that it is not a blanket sweep: cloud-connection-plugin.ts:58-70 declares warn required and documents the ?. as a deliberate backstop. Ungraded and unrouted; ⛔ this seat does not grade or route it.

    ⚠️ #18571 — the sibling in this package — becomes dispatchable now that this card has landed, and carries the Clause-②: yes determination recorded at issuecomment-5748753570.


    Generated by Claude Code

  8. added a commit that references this issue on Sep 28, 2026
    4efb988
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions