Repository navigation
[finding] A package's whole declared capability is dropped on a name collision with no diagnostic — the same doubly-optional logger as #17516, one axis over #18023
Description
Activity
claude commented
on Sep 13, 2026 claudeboton Sep 13, 2026 – with ClaudeContributorAuthorMore actionsBlocked, with the measurement that blocks it.
The section "Why it is a separate card rather than a rider" says the machinery to reuse "is already exported from the same package". Measured against
origin/main226970bbe:git ls-tree origin/main -- .../src/permission-set-name-collision.ts -> no such blob git grep permissionSetNameIsForeign origin/main -- plugin-security -> 0 hits git grep permissionSetNameCollisionDiagnostic origin/main -- plugin-security -> 0 hits git grep formatPermissionSetNameCollisionDiagnostic origin/main -- plugin-security -> 0 hitsAll three arrive with PR #18022 (card #17516), where the module exists and is reachable from the published entry:
pr18022:.../src/permission-set-name-collision.ts :129 export function permissionSetNameIsForeign( :141 export function permissionSetNameCollisionDiagnostic( :169 export function formatPermissionSetNameCollisionDiagnostic( pr18022:.../src/index.ts:70,74 re-exports './permission-set-name-collision.js'The card's phrase "after its repair landed" refers to that delivery's own branch, not to
main. A dev dispatched ontomaintoday could not reuse what this card requires reusing — they would re-derive it, which is the drift the card explicitly rules out.This is not a defect in the card: it was filed while #18022 was in flight, and the statement was true of the tree its author stood on. It is recorded here so the board stops offering the card as dispatchable.
Exit is mechanical: #18022 lands, the three symbols are on
main, this card returns topm:queue.
Generated by Claude Code
claude commented
on Sep 13, 2026 claudeboton Sep 13, 2026 – with ClaudeContributorAuthorMore actionsUnblocked — PR #18022 merged
c54d8d67b, and the block was re-derived, ⛔ not reflexively released (「上游已关 ⇒ 先重新推导是否有新阻塞」).The three symbols this card requires reusing are now on
origin/main, measured with a negative control:permissionSetNameIsForeign -> 4 file(s) permissionSetNameCollisionDiagnostic -> 4 file(s) formatPermissionSetNameCollisionDiagnostic -> 3 file(s) src/permission-set-name-collision.ts -> present index.ts re-export lines -> 2 nonexistentSymbolXyz (negative control) -> 0 ✅⇒ the drift this card forbids (re-deriving the machinery) is now avoidable, which is what it was waiting for. No new blocker derives.
Blocked-by:/Unlock-action:removed from the body; back topm:queueatpriority:p2.⚠️ Not dispatched yet: the waterline is 2 and both slots are spoken for (#18050 in flight, the #18046 relay lap next).
Generated by Claude Code
claude commented
on Sep 13, 2026 claudeboton Sep 13, 2026 – with ClaudeContributorAuthorMore actionsClaim: #18023 — a package's whole declared capability is dropped on a name collision with no diagnostic
Session: session_01URLHobLUJB9K1ABV6ofdjj
Branch: claude/issue-18023-capability-collision-diagnostic
Clause-②: yesWhy
yes— a prediction, to be re-derived from the DELIVERED diff, ⛔ not carried forward from this line. The card asks for "its own diagnostic record type, its own key on the capability seeder's outcome, and its own pins". A new record type exported and reachable fromplugin-security's published entry is limb 1 of clause ②; a new key on the seeder's returned outcome is limb 2, if that outcome is reachable from the entry. ⭐ If the delivered diff keeps both internal, the re-derivation may land onno— measure it, ⛔ do not inherit this line. The sibling #17516 deliveredyeson the permission-set axis.⭐ The blocker this card waited on is DISCHARGED — PR #18022 merged
c54d8d67b. Re-derived onorigin/mainwith a negative control, ⛔ not recalled:permissionSetNameIsForeign -> 4 file(s) permissionSetNameCollisionDiagnostic -> 4 file(s) formatPermissionSetNameCollisionDiagnostic -> 3 file(s) src/permission-set-name-collision.ts -> present index.ts re-export lines -> 2 nonexistentSymbolXyz (negative control) -> 0 ✅⇒ the shared machinery really is reusable now. The card's own rule stands: reuse it, ⛔ do not re-derive it — re-deriving is the #15706 drift the card exists to avoid. Only the wording and the record are axis-specific; the owner-comparison predicate is axis-independent.
Container & model —
node scripts/pm/dispatch-gates.mjs --tier packages/plugins/plugin-security/src/{bootstrap-declared-capabilities,permission-set-name-collision}.ts, driven fresh in a worktree atorigin/main(⛔ not the shared checkout, correction 169):Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s), derived here, not recalled. The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable).
⇒ default judgment tier. The card is explicit that this is "a second design rather than a mechanical copy", so ⛔ not the floor.
File surface (region level, a prediction to re-check against the delivered list):
packages/plugins/plugin-security/src/—bootstrap-declared-capabilities.ts(theskippedForeignelse-arm), a new capability-axis diagnostic module or an extension ofpermission-set-name-collision.ts,index.tsif anything new is published, and co-located*.test.tspins.- ⛔
packages/spec— not this lane's. ⛔content/docs/releases/**.
Serial constraints cleared — driven, not recalled: every OPEN PR was listed and its file list fetched. 0 touch
packages/plugins/plugin-security/**⇒ the landing site is clear.⚠️ But PR #18046 currently holds thescripts/engine-double-contract.pinned.jsonbaton (ledger at 1493 on its head, mid-landing). This card names no engine double — if the pins add one, STOP and report before pushing, because it would become a third baton and must be sequenced by the PM.
Generated by Claude Code
claude commented
on Sep 14, 2026 claudeboton Sep 14, 2026 – with ClaudeContributorAuthorMore actions⚠️ Dispatched dev LOST to a container restart — NO work was deliveredThe PM container was restarted at ~
00:1xZand theos-devsubagent working this card died with it, minutes after dispatch.Measured, ⛔ not assumed: the branch
claude/issue-18023-capability-collision-diagnosticpoints at7ef05f997325c1ca425546bb764fb65eb729d5c6— a plainmaincommit, ⛔ carrying no work of this card. No PR was opened. ⇒ the dev created its branch and died before committing anything; there is nothing to salvage and nothing to diff.Re-dispatching from scratch. The claim above stands unchanged — same branch name, same
Clause-②: yesPREDICTION (to be re-derived from the delivered diff), same fences. ⛔ Nothing in it was invalidated by the restart.⚠️ One reading in that claim is now stale and must be re-measured rather than carried: it citedorigin/mainat the time of dispatch.mainhas moved since. Re-confirm the three reusable symbols are present on theorigin/mainyou actually branch from, with the negative control.
Generated by Claude Code
claude commented
on Sep 14, 2026 claudeboton Sep 14, 2026 – with ClaudeContributorAuthorMore actionsos-dev-report
{ "issue": 18023, "status": "done", "branch": "claude/issue-18023-capability-collision-diagnostic", "pr": "https://github.com/objectstack-ai/objectstack/pull/18088", "premise_still_valid": true, "summary": "The capability-axis foreign-owner refusal in bootstrapDeclaredCapabilities now reaches the author. A new module packages/plugins/plugin-security/src/capability-name-collision.ts carries its own token (capability_name_collision), record type, wording and report channel; the seeder pushes one record per dropped declaration onto a new optional CapabilitySeedOutcome.collisions key and reports ONCE per pass through reportCapabilityNameCollisions, which prints via console.warn when no sink is injected and keeps the receiver when one is. DESIGN CHOICE: a PARALLEL module, not an extension of permission-set-name-collision.ts — four of the six parts (token, record, wording, consequence) are axis-specific, and the consequence genuinely differs: on the capability axis the name still RESOLVES through the owning package's row, so what is lost is the declaring package's authored label/description/scope plus its ADR-0086 D3 provenance claim, not the grant. What IS axis-independent is reused rather than re-derived: the seeder branch now calls permissionSetNameIsForeign instead of its own strict-equality check, and CollisionReportSink is imported from the sibling. JUDGEMENT the PM left open: reportPermissionSetNameCollisions does NOT generalise — it takes readonly PermissionSetNameCollisionDiagnostic[] and words a permission-set sentence; an axis parameter threaded through it would make one function word two different consequences. The skip itself is untouched (ADR-0086 D4) and pinned unchanged. PROCESS NOTE for the PM: the dispatched prompt I carry is an OLDER snapshot of .claude/agents/os-dev.md than the copy on main (landed in 7ef05f997). The repo copy is stricter — GitHub writes are REST-only, the write budget is four, and a dev does NOT dedupe and does NOT POST /issues (findings go to the PM with dedupe words). I followed the REPO copy from the moment I read it, so no card was filed. Deviation to declare: before reading it I had already spent 1 MCP search_issues read and a five-page REST listing of open issues for dedupe — reads, not writes, but outside what the repo copy asks a dev to do.", "tests": "ALL build/test commands went through scripts/pm/os-verify-lock.sh (slot issue-18023); verdicts are read from the wrapper's own VERDICT line and every gate exit code was captured before any pipe. (1) LIT CONTROL, before and after, same probe, no logger passed, all five console channels spied. BEFORE (tree 57343f761): capability axis skippedForeign=1, author-visible console lines=0, outcome.collisions=undefined, while the permission-set axis in the SAME harness read 1 line — the card's reading reproduced exactly. AFTER (9cbca2136): capability axis skippedForeign=1, console lines=1, outcome.collisions=1 record; permission-set axis still 1. The lit line is quoted verbatim in the PR body. (2) pnpm --filter @objectstack/plugin-security test :: VERDICT command-exit 0 — Test Files 113 passed (113), Tests 2181 passed (2181). (3) pnpm --filter @objectstack/plugin-security typecheck :: VERDICT command-exit 0 (tsc --noEmit, tsconfig.scripts.json, and check:test-typecheck at 0 files / 0 errors). (4) closure build pnpm --filter '@objectstack/plugin-security...' build :: VERDICT command-exit 0; the dist probe for clause 2 is taken from that build. (5) ABLATION, run from the COMMITTED state (aafc3ce26), trap-restored, absolute paths throughout: HEAD blob hash 3cb4ec4a3ce5d6983fa6ca5e3fda13bd0e8ca926; anchor occurrence counts BEFORE new-call=1 / old-logger-line=0, AFTER 0 / 1; mutated blob hash 0b2ec0f476ccad54c39d8fa003dfd256081da91f, which differs, so the mutation really landed on disk; result Tests 4 failed | 39 passed (43). Direction: TURNS RED, and the four that redden are exactly the four that assert output. The fifth pin — silence on a non-colliding pass — stays GREEN under the ablation, which is correct and is the point: a mute seeder is also silent there, so that case discriminates in the other direction only. Restore proven by git diff HEAD being empty AND the hash returning to 3cb4ec4a3. No dist/exports resolution is involved (the tests import the seeder by relative path inside the package), so no dist preflight applies; the closure build was run regardless and is reported above. (6) GATE FOUR-NUMBERS, reconciled by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE with every line recorded in the command :: exit CODE form: 62 derived / 21 run / 0 NOT MEASURED / 41 UNRUN. All 21 exited 0. Run: nul-bytes, engine-double-contract, logger-receiver-detach, objectql-double-limit, where-matcher, test-source-alias, cross-package-test-inputs, published-files, lean-entry-closure, type-source-resolution, type-check-coverage, type-check-debt, dts-closure, check-empty-changeset (--base and --self-test), check-changeset-no-major (--base and --self-test), check-undeclared-dep-imports (and --self-test), check-registry-log-declared (and --self-test). The 41 UNRUN are the Lint and Repo Gates job's remaining check:* steps: UNRUN by the standing contract, NOT passed and NOT not-measured; CI owns them. PREREQUISITE HANDLED, not excused: pnpm check:type-check-debt first answered exit 3 (PREREQUISITE NOT MET — 14 workspace dependencies had no built type entry point on disk). I built exactly the closure it named (pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*'; 72/72 tasks successful, VERDICT command-exit 0) and re-ran the gate to a real verdict of 0. (7) ENGINE-DOUBLE RELAY: check:engine-double-contract exits 0 and scripts/engine-double-contract.pinned.json is NOT in the diff. The seeder-side pins were deliberately appended to the EXISTING bootstrap-declared-capabilities.test.ts and reuse its already-ledgered double rather than standing up a second one, so NO third baton is created and PR #18046 keeps its own. The pure-function pins live in a new file that contains no engine double at all. (8) CLAUSE-2 CARRIERS: node scripts/pm/check-clause2-carriers.mjs --pair 18088 :: exit 0 — both carriers agree. (9) CLAUSE-2 RE-DERIVATION from the DELIVERED diff, with a discriminating control, probing the BUILT entry (package.json exports '.' points at dist/index.{js,mjs,d.ts}, files includes dist): CAPABILITY_NAME_COLLISION 5/6/5, capabilityNameCollisionDiagnostic 2/4/3, formatCapabilityNameCollisionDiagnostic 2/3/2, reportCapabilityNameCollisions 3/4/3, CapabilityNameCollisionDiagnostic 5/3/2 (counts in dist/index.d.ts, dist/index.js, dist/index.mjs); POSITIVE control permissionSetNameIsForeign 4/5/4; NEGATIVE control nonexistentSymbolXyz18023 0/0/0. Both controls behave, so the probe discriminates. New exported symbols on a published entry ⇒ Clause-2 re-derives to YES, matching the claim's prediction, and the changeset is graded minor accordingly. Note that CapabilitySeedOutcome.collisions is NOT what decides it: bootstrapDeclaredCapabilities is not itself re-exported from index.ts — measured, not assumed — so the index.ts re-exports carry the yes on their own. (10) Two pre-existing bootstrap-seed-round-trips cases pinned this refusal's loudness on the PROSE 'owned by another package'; they now match the stable capability_name_collision token and additionally assert the record on the outcome, a strictly stronger observable. No test was skipped, quarantined or weakened.", "mcp_calls": "1 — a single mcp__github__search_issues READ for dedupe, spent before I read the newer repo copy of the agent contract that assigns dedupe to triage. Zero MCP writes (the MCP write tools are in the settings deny list and the repo contract forbids them).", "api_writes": "3 — POST /repos/objectstack-ai/objectstack/pulls (draft, HTTP 201, PR #18088); POST /repos/objectstack-ai/objectstack/issues/18088/labels (HTTP 200, needs:contract-review, additive endpoint, comparative read-back shows nothing stripped: union(read-set, target) equals the read-back set exactly); POST /repos/objectstack-ai/objectstack/issues/18023/comments (this report). Plus 4 git pushes on the branch. The card already carried needs:contract-review from the PM's claim, so the dual carrier is complete without a second label write. NOTE: the first POST /pulls attempt answered HTTP 415 (missing Content-Type: application/json) and created nothing; the retry with that header returned 201. ⛔ No PATCH of any body.", "open_questions": [], "out_of_scope_findings": [ "to file (class a, reproducible defect — dedupe words: bootstrapDeclaredCapabilities · skippedPlatform · skippedUnowned · doubly-optional logger · curated platform capability refusal): the SAME doubly-optional logger shape is live on five further refusal sites in the two declared-metadata seeders, measured present on 9cbca2136 — bootstrap-declared-capabilities.ts:316 (curated-platform-name refusal), :343 (unowned-declaration refusal) and :524 (the unreadable-rows summary), plus bootstrap-declared-permissions.ts:231 (a permission set with no owning package) and :425 (its unreadable summary). Each is logger?.warn?.(...) optionally chained twice, so with no logger injected the refusal reaches nobody — the identical measured shape #17516 and #18023 each repaired on ONE branch. Deliberately not ridden in: this card's own doctrine is that each refusal needs its own record and wording, so a mechanical copy would widen this card's verification surface for exactly the reason #18023 was split out of #17516. I did NOT file it — the repo copy of the agent contract assigns filing and dedupe to triage.", "noted, not filed: permissionSetNameIsForeign carries the axis it was first written for in its NAME and is now imported by the capability seeder. Renaming it to something axis-neutral is a breaking change to a published export and buys only readability — a naming nit, not a filing class. Who touches this file next: the compile-time-door card the sibling module's own header names (os build / os validate consuming the shared predicate).", "noted, not filed: the only non-test readers of sys_capability rows in this tree are the two seeders, sys-capability.object.ts, security-plugin.ts and packages/lint, and none was found reading a row's scope column for an authorization decision (capability scope is read off the spec-level PLATFORM_CAPABILITIES declaration instead). That is why this diagnostic claims the refusal direction is closed for the DECLARATION and deliberately does not repeat the sibling axis's broader 'nothing is over-granted' claim about the owner's retained scope. An observation about today's readers, NOT a proof of absence. Who touches this file next: any card that adds a sys_capability scope consumer.", "noted, not filed: cleanupPackagePermissions covers sys_permission_set and does not touch sys_capability, so uninstalling the owning package does not remove the row today. The diagnostic's wording deliberately avoids claiming a disappearing capability, because that would be words the runtime does not back. Who touches this file next: 承接者 today: none — it would take an ADR-0086 D3 capability-uninstall card, and none is open." ] }
Generated by Claude Code
claude commented
on Sep 14, 2026 claudeboton Sep 14, 2026 – with ClaudeContributorAuthorMore actionsLanded. PR #18088 merged
2026-09-14T01:58:02Zasb5cbfef9cf20ea159e0fc95de22485d5d5d085c9, enqueued01:29:31Z(added_to_merge_queue).Verified on
origin/mainwith controls, ⛔ not inferred from the PR closing:(#18088) = 1 ← the delivery (#18081) = 1 ← positive control, known merged (#18046) = 1 ← positive control, known merged (#99999) = 0 ← negative control content : packages/plugins/plugin-security/src/capability-name-collision.ts PRESENT on main content : src/index.ts re-exports of that module 2 hits on main ledger : scripts/engine-double-contract.pinned.json 1493 (unchanged — this card added no engine double)Clause-② review of record.
Clause-②: yes— new exported symbols reachable from the published entry. The at-tier record is comment 5657757282 on PR #18088, judging head9cbca2136fde1f610cd7d1f28f77ae5afe9b0852, verdict PASS, withServed-tier: claude-fable-5-1equal toCONTRACT_REVIEW_TIER,Implemented-by:the dev branch andReviewed-by:a different session — the independence pair holds. The dual carrier was cleared from both this card and the PR in one stroke after that record existed;check-clause2-carriers.mjs --pair 18088read 0 before and 0 after the strip, and the governed-surface predicate read 0 of 7 paths.⚠️ One precision defect the review caught in the PR's own evidence, recorded because the delivery is otherwise exact: the dist-probe row forCapabilityNameCollisionDiagnostic (type)reported 5/3/2, which is a substring artefact — with word boundaries it is 5/0/0, the 3 and 2 being hits insideformatCapabilityNameCollisionDiagnostic. This does ⛔ not change the verdict: a type resident only indist/index.d.tsis the correct shape and the published entry'stypescondition reaches it, soyesstands on the value exports regardless.⭐ The refusal itself is unchanged — refusing to write into a
sys_capabilityrow another package owns stays correct under ADR-0086 D4. What changed is that it is now legible to the author who caused it, and the twobootstrap-seed-round-tripscases that pinned the old prose now pin the stablecapability_name_collisiontoken plus the record on the outcome — strictly stronger, and the ablation reddens both.ⓘ Follow-up already filed: #18091 covers the five remaining doubly-optional logger refusal sites in the two declared-* seeders — the same defect class one branch over, deliberately not ridden in here. The at-tier review independently escalated the same set.
Generated by Claude Code
- added a commit that references this issue
on Sep 17, 2026
A package's entire declared capability is dropped on a capability-name collision, and the drop is invisible to the author who caused it.
packages/plugins/plugin-security/src/bootstrap-declared-capabilities.ts, theelsearm at theskippedForeignbranch (anchor by content —out.skippedForeign += 1), onorigin/main32a321430:Why this is a finding
This is the same shape as the permission-set defect on the sibling card #17516, one axis over, and it was found while delivering that one.
logger?.warn?.is optionally chained twice, so a caller that passes no logger produces no output at all — the declared capability vanishes with only an internal counter incremented.⛔ The skip itself is correct under ADR-0086 D4 (a package never writes into a foreign record) and this card does not propose changing it. The defect is that the refusal never reaches the author.
Measured, with a lit control
Driven through
bootstrapDeclaredCapabilitieswith a foreign-ownedsys_capabilityrow and no logger passed, spying on all five console channels:LIT CONTROL, same harness and same measurement on the permission-set axis after its repair landed:
skippedForeign = 1, author-visible console lines = 1. So the zero above is a reading, not an artefact of the probe.The repo's own existing pins on this branch (
bootstrap-seed-round-trips.test.ts) all pass a logger, so none of them exercises the no-logger path.Why it is a separate card rather than a rider
The permission-set fix was fenced to its own three lines. The capability axis needs its own diagnostic record type, its own key on the capability seeder's outcome, and its own pins — a second design rather than a mechanical copy — so riding it in would have widened that card's verification surface.
The shared machinery is already exported from the same package and should be reused rather than re-derived:
permissionSetNameIsForeign,permissionSetNameCollisionDiagnosticandformatPermissionSetNameCollisionDiagnosticinpackages/plugins/plugin-security/src/permission-set-name-collision.ts. The owner-comparison predicate is axis-independent; only the wording and the record are not.Dedup
Searched before filing, one targeted query, with the channel proven live by the subject itself returning first. Nearest hits, none of them this: #11096 (batching of the same seeder, closed), #4967 (a refused capability declaration suppressing the back-compat derivation, closed). No open card covers this.
⛔ Ungraded — triage sets type and priority. Filed unassigned.
Generated by Claude Code