Repository navigation
cli: os build / os validate report a permission-set name collision — the compile door for #17516 - #18483
Conversation
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>
Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 24 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 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
|
Fixes #18024
Clause-②: no — this card adds NO published export and re-stamps no error-code vocabulary. It consumes exports
@objectstack/plugin-securityalready publishes (that is why PR #18022 itself gradedyes/minor), and making an existing refusal visible is a tightening or neutral, never a widening of an accept set. Thedispatcher-error-vocabularytoken stays stampedevent, 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 buildandos validatewalked 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 ispackages/cli/src/utils/nav-contribution-groups.ts; this change is the same half for permission sets.What it does NOT do
severity: 'warning'is declared at the producer and the failure direction is CLOSED — the set is not installed, so nothing is over-granted. This follows what decision(objectql): anavigationContributions[].groupthat names no group in the target app is silently RELOCATED to the top level — refuse, warn, or leave to the consumer? #14553 chose for nav (the maintainer's option B) rather than departing from it; making either command exit non-zero would narrow whatos buildaccepts, which is a different decision from the one this card carries.warningskey bothos build --jsonandos validate --jsonalready declare — the shapebuild-json-advisory-parityandbuild-json-undeclared-key-paritypin by name. Both commands compute the list, so the residue pin ("nothing rides in build that validate does not also report") holds rather than being weakened to fit.os validate --strictdoes promote them, as it promotes every other member ofwarnings— that is the declared meaning of the flag ("Treat warnings as errors"), and the nav half has had the same property since decision(objectql): anavigationContributions[].groupthat names no group in the target app is silently RELOCATED to the top level — refuse, warn, or leave to the consumer? #14553. Neither command's DEFAULT accept set changes.⭐ One derivation, consumed — not re-derived
packages/cli/src/utils/permission-set-name-collisions.tsimports the predicate and the wording from@objectstack/plugin-security's package entry, where #17516 published them for this consumer:permissionSetNameIsForeign— ⛔ never a local comparison, because a nullish owner is FOREIGN by that function's ruling;permissionSetNameCollisionDiagnostic+formatPermissionSetNameCollisionDiagnostic— ⛔ never retyped, because two doors phrasing one refusal differently IS the defect.reportPermissionSetNameCollisionsis 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 structuredmetarecord 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
if (!packageId)with a different warning and never reaches the collision branch.permissionscollection is read only when the stack carries nopackages[]:permissionscomposes byconcat, 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:
pnpm --filter '@objectstack/cli^...' buildpnpm --filter @objectstack/cli typechecktsc --noEmit+check:test-typecheck)pnpm --filter @objectstack/cli exec vitest run --project unitvitest run src/utils/permission-set-name-collisions.test.tsnode scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack--ran, every line carrying its own exit code so the zero is DERIVED, not claimedpnpm lint(eslint . --no-inline-config)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 commit20418d251(git rev-parse --short HEAD), clean worktree. The dependency-closure build, the full unit tier and the ablation were run on76a1ff09f, whose tree differs from the final one only by the added changeset and the two commitsorigin/mainbrought in (.claude/skills/**andscripts/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:
pnpm check:dual-build-cjs-loadsandpnpm check:i18n-coverageboth exited 3 —PREREQUISITE NOT MET, naming packages with nodist/. ⛔ Not a pass and not a finding. Afterpnpm build(73 tasks, 72 cached) both re-ran to exit 0: 104 require entry points across 67 packages load; 13 i18n configs, 621 baselined untranslated strings, none new.pnpm check:cross-package-test-inputsexits 1, rooted inpackages/cli/test/init-created-files-summary.e2e.test.ts— pre-existing on any tree withpackages/spec/distbuilt, already filed as [finding] check:cross-package-test-inputs passes in CI and fails on a built tree — its verdict is a function of gitignored build state #18353 / [regression] check:cross-package-test-inputs 的裁决取决于被 gitignore 的空目录 packages/spec/dist 存不存在 —— 构建过的工作树一律红,CI 绿只因那一步跑在构建之前(#18340 引入) #18348, untouched by this diff.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 buildthen 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-objectagainst theHEADblob, run, restored, and the restore proven by hash equality plus a cleangit diff HEAD. HEAD blobdbf66ed.permissionSetNameIsForeigncall — i.e. the wrong fix, a name-duplicate counterthe SAME package declaring its own set name twice is NOT a collision)dbf66edpackages[], read the flattened top level insteaddbf66edAcceptance notes — out of scope, not filed here
packages/cli/src/utils/nav-contribution-groups.tsexportsartifactPackagesOf, a SECOND implementation of the artifact package-id rule thatpackages/cli/src/utils/artifact-packages.tsowns, 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-emptynamebefore falling back to it;artifactPackagesdoes not). This change imports the owner rather than adding a third copy.packages/cli/test/validate-build-gate-parity.test.tssays of its roster "There is no third option — that is the whole point of the file", but its drift detector extracts onlylintFoo(/validateFoo(call sites, so afind*-named gate is invisible to it:findNavGroupDiagnosticshas been wired into both commands since decision(objectql): anavigationContributions[].groupthat 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