From 62a2026afcd8223bed178b0348fc84d9c03690f2 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sun, 2 Aug 2026 19:27:13 -0700 Subject: [PATCH] perf: memoize canonicalPath so scan loops resolve the project root once The containment gate re-canonicalized projectCwd for every target file in the per-edit and Stop loops. The hook runs as a fresh process per tool event, so a module-level memo makes it once-per-event work; the size cap only matters to long-lived importers like the test runner. Addresses Copilot review feedback on PR #471. Written with AI assistance (Claude Code). Co-Authored-By: Claude Code --- skill/scripts/hook-lib.mjs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/skill/scripts/hook-lib.mjs b/skill/scripts/hook-lib.mjs index 5967b4052..40645069d 100644 --- a/skill/scripts/hook-lib.mjs +++ b/skill/scripts/hook-lib.mjs @@ -1336,19 +1336,32 @@ function isInsideProject(filePath, projectCwd) { // not exist yet — the before-edit hook gates proposed Writes — canonicalize // the nearest existing ancestor and re-append the remainder, so a new file // under a symlinked root still compares equal to its canonical project. +// Memoized: the hook runs as a fresh process per tool event, so the cache +// amounts to once-per-event work — the scan loops re-check the same project +// root for every target file. The cap only matters to long-lived importers +// like the test runner. +const canonicalPathCache = new Map(); +const CANONICAL_PATH_CACHE_MAX = 1024; + function canonicalPath(p) { const resolved = path.resolve(p); + if (canonicalPathCache.has(resolved)) return canonicalPathCache.get(resolved); + let canonical = resolved; let dir = resolved; const tail = []; while (true) { try { - return tail.length ? path.join(fs.realpathSync(dir), ...tail) : fs.realpathSync(dir); + canonical = tail.length ? path.join(fs.realpathSync(dir), ...tail) : fs.realpathSync(dir); + break; } catch { /* keep climbing */ } const parent = path.dirname(dir); - if (parent === dir) return resolved; + if (parent === dir) break; tail.unshift(path.basename(dir)); dir = parent; } + if (canonicalPathCache.size >= CANONICAL_PATH_CACHE_MAX) canonicalPathCache.clear(); + canonicalPathCache.set(resolved, canonical); + return canonical; } // Containment gate shared by the before-edit hook and both scan passes. A