@vibgrate/cli 1.0.14 → 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 +1 -1
- package/dist/{baseline-RV3GAW72.js → baseline-M445KUZ4.js} +2 -2
- package/dist/{chunk-POMRKRQN.js → chunk-OPEOSIRY.js} +1 -1
- package/dist/{chunk-4N4BALQQ.js → chunk-QZV77UWV.js} +673 -18
- package/dist/cli.js +4 -4
- package/dist/index.d.ts +44 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/DOCS.md
CHANGED
|
@@ -187,12 +187,12 @@ function eolScore(projects) {
|
|
|
187
187
|
}
|
|
188
188
|
function computeDriftScore(projects) {
|
|
189
189
|
const rs = runtimeScore(projects);
|
|
190
|
-
const
|
|
190
|
+
const fs6 = frameworkScore(projects);
|
|
191
191
|
const ds = dependencyScore(projects);
|
|
192
192
|
const es = eolScore(projects);
|
|
193
193
|
const components = [
|
|
194
194
|
{ score: rs, weight: 0.25 },
|
|
195
|
-
{ score:
|
|
195
|
+
{ score: fs6, weight: 0.25 },
|
|
196
196
|
{ score: ds, weight: 0.3 },
|
|
197
197
|
{ score: es, weight: 0.2 }
|
|
198
198
|
];
|
|
@@ -203,7 +203,7 @@ function computeDriftScore(projects) {
|
|
|
203
203
|
riskLevel: "low",
|
|
204
204
|
components: {
|
|
205
205
|
runtimeScore: Math.round(rs ?? 100),
|
|
206
|
-
frameworkScore: Math.round(
|
|
206
|
+
frameworkScore: Math.round(fs6 ?? 100),
|
|
207
207
|
dependencyScore: Math.round(ds ?? 100),
|
|
208
208
|
eolScore: Math.round(es ?? 100)
|
|
209
209
|
}
|
|
@@ -221,7 +221,7 @@ function computeDriftScore(projects) {
|
|
|
221
221
|
else riskLevel = "high";
|
|
222
222
|
const measured = [];
|
|
223
223
|
if (rs !== null) measured.push("runtime");
|
|
224
|
-
if (
|
|
224
|
+
if (fs6 !== null) measured.push("framework");
|
|
225
225
|
if (ds !== null) measured.push("dependency");
|
|
226
226
|
if (es !== null) measured.push("eol");
|
|
227
227
|
return {
|
|
@@ -229,7 +229,7 @@ function computeDriftScore(projects) {
|
|
|
229
229
|
riskLevel,
|
|
230
230
|
components: {
|
|
231
231
|
runtimeScore: Math.round(rs ?? 100),
|
|
232
|
-
frameworkScore: Math.round(
|
|
232
|
+
frameworkScore: Math.round(fs6 ?? 100),
|
|
233
233
|
dependencyScore: Math.round(ds ?? 100),
|
|
234
234
|
eolScore: Math.round(es ?? 100)
|
|
235
235
|
},
|
|
@@ -585,6 +585,122 @@ function formatExtended(ext) {
|
|
|
585
585
|
lines.push("");
|
|
586
586
|
}
|
|
587
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("");
|
|
588
704
|
return lines;
|
|
589
705
|
}
|
|
590
706
|
function generatePriorityActions(artifact) {
|
|
@@ -1050,7 +1166,7 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
|
|
|
1050
1166
|
});
|
|
1051
1167
|
|
|
1052
1168
|
// src/commands/scan.ts
|
|
1053
|
-
import * as
|
|
1169
|
+
import * as path15 from "path";
|
|
1054
1170
|
import { Command as Command3 } from "commander";
|
|
1055
1171
|
import chalk5 from "chalk";
|
|
1056
1172
|
|
|
@@ -3909,6 +4025,528 @@ function scanServiceDependencies(projects) {
|
|
|
3909
4025
|
return result;
|
|
3910
4026
|
}
|
|
3911
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
|
+
|
|
3912
4550
|
// src/commands/scan.ts
|
|
3913
4551
|
async function runScan(rootDir, opts) {
|
|
3914
4552
|
const scanStart = Date.now();
|
|
@@ -3933,7 +4571,8 @@ async function runScan(rootDir, opts) {
|
|
|
3933
4571
|
...scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
|
|
3934
4572
|
...scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
|
|
3935
4573
|
...scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
|
|
3936
|
-
...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" }] : []
|
|
3937
4576
|
] : [],
|
|
3938
4577
|
{ id: "drift", label: "Computing drift score" },
|
|
3939
4578
|
{ id: "findings", label: "Generating findings" }
|
|
@@ -4090,6 +4729,22 @@ async function runScan(rootDir, opts) {
|
|
|
4090
4729
|
);
|
|
4091
4730
|
}
|
|
4092
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
|
+
}
|
|
4093
4748
|
}
|
|
4094
4749
|
progress.startStep("drift");
|
|
4095
4750
|
const drift = computeDriftScore(allProjects);
|
|
@@ -4122,7 +4777,7 @@ async function runScan(rootDir, opts) {
|
|
|
4122
4777
|
schemaVersion: "1.0",
|
|
4123
4778
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4124
4779
|
vibgrateVersion: VERSION,
|
|
4125
|
-
rootPath:
|
|
4780
|
+
rootPath: path15.basename(rootDir),
|
|
4126
4781
|
...vcs.type !== "unknown" ? { vcs } : {},
|
|
4127
4782
|
projects: allProjects,
|
|
4128
4783
|
drift,
|
|
@@ -4132,7 +4787,7 @@ async function runScan(rootDir, opts) {
|
|
|
4132
4787
|
filesScanned
|
|
4133
4788
|
};
|
|
4134
4789
|
if (opts.baseline) {
|
|
4135
|
-
const baselinePath =
|
|
4790
|
+
const baselinePath = path15.resolve(opts.baseline);
|
|
4136
4791
|
if (await pathExists(baselinePath)) {
|
|
4137
4792
|
try {
|
|
4138
4793
|
const baseline = await readJsonFile(baselinePath);
|
|
@@ -4143,15 +4798,15 @@ async function runScan(rootDir, opts) {
|
|
|
4143
4798
|
}
|
|
4144
4799
|
}
|
|
4145
4800
|
}
|
|
4146
|
-
const vibgrateDir =
|
|
4801
|
+
const vibgrateDir = path15.join(rootDir, ".vibgrate");
|
|
4147
4802
|
await ensureDir(vibgrateDir);
|
|
4148
|
-
await writeJsonFile(
|
|
4803
|
+
await writeJsonFile(path15.join(vibgrateDir, "scan_result.json"), artifact);
|
|
4149
4804
|
for (const project of allProjects) {
|
|
4150
4805
|
if (project.drift && project.path) {
|
|
4151
|
-
const projectDir =
|
|
4152
|
-
const projectVibgrateDir =
|
|
4806
|
+
const projectDir = path15.resolve(rootDir, project.path);
|
|
4807
|
+
const projectVibgrateDir = path15.join(projectDir, ".vibgrate");
|
|
4153
4808
|
await ensureDir(projectVibgrateDir);
|
|
4154
|
-
await writeJsonFile(
|
|
4809
|
+
await writeJsonFile(path15.join(projectVibgrateDir, "project_score.json"), {
|
|
4155
4810
|
projectId: project.projectId,
|
|
4156
4811
|
name: project.name,
|
|
4157
4812
|
type: project.type,
|
|
@@ -4168,7 +4823,7 @@ async function runScan(rootDir, opts) {
|
|
|
4168
4823
|
if (opts.format === "json") {
|
|
4169
4824
|
const jsonStr = JSON.stringify(artifact, null, 2);
|
|
4170
4825
|
if (opts.out) {
|
|
4171
|
-
await writeTextFile(
|
|
4826
|
+
await writeTextFile(path15.resolve(opts.out), jsonStr);
|
|
4172
4827
|
console.log(chalk5.green("\u2714") + ` JSON written to ${opts.out}`);
|
|
4173
4828
|
} else {
|
|
4174
4829
|
console.log(jsonStr);
|
|
@@ -4177,7 +4832,7 @@ async function runScan(rootDir, opts) {
|
|
|
4177
4832
|
const sarif = formatSarif(artifact);
|
|
4178
4833
|
const sarifStr = JSON.stringify(sarif, null, 2);
|
|
4179
4834
|
if (opts.out) {
|
|
4180
|
-
await writeTextFile(
|
|
4835
|
+
await writeTextFile(path15.resolve(opts.out), sarifStr);
|
|
4181
4836
|
console.log(chalk5.green("\u2714") + ` SARIF written to ${opts.out}`);
|
|
4182
4837
|
} else {
|
|
4183
4838
|
console.log(sarifStr);
|
|
@@ -4186,7 +4841,7 @@ async function runScan(rootDir, opts) {
|
|
|
4186
4841
|
const text = formatText(artifact);
|
|
4187
4842
|
console.log(text);
|
|
4188
4843
|
if (opts.out) {
|
|
4189
|
-
await writeTextFile(
|
|
4844
|
+
await writeTextFile(path15.resolve(opts.out), text);
|
|
4190
4845
|
}
|
|
4191
4846
|
}
|
|
4192
4847
|
return artifact;
|
|
@@ -4245,7 +4900,7 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
4245
4900
|
}
|
|
4246
4901
|
}
|
|
4247
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) => {
|
|
4248
|
-
const rootDir =
|
|
4903
|
+
const rootDir = path15.resolve(targetPath);
|
|
4249
4904
|
if (!await pathExists(rootDir)) {
|
|
4250
4905
|
console.error(chalk5.red(`Path does not exist: ${rootDir}`));
|
|
4251
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-
|
|
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-
|
|
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-
|
|
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
|
|
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