Skip to content

cli: os build / os validate report a permission-set name collision — the compile door for #17516 - #18483

Merged
os-support-ai merged 4 commits into
mainfrom
claude/issue-18024-permission-set-collision-compile-door
Sep 16, 2026
Merged

os-support-ai merged 4 commits into
mainfrom
claude/issue-18024-permission-set-collision-compile-door

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes #18024

Clause-②: no — this card adds NO published export and re-stamps no error-code vocabulary. It consumes exports @objectstack/plugin-security already publishes (that is why PR #18022 itself graded yes/minor), and making an existing refusal visible is a tightening or neutral, never a widening of an accept set. The dispatcher-error-vocabulary token stays stamped event, untouched: this change creates no stamp site at all. Both of the card's stated flip conditions were checked against the delivered diff and neither holds.

What this is

PR #18022 (card #17516) gave the permission-set name-collision refusal a runtime door. The compile-time door was missing: os build and os validate walked an artifact in which two packages declare the same permission-set name and said nothing, so the first an author heard of an entire declared set never materializing was a boot warning on a deployed environment.

#14553 set the shape for exactly this on navigationContributions — the same finding raised at both doors, behind one shared predicate so the two cannot drift. Its compile half is packages/cli/src/utils/nav-contribution-groups.ts; this change is the same half for permission sets.

What it does NOT do

⭐ One derivation, consumed — not re-derived

packages/cli/src/utils/permission-set-name-collisions.ts imports the predicate and the wording from @objectstack/plugin-security's package entry, where #17516 published them for this consumer:

  • the owner comparison is permissionSetNameIsForeign — ⛔ never a local comparison, because a nullish owner is FOREIGN by that function's ruling;
  • the sentence is permissionSetNameCollisionDiagnostic + formatPermissionSetNameCollisionDiagnostic — ⛔ never retyped, because two doors phrasing one refusal differently IS the defect.

reportPermissionSetNameCollisions is deliberately NOT consumed, and the bound is stated in the module header: it is a sink-printing pass reporter whose sentence is past-tense about a boot pass and whose second half is a structured meta record the CLI text face has no channel for. The per-finding lines are the shipped formatter's; the count heading is the command's own framing, exactly as the nav half leaves its heading to the commands.

The artifact package-id rule is imported too (artifactPackages), not re-spelled — that module's own header records why a second copy must not happen.

Bounds the check keeps

  • Only the composed case is judged. A name owned by a package installed from some OTHER artifact is invisible without a database, and guessing would report a package's own re-seed as a collision.
  • A package re-declaring its own set name is not a collision — that is an idempotent re-seed at runtime, which is why the check asks the shipped ownership predicate rather than counting duplicate names.
  • A declaration with no resolvable owner is dropped, never reported — the seeder returns at if (!packageId) with a different warning and never reaches the collision branch.
  • The top-level permissions collection is read only when the stack carries no packages[]: permissions composes by concat, so on an artifact the top level is the union of every package's sets with per-package provenance flattened away.

Verification

Build closure first, then every verdict:

what command result
dependency closure pnpm --filter '@objectstack/cli^...' build exit 0
type check pnpm --filter @objectstack/cli typecheck exit 0 (tsc --noEmit + check:test-typecheck)
unit tier pnpm --filter @objectstack/cli exec vitest run --project unit 211 files / 3003 tests, 2974 passed, 29 skipped, 0 failed
new suite vitest run src/utils/permission-set-name-collisions.test.ts 10 passed (10)
derived gate families node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack 63 derived / 63 run / 0 NOT-MEASURED / 0 UNRUN — reconciled with --ran, every line carrying its own exit code so the zero is DERIVED, not claimed
repo-wide lint pnpm lint (eslint . --no-inline-config) exit 0, no output

pnpm lint, the 63-command gate sweep, the type check and the two touched suites (permission-set-name-collisions.test.ts + validate-build-gate-parity.test.ts, 23 passed) were all run on this branch's final commit 20418d251 (git rev-parse --short HEAD), clean worktree. The dependency-closure build, the full unit tier and the ablation were run on 76a1ff09f, whose tree differs from the final one only by the added changeset and the two commits origin/main brought in (.claude/skills/** and scripts/check-bash32-floor.mjs) — no package source among them, so none can move a TypeScript or vitest verdict.

Three of the 63 gate commands did not return 0 on their first run, and each was resolved rather than recorded as a pass:

Two files in the unit tier first reported packages/cli is not built (./dist/index.js is absent) — a PREREQUISITE, not a verdict. pnpm --filter @objectstack/cli build then re-running them: 2 files / 29 tests passed.

Ablation — the pins FAIL on the wrong fix

Both legs mutated on disk, proven landed by git hash-object against the HEAD blob, run, restored, and the restore proven by hash equality plus a clean git diff HEAD. HEAD blob dbf66ed.

leg mutation mutated restored
baseline — — 10 passed (10)
A delete the permissionSetNameIsForeign call — i.e. the wrong fix, a name-duplicate counter 1 failed / 9 passed (the SAME package declaring its own set name twice is NOT a collision) 10 passed (10), hash back to dbf66ed
B stop walking packages[], read the flattened top level instead 5 failed / 5 passed 10 passed (10), hash back to dbf66ed

Acceptance notes — out of scope, not filed here

  1. packages/cli/src/utils/nav-contribution-groups.ts exports artifactPackagesOf, a SECOND implementation of the artifact package-id rule that packages/cli/src/utils/artifact-packages.ts owns, whose header says: "⛔ A second copy is the one that must not happen… Two readers computing 'which package is this' slightly differently is how one entry comes to judge a different set of packages than the other while both look right." The two already differ (the nav copy requires a non-empty name before falling back to it; artifactPackages does not). This change imports the owner rather than adding a third copy.
  2. packages/cli/test/validate-build-gate-parity.test.ts says of its roster "There is no third option — that is the whole point of the file", but its drift detector extracts only lintFoo( / validateFoo( call sites, so a find*-named gate is invisible to it: findNavGroupDiagnostics has been wired into both commands since decision(objectql): a navigationContributions[].group that names no group in the target app is silently RELOCATED to the top level — refuse, warn, or leave to the consumer? #14553 and appears in no roster. This PR lists its own gate by name and records the blind spot in the roster's header; nav's row is left to whoever owns that card.

Generated by Claude Code

The compile-time half of #17516's runtime refusal, behind the SAME published
predicate and the SAME sentence so the two doors cannot drift — the shape
#14553 established for navigationContributions.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
… producer

The intra-package leg is the assertion a name-duplicate counter cannot pass:
the same package re-declaring its own set name is an idempotent re-seed at
runtime, and permissionSetNameIsForeign is what draws that line.

Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 16, 2026
@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 11 documentable anchor(s).

17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 85c6d76ec42a879f219436eaa58bda06c68298b2.

⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: os validate (command, 49 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 85c6d76ec42a879f219436eaa58bda06c68298b2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 64e07f585bb0f33531c53a871706a63896d8ec1c — the merge of head 20418d251bd5db9c8a858d3944a553e9b7f97129 into base 85c6d76ec42a879f219436eaa58bda06c68298b2, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 64e07f585bb0f33531c53a871706a63896d8ec1c && git checkout 64e07f585bb0f33531c53a871706a63896d8ec1c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 85c6d76ec42a879f219436eaa58bda06c68298b2 20418d251bd5db9c8a858d3944a553e9b7f97129 && git checkout -B drift-repro 85c6d76ec42a879f219436eaa58bda06c68298b2 && git merge --no-ff 20418d251bd5db9c8a858d3944a553e9b7f97129

node scripts/docs-audit/affected-docs.mjs --json 85c6d76ec42a879f219436eaa58bda06c68298b2

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 85c6d76ec42a879f219436eaa58bda06c68298b2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants