Maintainer-action: rule how a domain:services card obtains the CONTRACT_REVIEW_TIER clause-② verdict its carrier needs — route it to the spec seat (naming how), or rule that this seat's default-judgment-tier review plus the gates suffices — done when node scripts/pm/check-clause2-carriers.mjs --pair 18022 exits 0 with the carrier cleared on both carriers
A package's entire declared permission set is dropped on a set-name collision, and the drop is invisible to the author who caused it.
packages/plugins/plugin-security/src/bootstrap-declared-permissions.ts, the else arm at the skippedForeign branch (anchor by content — out.skippedForeign += 1), on origin/main d9e15877aa read 2026-09-10T18:25Z:
} else {
// Package-namespaced object api names make set-name collisions a
// packaging bug, not a merge case — refuse loudly (ADR-0086 D4:
// a package never writes into a foreign record).
out.skippedForeign += 1;
logger?.warn?.('[security] permission set name owned by another package — skipped', {
name: ps.name, declaredBy: packageId, ownedBy: existing.package_id,
});
}
Why this is a finding and not a design choice
The comment says "refuse loudly". The code does not refuse loudly. ⚠️ Sharper than it looks: logger?.warn?. is optionally-chained twice, so a caller that passes no logger produces no output at all — the set vanishes with only an internal counter incremented. Nothing is written back where an author can read it: no diagnostic record, no os build check, no os validate check.
Measured absence, with a lit control so the zero means something:
skippedForeign referenced in packages/cli/src or packages/spec/src : 0 files
LIT CONTROL 'permission' in packages/cli/src : 27 files
Skipping the set is arguably the correct runtime behaviour (ADR-0086 D4: a package never writes into a foreign record). ⛔ This card does not propose changing that. The defect is that the refusal never reaches the author.
This is a class the repo has already ruled on — one layer over
This is the same failure shape #14553 just inverted for navigationContributions: present, plausible-looking, and silently different from what the author wrote. That fix gave nav a nav_contribution_group_missing diagnostic raised at BOTH doors — runtime (recordNavGroupMiss) and compile (os build / os validate via checkNavContributionGroups) — behind one shared predicate so the two cannot drift. The authorization surface never got the same treatment.
An AI author colliding on a set name is exactly how this fires, which puts it in class (c): metadata that will be stored and re-authored by someone other than its writer, silently dropped at the door.
Second, smaller defect in the same three lines
The comment's stated premise — "Package-namespaced object api names make set-name collisions a packaging bug, not a merge case" — is falsified by ADR-0130 D1, which lets N packages co-own one namespace. ADR-0130 already records this under its "What was NOT decided" section, so ⛔ no separate card is warranted; it belongs in the same change as the fix above.
Dedup
domain:spec open + closed, newest 100 each (⚠️ both at the API page cap, so bounded, not exhaustive). One near miss, not this: #16870 (readScope beside viewAllRecords). The round that surfaced this also searched via MCP and found only closed #9952 and #15298, neither of them this, with a live control hit.
Provenance
Surfaced by the #14488 proposal round, which was dispatched write-restricted (⛔ no card filing) and correctly handed it to the seat rather than filing it or dropping it. Re-verified independently by the seat before filing — including the double optional-chaining, which the round did not report.
⛔ Ungraded — triage sets type and priority. Successor: whoever builds #14488 edits this exact branch.
Generated by Claude Code
Maintainer-action: rule how a
domain:servicescard obtains theCONTRACT_REVIEW_TIERclause-② verdict its carrier needs — route it to the spec seat (naming how), or rule that this seat's default-judgment-tier review plus the gates suffices — done whennode scripts/pm/check-clause2-carriers.mjs --pair 18022exits 0 with the carrier cleared on both carriersA package's entire declared permission set is dropped on a set-name collision, and the drop is invisible to the author who caused it.
packages/plugins/plugin-security/src/bootstrap-declared-permissions.ts, theelsearm at theskippedForeignbranch (anchor by content —out.skippedForeign += 1), onorigin/maind9e15877aaread 2026-09-10T18:25Z:Why this is a finding and not a design choice
The comment says "refuse loudly". The code does not refuse loudly.⚠️ Sharper than it looks:
logger?.warn?.is optionally-chained twice, so a caller that passes no logger produces no output at all — the set vanishes with only an internal counter incremented. Nothing is written back where an author can read it: no diagnostic record, noos buildcheck, noos validatecheck.Measured absence, with a lit control so the zero means something:
Skipping the set is arguably the correct runtime behaviour (ADR-0086 D4: a package never writes into a foreign record). ⛔ This card does not propose changing that. The defect is that the refusal never reaches the author.
This is a class the repo has already ruled on — one layer over
This is the same failure shape #14553 just inverted for
navigationContributions: present, plausible-looking, and silently different from what the author wrote. That fix gave nav anav_contribution_group_missingdiagnostic raised at BOTH doors — runtime (recordNavGroupMiss) and compile (os build/os validateviacheckNavContributionGroups) — behind one shared predicate so the two cannot drift. The authorization surface never got the same treatment.An AI author colliding on a set name is exactly how this fires, which puts it in class (c): metadata that will be stored and re-authored by someone other than its writer, silently dropped at the door.
Second, smaller defect in the same three lines
The comment's stated premise — "Package-namespaced object api names make set-name collisions a packaging bug, not a merge case" — is falsified by ADR-0130 D1, which lets N packages co-own one namespace. ADR-0130 already records this under its "What was NOT decided" section, so ⛔ no separate card is warranted; it belongs in the same change as the fix above.
Dedup
domain:specopen + closed, newest 100 each (readScopebesideviewAllRecords). The round that surfaced this also searched via MCP and found only closed #9952 and #15298, neither of them this, with a live control hit.Provenance
Surfaced by the #14488 proposal round, which was dispatched write-restricted (⛔ no card filing) and correctly handed it to the seat rather than filing it or dropping it. Re-verified independently by the seat before filing — including the double optional-chaining, which the round did not report.
⛔ Ungraded — triage sets type and priority. Successor: whoever builds #14488 edits this exact branch.
Generated by Claude Code