Files
my-pi/pi-permission-system/docs/plans/archive/0021-split-index-into-focused-modules.md

34 KiB
Raw Permalink Blame History

issue, issue_title
issue issue_title
21 Split src/index.ts (1,983 lines) into focused modules

Split src/index.ts into focused modules

Problem Statement

src/index.ts is ~1,973 lines and houses at least seven distinct concerns beyond the extension factory that is its actual responsibility. AGENTS.md requires "one concern per file in src/" — index.ts is the last major outlier.

Goals

  • Mechanically extract cohesive groups of functions into new focused modules.
  • Reduce src/index.ts line count by moving all extractable module-scope functions out.
  • Preserve all existing behavior — no observable change from the test suite.
  • Add unit tests for the newly extracted modules and the pre-existing focused modules that lack them, using dependency injection and vitest mocks to test each module in isolation.

Non-Goals

  • Behavior changes of any kind.
  • Restructuring the piPermissionSystemExtension factory or its closure-scoped helpers (they are inherently coupled to extension lifecycle state).

Background

Dependency status

Issue Title Status Relevance
#10 Consolidate config layout Closed/implemented Was a prerequisite — no longer blocks.
#20 Delete permission-request event channel Closed/implemented Removed emitPermissionRequestEvent and related types, reducing index.ts slightly.

What was in src/index.ts (~1,973 lines)

The file contained these function/constant groups beyond the extension factory:

  1. Active-agent detection (~50 lines): ACTIVE_AGENT_TAG_REGEX, normalizeAgentName, getActiveAgentName, getActiveAgentNameFromSystemPrompt.
  2. External-directory / path utilities (~70 lines): PATH_BEARING_TOOLS, normalizePathForComparison, isPathWithinDirectory, getPathBearingToolPath, isPathOutsideWorkingDirectory.
  3. Permission prompt formatting (~250 lines): formatMissingToolNameReason, formatUnknownToolReason, formatPermissionHardStopHint, formatDenyReason, formatUserDeniedReason, formatAskPrompt, formatSkillAskPrompt, formatSkillPathAskPrompt, formatSkillPathDenyReason, formatExternalDirectoryHardStopHint, formatExternalDirectoryAskPrompt, formatExternalDirectoryDenyReason, formatExternalDirectoryUserDeniedReason.
  4. Tool-input preview / text utilities (~120 lines): TOOL_INPUT_PREVIEW_MAX_LENGTH, TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH, TOOL_TEXT_SUMMARY_MAX_LENGTH, truncateInlineText, sanitizeInlineText, countTextLines, formatCount, getPromptPath, formatEditInputForPrompt, formatWriteInputForPrompt, formatReadInputForPrompt, formatSearchInputForPrompt, serializeToolInputPreview, formatJsonInputForPrompt, formatToolInputForPrompt, formatGenericToolInputForLog, getToolInputPreviewForLog, getPermissionLogContext.
  5. Subagent context (~25 lines): normalizeFilesystemPath, isSubagentExecutionContext.
  6. Forwarded-permission file IO (~180 lines): sleep, formatUnknownErrorMessage, isErrnoCode, logPermissionForwardingWarning, logPermissionForwardingError, ensureDirectoryExists, getPermissionForwardingLocationForSession, ensurePermissionForwardingLocation, getExistingPermissionForwardingLocation, tryRemoveDirectoryIfEmpty, cleanupPermissionForwardingLocationIfEmpty, safeDeleteFile, writeJsonFileAtomic, readForwardedPermissionRequest, readForwardedPermissionResponse.
  7. Forwarded-permission polling + confirmation (~180 lines): formatForwardedPermissionPrompt, waitForForwardedPermissionApproval, processForwardedPermissionRequests, confirmPermission.
  8. Misc helpers (~30 lines): extractSkillNameFromInput, getEventToolName, getEventInput, getContextSystemPrompt, getSessionId, canRequestPermissionConfirmation, derivePiProjectPaths, createPermissionManagerForCwd.

Permission surfaces affected

None — pure refactor.

Test coverage gap

Before this issue, the entire src/index.ts — and every pre-existing focused module that was already extracted (bash-filter.ts, wildcard-matcher.ts, system-prompt-sanitizer.ts, skill-prompt-sanitizer.ts, permission-manager.ts, etc.) — was tested exclusively through 2 integration test files:

