@vibgrate/cli 1.0.14 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,7 +23,7 @@ var Semaphore = class {
23
23
  this.available--;
24
24
  return Promise.resolve();
25
25
  }
26
- return new Promise((resolve6) => this.queue.push(resolve6));
26
+ return new Promise((resolve7) => this.queue.push(resolve7));
27
27
  }
28
28
  release() {
29
29
  const next = this.queue.shift();
@@ -50,6 +50,140 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
50
50
  "packages",
51
51
  "TestResults"
52
52
  ]);
53
+ var TEXT_CACHE_MAX_BYTES = 1048576;
54
+ var FileCache = class _FileCache {
55
+ /** Directory walk results keyed by rootDir */
56
+ walkCache = /* @__PURE__ */ new Map();
57
+ /** File content keyed by absolute path (only files ≤ TEXT_CACHE_MAX_BYTES) */
58
+ textCache = /* @__PURE__ */ new Map();
59
+ /** Parsed JSON keyed by absolute path */
60
+ jsonCache = /* @__PURE__ */ new Map();
61
+ /** pathExists keyed by absolute path */
62
+ existsCache = /* @__PURE__ */ new Map();
63
+ // ── Directory walking ──
64
+ /**
65
+ * Walk the directory tree from `rootDir` once, skipping SKIP_DIRS plus
66
+ * common framework output dirs (.nuxt, .output, .svelte-kit).
67
+ *
68
+ * The result is memoised so every scanner filters the same array.
69
+ * Consumers that need additional filtering (e.g. SOURCE_EXTENSIONS,
70
+ * SKIP_EXTENSIONS) do so on the returned entries — no separate walk.
71
+ */
72
+ walkDir(rootDir) {
73
+ const cached = this.walkCache.get(rootDir);
74
+ if (cached) return cached;
75
+ const promise = this._doWalk(rootDir);
76
+ this.walkCache.set(rootDir, promise);
77
+ return promise;
78
+ }
79
+ /** Additional dirs skipped only by the cached walk (framework outputs) */
80
+ static EXTRA_SKIP = /* @__PURE__ */ new Set([".nuxt", ".output", ".svelte-kit"]);
81
+ async _doWalk(rootDir) {
82
+ const results = [];
83
+ const maxConcurrentReads = Math.max(8, Math.min(64, os.availableParallelism() * 4));
84
+ const sem = new Semaphore(maxConcurrentReads);
85
+ const extraSkip = _FileCache.EXTRA_SKIP;
86
+ async function walk(dir) {
87
+ let entries;
88
+ try {
89
+ entries = await fs.readdir(dir, { withFileTypes: true });
90
+ } catch {
91
+ return;
92
+ }
93
+ const subWalks = [];
94
+ for (const e of entries) {
95
+ const absPath = path.join(dir, e.name);
96
+ const relPath = path.relative(rootDir, absPath);
97
+ if (e.isDirectory()) {
98
+ if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
99
+ results.push({ absPath, relPath, name: e.name, isFile: false, isDirectory: true });
100
+ subWalks.push(sem.run(() => walk(absPath)));
101
+ } else if (e.isFile()) {
102
+ results.push({ absPath, relPath, name: e.name, isFile: true, isDirectory: false });
103
+ }
104
+ }
105
+ await Promise.all(subWalks);
106
+ }
107
+ await sem.run(() => walk(rootDir));
108
+ return results;
109
+ }
110
+ /**
111
+ * Find files matching a predicate from the cached walk.
112
+ * Returns absolute paths (same contract as the standalone `findFiles`).
113
+ */
114
+ async findFiles(rootDir, predicate) {
115
+ const entries = await this.walkDir(rootDir);
116
+ return entries.filter((e) => e.isFile && predicate(e.name)).map((e) => e.absPath);
117
+ }
118
+ async findPackageJsonFiles(rootDir) {
119
+ return this.findFiles(rootDir, (name) => name === "package.json");
120
+ }
121
+ async findCsprojFiles(rootDir) {
122
+ return this.findFiles(rootDir, (name) => name.endsWith(".csproj"));
123
+ }
124
+ async findSolutionFiles(rootDir) {
125
+ return this.findFiles(rootDir, (name) => name.endsWith(".sln"));
126
+ }
127
+ // ── File content reading ──
128
+ /**
129
+ * Read a text file. Files ≤ 1 MB are cached so subsequent calls from
130
+ * different scanners return the same string. Files > 1 MB (lockfiles,
131
+ * large generated files) are read directly and never retained.
132
+ */
133
+ readTextFile(filePath) {
134
+ const abs = path.resolve(filePath);
135
+ const cached = this.textCache.get(abs);
136
+ if (cached) return cached;
137
+ const promise = fs.readFile(abs, "utf8").then((content) => {
138
+ if (content.length > TEXT_CACHE_MAX_BYTES) {
139
+ this.textCache.delete(abs);
140
+ }
141
+ return content;
142
+ });
143
+ this.textCache.set(abs, promise);
144
+ return promise;
145
+ }
146
+ /**
147
+ * Read and parse a JSON file. The parsed object is cached; the raw
148
+ * text is evicted immediately so we never hold both representations.
149
+ */
150
+ readJsonFile(filePath) {
151
+ const abs = path.resolve(filePath);
152
+ const cached = this.jsonCache.get(abs);
153
+ if (cached) return cached;
154
+ const promise = this.readTextFile(abs).then((txt) => {
155
+ this.textCache.delete(abs);
156
+ return JSON.parse(txt);
157
+ });
158
+ this.jsonCache.set(abs, promise);
159
+ return promise;
160
+ }
161
+ // ── Existence checks ──
162
+ pathExists(p) {
163
+ const abs = path.resolve(p);
164
+ const cached = this.existsCache.get(abs);
165
+ if (cached) return cached;
166
+ const promise = fs.access(abs).then(() => true, () => false);
167
+ this.existsCache.set(abs, promise);
168
+ return promise;
169
+ }
170
+ // ── Lifecycle ──
171
+ /** Release all cached data. Call after the scan completes. */
172
+ clear() {
173
+ this.walkCache.clear();
174
+ this.textCache.clear();
175
+ this.jsonCache.clear();
176
+ this.existsCache.clear();
177
+ }
178
+ /** Number of file content entries currently held */
179
+ get textCacheSize() {
180
+ return this.textCache.size;
181
+ }
182
+ /** Number of parsed JSON entries currently held */
183
+ get jsonCacheSize() {
184
+ return this.jsonCache.size;
185
+ }
186
+ };
53
187
  async function findFiles(rootDir, predicate) {
54
188
  const results = [];
55
189
  const maxConcurrentReads = Math.max(8, Math.min(64, os.availableParallelism() * 4));
@@ -187,12 +321,12 @@ function eolScore(projects) {
187
321
  }
188
322
  function computeDriftScore(projects) {
189
323
  const rs = runtimeScore(projects);
190
- const fs5 = frameworkScore(projects);
324
+ const fs6 = frameworkScore(projects);
191
325
  const ds = dependencyScore(projects);
192
326
  const es = eolScore(projects);
193
327
  const components = [
194
328
  { score: rs, weight: 0.25 },
195
- { score: fs5, weight: 0.25 },
329
+ { score: fs6, weight: 0.25 },
196
330
  { score: ds, weight: 0.3 },
197
331
  { score: es, weight: 0.2 }
198
332
  ];
@@ -203,7 +337,7 @@ function computeDriftScore(projects) {
203
337
  riskLevel: "low",
204
338
  components: {
205
339
  runtimeScore: Math.round(rs ?? 100),
206
- frameworkScore: Math.round(fs5 ?? 100),
340
+ frameworkScore: Math.round(fs6 ?? 100),
207
341
  dependencyScore: Math.round(ds ?? 100),
208
342
  eolScore: Math.round(es ?? 100)
209
343
  }
@@ -221,7 +355,7 @@ function computeDriftScore(projects) {
221
355
  else riskLevel = "high";
222
356
  const measured = [];
223
357
  if (rs !== null) measured.push("runtime");
224
- if (fs5 !== null) measured.push("framework");
358
+ if (fs6 !== null) measured.push("framework");
225
359
  if (ds !== null) measured.push("dependency");
226
360
  if (es !== null) measured.push("eol");
227
361
  return {
@@ -229,7 +363,7 @@ function computeDriftScore(projects) {
229
363
  riskLevel,
230
364
  components: {
231
365
  runtimeScore: Math.round(rs ?? 100),
232
- frameworkScore: Math.round(fs5 ?? 100),
366
+ frameworkScore: Math.round(fs6 ?? 100),
233
367
  dependencyScore: Math.round(ds ?? 100),
234
368
  eolScore: Math.round(es ?? 100)
235
369
  },
@@ -585,6 +719,122 @@ function formatExtended(ext) {
585
719
  lines.push("");
586
720
  }
587
721
  }
722
+ if (ext.architecture) {
723
+ lines.push(...formatArchitectureDiagram(ext.architecture));
724
+ }
725
+ return lines;
726
+ }
727
+ var LAYER_LABELS = {
728
+ "presentation": "Presentation",
729
+ "routing": "Routing",
730
+ "middleware": "Middleware",
731
+ "services": "Services",
732
+ "domain": "Domain",
733
+ "data-access": "Data Access",
734
+ "infrastructure": "Infrastructure",
735
+ "config": "Config",
736
+ "shared": "Shared",
737
+ "testing": "Testing"
738
+ };
739
+ var LAYER_ICONS = {
740
+ "presentation": "\u{1F5A5}",
741
+ "routing": "\u{1F500}",
742
+ "middleware": "\u{1F517}",
743
+ "services": "\u2699",
744
+ "domain": "\u{1F48E}",
745
+ "data-access": "\u{1F5C4}",
746
+ "infrastructure": "\u{1F3D7}",
747
+ "config": "\u2699",
748
+ "shared": "\u{1F4E6}",
749
+ "testing": "\u{1F9EA}"
750
+ };
751
+ function formatArchitectureDiagram(arch) {
752
+ const lines = [];
753
+ lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
754
+ lines.push(chalk.bold.cyan("\u2551 Architecture Layers \u2551"));
755
+ lines.push(chalk.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
756
+ lines.push("");
757
+ const archetypeDisplay = arch.archetype === "unknown" ? "Unknown" : arch.archetype;
758
+ const confPct = Math.round(arch.archetypeConfidence * 100);
759
+ lines.push(chalk.bold(" Archetype: ") + chalk.cyan.bold(archetypeDisplay) + chalk.dim(` (${confPct}% confidence)`));
760
+ lines.push(chalk.dim(` ${arch.totalClassified} files classified \xB7 ${arch.unclassified} unclassified`));
761
+ lines.push("");
762
+ const boxWidth = 44;
763
+ const visibleLayers = arch.layers.filter((l) => l.fileCount > 0 || l.techStack.length > 0 || l.services.length > 0);
764
+ if (visibleLayers.length === 0) {
765
+ lines.push(chalk.dim(" No layers detected"));
766
+ lines.push("");
767
+ return lines;
768
+ }
769
+ for (let i = 0; i < visibleLayers.length; i++) {
770
+ const layer = visibleLayers[i];
771
+ const icon = LAYER_ICONS[layer.layer] ?? "\xB7";
772
+ const label = LAYER_LABELS[layer.layer] ?? layer.layer;
773
+ const scoreColor = layer.driftScore >= 70 ? chalk.green : layer.driftScore >= 40 ? chalk.yellow : chalk.red;
774
+ const riskBadgeStr = layer.riskLevel === "low" ? chalk.bgGreen.black(" LOW ") : layer.riskLevel === "moderate" ? chalk.bgYellow.black(" MOD ") : chalk.bgRed.white(" HIGH ");
775
+ if (i === 0) {
776
+ lines.push(chalk.cyan(` \u250C${"\u2500".repeat(boxWidth)}\u2510`));
777
+ }
778
+ const nameStr = `${icon} ${label}`;
779
+ const scoreStr = `${layer.driftScore}/100`;
780
+ const fileSuffix = `${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""}`;
781
+ const leftContent = ` ${nameStr}`;
782
+ const rightContent = `${fileSuffix} ${scoreStr} `;
783
+ const leftLen = nameStr.length + 2;
784
+ const rightLen = rightContent.length;
785
+ const padLen = Math.max(1, boxWidth - leftLen - rightLen);
786
+ lines.push(
787
+ chalk.cyan(" \u2502") + ` ${icon} ${chalk.bold(label)}` + " ".repeat(padLen) + chalk.dim(fileSuffix) + " " + scoreColor.bold(scoreStr) + " " + chalk.cyan("\u2502")
788
+ );
789
+ const barWidth = boxWidth - 8;
790
+ const filled = Math.round(layer.driftScore / 100 * barWidth);
791
+ const empty = barWidth - filled;
792
+ lines.push(
793
+ chalk.cyan(" \u2502") + " " + scoreColor("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty)) + " " + chalk.cyan("\u2502")
794
+ );
795
+ if (layer.techStack.length > 0) {
796
+ const techNames = layer.techStack.slice(0, 6).map((t) => t.name);
797
+ const moreCount = layer.techStack.length > 6 ? ` +${layer.techStack.length - 6}` : "";
798
+ const techLine = `Tech: ${techNames.join(", ")}${moreCount}`;
799
+ const truncated = techLine.length > boxWidth - 6 ? techLine.slice(0, boxWidth - 9) + "..." : techLine;
800
+ const techPad = Math.max(0, boxWidth - truncated.length - 4);
801
+ lines.push(
802
+ chalk.cyan(" \u2502") + " " + chalk.dim(truncated) + " ".repeat(techPad) + chalk.cyan("\u2502")
803
+ );
804
+ }
805
+ if (layer.services.length > 0) {
806
+ const svcNames = layer.services.slice(0, 5).map((s) => s.name);
807
+ const moreCount = layer.services.length > 5 ? ` +${layer.services.length - 5}` : "";
808
+ const svcLine = `Services: ${svcNames.join(", ")}${moreCount}`;
809
+ const truncated = svcLine.length > boxWidth - 6 ? svcLine.slice(0, boxWidth - 9) + "..." : svcLine;
810
+ const svcPad = Math.max(0, boxWidth - truncated.length - 4);
811
+ lines.push(
812
+ chalk.cyan(" \u2502") + " " + chalk.dim(truncated) + " ".repeat(svcPad) + chalk.cyan("\u2502")
813
+ );
814
+ }
815
+ const driftPkgs = layer.packages.filter((p) => p.majorsBehind !== null && p.majorsBehind > 0);
816
+ if (driftPkgs.length > 0) {
817
+ const worst = driftPkgs.sort((a, b) => (b.majorsBehind ?? 0) - (a.majorsBehind ?? 0));
818
+ const shown = worst.slice(0, 3);
819
+ const pkgStrs = shown.map((p) => {
820
+ const color = (p.majorsBehind ?? 0) >= 2 ? chalk.red : chalk.yellow;
821
+ return color(`${p.name} -${p.majorsBehind}`);
822
+ });
823
+ const moreCount = worst.length > 3 ? chalk.dim(` +${worst.length - 3}`) : "";
824
+ const pkgLine = pkgStrs.join(chalk.dim(", ")) + moreCount;
825
+ const roughLen = shown.map((p) => `${p.name} -${p.majorsBehind}`).join(", ").length + (worst.length > 3 ? ` +${worst.length - 3}`.length : 0);
826
+ const pkgPad = Math.max(0, boxWidth - roughLen - 4);
827
+ lines.push(
828
+ chalk.cyan(" \u2502") + " " + pkgLine + " ".repeat(pkgPad) + chalk.cyan("\u2502")
829
+ );
830
+ }
831
+ if (i < visibleLayers.length - 1) {
832
+ lines.push(chalk.cyan(` \u251C${"\u2500".repeat(boxWidth)}\u2524`));
833
+ } else {
834
+ lines.push(chalk.cyan(` \u2514${"\u2500".repeat(boxWidth)}\u2518`));
835
+ }
836
+ }
837
+ lines.push("");
588
838
  return lines;
589
839
  }
590
840
  function generatePriorityActions(artifact) {
@@ -1050,7 +1300,7 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
1050
1300
  });
1051
1301
 
1052
1302
  // src/commands/scan.ts
1053
- import * as path14 from "path";
1303
+ import * as path15 from "path";
1054
1304
  import { Command as Command3 } from "commander";
1055
1305
  import chalk5 from "chalk";
1056
1306
 
@@ -1070,7 +1320,7 @@ function maxStable(versions) {
1070
1320
  return stable.sort(semver.rcompare)[0] ?? null;
1071
1321
  }
1072
1322
  async function npmViewJson(args, cwd) {
1073
- return new Promise((resolve6, reject) => {
1323
+ return new Promise((resolve7, reject) => {
1074
1324
  const child = spawn("npm", ["view", ...args, "--json"], {
1075
1325
  cwd,
1076
1326
  stdio: ["ignore", "pipe", "pipe"]
@@ -1087,13 +1337,13 @@ async function npmViewJson(args, cwd) {
1087
1337
  }
1088
1338
  const trimmed = out.trim();
1089
1339
  if (!trimmed) {
1090
- resolve6(null);
1340
+ resolve7(null);
1091
1341
  return;
1092
1342
  }
1093
1343
  try {
1094
- resolve6(JSON.parse(trimmed));
1344
+ resolve7(JSON.parse(trimmed));
1095
1345
  } catch {
1096
- resolve6(trimmed.replace(/^"|"$/g, ""));
1346
+ resolve7(trimmed.replace(/^"|"$/g, ""));
1097
1347
  }
1098
1348
  });
1099
1349
  });
@@ -1229,12 +1479,12 @@ var KNOWN_FRAMEWORKS = {
1229
1479
  "storybook": "Storybook",
1230
1480
  "@storybook/react": "Storybook"
1231
1481
  };
1232
- async function scanNodeProjects(rootDir, npmCache) {
1233
- const packageJsonFiles = await findPackageJsonFiles(rootDir);
1482
+ async function scanNodeProjects(rootDir, npmCache, cache) {
1483
+ const packageJsonFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
1234
1484
  const results = [];
1235
1485
  for (const pjPath of packageJsonFiles) {
1236
1486
  try {
1237
- const scan = await scanOnePackageJson(pjPath, rootDir, npmCache);
1487
+ const scan = await scanOnePackageJson(pjPath, rootDir, npmCache, cache);
1238
1488
  results.push(scan);
1239
1489
  } catch (e) {
1240
1490
  const msg = e instanceof Error ? e.message : String(e);
@@ -1243,8 +1493,8 @@ async function scanNodeProjects(rootDir, npmCache) {
1243
1493
  }
1244
1494
  return results;
1245
1495
  }
1246
- async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
1247
- const pj = await readJsonFile(packageJsonPath);
1496
+ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
1497
+ const pj = cache ? await cache.readJsonFile(packageJsonPath) : await readJsonFile(packageJsonPath);
1248
1498
  const absProjectPath = path4.dirname(packageJsonPath);
1249
1499
  const projectPath = path4.relative(rootDir, absProjectPath) || ".";
1250
1500
  const nodeEngine = pj.engines?.node ?? void 0;
@@ -1571,13 +1821,13 @@ function parseCsproj(xml, filePath) {
1571
1821
  projectName: path5.basename(filePath, ".csproj")
1572
1822
  };
1573
1823
  }
1574
- async function scanDotnetProjects(rootDir) {
1575
- const csprojFiles = await findCsprojFiles(rootDir);
1576
- const slnFiles = await findSolutionFiles(rootDir);
1824
+ async function scanDotnetProjects(rootDir, cache) {
1825
+ const csprojFiles = cache ? await cache.findCsprojFiles(rootDir) : await findCsprojFiles(rootDir);
1826
+ const slnFiles = cache ? await cache.findSolutionFiles(rootDir) : await findSolutionFiles(rootDir);
1577
1827
  const slnCsprojPaths = /* @__PURE__ */ new Set();
1578
1828
  for (const slnPath of slnFiles) {
1579
1829
  try {
1580
- const slnContent = await readTextFile(slnPath);
1830
+ const slnContent = cache ? await cache.readTextFile(slnPath) : await readTextFile(slnPath);
1581
1831
  const slnDir = path5.dirname(slnPath);
1582
1832
  const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
1583
1833
  let match;
@@ -1594,7 +1844,7 @@ async function scanDotnetProjects(rootDir) {
1594
1844
  const results = [];
1595
1845
  for (const csprojPath of allCsprojFiles) {
1596
1846
  try {
1597
- const scan = await scanOneCsproj(csprojPath, rootDir);
1847
+ const scan = await scanOneCsproj(csprojPath, rootDir, cache);
1598
1848
  results.push(scan);
1599
1849
  } catch (e) {
1600
1850
  const msg = e instanceof Error ? e.message : String(e);
@@ -1603,8 +1853,8 @@ async function scanDotnetProjects(rootDir) {
1603
1853
  }
1604
1854
  return results;
1605
1855
  }
1606
- async function scanOneCsproj(csprojPath, rootDir) {
1607
- const xml = await readTextFile(csprojPath);
1856
+ async function scanOneCsproj(csprojPath, rootDir, cache) {
1857
+ const xml = cache ? await cache.readTextFile(csprojPath) : await readTextFile(csprojPath);
1608
1858
  const data = parseCsproj(xml, csprojPath);
1609
1859
  const primaryTfm = data.targetFrameworks[0];
1610
1860
  let runtimeMajorsBehind;
@@ -2212,7 +2462,7 @@ var OS_PATTERNS = [
2212
2462
  { pattern: /\bbash\b|#!\/bin\/bash/i, label: "bash-scripts" },
2213
2463
  { pattern: /\\\\/g, label: "backslash-paths" }
2214
2464
  ];
2215
- async function scanPlatformMatrix(rootDir) {
2465
+ async function scanPlatformMatrix(rootDir, cache) {
2216
2466
  const result = {
2217
2467
  dotnetTargetFrameworks: [],
2218
2468
  nativeModules: [],
@@ -2220,12 +2470,12 @@ async function scanPlatformMatrix(rootDir) {
2220
2470
  dockerBaseImages: [],
2221
2471
  nodeVersionFiles: []
2222
2472
  };
2223
- const pkgFiles = await findPackageJsonFiles(rootDir);
2473
+ const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
2224
2474
  const allDeps = /* @__PURE__ */ new Set();
2225
2475
  const osAssumptions = /* @__PURE__ */ new Set();
2226
2476
  for (const pjPath of pkgFiles) {
2227
2477
  try {
2228
- const pj = await readJsonFile(pjPath);
2478
+ const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
2229
2479
  if (pj.engines?.node && !result.nodeEngines) result.nodeEngines = pj.engines.node;
2230
2480
  if (pj.engines?.npm && !result.npmEngines) result.npmEngines = pj.engines.npm;
2231
2481
  if (pj.engines?.pnpm && !result.pnpmEngines) {
@@ -2261,11 +2511,11 @@ async function scanPlatformMatrix(rootDir) {
2261
2511
  }
2262
2512
  result.nativeModules.sort();
2263
2513
  result.osAssumptions = [...osAssumptions].sort();
2264
- const csprojFiles = await findFiles(rootDir, (name) => name.endsWith(".csproj"));
2514
+ const csprojFiles = cache ? await cache.findCsprojFiles(rootDir) : await findFiles(rootDir, (name) => name.endsWith(".csproj"));
2265
2515
  const tfms = /* @__PURE__ */ new Set();
2266
2516
  for (const csprojPath of csprojFiles) {
2267
2517
  try {
2268
- const xml = await readTextFile(csprojPath);
2518
+ const xml = cache ? await cache.readTextFile(csprojPath) : await readTextFile(csprojPath);
2269
2519
  const tfMatch = xml.match(/<TargetFramework>(.*?)<\/TargetFramework>/);
2270
2520
  if (tfMatch?.[1]) tfms.add(tfMatch[1]);
2271
2521
  const tfsMatch = xml.match(/<TargetFrameworks>(.*?)<\/TargetFrameworks>/);
@@ -2278,14 +2528,17 @@ async function scanPlatformMatrix(rootDir) {
2278
2528
  }
2279
2529
  }
2280
2530
  result.dotnetTargetFrameworks = [...tfms].sort();
2281
- const dockerfiles = await findFiles(
2531
+ const dockerfiles = cache ? await cache.findFiles(
2532
+ rootDir,
2533
+ (name) => name === "Dockerfile" || name.startsWith("Dockerfile.")
2534
+ ) : await findFiles(
2282
2535
  rootDir,
2283
2536
  (name) => name === "Dockerfile" || name.startsWith("Dockerfile.")
2284
2537
  );
2285
2538
  const baseImages = /* @__PURE__ */ new Set();
2286
2539
  for (const df of dockerfiles) {
2287
2540
  try {
2288
- const content = await readTextFile(df);
2541
+ const content = cache ? await cache.readTextFile(df) : await readTextFile(df);
2289
2542
  for (const line of content.split("\n")) {
2290
2543
  const trimmed = line.trim();
2291
2544
  if (/^FROM\s+/i.test(trimmed)) {
@@ -2303,7 +2556,8 @@ async function scanPlatformMatrix(rootDir) {
2303
2556
  }
2304
2557
  result.dockerBaseImages = [...baseImages].sort();
2305
2558
  for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
2306
- if (await pathExists(path8.join(rootDir, file))) {
2559
+ const exists = cache ? await cache.pathExists(path8.join(rootDir, file)) : await pathExists(path8.join(rootDir, file));
2560
+ if (exists) {
2307
2561
  result.nodeVersionFiles.push(file);
2308
2562
  }
2309
2563
  }
@@ -2429,7 +2683,7 @@ function parseYarnLock(content) {
2429
2683
  }
2430
2684
  return entries;
2431
2685
  }
2432
- async function scanDependencyGraph(rootDir) {
2686
+ async function scanDependencyGraph(rootDir, cache) {
2433
2687
  const result = {
2434
2688
  lockfileType: null,
2435
2689
  totalUnique: 0,
@@ -2441,17 +2695,19 @@ async function scanDependencyGraph(rootDir) {
2441
2695
  const pnpmLock = path9.join(rootDir, "pnpm-lock.yaml");
2442
2696
  const npmLock = path9.join(rootDir, "package-lock.json");
2443
2697
  const yarnLock = path9.join(rootDir, "yarn.lock");
2444
- if (await pathExists(pnpmLock)) {
2698
+ const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
2699
+ const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
2700
+ if (await _pathExists(pnpmLock)) {
2445
2701
  result.lockfileType = "pnpm";
2446
- const content = await readTextFile(pnpmLock);
2702
+ const content = await _readTextFile(pnpmLock);
2447
2703
  entries = parsePnpmLock(content);
2448
- } else if (await pathExists(npmLock)) {
2704
+ } else if (await _pathExists(npmLock)) {
2449
2705
  result.lockfileType = "npm";
2450
- const content = await readTextFile(npmLock);
2706
+ const content = await _readTextFile(npmLock);
2451
2707
  entries = parseNpmLock(content);
2452
- } else if (await pathExists(yarnLock)) {
2708
+ } else if (await _pathExists(yarnLock)) {
2453
2709
  result.lockfileType = "yarn";
2454
- const content = await readTextFile(yarnLock);
2710
+ const content = await _readTextFile(yarnLock);
2455
2711
  entries = parseYarnLock(content);
2456
2712
  }
2457
2713
  if (entries.length === 0) return result;
@@ -2479,12 +2735,12 @@ async function scanDependencyGraph(rootDir) {
2479
2735
  duplicated.sort((a, b) => b.versions.length - a.versions.length || a.name.localeCompare(b.name));
2480
2736
  result.duplicatedPackages = duplicated;
2481
2737
  const lockedNames = new Set(versionMap.keys());
2482
- const pkgFiles = await findPackageJsonFiles(rootDir);
2738
+ const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
2483
2739
  const phantoms = /* @__PURE__ */ new Set();
2484
2740
  const phantomDetails = [];
2485
2741
  for (const pjPath of pkgFiles) {
2486
2742
  try {
2487
- const pj = await readJsonFile(pjPath);
2743
+ const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
2488
2744
  const relPath = path9.relative(rootDir, pjPath);
2489
2745
  for (const section of ["dependencies", "devDependencies"]) {
2490
2746
  const deps = pj[section];
@@ -2869,7 +3125,7 @@ var IAC_EXTENSIONS = {
2869
3125
  ".tf": "terraform",
2870
3126
  ".bicep": "bicep"
2871
3127
  };
2872
- async function scanBuildDeploy(rootDir) {
3128
+ async function scanBuildDeploy(rootDir, cache) {
2873
3129
  const result = {
2874
3130
  ci: [],
2875
3131
  ciWorkflowCount: 0,
@@ -2879,26 +3135,37 @@ async function scanBuildDeploy(rootDir) {
2879
3135
  packageManagers: [],
2880
3136
  monorepoTools: []
2881
3137
  };
3138
+ const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
3139
+ const _findFiles = cache ? (dir, pred) => cache.findFiles(dir, pred) : findFiles;
3140
+ const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
2882
3141
  const ciSystems = /* @__PURE__ */ new Set();
2883
3142
  for (const [file, system] of Object.entries(CI_FILES)) {
2884
3143
  const fullPath = path10.join(rootDir, file);
2885
- if (await pathExists(fullPath)) {
3144
+ if (await _pathExists(fullPath)) {
2886
3145
  ciSystems.add(system);
2887
3146
  }
2888
3147
  }
2889
3148
  const ghWorkflowDir = path10.join(rootDir, ".github", "workflows");
2890
- if (await pathExists(ghWorkflowDir)) {
3149
+ if (await _pathExists(ghWorkflowDir)) {
2891
3150
  try {
2892
- const files = await findFiles(
2893
- ghWorkflowDir,
2894
- (name) => name.endsWith(".yml") || name.endsWith(".yaml")
2895
- );
2896
- result.ciWorkflowCount = files.length;
3151
+ if (cache) {
3152
+ const entries = await cache.walkDir(rootDir);
3153
+ const ghPrefix = path10.relative(rootDir, ghWorkflowDir) + path10.sep;
3154
+ result.ciWorkflowCount = entries.filter(
3155
+ (e) => e.isFile && e.relPath.startsWith(ghPrefix) && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))
3156
+ ).length;
3157
+ } else {
3158
+ const files = await _findFiles(
3159
+ ghWorkflowDir,
3160
+ (name) => name.endsWith(".yml") || name.endsWith(".yaml")
3161
+ );
3162
+ result.ciWorkflowCount = files.length;
3163
+ }
2897
3164
  } catch {
2898
3165
  }
2899
3166
  }
2900
3167
  result.ci = [...ciSystems].sort();
2901
- const dockerfiles = await findFiles(
3168
+ const dockerfiles = await _findFiles(
2902
3169
  rootDir,
2903
3170
  (name) => name === "Dockerfile" || name.startsWith("Dockerfile.")
2904
3171
  );
@@ -2906,7 +3173,7 @@ async function scanBuildDeploy(rootDir) {
2906
3173
  const baseImages = /* @__PURE__ */ new Set();
2907
3174
  for (const df of dockerfiles) {
2908
3175
  try {
2909
- const content = await readTextFile(df);
3176
+ const content = await _readTextFile(df);
2910
3177
  for (const line of content.split("\n")) {
2911
3178
  const trimmed = line.trim();
2912
3179
  if (/^FROM\s+/i.test(trimmed)) {
@@ -2926,24 +3193,24 @@ async function scanBuildDeploy(rootDir) {
2926
3193
  result.docker.baseImages = [...baseImages].sort();
2927
3194
  const iacSystems = /* @__PURE__ */ new Set();
2928
3195
  for (const [ext, system] of Object.entries(IAC_EXTENSIONS)) {
2929
- const files = await findFiles(rootDir, (name) => name.endsWith(ext));
3196
+ const files = await _findFiles(rootDir, (name) => name.endsWith(ext));
2930
3197
  if (files.length > 0) iacSystems.add(system);
2931
3198
  }
2932
- const cfnFiles = await findFiles(
3199
+ const cfnFiles = await _findFiles(
2933
3200
  rootDir,
2934
3201
  (name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
2935
3202
  );
2936
3203
  if (cfnFiles.length > 0) iacSystems.add("cloudformation");
2937
- if (await pathExists(path10.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
3204
+ if (await _pathExists(path10.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
2938
3205
  result.iac = [...iacSystems].sort();
2939
3206
  const releaseTools = /* @__PURE__ */ new Set();
2940
3207
  for (const [file, tool] of Object.entries(RELEASE_FILES)) {
2941
- if (await pathExists(path10.join(rootDir, file))) releaseTools.add(tool);
3208
+ if (await _pathExists(path10.join(rootDir, file))) releaseTools.add(tool);
2942
3209
  }
2943
- const pkgFiles = await findPackageJsonFiles(rootDir);
3210
+ const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
2944
3211
  for (const pjPath of pkgFiles) {
2945
3212
  try {
2946
- const pj = await readJsonFile(pjPath);
3213
+ const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
2947
3214
  for (const section of ["dependencies", "devDependencies"]) {
2948
3215
  const deps = pj[section];
2949
3216
  if (!deps) continue;
@@ -2963,12 +3230,12 @@ async function scanBuildDeploy(rootDir) {
2963
3230
  };
2964
3231
  const managers = /* @__PURE__ */ new Set();
2965
3232
  for (const [file, manager] of Object.entries(lockfileMap)) {
2966
- if (await pathExists(path10.join(rootDir, file))) managers.add(manager);
3233
+ if (await _pathExists(path10.join(rootDir, file))) managers.add(manager);
2967
3234
  }
2968
3235
  result.packageManagers = [...managers].sort();
2969
3236
  const monoTools = /* @__PURE__ */ new Set();
2970
3237
  for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
2971
- if (await pathExists(path10.join(rootDir, file))) monoTools.add(tool);
3238
+ if (await _pathExists(path10.join(rootDir, file))) monoTools.add(tool);
2972
3239
  }
2973
3240
  result.monorepoTools = [...monoTools].sort();
2974
3241
  return result;
@@ -2976,7 +3243,7 @@ async function scanBuildDeploy(rootDir) {
2976
3243
 
2977
3244
  // src/scanners/ts-modernity.ts
2978
3245
  import * as path11 from "path";
2979
- async function scanTsModernity(rootDir) {
3246
+ async function scanTsModernity(rootDir, cache) {
2980
3247
  const result = {
2981
3248
  typescriptVersion: null,
2982
3249
  strict: null,
@@ -2988,12 +3255,12 @@ async function scanTsModernity(rootDir) {
2988
3255
  moduleType: null,
2989
3256
  exportsField: false
2990
3257
  };
2991
- const pkgFiles = await findPackageJsonFiles(rootDir);
3258
+ const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
2992
3259
  let hasEsm = false;
2993
3260
  let hasCjs = false;
2994
3261
  for (const pjPath of pkgFiles) {
2995
3262
  try {
2996
- const pj = await readJsonFile(pjPath);
3263
+ const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
2997
3264
  if (!result.typescriptVersion) {
2998
3265
  const tsVer = pj.devDependencies?.["typescript"] ?? pj.dependencies?.["typescript"];
2999
3266
  if (tsVer) {
@@ -3014,8 +3281,9 @@ async function scanTsModernity(rootDir) {
3014
3281
  else if (hasEsm) result.moduleType = "esm";
3015
3282
  else if (hasCjs) result.moduleType = "cjs";
3016
3283
  let tsConfigPath = path11.join(rootDir, "tsconfig.json");
3017
- if (!await pathExists(tsConfigPath)) {
3018
- const tsConfigs = await findFiles(rootDir, (name) => name === "tsconfig.json");
3284
+ const tsConfigExists = cache ? await cache.pathExists(tsConfigPath) : await pathExists(tsConfigPath);
3285
+ if (!tsConfigExists) {
3286
+ const tsConfigs = cache ? await cache.findFiles(rootDir, (name) => name === "tsconfig.json") : await findFiles(rootDir, (name) => name === "tsconfig.json");
3019
3287
  if (tsConfigs.length > 0) {
3020
3288
  tsConfigPath = tsConfigs[0];
3021
3289
  } else {
@@ -3023,7 +3291,7 @@ async function scanTsModernity(rootDir) {
3023
3291
  }
3024
3292
  }
3025
3293
  try {
3026
- const raw = await readTextFile(tsConfigPath);
3294
+ const raw = cache ? await cache.readTextFile(tsConfigPath) : await readTextFile(tsConfigPath);
3027
3295
  const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,(\s*[}\]])/g, "$1");
3028
3296
  const tsConfig = JSON.parse(stripped);
3029
3297
  const co = tsConfig?.compilerOptions;
@@ -3394,43 +3662,63 @@ var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
3394
3662
  ".mp4",
3395
3663
  ".webm"
3396
3664
  ]);
3397
- async function scanFileHotspots(rootDir) {
3665
+ async function scanFileHotspots(rootDir, cache) {
3398
3666
  const extensionCounts = {};
3399
3667
  const allFiles = [];
3400
3668
  let maxDepth = 0;
3401
- async function walk(dir, depth) {
3402
- if (depth > maxDepth) maxDepth = depth;
3403
- let entries;
3404
- try {
3405
- const dirents = await fs4.readdir(dir, { withFileTypes: true });
3406
- entries = dirents.map((d) => ({
3407
- name: d.name,
3408
- isDirectory: d.isDirectory(),
3409
- isFile: d.isFile()
3410
- }));
3411
- } catch {
3412
- return;
3669
+ if (cache) {
3670
+ const entries = await cache.walkDir(rootDir);
3671
+ for (const entry of entries) {
3672
+ if (!entry.isFile) continue;
3673
+ const ext = path12.extname(entry.name).toLowerCase();
3674
+ if (SKIP_EXTENSIONS.has(ext)) continue;
3675
+ const depth = entry.relPath.split(path12.sep).length - 1;
3676
+ if (depth > maxDepth) maxDepth = depth;
3677
+ extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
3678
+ try {
3679
+ const stat3 = await fs4.stat(entry.absPath);
3680
+ allFiles.push({
3681
+ path: entry.relPath,
3682
+ bytes: stat3.size
3683
+ });
3684
+ } catch {
3685
+ }
3413
3686
  }
3414
- for (const e of entries) {
3415
- if (e.isDirectory) {
3416
- if (SKIP_DIRS2.has(e.name)) continue;
3417
- await walk(path12.join(dir, e.name), depth + 1);
3418
- } else if (e.isFile) {
3419
- const ext = path12.extname(e.name).toLowerCase();
3420
- if (SKIP_EXTENSIONS.has(ext)) continue;
3421
- extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
3422
- try {
3423
- const stat3 = await fs4.stat(path12.join(dir, e.name));
3424
- allFiles.push({
3425
- path: path12.relative(rootDir, path12.join(dir, e.name)),
3426
- bytes: stat3.size
3427
- });
3428
- } catch {
3687
+ } else {
3688
+ async function walk(dir, depth) {
3689
+ if (depth > maxDepth) maxDepth = depth;
3690
+ let entries;
3691
+ try {
3692
+ const dirents = await fs4.readdir(dir, { withFileTypes: true });
3693
+ entries = dirents.map((d) => ({
3694
+ name: d.name,
3695
+ isDirectory: d.isDirectory(),
3696
+ isFile: d.isFile()
3697
+ }));
3698
+ } catch {
3699
+ return;
3700
+ }
3701
+ for (const e of entries) {
3702
+ if (e.isDirectory) {
3703
+ if (SKIP_DIRS2.has(e.name)) continue;
3704
+ await walk(path12.join(dir, e.name), depth + 1);
3705
+ } else if (e.isFile) {
3706
+ const ext = path12.extname(e.name).toLowerCase();
3707
+ if (SKIP_EXTENSIONS.has(ext)) continue;
3708
+ extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
3709
+ try {
3710
+ const stat3 = await fs4.stat(path12.join(dir, e.name));
3711
+ allFiles.push({
3712
+ path: path12.relative(rootDir, path12.join(dir, e.name)),
3713
+ bytes: stat3.size
3714
+ });
3715
+ } catch {
3716
+ }
3429
3717
  }
3430
3718
  }
3431
3719
  }
3720
+ await walk(rootDir, 0);
3432
3721
  }
3433
- await walk(rootDir, 0);
3434
3722
  allFiles.sort((a, b) => b.bytes - a.bytes);
3435
3723
  const largestFiles = allFiles.slice(0, 20);
3436
3724
  return {
@@ -3452,7 +3740,7 @@ var LOCKFILES = {
3452
3740
  "bun.lockb": "bun",
3453
3741
  "packages.lock.json": "nuget"
3454
3742
  };
3455
- async function scanSecurityPosture(rootDir) {
3743
+ async function scanSecurityPosture(rootDir, cache) {
3456
3744
  const result = {
3457
3745
  lockfilePresent: false,
3458
3746
  multipleLockfileTypes: false,
@@ -3461,9 +3749,11 @@ async function scanSecurityPosture(rootDir) {
3461
3749
  envFilesTracked: false,
3462
3750
  lockfileTypes: []
3463
3751
  };
3752
+ const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
3753
+ const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
3464
3754
  const foundLockfiles = [];
3465
3755
  for (const [file, type] of Object.entries(LOCKFILES)) {
3466
- if (await pathExists(path13.join(rootDir, file))) {
3756
+ if (await _pathExists(path13.join(rootDir, file))) {
3467
3757
  foundLockfiles.push(type);
3468
3758
  }
3469
3759
  }
@@ -3471,9 +3761,9 @@ async function scanSecurityPosture(rootDir) {
3471
3761
  result.multipleLockfileTypes = foundLockfiles.length > 1;
3472
3762
  result.lockfileTypes = foundLockfiles.sort();
3473
3763
  const gitignorePath = path13.join(rootDir, ".gitignore");
3474
- if (await pathExists(gitignorePath)) {
3764
+ if (await _pathExists(gitignorePath)) {
3475
3765
  try {
3476
- const content = await readTextFile(gitignorePath);
3766
+ const content = await _readTextFile(gitignorePath);
3477
3767
  const lines = content.split("\n").map((l) => l.trim());
3478
3768
  result.gitignoreCoversEnv = lines.some(
3479
3769
  (line) => line === ".env" || line === ".env*" || line === ".env.*" || line === ".env.local" || line === "*.env"
@@ -3485,7 +3775,7 @@ async function scanSecurityPosture(rootDir) {
3485
3775
  }
3486
3776
  }
3487
3777
  for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
3488
- if (await pathExists(path13.join(rootDir, envFile))) {
3778
+ if (await _pathExists(path13.join(rootDir, envFile))) {
3489
3779
  if (!result.gitignoreCoversEnv) {
3490
3780
  result.envFilesTracked = true;
3491
3781
  break;
@@ -3909,12 +4199,545 @@ function scanServiceDependencies(projects) {
3909
4199
  return result;
3910
4200
  }
3911
4201
 
4202
+ // src/scanners/architecture.ts
4203
+ import * as path14 from "path";
4204
+ import * as fs5 from "fs/promises";
4205
+ var ARCHETYPE_SIGNALS = [
4206
+ // Meta-frameworks (highest priority — they imply routing patterns)
4207
+ { packages: ["next", "@next/core"], archetype: "nextjs", weight: 10 },
4208
+ { packages: ["@remix-run/react", "@remix-run/node", "@remix-run/dev"], archetype: "remix", weight: 10 },
4209
+ { packages: ["@sveltejs/kit"], archetype: "sveltekit", weight: 10 },
4210
+ { packages: ["nuxt"], archetype: "nuxt", weight: 10 },
4211
+ // Backend frameworks
4212
+ { packages: ["@nestjs/core", "@nestjs/common"], archetype: "nestjs", weight: 9 },
4213
+ { packages: ["fastify"], archetype: "fastify", weight: 8 },
4214
+ { packages: ["hono"], archetype: "hono", weight: 8 },
4215
+ { packages: ["koa"], archetype: "koa", weight: 8 },
4216
+ { packages: ["express"], archetype: "express", weight: 7 },
4217
+ // Serverless
4218
+ { packages: ["serverless", "aws-lambda", "@aws-sdk/client-lambda", "middy", "@cloudflare/workers-types"], archetype: "serverless", weight: 6 },
4219
+ // CLI
4220
+ { packages: ["commander", "yargs", "meow", "cac", "clipanion", "oclif"], archetype: "cli", weight: 5 }
4221
+ ];
4222
+ function detectArchetype(projects) {
4223
+ const allPackages = /* @__PURE__ */ new Set();
4224
+ for (const p of projects) {
4225
+ for (const d of p.dependencies) {
4226
+ allPackages.add(d.package);
4227
+ }
4228
+ }
4229
+ if (projects.length > 2) {
4230
+ return { archetype: "monorepo", confidence: 0.8 };
4231
+ }
4232
+ let bestArchetype = "unknown";
4233
+ let bestScore = 0;
4234
+ for (const signal of ARCHETYPE_SIGNALS) {
4235
+ const matched = signal.packages.filter((p) => allPackages.has(p)).length;
4236
+ if (matched > 0) {
4237
+ const score = matched * signal.weight;
4238
+ if (score > bestScore) {
4239
+ bestScore = score;
4240
+ bestArchetype = signal.archetype;
4241
+ }
4242
+ }
4243
+ }
4244
+ if (bestArchetype === "unknown") {
4245
+ bestArchetype = "library";
4246
+ bestScore = 3;
4247
+ }
4248
+ const confidence = Math.min(bestScore / 15, 1);
4249
+ return { archetype: bestArchetype, confidence: Math.round(confidence * 100) / 100 };
4250
+ }
4251
+ var PATH_RULES = [
4252
+ // ── Testing (high precision) ──
4253
+ { pattern: /\/__tests__\//, layer: "testing", confidence: 0.95, signal: "__tests__ directory" },
4254
+ { pattern: /\.test\.[jt]sx?$/, layer: "testing", confidence: 0.95, signal: ".test.* file" },
4255
+ { pattern: /\.spec\.[jt]sx?$/, layer: "testing", confidence: 0.95, signal: ".spec.* file" },
4256
+ { pattern: /\/test\//, layer: "testing", confidence: 0.85, signal: "test/ directory" },
4257
+ { pattern: /\/tests\//, layer: "testing", confidence: 0.85, signal: "tests/ directory" },
4258
+ { pattern: /\/__mocks__\//, layer: "testing", confidence: 0.9, signal: "__mocks__ directory" },
4259
+ { pattern: /\/fixtures\//, layer: "testing", confidence: 0.8, signal: "fixtures/ directory" },
4260
+ // ── Config/Infrastructure (high precision) ──
4261
+ { pattern: /\/config\.[jt]sx?$/, layer: "config", confidence: 0.85, signal: "config.* file" },
4262
+ { pattern: /\/config\//, layer: "config", confidence: 0.8, signal: "config/ directory" },
4263
+ { pattern: /\.config\.[jt]sx?$/, layer: "config", confidence: 0.9, signal: ".config.* file" },
4264
+ { pattern: /\/env\.[jt]sx?$/, layer: "config", confidence: 0.85, signal: "env.* file" },
4265
+ { pattern: /\/bootstrap\.[jt]sx?$/, layer: "config", confidence: 0.85, signal: "bootstrap file" },
4266
+ { pattern: /\/setup\.[jt]sx?$/, layer: "config", confidence: 0.8, signal: "setup file" },
4267
+ // ── Next.js (archetype-specific) ──
4268
+ { pattern: /(^|\/)app\/.*\/route\.[jt]sx?$/, layer: "routing", confidence: 0.95, signal: "Next.js App Router route", archetypes: ["nextjs"] },
4269
+ { pattern: /(^|\/)pages\/api\//, layer: "routing", confidence: 0.95, signal: "Next.js Pages API route", archetypes: ["nextjs"] },
4270
+ { pattern: /(^|\/)app\/.*page\.[jt]sx?$/, layer: "presentation", confidence: 0.9, signal: "Next.js page component", archetypes: ["nextjs"] },
4271
+ { pattern: /(^|\/)app\/.*layout\.[jt]sx?$/, layer: "presentation", confidence: 0.9, signal: "Next.js layout component", archetypes: ["nextjs"] },
4272
+ { pattern: /(^|\/)app\/.*loading\.[jt]sx?$/, layer: "presentation", confidence: 0.85, signal: "Next.js loading component", archetypes: ["nextjs"] },
4273
+ { pattern: /(^|\/)app\/.*error\.[jt]sx?$/, layer: "presentation", confidence: 0.85, signal: "Next.js error component", archetypes: ["nextjs"] },
4274
+ { pattern: /(^|\/)middleware\.[jt]sx?$/, layer: "middleware", confidence: 0.9, signal: "Next.js middleware", archetypes: ["nextjs"] },
4275
+ // ── Remix (archetype-specific) ──
4276
+ { pattern: /\/app\/routes\//, layer: "routing", confidence: 0.95, signal: "Remix route file", archetypes: ["remix"] },
4277
+ { pattern: /\/app\/root\.[jt]sx?$/, layer: "presentation", confidence: 0.9, signal: "Remix root", archetypes: ["remix"] },
4278
+ // ── SvelteKit (archetype-specific) ──
4279
+ { pattern: /\/src\/routes\/.*\+server\.[jt]s$/, layer: "routing", confidence: 0.95, signal: "SvelteKit API route", archetypes: ["sveltekit"] },
4280
+ { pattern: /\/src\/routes\/.*\+page\.svelte$/, layer: "presentation", confidence: 0.9, signal: "SvelteKit page", archetypes: ["sveltekit"] },
4281
+ { pattern: /\/src\/routes\/.*\+layout\.svelte$/, layer: "presentation", confidence: 0.9, signal: "SvelteKit layout", archetypes: ["sveltekit"] },
4282
+ { pattern: /\/src\/hooks\.server\.[jt]s$/, layer: "middleware", confidence: 0.9, signal: "SvelteKit server hooks", archetypes: ["sveltekit"] },
4283
+ // ── Nuxt (archetype-specific) ──
4284
+ { pattern: /\/server\/api\//, layer: "routing", confidence: 0.95, signal: "Nuxt server API", archetypes: ["nuxt"] },
4285
+ { pattern: /\/server\/routes\//, layer: "routing", confidence: 0.95, signal: "Nuxt server route", archetypes: ["nuxt"] },
4286
+ { pattern: /\/server\/middleware\//, layer: "middleware", confidence: 0.95, signal: "Nuxt server middleware", archetypes: ["nuxt"] },
4287
+ { pattern: /\/pages\//, layer: "presentation", confidence: 0.85, signal: "Nuxt pages directory", archetypes: ["nuxt"] },
4288
+ // ── NestJS (archetype-specific) ──
4289
+ { pattern: /\.controller\.[jt]sx?$/, layer: "routing", confidence: 0.95, signal: "NestJS controller", archetypes: ["nestjs"] },
4290
+ { pattern: /\.service\.[jt]sx?$/, layer: "services", confidence: 0.95, signal: "NestJS service", archetypes: ["nestjs"] },
4291
+ { pattern: /\.module\.[jt]sx?$/, layer: "config", confidence: 0.9, signal: "NestJS module", archetypes: ["nestjs"] },
4292
+ { pattern: /\.guard\.[jt]sx?$/, layer: "middleware", confidence: 0.9, signal: "NestJS guard", archetypes: ["nestjs"] },
4293
+ { pattern: /\.interceptor\.[jt]sx?$/, layer: "middleware", confidence: 0.9, signal: "NestJS interceptor", archetypes: ["nestjs"] },
4294
+ { pattern: /\.pipe\.[jt]sx?$/, layer: "middleware", confidence: 0.85, signal: "NestJS pipe", archetypes: ["nestjs"] },
4295
+ { pattern: /\.middleware\.[jt]sx?$/, layer: "middleware", confidence: 0.9, signal: "NestJS middleware", archetypes: ["nestjs"] },
4296
+ { pattern: /\.entity\.[jt]sx?$/, layer: "domain", confidence: 0.9, signal: "NestJS entity", archetypes: ["nestjs"] },
4297
+ { pattern: /\.dto\.[jt]sx?$/, layer: "domain", confidence: 0.85, signal: "NestJS DTO", archetypes: ["nestjs"] },
4298
+ { pattern: /\.repository\.[jt]sx?$/, layer: "data-access", confidence: 0.9, signal: "NestJS repository", archetypes: ["nestjs"] },
4299
+ // ── Generic routing patterns ──
4300
+ { pattern: /\/routes\//, layer: "routing", confidence: 0.8, signal: "routes/ directory" },
4301
+ { pattern: /\/router\//, layer: "routing", confidence: 0.8, signal: "router/ directory" },
4302
+ { pattern: /\/controllers\//, layer: "routing", confidence: 0.8, signal: "controllers/ directory" },
4303
+ { pattern: /\/handlers\//, layer: "routing", confidence: 0.75, signal: "handlers/ directory" },
4304
+ { pattern: /\/api\//, layer: "routing", confidence: 0.7, signal: "api/ directory" },
4305
+ { pattern: /\/endpoints\//, layer: "routing", confidence: 0.8, signal: "endpoints/ directory" },
4306
+ // ── Middleware ──
4307
+ { pattern: /\/middleware\//, layer: "middleware", confidence: 0.85, signal: "middleware/ directory" },
4308
+ { pattern: /\/middlewares\//, layer: "middleware", confidence: 0.85, signal: "middlewares/ directory" },
4309
+ { pattern: /\/hooks\//, layer: "middleware", confidence: 0.6, signal: "hooks/ directory" },
4310
+ { pattern: /\/plugins\//, layer: "middleware", confidence: 0.6, signal: "plugins/ directory" },
4311
+ { pattern: /\/guards\//, layer: "middleware", confidence: 0.85, signal: "guards/ directory" },
4312
+ { pattern: /\/interceptors\//, layer: "middleware", confidence: 0.85, signal: "interceptors/ directory" },
4313
+ // ── Services / application layer ──
4314
+ { pattern: /\/services\//, layer: "services", confidence: 0.85, signal: "services/ directory" },
4315
+ { pattern: /\/service\//, layer: "services", confidence: 0.8, signal: "service/ directory" },
4316
+ { pattern: /\/usecases\//, layer: "services", confidence: 0.85, signal: "usecases/ directory" },
4317
+ { pattern: /\/use-cases\//, layer: "services", confidence: 0.85, signal: "use-cases/ directory" },
4318
+ { pattern: /\/application\//, layer: "services", confidence: 0.7, signal: "application/ directory" },
4319
+ { pattern: /\/actions\//, layer: "services", confidence: 0.65, signal: "actions/ directory" },
4320
+ // ── Domain / models ──
4321
+ { pattern: /\/domain\//, layer: "domain", confidence: 0.85, signal: "domain/ directory" },
4322
+ { pattern: /\/models\//, layer: "domain", confidence: 0.8, signal: "models/ directory" },
4323
+ { pattern: /\/entities\//, layer: "domain", confidence: 0.85, signal: "entities/ directory" },
4324
+ { pattern: /\/types\//, layer: "domain", confidence: 0.7, signal: "types/ directory" },
4325
+ { pattern: /\/schemas\//, layer: "domain", confidence: 0.7, signal: "schemas/ directory" },
4326
+ { pattern: /\/validators\//, layer: "domain", confidence: 0.7, signal: "validators/ directory" },
4327
+ // ── Data access ──
4328
+ { pattern: /\/repositories\//, layer: "data-access", confidence: 0.9, signal: "repositories/ directory" },
4329
+ { pattern: /\/repository\//, layer: "data-access", confidence: 0.85, signal: "repository/ directory" },
4330
+ { pattern: /\/dao\//, layer: "data-access", confidence: 0.9, signal: "dao/ directory" },
4331
+ { pattern: /\/db\//, layer: "data-access", confidence: 0.8, signal: "db/ directory" },
4332
+ { pattern: /\/database\//, layer: "data-access", confidence: 0.8, signal: "database/ directory" },
4333
+ { pattern: /\/persistence\//, layer: "data-access", confidence: 0.85, signal: "persistence/ directory" },
4334
+ { pattern: /\/migrations\//, layer: "data-access", confidence: 0.9, signal: "migrations/ directory" },
4335
+ { pattern: /\/seeds\//, layer: "data-access", confidence: 0.85, signal: "seeds/ directory" },
4336
+ { pattern: /\/prisma\//, layer: "data-access", confidence: 0.85, signal: "prisma/ directory" },
4337
+ { pattern: /\/drizzle\//, layer: "data-access", confidence: 0.85, signal: "drizzle/ directory" },
4338
+ // ── Infrastructure ──
4339
+ { pattern: /\/infra\//, layer: "infrastructure", confidence: 0.85, signal: "infra/ directory" },
4340
+ { pattern: /\/infrastructure\//, layer: "infrastructure", confidence: 0.85, signal: "infrastructure/ directory" },
4341
+ { pattern: /\/adapters\//, layer: "infrastructure", confidence: 0.8, signal: "adapters/ directory" },
4342
+ { pattern: /\/clients\//, layer: "infrastructure", confidence: 0.75, signal: "clients/ directory" },
4343
+ { pattern: /\/integrations\//, layer: "infrastructure", confidence: 0.8, signal: "integrations/ directory" },
4344
+ { pattern: /\/external\//, layer: "infrastructure", confidence: 0.75, signal: "external/ directory" },
4345
+ { pattern: /\/queue\//, layer: "infrastructure", confidence: 0.8, signal: "queue/ directory" },
4346
+ { pattern: /\/jobs\//, layer: "infrastructure", confidence: 0.75, signal: "jobs/ directory" },
4347
+ { pattern: /\/workers\//, layer: "infrastructure", confidence: 0.75, signal: "workers/ directory" },
4348
+ { pattern: /\/cron\//, layer: "infrastructure", confidence: 0.8, signal: "cron/ directory" },
4349
+ // ── Presentation (UI layer) ──
4350
+ { pattern: /\/components\//, layer: "presentation", confidence: 0.85, signal: "components/ directory" },
4351
+ { pattern: /\/views\//, layer: "presentation", confidence: 0.85, signal: "views/ directory" },
4352
+ { pattern: /\/pages\//, layer: "presentation", confidence: 0.8, signal: "pages/ directory" },
4353
+ { pattern: /\/layouts\//, layer: "presentation", confidence: 0.85, signal: "layouts/ directory" },
4354
+ { pattern: /\/templates\//, layer: "presentation", confidence: 0.8, signal: "templates/ directory" },
4355
+ { pattern: /\/widgets\//, layer: "presentation", confidence: 0.8, signal: "widgets/ directory" },
4356
+ { pattern: /\/ui\//, layer: "presentation", confidence: 0.75, signal: "ui/ directory" },
4357
+ // ── Shared / utils ──
4358
+ { pattern: /\/utils\//, layer: "shared", confidence: 0.7, signal: "utils/ directory" },
4359
+ { pattern: /\/helpers\//, layer: "shared", confidence: 0.7, signal: "helpers/ directory" },
4360
+ { pattern: /\/lib\//, layer: "shared", confidence: 0.6, signal: "lib/ directory" },
4361
+ { pattern: /\/common\//, layer: "shared", confidence: 0.65, signal: "common/ directory" },
4362
+ { pattern: /\/shared\//, layer: "shared", confidence: 0.75, signal: "shared/ directory" },
4363
+ { pattern: /\/constants\//, layer: "shared", confidence: 0.7, signal: "constants/ directory" },
4364
+ // ── CLI-specific (command layer → routing) ──
4365
+ { pattern: /\/commands\//, layer: "routing", confidence: 0.8, signal: "commands/ directory", archetypes: ["cli"] },
4366
+ { pattern: /\/formatters\//, layer: "presentation", confidence: 0.8, signal: "formatters/ directory", archetypes: ["cli"] },
4367
+ { pattern: /\/scanners\//, layer: "services", confidence: 0.8, signal: "scanners/ directory", archetypes: ["cli"] },
4368
+ { pattern: /\/scoring\//, layer: "domain", confidence: 0.8, signal: "scoring/ directory", archetypes: ["cli"] },
4369
+ // ── Serverless-specific ──
4370
+ { pattern: /\/functions\//, layer: "routing", confidence: 0.8, signal: "functions/ directory", archetypes: ["serverless"] },
4371
+ { pattern: /\/lambdas\//, layer: "routing", confidence: 0.85, signal: "lambdas/ directory", archetypes: ["serverless"] },
4372
+ { pattern: /\/layers\//, layer: "shared", confidence: 0.7, signal: "Lambda layers/ directory", archetypes: ["serverless"] }
4373
+ ];
4374
+ var SUFFIX_RULES = [
4375
+ { suffix: ".controller", layer: "routing", confidence: 0.85, signal: "controller suffix" },
4376
+ { suffix: ".route", layer: "routing", confidence: 0.85, signal: "route suffix" },
4377
+ { suffix: ".router", layer: "routing", confidence: 0.85, signal: "router suffix" },
4378
+ { suffix: ".handler", layer: "routing", confidence: 0.8, signal: "handler suffix" },
4379
+ { suffix: ".middleware", layer: "middleware", confidence: 0.85, signal: "middleware suffix" },
4380
+ { suffix: ".guard", layer: "middleware", confidence: 0.85, signal: "guard suffix" },
4381
+ { suffix: ".interceptor", layer: "middleware", confidence: 0.85, signal: "interceptor suffix" },
4382
+ { suffix: ".service", layer: "services", confidence: 0.85, signal: "service suffix" },
4383
+ { suffix: ".usecase", layer: "services", confidence: 0.85, signal: "usecase suffix" },
4384
+ { suffix: ".model", layer: "domain", confidence: 0.8, signal: "model suffix" },
4385
+ { suffix: ".entity", layer: "domain", confidence: 0.85, signal: "entity suffix" },
4386
+ { suffix: ".dto", layer: "domain", confidence: 0.8, signal: "DTO suffix" },
4387
+ { suffix: ".schema", layer: "domain", confidence: 0.75, signal: "schema suffix" },
4388
+ { suffix: ".validator", layer: "domain", confidence: 0.75, signal: "validator suffix" },
4389
+ { suffix: ".repository", layer: "data-access", confidence: 0.9, signal: "repository suffix" },
4390
+ { suffix: ".repo", layer: "data-access", confidence: 0.85, signal: "repo suffix" },
4391
+ { suffix: ".dao", layer: "data-access", confidence: 0.9, signal: "dao suffix" },
4392
+ { suffix: ".migration", layer: "data-access", confidence: 0.85, signal: "migration suffix" },
4393
+ { suffix: ".adapter", layer: "infrastructure", confidence: 0.8, signal: "adapter suffix" },
4394
+ { suffix: ".client", layer: "infrastructure", confidence: 0.75, signal: "client suffix" },
4395
+ { suffix: ".provider", layer: "infrastructure", confidence: 0.7, signal: "provider suffix" },
4396
+ { suffix: ".config", layer: "config", confidence: 0.8, signal: "config suffix" },
4397
+ { suffix: ".component", layer: "presentation", confidence: 0.8, signal: "component suffix" },
4398
+ { suffix: ".page", layer: "presentation", confidence: 0.85, signal: "page suffix" },
4399
+ { suffix: ".view", layer: "presentation", confidence: 0.8, signal: "view suffix" },
4400
+ { suffix: ".layout", layer: "presentation", confidence: 0.85, signal: "layout suffix" },
4401
+ { suffix: ".util", layer: "shared", confidence: 0.7, signal: "util suffix" },
4402
+ { suffix: ".helper", layer: "shared", confidence: 0.7, signal: "helper suffix" },
4403
+ { suffix: ".constant", layer: "shared", confidence: 0.7, signal: "constant suffix" }
4404
+ ];
4405
+ var PACKAGE_LAYER_MAP = {
4406
+ // Routing/controllers
4407
+ "express": "routing",
4408
+ "fastify": "routing",
4409
+ "@nestjs/core": "routing",
4410
+ "hono": "routing",
4411
+ "koa": "routing",
4412
+ "koa-router": "routing",
4413
+ "@hapi/hapi": "routing",
4414
+ "h3": "routing",
4415
+ // Middleware
4416
+ "cors": "middleware",
4417
+ "helmet": "middleware",
4418
+ "passport": "middleware",
4419
+ "express-rate-limit": "middleware",
4420
+ "cookie-parser": "middleware",
4421
+ "body-parser": "middleware",
4422
+ "multer": "middleware",
4423
+ "morgan": "middleware",
4424
+ "compression": "middleware",
4425
+ "express-session": "middleware",
4426
+ // Services / application
4427
+ "bullmq": "services",
4428
+ "bull": "services",
4429
+ "agenda": "services",
4430
+ "pg-boss": "services",
4431
+ "inngest": "services",
4432
+ // Domain / validation
4433
+ "zod": "domain",
4434
+ "joi": "domain",
4435
+ "yup": "domain",
4436
+ "class-validator": "domain",
4437
+ "class-transformer": "domain",
4438
+ "superstruct": "domain",
4439
+ "valibot": "domain",
4440
+ // Data access / ORM
4441
+ "prisma": "data-access",
4442
+ "@prisma/client": "data-access",
4443
+ "drizzle-orm": "data-access",
4444
+ "typeorm": "data-access",
4445
+ "sequelize": "data-access",
4446
+ "knex": "data-access",
4447
+ "pg": "data-access",
4448
+ "mysql2": "data-access",
4449
+ "mongodb": "data-access",
4450
+ "mongoose": "data-access",
4451
+ "ioredis": "data-access",
4452
+ "redis": "data-access",
4453
+ "better-sqlite3": "data-access",
4454
+ "kysely": "data-access",
4455
+ "@mikro-orm/core": "data-access",
4456
+ // Infrastructure
4457
+ "@aws-sdk/client-s3": "infrastructure",
4458
+ "@aws-sdk/client-sqs": "infrastructure",
4459
+ "@aws-sdk/client-sns": "infrastructure",
4460
+ "@aws-sdk/client-ses": "infrastructure",
4461
+ "@aws-sdk/client-lambda": "infrastructure",
4462
+ "@google-cloud/storage": "infrastructure",
4463
+ "@azure/storage-blob": "infrastructure",
4464
+ "nodemailer": "infrastructure",
4465
+ "@sendgrid/mail": "infrastructure",
4466
+ "stripe": "infrastructure",
4467
+ "kafkajs": "infrastructure",
4468
+ "amqplib": "infrastructure",
4469
+ // Presentation
4470
+ "react": "presentation",
4471
+ "react-dom": "presentation",
4472
+ "vue": "presentation",
4473
+ "@angular/core": "presentation",
4474
+ "svelte": "presentation",
4475
+ // Shared
4476
+ "lodash": "shared",
4477
+ "dayjs": "shared",
4478
+ "date-fns": "shared",
4479
+ "uuid": "shared",
4480
+ "nanoid": "shared",
4481
+ // Testing
4482
+ "vitest": "testing",
4483
+ "jest": "testing",
4484
+ "mocha": "testing",
4485
+ "@playwright/test": "testing",
4486
+ "cypress": "testing",
4487
+ "supertest": "testing",
4488
+ // Observability → infrastructure
4489
+ "@sentry/node": "infrastructure",
4490
+ "@opentelemetry/api": "infrastructure",
4491
+ "pino": "infrastructure",
4492
+ "winston": "infrastructure",
4493
+ "dd-trace": "infrastructure"
4494
+ };
4495
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs", ".svelte", ".vue"]);
4496
+ var IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".nuxt", ".output", ".svelte-kit", "coverage", ".vibgrate"]);
4497
+ async function walkSourceFiles(rootDir, cache) {
4498
+ if (cache) {
4499
+ const entries = await cache.walkDir(rootDir);
4500
+ return entries.filter((e) => {
4501
+ if (!e.isFile) return false;
4502
+ const name = path14.basename(e.absPath);
4503
+ if (name.startsWith(".") && name !== ".") return false;
4504
+ const ext = path14.extname(name);
4505
+ return SOURCE_EXTENSIONS.has(ext);
4506
+ }).map((e) => e.relPath);
4507
+ }
4508
+ const files = [];
4509
+ async function walk(dir) {
4510
+ let entries;
4511
+ try {
4512
+ entries = await fs5.readdir(dir, { withFileTypes: true });
4513
+ } catch {
4514
+ return;
4515
+ }
4516
+ for (const entry of entries) {
4517
+ if (entry.name.startsWith(".") && entry.name !== ".") continue;
4518
+ const fullPath = path14.join(dir, entry.name);
4519
+ if (entry.isDirectory()) {
4520
+ if (!IGNORE_DIRS.has(entry.name)) {
4521
+ await walk(fullPath);
4522
+ }
4523
+ } else if (entry.isFile()) {
4524
+ const ext = path14.extname(entry.name);
4525
+ if (SOURCE_EXTENSIONS.has(ext)) {
4526
+ files.push(path14.relative(rootDir, fullPath));
4527
+ }
4528
+ }
4529
+ }
4530
+ }
4531
+ await walk(rootDir);
4532
+ return files;
4533
+ }
4534
+ function classifyFile(filePath, archetype) {
4535
+ const normalised = filePath.replace(/\\/g, "/");
4536
+ let bestMatch = null;
4537
+ for (const rule of PATH_RULES) {
4538
+ if (rule.archetypes && rule.archetypes.length > 0 && !rule.archetypes.includes(archetype)) {
4539
+ continue;
4540
+ }
4541
+ if (rule.pattern.test(normalised)) {
4542
+ const boost = rule.archetypes ? 0.05 : 0;
4543
+ const adjustedConfidence = Math.min(rule.confidence + boost, 1);
4544
+ if (!bestMatch || adjustedConfidence > bestMatch.confidence) {
4545
+ bestMatch = { layer: rule.layer, confidence: adjustedConfidence, signal: rule.signal };
4546
+ }
4547
+ }
4548
+ }
4549
+ if (!bestMatch || bestMatch.confidence < 0.7) {
4550
+ const baseName = path14.basename(filePath, path14.extname(filePath));
4551
+ const cleanBase = baseName.replace(/\.(test|spec)$/, "");
4552
+ for (const rule of SUFFIX_RULES) {
4553
+ if (cleanBase.endsWith(rule.suffix)) {
4554
+ if (!bestMatch || rule.confidence > bestMatch.confidence) {
4555
+ bestMatch = { layer: rule.layer, confidence: rule.confidence, signal: rule.signal };
4556
+ }
4557
+ }
4558
+ }
4559
+ }
4560
+ if (bestMatch) {
4561
+ return {
4562
+ filePath,
4563
+ layer: bestMatch.layer,
4564
+ confidence: bestMatch.confidence,
4565
+ signals: [bestMatch.signal]
4566
+ };
4567
+ }
4568
+ return null;
4569
+ }
4570
+ function computeLayerDrift(packages) {
4571
+ if (packages.length === 0) {
4572
+ return { score: 100, riskLevel: "low" };
4573
+ }
4574
+ let current = 0;
4575
+ let oneBehind = 0;
4576
+ let twoPlusBehind = 0;
4577
+ let unknown = 0;
4578
+ for (const pkg2 of packages) {
4579
+ if (pkg2.majorsBehind === null) {
4580
+ unknown++;
4581
+ } else if (pkg2.majorsBehind === 0) {
4582
+ current++;
4583
+ } else if (pkg2.majorsBehind === 1) {
4584
+ oneBehind++;
4585
+ } else {
4586
+ twoPlusBehind++;
4587
+ }
4588
+ }
4589
+ const known = current + oneBehind + twoPlusBehind;
4590
+ if (known === 0) return { score: 100, riskLevel: "low" };
4591
+ const currentPct = current / known;
4592
+ const onePct = oneBehind / known;
4593
+ const twoPct = twoPlusBehind / known;
4594
+ const score = Math.round(Math.max(0, Math.min(100, currentPct * 100 - onePct * 10 - twoPct * 40)));
4595
+ const riskLevel = score >= 70 ? "low" : score >= 40 ? "moderate" : "high";
4596
+ return { score, riskLevel };
4597
+ }
4598
+ function mapToolingToLayers(tooling, services, depsByLayer) {
4599
+ const layerTooling = /* @__PURE__ */ new Map();
4600
+ const layerServices = /* @__PURE__ */ new Map();
4601
+ const pkgLayerLookup = /* @__PURE__ */ new Map();
4602
+ for (const [layer, packages] of depsByLayer) {
4603
+ for (const pkg2 of packages) {
4604
+ pkgLayerLookup.set(pkg2, layer);
4605
+ }
4606
+ }
4607
+ if (tooling) {
4608
+ for (const [, items] of Object.entries(tooling)) {
4609
+ for (const item of items) {
4610
+ const layer = pkgLayerLookup.get(item.package) ?? PACKAGE_LAYER_MAP[item.package] ?? "shared";
4611
+ if (!layerTooling.has(layer)) layerTooling.set(layer, []);
4612
+ const existing = layerTooling.get(layer);
4613
+ if (!existing.some((t) => t.package === item.package)) {
4614
+ existing.push(item);
4615
+ }
4616
+ }
4617
+ }
4618
+ }
4619
+ if (services) {
4620
+ for (const [, items] of Object.entries(services)) {
4621
+ for (const item of items) {
4622
+ const layer = pkgLayerLookup.get(item.package) ?? PACKAGE_LAYER_MAP[item.package] ?? "infrastructure";
4623
+ if (!layerServices.has(layer)) layerServices.set(layer, []);
4624
+ const existing = layerServices.get(layer);
4625
+ if (!existing.some((s) => s.package === item.package)) {
4626
+ existing.push(item);
4627
+ }
4628
+ }
4629
+ }
4630
+ }
4631
+ return { layerTooling, layerServices };
4632
+ }
4633
+ async function scanArchitecture(rootDir, projects, tooling, services, cache) {
4634
+ const { archetype, confidence: archetypeConfidence } = detectArchetype(projects);
4635
+ const sourceFiles = await walkSourceFiles(rootDir, cache);
4636
+ const classifications = [];
4637
+ let unclassified = 0;
4638
+ for (const file of sourceFiles) {
4639
+ const classification = classifyFile(file, archetype);
4640
+ if (classification) {
4641
+ classifications.push(classification);
4642
+ } else {
4643
+ unclassified++;
4644
+ }
4645
+ }
4646
+ const allDeps = /* @__PURE__ */ new Map();
4647
+ for (const p of projects) {
4648
+ for (const d of p.dependencies) {
4649
+ if (!allDeps.has(d.package)) {
4650
+ allDeps.set(d.package, d);
4651
+ }
4652
+ }
4653
+ }
4654
+ const depsByLayer = /* @__PURE__ */ new Map();
4655
+ for (const [pkg2] of allDeps) {
4656
+ const layer = PACKAGE_LAYER_MAP[pkg2];
4657
+ if (layer) {
4658
+ if (!depsByLayer.has(layer)) depsByLayer.set(layer, /* @__PURE__ */ new Set());
4659
+ depsByLayer.get(layer).add(pkg2);
4660
+ }
4661
+ }
4662
+ const { layerTooling, layerServices } = mapToolingToLayers(tooling, services, depsByLayer);
4663
+ const ALL_LAYERS = [
4664
+ "routing",
4665
+ "middleware",
4666
+ "services",
4667
+ "domain",
4668
+ "data-access",
4669
+ "infrastructure",
4670
+ "presentation",
4671
+ "config",
4672
+ "testing",
4673
+ "shared"
4674
+ ];
4675
+ const layerFileCounts = /* @__PURE__ */ new Map();
4676
+ for (const c of classifications) {
4677
+ layerFileCounts.set(c.layer, (layerFileCounts.get(c.layer) ?? 0) + 1);
4678
+ }
4679
+ const layers = [];
4680
+ for (const layer of ALL_LAYERS) {
4681
+ const fileCount = layerFileCounts.get(layer) ?? 0;
4682
+ const layerPkgs = depsByLayer.get(layer) ?? /* @__PURE__ */ new Set();
4683
+ const tech = layerTooling.get(layer) ?? [];
4684
+ const svc = layerServices.get(layer) ?? [];
4685
+ if (fileCount === 0 && layerPkgs.size === 0 && tech.length === 0 && svc.length === 0) {
4686
+ continue;
4687
+ }
4688
+ const packages = [];
4689
+ for (const pkg2 of layerPkgs) {
4690
+ const dep = allDeps.get(pkg2);
4691
+ if (dep) {
4692
+ packages.push({
4693
+ name: dep.package,
4694
+ version: dep.resolvedVersion,
4695
+ latestStable: dep.latestStable,
4696
+ majorsBehind: dep.majorsBehind,
4697
+ drift: dep.drift
4698
+ });
4699
+ }
4700
+ }
4701
+ const { score, riskLevel } = computeLayerDrift(packages);
4702
+ layers.push({
4703
+ layer,
4704
+ fileCount,
4705
+ driftScore: score,
4706
+ riskLevel,
4707
+ techStack: tech,
4708
+ services: svc,
4709
+ packages
4710
+ });
4711
+ }
4712
+ const LAYER_ORDER = {
4713
+ "presentation": 0,
4714
+ "routing": 1,
4715
+ "middleware": 2,
4716
+ "services": 3,
4717
+ "domain": 4,
4718
+ "data-access": 5,
4719
+ "infrastructure": 6,
4720
+ "config": 7,
4721
+ "shared": 8,
4722
+ "testing": 9
4723
+ };
4724
+ layers.sort((a, b) => (LAYER_ORDER[a.layer] ?? 99) - (LAYER_ORDER[b.layer] ?? 99));
4725
+ return {
4726
+ archetype,
4727
+ archetypeConfidence,
4728
+ layers,
4729
+ totalClassified: classifications.length,
4730
+ unclassified
4731
+ };
4732
+ }
4733
+
3912
4734
  // src/commands/scan.ts
3913
4735
  async function runScan(rootDir, opts) {
3914
4736
  const scanStart = Date.now();
3915
4737
  const config = await loadConfig(rootDir);
3916
4738
  const sem = new Semaphore(opts.concurrency);
3917
4739
  const npmCache = new NpmCache(rootDir, sem);
4740
+ const fileCache = new FileCache();
3918
4741
  const scanners = config.scanners;
3919
4742
  let filesScanned = 0;
3920
4743
  const progress = new ScanProgress(rootDir);
@@ -3933,7 +4756,8 @@ async function runScan(rootDir, opts) {
3933
4756
  ...scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
3934
4757
  ...scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
3935
4758
  ...scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
3936
- ...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : []
4759
+ ...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
4760
+ ...scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : []
3937
4761
  ] : [],
3938
4762
  { id: "drift", label: "Computing drift score" },
3939
4763
  { id: "findings", label: "Generating findings" }
@@ -3945,7 +4769,7 @@ async function runScan(rootDir, opts) {
3945
4769
  const vcsDetail = vcs.type !== "unknown" ? `${vcs.type}${vcs.branch ? ` ${vcs.branch}` : ""}${vcs.shortSha ? ` @ ${vcs.shortSha}` : ""}` : "none detected";
3946
4770
  progress.completeStep("vcs", vcsDetail);
3947
4771
  progress.startStep("node");
3948
- const nodeProjects = await scanNodeProjects(rootDir, npmCache);
4772
+ const nodeProjects = await scanNodeProjects(rootDir, npmCache, fileCache);
3949
4773
  for (const p of nodeProjects) {
3950
4774
  progress.addDependencies(p.dependencies.length);
3951
4775
  progress.addFrameworks(p.frameworks.length);
@@ -3954,7 +4778,7 @@ async function runScan(rootDir, opts) {
3954
4778
  progress.addProjects(nodeProjects.length);
3955
4779
  progress.completeStep("node", `${nodeProjects.length} project${nodeProjects.length !== 1 ? "s" : ""}`, nodeProjects.length);
3956
4780
  progress.startStep("dotnet");
3957
- const dotnetProjects = await scanDotnetProjects(rootDir);
4781
+ const dotnetProjects = await scanDotnetProjects(rootDir, fileCache);
3958
4782
  for (const p of dotnetProjects) {
3959
4783
  progress.addDependencies(p.dependencies.length);
3960
4784
  progress.addFrameworks(p.frameworks.length);
@@ -3976,7 +4800,7 @@ async function runScan(rootDir, opts) {
3976
4800
  if (scanners?.platformMatrix?.enabled !== false) {
3977
4801
  progress.startStep("platform");
3978
4802
  scannerTasks.push(
3979
- scanPlatformMatrix(rootDir).then((result) => {
4803
+ scanPlatformMatrix(rootDir, fileCache).then((result) => {
3980
4804
  extended.platformMatrix = result;
3981
4805
  const nativeCount = result.nativeModules.length;
3982
4806
  const dockerCount = result.dockerBaseImages.length;
@@ -4025,7 +4849,7 @@ async function runScan(rootDir, opts) {
4025
4849
  if (scanners?.securityPosture?.enabled !== false) {
4026
4850
  progress.startStep("security");
4027
4851
  scannerTasks.push(
4028
- scanSecurityPosture(rootDir).then((result) => {
4852
+ scanSecurityPosture(rootDir, fileCache).then((result) => {
4029
4853
  extended.securityPosture = result;
4030
4854
  const secDetail = result.lockfilePresent ? `lockfile \u2714${result.gitignoreCoversEnv ? " \xB7 .env \u2714" : " \xB7 .env \u2716"}` : "no lockfile";
4031
4855
  progress.completeStep("security", secDetail);
@@ -4035,7 +4859,7 @@ async function runScan(rootDir, opts) {
4035
4859
  if (scanners?.buildDeploy?.enabled !== false) {
4036
4860
  progress.startStep("build");
4037
4861
  scannerTasks.push(
4038
- scanBuildDeploy(rootDir).then((result) => {
4862
+ scanBuildDeploy(rootDir, fileCache).then((result) => {
4039
4863
  extended.buildDeploy = result;
4040
4864
  const bdParts = [];
4041
4865
  if (result.ci.length > 0) bdParts.push(result.ci.join(", "));
@@ -4047,7 +4871,7 @@ async function runScan(rootDir, opts) {
4047
4871
  if (scanners?.tsModernity?.enabled !== false) {
4048
4872
  progress.startStep("ts");
4049
4873
  scannerTasks.push(
4050
- scanTsModernity(rootDir).then((result) => {
4874
+ scanTsModernity(rootDir, fileCache).then((result) => {
4051
4875
  extended.tsModernity = result;
4052
4876
  const tsParts = [];
4053
4877
  if (result.typescriptVersion) tsParts.push(`v${result.typescriptVersion}`);
@@ -4060,7 +4884,7 @@ async function runScan(rootDir, opts) {
4060
4884
  if (scanners?.fileHotspots?.enabled !== false) {
4061
4885
  progress.startStep("hotspots");
4062
4886
  scannerTasks.push(
4063
- scanFileHotspots(rootDir).then((result) => {
4887
+ scanFileHotspots(rootDir, fileCache).then((result) => {
4064
4888
  extended.fileHotspots = result;
4065
4889
  progress.completeStep("hotspots", `${result.totalFiles} files`, result.totalFiles);
4066
4890
  })
@@ -4069,7 +4893,7 @@ async function runScan(rootDir, opts) {
4069
4893
  if (scanners?.dependencyGraph?.enabled !== false) {
4070
4894
  progress.startStep("depgraph");
4071
4895
  scannerTasks.push(
4072
- scanDependencyGraph(rootDir).then((result) => {
4896
+ scanDependencyGraph(rootDir, fileCache).then((result) => {
4073
4897
  extended.dependencyGraph = result;
4074
4898
  const dgDetail = result.lockfileType ? `${result.lockfileType} \xB7 ${result.totalUnique} unique` : "no lockfile";
4075
4899
  progress.completeStep("depgraph", dgDetail, result.totalUnique);
@@ -4090,6 +4914,23 @@ async function runScan(rootDir, opts) {
4090
4914
  );
4091
4915
  }
4092
4916
  await Promise.all(scannerTasks);
4917
+ if (scanners?.architecture?.enabled !== false) {
4918
+ progress.startStep("architecture");
4919
+ extended.architecture = await scanArchitecture(
4920
+ rootDir,
4921
+ allProjects,
4922
+ extended.toolingInventory,
4923
+ extended.serviceDependencies,
4924
+ fileCache
4925
+ );
4926
+ const arch = extended.architecture;
4927
+ const layerCount = arch.layers.filter((l) => l.fileCount > 0).length;
4928
+ progress.completeStep(
4929
+ "architecture",
4930
+ `${arch.archetype} \xB7 ${layerCount} layer${layerCount !== 1 ? "s" : ""} \xB7 ${arch.totalClassified} files`,
4931
+ layerCount
4932
+ );
4933
+ }
4093
4934
  }
4094
4935
  progress.startStep("drift");
4095
4936
  const drift = computeDriftScore(allProjects);
@@ -4106,6 +4947,7 @@ async function runScan(rootDir, opts) {
4106
4947
  if (noteCount > 0) findingParts.push(`${noteCount} note${noteCount !== 1 ? "s" : ""}`);
4107
4948
  progress.completeStep("findings", findingParts.join(", ") || "none");
4108
4949
  progress.finish();
4950
+ fileCache.clear();
4109
4951
  if (allProjects.length === 0) {
4110
4952
  console.log(chalk5.yellow("No projects found."));
4111
4953
  }
@@ -4122,7 +4964,7 @@ async function runScan(rootDir, opts) {
4122
4964
  schemaVersion: "1.0",
4123
4965
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4124
4966
  vibgrateVersion: VERSION,
4125
- rootPath: path14.basename(rootDir),
4967
+ rootPath: path15.basename(rootDir),
4126
4968
  ...vcs.type !== "unknown" ? { vcs } : {},
4127
4969
  projects: allProjects,
4128
4970
  drift,
@@ -4132,7 +4974,7 @@ async function runScan(rootDir, opts) {
4132
4974
  filesScanned
4133
4975
  };
4134
4976
  if (opts.baseline) {
4135
- const baselinePath = path14.resolve(opts.baseline);
4977
+ const baselinePath = path15.resolve(opts.baseline);
4136
4978
  if (await pathExists(baselinePath)) {
4137
4979
  try {
4138
4980
  const baseline = await readJsonFile(baselinePath);
@@ -4143,15 +4985,15 @@ async function runScan(rootDir, opts) {
4143
4985
  }
4144
4986
  }
4145
4987
  }
4146
- const vibgrateDir = path14.join(rootDir, ".vibgrate");
4988
+ const vibgrateDir = path15.join(rootDir, ".vibgrate");
4147
4989
  await ensureDir(vibgrateDir);
4148
- await writeJsonFile(path14.join(vibgrateDir, "scan_result.json"), artifact);
4990
+ await writeJsonFile(path15.join(vibgrateDir, "scan_result.json"), artifact);
4149
4991
  for (const project of allProjects) {
4150
4992
  if (project.drift && project.path) {
4151
- const projectDir = path14.resolve(rootDir, project.path);
4152
- const projectVibgrateDir = path14.join(projectDir, ".vibgrate");
4993
+ const projectDir = path15.resolve(rootDir, project.path);
4994
+ const projectVibgrateDir = path15.join(projectDir, ".vibgrate");
4153
4995
  await ensureDir(projectVibgrateDir);
4154
- await writeJsonFile(path14.join(projectVibgrateDir, "project_score.json"), {
4996
+ await writeJsonFile(path15.join(projectVibgrateDir, "project_score.json"), {
4155
4997
  projectId: project.projectId,
4156
4998
  name: project.name,
4157
4999
  type: project.type,
@@ -4168,7 +5010,7 @@ async function runScan(rootDir, opts) {
4168
5010
  if (opts.format === "json") {
4169
5011
  const jsonStr = JSON.stringify(artifact, null, 2);
4170
5012
  if (opts.out) {
4171
- await writeTextFile(path14.resolve(opts.out), jsonStr);
5013
+ await writeTextFile(path15.resolve(opts.out), jsonStr);
4172
5014
  console.log(chalk5.green("\u2714") + ` JSON written to ${opts.out}`);
4173
5015
  } else {
4174
5016
  console.log(jsonStr);
@@ -4177,7 +5019,7 @@ async function runScan(rootDir, opts) {
4177
5019
  const sarif = formatSarif(artifact);
4178
5020
  const sarifStr = JSON.stringify(sarif, null, 2);
4179
5021
  if (opts.out) {
4180
- await writeTextFile(path14.resolve(opts.out), sarifStr);
5022
+ await writeTextFile(path15.resolve(opts.out), sarifStr);
4181
5023
  console.log(chalk5.green("\u2714") + ` SARIF written to ${opts.out}`);
4182
5024
  } else {
4183
5025
  console.log(sarifStr);
@@ -4186,7 +5028,7 @@ async function runScan(rootDir, opts) {
4186
5028
  const text = formatText(artifact);
4187
5029
  console.log(text);
4188
5030
  if (opts.out) {
4189
- await writeTextFile(path14.resolve(opts.out), text);
5031
+ await writeTextFile(path15.resolve(opts.out), text);
4190
5032
  }
4191
5033
  }
4192
5034
  return artifact;
@@ -4245,7 +5087,7 @@ async function autoPush(artifact, rootDir, opts) {
4245
5087
  }
4246
5088
  }
4247
5089
  var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").action(async (targetPath, opts) => {
4248
- const rootDir = path14.resolve(targetPath);
5090
+ const rootDir = path15.resolve(targetPath);
4249
5091
  if (!await pathExists(rootDir)) {
4250
5092
  console.error(chalk5.red(`Path does not exist: ${rootDir}`));
4251
5093
  process.exit(1);