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
normalizeOptionalStringArrayandnormalizeOptionalPositiveIntamong the generic guards to keep, but #547 already removed both (commit146844aa) when zod took over config validation. The surviving generic set is onlytoRecord+getNonEmptyString;value-guards.tsis 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) plustest/value-guards.test.ts. All three already import types from./types, so the repoint merges into an existing import source. - The
no-restricted-importsESLint rule onpermission-manager.tsblocks onlyaccess-intent/access-path, so importing a guard from./typesis allowed. - Removing the two exports from
value-guards.tsbreaks every importer at the type level in the same commit, so extraction + consumer updates + test moves are one atomic step — mirrors #479's single-stepcommon.tssplit. types.tstransitions 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 noask_usergate 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
fallowdoes not clearvalue-guards.tsto 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 thedead-codegate) still flagssrc/value-guards.tsas 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./typesimports (guard + type) as separate lines rather than merging them innormalize.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#532per 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
mainbetween 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
fallowmight not clearvalue-guards.tsto 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 projected0. 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 checkrun immediately after the import change, clean. - Incremental verification throughout.
Red ran the affected test file, Green re-ran it,
pnpm run checkfired 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-phaseEditbatch was rejected withedits.1.newText: must have required properties newTextwhen a block-deletion used an empty-stringnewText; 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 thearchitecture.mdmodule-tree fenced code block; caught against themarkdown-conventionsrule (issue refs inside fenced blocks are bare, matching the block's(#547)style) and corrected to bare#532before 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
- Appended this Final Retrospective stage entry to
packages/pi-permission-system/docs/retro/0532-split-value-guards-by-cohesion.md. NoAGENTS.mdor prompt changes — the session surfaced no reusable process gap.