File Tests Lines
tests/permission-system.test.ts 68 2,490
tests/session-start.test.ts 2 114

These are flat lists of test() calls (no describe() grouping) that exercise the full piPermissionSystemExtension factory end-to-end via a mock ExtensionAPI. The modules listed below have no dedicated unit test file at all:

  • src/active-agent.ts (58 lines) — newly extracted
  • src/bash-filter.ts (51 lines) — pre-existing
  • src/before-agent-start-cache.ts (44 lines) — pre-existing
  • src/common.ts (88 lines) — pre-existing
  • src/external-directory.ts (113 lines) — newly extracted
  • src/logging.ts (118 lines) — pre-existing
  • src/permission-dialog.ts (89 lines) — pre-existing
  • src/permission-forwarding.ts (126 lines) — pre-existing
  • src/permission-manager.ts (941 lines) — pre-existing
  • src/permission-prompts.ts (131 lines) — newly extracted
  • src/skill-prompt-sanitizer.ts (344 lines) — pre-existing
  • src/status.ts (35 lines) — pre-existing
  • src/subagent-context.ts (52 lines) — newly extracted
  • src/system-prompt-sanitizer.ts (210 lines) — pre-existing
  • src/tool-input-preview.ts (206 lines) — newly extracted
  • src/tool-registry.ts (139 lines) — pre-existing
  • src/wildcard-matcher.ts (84 lines) — pre-existing
  • src/yolo-mode.ts (29 lines) — pre-existing
  • src/forwarded-permissions/io.ts (328 lines) — newly extracted
  • src/forwarded-permissions/polling.ts (334 lines) — newly extracted

Design challenges still in index.ts

After extraction, src/index.ts is ~970 lines. The factory function itself is ~740 lines because it owns:

  1. Module-scope mutable stateextensionConfig, extensionLogger, loggingWarningReporter, and reportedLoggingWarnings live outside the factory. The factory writes to them via setExtensionConfig / setLoggingWarningReporter and every extracted module that needs logging receives it via setter injection (setForwardedPermissionLogger). This works but creates hidden temporal coupling: callers must call the setter before any logging function is invoked.

  2. Module-scope constants derived from getAgentDir()PI_AGENT_DIR, SESSIONS_DIR, SUBAGENT_SESSIONS_DIR, PERMISSION_FORWARDING_DIR, and GLOBAL_LOGS_DIR are all computed at import time, which violates the AGENTS.md rule "Do not cache getAgentDir() at module scope." They happen to work because getAgentDir() returns a stable value in production, but they make the module difficult to test in isolation (tests set PI_CODING_AGENT_DIR after import).

  3. Closure-scoped helpers that could be purerefreshExtensionConfig, saveExtensionConfig, resolveAgentName, shouldExposeTool, logResolvedConfigPaths, reviewPermissionDecision, promptPermission, startForwardedPermissionPolling, and stopForwardedPermissionPolling are all closures over permissionManager, runtimeContext, extensionConfig, and the forwarding timer. Some of these (e.g., resolveAgentName, shouldExposeTool) could be pure functions if given their dependencies as parameters; others (e.g., startForwardedPermissionPolling) genuinely need mutable timer state.

  4. Six event handlers inlinesession_start, resources_discover, session_shutdown, before_agent_start, input, and tool_call are defined inline as lambdas inside the factory. The tool_call handler alone is ~250 lines. These could be separate named functions that receive a context object with the shared state.

Addressing these is out of scope for this issue (pure refactor) but would be the next step toward a testable, sub-300-line index.ts.

Design Overview

New module layout (implemented)

New file Concern Actual lines
src/active-agent.ts Agent name extraction from session metadata and system prompt 58
src/external-directory.ts Path normalization, outside-cwd detection, PATH_BEARING_TOOLS, external-directory format helpers 113
src/permission-prompts.ts All format* helpers for ask/deny/user-denied prompts 131
src/tool-input-preview.ts Text utilities and tool-input formatting for prompts and logs 206
src/subagent-context.ts isSubagentExecutionContext, normalizeFilesystemPath 52
src/forwarded-permissions/io.ts Atomic JSON write, request/response read, directory ensure/cleanup, error helpers, logger setter 328
src/forwarded-permissions/polling.ts waitForForwardedPermissionApproval, processForwardedPermissionRequests, confirmPermission, getSessionId, getContextSystemPrompt, formatForwardedPermissionPrompt, PermissionForwardingDeps 334

