@vibgrate/cli 1.0.44 → 1.0.46

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.
@@ -315,6 +315,15 @@ function formatText(artifact) {
315
315
  if (artifact.extended?.architecture) {
316
316
  lines.push(...formatArchitectureDiagram(artifact.extended.architecture));
317
317
  }
318
+ if (artifact.relationshipDiagram?.mermaid) {
319
+ 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"));
320
+ lines.push(chalk.bold.cyan("\u2551 Project Relationship Diagram \u2551"));
321
+ 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"));
322
+ lines.push("");
323
+ lines.push(chalk.bold(" Mermaid"));
324
+ lines.push(chalk.dim(artifact.relationshipDiagram.mermaid));
325
+ lines.push("");
326
+ }
318
327
  const scoreColor = artifact.drift.score >= 70 ? chalk.green : artifact.drift.score >= 40 ? chalk.yellow : chalk.red;
319
328
  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"));
320
329
  lines.push(chalk.bold.cyan("\u2551 Drift Score Summary \u2551"));
@@ -469,6 +478,26 @@ function formatExtended(ext) {
469
478
  lines.push("");
470
479
  }
471
480
  }
481
+ if (ext.uiPurpose) {
482
+ const up = ext.uiPurpose;
483
+ lines.push(chalk.bold.underline(" Product Purpose Signals"));
484
+ lines.push(` Frameworks: ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") : chalk.dim("unknown")}`);
485
+ lines.push(` Evidence: ${up.topEvidence.length}${up.capped ? chalk.dim(` of ${up.evidenceCount} (capped)`) : ""}`);
486
+ const top = up.topEvidence.slice(0, 8);
487
+ if (top.length > 0) {
488
+ lines.push(" Top Signals:");
489
+ for (const item of top) {
490
+ lines.push(` - [${item.kind}] ${item.value} ${chalk.dim(`(${item.file})`)}`);
491
+ }
492
+ }
493
+ if (up.unknownSignals.length > 0) {
494
+ lines.push(" Unknowns:");
495
+ for (const u of up.unknownSignals.slice(0, 4)) {
496
+ lines.push(` - ${chalk.yellow(u)}`);
497
+ }
498
+ }
499
+ lines.push("");
500
+ }
472
501
  if (ext.owaspCategoryMapping) {
473
502
  const ow = ext.owaspCategoryMapping;
474
503
  lines.push(chalk.bold.underline(" OWASP Category Mapping"));
@@ -565,125 +594,22 @@ function formatExtended(ext) {
565
594
  }
566
595
  return lines;
567
596
  }
