Files
my-pi/pi-permission-system/docs/plans/archive/0110-split-external-directory.md
T

17 KiB

issue, issue_title
issue issue_title
110 refactor: split external-directory.ts into focused modules

Split external-directory.ts into focused modules

Problem Statement

src/external-directory.ts is ~760 lines bundling four unrelated concerns: global node_modules discovery, path classification utilities, prompt/reason message formatting, and a tree-sitter bash parser with AST walker. These concerns have no coupling to each other and should be independently testable, loadable, and changeable.

Issue #109 already extracted normalizePathForComparison and isPathWithinDirectory into src/path-utils.ts, but the remaining path-classification helpers, discovery logic, message formatters, and the entire tree-sitter parser still live in the monolithic file.

Goals

  • Split src/external-directory.ts into three new modules (path-classification helpers extend the existing src/path-utils.ts from #109).
  • Preserve all existing behavior — pure extraction refactoring, no logic changes.
  • Keep src/external-directory.ts as a barrel re-export so downstream imports can migrate incrementally.
  • Enable independent testing of each concern without pulling in heavy dependencies (tree-sitter, child_process).

Non-Goals

  • Changing any permission logic or policy semantics.
  • Re-deduplicating normalizePathForComparison / isPathWithinDirectory — already completed in #109.

Background

The file touches the external_directory permission surface. Current consumers of src/external-directory.ts:

Consumer Imports used
src/runtime.ts discoverGlobalNodeModulesRoot
src/handlers/gates/external-directory.ts formatExternalDirectoryAskPrompt, formatExternalDirectoryDenyReason, formatExternalDirectoryUserDeniedReason, getPathBearingToolPath, isPathOutsideWorkingDirectory, isPiInfrastructureRead (note: normalizePathForComparison already imports from path-utils)
src/handlers/gates/bash-external-directory.ts extractExternalPathsFromBashCommand, formatBashExternalDirectoryAskPrompt, formatBashExternalDirectoryDenyReason, formatExternalDirectoryHardStopHint
src/handlers/gates/tool.ts PATH_BEARING_TOOLS
src/handlers/gates/skill-read.ts normalizePathForComparison (already imports from path-utils)

Test files:

  • tests/external-directory.test.ts — tests path utils, discovery, messages, and constants.
  • tests/bash-external-directory.test.ts — tests tree-sitter path extraction.
  • tests/handlers/gates/external-directory.test.ts — gate-level integration.
  • tests/handlers/gates/bash-external-directory.test.ts — gate-level integration.

Design Overview

Split into four new modules, each owning one concern:

src/node-modules-discovery.ts

  • walkUpToNodeModules() (internal)
  • discoverGlobalNodeModulesViaSubprocess() (internal)
  • discoverGlobalNodeModulesRoot() (exported)

Dependencies: node:child_process, node:fs, node:path, node:url.

src/path-utils.ts (extend existing)

Issue #109 already created this file with normalizePathForComparison and isPathWithinDirectory. Add the remaining path-classification helpers:

  • isPathOutsideWorkingDirectory() (moved from external-directory.ts)
  • getPathBearingToolPath() (moved from external-directory.ts)
  • isPiInfrastructureRead() (moved from external-directory.ts)
  • isSafeSystemPath() (moved from external-directory.ts)
  • SAFE_SYSTEM_PATHS (moved from external-directory.ts)
  • PATH_BEARING_TOOLS (moved from external-directory.ts)
  • READ_ONLY_PATH_BEARING_TOOLS (moved from external-directory.ts)

Dependencies: adds node:path (for join), ./common (for getNonEmptyString, toRecord) to existing node:os, node:path.

src/external-directory-messages.ts

  • formatExternalDirectoryHardStopHint() (exported)
  • formatExternalDirectoryAskPrompt() (exported)
  • formatExternalDirectoryDenyReason() (exported)
  • formatExternalDirectoryUserDeniedReason() (exported)
  • formatBashExternalDirectoryAskPrompt() (exported)
  • formatBashExternalDirectoryDenyReason() (exported)

Dependencies: none (pure string builders).

src/bash-path-extractor.ts

  • All tree-sitter types (TSNode, TSParser)
  • Parser lifecycle (initParser, getParser, resetParserForTesting)
  • AST walker (resolveNodeText, collectPathCandidateTokens, collectPatternCommandTokens)
  • Pattern-first command config (PATTERN_FIRST_COMMANDS, SKIP_SUBTREE_TYPES, etc.)
  • Token classification (classifyTokenAsPathCandidate, URL_PATTERN, REGEX_METACHAR_PATTERN)
  • extractExternalPathsFromBashCommand() (exported)

Dependencies: web-tree-sitter, tree-sitter-bash, node:module, node:path. Imports normalizePathForComparison, isPathOutsideWorkingDirectory from ./path-utils.

src/external-directory.ts (barrel)

Becomes a thin barrel that re-exports everything from the three new modules plus ./path-utils. All existing consumer imports continue to work unchanged. Downstream consumers can optionally migrate to direct imports in follow-up work.

Module-Level Changes

New files

File Contents
src/node-modules-discovery.ts Global node_modules resolution
src/external-directory-messages.ts 6 format* pure string builders
src/bash-path-extractor.ts Tree-sitter parser, AST walker, extractExternalPathsFromBashCommand

Changed files

File Change
src/path-utils.ts Add remaining path-classification helpers (isPathOutsideWorkingDirectory, getPathBearingToolPath, isPiInfrastructureRead, isSafeSystemPath, SAFE_SYSTEM_PATHS, PATH_BEARING_TOOLS, READ_ONLY_PATH_BEARING_TOOLS)
src/external-directory.ts Replace implementation with barrel re-exports from three new modules plus path-utils

New test files

File Contents
tests/node-modules-discovery.test.ts Extracted from tests/external-directory.test.ts — discovery tests
tests/external-directory-messages.test.ts Extracted from tests/external-directory.test.ts — message format tests

Changed test files

File Change
tests/path-utils.test.ts Add tests for newly moved helpers (isPathOutsideWorkingDirectory, getPathBearingToolPath, isPiInfrastructureRead, isSafeSystemPath, constants)
tests/external-directory.test.ts Reduced to a thin smoke test verifying the barrel re-exports work, or deleted entirely if all tests migrate
tests/bash-external-directory.test.ts Update import from ../src/external-directory to ../src/bash-path-extractor (or keep barrel import)

No changes needed

File Reason
tests/handlers/gates/external-directory.test.ts Imports from gate module, not from external-directory.ts directly
tests/handlers/gates/bash-external-directory.test.ts Same — imports from gate module
src/handlers/gates/* All import from ../../external-directory barrel, which continues to work
src/runtime.ts Imports discoverGlobalNodeModulesRoot from barrel
schemas/permissions.schema.json No schema changes
config/config.example.json No config changes

Architecture docs

File Action
docs/architecture/target-architecture.md Check if it references external-directory.ts and update to reflect the split

Test Impact Analysis

  1. New unit tests enabled by extraction:

    • tests/node-modules-discovery.test.ts can mock child_process and fs without affecting path tests.
    • tests/path-utils.test.ts gains path-classification tests that run without tree-sitter or subprocess mocks (file already exists from #109).
    • tests/external-directory-messages.test.ts needs zero mocks — pure string assertions.
  2. Existing tests that become redundant:

    • tests/external-directory.test.ts currently tests all four concerns in one file with shared mocks. After extraction, its individual test blocks migrate to the new/extended test files. The barrel file itself needs only a re-export smoke test (or can be deleted).
  3. Existing tests that must stay as-is:

    • tests/bash-external-directory.test.ts — exercises extractExternalPathsFromBashCommand end-to-end with real tree-sitter.
    • tests/handlers/gates/*.test.ts — gate-level integration, unaffected by internal splits.

TDD Order

Step 1 — Extract node-modules-discovery.ts with tests

  1. Create src/node-modules-discovery.ts with the discovery functions.
  2. Create tests/node-modules-discovery.test.ts by extracting discoverGlobalNodeModulesRoot tests from tests/external-directory.test.ts.
  3. Update src/external-directory.ts to import-and-re-export from ./node-modules-discovery.
  4. Remove the original implementation from external-directory.ts.
  5. Verify all tests pass.

Commit: refactor: extract node-modules-discovery module (#110)

Step 2 — Move remaining path helpers into path-utils.ts

  1. Move isPathOutsideWorkingDirectory, getPathBearingToolPath, isPiInfrastructureRead, isSafeSystemPath, SAFE_SYSTEM_PATHS, PATH_BEARING_TOOLS, READ_ONLY_PATH_BEARING_TOOLS from external-directory.ts into src/path-utils.ts.
  2. Move corresponding tests from tests/external-directory.test.ts into tests/path-utils.test.ts.
  3. Update src/external-directory.ts to re-export the moved symbols from ./path-utils.
  4. Verify all tests pass.

Commit: refactor: move path-classification helpers into path-utils (#110)

Step 3 — Extract external-directory-messages.ts with tests

  1. Create src/external-directory-messages.ts with the 6 format* functions.
  2. Create tests/external-directory-messages.test.ts by extracting message tests from tests/external-directory.test.ts.
  3. Update src/external-directory.ts to import-and-re-export from ./external-directory-messages.
  4. Remove the original implementation from external-directory.ts.
  5. Verify all tests pass.

Commit: refactor: extract external-directory-messages module (#110)

Step 4 — Extract bash-path-extractor.ts

  1. Create src/bash-path-extractor.ts with tree-sitter parser, AST walker, and extractExternalPathsFromBashCommand.
  2. Update tests/bash-external-directory.test.ts imports if needed (or keep barrel).
  3. Update src/external-directory.ts to import-and-re-export from ./bash-path-extractor.
  4. Remove the original implementation from external-directory.ts.
  5. Verify all tests pass.

Commit: refactor: extract bash-path-extractor module (#110)

Step 5 — Clean up barrel and original test file

  1. Verify src/external-directory.ts is now a pure barrel of re-exports.
  2. Reduce tests/external-directory.test.ts to a smoke test (or delete if fully migrated).
  3. Run full test suite.

Commit: refactor: finalize external-directory barrel re-exports (#110)

Step 6 — Update architecture docs

  1. Update docs/architecture/target-architecture.md if it references the monolithic external-directory.ts.

Commit: docs: update architecture for external-directory split (#110)

Risks and Mitigations

Risk Mitigation
Could this silently weaken a permission? No — pure extraction refactoring; no logic changes; barrel preserves all exports.
Circular dependency between bash-path-extractor and path-utils bash-path-extractor imports from path-utils; no reverse dependency. Barrel re-exports both without creating a cycle.
Tree-sitter WASM loading breaks after move bash-path-extractor.ts preserves the same createRequire(import.meta.url) pattern — import.meta.url resolves to the new file's location, but WASM resolution uses require.resolve which walks node_modules, so it works from any file in src/.
Test mocks leak across modules Each new test file has its own vi.mock() scope. Discovery tests mock child_process/fs; path tests mock os; message tests need no mocks.

Open Questions

  • Should downstream consumers (src/handlers/gates/*, src/runtime.ts) be updated to import directly from the new modules in this PR, or deferred to a follow-up? Recommendation: defer — the barrel ensures backward compatibility and keeps the PR focused on extraction.