src/index.ts retains (~970 lines):

  • Imports from the new modules and existing ones.
  • Module-scope logging state and helpers (extensionConfig, extensionLogger, writeDebugLog, writeReviewLog, etc.).
  • Module-scope constants (PI_AGENT_DIR, SESSIONS_DIR, etc.).
  • Small helpers tightly coupled to module state (extractSkillNameFromInput, getEventToolName, getEventInput, canRequestPermissionConfirmation, derivePiProjectPaths, createPermissionManagerForCwd).
  • piPermissionSystemExtension factory (the default export) with closure-scoped helpers and six event handlers.

Module dependency direction

index.ts
  ├── active-agent.ts
  ├── external-directory.ts
  ├── permission-prompts.ts
  │     └── tool-input-preview.ts
  ├── subagent-context.ts
  └── forwarded-permissions/
        ├── io.ts
        └── polling.ts  (imports io.ts, active-agent.ts, subagent-context.ts)

No new module imports from index.ts — dependency flows one way (index → modules).

Dependency injection patterns used

  • isSubagentExecutionContext(ctx, subagentSessionsDir) — takes the directory as a parameter instead of reading a module-scope constant.
  • getToolInputPreviewForLog(result, input, pathBearingTools) / getPermissionLogContext(result, input, pathBearingTools) — receive the PATH_BEARING_TOOLS set as a parameter.
  • ensurePermissionForwardingLocation(forwardingDir, sessionId) / getExistingPermissionForwardingLocation(forwardingDir, sessionId) — receive the forwarding directory as a parameter.
  • setForwardedPermissionLogger({ writeReviewLog, writeDebugLog }) — setter injection for the IO module's logger.
  • PermissionForwardingDeps — context object passed to confirmPermission, processForwardedPermissionRequests, and waitForForwardedPermissionApproval carrying forwardingDir, subagentSessionsDir, writeReviewLog, requestPermissionDecisionFromUi, and shouldAutoApprove.

Testing strategy: dependency injection and vitest mocks

Every module has collaborators — other modules it imports and calls. Unit tests should verify each module in isolation by mocking its collaborators with vi.mock() and, where needed, vi.fn() / vi.spyOn(). This ensures tests exercise the module's own logic and boundary conditions without coupling to the real behavior of dependencies.

Guiding principles:

  1. Mock collaborators, not the module under test. If permission-prompts.ts imports formatToolInputForPrompt from tool-input-preview.ts, the permission-prompts tests mock tool-input-preview.ts and verify that the prompts module calls it with the right arguments and uses its return value correctly.
  2. Use vi.mock() for module-level imports. Vitest hoists vi.mock() calls so the module under test receives mocked versions of its dependencies at import time.
  3. Use vi.fn() for injected function dependencies. When a function takes a callback or deps object (e.g., PermissionForwardingDeps), pass vi.fn() stubs and assert they were called correctly.
  4. Mock ExtensionContext as a plain object. The Pi ExtensionContext is an interface — tests construct minimal objects satisfying only the properties the module actually reads (e.g., { sessionManager: { getEntries: vi.fn() } }).
  5. Mock filesystem operations. Modules that use node:fs (forwarded-permissions/io.ts) should have node:fs mocked via vi.mock("node:fs") so tests never touch the real filesystem.
  6. Restore mocks between tests. Use afterEach(() => { vi.restoreAllMocks(); }) to prevent test pollution.

Example pattern for a module with collaborators:

import { describe, expect, test, vi, afterEach } from "vitest";

// Mock the collaborator module before importing the module under test.
vi.mock("../tool-input-preview.js", () => ({
  formatToolInputForPrompt: vi.fn(() => "mocked preview"),
}));

import { formatAskPrompt } from "../src/permission-prompts.js";
import { formatToolInputForPrompt } from "../src/tool-input-preview.js";

afterEach(() => {
  vi.restoreAllMocks();
});

describe("formatAskPrompt", () => {
  test("includes tool input preview for non-bash tools", () => {
    const result = formatAskPrompt(
      { toolName: "read", state: "ask", source: "tool" },
      "my-agent",
      { path: "/foo" },
    );
    expect(formatToolInputForPrompt).toHaveBeenCalledWith("read", { path: "/foo" });
    expect(result).toContain("mocked preview");
  });
});

