@vibgrate/cli 1.0.13 → 1.0.15

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.
package/DOCS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vibgrate CLI — Full Documentation
2
2
 
3
- > Continuous Upgrade Drift Intelligence for Node & .NET
3
+ > Continuous Drift Intelligence for Node & .NET
4
4
 
5
5
  For a quick overview, see the [README](./README.md). This document covers everything in detail.
6
6
 
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  baselineCommand,
3
3
  runBaseline
4
- } from "./chunk-QHBZIYWP.js";
5
- import "./chunk-S4C6KWCP.js";
4
+ } from "./chunk-OPEOSIRY.js";
5
+ import "./chunk-QZV77UWV.js";
6
6
  export {
7
7
  baselineCommand,
8
8
  runBaseline
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  runScan,
3
3
  writeJsonFile
4
- } from "./chunk-S4C6KWCP.js";
4
+ } from "./chunk-QZV77UWV.js";
5
5
 
6
6
  // src/commands/baseline.ts
7
7
  import * as path from "path";
@@ -1,6 +1,38 @@
1
1
  // src/utils/fs.ts
2
2
  import * as fs from "fs/promises";
3
+ import * as os from "os";
3
4
  import * as path from "path";
5
+
6
+ // src/utils/semaphore.ts
7
+ var Semaphore = class {
8
+ available;
9
+ queue = [];
10
+ constructor(max) {
11
+ this.available = max;
12
+ }
13
+ async run(fn) {
14
+ await this.acquire();
15
+ try {
16
+ return await fn();
17
+ } finally {
18
+ this.release();
19
+ }
20
+ }
21
+ acquire() {
22
+ if (this.available > 0) {
23
+ this.available--;
24
+ return Promise.resolve();
25
+ }
26
+ return new Promise((resolve6) => this.queue.push(resolve6));
27
+ }
28
+ release() {
29
+ const next = this.queue.shift();
30
+ if (next) next();
31
+ else this.available++;
32
+ }
33
+ };
34
+
35
+ // src/utils/fs.ts
4
36
  var SKIP_DIRS = /* @__PURE__ */ new Set([
5
37
  "node_modules",
6
38
  ".git",
@@ -20,28 +52,27 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
20
52
  ]);
