mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: vendor permission system source
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
---
|
||||
issue: 368
|
||||
issue_title: "Remove the `config-modal` controller reach-through"
|
||||
---
|
||||
|
||||
# Remove the `config-modal` controller reach-through
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The `show` branch of the `/permission-system` command handler reaches through the controller dependency bag to two strangers in a single expression:
|
||||
|
||||
```typescript
|
||||
const rules = controller.permissionManager.getComposedConfigRules(
|
||||
controller.session.lastKnownActiveAgentName ?? undefined,
|
||||
);
|
||||
```
|
||||
|
||||
The command should not know that the active agent name lives on `session.lastKnownActiveAgentName` and that it must be threaded into `permissionManager.getComposedConfigRules`.
|
||||
That is a Law-of-Demeter violation: the handler talks to two collaborators it reaches through the controller bag.
|
||||
The same coupling keeps `PermissionSession.lastKnownActiveAgentName` alive only through object-literal wiring in the composition root, which is why `fallow` flags the getter as a false-positive unused member (carried today with a suppression).
|
||||
|
||||
This is Phase 5 Step 7 (Track D) of the `pi-permission-system` improvement roadmap.
|
||||
It is independent of all other tracks and touches only `config-modal.ts` and its composition-root wiring.
|
||||
|
||||
## Goals
|
||||
|
||||
- Collapse the controller's `permissionManager` + `session` fields into a single `getActiveAgentConfigRules(): Ruleset` accessor.
|
||||
- Wire that accessor in the composition root (`index.ts`) as a thin adapter closure, so the reach-through lives where both collaborators are already in scope.
|
||||
- Have the `show` handler issue a single tell (`controller.getActiveAgentConfigRules()`) instead of chaining through the bag.
|
||||
- Retire the `fallow` false-positive suppression on `PermissionSession.lastKnownActiveAgentName` now that it is consumed through a real closure body (a traced read) rather than object-literal wiring.
|
||||
- Mark Phase 5 Step 7 complete in `docs/architecture/architecture.md`.
|
||||
|
||||
This change is **not breaking**: `PermissionSystemConfigController` is a package-internal type, the wiring is internal to `index.ts`, and the observable behavior of `/permission-system show` is unchanged.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No change to `getComposedConfigRules` on `PermissionManager` — its signature and behavior stay as-is.
|
||||
- No change to the `lastKnownActiveAgentName` getter itself — it stays on `PermissionSession`; only the suppression comment and its doc comment change.
|
||||
- No change to any other Phase 5 track (Steps 1–6).
|
||||
- No change to the `show` output format, the config summary, or rule-origin display.
|
||||
|
||||
## Background
|
||||
|
||||
Relevant modules:
|
||||
|
||||
- `src/config-modal.ts` — defines the package-internal `PermissionSystemConfigController` interface and the `registerPermissionSystemCommand` factory.
|
||||
The `handleArgs` function's `show` branch performs the reach-through.
|
||||
- `src/index.ts` (≈ line 113) — the composition root constructs `permissionManager` and `session` as locals, then passes them into the controller bag via `registerPermissionSystemCommand(pi, { config, configPath, permissionManager, session })`.
|
||||
- `src/permission-manager.ts` (≈ line 195) — `getComposedConfigRules(agentName?: string): Ruleset` returns the composed config-layer rules; it always returns a `Ruleset` (never `undefined`).
|
||||
- `src/permission-session.ts` (≈ line 153) — the `lastKnownActiveAgentName` getter carries a `fallow-ignore-next-line unused-class-member` suppression plus a comment explaining the object-literal-wiring blind spot.
|
||||
|
||||
Constraint from the package skill / retro `0341`: `fallow`'s blind spot is the object-literal wiring in `index.ts` — config-modal receives `session` as an object-literal property, not a traced positional argument, so `fallow` cannot see the getter being read.
|
||||
Moving the read into a real arrow-function body in `index.ts` (`session.lastKnownActiveAgentName`) makes it a directly traced property access, which is exactly the usage `fallow` can follow.
|
||||
This is what makes retiring the suppression safe.
|
||||
|
||||
Constraint from AGENTS.md / package skill: keep schema, example config, loader, and docs aligned — none of those are touched here (no config surface changes), but the architecture roadmap step must be marked complete in the same change.
|
||||
|
||||
## Design Overview
|
||||
|
||||
Replace the two narrow collaborator references on the controller interface with a single value-returning accessor.
|
||||
|
||||
Before:
|
||||
|
||||
```typescript
|
||||
interface PermissionSystemConfigController {
|
||||
config: CommandConfigStore;
|
||||
configPath: string;
|
||||
permissionManager: { getComposedConfigRules(agentName?: string): Ruleset };
|
||||
session: { readonly lastKnownActiveAgentName: string | null };
|
||||
}
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```typescript
|
||||
interface PermissionSystemConfigController {
|
||||
config: CommandConfigStore;
|
||||
configPath: string;
|
||||
/** Returns the composed config-layer ruleset for the active agent scope. */
|
||||
getActiveAgentConfigRules(): Ruleset;
|
||||
}
|
||||
```
|
||||
|
||||
The `show` branch becomes a single tell:
|
||||
|
||||
```typescript
|
||||
if (normalized === "show") {
|
||||
const rules = controller.getActiveAgentConfigRules();
|
||||
ctx.ui.notify(
|
||||
`permission-system: ${summarizeConfig(controller.config.current(), rules)}`,
|
||||
"info",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
Composition-root wiring (`index.ts`) — the reach-through collapses into a thin adapter closure where both locals are already in scope:
|
||||
|
||||
```typescript
|
||||
registerPermissionSystemCommand(pi, {
|
||||
config: configStore,
|
||||
configPath,
|
||||
getActiveAgentConfigRules: () =>
|
||||
permissionManager.getComposedConfigRules(
|
||||
session.lastKnownActiveAgentName ?? undefined,
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
Design rationale:
|
||||
|
||||
- The accessor returns a value (the `Ruleset`), so this is a genuine encapsulation of a query, not procedure-splitting — it removes a Law-of-Demeter reach-through and gives the handler one collaborator to tell.
|
||||
- Field count on `PermissionSystemConfigController` drops from 4 to 3; two fields that always travelled together (`permissionManager` + `session`, used only to compute one ruleset) collapse into the one query the handler actually needs (ISP).
|
||||
- `getComposedConfigRules` always returns a `Ruleset`, so `getActiveAgentConfigRules()` always returns a defined `Ruleset` (possibly empty).
|
||||
`summarizeConfig` already handles an empty ruleset via `formatRulesSummary` returning `""`, so the existing "omit rule summary when no config rules" behavior is preserved without any optionality.
|
||||
|
||||
Edge cases:
|
||||
|
||||
- Empty ruleset → `formatRulesSummary` returns `""`, summary shows knobs only (unchanged).
|
||||
- `lastKnownActiveAgentName` is `null` → coalesced to `undefined`, passed to `getComposedConfigRules` (unchanged — this logic simply moves from the handler into the closure).
|
||||
|
||||
## Module-Level Changes
|
||||
|
||||
- `src/config-modal.ts`
|
||||
- Replace the `permissionManager` and `session` fields on `PermissionSystemConfigController` with a single `getActiveAgentConfigRules(): Ruleset` method.
|
||||
- Update the `show` branch in `handleArgs` to call `controller.getActiveAgentConfigRules()`.
|
||||
- The `Ruleset` import stays (still referenced by `getActiveAgentConfigRules` and `formatRulesSummary`).
|
||||
- `src/index.ts`
|
||||
- Change the `registerPermissionSystemCommand` call site to pass `getActiveAgentConfigRules: () => permissionManager.getComposedConfigRules(session.lastKnownActiveAgentName ?? undefined)` in place of the `permissionManager` and `session` properties.
|
||||
- `src/permission-session.ts`
|
||||
- Remove the `fallow-ignore-next-line unused-class-member` suppression on the `lastKnownActiveAgentName` getter.
|
||||
- Update the preceding comment from "Read by config-modal (`controller.session.lastKnownActiveAgentName`)" to note it is read by the `index.ts` config-modal adapter closure.
|
||||
- `test/config-modal.test.ts`
|
||||
- Update all four controller literals: replace `permissionManager: { getComposedConfigRules: () => ... }` + `session: { lastKnownActiveAgentName: null }` with `getActiveAgentConfigRules: () => ...` (preserving each test's intended ruleset: `[] as Ruleset` or the `composedRules` fixture).
|
||||
- `docs/architecture/architecture.md`
|
||||
- Append `✓ complete` to the Phase 5 Step 7 line (Track D, `[#368]`).
|
||||
- The metrics table row "`config-modal` controller reach-throughs" baseline `1` → target `0` is now met; leave the table as the historical baseline record (no edit needed beyond the step-complete marker, consistent with how prior steps were marked).
|
||||
|
||||
## Test Impact Analysis
|
||||
|
||||
1. New tests enabled by the change: none of substance.
|
||||
The new `getActiveAgentConfigRules` is a wiring closure in `index.ts`, not a new extracted module with independently testable logic.
|
||||
The existing `config-modal.test.ts` show-output tests already exercise the accessor seam (they inject the ruleset directly), so behavior remains covered at the same layer.
|
||||
2. Tests that become redundant: none.
|
||||
The two behavioral show-output tests (rule origins present / rule summary omitted) remain meaningful — they now drive the single accessor instead of the two-field bag.
|
||||
3. Tests that must stay as-is: `test/permission-session.test.ts` "exposes lastKnownActiveAgentName" genuinely exercises the getter on `PermissionSession` and is unaffected by the controller-interface change.
|
||||
|
||||
## TDD Order
|
||||
|
||||
This is a behavior-preserving refactor whose interface change breaks `index.ts` and every `config-modal.test.ts` controller literal at the type level in the same commit, so it lands as one atomic step (per the AGENTS.md rule: removing/replacing interface fields with constructed call sites must update production wiring and consumer tests together).
|
||||
|
||||
1. `refactor: collapse config-modal controller reach-through into getActiveAgentConfigRules accessor (#368)`
|
||||
- Test surface: `test/config-modal.test.ts` — update all four controller literals to the `getActiveAgentConfigRules` shape; the existing show-output assertions ("includes rule origins", "omits rule summary") are the behavior-preserving safety net and must continue to pass unchanged.
|
||||
- Production: replace the two controller fields with `getActiveAgentConfigRules(): Ruleset` in `config-modal.ts`; update the `show` branch to a single tell; move the reach-through into the adapter closure in `index.ts`; remove the `fallow` suppression and update the doc comment in `permission-session.ts`.
|
||||
- Verify: `pnpm --filter @gotgenes/pi-permission-system run check`, `run lint`, `run test`, and `pnpm fallow dead-code` (confirming `lastKnownActiveAgentName` is no longer reported now that the closure reads it directly).
|
||||
2. `docs: mark Phase 5 Step 7 complete in architecture roadmap (#368)`
|
||||
- Surface: `docs/architecture/architecture.md` — append `✓ complete` to the Step 7 (Track D) line.
|
||||
- Commit separately so the doc-only change does not couple to the code commit's review; it touches an excluded path and does not trigger a release.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- Risk: `fallow` still flags `lastKnownActiveAgentName` after the change (the retro `0341` attempt with a named interface did not satisfy `fallow`).
|
||||
Mitigation: the prior failure was object-literal wiring; this change makes `session.lastKnownActiveAgentName` a direct read in a real arrow-function body in `index.ts`, which `fallow` traces.
|
||||
The verify step in cycle 1 runs `pnpm fallow dead-code` before commit — if the getter is still flagged, restore a single justified suppression (with an updated rationale) rather than blocking, and note the residual blind spot in the retro.
|
||||
- Risk: a missed controller literal in `test/config-modal.test.ts` leaves a stale `permissionManager`/`session` shape.
|
||||
Mitigation: TypeScript's excess-property checking rejects the stale fields immediately at `pnpm run check`; all four literals are enumerated in Module-Level Changes.
|
||||
- Risk: silently changing `show` output when the ruleset is empty.
|
||||
Mitigation: `getComposedConfigRules` always returns a `Ruleset` and `formatRulesSummary` already returns `""` for an empty config layer; the "omits rule summary" test guards this.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None blocking.
|
||||
The `fallow` outcome is the only thing to confirm empirically during cycle 1; the plan carries a documented fallback if the suppression cannot be fully retired.
|
||||
Reference in New Issue
Block a user