568
- var LAYER_LABELS = {
569
- "presentation": "Presentation",
570
- "routing": "Routing",
571
- "middleware": "Middleware",
572
- "services": "Services",
573
- "domain": "Domain",
574
- "data-access": "Data Access",
575
- "infrastructure": "Infrastructure",
576
- "config": "Config",
577
- "shared": "Shared",
578
- "testing": "Testing"
579
- };
580
- var LAYER_ICONS = {
581
- "presentation": "\u{1F5A5}",
582
- "routing": "\u{1F500}",
583
- "middleware": "\u{1F517}",
584
- "services": "\u2699",
585
- "domain": "\u{1F48E}",
586
- "data-access": "\u{1F5C4}",
587
- "infrastructure": "\u{1F3D7}",
588
- "config": "\u2699",
589
- "shared": "\u{1F4E6}",
590
- "testing": "\u{1F9EA}"
591
- };
592
597
  function formatArchitectureDiagram(arch) {
593
598
  const lines = [];
594
599
  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"));
595
600
  lines.push(chalk.bold.cyan("\u2551 Architecture Layers \u2551"));
596
601
  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"));
597
602
  lines.push("");
598
- const archetypeDisplay = arch.archetype === "unknown" ? "Unknown" : arch.archetype;
599
- const confPct = Math.round(arch.archetypeConfidence * 100);
600
- lines.push(chalk.bold(" Archetype: ") + chalk.cyan.bold(archetypeDisplay) + chalk.dim(` (${confPct}% confidence)`));
601
- lines.push(chalk.dim(` ${arch.totalClassified} files classified \xB7 ${arch.unclassified} unclassified`));
603
+ lines.push(chalk.bold(" Archetype: ") + `${arch.archetype}` + chalk.dim(` (${Math.round(arch.archetypeConfidence * 100)}% confidence)`));
604
+ lines.push(` Files classified: ${arch.totalClassified}` + (arch.unclassified > 0 ? chalk.dim(` (${arch.unclassified} unclassified)`) : ""));
602
605
  lines.push("");
603
- const boxWidth = 44;
604
- const visibleLayers = arch.layers.filter((l) => l.fileCount > 0 || l.techStack.length > 0 || l.services.length > 0);
605
- if (visibleLayers.length === 0) {
606
- lines.push(chalk.dim(" No layers detected"));
607
- lines.push("");
608
- return lines;
609
- }
610
- for (let i = 0; i < visibleLayers.length; i++) {
611
- const layer = visibleLayers[i];
612
- const icon = LAYER_ICONS[layer.layer] ?? "\xB7";
613
- const label = LAYER_LABELS[layer.layer] ?? layer.layer;
614
- const hasScore = layer.packages.length > 0;
615
- const ds = layer.driftScore;
616
- const scoreColor = !hasScore ? chalk.dim : ds >= 70 ? chalk.green : ds >= 40 ? chalk.yellow : chalk.red;
617
- const riskBadgeStr = layer.riskLevel === "low" ? chalk.bgGreen.black(" LOW ") : layer.riskLevel === "moderate" ? chalk.bgYellow.black(" MOD ") : chalk.bgRed.white(" HIGH ");
618
- if (i === 0) {
619
- lines.push(chalk.cyan(` \u250C${"\u2500".repeat(boxWidth)}\u2510`));
620
- }
621
- const nameStr = `${icon} ${label}`;
622
- const scoreStr = hasScore ? `${layer.driftScore}/100` : "n/a";
623
- const fileSuffix = `${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""}`;
624
- const leftContent = ` ${nameStr}`;
625
- const rightContent = `${fileSuffix} ${scoreStr} `;
626
- const leftLen = nameStr.length + 2;
627
- const rightLen = rightContent.length;
628
- const padLen = Math.max(1, boxWidth - leftLen - rightLen);
629
- lines.push(
630
- chalk.cyan(" \u2502") + ` ${icon} ${chalk.bold(label)}` + " ".repeat(padLen) + chalk.dim(fileSuffix) + " " + scoreColor.bold(scoreStr) + " " + chalk.cyan("\u2502")
631
- );
632
- const barWidth = boxWidth - 8;
633
- if (hasScore) {
634
- const filled = Math.round(layer.driftScore / 100 * barWidth);
635
- const empty = barWidth - filled;
636
- lines.push(
637
- chalk.cyan(" \u2502") + " " + scoreColor("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty)) + " " + chalk.cyan("\u2502")
638
- );
639
- } else {
640
- lines.push(
641
- chalk.cyan(" \u2502") + " " + chalk.dim("\xB7".repeat(barWidth)) + " " + chalk.cyan("\u2502")
642
- );
643
- }
644
- if (layer.techStack.length > 0) {
645
- const techNames = layer.techStack.slice(0, 6).map((t) => t.name);
646
- const moreCount = layer.techStack.length > 6 ? ` +${layer.techStack.length - 6}` : "";
647
- const techLine = `Tech: ${techNames.join(", ")}${moreCount}`;
648
- const truncated = techLine.length > boxWidth - 6 ? techLine.slice(0, boxWidth - 9) + "..." : techLine;
649
- const techPad = Math.max(0, boxWidth - truncated.length - 4);
650
- lines.push(
651
- chalk.cyan(" \u2502") + " " + chalk.dim(truncated) + " ".repeat(techPad) + chalk.cyan("\u2502")
652
- );
653
- }
654
- if (layer.services.length > 0) {
655
- const svcNames = layer.services.slice(0, 5).map((s) => s.name);
656
- const moreCount = layer.services.length > 5 ? ` +${layer.services.length - 5}` : "";
657
- const svcLine = `Services: ${svcNames.join(", ")}${moreCount}`;
658
- const truncated = svcLine.length > boxWidth - 6 ? svcLine.slice(0, boxWidth - 9) + "..." : svcLine;
659
- const svcPad = Math.max(0, boxWidth - truncated.length - 4);
660
- lines.push(
661
- chalk.cyan(" \u2502") + " " + chalk.dim(truncated) + " ".repeat(svcPad) + chalk.cyan("\u2502")
662
- );
663
- }
664
- const driftPkgs = layer.packages.filter((p) => p.majorsBehind !== null && p.majorsBehind > 0);
665
- if (driftPkgs.length > 0) {
666
- const worst = driftPkgs.sort((a, b) => (b.majorsBehind ?? 0) - (a.majorsBehind ?? 0));
667
- const shown = worst.slice(0, 3);
668
- const pkgStrs = shown.map((p) => {
669
- const color = (p.majorsBehind ?? 0) >= 2 ? chalk.red : chalk.yellow;
670
- return color(`${p.name} -${p.majorsBehind}`);
671
- });
672
- const moreCount = worst.length > 3 ? chalk.dim(` +${worst.length - 3}`) : "";
673
- const pkgLine = pkgStrs.join(chalk.dim(", ")) + moreCount;
674
- const roughLen = shown.map((p) => `${p.name} -${p.majorsBehind}`).join(", ").length + (worst.length > 3 ? ` +${worst.length - 3}`.length : 0);
675
- const pkgPad = Math.max(0, boxWidth - roughLen - 4);
676
- lines.push(
677
- chalk.cyan(" \u2502") + " " + pkgLine + " ".repeat(pkgPad) + chalk.cyan("\u2502")
678
- );
679
- }
680
- if (i < visibleLayers.length - 1) {
681
- lines.push(chalk.cyan(` \u251C${"\u2500".repeat(boxWidth)}\u2524`));
682
- } else {
683
- lines.push(chalk.cyan(` \u2514${"\u2500".repeat(boxWidth)}\u2518`));
606
+ if (arch.layers.length > 0) {
607
+ for (const layer of arch.layers) {
608
+ const risk = layer.riskLevel === "low" ? chalk.green("low") : layer.riskLevel === "moderate" ? chalk.yellow("moderate") : chalk.red("high");
609
+ lines.push(` ${chalk.bold(layer.layer)} ${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""} drift ${scoreBar(layer.driftScore)} risk ${risk}`);
684
610
  }
611
+ lines.push("");
685
612
  }
686
- lines.push("");
687
613
  return lines;
688
614
  }
689
615
  function generatePriorityActions(artifact) {
@@ -1169,8 +1095,8 @@ import * as semver2 from "semver";
1169
1095
  // src/utils/timeout.ts
1170
1096
  async function withTimeout(promise, ms) {
1171
1097
  let timer;
1172
- const timeout = new Promise((resolve8) => {
1173
- timer = setTimeout(() => resolve8({ ok: false }), ms);
1098
+ const timeout = new Promise((resolve9) => {
1099
+ timer = setTimeout(() => resolve9({ ok: false }), ms);
1174
1100
  });
1175
1101
  try {
1176
1102
  const result = await Promise.race([
@@ -1195,7 +1121,7 @@ function maxStable(versions) {
1195
1121
  return stable.sort(semver.rcompare)[0] ?? null;
1196
1122
  }
1197
1123
  async function npmViewJson(args, cwd) {
1198
- return new Promise((resolve8, reject) => {
1124
+ return new Promise((resolve9, reject) => {
1199
1125
  const child = spawn("npm", ["view", ...args, "--json"], {
1200
1126
  cwd,
1201
1127
  shell: true,
@@ -1213,13 +1139,13 @@ async function npmViewJson(args, cwd) {
1213
1139
  }
1214
1140
  const trimmed = out.trim();
1215
1141
  if (!trimmed) {
1216
- resolve8(null);
1142
+ resolve9(null);
1217
1143
  return;
1218
1144
  }
1219
1145
  try {
1220
- resolve8(JSON.parse(trimmed));
1146
+ resolve9(JSON.parse(trimmed));
1221
1147
  } catch {
1222
- resolve8(trimmed.replace(/^"|"$/g, ""));
1148
+ resolve9(trimmed.replace(/^"|"$/g, ""));
1223
1149
  }
1224
1150
  });
1225
1151
  });
@@ -5060,7 +4986,7 @@ var SECRET_HEURISTICS = [
5060
4986
  { detector: "private-key", pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
5061
4987
  { detector: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g }
5062
4988
  ];
5063
- var defaultRunner = (command, args) => new Promise((resolve8, reject) => {
4989
+ var defaultRunner = (command, args) => new Promise((resolve9, reject) => {
5064
4990
  const child = spawn2(command, args, { stdio: ["ignore", "pipe", "pipe"] });
5065
4991
  let stdout = "";
5066
4992
  let stderr = "";
@@ -5072,7 +4998,7 @@ var defaultRunner = (command, args) => new Promise((resolve8, reject) => {
5072
4998
  });
5073
4999
  child.on("error", reject);
5074
5000
  child.on("close", (code) => {
5075
- resolve8({ stdout, stderr, exitCode: code ?? 1 });
5001
+ resolve9({ stdout, stderr, exitCode: code ?? 1 });
5076
5002
  });
5077
5003
  });
5078
5004
  function compareSemver(a, b) {
@@ -6022,6 +5948,56 @@ function mapToolingToLayers(tooling, services, depsByLayer) {
6022
5948
  }
6023
5949
  return { layerTooling, layerServices };
6024
5950
  }
5951
+ function generateLayerFlowMermaid(layers) {
5952
+ const labels = {
5953
+ presentation: "Presentation",
5954
+ routing: "Routing",
5955
+ middleware: "Middleware",
5956
+ services: "Services",
5957
+ domain: "Domain",
5958
+ "data-access": "Data Access",
5959
+ infrastructure: "Infrastructure",
5960
+ config: "Config",
5961
+ shared: "Shared",
5962
+ testing: "Testing"
5963
+ };
5964
+ if (layers.length === 0) {
5965
+ return 'flowchart TD\n APP["Project"]';
5966
+ }
5967
+ const ordered = [...layers];
5968
+ const lines = ["flowchart TD"];
5969
+ for (let i = 0; i < ordered.length; i++) {
5970
+ const layer = ordered[i];
5971
+ lines.push(` L${i}["${labels[layer]}"]`);
5972
+ if (i > 0) lines.push(` L${i - 1} --> L${i}`);
5973
+ }
5974
+ return lines.join("\n");
5975
+ }
5976
+ async function buildProjectArchitectureMermaid(rootDir, project, archetype, cache) {
5977
+ const projectRoot = path17.resolve(rootDir, project.path || ".");
5978
+ const allFiles = await walkSourceFiles(projectRoot, cache);
5979
+ const layerSet = /* @__PURE__ */ new Set();
5980
+ for (const rel of allFiles) {
5981
+ const classification = classifyFile(rel, archetype);
5982
+ if (classification) {
5983
+ layerSet.add(classification.layer);
5984
+ }
5985
+ }
5986
+ const layerOrder = [
5987
+ "presentation",
5988
+ "routing",
5989
+ "middleware",
5990
+ "services",
5991
+ "domain",
5992
+ "data-access",
5993
+ "infrastructure",
5994
+ "config",
5995
+ "shared",
5996
+ "testing"
5997
+ ];
5998
+ const orderedLayers = layerOrder.filter((l) => layerSet.has(l));
5999
+ return generateLayerFlowMermaid(orderedLayers);
6000
+ }
6025
6001
  async function scanArchitecture(rootDir, projects, tooling, services, cache) {
6026
6002
  const { archetype, confidence: archetypeConfidence } = detectArchetype(projects);
6027
6003
  const sourceFiles = await walkSourceFiles(rootDir, cache);
@@ -6382,7 +6358,7 @@ var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
6382
6358
  ".env"
6383
6359
  ]);
6384
6360
  async function runSemgrep(args, cwd, stdin) {
6385
- return new Promise((resolve8, reject) => {
6361
+ return new Promise((resolve9, reject) => {
6386
6362
  const child = spawn3("semgrep", args, {
6387
6363
  cwd,
6388
6364
  shell: true,
@@ -6398,7 +6374,7 @@ async function runSemgrep(args, cwd, stdin) {
6398
6374
  });
6399
6375
  child.on("error", reject);
6400
6376
  child.on("close", (code) => {
6401
- resolve8({ code: code ?? 1, stdout, stderr });
6377
+ resolve9({ code: code ?? 1, stdout, stderr });
6402
6378
  });
6403
6379
  if (stdin !== void 0) child.stdin.write(stdin);
6404
6380
  child.stdin.end();
@@ -6528,6 +6504,221 @@ async function scanOwaspCategoryMapping(rootDir, cache, options = {}, runner = r
6528
6504
  };
6529
6505
  }
6530
6506
 
6507
+ // src/scanners/ui-purpose.ts
6508
+ var UI_EXTENSIONS = /* @__PURE__ */ new Set([
6509
+ ".tsx",
6510
+ ".jsx",
6511
+ ".ts",
6512
+ ".js",
6513
+ ".vue",
6514
+ ".svelte",
6515
+ ".html",
6516
+ ".mdx",
6517
+ ".json",
6518
+ ".yml",
6519
+ ".yaml"
6520
+ ]);
6521
+ var HIGH_SIGNAL_DEPENDENCIES = /* @__PURE__ */ new Set([
6522
+ "stripe",
6523
+ "posthog-js",
6524
+ "posthog-node",
6525
+ "@sentry/node",
6526
+ "@sentry/browser",
6527
+ "@sentry/react",
6528
+ "next-auth",
6529
+ "firebase",
6530
+ "auth0",
6531
+ "@auth0/auth0-react",
6532
+ "@supabase/supabase-js",
6533
+ "supabase",
6534
+ "clerk",
6535
+ "@clerk/clerk-js"
6536
+ ]);
6537
+ var GENERIC_LOW_SIGNAL = /* @__PURE__ */ new Set(["welcome", "home", "click here", "learn more", "submit", "cancel"]);
6538
+ async function scanUiPurpose(rootDir, fileCache, maxItems = 300) {
6539
+ const entries = await fileCache.walkDir(rootDir);
6540
+ const files = entries.filter((e) => e.isFile);
6541
+ const packageJsonEntry = files.find((e) => e.relPath === "package.json");
6542
+ let packageJson = {};
6543
+ if (packageJsonEntry) {
6544
+ try {
6545
+ packageJson = JSON.parse(await fileCache.readTextFile(packageJsonEntry.absPath));
6546
+ } catch {
6547
+ packageJson = {};
6548
+ }
6549
+ }
6550
+ const frameworks = detectFrameworks(packageJson);
6551
+ const items = [];
6552
+ for (const entry of files) {
6553
+ const ext = extension(entry.relPath);
6554
+ if (!UI_EXTENSIONS.has(ext)) continue;
6555
+ if (!isLikelyUiPath(entry.relPath)) {
6556
+ const routeHints2 = extractRouteHints(entry.relPath, frameworks);
6557
+ if (routeHints2.length > 0) {
6558
+ items.push(...routeHints2.map((r) => ({ ...r, file: entry.relPath })));
6559
+ }
6560
+ continue;
6561
+ }
6562
+ const routeHints = extractRouteHints(entry.relPath, frameworks);
6563
+ if (routeHints.length > 0) {
6564
+ items.push(...routeHints.map((r) => ({ ...r, file: entry.relPath })));
6565
+ }
6566
+ const src = await fileCache.readTextFile(entry.absPath);
6567
+ if (!src || src.length > 512e3) continue;
6568
+ const strings = extractUiStrings(src);
6569
+ for (const text of strings) {
6570
+ const kind = classifyString(text);
6571
+ const weight = scoreString(kind, text);
6572
+ items.push({ kind, value: text, file: entry.relPath, weight });
6573
+ }
6574
+ if (/(featureFlag|FEATURE_FLAG|launchDarkly|isFeatureEnabled|flags\.)/.test(src)) {
6575
+ items.push({ kind: "feature_flag", value: `flags in ${entry.relPath}`, file: entry.relPath, weight: 2 });
6576
+ }
6577
+ }
6578
+ const deps = getDependencies(packageJson);
6579
+ for (const [name, version] of deps) {
6580
+ if (HIGH_SIGNAL_DEPENDENCIES.has(name)) {
6581
+ items.push({ kind: "dependency", value: `${name}@${version}`, file: "package.json", weight: 4 });
6582
+ }
6583
+ }
6584
+ const deduped = dedupeByKindValue(items).sort((a, b) => b.weight - a.weight || a.value.localeCompare(b.value));
6585
+ const cappedItems = deduped.slice(0, maxItems);
6586
+ const unknownSignals = buildUnknowns(cappedItems);
6587
+ return {
6588
+ enabled: true,
6589
+ detectedFrameworks: frameworks,
6590
+ evidenceCount: deduped.length,
6591
+ capped: deduped.length > cappedItems.length,
6592
+ topEvidence: cappedItems,
6593
+ unknownSignals
6594
+ };
6595
+ }
6596
+ function extension(relPath) {
6597
+ const i = relPath.lastIndexOf(".");
6598
+ return i === -1 ? "" : relPath.slice(i).toLowerCase();
6599
+ }
6600
+ function isLikelyUiPath(relPath) {
6601
+ const lower = relPath.toLowerCase();
6602
+ return lower.startsWith("pages/") || lower.includes("/pages/") || lower.startsWith("app/") || lower.includes("/app/") || lower.startsWith("components/") || lower.includes("/components/") || lower.startsWith("ui/") || lower.includes("/ui/") || lower.startsWith("views/") || lower.includes("/views/") || lower.includes("/routes") || lower.startsWith("routes") || lower.includes("/router") || lower.startsWith("router") || lower.startsWith("locales/") || lower.includes("/locales/") || lower.startsWith("i18n/") || lower.includes("/i18n/") || lower.endsWith(".html") || lower.endsWith(".mdx");
6603
+ }
6604
+ function detectFrameworks(pkgJson) {
6605
+ const deps = {
6606
+ ...asRecord(pkgJson.dependencies),
6607
+ ...asRecord(pkgJson.devDependencies)
6608
+ };
6609
+ const hits = [];
6610
+ if (deps.next) hits.push("nextjs");
6611
+ if (deps.nuxt || deps.nuxt3) hits.push("nuxt");
6612
+ if (deps.react) hits.push("react");
6613
+ if (deps.vue) hits.push("vue");
6614
+ if (deps.svelte) hits.push("svelte");
6615
+ if (deps["@angular/core"]) hits.push("angular");
6616
+ return Array.from(new Set(hits));
6617
+ }
6618
+ function extractRouteHints(relPath, frameworks) {
6619
+ const items = [];
6620
+ if (frameworks.includes("nextjs")) {
6621
+ const m = relPath.match(/^(?:src\/)?(pages|app)\/(.+)\.(tsx|jsx|ts|js)$/);
6622
+ if (m) {
6623
+ let route = "/" + m[2].replace(/(^|\/)page$/i, "").replace(/(^|\/)route$/i, "").replace(/index$/i, "").replace(/\[(?:\.\.\.)?(.+?)\]/g, ":$1").replace(/\/+/g, "/");
6624
+ if (route !== "/" && route.endsWith("/")) route = route.slice(0, -1);
6625
+ items.push({ kind: "route", value: route || "/", weight: 5 });
6626
+ }
6627
+ }
6628
+ if (/(^|\/)(routes?|router)\.(ts|js|json)$/.test(relPath) || relPath.includes("/router/")) {
6629
+ items.push({ kind: "route", value: `route config: ${relPath}`, weight: 3 });
6630
+ }
6631
+ return items;
6632
+ }
6633
+ function extractUiStrings(src) {
6634
+ const out = [];
6635
+ const textNodeRegex = />\s*([A-Za-z0-9][^<>]{2,120}?)\s*</g;
6636
+ for (const m of src.matchAll(textNodeRegex)) {
6637
+ const s = normaliseText(m[1] ?? "");
6638
+ if (isUsefulString(s)) out.push(s);
6639
+ }
6640
+ const titleRegex = /<title>\s*([^<]{2,120})\s*<\/title>|title\s*[:=]\s*["'`](.{2,120}?)["'`]/g;
6641
+ for (const m of src.matchAll(titleRegex)) {
6642
+ const s = normaliseText((m[1] ?? m[2] ?? "").trim());
6643
+ if (isUsefulString(s)) out.push(s);
6644
+ }
6645
+ const attrRegex = /(?:aria-label|label|placeholder|alt)\s*=\s*["'`](.{2,120}?)["'`]/g;
6646
+ for (const m of src.matchAll(attrRegex)) {
6647
+ const s = normaliseText(m[1] ?? "");
6648
+ if (isUsefulString(s)) out.push(s);
6649
+ }
6650
+ const jsonValueRegex = /:\s*["'`](.{2,140}?)["'`]\s*[,\n]/g;
6651
+ for (const m of src.matchAll(jsonValueRegex)) {
6652
+ const s = normaliseText(m[1] ?? "");
6653
+ if (isUsefulString(s)) out.push(s);
6654
+ }
6655
+ return out;
6656
+ }
6657
+ function classifyString(s) {
6658
+ const lower = s.toLowerCase();
6659
+ if (/(pricing|plan|billing|subscription|trial|credit)/.test(lower)) return "copy";
6660
+ if (/(sign in|sign up|log in|register|invite|workspace|organization|sso|oauth)/.test(lower)) return "copy";
6661
+ if (/(dashboard|reports|settings|integrations|users|roles|permissions)/.test(lower)) return "heading";
6662
+ if (/(get started|start|scan|generate|export|run|deploy|upgrade|analy[sz]e)/.test(lower)) return "cta";
6663
+ if (/(overview|features|about|documentation|docs)/.test(lower)) return "title";
6664
+ if (/(menu|navigation|sidebar|breadcrumb)/.test(lower)) return "nav";
6665
+ return "copy";
6666
+ }
6667
+ function scoreString(kind, value) {
6668
+ const lower = value.toLowerCase();
6669
+ if (GENERIC_LOW_SIGNAL.has(lower)) return 0;
6670
+ let score = 1;
6671
+ if (kind === "route") score += 4;
6672
+ if (kind === "nav") score += 3;
6673
+ if (kind === "title") score += 2;
6674
+ if (kind === "heading") score += 2;
6675
+ if (kind === "cta") score += 2;
6676
+ if (/(pricing|billing|subscription|auth|security|integration)/.test(lower)) score += 2;
6677
+ if (/(dashboard|report|scan|workspace|project|repository)/.test(lower)) score += 1;
6678
+ return score;
6679
+ }
6680
+ function dedupeByKindValue(items) {
6681
+ const seen = /* @__PURE__ */ new Map();
6682
+ for (const item of items) {
6683
+ const key = `${item.kind}::${item.value.toLowerCase()}`;
6684
+ const prev = seen.get(key);
6685
+ if (!prev || item.weight > prev.weight) {
6686
+ seen.set(key, item);
6687
+ }
6688
+ }
6689
+ return Array.from(seen.values()).filter((i) => i.weight > 0);
6690
+ }
6691
+ function buildUnknowns(items) {
6692
+ const unknowns = [];
6693
+ const hasPricing = items.some((i) => /pricing|billing|subscription|trial|credit/i.test(i.value));
6694
+ const hasAuth = items.some((i) => /sign in|sign up|login|auth|sso|oauth|invite/i.test(i.value));
6695
+ const hasIntegrations = items.some((i) => /integration|webhook|api key|connector/i.test(i.value));
6696
+ const hasRoutes = items.some((i) => i.kind === "route");
6697
+ if (!hasPricing) unknowns.push("No pricing or billing evidence found.");
6698
+ if (!hasAuth) unknowns.push("No authentication or user access flow evidence found.");
6699
+ if (!hasIntegrations) unknowns.push("No integrations/connectors evidence found.");
6700
+ if (!hasRoutes) unknowns.push("No route structure evidence found.");
6701
+ return unknowns;
6702
+ }
6703
+ function asRecord(value) {
6704
+ if (!value || typeof value !== "object") return {};
6705
+ return value;
6706
+ }
6707
+ function getDependencies(pkgJson) {
6708
+ return Object.entries(asRecord(pkgJson.dependencies));
6709
+ }
6710
+ function normaliseText(s) {
6711
+ return s.replace(/\s+/g, " ").trim();
6712
+ }
6713
+ function isUsefulString(s) {
6714
+ if (!s || s.length < 3 || s.length > 160) return false;
6715
+ if (/^[0-9_./-]+$/.test(s)) return false;
6716
+ if (/^(true|false|null|undefined)$/i.test(s)) return false;
6717
+ if (/[<>{}]/.test(s)) return false;
6718
+ if (/function\s*\(|=>|console\.|import\s+|export\s+/.test(s)) return false;
6719
+ return true;
6720
+ }
6721
+
6531
6722
  // src/utils/tool-installer.ts
6532
6723
  import { spawn as spawn4 } from "child_process";
6533
6724
  import chalk5 from "chalk";
@@ -6538,7 +6729,7 @@ var SECURITY_TOOLS = [
6538
6729
  ];
6539
6730
  var IS_WIN = process.platform === "win32";
6540
6731
  function runCommand(cmd, args) {
6541
- return new Promise((resolve8) => {
6732
+ return new Promise((resolve9) => {
6542
6733
  const child = spawn4(cmd, args, {
6543
6734
  stdio: ["ignore", "pipe", "pipe"],
6544
6735
  shell: IS_WIN
@@ -6552,8 +6743,8 @@ function runCommand(cmd, args) {
6552
6743
  child.stderr.on("data", (d) => {
6553
6744
  stderr += d.toString();
6554
6745
  });
6555
- child.on("error", () => resolve8({ exitCode: 127, stdout, stderr }));
6556
- child.on("close", (code) => resolve8({ exitCode: code ?? 1, stdout, stderr }));
6746
+ child.on("error", () => resolve9({ exitCode: 127, stdout, stderr }));
6747
+ child.on("close", (code) => resolve9({ exitCode: code ?? 1, stdout, stderr }));
6557
6748
  });
6558
6749
  }
6559
6750
  async function commandExists(command) {
@@ -6620,6 +6811,76 @@ async function installMissingTools(log = (m) => process.stderr.write(m + "\n"))
6620
6811
  return result;
6621
6812
  }
6622
6813
 
6814
+ // src/utils/mermaid.ts
6815
+ function sanitizeId(input) {
6816
+ return input.replace(/[^a-zA-Z0-9_]/g, "_");
6817
+ }
6818
+ function escapeLabel(input) {
6819
+ return input.replace(/"/g, '\\"');
6820
+ }
6821
+ function scoreClass(score) {
6822
+ if (score === void 0 || Number.isNaN(score)) return "scoreUnknown";
6823
+ if (score >= 70) return "scoreHigh";
6824
+ if (score >= 40) return "scoreModerate";
6825
+ return "scoreLow";
6826
+ }
6827
+ function nodeLabel(project) {
6828
+ const score = project.drift?.score;
6829
+ const scoreText = typeof score === "number" ? ` (${score})` : " (n/a)";
6830
+ return `${project.name}${scoreText}`;
6831
+ }
6832
+ function buildDefs() {
6833
+ return [
6834
+ "classDef scoreHigh fill:#064e3b,stroke:#10b981,color:#d1fae5,stroke-width:2px",
6835
+ "classDef scoreModerate fill:#78350f,stroke:#f59e0b,color:#fef3c7,stroke-width:2px",
6836
+ "classDef scoreLow fill:#7f1d1d,stroke:#ef4444,color:#fee2e2,stroke-width:2px",
6837
+ "classDef scoreUnknown fill:#334155,stroke:#94a3b8,color:#e2e8f0,stroke-width:2px"
6838
+ ];
6839
+ }
6840
+ function generateWorkspaceRelationshipMermaid(projects) {
6841
+ const lines = ["flowchart LR"];
6842
+ const byPath = new Map(projects.map((p) => [p.path, p]));
6843
+ for (const project of projects) {
6844
+ const id = sanitizeId(project.projectId || project.path || project.name);
6845
+ lines.push(`${id}["${escapeLabel(nodeLabel(project))}"]`);
6846
+ lines.push(`class ${id} ${scoreClass(project.drift?.score)}`);
6847
+ }
6848
+ for (const project of projects) {
6849
+ const fromId = sanitizeId(project.projectId || project.path || project.name);
6850
+ for (const ref of project.projectReferences ?? []) {
6851
+ const target = byPath.get(ref.path);
6852
+ if (!target) continue;
6853
+ const toId = sanitizeId(target.projectId || target.path || target.name);
6854
+ lines.push(`${fromId} --> ${toId}`);
6855
+ }
6856
+ }
6857
+ lines.push(...buildDefs());
6858
+ return { mermaid: lines.join("\n") };
6859
+ }
6860
+ function generateProjectRelationshipMermaid(project, projects) {
6861
+ const lines = ["flowchart LR"];
6862
+ const byPath = new Map(projects.map((p) => [p.path, p]));
6863
+ const parents = projects.filter((p) => p.projectReferences?.some((r) => r.path === project.path));
6864
+ const children = (project.projectReferences ?? []).map((r) => byPath.get(r.path)).filter((p) => Boolean(p));
6865
+ const centerId = sanitizeId(project.projectId || project.path || project.name);
6866
+ lines.push(`${centerId}["${escapeLabel(nodeLabel(project))}"]`);
6867
+ lines.push(`class ${centerId} ${scoreClass(project.drift?.score)}`);
6868
+ for (const parent of parents) {
6869
+ const id = sanitizeId(parent.projectId || parent.path || parent.name);
6870
+ lines.push(`${id}["${escapeLabel(nodeLabel(parent))}"]`);
6871
+ lines.push(`class ${id} ${scoreClass(parent.drift?.score)}`);
6872
+ lines.push(`${id} --> ${centerId}`);
6873
+ }
6874
+ for (const child of children) {
6875
+ const id = sanitizeId(child.projectId || child.path || child.name);
6876
+ lines.push(`${id}["${escapeLabel(nodeLabel(child))}"]`);
6877
+ lines.push(`class ${id} ${scoreClass(child.drift?.score)}`);
6878
+ lines.push(`${centerId} --> ${id}`);
6879
+ }
6880
+ lines.push(...buildDefs());
6881
+ return { mermaid: lines.join("\n") };
6882
+ }
6883
+
6623
6884
  // src/commands/scan.ts
6624
6885
  async function runScan(rootDir, opts) {
6625
6886
  const scanStart = Date.now();
@@ -6659,7 +6920,8 @@ async function runScan(rootDir, opts) {
6659
6920
  ...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
6660
6921
  ...scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : [],
6661
6922
  ...scanners?.codeQuality?.enabled !== false ? [{ id: "codequality", label: "Code quality metrics" }] : [],
6662
- ...scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : []
6923
+ ...scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : [],
6924
+ ...opts.uiPurpose || scanners?.uiPurpose?.enabled === true ? [{ id: "uipurpose", label: "UI purpose evidence" }] : []
6663
6925
  ] : [],
6664
6926
  { id: "drift", label: "Computing drift score" },
6665
6927
  { id: "findings", label: "Generating findings" }
@@ -6748,6 +7010,10 @@ async function runScan(rootDir, opts) {
6748
7010
  project.drift = computeDriftScore([project]);
6749
7011
  project.projectId = computeProjectId(project.path, project.name, workspaceId);
6750
7012
  }
7013
+ for (const project of allProjects) {
7014
+ project.relationshipDiagram = generateProjectRelationshipMermaid(project, allProjects);
7015
+ }
7016
+ const relationshipDiagram = generateWorkspaceRelationshipMermaid(allProjects);
6751
7017
  const extended = {};
6752
7018
  if (scanners !== false) {
6753
7019
  const scannerTasks = [];
@@ -6923,6 +7189,13 @@ async function runScan(rootDir, opts) {
6923
7189
  );
6924
7190
  }
6925
7191
  }
7192
+ if (opts.uiPurpose || scanners?.uiPurpose?.enabled === true) {
7193
+ progress.startStep("uipurpose");
7194
+ extended.uiPurpose = await scanUiPurpose(rootDir, fileCache);
7195
+ const up = extended.uiPurpose;
7196
+ const summary = [`${up.topEvidence.length} evidence`, ...up.capped ? ["capped"] : []].join(" \xB7 ");
7197
+ progress.completeStep("uipurpose", summary, up.topEvidence.length);
7198
+ }
6926
7199
  if (scanners?.architecture?.enabled !== false) {
6927
7200
  progress.startStep("architecture");
6928
7201
  extended.architecture = await scanArchitecture(
@@ -6934,6 +7207,14 @@ async function runScan(rootDir, opts) {
6934
7207
  );
6935
7208
  const arch = extended.architecture;
6936
7209
  const layerCount = arch.layers.filter((l) => l.fileCount > 0).length;
7210
+ await Promise.all(allProjects.map(async (project) => {
7211
+ project.architectureMermaid = await buildProjectArchitectureMermaid(
7212
+ rootDir,
7213
+ project,
7214
+ arch.archetype,
7215
+ fileCache
7216
+ );
7217
+ }));
6937
7218
  progress.completeStep(
6938
7219
  "architecture",
6939
7220
  `${arch.archetype} \xB7 ${layerCount} layer${layerCount !== 1 ? "s" : ""} \xB7 ${arch.totalClassified} files`,
@@ -7001,6 +7282,7 @@ async function runScan(rootDir, opts) {
7001
7282
  }
7002
7283
  if (extended.codeQuality) filesScanned += extended.codeQuality.filesAnalyzed;
7003
7284
  if (extended.owaspCategoryMapping) filesScanned += extended.owaspCategoryMapping.scannedFiles;
7285
+ if (extended.uiPurpose) filesScanned += extended.uiPurpose.topEvidence.length;
7004
7286
  const durationMs = Date.now() - scanStart;
7005
7287
  const artifact = {
7006
7288
  schemaVersion: "1.0",
@@ -7014,7 +7296,8 @@ async function runScan(rootDir, opts) {
7014
7296
  ...Object.keys(extended).length > 0 ? { extended } : {},
7015
7297
  durationMs,
7016
7298
  filesScanned,
7017
- treeSummary: treeCount
7299
+ treeSummary: treeCount,
7300
+ relationshipDiagram
7018
7301
  };
7019
7302
  if (opts.baseline) {
7020
7303
  const baselinePath = path20.resolve(opts.baseline);
@@ -7145,7 +7428,15 @@ async function autoPush(artifact, rootDir, opts) {
7145
7428
  if (opts.strict) process.exit(1);
7146
7429
  }
7147
7430
  }
7148
- 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").option("--install-tools", "Auto-install missing security scanners via Homebrew").action(async (targetPath, opts) => {
7431
+ function parseNonNegativeNumber(value, label) {
7432
+ if (value === void 0) return void 0;
7433
+ const parsed = Number(value);
7434
+ if (!Number.isFinite(parsed) || parsed < 0) {
7435
+ throw new Error(`${label} must be a non-negative number.`);
7436
+ }
7437
+ return parsed;
7438
+ }
7439
+ 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").option("--install-tools", "Auto-install missing security scanners via Homebrew").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--drift-budget <score>", "Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").action(async (targetPath, opts) => {
7149
7440
  const rootDir = path20.resolve(targetPath);
7150
7441
  if (!await pathExists(rootDir)) {
7151
7442
  console.error(chalk6.red(`Path does not exist: ${rootDir}`));
@@ -7162,7 +7453,10 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
7162
7453
  dsn: opts.dsn,
7163
7454
  region: opts.region,
7164
7455
  strict: opts.strict,
7165
- installTools: opts.installTools
7456
+ installTools: opts.installTools,
7457
+ uiPurpose: opts.uiPurpose,
7458
+ driftBudget: parseNonNegativeNumber(opts.driftBudget, "--drift-budget"),
7459
+ driftWorseningPercent: parseNonNegativeNumber(opts.driftWorsening, "--drift-worsening")
7166
7460
  };
7167
7461
  const artifact = await runScan(rootDir, scanOpts);
7168
7462
  if (opts.failOn) {
@@ -7179,6 +7473,27 @@ Failing: findings detected at warn level or above.`));
7179
7473
  process.exit(2);
7180
7474
  }
7181
7475
  }
7476
+ if (scanOpts.driftBudget !== void 0 && artifact.drift.score > scanOpts.driftBudget) {
7477
+ console.error(chalk6.red(`
7478
+ Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
7479
+ process.exit(2);
7480
+ }
7481
+ if (scanOpts.driftWorseningPercent !== void 0) {
7482
+ if (artifact.delta === void 0) {
7483
+ console.error(chalk6.red("\nFailing fitness function: --drift-worsening requires --baseline to compare against previous drift."));
7484
+ process.exit(2);
7485
+ }
7486
+ if (artifact.delta > 0) {
7487
+ const baselineScore = artifact.drift.score - artifact.delta;
7488
+ const denominator = Math.max(Math.abs(baselineScore), 1e-4);
7489
+ const worseningPercent = artifact.delta / denominator * 100;
7490
+ if (worseningPercent > scanOpts.driftWorseningPercent) {
7491
+ console.error(chalk6.red(`
7492
+ Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (threshold ${scanOpts.driftWorseningPercent}%).`));
7493
+ process.exit(2);
7494
+ }
7495
+ }
7496
+ }
7182
7497
  const hasDsn = !!(opts.dsn || process.env.VIBGRATE_DSN);
7183
7498
  if (opts.push || hasDsn) {
7184
7499
  await autoPush(artifact, rootDir, scanOpts);