/* Shared utility for scanning TypeScript source files for import specifiers matching a pattern. Used by werkstatt.autonomy.validate, werkstatt.shared.validate, and forge.autonomy.validate to avoid code duplication (Fowler: Duplicated Code). Does not define what is forbidden or exempt — callers provide the pattern and filter. Does not scan test files — .test.ts and .spec.ts are always excluded. RFC-0868: extract shared directory-scanning utility from autonomy-validate and shared-validate. RFC-0940: move to @warpgogol/werkstatt-shared so forge can import without depending on @warpgogol/werkstatt-engine. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1099: export EXCLUDE_DIRS + EXCLUDE_SUFFIXES; extend EXCLUDE_DIRS with cache/build/test dirs (.turbo, .astro, .wrangler, .cache, .stryker-tmp, coverage, __tests__) so the werkstatt-typescript workspace model and all import-scan consumers share one canonical exclusion set. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ import { readdir, readFile } from "node:fs/promises"; import { join, relative } from "node:path"; export interface ImportViolation { file: string; specifier: string; } export const EXCLUDE_DIRS = new Set([ "node_modules", "tests", "tests-handoff", "dist", "templates", ".turbo", ".astro", ".wrangler", ".cache", ".stryker-tmp", "coverage", "__tests__", ]); export const EXCLUDE_SUFFIXES = [".test.ts", ".spec.ts"]; const IMPORT_PATTERN = /(?:^|\n)\s*(?:import\s+(?:type\s+)?[^;]+?\s+from\s+|require\s*\(\s*)["'`]([^"'`]+)["'`]/g; function shouldExcludeFile(fileName: string): boolean { return EXCLUDE_SUFFIXES.some((suffix) => fileName.endsWith(suffix)); } export async function scanDirectoryForImports( dir: string, workspaceRoot: string, specifierFilter: (specifier: string) => boolean, ): Promise<{ violations: ImportViolation[]; scannedFiles: number }> { let scannedFiles = 0; const violations: ImportViolation[] = []; const entries = await readdir(dir, { withFileTypes: true }).catch(() => []); for (const entry of entries) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { if (EXCLUDE_DIRS.has(entry.name)) continue; const subResult = await scanDirectoryForImports(fullPath, workspaceRoot, specifierFilter); scannedFiles += subResult.scannedFiles; violations.push(...subResult.violations); } else if (entry.name.endsWith(".ts") && !shouldExcludeFile(entry.name)) { scannedFiles++; const content = await readFile(fullPath, "utf8").catch(() => ""); let match: RegExpExecArray | null; const pattern = new RegExp(IMPORT_PATTERN.source, "g"); while ((match = pattern.exec(content)) !== null) { const specifier = match[1]!; if (specifierFilter(specifier)) { violations.push({ file: relative(workspaceRoot, fullPath), specifier, }); } } } } return { violations, scannedFiles }; }