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

12 KiB
Raw Permalink Blame History

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.