Export strategy

Each new module exports only the functions and constants that index.ts (or sibling modules) actually reference. PermissionReviewSource stays in index.ts as it is only used there.

Module-scope constant rule

Per AGENTS.md, getAgentDir() must not be cached at module scope. The extracted modules receive directory values as parameters; index.ts calls getAgentDir() at invocation time inside closures (no change from current behavior). Constants like ACTIVE_AGENT_TAG_REGEX, PATH_BEARING_TOOLS, and length limits are safe at module scope since they do not depend on the environment.

Module-Level Changes

Added

File Content
src/active-agent.ts ACTIVE_AGENT_TAG_REGEX, normalizeAgentName, getActiveAgentName, getActiveAgentNameFromSystemPrompt
src/external-directory.ts PATH_BEARING_TOOLS, normalizePathForComparison, isPathWithinDirectory, getPathBearingToolPath, isPathOutsideWorkingDirectory, formatExternalDirectoryHardStopHint, formatExternalDirectoryAskPrompt, formatExternalDirectoryDenyReason, formatExternalDirectoryUserDeniedReason
src/permission-prompts.ts formatMissingToolNameReason, formatUnknownToolReason, formatPermissionHardStopHint, formatDenyReason, formatUserDeniedReason, formatAskPrompt, formatSkillAskPrompt, formatSkillPathAskPrompt, formatSkillPathDenyReason
src/tool-input-preview.ts truncateInlineText, sanitizeInlineText, countTextLines, formatCount, getPromptPath, formatEditInputForPrompt, formatWriteInputForPrompt, formatReadInputForPrompt, formatSearchInputForPrompt, serializeToolInputPreview, formatJsonInputForPrompt, formatToolInputForPrompt, formatGenericToolInputForLog, getToolInputPreviewForLog, getPermissionLogContext, length constants
src/subagent-context.ts normalizeFilesystemPath, isSubagentExecutionContext
src/forwarded-permissions/io.ts sleep, formatUnknownErrorMessage, isErrnoCode, logPermissionForwardingWarning, logPermissionForwardingError, ensureDirectoryExists, getPermissionForwardingLocationForSession, ensurePermissionForwardingLocation, getExistingPermissionForwardingLocation, tryRemoveDirectoryIfEmpty, cleanupPermissionForwardingLocationIfEmpty, safeDeleteFile, writeJsonFileAtomic, readForwardedPermissionRequest, readForwardedPermissionResponse, listRequestFiles, setForwardedPermissionLogger, ForwardedPermissionLogger
src/forwarded-permissions/polling.ts getSessionId, getContextSystemPrompt, formatForwardedPermissionPrompt, waitForForwardedPermissionApproval, processForwardedPermissionRequests, confirmPermission, PermissionForwardingDeps

Changed

File Change
src/index.ts Removed extracted functions/constants; added imports from new modules; reduced from ~1,973 to ~970 lines

Unchanged

All pre-existing modules (src/permission-manager.ts, src/bash-filter.ts, src/wildcard-matcher.ts, src/system-prompt-sanitizer.ts, src/skill-prompt-sanitizer.ts, src/extension-config.ts, etc.) and test files are not modified.

TDD Order

Phase 1: Extract modules (steps 18, completed)

