12 KiB
12 KiB
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 statusreports nothing) and no in-flight worktrees with uncommitted work (git worktree listthengit statusin 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.mdmatchesfind packages -maxdepth 3 -name package.jsoncount. 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.tsanddefault-preset.types.tsend-to-end - 1.2 Read
../suaveplan/packages/universal/permissions/src/permission-events.tsend-to-end - 1.3 Read
../suaveplan/packages/universal/permissions/src/utils/deep-get.tsandutils/pattern-matching.tsend-to-end - 1.4 Read genesis
packages/auth/permissions/src/{core,conditions,adapters,events,presets}to confirm what exists - 1.5 Read
openspec/AGENTS.mdSections 13 (spec-conformance) and 14 (source mapping) - 1.6 Draft
proposal.mdwith explicit ADDED / MODIFIED lists and a## Source mappingtable citing upstream line ranges - 1.7 Draft
specs/permissions/spec.mdwith an## ADDED Requirementssection per gap and## MODIFIED Requirementsfor the three behavior extensions - 1.8 Every requirement carries a
**Verified by:**line naming concreteit(...)titles - 1.9
openspec change validate permissions-completionexits with no errors
2. createPermissions factory + PermissionsInstance
- 2.1 Create
packages/auth/permissions/src/presets/default-preset/default-preset.types.tswithPresetOptions<TAction, TSubject, TConditions>(overrides forconditionMatcher,fieldMatcher,subjectDetector,adapter,builder) - 2.2 Add
PermissionsInstance<TAction, TSubject, TConditions>interface with readonly fieldsability,builder,adapter,conditionMatcher,fieldMatcher,subjectDetector - 2.3 Port
createPermissionsfrom upstreamdefault-preset.tsL60-L97 intopackages/auth/permissions/src/presets/default-preset/default-preset.ts. Preserve generic parameters (TAction extends string = DefaultAction, etc.) - 2.4 Wire
MemoryAdapter,AbilityBuilder,DefaultConditionMatcher,DefaultFieldMatcher,DefaultSubjectDetectordefaults; honour every override fromPresetOptions - 2.5
Object.freezethe returnedPermissionsInstanceso callers can't mutate the matcher slot post-creation - 2.6 Add subpath export
./presets/default-preset/default-presetinpackage.json - 2.7 Re-export
createPermissions,PermissionsInstance,PresetOptionsfromsrc/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.tsto export a module-scopepermissionEventssingleton typed asEventBusLike<PermissionEventMap>from@suaveplan/events - 3.2 Use the genesis-canonical
createEventBus({ name: "permissions" })factory (not the upstream customEventEmitterclass) - 3.3 Update
core/permission-manager/permission-manager.tsto default topermissionEventswhen no bus is passed via constructor options - 3.4 Update
core/role-manager/role-manager.tslikewise - 3.5 Update
core/policy-engine/policy-engine.tsto emitpermission:checkedthrough the singleton - 3.6 Add subpath export
./events/permission-eventsinpackage.json - 3.7 Re-export
permissionEventsfromsrc/index.ts - 3.8 Sibling
permission-events.mdupdated 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.tswithdeepGet(obj, path)anddeepHas(obj, path) - 4.2 Support dot-notation segments (
user.profile.role) - 4.3 Support numeric array-index segments (
tags.0resolvesobj.tags[0]); detected by/^\d+$/.test(segment) - 4.4 Return
undefinedfromdeepGet(andfalsefromdeepHas) when any segment is missing orcurrentisnull/undefined/ non-object - 4.5 Update
conditions/condition-matcher/condition-matcher.tsDefaultConditionMatcher.match(conditions, context)to usedeepGet(context, key)instead ofcontext[key as keyof typeof context] - 4.6 Add subpath export
./utils/deep-get/deep-getinpackage.json - 4.7 Re-export
deepGet,deepHasfromsrc/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.tswithmatchPattern(input, pattern)andpatternToRegex(pattern) - 5.2 Patterns:
*and"manage"match everything;foo*is prefix match;*foois suffix match;foo*baris prefix + suffix; otherwise exact equality - 5.3 Escape regex special characters in non-wildcard portions before compiling:
.,+,?,^,$,{,},(,),|,[,],\ - 5.4 Cache compiled
RegExpin a module-scopeMap<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.tsDefaultFieldMatcher.match(pattern, field)to delegate tomatchPattern(field, pattern)(keeping the existing exact-match fast path before pattern parse) - 5.7 Update
core/ability/ability.tsAbility.canaction / subject lookup to delegate tomatchPatternso registered wildcard rules (e.g.can("read*", "Article")) are honoured - 5.8 Add subpath export
./utils/pattern-match/pattern-matchinpackage.json - 5.9 Re-export
matchPattern,patternToRegexfromsrc/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—expectTypeOfchecks generics flow through; result isObject.isFrozen;PresetOptionsaccepts every documented override - 6.3
events/permission-events.test.ts— listener registration; payload shapes for all 5 event names; manager auto-emitspermission:checkedandpermission:grantedwhen no bus passed - 6.4
events/permission-events.spec-conformance.test.ts— re-importing the module returns the samepermissionEventsinstance;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 returnsundefined; null-safe traversal;deepHasreports 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;managewildcard;foo*prefix;*foosuffix;foo*barboth-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.roleresolves throughdeepGet) - 6.10
conditions/field-matcher/field-matcher.test.ts— extend with wildcard-pattern matching (field*matchesfieldName;*NamematchesfirstName) - 6.11
core/ability/ability.test.ts— extend with wildcard-action matching (can("read*", "Article")matchesability.can("read", "Article")andability.can("readMeta", "Article")) - 6.12 Every
it(...)in 6.1–6.11 carries{ timeout: 5000 }
7. Documentation
- 7.1 Sibling
.mdfor every new implementation.ts(≥200 words, basic + advanced examples, full API reference) - 7.2 Update
packages/auth/permissions/README.mdto link the four new co-located.mdfiles under## Modules - 7.3 Update
CLAUDE.mdpackage listing for@suaveplan/permissionsto 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, noerrorfindings - 8.5
bunx turbo build --filter=@suaveplan/permissions— green
9. Pre-archive 7-row checklist (AGENTS.md Section 13)
- 9.1 Generics threaded;
expectTypeOfassertions onPermissionsInstance<TAction, TSubject, TConditions>andPresetOptions<...> - 9.2 Zod schemas wired at every declared boundary —
PresetOptionsvalidated atcreatePermissionsentry;permissionEventspayloads typed againstPermissionEventMap - 9.3 Every existing error code in
errors/permission-error.tsstill 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.canstrict-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.tsreturns the same singleton;patternToRegexcache reuse - 9.7 No retroactive tick — every box above reflects work done in this change
10. Archive
- 10.1 Fold
specs/permissions/spec.mdintoopenspec/specs/permissions/spec.md(creating the cap file — nopermissionscap exists inopenspec/specstoday) - 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.mdwith 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.tsre-exports the existing surface (Ability, AbilityBuilder, PermissionManager, RoleManager, PolicyEngine, MemoryAdapter, role presets) but does NOT exportcreatePermissions,permissionEvents,deepGet,deepHas,matchPattern, orpatternToRegex.src/utils/does not exist; nopattern-match/ordeep-get/directories.src/presets/contains onlyrole-presets/; nodefault-preset/(and therefore nocreatePermissionsfactory + frozenPermissionsInstance).events/permission-events.tsexports a type only (PermissionEventMap), not thepermissionEventssingleton wired through@suaveplan/events.
Missing pieces:
presets/default-preset/default-preset.ts(createPermissionsfactory returning a frozenPermissionsInstance).events/permission-events.tsruntime export of the singletonEventBusLike<PermissionEventMap>, plus auto-wiring through PermissionManager / RoleManager / PolicyEngine.utils/deep-get/andutils/pattern-match/(with cache eviction at 1024 entries) and the integrations incondition-matcher,field-matcher, andAbility.canwildcard 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.