Files
2026-05-27 23:29:53 +01:00

133 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tasks — Permissions completion
> Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work that was already in main when this change opened. Do not tick a box for a test that doesn't exist or doesn't pin the requirement it claims to verify.
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## 1. Spec authoring
- [ ] 1.1 Read `../suaveplan/packages/universal/permissions/src/presets/default-preset/default-preset.ts` and `default-preset.types.ts` end-to-end
- [ ] 1.2 Read `../suaveplan/packages/universal/permissions/src/permission-events.ts` end-to-end
- [ ] 1.3 Read `../suaveplan/packages/universal/permissions/src/utils/deep-get.ts` and `utils/pattern-matching.ts` end-to-end
- [ ] 1.4 Read genesis `packages/auth/permissions/src/{core,conditions,adapters,events,presets}` to confirm what exists
- [ ] 1.5 Read `openspec/AGENTS.md` Sections 13 (spec-conformance) and 14 (source mapping)
- [ ] 1.6 Draft `proposal.md` with explicit ADDED / MODIFIED lists and a `## Source mapping` table citing upstream line ranges
- [ ] 1.7 Draft `specs/permissions/spec.md` with an `## ADDED Requirements` section per gap and `## MODIFIED Requirements` for the three behavior extensions
- [ ] 1.8 Every requirement carries a `**Verified by:**` line naming concrete `it(...)` titles
- [ ] 1.9 `openspec change validate permissions-completion` exits with no errors
## 2. `createPermissions` factory + `PermissionsInstance`
- [ ] 2.1 Create `packages/auth/permissions/src/presets/default-preset/default-preset.types.ts` with `PresetOptions<TAction, TSubject, TConditions>` (overrides for `conditionMatcher`, `fieldMatcher`, `subjectDetector`, `adapter`, `builder`)
- [ ] 2.2 Add `PermissionsInstance<TAction, TSubject, TConditions>` interface with readonly fields `ability`, `builder`, `adapter`, `conditionMatcher`, `fieldMatcher`, `subjectDetector`
- [ ] 2.3 Port `createPermissions` from upstream `default-preset.ts` L60-L97 into `packages/auth/permissions/src/presets/default-preset/default-preset.ts`. Preserve generic parameters (`TAction extends string = DefaultAction`, etc.)
- [ ] 2.4 Wire `MemoryAdapter`, `AbilityBuilder`, `DefaultConditionMatcher`, `DefaultFieldMatcher`, `DefaultSubjectDetector` defaults; honour every override from `PresetOptions`
- [ ] 2.5 `Object.freeze` the returned `PermissionsInstance` so callers can't mutate the matcher slot post-creation
- [ ] 2.6 Add subpath export `./presets/default-preset/default-preset` in `package.json`
- [ ] 2.7 Re-export `createPermissions`, `PermissionsInstance`, `PresetOptions` from `src/index.ts`
- [ ] 2.8 Sibling `default-preset.md` (≥200 words, basic + advanced examples)
## 3. `permissionEvents` singleton
- [ ] 3.1 Modify `packages/auth/permissions/src/events/permission-events.ts` to export a module-scope `permissionEvents` singleton typed as `EventBusLike<PermissionEventMap>` from `@suaveplan/events`
- [ ] 3.2 Use the genesis-canonical `createEventBus({ name: "permissions" })` factory (not the upstream custom `EventEmitter` class)
- [ ] 3.3 Update `core/permission-manager/permission-manager.ts` to default to `permissionEvents` when no bus is passed via constructor options
- [ ] 3.4 Update `core/role-manager/role-manager.ts` likewise
- [ ] 3.5 Update `core/policy-engine/policy-engine.ts` to emit `permission:checked` through the singleton
- [ ] 3.6 Add subpath export `./events/permission-events` in `package.json`
- [ ] 3.7 Re-export `permissionEvents` from `src/index.ts`
- [ ] 3.8 Sibling `permission-events.md` updated to document the singleton (≥200 words, basic listener + advanced filter-by-userId examples)
## 4. `deepGet` / `deepHas` utilities
- [ ] 4.1 Create `packages/auth/permissions/src/utils/deep-get/deep-get.ts` with `deepGet(obj, path)` and `deepHas(obj, path)`
- [ ] 4.2 Support dot-notation segments (`user.profile.role`)
- [ ] 4.3 Support numeric array-index segments (`tags.0` resolves `obj.tags[0]`); detected by `/^\d+$/.test(segment)`
- [ ] 4.4 Return `undefined` from `deepGet` (and `false` from `deepHas`) when any segment is missing or `current` is `null` / `undefined` / non-object
- [ ] 4.5 Update `conditions/condition-matcher/condition-matcher.ts` `DefaultConditionMatcher.match(conditions, context)` to use `deepGet(context, key)` instead of `context[key as keyof typeof context]`
- [ ] 4.6 Add subpath export `./utils/deep-get/deep-get` in `package.json`
- [ ] 4.7 Re-export `deepGet`, `deepHas` from `src/index.ts`
- [ ] 4.8 Sibling `deep-get.md` (≥200 words, basic + advanced array-index example)
## 5. `matchPattern` / `patternToRegex` utilities
- [ ] 5.1 Create `packages/auth/permissions/src/utils/pattern-match/pattern-match.ts` with `matchPattern(input, pattern)` and `patternToRegex(pattern)`
- [ ] 5.2 Patterns: `*` and `"manage"` match everything; `foo*` is prefix match; `*foo` is suffix match; `foo*bar` is prefix + suffix; otherwise exact equality
- [ ] 5.3 Escape regex special characters in non-wildcard portions before compiling: `.`, `+`, `?`, `^`, `$`, `{`, `}`, `(`, `)`, `|`, `[`, `]`, `\`
- [ ] 5.4 Cache compiled `RegExp` in a module-scope `Map<string, RegExp>` keyed by pattern string
- [ ] 5.5 LRU-evict at 1024 entries (insertion-order eviction via `Map.keys().next().value`)
- [ ] 5.6 Update `conditions/field-matcher/field-matcher.ts` `DefaultFieldMatcher.match(pattern, field)` to delegate to `matchPattern(field, pattern)` (keeping the existing exact-match fast path before pattern parse)
- [ ] 5.7 Update `core/ability/ability.ts` `Ability.can` action / subject lookup to delegate to `matchPattern` so registered wildcard rules (e.g. `can("read*", "Article")`) are honoured
- [ ] 5.8 Add subpath export `./utils/pattern-match/pattern-match` in `package.json`
- [ ] 5.9 Re-export `matchPattern`, `patternToRegex` from `src/index.ts`
- [ ] 5.10 Sibling `pattern-match.md` (≥200 words, basic + advanced example showing both prefix and suffix wildcards)
## 6. Tests
- [ ] 6.1 `presets/default-preset/default-preset.test.ts` — destructure-and-use; matcher overrides honoured; adapter override honoured; multiple instances are independent (changing one's matcher does not affect the other)
- [ ] 6.2 `presets/default-preset/default-preset.spec-conformance.test.ts` — `expectTypeOf` checks generics flow through; result is `Object.isFrozen`; `PresetOptions` accepts every documented override
- [ ] 6.3 `events/permission-events.test.ts` — listener registration; payload shapes for all 5 event names; manager auto-emits `permission:checked` and `permission:granted` when no bus passed
- [ ] 6.4 `events/permission-events.spec-conformance.test.ts` — re-importing the module returns the same `permissionEvents` instance; `EventBusLike<PermissionEventMap>` contract pinned (`expectTypeOf<typeof permissionEvents>().toMatchTypeOf<EventBusLike<PermissionEventMap>>()`)
- [ ] 6.5 `utils/deep-get/deep-get.test.ts` — dot-path resolution at depth 3; array-index resolution; missing segment returns `undefined`; null-safe traversal; `deepHas` reports correctly
- [ ] 6.6 `utils/deep-get/deep-get.spec-conformance.test.ts` — `deepHas(obj, p) === (deepGet(obj, p) !== undefined)` for every test fixture
- [ ] 6.7 `utils/pattern-match/pattern-match.test.ts` — exact match; `*` wildcard; `manage` wildcard; `foo*` prefix; `*foo` suffix; `foo*bar` both-side; special-char input must not be matched accidentally (e.g. `.` in input)
- [ ] 6.8 `utils/pattern-match/pattern-match.spec-conformance.test.ts` — RegExp cache: same pattern returns same RegExp instance; cache evicts at 1024 entries (insertion-order)
- [ ] 6.9 `conditions/condition-matcher/condition-matcher.test.ts` — extend with dot-path keys (`user.profile.role` resolves through `deepGet`)
- [ ] 6.10 `conditions/field-matcher/field-matcher.test.ts` — extend with wildcard-pattern matching (`field*` matches `fieldName`; `*Name` matches `firstName`)
- [ ] 6.11 `core/ability/ability.test.ts` — extend with wildcard-action matching (`can("read*", "Article")` matches `ability.can("read", "Article")` and `ability.can("readMeta", "Article")`)
- [ ] 6.12 Every `it(...)` in 6.1–6.11 carries `{ timeout: 5000 }`
## 7. Documentation
- [ ] 7.1 Sibling `.md` for every new implementation `.ts` (≥200 words, basic + advanced examples, full API reference)
- [ ] 7.2 Update `packages/auth/permissions/README.md` to link the four new co-located `.md` files under `## Modules`
- [ ] 7.3 Update `CLAUDE.md` package listing for `@suaveplan/permissions` to reflect the four new exports
## 8. Pre-archive verification (the 5-gate triple)
- [ ] 8.1 `bun run check` — Biome 0/0/0 across the whole monorepo
- [ ] 8.2 `bunx turbo lint typecheck test --filter=@suaveplan/permissions` — green
- [ ] 8.3 `bunx turbo test:coverage --filter=@suaveplan/permissions` — 100/100/100/100
- [ ] 8.4 `bun scripts/openspec-conformance.ts @suaveplan/permissions` — exit 0, no `error` findings
- [ ] 8.5 `bunx turbo build --filter=@suaveplan/permissions` — green
## 9. Pre-archive 7-row checklist (AGENTS.md Section 13)
- [ ] 9.1 Generics threaded; `expectTypeOf` assertions on `PermissionsInstance<TAction, TSubject, TConditions>` and `PresetOptions<...>`
- [ ] 9.2 Zod schemas wired at every declared boundary — `PresetOptions` validated at `createPermissions` entry; `permissionEvents` payloads typed against `PermissionEventMap`
- [ ] 9.3 Every existing error code in `errors/permission-error.ts` still thrown by at least one production path (no orphans introduced by the refactor)
- [ ] 9.4 Strict-mode behaviors tested both on and off (e.g. `Ability.can` strict-action mode vs. wildcard mode)
- [ ] 9.5 OTel instruments emitted from production paths and asserted with a real in-memory `MeterProvider`: `permissions.checks_total`, `permissions.grants_total`, `permissions.denies_total`, `permissions.event_emit_count`
- [ ] 9.6 Lifecycle dedup tested where claimed: re-importing `events/permission-events.ts` returns the same singleton; `patternToRegex` cache reuse
- [ ] 9.7 No retroactive tick — every box above reflects work done in this change
## 10. Archive
- [ ] 10.1 Fold `specs/permissions/spec.md` into `openspec/specs/permissions/spec.md` (creating the cap file — no `permissions` cap exists in `openspec/specs` today)
- [ ] 10.2 Move `openspec/changes/permissions-completion/` → `openspec/archive/<yyyy-mm-dd>-permissions-completion/`
- [ ] 10.3 Refresh `MANIFEST.md` (no package set or dep changes expected, but rerun for safety)
- [ ] 10.4 Update `STATUS.md` with the rolled-up gap closure for the four enumerated features
## Audit Note (2026-05-02)
Audit verdict: **DEFERRED — 0/74 tasks evidenced in code**.
Evidence reviewed:
- `src/index.ts` re-exports the existing surface (Ability, AbilityBuilder, PermissionManager, RoleManager, PolicyEngine, MemoryAdapter, role presets) but does NOT export `createPermissions`, `permissionEvents`, `deepGet`, `deepHas`, `matchPattern`, or `patternToRegex`.
- `src/utils/` does not exist; no `pattern-match/` or `deep-get/` directories.
- `src/presets/` contains only `role-presets/`; no `default-preset/` (and therefore no `createPermissions` factory + frozen `PermissionsInstance`).
- `events/permission-events.ts` exports a type only (`PermissionEventMap`), not the `permissionEvents` singleton wired through `@suaveplan/events`.
Missing pieces:
1. `presets/default-preset/default-preset.ts` (`createPermissions` factory returning a frozen `PermissionsInstance`).
2. `events/permission-events.ts` runtime export of the singleton `EventBusLike<PermissionEventMap>`, plus auto-wiring through PermissionManager / RoleManager / PolicyEngine.
3. `utils/deep-get/` and `utils/pattern-match/` (with cache eviction at 1024 entries) and the integrations in `condition-matcher`, `field-matcher`, and `Ability.can` wildcard handling.
Decision: leave change open. The four feature blocks (factory, events singleton, deep-get, pattern-match) are independent additions, each with its own tests, docs, and OTel instruments — not a tick-and-archive job.