Files
my-pi/pi-permission-system/docs/retro/0532-split-value-guards-by-cohesion.md
T

7.8 KiB

issue, issue_title
issue issue_title
532 pi-permission-system: split value-guards.ts by cohesion

Retro: #532 — pi-permission-system: split value-guards.ts by cohesion

Stage: Planning (2026-07-07T00:00:00Z)

Session summary

Planned Phase 8 Step 8: split value-guards.ts by cohesion, keeping the generic parsing guards (toRecord, getNonEmptyString) and moving the domain guards (isPermissionState, isDenyWithReason) to types.ts beside the PermissionState / DenyWithReason types they narrow. The plan is a single atomic refactor: step (extraction + three src/ consumer repoints + test move) plus a doc-update step, releasing independently.

Observations

  • The issue's "Proposed change" lists normalizeOptionalStringArray and normalizeOptionalPositiveInt among the generic guards to keep, but #547 already removed both (commit 146844aa) when zod took over config validation. The surviving generic set is only toRecord + getNonEmptyString; value-guards.ts is now 38 LOC, not the 56 the issue cites. The roadmap step note already records this shrink.
  • Domain-guard consumers are exactly three src/ files (permission-manager.ts, normalize.ts, config-loader.ts) plus test/value-guards.test.ts. All three already import types from ./types, so the repoint merges into an existing import source.
  • The no-restricted-imports ESLint rule on permission-manager.ts blocks only access-intent/access-path, so importing a guard from ./types is allowed.
  • Removing the two exports from value-guards.ts breaks every importer at the type level in the same commit, so extraction + consumer updates + test moves are one atomic step — mirrors #479's single-step common.ts split.
  • types.ts transitions from types-only to types-plus-guards; judged a natural co-location (a guard is the runtime companion of its type) and endorsed by the roadmap, so no ask_user gate was needed.
  • Doc updates confined to the live architecture.md (module-tree lines, Step 8 marker + Mermaid node, fallow health row 1 → 0); all other doc hits are historical records (docs/plans, docs/retro, docs/decisions, history/) that must not be edited.
  • Risk flagged: if fallow does not clear value-guards.ts to 0 targets (its fan-in is on the retained generic guards), record the actual count rather than forcing the health-row edit — the mixed-cohesion smell, not fan-in, is what this step targets.

Stage: Implementation — TDD (2026-07-07T18:59:00Z)

Session summary

Executed the plan's single TDD cycle: moved isPermissionState and isDenyWithReason from src/value-guards.ts to src/types.ts, repointed the three domain-guard consumers (permission-manager.ts, normalize.ts, config-loader.ts), and split the guard tests into a new test/types.test.ts. Followed with a docs: commit marking Phase 8 Step 8 (and the phase itself) complete in architecture.md. All 2272 pi-permission-system tests pass (no count delta — assertions moved, none added or removed); full monorepo check/lint/test/fallow dead-code all green.

Observations

  • Confirmed the plan's anticipated deviation: fallow (the refactoring-target report, not the dead-code gate) still flags src/value-guards.ts as a target (19 dependents) after the move, because the fan-in comes from the retained generic guards (toRecord, getNonEmptyString), not the relocated domain guards. Per the plan's own Risks section, did not force the health-metric row to the projected 0 — recorded the actual state with an inline explanation instead. pnpm fallow dead-code, the actual CI gate, stayed clean throughout.
  • The autoformatter (pi-autoformat) kept the two ./types imports (guard + type) as separate lines rather than merging them in normalize.ts / config-loader.ts; both forms resolve identically, so no follow-up needed.
  • Added (complete) to the Phase 8 heading, matching the convention already used for Phase 7 — all 8 roadmap steps are now .
  • A [#532] reference inside the module-tree fenced code block had to be corrected to bare #532 per the markdown-conventions skill (issue refs inside fenced code blocks are bare, matching the block's existing (#547) style) — caught before commit.
  • Cross-checked the plan's Module-Level Changes file list against git diff --name-only 7e00da1c^..72cb13a7: exact match, no drift.
  • Unrelated concurrent work from other sessions (issues #530, #531, #554) landed on main between the planning and TDD sessions; scoped the pre-completion reviewer's diff explicitly to this issue's two commits to avoid it reviewing unrelated changes.
  • Pre-completion reviewer: PASS. No blocking or non-blocking findings; the fallow health-row deviation was explicitly called out as documented, not a defect.

Stage: Final Retrospective (2026-07-07T19:15:00Z)

Session summary

Shipped Phase 8 Step 8 across three clean stages (Planning, TDD, Ship): moved the domain guards isPermissionState/isDenyWithReason from value-guards.ts to types.ts, repointed three consumers, split the guard tests into test/types.test.ts, and marked Phase 8 complete in architecture.md. CI landed green on 554479f9, issue #532 was closed, and — as expected for an all-refactor:/excluded-docs: range — no release was cut; the work auto-batches into the next release.

Observations

What went well

  • The plan pre-priced its own deviation. The plan's Risks section predicted that fallow might not clear value-guards.ts to 0 targets (the fan-in lives on the retained generic guards, not the moved domain guards) and pre-authorized recording the actual count instead of forcing the projected 0. When the deviation materialized exactly as predicted, the TDD stage applied the pre-agreed handling with no scramble and no re-decision — a case of planning-stage foresight eliminating implementation-stage friction. The pre-completion reviewer then classified it as documented, not a defect.
  • Single-atomic-step discipline held. Removing the two exports breaks every importer at the type level in one commit, so the plan folded extraction + three consumer repoints + test move into one step (mirroring #479). Execution matched: one refactor: commit, pnpm run check run immediately after the import change, clean.
  • Incremental verification throughout. Red ran the affected test file, Green re-ran it, pnpm run check fired right after the shared-import change, then full suite → lint → fallow dead-code — no end-of-session verification pile-up.

What caused friction (agent side)

  • other (tool-interaction) — the first Red-phase Edit batch was rejected with edits.1.newText: must have required properties newText when a block-deletion used an empty-string newText; the immediate retry with the same empty-string replacement succeeded. Impact: one extra tool call, no rework, no behavior effect. Too minor and too generic (not project-specific) to warrant a rule.
  • other (convention nuance, self-identified) — a [#532] reference-style link was initially placed inside the architecture.md module-tree fenced code block; caught against the markdown-conventions rule (issue refs inside fenced blocks are bare, matching the block's (#547) style) and corrected to bare #532 before commit. Impact: caught pre-commit, no rework.

What caused friction (user side)

  • None. The issue was operator-authored, the plan was unambiguous, and no mid-session correction was needed.

Changes made

  1. Appended this Final Retrospective stage entry to packages/pi-permission-system/docs/retro/0532-split-value-guards-by-cohesion.md. No AGENTS.md or prompt changes — the session surfaced no reusable process gap.