21
53
  async function findFiles(rootDir, predicate) {
22
54
  const results = [];
55
+ const maxConcurrentReads = Math.max(8, Math.min(64, os.availableParallelism() * 4));
56
+ const readDirSemaphore = new Semaphore(maxConcurrentReads);
23
57
  async function walk(dir) {
24
58
  let entries;
25
59
  try {
26
- const dirents = await fs.readdir(dir, { withFileTypes: true });
27
- entries = dirents.map((d) => ({
28
- name: d.name,
29
- isDirectory: d.isDirectory(),
30
- isFile: d.isFile()
31
- }));
60
+ entries = await fs.readdir(dir, { withFileTypes: true });
32
61
  } catch {
33
62
  return;
34
63
  }
64
+ const subDirectoryWalks = [];
35
65
  for (const e of entries) {
36
- if (e.isDirectory) {
66
+ if (e.isDirectory()) {
37
67
  if (SKIP_DIRS.has(e.name)) continue;
38
- await walk(path.join(dir, e.name));
39
- } else if (e.isFile && predicate(e.name)) {
68
+ subDirectoryWalks.push(readDirSemaphore.run(() => walk(path.join(dir, e.name))));
69
+ } else if (e.isFile() && predicate(e.name)) {
40
70
  results.push(path.join(dir, e.name));
41
71
  }
42
72
  }
73
+ await Promise.all(subDirectoryWalks);
43
74
  }
44
- await walk(rootDir);
75
+ await readDirSemaphore.run(() => walk(rootDir));
45
76
  return results;
46
77
  }
47
78
  async function findPackageJsonFiles(rootDir) {
@@ -156,12 +187,12 @@ function eolScore(projects) {
156
187
  }
157
188
  function computeDriftScore(projects) {
158
189
  const rs = runtimeScore(projects);
159
- const fs5 = frameworkScore(projects);
190
+ const fs6 = frameworkScore(projects);
160
191
  const ds = dependencyScore(projects);
161
192
  const es = eolScore(projects);
162
193
  const components = [
163
194
  { score: rs, weight: 0.25 },
164
- { score: fs5, weight: 0.25 },
195
+ { score: fs6, weight: 0.25 },
165
196
  { score: ds, weight: 0.3 },
166
197
  { score: es, weight: 0.2 }
167
198
  ];
@@ -172,7 +203,7 @@ function computeDriftScore(projects) {
172
203
  riskLevel: "low",
173
204
  components: {
174
205
  runtimeScore: Math.round(rs ?? 100),
175
- frameworkScore: Math.round(fs5 ?? 100),
206
+ frameworkScore: Math.round(fs6 ?? 100),
176
207
  dependencyScore: Math.round(ds ?? 100),
177
208
  eolScore: Math.round(es ?? 100)
178
209
  }
@@ -190,7 +221,7 @@ function computeDriftScore(projects) {
190
221
  else riskLevel = "high";
191
222
  const measured = [];
192
223
  if (rs !== null) measured.push("runtime");
193
- if (fs5 !== null) measured.push("framework");
224
+ if (fs6 !== null) measured.push("framework");
194
225
  if (ds !== null) measured.push("dependency");
195
226
  if (es !== null) measured.push("eol");
196
227
  return {
@@ -198,7 +229,7 @@ function computeDriftScore(projects) {
198
229
  riskLevel,
199
230
  components: {
200
231
  runtimeScore: Math.round(rs ?? 100),
201
- frameworkScore: Math.round(fs5 ?? 100),
232
+ frameworkScore: Math.round(fs6 ?? 100),
202
233
  dependencyScore: Math.round(ds ?? 100),
203
234
  eolScore: Math.round(es ?? 100)
204
235
  },
@@ -554,6 +585,122 @@ function formatExtended(ext) {
554
585
  lines.push("");
555
586
  }
556
587
  }
588
+ if (ext.architecture) {
589
+ lines.push(...formatArchitectureDiagram(ext.architecture));
590
+ }
591
+ return lines;
592
+ }
593
+ var LAYER_LABELS = {
594
+ "presentation": "Presentation",
595
+ "routing": "Routing",
596
+ "middleware": "Middleware",
597
+ "services": "Services",
598
+ "domain": "Domain",
599
+ "data-access": "Data Access",
600
+ "infrastructure": "Infrastructure",
601
+ "config": "Config",
602
+ "shared": "Shared",
603
+ "testing": "Testing"
604
+ };
605
+ var LAYER_ICONS = {
606
+ "presentation": "\u{1F5A5}",
607
+ "routing": "\u{1F500}",
608
+ "middleware": "\u{1F517}",
609
+ "services": "\u2699",
610
+ "domain": "\u{1F48E}",
611
+ "data-access": "\u{1F5C4}",
612
+ "infrastructure": "\u{1F3D7}",
613
+ "config": "\u2699",
614
+ "shared": "\u{1F4E6}",
615
+ "testing": "\u{1F9EA}"
616
+ };
617
+ function formatArchitectureDiagram(arch) {
618
+ const lines = [];
619
+ 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"));
620
+ lines.push(chalk.bold.cyan("\u2551 Architecture Layers \u2551"));
621
+ 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"));
622
+ lines.push("");
623
+ const archetypeDisplay = arch.archetype === "unknown" ? "Unknown" : arch.archetype;
624
+ const confPct = Math.round(arch.archetypeConfidence * 100);
625
+ lines.push(chalk.bold(" Archetype: ") + chalk.cyan.bold(archetypeDisplay) + chalk.dim(` (${confPct}% confidence)`));
626
+ lines.push(chalk.dim(` ${arch.totalClassified} files classified \xB7 ${arch.unclassified} unclassified`));
627
+ lines.push("");
628
+ const boxWidth = 44;
629
+ const visibleLayers = arch.layers.filter((l) => l.fileCount > 0 || l.techStack.length > 0 || l.services.length > 0);
630
+ if (visibleLayers.length === 0) {
631
+ lines.push(chalk.dim(" No layers detected"));
632
+ lines.push("");
633
+ return lines;
634
+ }
635
+ for (let i = 0; i < visibleLayers.length; i++) {
636
+ const layer = visibleLayers[i];
637
+ const icon = LAYER_ICONS[layer.layer] ?? "\xB7";
638
+ const label = LAYER_LABELS[layer.layer] ?? layer.layer;
639
+ const scoreColor = layer.driftScore >= 70 ? chalk.green : layer.driftScore >= 40 ? chalk.yellow : chalk.red;
640
+ const riskBadgeStr = layer.riskLevel === "low" ? chalk.bgGreen.black(" LOW ") : layer.riskLevel === "moderate" ? chalk.bgYellow.black(" MOD ") : chalk.bgRed.white(" HIGH ");
641
+ if (i === 0) {
642
+ lines.push(chalk.cyan(` \u250C${"\u2500".repeat(boxWidth)}\u2510`));
643
+ }
644
+ const nameStr = `${icon} ${label}`;
645
+ const scoreStr = `${layer.driftScore}/100`;
646
+ const fileSuffix = `${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""}`;
647
+ const leftContent = ` ${nameStr}`;
648
+ const rightContent = `${fileSuffix} ${scoreStr} `;
649
+ const leftLen = nameStr.length + 2;
650
+ const rightLen = rightContent.length;
651
+ const padLen = Math.max(1, boxWidth - leftLen - rightLen);
652
+ lines.push(
653
+ chalk.cyan(" \u2502") + ` ${icon} ${chalk.bold(label)}` + " ".repeat(padLen) + chalk.dim(fileSuffix) + " " + scoreColor.bold(scoreStr) + " " + chalk.cyan("\u2502")
654
+ );
655
+ const barWidth = boxWidth - 8;
656
+ const filled = Math.round(layer.driftScore / 100 * barWidth);
657
+ const empty = barWidth - filled;
658
+ lines.push(
659
+ chalk.cyan(" \u2502") + " " + scoreColor("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty)) + " " + chalk.cyan("\u2502")
660
+ );
661
+ if (layer.techStack.length > 0) {
662
+ const techNames = layer.techStack.slice(0, 6).map((t) => t.name);
663
+ const moreCount = layer.techStack.length > 6 ? ` +${layer.techStack.length - 6}` : "";
664
+ const techLine = `Tech: ${techNames.join(", ")}${moreCount}`;
665
+ const truncated = techLine.length > boxWidth - 6 ? techLine.slice(0, boxWidth - 9) + "..." : techLine;
666
+ const techPad = Math.max(0, boxWidth - truncated.length - 4);
667
+ lines.push(
668
+ chalk.cyan(" \u2502") + " " + chalk.dim(truncated) + " ".repeat(techPad) + chalk.cyan("\u2502")
669
+ );
670
+ }
671
+ if (layer.services.length > 0) {
672
+ const svcNames = layer.services.slice(0, 5).map((s) => s.name);
673
+ const moreCount = layer.services.length > 5 ? ` +${layer.services.length - 5}` : "";
674
+ const svcLine = `Services: ${svcNames.join(", ")}${moreCount}`;
675
+ const truncated = svcLine.length > boxWidth - 6 ? svcLine.slice(0, boxWidth - 9) + "..." : svcLine;
676
+ const svcPad = Math.max(0, boxWidth - truncated.length - 4);
677
+ lines.push(
678
+ chalk.cyan(" \u2502") + " " + chalk.dim(truncated) + " ".repeat(svcPad) + chalk.cyan("\u2502")
679
+ );
680
+ }
681
+ const driftPkgs = layer.packages.filter((p) => p.majorsBehind !== null && p.majorsBehind > 0);
682
+ if (driftPkgs.length > 0) {
683
+ const worst = driftPkgs.sort((a, b) => (b.majorsBehind ?? 0) - (a.majorsBehind ?? 0));
684
+ const shown = worst.slice(0, 3);
685
+ const pkgStrs = shown.map((p) => {
686
+ const color = (p.majorsBehind ?? 0) >= 2 ? chalk.red : chalk.yellow;
687
+ return color(`${p.name} -${p.majorsBehind}`);
688
+ });
689
+ const moreCount = worst.length > 3 ? chalk.dim(` +${worst.length - 3}`) : "";
690
+ const pkgLine = pkgStrs.join(chalk.dim(", ")) + moreCount;
691
+ const roughLen = shown.map((p) => `${p.name} -${p.majorsBehind}`).join(", ").length + (worst.length > 3 ? ` +${worst.length - 3}`.length : 0);
692
+ const pkgPad = Math.max(0, boxWidth - roughLen - 4);
693
+ lines.push(
694
+ chalk.cyan(" \u2502") + " " + pkgLine + " ".repeat(pkgPad) + chalk.cyan("\u2502")
695
+ );
696
+ }
697
+ if (i < visibleLayers.length - 1) {
698
+ lines.push(chalk.cyan(` \u251C${"\u2500".repeat(boxWidth)}\u2524`));
699
+ } else {
700
+ lines.push(chalk.cyan(` \u2514${"\u2500".repeat(boxWidth)}\u2518`));
701
+ }
702
+ }
703
+ lines.push("");
557
704
  return lines;
558
705
  }
559
706
  function generatePriorityActions(artifact) {
@@ -1019,7 +1166,7 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
1019
1166
  });
1020
1167
 
1021
1168
  // src/commands/scan.ts
1022
- import * as path14 from "path";
1169
+ import * as path15 from "path";
1023
1170
  import { Command as Command3 } from "commander";
1024
1171
  import chalk5 from "chalk";
1025
1172
 
@@ -1620,35 +1767,6 @@ async function scanOneCsproj(csprojPath, rootDir) {
1620
1767
  };
1621
1768
  }
1622
1769
 
1623
- // src/utils/semaphore.ts
1624
- var Semaphore = class {
1625
- available;
1626
- queue = [];
1627
- constructor(max) {
1628
- this.available = max;
1629
- }
1630
- async run(fn) {
1631
- await this.acquire();
1632
- try {
1633
- return await fn();
1634
- } finally {
1635
- this.release();
1636
- }
1637
- }
1638
- acquire() {
1639
- if (this.available > 0) {
1640
- this.available--;
1641
- return Promise.resolve();
1642
- }
1643
- return new Promise((resolve6) => this.queue.push(resolve6));
1644
- }
1645
- release() {
1646
- const next = this.queue.shift();
1647
- if (next) next();
1648
- else this.available++;
1649
- }
1650
- };
1651
-
1652
1770
  // src/config.ts
1653
1771
  import * as path6 from "path";
1654
1772
  import * as fs2 from "fs/promises";
@@ -3907,6 +4025,528 @@ function scanServiceDependencies(projects) {
3907
4025
  return result;
3908
4026
  }
3909
4027
 
4028
+ // src/scanners/architecture.ts
4029
+ import * as path14 from "path";
4030
+ import * as fs5 from "fs/promises";
4031
+ var ARCHETYPE_SIGNALS = [
4032
+ // Meta-frameworks (highest priority — they imply routing patterns)
4033
+ { packages: ["next", "@next/core"], archetype: "nextjs", weight: 10 },
4034
+ { packages: ["@remix-run/react", "@remix-run/node", "@remix-run/dev"], archetype: "remix", weight: 10 },
4035
+ { packages: ["@sveltejs/kit"], archetype: "sveltekit", weight: 10 },
4036
+ { packages: ["nuxt"], archetype: "nuxt", weight: 10 },
4037
+ // Backend frameworks
4038
+ { packages: ["@nestjs/core", "@nestjs/common"], archetype: "nestjs", weight: 9 },
4039
+ { packages: ["fastify"], archetype: "fastify", weight: 8 },
4040
+ { packages: ["hono"], archetype: "hono", weight: 8 },
4041
+ { packages: ["koa"], archetype: "koa", weight: 8 },
4042
+ { packages: ["express"], archetype: "express", weight: 7 },
4043
+ // Serverless
4044
+ { packages: ["serverless", "aws-lambda", "@aws-sdk/client-lambda", "middy", "@cloudflare/workers-types"], archetype: "serverless", weight: 6 },
4045
+ // CLI
4046
+ { packages: ["commander", "yargs", "meow", "cac", "clipanion", "oclif"], archetype: "cli", weight: 5 }
4047
+ ];
4048
+ function detectArchetype(projects) {
4049
+ const allPackages = /* @__PURE__ */ new Set();
4050
+ for (const p of projects) {
4051
+ for (const d of p.dependencies) {
4052
+ allPackages.add(d.package);
4053
+ }
4054
+ }
4055
+ if (projects.length > 2) {
4056
+ return { archetype: "monorepo", confidence: 0.8 };
4057
+ }
4058
+ let bestArchetype = "unknown";
4059
+ let bestScore = 0;
4060
+ for (const signal of ARCHETYPE_SIGNALS) {
4061
+ const matched = signal.packages.filter((p) => allPackages.has(p)).length;
4062
+ if (matched > 0) {
4063
+ const score = matched * signal.weight;
4064
+ if (score > bestScore) {
4065
+ bestScore = score;
4066
+ bestArchetype = signal.archetype;
4067
+ }
4068
+ }
4069
+ }
4070
+ if (bestArchetype === "unknown") {
4071
+ bestArchetype = "library";
4072
+ bestScore = 3;
4073
+ }
4074
+ const confidence = Math.min(bestScore / 15, 1);
4075
+ return { archetype: bestArchetype, confidence: Math.round(confidence * 100) / 100 };
4076
+ }
4077
+ var PATH_RULES = [
4078
+ // ── Testing (high precision) ──
4079
+ { pattern: /\/__tests__\//, layer: "testing", confidence: 0.95, signal: "__tests__ directory" },
4080
+ { pattern: /\.test\.[jt]sx?$/, layer: "testing", confidence: 0.95, signal: ".test.* file" },
4081
+ { pattern: /\.spec\.[jt]sx?$/, layer: "testing", confidence: 0.95, signal: ".spec.* file" },
4082
+ { pattern: /\/test\//, layer: "testing", confidence: 0.85, signal: "test/ directory" },
4083
+ { pattern: /\/tests\//, layer: "testing", confidence: 0.85, signal: "tests/ directory" },
4084
+ { pattern: /\/__mocks__\//, layer: "testing", confidence: 0.9, signal: "__mocks__ directory" },
4085
+ { pattern: /\/fixtures\//, layer: "testing", confidence: 0.8, signal: "fixtures/ directory" },
4086
+ // ── Config/Infrastructure (high precision) ──
4087
+ { pattern: /\/config\.[jt]sx?$/, layer: "config", confidence: 0.85, signal: "config.* file" },
4088
+ { pattern: /\/config\//, layer: "config", confidence: 0.8, signal: "config/ directory" },
4089
+ { pattern: /\.config\.[jt]sx?$/, layer: "config", confidence: 0.9, signal: ".config.* file" },
4090
+ { pattern: /\/env\.[jt]sx?$/, layer: "config", confidence: 0.85, signal: "env.* file" },
4091
+ { pattern: /\/bootstrap\.[jt]sx?$/, layer: "config", confidence: 0.85, signal: "bootstrap file" },
4092
+ { pattern: /\/setup\.[jt]sx?$/, layer: "config", confidence: 0.8, signal: "setup file" },
4093
+ // ── Next.js (archetype-specific) ──
4094
+ { pattern: /(^|\/)app\/.*\/route\.[jt]sx?$/, layer: "routing", confidence: 0.95, signal: "Next.js App Router route", archetypes: ["nextjs"] },
4095
+ { pattern: /(^|\/)pages\/api\//, layer: "routing", confidence: 0.95, signal: "Next.js Pages API route", archetypes: ["nextjs"] },
4096
+ { pattern: /(^|\/)app\/.*page\.[jt]sx?$/, layer: "presentation", confidence: 0.9, signal: "Next.js page component", archetypes: ["nextjs"] },
4097
+ { pattern: /(^|\/)app\/.*layout\.[jt]sx?$/, layer: "presentation", confidence: 0.9, signal: "Next.js layout component", archetypes: ["nextjs"] },
4098
+ { pattern: /(^|\/)app\/.*loading\.[jt]sx?$/, layer: "presentation", confidence: 0.85, signal: "Next.js loading component", archetypes: ["nextjs"] },
4099
+ { pattern: /(^|\/)app\/.*error\.[jt]sx?$/, layer: "presentation", confidence: 0.85, signal: "Next.js error component", archetypes: ["nextjs"] },
4100
+ { pattern: /(^|\/)middleware\.[jt]sx?$/, layer: "middleware", confidence: 0.9, signal: "Next.js middleware", archetypes: ["nextjs"] },
4101
+ // ── Remix (archetype-specific) ──
4102
+ { pattern: /\/app\/routes\//, layer: "routing", confidence: 0.95, signal: "Remix route file", archetypes: ["remix"] },
4103
+ { pattern: /\/app\/root\.[jt]sx?$/, layer: "presentation", confidence: 0.9, signal: "Remix root", archetypes: ["remix"] },
4104
+ // ── SvelteKit (archetype-specific) ──
4105
+ { pattern: /\/src\/routes\/.*\+server\.[jt]s$/, layer: "routing", confidence: 0.95, signal: "SvelteKit API route", archetypes: ["sveltekit"] },
4106
+ { pattern: /\/src\/routes\/.*\+page\.svelte$/, layer: "presentation", confidence: 0.9, signal: "SvelteKit page", archetypes: ["sveltekit"] },
4107
+ { pattern: /\/src\/routes\/.*\+layout\.svelte$/, layer: "presentation", confidence: 0.9, signal: "SvelteKit layout", archetypes: ["sveltekit"] },
4108
+ { pattern: /\/src\/hooks\.server\.[jt]s$/, layer: "middleware", confidence: 0.9, signal: "SvelteKit server hooks", archetypes: ["sveltekit"] },
4109
+ // ── Nuxt (archetype-specific) ──
4110
+ { pattern: /\/server\/api\//, layer: "routing", confidence: 0.95, signal: "Nuxt server API", archetypes: ["nuxt"] },
4111
+ { pattern: /\/server\/routes\//, layer: "routing", confidence: 0.95, signal: "Nuxt server route", archetypes: ["nuxt"] },
4112
+ { pattern: /\/server\/middleware\//, layer: "middleware", confidence: 0.95, signal: "Nuxt server middleware", archetypes: ["nuxt"] },
4113
+ { pattern: /\/pages\//, layer: "presentation", confidence: 0.85, signal: "Nuxt pages directory", archetypes: ["nuxt"] },
4114
+ // ── NestJS (archetype-specific) ──
4115
+ { pattern: /\.controller\.[jt]sx?$/, layer: "routing", confidence: 0.95, signal: "NestJS controller", archetypes: ["nestjs"] },
4116
+ { pattern: /\.service\.[jt]sx?$/, layer: "services", confidence: 0.95, signal: "NestJS service", archetypes: ["nestjs"] },
4117
+ { pattern: /\.module\.[jt]sx?$/, layer: "config", confidence: 0.9, signal: "NestJS module", archetypes: ["nestjs"] },
4118
+ { pattern: /\.guard\.[jt]sx?$/, layer: "middleware", confidence: 0.9, signal: "NestJS guard", archetypes: ["nestjs"] },
4119
+ { pattern: /\.interceptor\.[jt]sx?$/, layer: "middleware", confidence: 0.9, signal: "NestJS interceptor", archetypes: ["nestjs"] },
4120
+ { pattern: /\.pipe\.[jt]sx?$/, layer: "middleware", confidence: 0.85, signal: "NestJS pipe", archetypes: ["nestjs"] },
4121
+ { pattern: /\.middleware\.[jt]sx?$/, layer: "middleware", confidence: 0.9, signal: "NestJS middleware", archetypes: ["nestjs"] },
4122
+ { pattern: /\.entity\.[jt]sx?$/, layer: "domain", confidence: 0.9, signal: "NestJS entity", archetypes: ["nestjs"] },
4123
+ { pattern: /\.dto\.[jt]sx?$/, layer: "domain", confidence: 0.85, signal: "NestJS DTO", archetypes: ["nestjs"] },
4124
+ { pattern: /\.repository\.[jt]sx?$/, layer: "data-access", confidence: 0.9, signal: "NestJS repository", archetypes: ["nestjs"] },
4125
+ // ── Generic routing patterns ──
4126
+ { pattern: /\/routes\//, layer: "routing", confidence: 0.8, signal: "routes/ directory" },
4127
+ { pattern: /\/router\//, layer: "routing", confidence: 0.8, signal: "router/ directory" },
4128
+ { pattern: /\/controllers\//, layer: "routing", confidence: 0.8, signal: "controllers/ directory" },
4129
+ { pattern: /\/handlers\//, layer: "routing", confidence: 0.75, signal: "handlers/ directory" },
4130
+ { pattern: /\/api\//, layer: "routing", confidence: 0.7, signal: "api/ directory" },
4131
+ { pattern: /\/endpoints\//, layer: "routing", confidence: 0.8, signal: "endpoints/ directory" },
4132
+ // ── Middleware ──
4133
+ { pattern: /\/middleware\//, layer: "middleware", confidence: 0.85, signal: "middleware/ directory" },
4134
+ { pattern: /\/middlewares\//, layer: "middleware", confidence: 0.85, signal: "middlewares/ directory" },
4135
+ { pattern: /\/hooks\//, layer: "middleware", confidence: 0.6, signal: "hooks/ directory" },
4136
+ { pattern: /\/plugins\//, layer: "middleware", confidence: 0.6, signal: "plugins/ directory" },
4137
+ { pattern: /\/guards\//, layer: "middleware", confidence: 0.85, signal: "guards/ directory" },
4138
+ { pattern: /\/interceptors\//, layer: "middleware", confidence: 0.85, signal: "interceptors/ directory" },
4139
+ // ── Services / application layer ──
4140
+ { pattern: /\/services\//, layer: "services", confidence: 0.85, signal: "services/ directory" },
4141
+ { pattern: /\/service\//, layer: "services", confidence: 0.8, signal: "service/ directory" },
4142
+ { pattern: /\/usecases\//, layer: "services", confidence: 0.85, signal: "usecases/ directory" },
4143
+ { pattern: /\/use-cases\//, layer: "services", confidence: 0.85, signal: "use-cases/ directory" },
4144
+ { pattern: /\/application\//, layer: "services", confidence: 0.7, signal: "application/ directory" },
4145
+ { pattern: /\/actions\//, layer: "services", confidence: 0.65, signal: "actions/ directory" },
4146
+ // ── Domain / models ──
4147
+ { pattern: /\/domain\//, layer: "domain", confidence: 0.85, signal: "domain/ directory" },
4148
+ { pattern: /\/models\//, layer: "domain", confidence: 0.8, signal: "models/ directory" },
4149
+ { pattern: /\/entities\//, layer: "domain", confidence: 0.85, signal: "entities/ directory" },
4150
+ { pattern: /\/types\//, layer: "domain", confidence: 0.7, signal: "types/ directory" },
4151
+ { pattern: /\/schemas\//, layer: "domain", confidence: 0.7, signal: "schemas/ directory" },
4152
+ { pattern: /\/validators\//, layer: "domain", confidence: 0.7, signal: "validators/ directory" },
4153
+ // ── Data access ──
4154
+ { pattern: /\/repositories\//, layer: "data-access", confidence: 0.9, signal: "repositories/ directory" },
4155
+ { pattern: /\/repository\//, layer: "data-access", confidence: 0.85, signal: "repository/ directory" },
4156
+ { pattern: /\/dao\//, layer: "data-access", confidence: 0.9, signal: "dao/ directory" },
4157
+ { pattern: /\/db\//, layer: "data-access", confidence: 0.8, signal: "db/ directory" },
4158
+ { pattern: /\/database\//, layer: "data-access", confidence: 0.8, signal: "database/ directory" },
4159
+ { pattern: /\/persistence\//, layer: "data-access", confidence: 0.85, signal: "persistence/ directory" },
4160
+ { pattern: /\/migrations\//, layer: "data-access", confidence: 0.9, signal: "migrations/ directory" },
4161
+ { pattern: /\/seeds\//, layer: "data-access", confidence: 0.85, signal: "seeds/ directory" },
4162
+ { pattern: /\/prisma\//, layer: "data-access", confidence: 0.85, signal: "prisma/ directory" },
4163
+ { pattern: /\/drizzle\//, layer: "data-access", confidence: 0.85, signal: "drizzle/ directory" },
4164
+ // ── Infrastructure ──
4165
+ { pattern: /\/infra\//, layer: "infrastructure", confidence: 0.85, signal: "infra/ directory" },
4166
+ { pattern: /\/infrastructure\//, layer: "infrastructure", confidence: 0.85, signal: "infrastructure/ directory" },
4167
+ { pattern: /\/adapters\//, layer: "infrastructure", confidence: 0.8, signal: "adapters/ directory" },
4168
+ { pattern: /\/clients\//, layer: "infrastructure", confidence: 0.75, signal: "clients/ directory" },
4169
+ { pattern: /\/integrations\//, layer: "infrastructure", confidence: 0.8, signal: "integrations/ directory" },
4170
+ { pattern: /\/external\//, layer: "infrastructure", confidence: 0.75, signal: "external/ directory" },
4171
+ { pattern: /\/queue\//, layer: "infrastructure", confidence: 0.8, signal: "queue/ directory" },
4172
+ { pattern: /\/jobs\//, layer: "infrastructure", confidence: 0.75, signal: "jobs/ directory" },
4173
+ { pattern: /\/workers\//, layer: "infrastructure", confidence: 0.75, signal: "workers/ directory" },
4174
+ { pattern: /\/cron\//, layer: "infrastructure", confidence: 0.8, signal: "cron/ directory" },
4175
+ // ── Presentation (UI layer) ──
4176
+ { pattern: /\/components\//, layer: "presentation", confidence: 0.85, signal: "components/ directory" },
4177
+ { pattern: /\/views\//, layer: "presentation", confidence: 0.85, signal: "views/ directory" },
4178
+ { pattern: /\/pages\//, layer: "presentation", confidence: 0.8, signal: "pages/ directory" },
4179
+ { pattern: /\/layouts\//, layer: "presentation", confidence: 0.85, signal: "layouts/ directory" },
4180
+ { pattern: /\/templates\//, layer: "presentation", confidence: 0.8, signal: "templates/ directory" },
4181
+ { pattern: /\/widgets\//, layer: "presentation", confidence: 0.8, signal: "widgets/ directory" },
4182
+ { pattern: /\/ui\//, layer: "presentation", confidence: 0.75, signal: "ui/ directory" },
4183
+ // ── Shared / utils ──
4184
+ { pattern: /\/utils\//, layer: "shared", confidence: 0.7, signal: "utils/ directory" },
4185
+ { pattern: /\/helpers\//, layer: "shared", confidence: 0.7, signal: "helpers/ directory" },
4186
+ { pattern: /\/lib\//, layer: "shared", confidence: 0.6, signal: "lib/ directory" },
4187
+ { pattern: /\/common\//, layer: "shared", confidence: 0.65, signal: "common/ directory" },
4188
+ { pattern: /\/shared\//, layer: "shared", confidence: 0.75, signal: "shared/ directory" },
4189
+ { pattern: /\/constants\//, layer: "shared", confidence: 0.7, signal: "constants/ directory" },
4190
+ // ── CLI-specific (command layer → routing) ──
4191
+ { pattern: /\/commands\//, layer: "routing", confidence: 0.8, signal: "commands/ directory", archetypes: ["cli"] },
4192
+ { pattern: /\/formatters\//, layer: "presentation", confidence: 0.8, signal: "formatters/ directory", archetypes: ["cli"] },
4193
+ { pattern: /\/scanners\//, layer: "services", confidence: 0.8, signal: "scanners/ directory", archetypes: ["cli"] },
4194
+ { pattern: /\/scoring\//, layer: "domain", confidence: 0.8, signal: "scoring/ directory", archetypes: ["cli"] },
4195
+ // ── Serverless-specific ──
4196
+ { pattern: /\/functions\//, layer: "routing", confidence: 0.8, signal: "functions/ directory", archetypes: ["serverless"] },
4197
+ { pattern: /\/lambdas\//, layer: "routing", confidence: 0.85, signal: "lambdas/ directory", archetypes: ["serverless"] },
4198
+ { pattern: /\/layers\//, layer: "shared", confidence: 0.7, signal: "Lambda layers/ directory", archetypes: ["serverless"] }
4199
+ ];
4200
+ var SUFFIX_RULES = [
4201
+ { suffix: ".controller", layer: "routing", confidence: 0.85, signal: "controller suffix" },
4202
+ { suffix: ".route", layer: "routing", confidence: 0.85, signal: "route suffix" },
4203
+ { suffix: ".router", layer: "routing", confidence: 0.85, signal: "router suffix" },
4204
+ { suffix: ".handler", layer: "routing", confidence: 0.8, signal: "handler suffix" },
4205
+ { suffix: ".middleware", layer: "middleware", confidence: 0.85, signal: "middleware suffix" },
4206
+ { suffix: ".guard", layer: "middleware", confidence: 0.85, signal: "guard suffix" },
4207
+ { suffix: ".interceptor", layer: "middleware", confidence: 0.85, signal: "interceptor suffix" },
4208
+ { suffix: ".service", layer: "services", confidence: 0.85, signal: "service suffix" },
4209
+ { suffix: ".usecase", layer: "services", confidence: 0.85, signal: "usecase suffix" },
4210
+ { suffix: ".model", layer: "domain", confidence: 0.8, signal: "model suffix" },
4211
+ { suffix: ".entity", layer: "domain", confidence: 0.85, signal: "entity suffix" },
4212
+ { suffix: ".dto", layer: "domain", confidence: 0.8, signal: "DTO suffix" },
4213
+ { suffix: ".schema", layer: "domain", confidence: 0.75, signal: "schema suffix" },
4214
+ { suffix: ".validator", layer: "domain", confidence: 0.75, signal: "validator suffix" },
4215
+ { suffix: ".repository", layer: "data-access", confidence: 0.9, signal: "repository suffix" },
4216
+ { suffix: ".repo", layer: "data-access", confidence: 0.85, signal: "repo suffix" },
4217
+ { suffix: ".dao", layer: "data-access", confidence: 0.9, signal: "dao suffix" },
4218
+ { suffix: ".migration", layer: "data-access", confidence: 0.85, signal: "migration suffix" },
4219
+ { suffix: ".adapter", layer: "infrastructure", confidence: 0.8, signal: "adapter suffix" },
4220
+ { suffix: ".client", layer: "infrastructure", confidence: 0.75, signal: "client suffix" },
4221
+ { suffix: ".provider", layer: "infrastructure", confidence: 0.7, signal: "provider suffix" },
4222
+ { suffix: ".config", layer: "config", confidence: 0.8, signal: "config suffix" },
4223
+ { suffix: ".component", layer: "presentation", confidence: 0.8, signal: "component suffix" },
4224
+ { suffix: ".page", layer: "presentation", confidence: 0.85, signal: "page suffix" },
4225
+ { suffix: ".view", layer: "presentation", confidence: 0.8, signal: "view suffix" },
4226
+ { suffix: ".layout", layer: "presentation", confidence: 0.85, signal: "layout suffix" },
4227
+ { suffix: ".util", layer: "shared", confidence: 0.7, signal: "util suffix" },
4228
+ { suffix: ".helper", layer: "shared", confidence: 0.7, signal: "helper suffix" },
4229
+ { suffix: ".constant", layer: "shared", confidence: 0.7, signal: "constant suffix" }
4230
+ ];
4231
+ var PACKAGE_LAYER_MAP = {
4232
+ // Routing/controllers
4233
+ "express": "routing",
4234
+ "fastify": "routing",
4235
+ "@nestjs/core": "routing",
4236
+ "hono": "routing",
4237
+ "koa": "routing",
4238
+ "koa-router": "routing",
4239
+ "@hapi/hapi": "routing",
4240
+ "h3": "routing",
4241
+ // Middleware
4242
+ "cors": "middleware",
4243
+ "helmet": "middleware",
4244
+ "passport": "middleware",
4245
+ "express-rate-limit": "middleware",
4246
+ "cookie-parser": "middleware",
4247
+ "body-parser": "middleware",
4248
+ "multer": "middleware",
4249
+ "morgan": "middleware",
4250
+ "compression": "middleware",
4251
+ "express-session": "middleware",
4252
+ // Services / application
4253
+ "bullmq": "services",
4254
+ "bull": "services",
4255
+ "agenda": "services",
4256
+ "pg-boss": "services",
4257
+ "inngest": "services",
4258
+ // Domain / validation
4259
+ "zod": "domain",
4260
+ "joi": "domain",
4261
+ "yup": "domain",
4262
+ "class-validator": "domain",
4263
+ "class-transformer": "domain",
4264
+ "superstruct": "domain",
4265
+ "valibot": "domain",
4266
+ // Data access / ORM
4267
+ "prisma": "data-access",
4268
+ "@prisma/client": "data-access",
4269
+ "drizzle-orm": "data-access",
4270
+ "typeorm": "data-access",
4271
+ "sequelize": "data-access",
4272
+ "knex": "data-access",
4273
+ "pg": "data-access",
4274
+ "mysql2": "data-access",
4275
+ "mongodb": "data-access",
4276
+ "mongoose": "data-access",
4277
+ "ioredis": "data-access",
4278
+ "redis": "data-access",
4279
+ "better-sqlite3": "data-access",
4280
+ "kysely": "data-access",
4281
+ "@mikro-orm/core": "data-access",
4282
+ // Infrastructure
4283
+ "@aws-sdk/client-s3": "infrastructure",
4284
+ "@aws-sdk/client-sqs": "infrastructure",
4285
+ "@aws-sdk/client-sns": "infrastructure",
4286
+ "@aws-sdk/client-ses": "infrastructure",
4287
+ "@aws-sdk/client-lambda": "infrastructure",
4288
+ "@google-cloud/storage": "infrastructure",
4289
+ "@azure/storage-blob": "infrastructure",
4290
+ "nodemailer": "infrastructure",
4291
+ "@sendgrid/mail": "infrastructure",
4292
+ "stripe": "infrastructure",
4293
+ "kafkajs": "infrastructure",
4294
+ "amqplib": "infrastructure",
4295
+ // Presentation
4296
+ "react": "presentation",
4297
+ "react-dom": "presentation",
4298
+ "vue": "presentation",
4299
+ "@angular/core": "presentation",
4300
+ "svelte": "presentation",
4301
+ // Shared
4302
+ "lodash": "shared",
4303
+ "dayjs": "shared",
4304
+ "date-fns": "shared",
4305
+ "uuid": "shared",
4306
+ "nanoid": "shared",
4307
+ // Testing
4308
+ "vitest": "testing",
4309
+ "jest": "testing",
4310
+ "mocha": "testing",
4311
+ "@playwright/test": "testing",
4312
+ "cypress": "testing",
4313
+ "supertest": "testing",
4314
+ // Observability → infrastructure
4315
+ "@sentry/node": "infrastructure",
4316
+ "@opentelemetry/api": "infrastructure",
4317
+ "pino": "infrastructure",
4318
+ "winston": "infrastructure",
4319
+ "dd-trace": "infrastructure"
4320
+ };
4321
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs", ".svelte", ".vue"]);
4322
+ var IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".nuxt", ".output", ".svelte-kit", "coverage", ".vibgrate"]);
4323
+ async function walkSourceFiles(rootDir) {
4324
+ const files = [];
4325
+ async function walk(dir) {
4326
+ let entries;
4327
+ try {
4328
+ entries = await fs5.readdir(dir, { withFileTypes: true });
4329
+ } catch {
4330
+ return;
4331
+ }
4332
+ for (const entry of entries) {
4333
+ if (entry.name.startsWith(".") && entry.name !== ".") continue;
4334
+ const fullPath = path14.join(dir, entry.name);
4335
+ if (entry.isDirectory()) {
4336
+ if (!IGNORE_DIRS.has(entry.name)) {
4337
+ await walk(fullPath);
4338
+ }
4339
+ } else if (entry.isFile()) {
4340
+ const ext = path14.extname(entry.name);
4341
+ if (SOURCE_EXTENSIONS.has(ext)) {
4342
+ files.push(path14.relative(rootDir, fullPath));
4343
+ }
4344
+ }
4345
+ }
4346
+ }
4347
+ await walk(rootDir);
4348
+ return files;
4349
+ }
4350
+ function classifyFile(filePath, archetype) {
4351
+ const normalised = filePath.replace(/\\/g, "/");
4352
+ let bestMatch = null;
4353
+ for (const rule of PATH_RULES) {
4354
+ if (rule.archetypes && rule.archetypes.length > 0 && !rule.archetypes.includes(archetype)) {
4355
+ continue;
4356
+ }
4357
+ if (rule.pattern.test(normalised)) {
4358
+ const boost = rule.archetypes ? 0.05 : 0;
4359
+ const adjustedConfidence = Math.min(rule.confidence + boost, 1);
4360
+ if (!bestMatch || adjustedConfidence > bestMatch.confidence) {
4361
+ bestMatch = { layer: rule.layer, confidence: adjustedConfidence, signal: rule.signal };
4362
+ }
4363
+ }
4364
+ }
4365
+ if (!bestMatch || bestMatch.confidence < 0.7) {
4366
+ const baseName = path14.basename(filePath, path14.extname(filePath));
4367
+ const cleanBase = baseName.replace(/\.(test|spec)$/, "");
4368
+ for (const rule of SUFFIX_RULES) {
4369
+ if (cleanBase.endsWith(rule.suffix)) {
4370
+ if (!bestMatch || rule.confidence > bestMatch.confidence) {
4371
+ bestMatch = { layer: rule.layer, confidence: rule.confidence, signal: rule.signal };
4372
+ }
4373
+ }
4374
+ }
4375
+ }
4376
+ if (bestMatch) {
4377
+ return {
4378
+ filePath,
4379
+ layer: bestMatch.layer,
4380
+ confidence: bestMatch.confidence,
4381
+ signals: [bestMatch.signal]
4382
+ };
4383
+ }
4384
+ return null;
4385
+ }
4386
+ function computeLayerDrift(packages) {
4387
+ if (packages.length === 0) {
4388
+ return { score: 100, riskLevel: "low" };
4389
+ }
4390
+ let current = 0;
4391
+ let oneBehind = 0;
4392
+ let twoPlusBehind = 0;
4393
+ let unknown = 0;
4394
+ for (const pkg2 of packages) {
4395
+ if (pkg2.majorsBehind === null) {
4396
+ unknown++;
4397
+ } else if (pkg2.majorsBehind === 0) {
4398
+ current++;
4399
+ } else if (pkg2.majorsBehind === 1) {
4400
+ oneBehind++;
4401
+ } else {
4402
+ twoPlusBehind++;
4403
+ }
4404
+ }
4405
+ const known = current + oneBehind + twoPlusBehind;
4406
+ if (known === 0) return { score: 100, riskLevel: "low" };
4407
+ const currentPct = current / known;
4408
+ const onePct = oneBehind / known;
4409
+ const twoPct = twoPlusBehind / known;
4410
+ const score = Math.round(Math.max(0, Math.min(100, currentPct * 100 - onePct * 10 - twoPct * 40)));
4411
+ const riskLevel = score >= 70 ? "low" : score >= 40 ? "moderate" : "high";
4412
+ return { score, riskLevel };
4413
+ }
4414
+ function mapToolingToLayers(tooling, services, depsByLayer) {
4415
+ const layerTooling = /* @__PURE__ */ new Map();
4416
+ const layerServices = /* @__PURE__ */ new Map();
4417
+ const pkgLayerLookup = /* @__PURE__ */ new Map();
4418
+ for (const [layer, packages] of depsByLayer) {
4419
+ for (const pkg2 of packages) {
4420
+ pkgLayerLookup.set(pkg2, layer);
4421
+ }
4422
+ }
4423
+ if (tooling) {
4424
+ for (const [, items] of Object.entries(tooling)) {
4425
+ for (const item of items) {
4426
+ const layer = pkgLayerLookup.get(item.package) ?? PACKAGE_LAYER_MAP[item.package] ?? "shared";
4427
+ if (!layerTooling.has(layer)) layerTooling.set(layer, []);
4428
+ const existing = layerTooling.get(layer);
4429
+ if (!existing.some((t) => t.package === item.package)) {
4430
+ existing.push(item);
4431
+ }
4432
+ }
4433
+ }
4434
+ }
4435
+ if (services) {
4436
+ for (const [, items] of Object.entries(services)) {
4437
+ for (const item of items) {
4438
+ const layer = pkgLayerLookup.get(item.package) ?? PACKAGE_LAYER_MAP[item.package] ?? "infrastructure";
4439
+ if (!layerServices.has(layer)) layerServices.set(layer, []);
4440
+ const existing = layerServices.get(layer);
4441
+ if (!existing.some((s) => s.package === item.package)) {
4442
+ existing.push(item);
4443
+ }
4444
+ }
4445
+ }
4446
+ }
4447
+ return { layerTooling, layerServices };
4448
+ }
4449
+ async function scanArchitecture(rootDir, projects, tooling, services) {
4450
+ const { archetype, confidence: archetypeConfidence } = detectArchetype(projects);
4451
+ const sourceFiles = await walkSourceFiles(rootDir);
4452
+ const classifications = [];
4453
+ let unclassified = 0;
4454
+ for (const file of sourceFiles) {
4455
+ const classification = classifyFile(file, archetype);
4456
+ if (classification) {
4457
+ classifications.push(classification);
4458
+ } else {
4459
+ unclassified++;
4460
+ }
4461
+ }
4462
+ const allDeps = /* @__PURE__ */ new Map();
4463
+ for (const p of projects) {
4464
+ for (const d of p.dependencies) {
4465
+ if (!allDeps.has(d.package)) {
4466
+ allDeps.set(d.package, d);
4467
+ }
4468
+ }
4469
+ }
4470
+ const depsByLayer = /* @__PURE__ */ new Map();
4471
+ for (const [pkg2] of allDeps) {
4472
+ const layer = PACKAGE_LAYER_MAP[pkg2];
4473
+ if (layer) {
4474
+ if (!depsByLayer.has(layer)) depsByLayer.set(layer, /* @__PURE__ */ new Set());
4475
+ depsByLayer.get(layer).add(pkg2);
4476
+ }
4477
+ }
4478
+ const { layerTooling, layerServices } = mapToolingToLayers(tooling, services, depsByLayer);
4479
+ const ALL_LAYERS = [
4480
+ "routing",
4481
+ "middleware",
4482
+ "services",
4483
+ "domain",
4484
+ "data-access",
4485
+ "infrastructure",
4486
+ "presentation",
4487
+ "config",
4488
+ "testing",
4489
+ "shared"
4490
+ ];
4491
+ const layerFileCounts = /* @__PURE__ */ new Map();
4492
+ for (const c of classifications) {
4493
+ layerFileCounts.set(c.layer, (layerFileCounts.get(c.layer) ?? 0) + 1);
4494
+ }
4495
+ const layers = [];
4496
+ for (const layer of ALL_LAYERS) {
4497
+ const fileCount = layerFileCounts.get(layer) ?? 0;
4498
+ const layerPkgs = depsByLayer.get(layer) ?? /* @__PURE__ */ new Set();
4499
+ const tech = layerTooling.get(layer) ?? [];
4500
+ const svc = layerServices.get(layer) ?? [];
4501
+ if (fileCount === 0 && layerPkgs.size === 0 && tech.length === 0 && svc.length === 0) {
4502
+ continue;
4503
+ }
4504
+ const packages = [];
4505
+ for (const pkg2 of layerPkgs) {
4506
+ const dep = allDeps.get(pkg2);
4507
+ if (dep) {
4508
+ packages.push({
4509
+ name: dep.package,
4510
+ version: dep.resolvedVersion,
4511
+ latestStable: dep.latestStable,
4512
+ majorsBehind: dep.majorsBehind,
4513
+ drift: dep.drift
4514
+ });
4515
+ }
4516
+ }
4517
+ const { score, riskLevel } = computeLayerDrift(packages);
4518
+ layers.push({
4519
+ layer,
4520
+ fileCount,
4521
+ driftScore: score,
4522
+ riskLevel,
4523
+ techStack: tech,
4524
+ services: svc,
4525
+ packages
4526
+ });
4527
+ }
4528
+ const LAYER_ORDER = {
4529
+ "presentation": 0,
4530
+ "routing": 1,
4531
+ "middleware": 2,
4532
+ "services": 3,
4533
+ "domain": 4,
4534
+ "data-access": 5,
4535
+ "infrastructure": 6,
4536
+ "config": 7,
4537
+ "shared": 8,
4538
+ "testing": 9
4539
+ };
4540
+ layers.sort((a, b) => (LAYER_ORDER[a.layer] ?? 99) - (LAYER_ORDER[b.layer] ?? 99));
4541
+ return {
4542
+ archetype,
4543
+ archetypeConfidence,
4544
+ layers,
4545
+ totalClassified: classifications.length,
4546
+ unclassified
4547
+ };
4548
+ }
4549
+
3910
4550
  // src/commands/scan.ts
3911
4551
  async function runScan(rootDir, opts) {
3912
4552
  const scanStart = Date.now();
@@ -3931,7 +4571,8 @@ async function runScan(rootDir, opts) {
3931
4571
  ...scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
3932
4572
  ...scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
3933
4573
  ...scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
3934
- ...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : []
4574
+ ...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
4575
+ ...scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : []
3935
4576
  ] : [],
3936
4577
  { id: "drift", label: "Computing drift score" },
3937
4578
  { id: "findings", label: "Generating findings" }
@@ -4088,6 +4729,22 @@ async function runScan(rootDir, opts) {
4088
4729
  );
4089
4730
  }
4090
4731
  await Promise.all(scannerTasks);
4732
+ if (scanners?.architecture?.enabled !== false) {
4733
+ progress.startStep("architecture");
4734
+ extended.architecture = await scanArchitecture(
4735
+ rootDir,
4736
+ allProjects,
4737
+ extended.toolingInventory,
4738
+ extended.serviceDependencies
4739
+ );
4740
+ const arch = extended.architecture;
4741
+ const layerCount = arch.layers.filter((l) => l.fileCount > 0).length;
4742
+ progress.completeStep(
4743
+ "architecture",
4744
+ `${arch.archetype} \xB7 ${layerCount} layer${layerCount !== 1 ? "s" : ""} \xB7 ${arch.totalClassified} files`,
4745
+ layerCount
4746
+ );
4747
+ }
4091
4748
  }
4092
4749
  progress.startStep("drift");
4093
4750
  const drift = computeDriftScore(allProjects);
@@ -4120,7 +4777,7 @@ async function runScan(rootDir, opts) {
4120
4777
  schemaVersion: "1.0",
4121
4778
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4122
4779
  vibgrateVersion: VERSION,
4123
- rootPath: path14.basename(rootDir),
4780
+ rootPath: path15.basename(rootDir),
4124
4781
  ...vcs.type !== "unknown" ? { vcs } : {},
4125
4782
  projects: allProjects,
4126
4783
  drift,
@@ -4130,7 +4787,7 @@ async function runScan(rootDir, opts) {
4130
4787
  filesScanned
4131
4788
  };
4132
4789
  if (opts.baseline) {
4133
- const baselinePath = path14.resolve(opts.baseline);
4790
+ const baselinePath = path15.resolve(opts.baseline);
4134
4791
  if (await pathExists(baselinePath)) {
4135
4792
  try {
4136
4793
  const baseline = await readJsonFile(baselinePath);
@@ -4141,15 +4798,15 @@ async function runScan(rootDir, opts) {
4141
4798
  }
4142
4799
  }
4143
4800
  }
4144
- const vibgrateDir = path14.join(rootDir, ".vibgrate");
4801
+ const vibgrateDir = path15.join(rootDir, ".vibgrate");
4145
4802
  await ensureDir(vibgrateDir);
4146
- await writeJsonFile(path14.join(vibgrateDir, "scan_result.json"), artifact);
4803
+ await writeJsonFile(path15.join(vibgrateDir, "scan_result.json"), artifact);
4147
4804
  for (const project of allProjects) {
4148
4805
  if (project.drift && project.path) {
4149
- const projectDir = path14.resolve(rootDir, project.path);
4150
- const projectVibgrateDir = path14.join(projectDir, ".vibgrate");
4806
+ const projectDir = path15.resolve(rootDir, project.path);
4807
+ const projectVibgrateDir = path15.join(projectDir, ".vibgrate");
4151
4808
  await ensureDir(projectVibgrateDir);
4152
- await writeJsonFile(path14.join(projectVibgrateDir, "project_score.json"), {
4809
+ await writeJsonFile(path15.join(projectVibgrateDir, "project_score.json"), {
4153
4810
  projectId: project.projectId,
4154
4811
  name: project.name,
4155
4812
  type: project.type,
@@ -4166,7 +4823,7 @@ async function runScan(rootDir, opts) {
4166
4823
  if (opts.format === "json") {
4167
4824
  const jsonStr = JSON.stringify(artifact, null, 2);
4168
4825
  if (opts.out) {
4169
- await writeTextFile(path14.resolve(opts.out), jsonStr);
4826
+ await writeTextFile(path15.resolve(opts.out), jsonStr);
4170
4827
  console.log(chalk5.green("\u2714") + ` JSON written to ${opts.out}`);
4171
4828
  } else {
4172
4829
  console.log(jsonStr);
@@ -4175,7 +4832,7 @@ async function runScan(rootDir, opts) {
4175
4832
  const sarif = formatSarif(artifact);
4176
4833
  const sarifStr = JSON.stringify(sarif, null, 2);
4177
4834
  if (opts.out) {
4178
- await writeTextFile(path14.resolve(opts.out), sarifStr);
4835
+ await writeTextFile(path15.resolve(opts.out), sarifStr);
4179
4836
  console.log(chalk5.green("\u2714") + ` SARIF written to ${opts.out}`);
4180
4837
  } else {
4181
4838
  console.log(sarifStr);
@@ -4184,7 +4841,7 @@ async function runScan(rootDir, opts) {
4184
4841
  const text = formatText(artifact);
4185
4842
  console.log(text);
4186
4843
  if (opts.out) {
4187
- await writeTextFile(path14.resolve(opts.out), text);
4844
+ await writeTextFile(path15.resolve(opts.out), text);
4188
4845
  }
4189
4846
  }
4190
4847
  return artifact;
@@ -4243,7 +4900,7 @@ async function autoPush(artifact, rootDir, opts) {
4243
4900
  }
4244
4901
  }
4245
4902
  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) => {
4246
- const rootDir = path14.resolve(targetPath);
4903
+ const rootDir = path15.resolve(targetPath);
4247
4904
  if (!await pathExists(rootDir)) {
4248
4905
  console.error(chalk5.red(`Path does not exist: ${rootDir}`));
4249
4906
  process.exit(1);
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VXZT34Y5.js";
5
5
  import {
6
6
  baselineCommand
7
- } from "./chunk-QHBZIYWP.js";
7
+ } from "./chunk-OPEOSIRY.js";
8
8
  import {
9
9
  VERSION,
10
10
  dsnCommand,
@@ -15,7 +15,7 @@ import {
15
15
  readJsonFile,
16
16
  scanCommand,
17
17
  writeDefaultConfig
18
- } from "./chunk-S4C6KWCP.js";
18
+ } from "./chunk-QZV77UWV.js";
19
19
 
20
20
  // src/cli.ts
21
21
  import { Command as Command4 } from "commander";
@@ -38,7 +38,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
38
38
  console.log(chalk.green("\u2714") + ` Created ${chalk.bold("vibgrate.config.ts")}`);
39
39
  }
40
40
  if (opts.baseline) {
41
- const { runBaseline } = await import("./baseline-OPUTCJOQ.js");
41
+ const { runBaseline } = await import("./baseline-M445KUZ4.js");
42
42
  await runBaseline(rootDir);
43
43
  }
44
44
  console.log("");
@@ -222,7 +222,7 @@ var updateCommand = new Command3("update").description("Update vibgrate to the l
222
222
 
223
223
  // src/cli.ts
224
224
  var program = new Command4();
225
- program.name("vibgrate").description("Continuous Upgrade Drift Intelligence for Node & .NET").version(VERSION);
225
+ program.name("vibgrate").description("Continuous Drift Intelligence for Node & .NET").version(VERSION);
226
226
  program.addCommand(initCommand);
227
227
  program.addCommand(scanCommand);
228
228
  program.addCommand(baselineCommand);
package/dist/index.d.ts CHANGED
@@ -110,6 +110,7 @@ interface ScannersConfig {
110
110
  fileHotspots?: ScannerToggle;
111
111
  securityPosture?: ScannerToggle;
112
112
  serviceDependencies?: ScannerToggle;
113
+ architecture?: ScannerToggle;
113
114
  }
114
115
  interface VibgrateConfig {
115
116
  include?: string[];
@@ -246,6 +247,48 @@ interface ServiceDependenciesResult {
246
247
  storage: ServiceDependencyItem[];
247
248
  search: ServiceDependencyItem[];
248
249
  }
250
+ /** Detected project archetype (fingerprint) */
251
+ type ProjectArchetype = 'nextjs' | 'remix' | 'sveltekit' | 'nuxt' | 'nestjs' | 'express' | 'fastify' | 'hono' | 'koa' | 'serverless' | 'library' | 'cli' | 'monorepo' | 'unknown';
252
+ /** Architectural layer classification */
253
+ type ArchitectureLayer = 'routing' | 'middleware' | 'services' | 'domain' | 'data-access' | 'infrastructure' | 'presentation' | 'config' | 'testing' | 'shared';
254
+ /** Per-layer aggregated data */
255
+ interface LayerSummary {
256
+ /** The layer name */
257
+ layer: ArchitectureLayer;
258
+ /** Number of files in this layer */
259
+ fileCount: number;
260
+ /** Drift score for dependencies used in this layer (0–100) */
261
+ driftScore: number;
262
+ /** Risk level derived from drift score */
263
+ riskLevel: RiskLevel;
264
+ /** Tech stack components detected in this layer */
265
+ techStack: InventoryItem[];
266
+ /** Services/integrations used in this layer */
267
+ services: ServiceDependencyItem[];
268
+ /** Packages referenced in this layer with their drift status */
269
+ packages: LayerPackageRef[];
270
+ }
271
+ /** Package reference within a layer */
272
+ interface LayerPackageRef {
273
+ name: string;
274
+ version: string | null;
275
+ latestStable: string | null;
276
+ majorsBehind: number | null;
277
+ drift: 'current' | 'minor-behind' | 'major-behind' | 'unknown';
278
+ }
279
+ /** Full architecture detection result */
280
+ interface ArchitectureResult {
281
+ /** Detected project archetype */
282
+ archetype: ProjectArchetype;
283
+ /** Confidence of archetype detection (0–1) */
284
+ archetypeConfidence: number;
285
+ /** Per-layer summaries with drift + tech data */
286
+ layers: LayerSummary[];
287
+ /** Total files classified */
288
+ totalClassified: number;
289
+ /** Files that could not be classified */
290
+ unclassified: number;
291
+ }
249
292
  interface ExtendedScanResults {
250
293
  platformMatrix?: PlatformMatrixResult;
251
294
  dependencyRisk?: DependencyRiskResult;
@@ -257,6 +300,7 @@ interface ExtendedScanResults {
257
300
  fileHotspots?: FileHotspotsResult;
258
301
  securityPosture?: SecurityPostureResult;
259
302
  serviceDependencies?: ServiceDependenciesResult;
303
+ architecture?: ArchitectureResult;
260
304
  }
261
305
 
262
306
  declare function runScan(rootDir: string, opts: ScanOptions): Promise<ScanArtifact>;
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  formatText,
8
8
  generateFindings,
9
9
  runScan
10
- } from "./chunk-S4C6KWCP.js";
10
+ } from "./chunk-QZV77UWV.js";
11
11
  export {
12
12
  computeDriftScore,
13
13
  formatMarkdown,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibgrate/cli",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "CLI for measuring upgrade drift across Node & .NET projects",
5
5
  "type": "module",
6
6
  "bin": {