Since this was a pure mechanical refactor with no behavior change, the cycle was extract → verify → commit. Existing tests passed after each step without modification.

  1. Extract src/active-agent.ts — move agent-name detection functions. Commit: refactor: extract active-agent detection into src/active-agent.ts (#21)

  2. Extract src/subagent-context.ts — move subagent detection helpers. Commit: refactor: extract subagent context into src/subagent-context.ts (#21)

  3. Extract src/tool-input-preview.ts — move text utilities and tool-input formatters. Commit: refactor: extract tool-input preview into src/tool-input-preview.ts (#21)

  4. Extract src/external-directory.ts — move path utilities and external-directory format helpers. Commit: refactor: extract external-directory logic into src/external-directory.ts (#21)

  5. Extract src/permission-prompts.ts — move ask/deny/user-denied prompt formatters (imports tool-input-preview.ts). Commit: refactor: extract permission prompts into src/permission-prompts.ts (#21)

  6. Extract src/forwarded-permissions/io.ts — move file IO, directory management, and error helpers. Commit: refactor: extract forwarded-permission IO into src/forwarded-permissions/io.ts (#21)

  7. Extract src/forwarded-permissions/polling.ts — move polling loop and confirmPermission. Commit: refactor: extract forwarded-permission polling into src/forwarded-permissions/polling.ts (#21)

  8. Final cleanup — remove dead imports, fix lint warnings. Commit: refactor: finalize index.ts split (#21)

Phase 2: Add unit tests (steps 920)

Each step adds a dedicated test file for a module that currently has no unit tests. The goal is direct coverage of each module's exported functions in isolation — using vi.mock() to replace collaborator modules and vi.fn() for injected dependencies. Tests exercise edge cases and boundary conditions that are hard to reach through the end-to-end factory tests.

Every test file should use describe() blocks to group tests by exported function, and afterEach(() => { vi.restoreAllMocks(); }) to prevent cross-test pollution.

  1. tests/wildcard-matcher.test.ts — test compileWildcardPatternEntries, findCompiledWildcardMatch, findCompiledWildcardMatchForNames. No collaborators to mock (pure algorithm). Cover: empty patterns, exact match, glob * matching, last-match-wins precedence, multi-name lookup, no-match returns null. Commit: test: add unit tests for wildcard-matcher (#21)

  2. tests/common.test.ts — test toRecord, getNonEmptyString, isPermissionState, extractFrontmatter, parseSimpleYamlMap. No collaborators to mock (pure functions). Cover: non-object inputs to toRecord, whitespace-only strings, all three permission states, malformed frontmatter delimiters, empty YAML map, multi-line values. Commit: test: add unit tests for common (#21)

  3. tests/bash-filter.test.ts — test BashFilter.check. Mock: vi.mock("./wildcard-matcher.js") to verify BashFilter delegates pattern matching to the wildcard-matcher and applies the default fallback correctly. Cover: exact match, glob patterns, last-match-wins, default fallback for unmatched commands, empty command, whitespace normalization. Commit: test: add unit tests for bash-filter (#21)

  4. tests/yolo-mode.test.ts — test shouldAutoApprovePermissionState, canResolveAskPermissionRequest. No collaborators to mock (pure functions taking config/flags). Cover: yolo on/off × ask/allow/deny, subagent with no UI and yolo off, subagent with no UI and yolo on. Commit: test: add unit tests for yolo-mode (#21)

  5. tests/tool-input-preview.test.ts — test all exported formatters and getPermissionLogContext. Mock: vi.mock("./logging.js") so safeJsonStringify returns controlled output — verifies the module delegates serialization to its collaborator and handles the result. Cover: truncation at exact boundary, multi-line content, empty input, edit with multiple replacements, path-bearing vs non-path-bearing tools in getPermissionLogContext. Commit: test: add unit tests for tool-input-preview (#21)

  6. tests/external-directory.test.ts — test normalizePathForComparison, isPathWithinDirectory, isPathOutsideWorkingDirectory, getPathBearingToolPath, and format helpers. Mock: vi.mock("node:os", () => ({ homedir: vi.fn(() => "/mock/home") })) so tilde-expansion tests are deterministic and platform-independent. Cover: tilde expansion, relative paths, path-bearing vs non-path-bearing tools, empty strings, quoted paths, @-prefixed paths, format helpers with/without agent name. Commit: test: add unit tests for external-directory (#21)

  7. tests/permission-prompts.test.ts — test all format* exported functions. Mock: vi.mock("./tool-input-preview.js") so formatToolInputForPrompt returns controlled strings — verifies formatAskPrompt calls the collaborator with the right tool name and input and incorporates the preview into the prompt string. Cover: with/without agent name, MCP target, bash command with/without matched pattern, denial reason, skill path deny/ask. Commit: test: add unit tests for permission-prompts (#21)

  8. tests/active-agent.test.ts — test normalizeAgentName, getActiveAgentName, getActiveAgentNameFromSystemPrompt. Mock ExtensionContext as a plain object: { sessionManager: { getEntries: vi.fn(() => [...]) } }. Cover: whitespace-only name, null, tag variations in system prompt, missing tag, session entries with active_agent custom type, last-entry-wins when multiple entries exist, entry with name: null resets. Commit: test: add unit tests for active-agent (#21)

  9. tests/subagent-context.test.ts — test isSubagentExecutionContext, normalizeFilesystemPath. Mock ExtensionContext as a plain object: { sessionManager: { getSessionDir: vi.fn() } }. Use vi.stubEnv() / vi.unstubAllEnvs() to control SUBAGENT_ENV_HINT_KEYS without leaking across tests. Cover: env variable detection (each of the 3 hint keys), session dir within/outside subagent root, missing session dir, empty env values. Commit: test: add unit tests for subagent-context (#21)

  10. tests/tool-registry.test.ts — test checkRequestedToolRegistration, getToolNameFromValue. Mock: vi.mock("./common.js") to control getNonEmptyString / toRecord return values — verifies the registry delegates input parsing to its collaborators. Cover: registered tool, unregistered tool, missing tool name, event with input vs arguments, empty tool list. Commit: test: add unit tests for tool-registry (#21)

  11. tests/system-prompt-sanitizer.test.ts — test sanitizeAvailableToolsSection. No collaborators to mock (pure string transformation). Cover: removing denied tools, preserving allowed tools, multi-section prompts, missing Available tools section, tool guidance blocks for inactive tools, empty allowed-tools list. Commit: test: add unit tests for system-prompt-sanitizer (#21)

  12. tests/skill-prompt-sanitizer.test.ts — test resolveSkillPromptEntries, findSkillPathMatch. Mock PermissionManager as a plain object: { checkPermission: vi.fn() } — verifies the sanitizer delegates permission checks to the manager and uses the returned state to decide whether to strip, keep, or mark skill blocks. Cover: skill allow/deny/ask, path matching within/outside skill directories, multi-skill prompts, no skill blocks in prompt. Commit: test: add unit tests for skill-prompt-sanitizer (#21)

Risks and Mitigations

Risk Mitigation
Could this silently weaken a permission? No — pure refactor moves functions without changing logic. Every step verifies npm test passes, and the test suite covers tool/bash/mcp/skill/special/external-directory permission decisions.
Circular dependency between new modules Dependency flows one way (index → modules → shared types). No module imports from index.ts. tool-input-preview.ts is imported by permission-prompts.ts only.
Module-scope caching of getAgentDir() Extracted modules receive directory paths as parameters. getAgentDir() is called only in index.ts closures at invocation time, matching the existing pattern and the AGENTS.md rule. Note: index.ts itself still caches PI_AGENT_DIR at module scope — this is a pre-existing violation, not introduced by this change.
Import path breaks in tests Only two test files import from index.ts (tests/permission-system.test.ts, tests/session-start.test.ts), both importing only the default export piPermissionSystemExtension, which remains in index.ts.
Forwarded-permission closures depend on logger state forwarded-permissions/io.ts uses a setter-injected logger (setForwardedPermissionLogger). The setter must be called before any IO function that logs. This is wired up in the factory before refreshExtensionConfig().
Mocked unit tests could drift from real module contracts Unit tests verify each module's contract with its collaborators (correct arguments passed, return values used). Integration tests in permission-system.test.ts continue to verify end-to-end wiring with real collaborators. Both must pass — mocks catch contract violations early, integration tests catch wiring mistakes.
Over-mocking hides real bugs Mock only direct collaborators (one level deep). Never mock the module under test. If a test needs to mock more than 23 collaborators, that is a signal the module has too many responsibilities and should be split further.

Open Questions

  • Should PermissionReviewSource move to src/types.ts? Currently only used in index.ts. Defer until a second module needs it.
  • Should extractSkillNameFromInput move to src/skill-prompt-sanitizer.ts? It's closely related but currently only called in the input event handler. Defer to keep this change mechanical.
  • Should permission-manager.ts (941 lines) get its own unit test file? It has complex logic (MCP target resolution, policy merge, caching) that would benefit from direct tests with vi.mock("node:fs") to control config file reads. Deferred — it would be a large effort and the integration tests cover the main paths. Consider as a separate issue.
  • Should the module-scope PI_AGENT_DIR constants be moved inside the factory? This would fix the AGENTS.md rule violation but requires threading the values through more call sites. Consider as part of a future factory restructuring issue.
  • Should forwarded-permissions/io.ts replace setter injection with parameter injection? The current setForwardedPermissionLogger pattern creates temporal coupling. An alternative is to pass the logger as a parameter to each function that logs (matching the PermissionForwardingDeps pattern in polling.ts). This would make the module fully stateless and easier to test without calling a setter first. Consider for a future cleanup.