@launchsecure/launch-kit 0.0.12 → 0.0.14

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.
@@ -54,46 +54,19 @@ var init_config = __esm({
54
54
  }
55
55
  });
56
56
 
57
- // src/server/chart-serve.ts
58
- var chart_serve_exports = {};
59
- __export(chart_serve_exports, {
60
- runServeCli: () => runServeCli,
61
- startChartServer: () => startChartServer
62
- });
63
- module.exports = __toCommonJS(chart_serve_exports);
64
- var import_node_http = __toESM(require("node:http"));
65
- var import_node_fs13 = __toESM(require("node:fs"));
66
- var import_node_path15 = __toESM(require("node:path"));
67
-
68
- // src/server/graph/index.ts
69
- var import_node_fs11 = require("node:fs");
70
- var import_node_path13 = require("node:path");
71
-
72
- // src/server/graph/core/graph-builder.ts
73
- var import_node_fs8 = require("node:fs");
74
- var import_node_path9 = require("node:path");
75
- init_config();
76
-
77
- // src/server/graph/core/parser-registry.ts
78
- var import_node_path8 = require("node:path");
79
-
80
- // src/server/graph/parsers/ui/react-nextjs.ts
81
- var import_node_fs3 = require("node:fs");
82
- var import_node_path3 = require("node:path");
83
-
84
57
  // src/server/graph/core/ts-extractor.ts
85
- var import_node_fs2 = require("node:fs");
86
- var import_node_path2 = require("node:path");
87
- var tsxLanguage;
88
- var parserInstance;
89
- var initPromise;
90
- var initialized = false;
91
- var queriesDir = (() => {
92
- const srcPath = (0, import_node_path2.join)((0, import_node_path2.dirname)(__filename), "..", "queries");
93
- if (require("fs").existsSync(srcPath)) return srcPath;
94
- return (0, import_node_path2.join)((0, import_node_path2.dirname)(__filename), "graph", "queries");
95
- })();
96
- var queryCache = /* @__PURE__ */ new Map();
58
+ var ts_extractor_exports = {};
59
+ __export(ts_extractor_exports, {
60
+ classifyFile: () => classifyFile,
61
+ createQuery: () => createQuery,
62
+ extractAuthWrappersTS: () => extractAuthWrappersTS,
63
+ extractDbCallsTS: () => extractDbCallsTS,
64
+ extractDeep: () => extractDeep,
65
+ initTreeSitter: () => initTreeSitter,
66
+ parseCodeTS: () => parseCodeTS,
67
+ parseFileTS: () => parseFileTS,
68
+ setExtractorConfig: () => setExtractorConfig
69
+ });
97
70
  async function initTreeSitter() {
98
71
  if (initialized) return;
99
72
  if (initPromise) return initPromise;
@@ -117,31 +90,25 @@ function getQuery(name) {
117
90
  ensureInit();
118
91
  const cached = queryCache.get(name);
119
92
  if (cached) return cached;
120
- const scmPath = (0, import_node_path2.join)(queriesDir, `${name}.scm`);
121
- const scm = (0, import_node_fs2.readFileSync)(scmPath, "utf-8");
93
+ const scmPath = (0, import_node_path3.join)(queriesDir, `${name}.scm`);
94
+ const scm = (0, import_node_fs3.readFileSync)(scmPath, "utf-8");
122
95
  const query = tsxLanguage.query(scm);
123
96
  queryCache.set(name, query);
124
97
  return query;
125
98
  }
126
99
  function parseSource(absPath) {
127
100
  ensureInit();
128
- const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
101
+ const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
129
102
  return parserInstance.parse(content);
130
103
  }
131
- var PRISMA_MUTATION_METHODS_BUILTIN = [
132
- "create",
133
- "createMany",
134
- "createManyAndReturn",
135
- "update",
136
- "updateMany",
137
- "updateManyAndReturn",
138
- "upsert",
139
- "delete",
140
- "deleteMany"
141
- ];
142
- var DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
143
- var extraDbIdentifiers = [];
144
- var extraMutationMethods = [];
104
+ function parseCodeTS(code) {
105
+ ensureInit();
106
+ return parserInstance.parse(code);
107
+ }
108
+ function createQuery(pattern) {
109
+ ensureInit();
110
+ return tsxLanguage.query(pattern);
111
+ }
145
112
  function setExtractorConfig(config) {
146
113
  extraDbIdentifiers = config.dbIdentifiers ?? [];
147
114
  extraMutationMethods = config.mutationMethods ?? [];
@@ -408,6 +375,24 @@ function extractDbCallsTS(absPath) {
408
375
  }
409
376
  return calls;
410
377
  }
378
+ function classifyFile(absPath) {
379
+ const fileName = require("path").basename(absPath);
380
+ if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("__test")) return "test";
381
+ if (fileName.includes(".stories.")) return "story";
382
+ const tree = parseSource(absPath);
383
+ const root = tree.rootNode;
384
+ const classifyQuery = getQuery("classify");
385
+ const captures = classifyQuery.captures(root);
386
+ const capNames = new Set(captures.map((c) => c.name));
387
+ if (capNames.has("http_export") || capNames.has("http_export_fn")) return "endpoint";
388
+ if (fileName === "page.tsx" && capNames.has("has_jsx")) return "page";
389
+ if (fileName === "layout.tsx" && capNames.has("has_jsx")) return "layout";
390
+ if (capNames.has("has_create_context") || capNames.has("has_create_context_bare")) return "context";
391
+ if (capNames.has("has_jsx")) return "component";
392
+ if (capNames.has("hook_decl") || capNames.has("hook_const")) return "hook";
393
+ if (fileName.includes("config") || fileName.includes(".config.")) return "config";
394
+ return "lib";
395
+ }
411
396
  function extractAuthWrappersTS(absPath) {
412
397
  const tree = parseSource(absPath);
413
398
  const root = tree.rootNode;
@@ -578,17 +563,108 @@ function extractDeep(absPath) {
578
563
  }
579
564
  return { elements, stateVars, conditions, variables, responses, params };
580
565
  }
566
+ var import_node_fs3, import_node_path3, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
567
+ var init_ts_extractor = __esm({
568
+ "src/server/graph/core/ts-extractor.ts"() {
569
+ "use strict";
570
+ import_node_fs3 = require("node:fs");
571
+ import_node_path3 = require("node:path");
572
+ initialized = false;
573
+ queriesDir = (() => {
574
+ const srcPath = (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "..", "queries");
575
+ if (require("fs").existsSync(srcPath)) return srcPath;
576
+ return (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "graph", "queries");
577
+ })();
578
+ queryCache = /* @__PURE__ */ new Map();
579
+ PRISMA_MUTATION_METHODS_BUILTIN = [
580
+ "create",
581
+ "createMany",
582
+ "createManyAndReturn",
583
+ "update",
584
+ "updateMany",
585
+ "updateManyAndReturn",
586
+ "upsert",
587
+ "delete",
588
+ "deleteMany"
589
+ ];
590
+ DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
591
+ extraDbIdentifiers = [];
592
+ extraMutationMethods = [];
593
+ }
594
+ });
595
+
596
+ // src/server/chart-serve.ts
597
+ var chart_serve_exports = {};
598
+ __export(chart_serve_exports, {
599
+ runServeCli: () => runServeCli,
600
+ startChartServer: () => startChartServer
601
+ });
602
+ module.exports = __toCommonJS(chart_serve_exports);
603
+ var import_node_http = __toESM(require("node:http"));
604
+ var import_node_fs17 = __toESM(require("node:fs"));
605
+ var import_node_path19 = __toESM(require("node:path"));
606
+
607
+ // src/server/graph/index.ts
608
+ var import_node_fs14 = require("node:fs");
609
+ var import_node_path16 = require("node:path");
610
+
611
+ // src/server/graph/core/graph-builder.ts
612
+ var import_node_fs11 = require("node:fs");
613
+ var import_node_path12 = require("node:path");
614
+ init_config();
615
+
616
+ // src/server/graph/core/parser-registry.ts
617
+ var import_node_path11 = require("node:path");
618
+
619
+ // src/server/graph/parsers/ts/typescript-project.ts
620
+ var import_node_fs4 = require("node:fs");
621
+ var import_node_path4 = require("node:path");
622
+ init_config();
623
+
624
+ // src/server/graph/core/resolve-paths.ts
625
+ var import_node_fs2 = require("node:fs");
626
+ var import_node_path2 = require("node:path");
627
+ function resolveProjectPaths(rootDir, config) {
628
+ if (config.paths?.appDir) {
629
+ const appDir = (0, import_node_path2.join)(rootDir, config.paths.appDir);
630
+ const srcDir = config.paths.srcDir ? (0, import_node_path2.join)(rootDir, config.paths.srcDir) : (0, import_node_path2.dirname)(appDir);
631
+ return { srcDir, appDir, apiDir: (0, import_node_path2.join)(appDir, "api") };
632
+ }
633
+ const srcApp = (0, import_node_path2.join)(rootDir, "src", "app");
634
+ if ((0, import_node_fs2.existsSync)(srcApp)) {
635
+ return { srcDir: (0, import_node_path2.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path2.join)(srcApp, "api") };
636
+ }
637
+ const rootApp = (0, import_node_path2.join)(rootDir, "app");
638
+ if ((0, import_node_fs2.existsSync)(rootApp)) {
639
+ return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path2.join)(rootApp, "api") };
640
+ }
641
+ return null;
642
+ }
581
643
 
582
- // src/server/graph/parsers/ui/react-nextjs.ts
583
- var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
644
+ // src/server/graph/parsers/ts/typescript-project.ts
645
+ init_ts_extractor();
646
+ var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
647
+ var CLASSIFICATION_TO_LAYER = {
648
+ endpoint: "api",
649
+ page: "ui",
650
+ layout: "ui",
651
+ component: "ui",
652
+ ui: "ui",
653
+ hook: "ui",
654
+ context: "ui",
655
+ config: "ui",
656
+ lib: "ui",
657
+ "mcp-tool": "ui",
658
+ external: "ui"
659
+ };
584
660
  function walk(dir, exts) {
585
661
  const results = [];
586
- if (!(0, import_node_fs3.existsSync)(dir)) return results;
587
- for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
588
- const full = (0, import_node_path3.join)(dir, entry.name);
662
+ if (!(0, import_node_fs4.existsSync)(dir)) return results;
663
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
664
+ const full = (0, import_node_path4.join)(dir, entry.name);
589
665
  if (entry.isDirectory()) {
590
666
  results.push(...walk(full, exts));
591
- } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
667
+ } else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
592
668
  results.push(full);
593
669
  }
594
670
  }
@@ -596,33 +672,33 @@ function walk(dir, exts) {
596
672
  }
597
673
  function walkWithIgnore(dir, exts, ignoreDirs) {
598
674
  const results = [];
599
- if (!(0, import_node_fs3.existsSync)(dir)) return results;
600
- for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
675
+ if (!(0, import_node_fs4.existsSync)(dir)) return results;
676
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
601
677
  if (entry.isDirectory()) {
602
678
  if (ignoreDirs.has(entry.name)) continue;
603
- results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, ignoreDirs));
604
- } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
605
- results.push((0, import_node_path3.join)(dir, entry.name));
679
+ results.push(...walkWithIgnore((0, import_node_path4.join)(dir, entry.name), exts, ignoreDirs));
680
+ } else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
681
+ results.push((0, import_node_path4.join)(dir, entry.name));
606
682
  }
607
683
  }
608
684
  return results;
609
685
  }
610
686
  function toNodeId(srcDir, absPath) {
611
- return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
687
+ return (0, import_node_path4.relative)(srcDir, absPath).replace(/\\/g, "/");
612
688
  }
613
689
  function resolveImport(srcDir, specifier) {
614
690
  if (!specifier.startsWith("@/")) return null;
615
691
  const rel = specifier.slice(2);
616
- const base = (0, import_node_path3.join)(srcDir, rel);
617
- for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path3.join)(base, "index.ts"), (0, import_node_path3.join)(base, "index.tsx")]) {
618
- if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
692
+ const base = (0, import_node_path4.join)(srcDir, rel);
693
+ for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
694
+ if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
619
695
  }
620
696
  return null;
621
697
  }
622
698
  function resolveRelativeImport(fromFile, specifier) {
623
- const base = (0, import_node_path3.join)((0, import_node_path3.dirname)(fromFile), specifier);
624
- for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path3.join)(base, "index.ts"), (0, import_node_path3.join)(base, "index.tsx")]) {
625
- if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
699
+ const base = (0, import_node_path4.join)((0, import_node_path4.dirname)(fromFile), specifier);
700
+ for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
701
+ if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
626
702
  }
627
703
  return null;
628
704
  }
@@ -643,7 +719,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
643
719
  const resolved = resolveRelativeImport(barrelAbsPath, re.from);
644
720
  if (!resolved) continue;
645
721
  if (re.isWildcard) {
646
- const targetBn = (0, import_node_path3.basename)(resolved);
722
+ const targetBn = (0, import_node_path4.basename)(resolved);
647
723
  const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
648
724
  if (targetIsBarrel) {
649
725
  const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
@@ -670,31 +746,21 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
670
746
  const barrels = /* @__PURE__ */ new Map();
671
747
  const memo = /* @__PURE__ */ new Map();
672
748
  for (const [absPath, parsed] of parsedByPath) {
673
- const bn = (0, import_node_path3.basename)(absPath);
749
+ const bn = (0, import_node_path4.basename)(absPath);
674
750
  if (bn !== "index.ts" && bn !== "index.tsx") continue;
675
751
  if (parsed.reExports.length === 0) continue;
676
752
  const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
677
753
  if (map.size > 0) {
678
- const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
754
+ const barrelId = (0, import_node_path4.relative)(srcDir, (0, import_node_path4.dirname)(absPath)).replace(/\\/g, "/");
679
755
  barrels.set(barrelId, map);
680
756
  }
681
757
  }
682
758
  return barrels;
683
759
  }
684
- function classifyType(id) {
685
- if (id.endsWith("/page.tsx")) return "page";
686
- if (id.endsWith("/layout.tsx")) return "layout";
687
- if (id.startsWith("client/components/ui/")) return "ui";
688
- if (id.startsWith("client/components/")) return "component";
689
- if (id.startsWith("client/hooks/")) return "hook";
690
- if (/client\/lib\/.*-context\./.test(id)) return "context";
691
- if (id.startsWith("client/lib/")) return id.includes("config") ? "config" : "util";
692
- if (id.startsWith("client/api/")) return "util";
693
- if (id.startsWith("server/mcp/")) return "mcp-tool";
694
- if (id.startsWith("server/lib/")) return "lib";
695
- if (id.startsWith("server/")) return "lib";
696
- if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
697
- return "component";
760
+ function classifyType(absPath, id) {
761
+ const contentType = classifyFile(absPath);
762
+ if (contentType === "lib" && id.startsWith("server/mcp/")) return "mcp-tool";
763
+ return contentType;
698
764
  }
699
765
  function extractRoute(id) {
700
766
  if (!id.endsWith("/page.tsx")) return null;
@@ -706,7 +772,18 @@ function extractRoute(id) {
706
772
  return route || "/";
707
773
  }
708
774
  function nameFromFilename(absPath) {
709
- return (0, import_node_path3.basename)(absPath, (0, import_node_path3.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
775
+ return (0, import_node_path4.basename)(absPath, (0, import_node_path4.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
776
+ }
777
+ function filePathToApiRoute(apiDir, absPath) {
778
+ let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
779
+ route = route.replace(/\[([^\]]+)\]/g, ":$1");
780
+ route = route.replace(/\/+/g, "/");
781
+ if (route === "/") return "/api";
782
+ return "/api" + route;
783
+ }
784
+ function camelToPascal(s) {
785
+ if (!s) return s;
786
+ return s.charAt(0).toUpperCase() + s.slice(1);
710
787
  }
711
788
  function resolveTemplateLiteralRoute(template, routeToNodeId) {
712
789
  const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -787,7 +864,7 @@ function matchRouteToPage(route, routeToNodeId) {
787
864
  if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
788
865
  return null;
789
866
  }
790
- function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
867
+ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps, routeToNodeId) {
791
868
  const edges = [];
792
869
  const flagged = [];
793
870
  const seen = /* @__PURE__ */ new Set();
@@ -799,13 +876,10 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
799
876
  if (label) edge.label = label;
800
877
  edges.push(edge);
801
878
  }
802
- function edgeTypeFor(targetId, isTypeOnlyImport, importedNames) {
879
+ function edgeTypeFor(isTypeOnlyImport, importedNames) {
803
880
  if (isTypeOnlyImport) return "imports";
804
- const targetType = nodeTypeMap.get(targetId);
805
- if (targetType && RENDER_TYPES.has(targetType)) {
806
- const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
807
- if (anyRendered) return "renders";
808
- }
881
+ const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
882
+ if (anyRendered) return "renders";
809
883
  return "imports";
810
884
  }
811
885
  for (const imp of parsed.imports) {
@@ -827,14 +901,14 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
827
901
  }
828
902
  for (const [targetId, targetNames] of byTarget) {
829
903
  const allType = isTypeOnly || targetNames.every((n) => typeNames.has(n));
830
- addEdge(targetId, edgeTypeFor(targetId, allType, targetNames));
904
+ addEdge(targetId, edgeTypeFor(allType, targetNames));
831
905
  }
832
906
  } else {
833
907
  const resolved = resolveImport(srcDir, specifier);
834
908
  if (resolved) {
835
909
  const targetId = toNodeId(srcDir, resolved);
836
910
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
837
- addEdge(targetId, edgeTypeFor(targetId, isTypeOnly, names));
911
+ addEdge(targetId, edgeTypeFor(isTypeOnly, names));
838
912
  }
839
913
  }
840
914
  }
@@ -843,7 +917,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
843
917
  if (resolved) {
844
918
  const targetId = toNodeId(srcDir, resolved);
845
919
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
846
- addEdge(targetId, edgeTypeFor(targetId, isTypeOnly, names));
920
+ addEdge(targetId, edgeTypeFor(isTypeOnly, names));
847
921
  }
848
922
  }
849
923
  }
@@ -885,74 +959,113 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
885
959
  }
886
960
  return { edges, flagged };
887
961
  }
962
+ function hasNextConfig(rootDir) {
963
+ return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.ts")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.js")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.mjs"));
964
+ }
888
965
  function detect(rootDir) {
889
- return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "src", "app")) && (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.ts")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.js")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.mjs"));
966
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
967
+ return paths !== null && hasNextConfig(rootDir);
890
968
  }
891
969
  function generate(rootDir) {
892
- const srcDir = (0, import_node_path3.join)(rootDir, "src");
893
- const appFiles = walk((0, import_node_path3.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
894
- (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
895
- );
896
- const clientFiles = walk((0, import_node_path3.join)(srcDir, "client"), [".tsx", ".ts"]);
897
- const serverFiles = walk((0, import_node_path3.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
898
- (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
899
- );
900
- const libFiles = walk((0, import_node_path3.join)(srcDir, "lib"), [".ts", ".tsx"]);
901
- const configFiles = walk((0, import_node_path3.join)(srcDir, "config"), [".ts", ".tsx"]);
970
+ const config = loadConfig(rootDir);
971
+ const paths = resolveProjectPaths(rootDir, config);
972
+ const srcDir = paths.srcDir;
973
+ const apiDir = paths.apiDir;
974
+ const appFiles = walk(paths.appDir, [".tsx", ".ts"]);
975
+ const clientFiles = walk((0, import_node_path4.join)(srcDir, "client"), [".tsx", ".ts"]);
976
+ const serverFiles = walk((0, import_node_path4.join)(srcDir, "server"), [".ts", ".tsx"]);
977
+ const libFiles = walk((0, import_node_path4.join)(srcDir, "lib"), [".ts", ".tsx"]);
978
+ const configFiles = walk((0, import_node_path4.join)(srcDir, "config"), [".ts", ".tsx"]);
902
979
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
903
980
  const parsedByPath = /* @__PURE__ */ new Map();
904
981
  for (const absPath of allDiscovered) {
905
982
  parsedByPath.set(absPath, parseFileTS(absPath));
906
983
  }
907
984
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
908
- const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
909
- const nodes = [];
985
+ const uiNodes = [];
986
+ const apiNodes = [];
910
987
  const nodeIdSet = /* @__PURE__ */ new Set();
911
- const nodeTypeMap = /* @__PURE__ */ new Map();
912
988
  const routeToNodeId = /* @__PURE__ */ new Map();
989
+ const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
913
990
  for (const absPath of fileSet) {
914
991
  const id = toNodeId(srcDir, absPath);
915
- const type = classifyType(id);
992
+ const type = classifyType(absPath, id);
993
+ if (type === "test" || type === "story") continue;
916
994
  const parsed = parsedByPath.get(absPath);
917
995
  const name = parsed.name || nameFromFilename(absPath);
918
- const route = extractRoute(id);
919
- const deep = extractDeep(absPath);
920
- nodes.push({
921
- id,
922
- type,
923
- name,
924
- route,
925
- exports: parsed.exports,
926
- elements: deep.elements,
927
- stateVars: deep.stateVars,
928
- conditions: deep.conditions,
929
- variables: deep.variables
930
- });
996
+ const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
931
997
  nodeIdSet.add(id);
932
- nodeTypeMap.set(id, type);
933
- if (route) routeToNodeId.set(route, id);
998
+ if (layer === "api") {
999
+ const methods = [];
1000
+ for (const exp of parsed.exports) {
1001
+ if (HTTP_METHODS.has(exp)) methods.push(exp);
1002
+ }
1003
+ const dbCalls = extractDbCallsTS(absPath);
1004
+ const authWrappers = extractAuthWrappersTS(absPath);
1005
+ const deep = extractDeep(absPath);
1006
+ const routePath = (0, import_node_fs4.existsSync)(apiDir) ? filePathToApiRoute(apiDir, absPath) : `/api/${id.replace(/\/route\.tsx?$/, "")}`;
1007
+ const mutations = dbCalls.filter((c) => c.isMutation);
1008
+ const mutates = mutations.length > 0;
1009
+ const authStrategy = [...authWrappers];
1010
+ apiNodes.push({
1011
+ id,
1012
+ type: "endpoint",
1013
+ name: routePath,
1014
+ layer: "api",
1015
+ path: routePath,
1016
+ methods,
1017
+ handler: id,
1018
+ mutates,
1019
+ auth: authStrategy.length > 0 ? authStrategy : ["public"],
1020
+ db_models: [...new Set(dbCalls.map((c) => c.model))],
1021
+ db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
1022
+ conditions: deep.conditions,
1023
+ variables: deep.variables,
1024
+ responses: deep.responses,
1025
+ params: deep.params,
1026
+ _dbCalls: dbCalls
1027
+ // temp: used for cross-ref building below
1028
+ });
1029
+ } else {
1030
+ const route = extractRoute(id);
1031
+ const deep = extractDeep(absPath);
1032
+ uiNodes.push({
1033
+ id,
1034
+ type,
1035
+ name,
1036
+ layer: "ui",
1037
+ route,
1038
+ exports: parsed.exports,
1039
+ elements: deep.elements,
1040
+ stateVars: deep.stateVars,
1041
+ conditions: deep.conditions,
1042
+ variables: deep.variables
1043
+ });
1044
+ if (route) routeToNodeId.set(route, id);
1045
+ }
934
1046
  }
935
- const allEdges = [];
936
- const allFlagged = [];
1047
+ const uiEdges = [];
1048
+ const uiFlagged = [];
937
1049
  for (const absPath of fileSet) {
938
- const sourceId = toNodeId(srcDir, absPath);
1050
+ const id = toNodeId(srcDir, absPath);
1051
+ if (!nodeIdSet.has(id)) continue;
939
1052
  const parsed = parsedByPath.get(absPath);
940
1053
  const { edges, flagged } = extractEdges(
941
1054
  srcDir,
942
1055
  absPath,
943
- sourceId,
1056
+ id,
944
1057
  parsed,
945
1058
  nodeIdSet,
946
- nodeTypeMap,
947
1059
  barrelMaps,
948
1060
  routeToNodeId
949
1061
  );
950
- allEdges.push(...edges);
951
- allFlagged.push(...flagged);
1062
+ uiEdges.push(...edges);
1063
+ uiFlagged.push(...flagged);
952
1064
  }
953
1065
  const fetchCallEntries = [];
954
1066
  for (const absPath of fileSet) {
955
1067
  const sourceId = toNodeId(srcDir, absPath);
1068
+ if (!nodeIdSet.has(sourceId)) continue;
956
1069
  const parsed = parsedByPath.get(absPath);
957
1070
  if (parsed.fetchCalls.length === 0) continue;
958
1071
  fetchCallEntries.push({
@@ -990,11 +1103,11 @@ function generate(rootDir) {
990
1103
  } catch {
991
1104
  continue;
992
1105
  }
993
- const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
1106
+ const externalId = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
994
1107
  const edgesFromThis = [];
995
1108
  const seen = /* @__PURE__ */ new Set();
996
1109
  for (const imp of parsed.imports) {
997
- const { specifier, isTypeOnly, names } = imp;
1110
+ const { specifier, names } = imp;
998
1111
  let resolved = null;
999
1112
  if (specifier.startsWith("@/")) {
1000
1113
  const relToSrc = specifier.slice(2);
@@ -1020,25 +1133,52 @@ function generate(rootDir) {
1020
1133
  const targetId = toNodeId(srcDir, resolved);
1021
1134
  if (!nodeIdSet.has(targetId)) continue;
1022
1135
  if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
1023
- const key = `${externalId}\u2192${targetId}\u2192${isTypeOnly ? "type" : "value"}`;
1136
+ const key = `${externalId}\u2192${targetId}`;
1024
1137
  if (seen.has(key)) continue;
1025
1138
  seen.add(key);
1026
1139
  edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
1027
1140
  }
1028
1141
  if (edgesFromThis.length === 0) continue;
1029
- nodes.push({
1142
+ uiNodes.push({
1030
1143
  id: externalId,
1031
1144
  type: "external",
1032
1145
  name: parsed.name || nameFromFilename(absPath),
1146
+ layer: "ui",
1033
1147
  route: null,
1034
1148
  exports: parsed.exports
1035
1149
  });
1036
1150
  nodeIdSet.add(externalId);
1037
- nodeTypeMap.set(externalId, "external");
1038
- allEdges.push(...edgesFromThis);
1151
+ uiEdges.push(...edgesFromThis);
1152
+ }
1153
+ const apiCrossRefs = [];
1154
+ for (const node of apiNodes) {
1155
+ const dbCalls = node._dbCalls;
1156
+ if (!dbCalls) continue;
1157
+ const seenModels = /* @__PURE__ */ new Set();
1158
+ for (const call of dbCalls) {
1159
+ if (seenModels.has(call.model)) continue;
1160
+ seenModels.add(call.model);
1161
+ apiCrossRefs.push({
1162
+ source: node.id,
1163
+ target: camelToPascal(call.model),
1164
+ type: call.isMutation ? "mutates" : "reads",
1165
+ layer: "db"
1166
+ });
1167
+ }
1168
+ delete node._dbCalls;
1169
+ }
1170
+ const apiNodeIds = new Set(apiNodes.map((n) => n.id));
1171
+ const apiEdges = [];
1172
+ const uiOnlyEdges = [];
1173
+ for (const edge of uiEdges) {
1174
+ if (apiNodeIds.has(edge.source)) {
1175
+ apiEdges.push(edge);
1176
+ } else {
1177
+ uiOnlyEdges.push(edge);
1178
+ }
1039
1179
  }
1040
1180
  const flaggedSet = /* @__PURE__ */ new Set();
1041
- const dedupedFlagged = allFlagged.filter((f) => {
1181
+ const dedupedFlagged = uiFlagged.filter((f) => {
1042
1182
  const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
1043
1183
  if (flaggedSet.has(key)) return false;
1044
1184
  flaggedSet.add(key);
@@ -1055,10 +1195,12 @@ function generate(rootDir) {
1055
1195
  hook: 7,
1056
1196
  lib: 8
1057
1197
  };
1058
- nodes.sort((a, b) => (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99) || a.id.localeCompare(b.id));
1059
- allEdges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1060
- const byType = (t) => nodes.filter((n) => n.type === t).length;
1061
- const stats = {
1198
+ uiNodes.sort((a, b) => (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99) || a.id.localeCompare(b.id));
1199
+ uiOnlyEdges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1200
+ apiNodes.sort((a, b) => (a.path ?? "").localeCompare(b.path ?? ""));
1201
+ apiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1202
+ const byType = (t) => uiNodes.filter((n) => n.type === t).length;
1203
+ const uiStats = {
1062
1204
  total_pages: byType("page"),
1063
1205
  total_layouts: byType("layout"),
1064
1206
  total_components: byType("component"),
@@ -1069,182 +1211,99 @@ function generate(rootDir) {
1069
1211
  total_utils: byType("util"),
1070
1212
  total_libs: byType("lib"),
1071
1213
  total_external: byType("external"),
1072
- total_edges: allEdges.length,
1214
+ total_edges: uiOnlyEdges.length,
1073
1215
  total_flagged: dedupedFlagged.length
1074
1216
  };
1075
- return {
1217
+ const stripLayer = (nodes) => nodes.map(({ layer: _, ...rest }) => rest);
1218
+ const result = /* @__PURE__ */ new Map();
1219
+ result.set("ui", {
1076
1220
  metadata: {
1077
1221
  generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1078
1222
  scope: "main-app-only",
1079
1223
  app_root: "src/",
1080
1224
  layer: "ui",
1081
- parser: "react-nextjs-ast",
1082
- ...stats,
1225
+ parser: "typescript-project",
1226
+ ...uiStats,
1083
1227
  notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
1084
1228
  },
1085
- nodes,
1086
- edges: allEdges,
1229
+ nodes: stripLayer(uiNodes),
1230
+ edges: uiOnlyEdges,
1087
1231
  cross_refs: [],
1088
1232
  contradictions: [],
1089
1233
  warnings: [],
1090
1234
  flagged_edges: dedupedFlagged,
1091
1235
  patterns: {
1092
- total_nodes: nodes.length,
1093
- by_type: stats,
1236
+ total_nodes: uiNodes.length,
1237
+ by_type: uiStats,
1094
1238
  by_edge_type: {
1095
- renders: allEdges.filter((e) => e.type === "renders").length,
1096
- imports: allEdges.filter((e) => e.type === "imports").length,
1097
- navigates: allEdges.filter((e) => e.type === "navigates").length
1239
+ renders: uiOnlyEdges.filter((e) => e.type === "renders").length,
1240
+ imports: uiOnlyEdges.filter((e) => e.type === "imports").length,
1241
+ navigates: uiOnlyEdges.filter((e) => e.type === "navigates").length
1098
1242
  },
1099
1243
  fetch_calls: fetchCallEntries
1100
1244
  }
1101
- };
1102
- }
1103
- var reactNextjsParser = {
1104
- id: "react-nextjs",
1105
- layer: "ui",
1106
- detect,
1107
- generate
1108
- };
1109
-
1110
- // src/server/graph/parsers/api/nextjs-routes.ts
1111
- var import_node_fs4 = require("node:fs");
1112
- var import_node_path4 = require("node:path");
1113
- var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
1114
- function walk2(dir) {
1115
- const results = [];
1116
- if (!(0, import_node_fs4.existsSync)(dir)) return results;
1117
- for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
1118
- const full = (0, import_node_path4.join)(dir, entry.name);
1119
- if (entry.isDirectory()) {
1120
- results.push(...walk2(full));
1121
- } else if (entry.name === "route.ts" || entry.name === "route.tsx") {
1122
- results.push(full);
1245
+ });
1246
+ if (apiNodes.length > 0) {
1247
+ const mutatorNodes = apiNodes.filter((n) => n.mutates).length;
1248
+ const readOnlyNodes = apiNodes.filter((n) => !n.mutates).length;
1249
+ const authUsage = {};
1250
+ let endpointsWithAuth = 0;
1251
+ for (const n of apiNodes) {
1252
+ const auth = n.auth;
1253
+ if (auth.length > 0 && auth[0] !== "public") {
1254
+ endpointsWithAuth++;
1255
+ for (const w of auth) {
1256
+ authUsage[w] = (authUsage[w] ?? 0) + 1;
1257
+ }
1258
+ }
1123
1259
  }
1124
- }
1125
- return results;
1126
- }
1127
- function filePathToRoute(apiDir, absPath) {
1128
- let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
1129
- route = route.replace(/\[([^\]]+)\]/g, ":$1");
1130
- route = route.replace(/\/+/g, "/");
1131
- if (route === "/") return "/api";
1132
- return "/api" + route;
1133
- }
1134
- function camelToPascal(s) {
1135
- if (!s) return s;
1136
- return s.charAt(0).toUpperCase() + s.slice(1);
1137
- }
1138
- function detect2(rootDir) {
1139
- return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
1140
- }
1141
- function generate2(rootDir) {
1142
- const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
1143
- const routeFiles = walk2(apiDir);
1144
- const nodes = [];
1145
- const edges = [];
1146
- const crossRefs = [];
1147
- const mutatorCount = {};
1148
- const authUsage = {};
1149
- let endpointsWithAuth = 0;
1150
- let endpointsWithDbAccess = 0;
1151
- for (const absPath of routeFiles) {
1152
- const parsed = parseFileTS(absPath);
1153
- const dbCalls = extractDbCallsTS(absPath);
1154
- const authWrappers = extractAuthWrappersTS(absPath);
1155
- const methods = [];
1156
- for (const exp of parsed.exports) {
1157
- if (HTTP_METHODS.has(exp)) methods.push(exp);
1158
- }
1159
- const routePath = filePathToRoute(apiDir, absPath);
1160
- const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
1161
- const mutations = dbCalls.filter((c) => c.isMutation);
1162
- const reads = dbCalls.filter((c) => !c.isMutation);
1163
- const mutates = mutations.length > 0;
1164
- if (mutates) {
1165
- for (const m of mutations) {
1166
- mutatorCount[m.method] = (mutatorCount[m.method] ?? 0) + 1;
1167
- }
1168
- }
1169
- if (dbCalls.length > 0) endpointsWithDbAccess++;
1170
- const authStrategy = [];
1171
- for (const w of authWrappers) {
1172
- authStrategy.push(w);
1173
- authUsage[w] = (authUsage[w] ?? 0) + 1;
1174
- }
1175
- if (authStrategy.length > 0) endpointsWithAuth++;
1176
- const deep = extractDeep(absPath);
1177
- nodes.push({
1178
- id: relPath,
1179
- type: "endpoint",
1180
- name: routePath,
1181
- path: routePath,
1182
- methods,
1183
- handler: relPath,
1184
- // Behavioral classification from handler body (AST-derived).
1185
- mutates,
1186
- auth: authStrategy.length > 0 ? authStrategy : ["public"],
1187
- db_models: [...new Set(dbCalls.map((c) => c.model))],
1188
- db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
1189
- // Deep extraction fields
1190
- conditions: deep.conditions,
1191
- variables: deep.variables,
1192
- responses: deep.responses,
1193
- params: deep.params
1194
- });
1195
- const seenModels = /* @__PURE__ */ new Set();
1196
- for (const call of dbCalls) {
1197
- if (seenModels.has(call.model)) continue;
1198
- seenModels.add(call.model);
1199
- crossRefs.push({
1200
- source: relPath,
1201
- target: camelToPascal(call.model),
1202
- type: call.isMutation ? "mutates" : "reads",
1203
- layer: "db"
1204
- });
1260
+ const mutatorCount = {};
1261
+ for (const n of apiNodes) {
1262
+ const ops = n.db_operations;
1263
+ for (const op of ops) {
1264
+ const method = op.split(".")[1];
1265
+ if (method) mutatorCount[method] = (mutatorCount[method] ?? 0) + 1;
1266
+ }
1205
1267
  }
1268
+ result.set("api", {
1269
+ metadata: {
1270
+ generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1271
+ scope: "main-app-only",
1272
+ stack: "nextjs-app-router",
1273
+ layer: "api",
1274
+ parser: "typescript-project",
1275
+ total_endpoints: apiNodes.length,
1276
+ total_methods: apiNodes.reduce((sum, n) => sum + n.methods.length, 0),
1277
+ endpoints_with_auth: endpointsWithAuth,
1278
+ endpoints_with_db_access: apiNodes.filter((n) => n.db_models.length > 0).length,
1279
+ mutator_endpoints: mutatorNodes,
1280
+ read_only_endpoints: readOnlyNodes
1281
+ },
1282
+ nodes: stripLayer(apiNodes),
1283
+ edges: apiEdges,
1284
+ cross_refs: apiCrossRefs,
1285
+ contradictions: [],
1286
+ warnings: [],
1287
+ flagged_edges: [],
1288
+ patterns: {
1289
+ total_endpoints: apiNodes.length,
1290
+ methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
1291
+ acc[m] = apiNodes.filter((n) => n.methods.includes(m)).length;
1292
+ return acc;
1293
+ }, {}),
1294
+ auth_strategies: authUsage,
1295
+ mutation_operations: mutatorCount,
1296
+ mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
1297
+ }
1298
+ });
1206
1299
  }
1207
- nodes.sort((a, b) => a.path.localeCompare(b.path));
1208
- crossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1209
- const mutatorNodes = nodes.filter((n) => n.mutates).length;
1210
- const readOnlyNodes = nodes.filter((n) => !n.mutates).length;
1211
- return {
1212
- metadata: {
1213
- generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1214
- scope: "main-app-only",
1215
- stack: "nextjs-app-router",
1216
- layer: "api",
1217
- parser: "nextjs-routes-ast",
1218
- total_endpoints: nodes.length,
1219
- total_methods: nodes.reduce((sum, n) => sum + n.methods.length, 0),
1220
- endpoints_with_auth: endpointsWithAuth,
1221
- endpoints_with_db_access: endpointsWithDbAccess,
1222
- mutator_endpoints: mutatorNodes,
1223
- read_only_endpoints: readOnlyNodes
1224
- },
1225
- nodes,
1226
- edges,
1227
- cross_refs: crossRefs,
1228
- contradictions: [],
1229
- warnings: [],
1230
- flagged_edges: [],
1231
- patterns: {
1232
- total_endpoints: nodes.length,
1233
- methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
1234
- acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
1235
- return acc;
1236
- }, {}),
1237
- auth_strategies: authUsage,
1238
- mutation_operations: mutatorCount,
1239
- mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
1240
- }
1241
- };
1300
+ return result;
1242
1301
  }
1243
- var nextjsRoutesParser = {
1244
- id: "nextjs-routes",
1245
- layer: "api",
1246
- detect: detect2,
1247
- generate: generate2
1302
+ var typescriptProjectParser = {
1303
+ id: "typescript-project",
1304
+ layers: ["ui", "api"],
1305
+ detect,
1306
+ generate
1248
1307
  };
1249
1308
 
1250
1309
  // src/server/graph/parsers/db/prisma-schema.ts
@@ -1339,10 +1398,10 @@ function parseEnums(content) {
1339
1398
  }
1340
1399
  return nodes;
1341
1400
  }
1342
- function detect3(rootDir) {
1401
+ function detect2(rootDir) {
1343
1402
  return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
1344
1403
  }
1345
- function generate3(rootDir) {
1404
+ function generate2(rootDir) {
1346
1405
  const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
1347
1406
  const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
1348
1407
  const { nodes: modelNodes, relations } = parseModels(content);
@@ -1396,6 +1455,424 @@ function generate3(rootDir) {
1396
1455
  var prismaSchemaParser = {
1397
1456
  id: "prisma-schema",
1398
1457
  layer: "db",
1458
+ detect: detect2,
1459
+ generate: generate2
1460
+ };
1461
+
1462
+ // src/server/graph/parsers/db/sql-migrations.ts
1463
+ var import_node_fs6 = require("node:fs");
1464
+ var import_node_path6 = require("node:path");
1465
+ var PG_TO_PRISMA = {
1466
+ "TEXT": "String",
1467
+ "VARCHAR": "String",
1468
+ "CHAR": "String",
1469
+ "INTEGER": "Int",
1470
+ "INT": "Int",
1471
+ "SMALLINT": "Int",
1472
+ "BIGINT": "BigInt",
1473
+ "SERIAL": "Int",
1474
+ "BOOLEAN": "Boolean",
1475
+ "BOOL": "Boolean",
1476
+ "TIMESTAMP(3)": "DateTime",
1477
+ "TIMESTAMP": "DateTime",
1478
+ "TIMESTAMPTZ": "DateTime",
1479
+ "DATE": "DateTime",
1480
+ "DOUBLE PRECISION": "Float",
1481
+ "FLOAT": "Float",
1482
+ "REAL": "Float",
1483
+ "DECIMAL": "Decimal",
1484
+ "NUMERIC": "Decimal",
1485
+ "JSONB": "Json",
1486
+ "JSON": "Json",
1487
+ "BYTEA": "Bytes",
1488
+ "UUID": "String",
1489
+ "TEXT[]": "String[]"
1490
+ };
1491
+ function pgTypeToPrisma(pgType) {
1492
+ const upper = pgType.toUpperCase().trim();
1493
+ return PG_TO_PRISMA[upper] ?? upper;
1494
+ }
1495
+ function parseCreateTable(sql, state) {
1496
+ const re = /CREATE\s+TABLE\s+"(\w+)"\s*\(([\s\S]*?)\);/gi;
1497
+ let m;
1498
+ while ((m = re.exec(sql)) !== null) {
1499
+ const tableName = m[1];
1500
+ const body = m[2];
1501
+ const columns = /* @__PURE__ */ new Map();
1502
+ let primaryCol = null;
1503
+ for (const line of body.split("\n")) {
1504
+ const trimmed = line.trim().replace(/,\s*$/, "");
1505
+ if (!trimmed || trimmed.startsWith("--")) continue;
1506
+ const pkMatch = trimmed.match(/CONSTRAINT\s+"[^"]+"\s+PRIMARY\s+KEY\s*\("(\w+)"\)/i);
1507
+ if (pkMatch) {
1508
+ primaryCol = pkMatch[1];
1509
+ continue;
1510
+ }
1511
+ if (/^\s*CONSTRAINT\s/i.test(trimmed)) continue;
1512
+ const colMatch = trimmed.match(/^"(\w+)"\s+(.+)/);
1513
+ if (!colMatch) continue;
1514
+ const colName = colMatch[1];
1515
+ let rest = colMatch[2];
1516
+ const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
1517
+ const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)(?:\s*,?\s*$)/i);
1518
+ const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
1519
+ let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim().replace(/,\s*$/, "").trim();
1520
+ columns.set(colName, {
1521
+ name: colName,
1522
+ type: colType,
1523
+ nullable: !isNotNull,
1524
+ primary: false,
1525
+ unique: false,
1526
+ default: defaultVal
1527
+ });
1528
+ }
1529
+ if (primaryCol && columns.has(primaryCol)) {
1530
+ columns.get(primaryCol).primary = true;
1531
+ }
1532
+ state.tables.set(tableName, { name: tableName, columns });
1533
+ }
1534
+ }
1535
+ function parseCreateEnum(sql, state) {
1536
+ const re = /CREATE\s+TYPE\s+"(\w+)"\s+AS\s+ENUM\s*\(([^)]+)\)/gi;
1537
+ let m;
1538
+ while ((m = re.exec(sql)) !== null) {
1539
+ const enumName = m[1];
1540
+ const valuesStr = m[2];
1541
+ const values = new Set(
1542
+ valuesStr.split(",").map((v) => v.trim().replace(/^'(.*)'$/, "$1")).filter(Boolean)
1543
+ );
1544
+ state.enums.set(enumName, { name: enumName, values });
1545
+ }
1546
+ }
1547
+ function parseAlterTable(sql, state) {
1548
+ const addColRe = /ALTER\s+TABLE\s+"(\w+)"\s+ADD\s+COLUMN\s+"(\w+)"\s+(.+?);/gi;
1549
+ let m;
1550
+ while ((m = addColRe.exec(sql)) !== null) {
1551
+ const tableName = m[1];
1552
+ const colName = m[2];
1553
+ let rest = m[3];
1554
+ const table = state.tables.get(tableName);
1555
+ if (!table) continue;
1556
+ const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
1557
+ const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)$/i);
1558
+ const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
1559
+ let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim();
1560
+ table.columns.set(colName, {
1561
+ name: colName,
1562
+ type: colType,
1563
+ nullable: !isNotNull,
1564
+ primary: false,
1565
+ unique: false,
1566
+ default: defaultVal
1567
+ });
1568
+ }
1569
+ const dropColRe = /ALTER\s+TABLE\s+"(\w+)"\s+DROP\s+COLUMN\s+"(\w+)"/gi;
1570
+ while ((m = dropColRe.exec(sql)) !== null) {
1571
+ const table = state.tables.get(m[1]);
1572
+ if (table) table.columns.delete(m[2]);
1573
+ }
1574
+ const fkRe = /ALTER\s+TABLE\s+"(\w+)"\s+ADD\s+CONSTRAINT\s+"([^"]+)"\s+FOREIGN\s+KEY\s*\("(\w+)"\)\s+REFERENCES\s+"(\w+)"\("(\w+)"\)(?:\s+ON\s+DELETE\s+(\w+(?:\s+\w+)?))?/gi;
1575
+ while ((m = fkRe.exec(sql)) !== null) {
1576
+ state.fks.push({
1577
+ constraintName: m[2],
1578
+ sourceTable: m[1],
1579
+ sourceColumn: m[3],
1580
+ targetTable: m[4],
1581
+ targetColumn: m[5],
1582
+ onDelete: m[6] ?? null
1583
+ });
1584
+ }
1585
+ }
1586
+ function parseAlterEnum(sql, state) {
1587
+ const re = /ALTER\s+TYPE\s+"(\w+)"\s+ADD\s+VALUE\s+'([^']+)'/gi;
1588
+ let m;
1589
+ while ((m = re.exec(sql)) !== null) {
1590
+ const en = state.enums.get(m[1]);
1591
+ if (en) en.values.add(m[2]);
1592
+ }
1593
+ }
1594
+ function parseDropTable(sql, state) {
1595
+ const re = /DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?"(\w+)"/gi;
1596
+ let m;
1597
+ while ((m = re.exec(sql)) !== null) {
1598
+ state.tables.delete(m[1]);
1599
+ state.fks = state.fks.filter((fk) => fk.sourceTable !== m[1] && fk.targetTable !== m[1]);
1600
+ }
1601
+ }
1602
+ function parseUniqueIndex(sql, state) {
1603
+ const re = /CREATE\s+UNIQUE\s+INDEX\s+"[^"]+"\s+ON\s+"(\w+)"\("(\w+)"\)/gi;
1604
+ let m;
1605
+ while ((m = re.exec(sql)) !== null) {
1606
+ const table = state.tables.get(m[1]);
1607
+ const col = table?.columns.get(m[2]);
1608
+ if (col) col.unique = true;
1609
+ if (!state.uniqueIndexes.has(m[1])) state.uniqueIndexes.set(m[1], /* @__PURE__ */ new Set());
1610
+ state.uniqueIndexes.get(m[1]).add(m[2]);
1611
+ }
1612
+ }
1613
+ function parseMigrations(rootDir) {
1614
+ const migrationsDir = (0, import_node_path6.join)(rootDir, "prisma", "migrations");
1615
+ const state = {
1616
+ tables: /* @__PURE__ */ new Map(),
1617
+ enums: /* @__PURE__ */ new Map(),
1618
+ fks: [],
1619
+ uniqueIndexes: /* @__PURE__ */ new Map()
1620
+ };
1621
+ if (!(0, import_node_fs6.existsSync)(migrationsDir)) return state;
1622
+ const dirs = (0, import_node_fs6.readdirSync)(migrationsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
1623
+ for (const dir of dirs) {
1624
+ const sqlPath = (0, import_node_path6.join)(migrationsDir, dir, "migration.sql");
1625
+ if (!(0, import_node_fs6.existsSync)(sqlPath)) continue;
1626
+ const sql = (0, import_node_fs6.readFileSync)(sqlPath, "utf-8");
1627
+ parseCreateEnum(sql, state);
1628
+ parseCreateTable(sql, state);
1629
+ parseAlterTable(sql, state);
1630
+ parseAlterEnum(sql, state);
1631
+ parseDropTable(sql, state);
1632
+ parseUniqueIndex(sql, state);
1633
+ }
1634
+ return state;
1635
+ }
1636
+ function loadPrismaState(rootDir) {
1637
+ const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
1638
+ if (!(0, import_node_fs6.existsSync)(schemaPath)) return null;
1639
+ const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
1640
+ const tables = /* @__PURE__ */ new Map();
1641
+ const enums = /* @__PURE__ */ new Map();
1642
+ const relations = [];
1643
+ const modelRe = /model\s+(\w+)\s*\{([^}]+)\}/g;
1644
+ let m;
1645
+ while ((m = modelRe.exec(content)) !== null) {
1646
+ const modelName = m[1];
1647
+ const body = m[2];
1648
+ const cols = [];
1649
+ for (const line of body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("@@"))) {
1650
+ const fm = line.match(/^(\w+)\s+(\S+)(.*)/);
1651
+ if (!fm) continue;
1652
+ const fieldName = fm[1];
1653
+ const fieldType = fm[2];
1654
+ const rest = fm[3] ?? "";
1655
+ const baseType = fieldType.replace(/[?\[\]]/g, "");
1656
+ const isRelation = rest.includes("@relation");
1657
+ if (isRelation) {
1658
+ const relArgs = rest.match(/@relation\(([^)]*)\)/)?.[1] ?? "";
1659
+ const fieldsMatch = relArgs.match(/fields:\s*\[([^\]]+)\]/);
1660
+ if (fieldsMatch) {
1661
+ relations.push({ source: modelName, target: baseType, fk: fieldsMatch[1].trim() });
1662
+ }
1663
+ continue;
1664
+ }
1665
+ if (fieldType.endsWith("[]") || fieldType.endsWith("?") && content.includes(`model ${baseType}`)) {
1666
+ if (new RegExp(`model\\s+${baseType}\\s*\\{`).test(content)) continue;
1667
+ }
1668
+ cols.push({
1669
+ name: fieldName,
1670
+ type: fieldType.replace("?", ""),
1671
+ nullable: fieldType.endsWith("?") || fieldType.includes("?"),
1672
+ primary: rest.includes("@id"),
1673
+ unique: rest.includes("@unique")
1674
+ });
1675
+ }
1676
+ tables.set(modelName, { columns: cols });
1677
+ }
1678
+ const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
1679
+ while ((m = enumRe.exec(content)) !== null) {
1680
+ const values = m[2].split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
1681
+ enums.set(m[1], values);
1682
+ }
1683
+ return { tables, enums, relations };
1684
+ }
1685
+ function verify(sqlState, prisma) {
1686
+ const contradictions = [];
1687
+ const flaggedEdges = [];
1688
+ for (const [name] of sqlState.tables) {
1689
+ if (!prisma.tables.has(name)) {
1690
+ contradictions.push({
1691
+ entity: name,
1692
+ source_a: "sql-migrations",
1693
+ source_b: "prisma-schema",
1694
+ detail: `Table "${name}" exists in migrations but not in schema.prisma`
1695
+ });
1696
+ }
1697
+ }
1698
+ for (const [name] of prisma.tables) {
1699
+ if (!sqlState.tables.has(name)) {
1700
+ contradictions.push({
1701
+ entity: name,
1702
+ source_a: "prisma-schema",
1703
+ source_b: "sql-migrations",
1704
+ detail: `Table "${name}" in schema.prisma has no CREATE TABLE in migrations`
1705
+ });
1706
+ }
1707
+ }
1708
+ for (const [tableName, prismaTable] of prisma.tables) {
1709
+ const sqlTable = sqlState.tables.get(tableName);
1710
+ if (!sqlTable) continue;
1711
+ for (const prismaCol of prismaTable.columns) {
1712
+ const sqlCol = sqlTable.columns.get(prismaCol.name);
1713
+ if (!sqlCol) {
1714
+ contradictions.push({
1715
+ entity: `${tableName}.${prismaCol.name}`,
1716
+ source_a: "prisma-schema",
1717
+ source_b: "sql-migrations",
1718
+ detail: `Column "${tableName}.${prismaCol.name}" in schema.prisma but not in migrations`
1719
+ });
1720
+ continue;
1721
+ }
1722
+ const mappedSqlType = pgTypeToPrisma(sqlCol.type);
1723
+ const prismaBaseType = prismaCol.type.replace(/[?\[\]]/g, "");
1724
+ if (mappedSqlType !== prismaBaseType && mappedSqlType !== prismaCol.type) {
1725
+ if (!sqlState.enums.has(prismaBaseType)) {
1726
+ contradictions.push({
1727
+ entity: `${tableName}.${prismaCol.name}`,
1728
+ source_a: "sql-migrations",
1729
+ source_b: "prisma-schema",
1730
+ detail: `Column "${tableName}.${prismaCol.name}": SQL type "${sqlCol.type}" (\u2192${mappedSqlType}) vs Prisma type "${prismaCol.type}"`
1731
+ });
1732
+ }
1733
+ }
1734
+ if (sqlCol.nullable !== prismaCol.nullable) {
1735
+ contradictions.push({
1736
+ entity: `${tableName}.${prismaCol.name}`,
1737
+ source_a: "sql-migrations",
1738
+ source_b: "prisma-schema",
1739
+ detail: `Column "${tableName}.${prismaCol.name}": ${sqlCol.nullable ? "nullable" : "NOT NULL"} in SQL vs ${prismaCol.nullable ? "optional" : "required"} in Prisma`
1740
+ });
1741
+ }
1742
+ }
1743
+ for (const [colName] of sqlTable.columns) {
1744
+ const inPrisma = prismaTable.columns.some((c) => c.name === colName);
1745
+ if (!inPrisma) {
1746
+ contradictions.push({
1747
+ entity: `${tableName}.${colName}`,
1748
+ source_a: "sql-migrations",
1749
+ source_b: "prisma-schema",
1750
+ detail: `Column "${tableName}.${colName}" in migrations but not in schema.prisma`
1751
+ });
1752
+ }
1753
+ }
1754
+ }
1755
+ for (const [name, sqlEnum] of sqlState.enums) {
1756
+ const prismaValues = prisma.enums.get(name);
1757
+ if (!prismaValues) {
1758
+ contradictions.push({
1759
+ entity: name,
1760
+ source_a: "sql-migrations",
1761
+ source_b: "prisma-schema",
1762
+ detail: `Enum "${name}" exists in migrations but not in schema.prisma`
1763
+ });
1764
+ continue;
1765
+ }
1766
+ const prismaSet = new Set(prismaValues);
1767
+ for (const val of sqlEnum.values) {
1768
+ if (!prismaSet.has(val)) {
1769
+ contradictions.push({
1770
+ entity: `${name}.${val}`,
1771
+ source_a: "sql-migrations",
1772
+ source_b: "prisma-schema",
1773
+ detail: `Enum "${name}": value "${val}" in migrations but not in schema.prisma`
1774
+ });
1775
+ }
1776
+ }
1777
+ for (const val of prismaValues) {
1778
+ if (!sqlEnum.values.has(val)) {
1779
+ contradictions.push({
1780
+ entity: `${name}.${val}`,
1781
+ source_a: "prisma-schema",
1782
+ source_b: "sql-migrations",
1783
+ detail: `Enum "${name}": value "${val}" in schema.prisma but not in migrations`
1784
+ });
1785
+ }
1786
+ }
1787
+ }
1788
+ const prismaFkSet = new Set(prisma.relations.map((r) => `${r.source}|${r.target}|${r.fk}`));
1789
+ for (const fk of sqlState.fks) {
1790
+ const key = `${fk.sourceTable}|${fk.targetTable}|${fk.sourceColumn}`;
1791
+ if (!prismaFkSet.has(key)) {
1792
+ flaggedEdges.push({
1793
+ source: fk.sourceTable,
1794
+ target: fk.targetTable,
1795
+ type: "belongs_to",
1796
+ label: `FK "${fk.constraintName}" (${fk.sourceColumn}\u2192${fk.targetTable}) in migrations but no @relation in schema.prisma`,
1797
+ confidence: "high"
1798
+ });
1799
+ }
1800
+ }
1801
+ return { contradictions, flaggedEdges };
1802
+ }
1803
+ function detect3(rootDir) {
1804
+ const migrationsDir = (0, import_node_path6.join)(rootDir, "prisma", "migrations");
1805
+ if (!(0, import_node_fs6.existsSync)(migrationsDir)) return false;
1806
+ return (0, import_node_fs6.readdirSync)(migrationsDir, { withFileTypes: true }).some((d) => d.isDirectory() && (0, import_node_fs6.existsSync)((0, import_node_path6.join)(migrationsDir, d.name, "migration.sql")));
1807
+ }
1808
+ function generate3(rootDir) {
1809
+ const sqlState = parseMigrations(rootDir);
1810
+ const prisma = loadPrismaState(rootDir);
1811
+ const prismaTableIds = prisma ? new Set(prisma.tables.keys()) : /* @__PURE__ */ new Set();
1812
+ const prismaEnumIds = prisma ? new Set(prisma.enums.keys()) : /* @__PURE__ */ new Set();
1813
+ const nodes = [];
1814
+ for (const [name, table] of sqlState.tables) {
1815
+ if (prismaTableIds.has(name)) continue;
1816
+ nodes.push({
1817
+ id: name,
1818
+ type: "table",
1819
+ name,
1820
+ source: "sql",
1821
+ columns: [...table.columns.values()].map((c) => ({
1822
+ name: c.name,
1823
+ type: c.type,
1824
+ primary: c.primary,
1825
+ unique: c.unique,
1826
+ nullable: c.nullable,
1827
+ default: c.default,
1828
+ isRelation: false,
1829
+ comment: null
1830
+ }))
1831
+ });
1832
+ }
1833
+ for (const [name, sqlEnum] of sqlState.enums) {
1834
+ if (prismaEnumIds.has(name)) continue;
1835
+ nodes.push({
1836
+ id: name,
1837
+ type: "enum",
1838
+ name,
1839
+ source: "sql",
1840
+ values: [...sqlEnum.values]
1841
+ });
1842
+ }
1843
+ const sqlOnlyTables = new Set(nodes.filter((n) => n.type === "table").map((n) => n.id));
1844
+ const edges = sqlState.fks.filter((fk) => sqlOnlyTables.has(fk.sourceTable)).map((fk) => ({
1845
+ source: fk.sourceTable,
1846
+ target: fk.targetTable,
1847
+ type: "belongs_to",
1848
+ fk: fk.sourceColumn,
1849
+ onDelete: fk.onDelete
1850
+ }));
1851
+ const { contradictions, flaggedEdges } = prisma ? verify(sqlState, prisma) : { contradictions: [], flaggedEdges: [] };
1852
+ return {
1853
+ metadata: {
1854
+ generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1855
+ scope: "sql-migrations",
1856
+ source: "prisma/migrations/",
1857
+ layer: "db",
1858
+ sql_tables: sqlState.tables.size,
1859
+ sql_enums: sqlState.enums.size,
1860
+ sql_fks: sqlState.fks.length,
1861
+ additive_nodes: nodes.length,
1862
+ contradictions_found: contradictions.length,
1863
+ flagged_edges_found: flaggedEdges.length
1864
+ },
1865
+ nodes,
1866
+ edges,
1867
+ cross_refs: [],
1868
+ contradictions,
1869
+ warnings: [],
1870
+ flagged_edges: flaggedEdges
1871
+ };
1872
+ }
1873
+ var sqlMigrationsParser = {
1874
+ id: "sql-migrations",
1875
+ layer: "db",
1399
1876
  detect: detect3,
1400
1877
  generate: generate3
1401
1878
  };
@@ -1613,31 +2090,31 @@ var fetchResolverParser = {
1613
2090
  };
1614
2091
 
1615
2092
  // src/server/graph/parsers/crosslayer/api-annotations.ts
1616
- var import_node_fs6 = require("node:fs");
1617
- var import_node_path6 = require("node:path");
2093
+ var import_node_fs7 = require("node:fs");
2094
+ var import_node_path7 = require("node:path");
1618
2095
  var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
1619
- function walk3(dir, exts) {
1620
- if (!(0, import_node_fs6.existsSync)(dir)) return [];
2096
+ function walk2(dir, exts) {
2097
+ if (!(0, import_node_fs7.existsSync)(dir)) return [];
1621
2098
  const results = [];
1622
- for (const entry of (0, import_node_fs6.readdirSync)(dir, { withFileTypes: true })) {
2099
+ for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
1623
2100
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1624
- const full = (0, import_node_path6.join)(dir, entry.name);
2101
+ const full = (0, import_node_path7.join)(dir, entry.name);
1625
2102
  if (entry.isDirectory()) {
1626
- results.push(...walk3(full, exts));
1627
- } else if (exts.includes((0, import_node_path6.extname)(entry.name))) {
2103
+ results.push(...walk2(full, exts));
2104
+ } else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
1628
2105
  results.push(full);
1629
2106
  }
1630
2107
  }
1631
2108
  return results;
1632
2109
  }
1633
2110
  function toNodeId2(srcDir, absPath) {
1634
- return (0, import_node_path6.relative)(srcDir, absPath).replace(/\\/g, "/");
2111
+ return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
1635
2112
  }
1636
2113
  var apiAnnotationsParser = {
1637
2114
  id: "api-annotations",
1638
2115
  layer: "crosslayer",
1639
2116
  detect(rootDir) {
1640
- return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "src"));
2117
+ return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
1641
2118
  },
1642
2119
  generate(rootDir, layerOutputs) {
1643
2120
  const apiOutput = layerOutputs.get("api");
@@ -1648,13 +2125,13 @@ var apiAnnotationsParser = {
1648
2125
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1649
2126
  const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1650
2127
  const apiPathMap = buildApiPathMap(apiRoutes);
1651
- const srcDir = (0, import_node_path6.join)(rootDir, "src");
1652
- const files = walk3(srcDir, [".ts", ".tsx"]);
2128
+ const srcDir = (0, import_node_path7.join)(rootDir, "src");
2129
+ const files = walk2(srcDir, [".ts", ".tsx"]);
1653
2130
  const crossRefs = [];
1654
2131
  const flaggedEdges = [];
1655
2132
  const seen = /* @__PURE__ */ new Set();
1656
2133
  for (const absPath of files) {
1657
- const content = (0, import_node_fs6.readFileSync)(absPath, "utf-8");
2134
+ const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
1658
2135
  const sourceId = toNodeId2(srcDir, absPath);
1659
2136
  if (!uiNodeIds.has(sourceId)) continue;
1660
2137
  let match;
@@ -1693,73 +2170,727 @@ var apiAnnotationsParser = {
1693
2170
  annotations_resolved: crossRefs.length,
1694
2171
  annotations_unresolved: flaggedEdges.length
1695
2172
  }
1696
- };
2173
+ };
2174
+ }
2175
+ };
2176
+
2177
+ // src/server/graph/parsers/crosslayer/url-literal-scanner.ts
2178
+ var import_node_fs8 = require("node:fs");
2179
+ var import_node_path8 = require("node:path");
2180
+ init_config();
2181
+ var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
2182
+ function walk3(dir, exts) {
2183
+ if (!(0, import_node_fs8.existsSync)(dir)) return [];
2184
+ const results = [];
2185
+ for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
2186
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2187
+ const full = (0, import_node_path8.join)(dir, entry.name);
2188
+ if (entry.isDirectory()) {
2189
+ results.push(...walk3(full, exts));
2190
+ } else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
2191
+ results.push(full);
2192
+ }
2193
+ }
2194
+ return results;
2195
+ }
2196
+ function toNodeId3(srcDir, absPath) {
2197
+ return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
2198
+ }
2199
+ var urlLiteralScannerParser = {
2200
+ id: "url-literal-scanner",
2201
+ layer: "crosslayer",
2202
+ detect(rootDir) {
2203
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2204
+ return paths !== null;
2205
+ },
2206
+ generate(rootDir, layerOutputs) {
2207
+ const apiOutput = layerOutputs.get("api");
2208
+ if (!apiOutput) {
2209
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
2210
+ }
2211
+ const uiOutput = layerOutputs.get("ui");
2212
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
2213
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
2214
+ const apiPathMap = buildApiPathMap(apiRoutes);
2215
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2216
+ const srcDir = paths.srcDir;
2217
+ const clientDir = (0, import_node_path8.join)(srcDir, "client");
2218
+ const files = [
2219
+ ...walk3(clientDir, [".ts", ".tsx"]),
2220
+ ...walk3(paths.appDir, [".ts", ".tsx"])
2221
+ ];
2222
+ const crossRefs = [];
2223
+ const seen = /* @__PURE__ */ new Set();
2224
+ for (const absPath of files) {
2225
+ const sourceId = toNodeId3(srcDir, absPath);
2226
+ if (!uiNodeIds.has(sourceId)) continue;
2227
+ const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
2228
+ let match;
2229
+ URL_LITERAL_RE.lastIndex = 0;
2230
+ while ((match = URL_LITERAL_RE.exec(content)) !== null) {
2231
+ const urlPath = match[1];
2232
+ const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
2233
+ if (result.kind === "resolved" && result.nodeId) {
2234
+ const key = `${sourceId}|${result.nodeId}|references_api`;
2235
+ if (seen.has(key)) continue;
2236
+ seen.add(key);
2237
+ crossRefs.push({
2238
+ source: sourceId,
2239
+ target: result.nodeId,
2240
+ type: "references_api",
2241
+ layer: "api"
2242
+ });
2243
+ }
2244
+ }
2245
+ }
2246
+ return {
2247
+ cross_refs: crossRefs,
2248
+ flagged_edges: [],
2249
+ warnings: [],
2250
+ patterns: {
2251
+ url_literals_resolved: crossRefs.length
2252
+ }
2253
+ };
2254
+ }
2255
+ };
2256
+
2257
+ // src/server/graph/parsers/static/static-values.ts
2258
+ var import_node_fs9 = require("node:fs");
2259
+ var import_node_path9 = require("node:path");
2260
+ var parseCode = null;
2261
+ function tryLoadTreeSitter() {
2262
+ if (parseCode) return true;
2263
+ try {
2264
+ const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
2265
+ if (typeof extractor.parseCodeTS === "function") {
2266
+ parseCode = extractor.parseCodeTS;
2267
+ return true;
2268
+ }
2269
+ } catch {
2270
+ }
2271
+ return false;
2272
+ }
2273
+ var SHARED_MODELS = /* @__PURE__ */ new Set(["permission", "role", "tag"]);
2274
+ var DB_MODELS = /* @__PURE__ */ new Set(["subscriptionPlan", "providerDefinition", "pipelineMasterTemplate"]);
2275
+ function classifyScope(source, model) {
2276
+ if (source.includes("prisma/schema.prisma")) return "shared";
2277
+ if (source.includes("prisma/seed") && model) {
2278
+ if (SHARED_MODELS.has(model)) return "shared";
2279
+ if (DB_MODELS.has(model)) return "db";
2280
+ return "shared";
2281
+ }
2282
+ if (source.startsWith("src/client/") || source.startsWith("src/app/")) return "fe";
2283
+ if (source.startsWith("src/server/")) return "be";
2284
+ if (source.startsWith("src/config/")) return "be";
2285
+ if (source.startsWith("src/lib/")) return "shared";
2286
+ return "shared";
2287
+ }
2288
+ function extractEnumValues(rootDir) {
2289
+ const nodes = [];
2290
+ const edges = [];
2291
+ const schemaPaths = [
2292
+ (0, import_node_path9.join)(rootDir, "prisma", "schema.prisma"),
2293
+ (0, import_node_path9.join)(rootDir, "prisma", "schema")
2294
+ ];
2295
+ let content = "";
2296
+ for (const p of schemaPaths) {
2297
+ if ((0, import_node_fs9.existsSync)(p)) {
2298
+ try {
2299
+ const stat = (0, import_node_fs9.statSync)(p);
2300
+ if (stat.isFile()) {
2301
+ content = (0, import_node_fs9.readFileSync)(p, "utf-8");
2302
+ } else if (stat.isDirectory()) {
2303
+ const files = (0, import_node_fs9.readdirSync)(p).filter((f) => f.endsWith(".prisma"));
2304
+ content = files.map((f) => (0, import_node_fs9.readFileSync)((0, import_node_path9.join)(p, f), "utf-8")).join("\n");
2305
+ }
2306
+ } catch {
2307
+ continue;
2308
+ }
2309
+ break;
2310
+ }
2311
+ }
2312
+ if (!content) return { nodes, edges };
2313
+ const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
2314
+ let m;
2315
+ while ((m = enumRe.exec(content)) !== null) {
2316
+ const enumName = m[1];
2317
+ const body = m[2];
2318
+ const values = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
2319
+ const scope = classifyScope("prisma/schema.prisma");
2320
+ for (const val of values) {
2321
+ const nodeId = `${enumName}.${val}`;
2322
+ nodes.push({
2323
+ id: nodeId,
2324
+ type: "enum_value",
2325
+ name: val,
2326
+ parentEnum: enumName,
2327
+ value: val,
2328
+ scope,
2329
+ source: "prisma/schema.prisma"
2330
+ });
2331
+ edges.push({
2332
+ source: nodeId,
2333
+ target: `enum:${enumName}`,
2334
+ type: "member_of"
2335
+ });
2336
+ }
2337
+ nodes.push({
2338
+ id: `enum:${enumName}`,
2339
+ type: "enum_group",
2340
+ name: enumName,
2341
+ valueCount: values.length,
2342
+ scope,
2343
+ source: "prisma/schema.prisma"
2344
+ });
2345
+ }
2346
+ return { nodes, edges };
2347
+ }
2348
+ function extractPropsFromObjectNode(node) {
2349
+ const props = {};
2350
+ for (const child of node.namedChildren) {
2351
+ if (child.type !== "pair") continue;
2352
+ const keyNode = child.childForFieldName("key");
2353
+ const valNode = child.childForFieldName("value");
2354
+ if (!keyNode || !valNode) continue;
2355
+ const key = keyNode.type === "property_identifier" ? keyNode.text : keyNode.text.replace(/['"]/g, "");
2356
+ if (valNode.type === "string" || valNode.type === "template_string") {
2357
+ const fragment = valNode.namedChildren.find((c) => c.type === "string_fragment" || c.type === "template_substitution");
2358
+ props[key] = fragment ? fragment.text : valNode.text.replace(/^['"`]|['"`]$/g, "");
2359
+ } else if (valNode.type === "number") {
2360
+ props[key] = valNode.text;
2361
+ } else if (valNode.type === "true" || valNode.type === "false") {
2362
+ props[key] = valNode.text;
2363
+ }
2364
+ }
2365
+ return props;
2366
+ }
2367
+ function extractStringArrayFromNode(node) {
2368
+ const values = [];
2369
+ for (const child of node.namedChildren) {
2370
+ if (child.type === "string") {
2371
+ const frag = child.namedChildren.find((c) => c.type === "string_fragment");
2372
+ if (frag) values.push(frag.text);
2373
+ }
2374
+ }
2375
+ return values;
2376
+ }
2377
+ function findArrayDecl(root, varName) {
2378
+ function walk5(node) {
2379
+ if (node.type === "variable_declarator") {
2380
+ const nameNode = node.childForFieldName("name");
2381
+ const valueNode = node.childForFieldName("value");
2382
+ if (nameNode?.text === varName && valueNode?.type === "array") {
2383
+ return valueNode;
2384
+ }
2385
+ if (nameNode?.text === varName && valueNode?.type === "as_expression") {
2386
+ const inner = valueNode.namedChildren.find((c) => c.type === "array");
2387
+ if (inner) return inner;
2388
+ }
2389
+ }
2390
+ for (const child of node.namedChildren) {
2391
+ const found = walk5(child);
2392
+ if (found) return found;
2393
+ }
2394
+ return null;
2395
+ }
2396
+ return walk5(root);
2397
+ }
2398
+ function extractObjectPropsRegex(objStr) {
2399
+ const props = {};
2400
+ const propRe = /(\w+):\s*['"]([^'"]*)['"]/g;
2401
+ let m;
2402
+ while ((m = propRe.exec(objStr)) !== null) props[m[1]] = m[2];
2403
+ const numRe = /(\w+):\s*(-?\d+(?:\.\d+)?)\s*[,\n}]/g;
2404
+ while ((m = numRe.exec(objStr)) !== null) if (!props[m[1]]) props[m[1]] = m[2];
2405
+ return props;
2406
+ }
2407
+ function extractStringArrayRegex(arrStr) {
2408
+ return (arrStr.match(/'([^']+)'/g) ?? []).map((s) => s.replace(/'/g, ""));
2409
+ }
2410
+ function splitArrayObjectsRegex(arrayBody) {
2411
+ const objects = [];
2412
+ let depth = 0;
2413
+ let start = -1;
2414
+ for (let i = 0; i < arrayBody.length; i++) {
2415
+ if (arrayBody[i] === "{") {
2416
+ if (depth === 0) start = i;
2417
+ depth++;
2418
+ } else if (arrayBody[i] === "}") {
2419
+ depth--;
2420
+ if (depth === 0 && start >= 0) {
2421
+ objects.push(arrayBody.slice(start, i + 1));
2422
+ start = -1;
2423
+ }
2424
+ }
2425
+ }
2426
+ return objects;
2427
+ }
2428
+ function detectSeededArrays(content, sourceFile) {
2429
+ const results = [];
2430
+ const forOfRe = /for\s*\(\s*const\s+\w+\s+of\s+(\w+)\s*\)\s*\{/g;
2431
+ let fm;
2432
+ while ((fm = forOfRe.exec(content)) !== null) {
2433
+ const arrayName = fm[1];
2434
+ const lookahead = content.slice(fm.index + fm[0].length, fm.index + fm[0].length + 500);
2435
+ const prismaMatch = lookahead.match(/prisma\.(\w+)\.(create|upsert|update|createMany|findFirst)/);
2436
+ if (!prismaMatch) continue;
2437
+ results.push({ arrayName, prismaModel: prismaMatch[1], sourceFile });
2438
+ }
2439
+ return results;
2440
+ }
2441
+ function pickIdField(props) {
2442
+ for (const key of ["key", "slug", "id", "name", "templateId"]) {
2443
+ if (props[key]) return props[key];
2444
+ }
2445
+ return null;
2446
+ }
2447
+ function pickNameField(props) {
2448
+ for (const key of ["name", "slug", "key", "id"]) {
2449
+ if (props[key]) return props[key];
2450
+ }
2451
+ return null;
2452
+ }
2453
+ function modelToNodeType(model) {
2454
+ return `seed_${model.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "")}`;
2455
+ }
2456
+ function extractSeedData(rootDir) {
2457
+ const nodes = [];
2458
+ const edges = [];
2459
+ const seedFiles = [
2460
+ (0, import_node_path9.join)(rootDir, "prisma", "seed.ts"),
2461
+ (0, import_node_path9.join)(rootDir, "prisma", "seed.js"),
2462
+ (0, import_node_path9.join)(rootDir, "src", "server", "lib", "system-tags.ts")
2463
+ ].filter(import_node_fs9.existsSync);
2464
+ const useTreeSitter = tryLoadTreeSitter();
2465
+ for (const filePath of seedFiles) {
2466
+ const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
2467
+ const relPath = (0, import_node_path9.relative)(rootDir, filePath);
2468
+ const seeded = detectSeededArrays(content, relPath);
2469
+ let astRoot = null;
2470
+ if (useTreeSitter && parseCode) {
2471
+ try {
2472
+ const tree = parseCode(content);
2473
+ astRoot = tree.rootNode;
2474
+ } catch {
2475
+ }
2476
+ }
2477
+ for (const { arrayName, prismaModel, sourceFile } of seeded) {
2478
+ const nodeType = modelToNodeType(prismaModel);
2479
+ const scope = classifyScope(sourceFile, prismaModel);
2480
+ if (astRoot) {
2481
+ const arrayNode = findArrayDecl(astRoot, arrayName);
2482
+ if (!arrayNode) continue;
2483
+ for (const child of arrayNode.namedChildren) {
2484
+ if (child.type !== "object") continue;
2485
+ const props = extractPropsFromObjectNode(child);
2486
+ const idVal = pickIdField(props);
2487
+ if (!idVal) continue;
2488
+ const nameVal = pickNameField(props) ?? idVal;
2489
+ const nodeId = `seed:${prismaModel}:${idVal}`;
2490
+ const { scope: _seedScope, ...safeProps } = props;
2491
+ nodes.push({
2492
+ id: nodeId,
2493
+ type: nodeType,
2494
+ name: nameVal,
2495
+ value: idVal,
2496
+ model: prismaModel,
2497
+ ...safeProps,
2498
+ seedScope: _seedScope ?? void 0,
2499
+ // preserve as seedScope
2500
+ scope,
2501
+ source: sourceFile
2502
+ });
2503
+ const permsArrayNode = child.namedChildren.filter((c) => c.type === "pair").find((c) => c.childForFieldName("key")?.text === "permissions");
2504
+ if (permsArrayNode) {
2505
+ const valNode = permsArrayNode.childForFieldName("value");
2506
+ if (valNode?.type === "array") {
2507
+ const permKeys = extractStringArrayFromNode(valNode);
2508
+ for (const pk of permKeys) {
2509
+ if (pk === "*") continue;
2510
+ edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
2511
+ }
2512
+ }
2513
+ }
2514
+ }
2515
+ } else {
2516
+ const constRe = new RegExp(`const\\s+${arrayName}\\s*(?::[^=]+)?=\\s*\\[`, "g");
2517
+ const cm = constRe.exec(content);
2518
+ if (!cm) continue;
2519
+ const bracketStart = cm.index + cm[0].length - 1;
2520
+ let depth = 1, i = bracketStart + 1;
2521
+ while (i < content.length && depth > 0) {
2522
+ if (content[i] === "[") depth++;
2523
+ else if (content[i] === "]") depth--;
2524
+ i++;
2525
+ }
2526
+ if (depth !== 0) continue;
2527
+ const body = content.slice(bracketStart + 1, i - 1);
2528
+ const objects = splitArrayObjectsRegex(body);
2529
+ for (const objStr of objects) {
2530
+ const props = extractObjectPropsRegex(objStr);
2531
+ const idVal = pickIdField(props);
2532
+ if (!idVal) continue;
2533
+ const nameVal = pickNameField(props) ?? idVal;
2534
+ const nodeId = `seed:${prismaModel}:${idVal}`;
2535
+ const { scope: _rScope, ...rSafeProps } = props;
2536
+ nodes.push({
2537
+ id: nodeId,
2538
+ type: nodeType,
2539
+ name: nameVal,
2540
+ value: idVal,
2541
+ model: prismaModel,
2542
+ ...rSafeProps,
2543
+ seedScope: _rScope ?? void 0,
2544
+ scope,
2545
+ source: sourceFile
2546
+ });
2547
+ const permArrayMatch = objStr.match(/permissions:\s*\[([^\]]*)\]/);
2548
+ if (permArrayMatch) {
2549
+ for (const pk of extractStringArrayRegex(permArrayMatch[1])) {
2550
+ if (pk === "*") continue;
2551
+ edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
2552
+ }
2553
+ }
2554
+ }
2555
+ }
2556
+ }
2557
+ }
2558
+ return { nodes, edges };
2559
+ }
2560
+ function walkDir(dir, exts) {
2561
+ if (!(0, import_node_fs9.existsSync)(dir)) return [];
2562
+ const results = [];
2563
+ for (const entry of (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true })) {
2564
+ if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
2565
+ const full = (0, import_node_path9.join)(dir, entry.name);
2566
+ if (entry.isDirectory()) results.push(...walkDir(full, exts));
2567
+ else if (exts.some((ext) => entry.name.endsWith(ext))) results.push(full);
2568
+ }
2569
+ return results;
2570
+ }
2571
+ function extractConstants(rootDir) {
2572
+ const nodes = [];
2573
+ const srcDir = (0, import_node_path9.join)(rootDir, "src");
2574
+ if (!(0, import_node_fs9.existsSync)(srcDir)) return { nodes };
2575
+ for (const filePath of walkDir(srcDir, [".ts", ".tsx"])) {
2576
+ const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
2577
+ const relPath = (0, import_node_path9.relative)(rootDir, filePath);
2578
+ const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
2579
+ let cm;
2580
+ while ((cm = constArrayRe.exec(content)) !== null) {
2581
+ const constName = cm[1];
2582
+ const bracketStart = cm.index + cm[0].length - 1;
2583
+ let depth = 1, i = bracketStart + 1;
2584
+ while (i < content.length && depth > 0) {
2585
+ if (content[i] === "[") depth++;
2586
+ else if (content[i] === "]") depth--;
2587
+ i++;
2588
+ }
2589
+ if (depth !== 0) continue;
2590
+ const body = content.slice(bracketStart + 1, i - 1);
2591
+ const stringValues = extractStringArrayRegex(body);
2592
+ const objectCount = splitArrayObjectsRegex(body).length;
2593
+ const valueCount = Math.max(stringValues.length, objectCount);
2594
+ if (valueCount < 2) continue;
2595
+ const scope = classifyScope(relPath);
2596
+ nodes.push({
2597
+ id: `const:${constName}`,
2598
+ type: "constant",
2599
+ name: constName,
2600
+ valueCount,
2601
+ values: stringValues.length > 0 && stringValues.length <= 30 ? stringValues : void 0,
2602
+ scope,
2603
+ source: relPath
2604
+ });
2605
+ }
2606
+ }
2607
+ return { nodes };
2608
+ }
2609
+ function detect4(rootDir) {
2610
+ return (0, import_node_fs9.existsSync)((0, import_node_path9.join)(rootDir, "prisma", "schema.prisma")) || (0, import_node_fs9.existsSync)((0, import_node_path9.join)(rootDir, "prisma", "seed.ts"));
2611
+ }
2612
+ function generate4(rootDir) {
2613
+ const enumResult = extractEnumValues(rootDir);
2614
+ const seedResult = extractSeedData(rootDir);
2615
+ const constResult = extractConstants(rootDir);
2616
+ const allNodes = [...enumResult.nodes, ...seedResult.nodes, ...constResult.nodes];
2617
+ const allEdges = [...enumResult.edges, ...seedResult.edges];
2618
+ const typeOrder = {
2619
+ enum_group: 0,
2620
+ enum_value: 1,
2621
+ seed_permission: 2,
2622
+ seed_role: 3,
2623
+ seed_tag: 4,
2624
+ seed_plan: 5,
2625
+ seed_provider: 6,
2626
+ constant: 7
2627
+ };
2628
+ allNodes.sort((a, b) => {
2629
+ const ta = typeOrder[a.type] ?? 8;
2630
+ const tb = typeOrder[b.type] ?? 8;
2631
+ if (ta !== tb) return ta - tb;
2632
+ return a.name.localeCompare(b.name);
2633
+ });
2634
+ const enumGroups = allNodes.filter((n) => n.type === "enum_group").length;
2635
+ const enumValues = allNodes.filter((n) => n.type === "enum_value").length;
2636
+ const seedNodes = allNodes.filter((n) => n.type.startsWith("seed_")).length;
2637
+ const constNodes = allNodes.filter((n) => n.type === "constant").length;
2638
+ const byScope = {};
2639
+ for (const n of allNodes) {
2640
+ const s = n.scope ?? "unknown";
2641
+ byScope[s] = (byScope[s] ?? 0) + 1;
1697
2642
  }
2643
+ return {
2644
+ metadata: {
2645
+ generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
2646
+ scope: "static-values",
2647
+ layer: "static",
2648
+ enum_groups: enumGroups,
2649
+ enum_values: enumValues,
2650
+ seed_records: seedNodes,
2651
+ constants: constNodes,
2652
+ total_nodes: allNodes.length,
2653
+ total_edges: allEdges.length,
2654
+ parser: tryLoadTreeSitter() ? "tree-sitter" : "regex-fallback"
2655
+ },
2656
+ nodes: allNodes,
2657
+ edges: allEdges,
2658
+ cross_refs: [],
2659
+ contradictions: [],
2660
+ warnings: [],
2661
+ flagged_edges: [],
2662
+ patterns: {
2663
+ by_type: {
2664
+ enum_group: enumGroups,
2665
+ enum_value: enumValues,
2666
+ seed_permission: allNodes.filter((n) => n.type === "seed_permission").length,
2667
+ seed_role: allNodes.filter((n) => n.type === "seed_role").length,
2668
+ seed_tag: allNodes.filter((n) => n.type === "seed_tag").length,
2669
+ seed_plan: allNodes.filter((n) => n.type === "seed_plan").length,
2670
+ seed_provider: allNodes.filter((n) => n.type === "seed_provider").length,
2671
+ constant: constNodes
2672
+ },
2673
+ by_scope: byScope
2674
+ }
2675
+ };
2676
+ }
2677
+ var staticValuesParser = {
2678
+ id: "static-values",
2679
+ layer: "static",
2680
+ detect: detect4,
2681
+ generate: generate4
1698
2682
  };
1699
2683
 
1700
- // src/server/graph/parsers/crosslayer/url-literal-scanner.ts
1701
- var import_node_fs7 = require("node:fs");
1702
- var import_node_path7 = require("node:path");
1703
- var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
2684
+ // src/server/graph/parsers/crosslayer/static-ref-scanner.ts
2685
+ var import_node_fs10 = require("node:fs");
2686
+ var import_node_path10 = require("node:path");
2687
+ init_config();
1704
2688
  function walk4(dir, exts) {
1705
- if (!(0, import_node_fs7.existsSync)(dir)) return [];
2689
+ if (!(0, import_node_fs10.existsSync)(dir)) return [];
1706
2690
  const results = [];
1707
- for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
1708
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1709
- const full = (0, import_node_path7.join)(dir, entry.name);
1710
- if (entry.isDirectory()) {
1711
- results.push(...walk4(full, exts));
1712
- } else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
1713
- results.push(full);
2691
+ function recurse(d) {
2692
+ for (const entry of (0, import_node_fs10.readdirSync)(d, { withFileTypes: true })) {
2693
+ const full = (0, import_node_path10.join)(d, entry.name);
2694
+ if (entry.isDirectory()) {
2695
+ if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
2696
+ recurse(full);
2697
+ } else if (exts.some((ext) => entry.name.endsWith(ext))) {
2698
+ results.push(full);
2699
+ }
1714
2700
  }
1715
2701
  }
2702
+ recurse(dir);
1716
2703
  return results;
1717
2704
  }
1718
- function toNodeId3(srcDir, absPath) {
1719
- return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
2705
+ var MIN_VALUE_LENGTH = 4;
2706
+ var SKIP_VALUES = /* @__PURE__ */ new Set([
2707
+ "true",
2708
+ "false",
2709
+ "null",
2710
+ "undefined",
2711
+ "none",
2712
+ "default",
2713
+ "name",
2714
+ "type",
2715
+ "data",
2716
+ "text",
2717
+ "info",
2718
+ "error",
2719
+ "open",
2720
+ "read",
2721
+ "user",
2722
+ "test",
2723
+ "json",
2724
+ "form"
2725
+ ]);
2726
+ function isInCommentOrType(node) {
2727
+ let current = node.parent;
2728
+ while (current) {
2729
+ if (current.type === "comment" || current.type === "type_annotation" || current.type === "type_alias_declaration" || current.type === "interface_declaration" || current.type === "jsdoc") {
2730
+ return true;
2731
+ }
2732
+ current = current.parent;
2733
+ }
2734
+ return false;
1720
2735
  }
1721
- var urlLiteralScannerParser = {
1722
- id: "url-literal-scanner",
2736
+ function collectStaticRefs(root, valueLookup) {
2737
+ const refs = [];
2738
+ const seen = /* @__PURE__ */ new Set();
2739
+ function visit(node) {
2740
+ if (node.type === "string_fragment") {
2741
+ const val = node.text;
2742
+ if (val.length >= MIN_VALUE_LENGTH && !SKIP_VALUES.has(val.toLowerCase())) {
2743
+ const targets = valueLookup.get(val);
2744
+ if (targets && !isInCommentOrType(node)) {
2745
+ for (const t of targets) {
2746
+ const key = t;
2747
+ if (!seen.has(key)) {
2748
+ seen.add(key);
2749
+ refs.push({ value: val, targetIds: [t] });
2750
+ }
2751
+ }
2752
+ }
2753
+ }
2754
+ }
2755
+ if (node.type === "member_expression") {
2756
+ const objNode = node.namedChildren.find((c) => c.type === "identifier");
2757
+ const propNode = node.namedChildren.find((c) => c.type === "property_identifier");
2758
+ if (objNode && propNode) {
2759
+ const combined = `${objNode.text}.${propNode.text}`;
2760
+ const targets = valueLookup.get(propNode.text);
2761
+ const directTarget = valueLookup.get(combined);
2762
+ if (directTarget && !isInCommentOrType(node)) {
2763
+ for (const t of directTarget) {
2764
+ if (!seen.has(t)) {
2765
+ seen.add(t);
2766
+ refs.push({ value: combined, targetIds: [t] });
2767
+ }
2768
+ }
2769
+ } else if (targets && !isInCommentOrType(node)) {
2770
+ for (const t of targets) {
2771
+ if (!seen.has(t)) {
2772
+ seen.add(t);
2773
+ refs.push({ value: propNode.text, targetIds: [t] });
2774
+ }
2775
+ }
2776
+ }
2777
+ }
2778
+ }
2779
+ for (const child of node.namedChildren) {
2780
+ visit(child);
2781
+ }
2782
+ }
2783
+ visit(root);
2784
+ return refs;
2785
+ }
2786
+ function collectStaticRefsRegex(content, valueLookup, allValues) {
2787
+ const refs = [];
2788
+ const seen = /* @__PURE__ */ new Set();
2789
+ const escaped = allValues.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
2790
+ const pattern = new RegExp(
2791
+ `(?:['"\`])(${escaped.join("|")})(?:['"\`])|\\b(\\w+)\\.(${escaped.join("|")})\\b`,
2792
+ "g"
2793
+ );
2794
+ let match;
2795
+ pattern.lastIndex = 0;
2796
+ while ((match = pattern.exec(content)) !== null) {
2797
+ const val = match[1] ?? match[3];
2798
+ if (!val) continue;
2799
+ const targets = valueLookup.get(val);
2800
+ if (!targets) continue;
2801
+ for (const t of targets) {
2802
+ if (!seen.has(t)) {
2803
+ seen.add(t);
2804
+ refs.push({ value: val, targetIds: [t] });
2805
+ }
2806
+ }
2807
+ }
2808
+ return refs;
2809
+ }
2810
+ var staticRefScannerParser = {
2811
+ id: "static-ref-scanner",
1723
2812
  layer: "crosslayer",
1724
2813
  detect(rootDir) {
1725
- return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
2814
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2815
+ return paths !== null;
1726
2816
  },
1727
2817
  generate(rootDir, layerOutputs) {
1728
- const apiOutput = layerOutputs.get("api");
1729
- if (!apiOutput) {
2818
+ const staticOutput = layerOutputs.get("static");
2819
+ if (!staticOutput || staticOutput.nodes.length === 0) {
1730
2820
  return { cross_refs: [], flagged_edges: [], warnings: [] };
1731
2821
  }
1732
- const uiOutput = layerOutputs.get("ui");
1733
- const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1734
- const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1735
- const apiPathMap = buildApiPathMap(apiRoutes);
1736
- const srcDir = (0, import_node_path7.join)(rootDir, "src");
1737
- const clientDir = (0, import_node_path7.join)(srcDir, "client");
1738
- const appDir = (0, import_node_path7.join)(srcDir, "app");
2822
+ const valueLookup = /* @__PURE__ */ new Map();
2823
+ for (const node of staticOutput.nodes) {
2824
+ const type = node.type;
2825
+ let valueStr = null;
2826
+ if (type === "enum_value") {
2827
+ valueStr = node.value;
2828
+ const fullId = node.id;
2829
+ if (!valueLookup.has(fullId)) valueLookup.set(fullId, []);
2830
+ valueLookup.get(fullId).push(node.id);
2831
+ } else if (type.startsWith("seed_")) {
2832
+ valueStr = node.value;
2833
+ }
2834
+ if (!valueStr || valueStr.length < MIN_VALUE_LENGTH || SKIP_VALUES.has(valueStr.toLowerCase())) continue;
2835
+ if (!valueLookup.has(valueStr)) valueLookup.set(valueStr, []);
2836
+ valueLookup.get(valueStr).push(node.id);
2837
+ }
2838
+ const allValues = [...valueLookup.keys()].sort((a, b) => b.length - a.length);
2839
+ if (allValues.length === 0) {
2840
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
2841
+ }
2842
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2843
+ if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
2844
+ const srcDir = paths.srcDir;
1739
2845
  const files = [
1740
- ...walk4(clientDir, [".ts", ".tsx"]),
1741
- ...walk4(appDir, [".ts", ".tsx"])
2846
+ ...walk4((0, import_node_path10.join)(srcDir, "client"), [".ts", ".tsx"]),
2847
+ ...walk4(paths.appDir, [".ts", ".tsx"]),
2848
+ ...walk4((0, import_node_path10.join)(srcDir, "server"), [".ts", ".tsx"]),
2849
+ ...walk4((0, import_node_path10.join)(srcDir, "lib"), [".ts", ".tsx"]),
2850
+ ...walk4((0, import_node_path10.join)(srcDir, "config"), [".ts", ".tsx"])
1742
2851
  ];
2852
+ const uiOutput = layerOutputs.get("ui");
2853
+ const apiOutput = layerOutputs.get("api");
2854
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
2855
+ const apiNodeIds = new Set(apiOutput?.nodes.map((n) => n.id) ?? []);
2856
+ let parseCode2 = null;
2857
+ try {
2858
+ const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
2859
+ if (typeof extractor.parseCodeTS === "function") {
2860
+ parseCode2 = extractor.parseCodeTS;
2861
+ }
2862
+ } catch {
2863
+ }
1743
2864
  const crossRefs = [];
1744
2865
  const seen = /* @__PURE__ */ new Set();
2866
+ let filesScanned = 0;
1745
2867
  for (const absPath of files) {
1746
- const sourceId = toNodeId3(srcDir, absPath);
1747
- if (!uiNodeIds.has(sourceId)) continue;
1748
- const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
1749
- let match;
1750
- URL_LITERAL_RE.lastIndex = 0;
1751
- while ((match = URL_LITERAL_RE.exec(content)) !== null) {
1752
- const urlPath = match[1];
1753
- const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
1754
- if (result.kind === "resolved" && result.nodeId) {
1755
- const key = `${sourceId}|${result.nodeId}|references_api`;
2868
+ const sourceId = (0, import_node_path10.relative)(srcDir, absPath).replace(/\\/g, "/");
2869
+ const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
2870
+ if (!sourceLayer) continue;
2871
+ const content = (0, import_node_fs10.readFileSync)(absPath, "utf-8");
2872
+ filesScanned++;
2873
+ let fileRefs;
2874
+ if (parseCode2) {
2875
+ try {
2876
+ const tree = parseCode2(content);
2877
+ fileRefs = collectStaticRefs(tree.rootNode, valueLookup);
2878
+ } catch {
2879
+ fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
2880
+ }
2881
+ } else {
2882
+ fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
2883
+ }
2884
+ for (const ref of fileRefs) {
2885
+ for (const targetId of ref.targetIds) {
2886
+ const key = `${sourceId}|${targetId}`;
1756
2887
  if (seen.has(key)) continue;
1757
2888
  seen.add(key);
1758
2889
  crossRefs.push({
1759
2890
  source: sourceId,
1760
- target: result.nodeId,
1761
- type: "references_api",
1762
- layer: "api"
2891
+ target: targetId,
2892
+ type: "references_static",
2893
+ layer: "static"
1763
2894
  });
1764
2895
  }
1765
2896
  }
@@ -1769,16 +2900,23 @@ var urlLiteralScannerParser = {
1769
2900
  flagged_edges: [],
1770
2901
  warnings: [],
1771
2902
  patterns: {
1772
- url_literals_resolved: crossRefs.length
2903
+ files_scanned: filesScanned,
2904
+ static_values_tracked: valueLookup.size,
2905
+ references_found: crossRefs.length,
2906
+ parser: parseCode2 ? "tree-sitter" : "regex-fallback"
1773
2907
  }
1774
2908
  };
1775
2909
  }
1776
2910
  };
1777
2911
 
1778
2912
  // src/server/graph/core/parser-registry.ts
2913
+ function isMultiLayerParser(p) {
2914
+ return "layers" in p && Array.isArray(p.layers);
2915
+ }
1779
2916
  var ParserRegistry = class {
1780
2917
  constructor() {
1781
- this.parsers = /* @__PURE__ */ new Map();
2918
+ this.singleLayerParsers = /* @__PURE__ */ new Map();
2919
+ this.multiLayerParsers = [];
1782
2920
  this.ids = /* @__PURE__ */ new Set();
1783
2921
  }
1784
2922
  register(parser) {
@@ -1786,30 +2924,57 @@ var ParserRegistry = class {
1786
2924
  throw new Error(`Duplicate parser id: ${parser.id}`);
1787
2925
  }
1788
2926
  this.ids.add(parser.id);
1789
- const list = this.parsers.get(parser.layer) ?? [];
1790
- list.push(parser);
1791
- this.parsers.set(parser.layer, list);
2927
+ if (isMultiLayerParser(parser)) {
2928
+ this.multiLayerParsers.push(parser);
2929
+ } else {
2930
+ const list = this.singleLayerParsers.get(parser.layer) ?? [];
2931
+ list.push(parser);
2932
+ this.singleLayerParsers.set(parser.layer, list);
2933
+ }
1792
2934
  }
2935
+ /** Get single-layer parsers for a specific layer. */
1793
2936
  getParsers(layer) {
1794
- return this.parsers.get(layer) ?? [];
2937
+ return this.singleLayerParsers.get(layer) ?? [];
2938
+ }
2939
+ /** Get multi-layer parsers that can produce output for the given layer. */
2940
+ getMultiLayerParsersFor(layer) {
2941
+ return this.multiLayerParsers.filter((p) => p.layers.includes(layer));
2942
+ }
2943
+ /** Get all multi-layer parsers. */
2944
+ getMultiLayerParsers() {
2945
+ return [...this.multiLayerParsers];
1795
2946
  }
1796
2947
  getCrossLayerParsers() {
1797
- return this.parsers.get("crosslayer") ?? [];
2948
+ return this.singleLayerParsers.get("crosslayer") ?? [];
2949
+ }
2950
+ /** All layers that registered parsers can produce (single + multi). */
2951
+ getAvailableLayers() {
2952
+ const layers = /* @__PURE__ */ new Set();
2953
+ for (const key of this.singleLayerParsers.keys()) {
2954
+ if (key !== "crosslayer") layers.add(key);
2955
+ }
2956
+ for (const mp of this.multiLayerParsers) {
2957
+ for (const l of mp.layers) layers.add(l);
2958
+ }
2959
+ return [...layers];
1798
2960
  }
1799
2961
  getAll() {
1800
2962
  const all = [];
1801
- for (const list of this.parsers.values()) all.push(...list);
2963
+ for (const list of this.singleLayerParsers.values()) all.push(...list);
2964
+ all.push(...this.multiLayerParsers);
1802
2965
  return all;
1803
2966
  }
1804
2967
  };
1805
2968
  function registerBuiltins(registry, disabled) {
1806
2969
  const builtins = [
1807
- reactNextjsParser,
1808
- nextjsRoutesParser,
2970
+ typescriptProjectParser,
1809
2971
  prismaSchemaParser,
2972
+ sqlMigrationsParser,
2973
+ staticValuesParser,
1810
2974
  fetchResolverParser,
1811
2975
  apiAnnotationsParser,
1812
- urlLiteralScannerParser
2976
+ urlLiteralScannerParser,
2977
+ staticRefScannerParser
1813
2978
  ];
1814
2979
  for (const parser of builtins) {
1815
2980
  if (disabled.has(parser.id)) continue;
@@ -1819,11 +2984,11 @@ function registerBuiltins(registry, disabled) {
1819
2984
  function loadCustomParsers(registry, config, rootDir, disabled) {
1820
2985
  for (const entry of config.parsers?.custom ?? []) {
1821
2986
  try {
1822
- const absPath = (0, import_node_path8.resolve)(rootDir, entry.path);
2987
+ const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
1823
2988
  const mod = require(absPath);
1824
2989
  const parser = "default" in mod ? mod.default : mod;
1825
2990
  if (disabled.has(parser.id)) continue;
1826
- if (parser.layer !== entry.layer) {
2991
+ if (!isMultiLayerParser(parser) && parser.layer !== entry.layer) {
1827
2992
  process.stderr.write(
1828
2993
  `[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
1829
2994
  `
@@ -1969,10 +3134,10 @@ function applyCrossLayerResults(uiOutput, results, primaryId) {
1969
3134
 
1970
3135
  // src/server/graph/core/graph-builder.ts
1971
3136
  function readGraphFromDisk(rootDir, layer) {
1972
- const filePath = (0, import_node_path9.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
1973
- if (!(0, import_node_fs8.existsSync)(filePath)) return null;
3137
+ const filePath = (0, import_node_path12.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
3138
+ if (!(0, import_node_fs11.existsSync)(filePath)) return null;
1974
3139
  try {
1975
- return JSON.parse((0, import_node_fs8.readFileSync)(filePath, "utf-8"));
3140
+ return JSON.parse((0, import_node_fs11.readFileSync)(filePath, "utf-8"));
1976
3141
  } catch {
1977
3142
  return null;
1978
3143
  }
@@ -1980,18 +3145,24 @@ function readGraphFromDisk(rootDir, layer) {
1980
3145
  function generateLayer(rootDir, layer) {
1981
3146
  const config = loadConfig(rootDir);
1982
3147
  const registry = createRegistry(config, rootDir);
1983
- const parsers = registry.getParsers(layer);
1984
3148
  const outputs = [];
1985
- for (const parser of parsers) {
3149
+ for (const parser of registry.getParsers(layer)) {
1986
3150
  if (!parser.detect(rootDir)) continue;
1987
3151
  outputs.push(parser.generate(rootDir));
1988
3152
  }
3153
+ for (const mp of registry.getMultiLayerParsersFor(layer)) {
3154
+ if (!mp.detect(rootDir)) continue;
3155
+ const multiOutput = mp.generate(rootDir);
3156
+ const layerOutput = multiOutput.get(layer);
3157
+ if (layerOutput) outputs.push(layerOutput);
3158
+ }
1989
3159
  if (outputs.length === 0) return null;
1990
3160
  let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
1991
3161
  if (layer === "ui") {
1992
3162
  const layerOutputs = /* @__PURE__ */ new Map();
1993
3163
  layerOutputs.set("ui", merged);
1994
- for (const otherLayer of ["api", "db"]) {
3164
+ for (const otherLayer of registry.getAvailableLayers()) {
3165
+ if (otherLayer === "ui") continue;
1995
3166
  const existing = readGraphFromDisk(rootDir, otherLayer);
1996
3167
  if (existing) layerOutputs.set(otherLayer, existing);
1997
3168
  }
@@ -2012,16 +3183,29 @@ function generateLayer(rootDir, layer) {
2012
3183
  function generateAll(rootDir) {
2013
3184
  const config = loadConfig(rootDir);
2014
3185
  const registry = createRegistry(config, rootDir);
2015
- const layerOrder = ["api", "db", "ui"];
3186
+ const allLayers = registry.getAvailableLayers();
3187
+ const layerOrder = [
3188
+ ...allLayers.filter((l) => l !== "ui"),
3189
+ ...allLayers.filter((l) => l === "ui")
3190
+ ];
2016
3191
  const layerOutputs = /* @__PURE__ */ new Map();
2017
3192
  const results = [];
3193
+ const multiLayerResults = /* @__PURE__ */ new Map();
2018
3194
  for (const layer of layerOrder) {
2019
- const parsers = registry.getParsers(layer);
2020
3195
  const outputs = [];
2021
- for (const parser of parsers) {
3196
+ for (const parser of registry.getParsers(layer)) {
2022
3197
  if (!parser.detect(rootDir)) continue;
2023
3198
  outputs.push(parser.generate(rootDir));
2024
3199
  }
3200
+ for (const mp of registry.getMultiLayerParsersFor(layer)) {
3201
+ if (!mp.detect(rootDir)) continue;
3202
+ if (!multiLayerResults.has(mp.id)) {
3203
+ multiLayerResults.set(mp.id, mp.generate(rootDir));
3204
+ }
3205
+ const cached = multiLayerResults.get(mp.id);
3206
+ const layerOutput = cached.get(layer);
3207
+ if (layerOutput) outputs.push(layerOutput);
3208
+ }
2025
3209
  if (outputs.length === 0) continue;
2026
3210
  const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
2027
3211
  layerOutputs.set(layer, merged);
@@ -2047,18 +3231,20 @@ function generateAll(rootDir) {
2047
3231
  }
2048
3232
  }
2049
3233
  const byLayer = new Map(results.map((r) => [r.layer, r]));
2050
- return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
3234
+ const wellKnownOrder = ["ui", "api", "db"];
3235
+ const extras = [...byLayer.keys()].filter((l) => !wellKnownOrder.includes(l)).sort();
3236
+ return [...wellKnownOrder, ...extras].map((l) => byLayer.get(l)).filter((r) => !!r);
2051
3237
  }
2052
3238
 
2053
3239
  // src/server/graph/index.ts
2054
3240
  init_config();
2055
3241
 
2056
3242
  // src/server/graph/core/tagger-registry.ts
2057
- var import_node_path11 = require("node:path");
3243
+ var import_node_path14 = require("node:path");
2058
3244
 
2059
3245
  // src/server/graph/taggers/module-tagger.ts
2060
- var import_node_fs9 = require("node:fs");
2061
- var import_node_path10 = require("node:path");
3246
+ var import_node_fs12 = require("node:fs");
3247
+ var import_node_path13 = require("node:path");
2062
3248
  function matchGlob(pattern, id) {
2063
3249
  const patParts = pattern.split("/");
2064
3250
  const idParts = id.split("/");
@@ -2091,18 +3277,18 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
2091
3277
  const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
2092
3278
  const searchDirs = [
2093
3279
  rootDir,
2094
- (0, import_node_path10.join)(rootDir, "src"),
2095
- (0, import_node_path10.join)(rootDir, "app"),
2096
- (0, import_node_path10.join)(rootDir, "lib")
3280
+ (0, import_node_path13.join)(rootDir, "src"),
3281
+ (0, import_node_path13.join)(rootDir, "app"),
3282
+ (0, import_node_path13.join)(rootDir, "lib")
2097
3283
  ];
2098
3284
  for (const base of searchDirs) {
2099
3285
  for (const convention of conventionDirs) {
2100
- const dir = (0, import_node_path10.join)(base, convention);
2101
- if (!(0, import_node_fs9.existsSync)(dir)) continue;
3286
+ const dir = (0, import_node_path13.join)(base, convention);
3287
+ if (!(0, import_node_fs12.existsSync)(dir)) continue;
2102
3288
  try {
2103
- const stat = (0, import_node_fs9.statSync)(dir);
3289
+ const stat = (0, import_node_fs12.statSync)(dir);
2104
3290
  if (!stat.isDirectory()) continue;
2105
- const entries = (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
3291
+ const entries = (0, import_node_fs12.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
2106
3292
  if (entries.length > 0) {
2107
3293
  const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
2108
3294
  result.set(relPath, entries);
@@ -2396,7 +3582,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
2396
3582
  for (const entry of config.taggers?.custom ?? []) {
2397
3583
  if (disabled.has(entry.id)) continue;
2398
3584
  try {
2399
- const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
3585
+ const absPath = (0, import_node_path14.resolve)(rootDir, entry.path);
2400
3586
  const mod = require(absPath);
2401
3587
  const tagger = "default" in mod ? mod.default : mod;
2402
3588
  const override = config.taggers?.trackUntagged?.[tagger.id];
@@ -2419,24 +3605,24 @@ function createTaggerRegistry(config, rootDir) {
2419
3605
  }
2420
3606
 
2421
3607
  // src/server/graph/core/tag-store.ts
2422
- var import_node_fs10 = require("node:fs");
2423
- var import_node_path12 = require("node:path");
3608
+ var import_node_fs13 = require("node:fs");
3609
+ var import_node_path15 = require("node:path");
2424
3610
  var TAGS_FILENAME = "tags.json";
2425
3611
  var GRAPHS_DIR = ".launchsecure/graphs";
2426
3612
  var tagCache = /* @__PURE__ */ new Map();
2427
3613
  function tagsFilePath(rootDir) {
2428
- return (0, import_node_path12.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
3614
+ return (0, import_node_path15.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
2429
3615
  }
2430
3616
  function readTagStore(rootDir) {
2431
3617
  const filePath = tagsFilePath(rootDir);
2432
- if (!(0, import_node_fs10.existsSync)(filePath)) return {};
2433
- const stat = (0, import_node_fs10.statSync)(filePath);
3618
+ if (!(0, import_node_fs13.existsSync)(filePath)) return {};
3619
+ const stat = (0, import_node_fs13.statSync)(filePath);
2434
3620
  const cached = tagCache.get(filePath);
2435
3621
  if (cached && cached.mtimeMs === stat.mtimeMs) {
2436
3622
  return cached.store;
2437
3623
  }
2438
3624
  try {
2439
- const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
3625
+ const content = (0, import_node_fs13.readFileSync)(filePath, "utf-8");
2440
3626
  const store = JSON.parse(content);
2441
3627
  tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
2442
3628
  return store;
@@ -2446,15 +3632,15 @@ function readTagStore(rootDir) {
2446
3632
  }
2447
3633
  function writeTagStore(rootDir, store) {
2448
3634
  const filePath = tagsFilePath(rootDir);
2449
- const dir = (0, import_node_path12.dirname)(filePath);
2450
- (0, import_node_fs10.mkdirSync)(dir, { recursive: true });
3635
+ const dir = (0, import_node_path15.dirname)(filePath);
3636
+ (0, import_node_fs13.mkdirSync)(dir, { recursive: true });
2451
3637
  const cleaned = {};
2452
3638
  for (const [nodeId, tags] of Object.entries(store)) {
2453
3639
  if (Object.keys(tags).length > 0) {
2454
3640
  cleaned[nodeId] = tags;
2455
3641
  }
2456
3642
  }
2457
- (0, import_node_fs10.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
3643
+ (0, import_node_fs13.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
2458
3644
  tagCache.delete(filePath);
2459
3645
  }
2460
3646
  function setTag(rootDir, nodeId, key, value) {
@@ -2474,22 +3660,27 @@ function removeTag(rootDir, nodeId, key) {
2474
3660
  }
2475
3661
 
2476
3662
  // src/server/graph/index.ts
3663
+ init_ts_extractor();
2477
3664
  var GRAPHS_DIR2 = ".launchsecure/graphs";
2478
- var LAYERS = ["ui", "api", "db"];
3665
+ function getAvailableLayers(rootDir) {
3666
+ const dir = (0, import_node_path16.join)(rootDir, GRAPHS_DIR2);
3667
+ if (!(0, import_node_fs14.existsSync)(dir)) return [];
3668
+ return (0, import_node_fs14.readdirSync)(dir).filter((f) => f.endsWith(".json") && f !== "tags.json").map((f) => f.replace(".json", ""));
3669
+ }
2479
3670
  var graphCache = /* @__PURE__ */ new Map();
2480
3671
  var taggedCache = /* @__PURE__ */ new Map();
2481
3672
  function graphsDir(rootDir) {
2482
- return (0, import_node_path13.join)(rootDir, GRAPHS_DIR2);
3673
+ return (0, import_node_path16.join)(rootDir, GRAPHS_DIR2);
2483
3674
  }
2484
3675
  function graphFilePath(rootDir, layer) {
2485
- return (0, import_node_path13.join)(graphsDir(rootDir), `${layer}.json`);
3676
+ return (0, import_node_path16.join)(graphsDir(rootDir), `${layer}.json`);
2486
3677
  }
2487
3678
  function tagsFilePath2(rootDir) {
2488
- return (0, import_node_path13.join)(graphsDir(rootDir), "tags.json");
3679
+ return (0, import_node_path16.join)(graphsDir(rootDir), "tags.json");
2489
3680
  }
2490
3681
  function getMtimeMs(filePath) {
2491
- if (!(0, import_node_fs11.existsSync)(filePath)) return 0;
2492
- return (0, import_node_fs11.statSync)(filePath).mtimeMs;
3682
+ if (!(0, import_node_fs14.existsSync)(filePath)) return 0;
3683
+ return (0, import_node_fs14.statSync)(filePath).mtimeMs;
2493
3684
  }
2494
3685
  function invalidateCache(filePath) {
2495
3686
  graphCache.delete(filePath);
@@ -2528,20 +3719,20 @@ function applyTags(graph, layer, rootDir) {
2528
3719
  }
2529
3720
  function readGraphRaw(rootDir, layer) {
2530
3721
  const filePath = graphFilePath(rootDir, layer);
2531
- if (!(0, import_node_fs11.existsSync)(filePath)) return null;
2532
- const stat = (0, import_node_fs11.statSync)(filePath);
3722
+ if (!(0, import_node_fs14.existsSync)(filePath)) return null;
3723
+ const stat = (0, import_node_fs14.statSync)(filePath);
2533
3724
  const cached = graphCache.get(filePath);
2534
3725
  if (cached && cached.mtimeMs === stat.mtimeMs) {
2535
3726
  return cached.graph;
2536
3727
  }
2537
- const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
3728
+ const content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
2538
3729
  const graph = JSON.parse(content);
2539
3730
  graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
2540
3731
  return graph;
2541
3732
  }
2542
3733
  function readGraph(rootDir, layer) {
2543
3734
  const rawFilePath = graphFilePath(rootDir, layer);
2544
- if (!(0, import_node_fs11.existsSync)(rawFilePath)) return null;
3735
+ if (!(0, import_node_fs14.existsSync)(rawFilePath)) return null;
2545
3736
  const rawMtime = getMtimeMs(rawFilePath);
2546
3737
  const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
2547
3738
  const cacheKey = `${rootDir}:${layer}`;
@@ -2557,7 +3748,7 @@ function readGraph(rootDir, layer) {
2557
3748
  }
2558
3749
  function readAllGraphs(rootDir) {
2559
3750
  const result = {};
2560
- for (const layer of LAYERS) {
3751
+ for (const layer of getAvailableLayers(rootDir)) {
2561
3752
  const graph = readGraph(rootDir, layer);
2562
3753
  if (graph) result[layer] = graph;
2563
3754
  }
@@ -2571,11 +3762,11 @@ async function generateGraph(rootDir, layer) {
2571
3762
  mutationMethods: config.parsers?.patterns?.mutationMethods
2572
3763
  });
2573
3764
  const dir = graphsDir(rootDir);
2574
- (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
3765
+ (0, import_node_fs14.mkdirSync)(dir, { recursive: true });
2575
3766
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
2576
3767
  for (const result of results) {
2577
3768
  const filePath = graphFilePath(rootDir, result.layer);
2578
- (0, import_node_fs11.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
3769
+ (0, import_node_fs14.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
2579
3770
  invalidateCache(filePath);
2580
3771
  invalidateTaggedCache(rootDir, result.layer);
2581
3772
  }
@@ -2584,28 +3775,28 @@ async function generateGraph(rootDir, layer) {
2584
3775
 
2585
3776
  // src/server/lockfile.ts
2586
3777
  var import_node_child_process = require("node:child_process");
2587
- var import_node_fs12 = require("node:fs");
3778
+ var import_node_fs15 = require("node:fs");
2588
3779
  var import_node_os = require("node:os");
2589
- var import_node_path14 = require("node:path");
3780
+ var import_node_path17 = require("node:path");
2590
3781
  function lockDir(projectRoot) {
2591
3782
  if (projectRoot) {
2592
- return (0, import_node_path14.join)(projectRoot, ".launchsecure");
3783
+ return (0, import_node_path17.join)(projectRoot, ".launchsecure");
2593
3784
  }
2594
- return (0, import_node_path14.join)((0, import_node_os.homedir)(), ".launchsecure");
3785
+ return (0, import_node_path17.join)((0, import_node_os.homedir)(), ".launchsecure");
2595
3786
  }
2596
3787
  function lockPath(projectRoot) {
2597
- return (0, import_node_path14.join)(lockDir(projectRoot), "launch-chart.lock");
3788
+ return (0, import_node_path17.join)(lockDir(projectRoot), "launch-chart.lock");
2598
3789
  }
2599
3790
  var _activeProjectRoot;
2600
3791
  function readLock(projectRoot) {
2601
3792
  const root = projectRoot ?? _activeProjectRoot;
2602
3793
  const p = lockPath(root);
2603
- if (!(0, import_node_fs12.existsSync)(p)) {
3794
+ if (!(0, import_node_fs15.existsSync)(p)) {
2604
3795
  if (root) {
2605
3796
  const globalP = lockPath();
2606
- if ((0, import_node_fs12.existsSync)(globalP)) {
3797
+ if ((0, import_node_fs15.existsSync)(globalP)) {
2607
3798
  try {
2608
- const data = JSON.parse((0, import_node_fs12.readFileSync)(globalP, "utf-8"));
3799
+ const data = JSON.parse((0, import_node_fs15.readFileSync)(globalP, "utf-8"));
2609
3800
  if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
2610
3801
  return data;
2611
3802
  }
@@ -2616,7 +3807,7 @@ function readLock(projectRoot) {
2616
3807
  return null;
2617
3808
  }
2618
3809
  try {
2619
- const data = JSON.parse((0, import_node_fs12.readFileSync)(p, "utf-8"));
3810
+ const data = JSON.parse((0, import_node_fs15.readFileSync)(p, "utf-8"));
2620
3811
  if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
2621
3812
  return data;
2622
3813
  } catch {
@@ -2653,7 +3844,7 @@ function getLiveLock(projectRoot) {
2653
3844
  const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
2654
3845
  if (!live) {
2655
3846
  try {
2656
- (0, import_node_fs12.unlinkSync)(lockPath(root));
3847
+ (0, import_node_fs15.unlinkSync)(lockPath(root));
2657
3848
  } catch {
2658
3849
  }
2659
3850
  return null;
@@ -2662,20 +3853,299 @@ function getLiveLock(projectRoot) {
2662
3853
  }
2663
3854
  function writeLock(data, projectRoot) {
2664
3855
  const root = projectRoot ?? _activeProjectRoot;
2665
- (0, import_node_fs12.mkdirSync)(lockDir(root), { recursive: true });
2666
- (0, import_node_fs12.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
3856
+ (0, import_node_fs15.mkdirSync)(lockDir(root), { recursive: true });
3857
+ (0, import_node_fs15.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
2667
3858
  if (root) _activeProjectRoot = root;
2668
3859
  }
2669
3860
  function clearLock(projectRoot) {
2670
3861
  const root = projectRoot ?? _activeProjectRoot;
2671
3862
  try {
2672
- (0, import_node_fs12.unlinkSync)(lockPath(root));
3863
+ (0, import_node_fs15.unlinkSync)(lockPath(root));
2673
3864
  } catch {
2674
3865
  }
2675
3866
  }
2676
3867
 
2677
3868
  // src/server/chart-serve.ts
2678
3869
  init_config();
3870
+
3871
+ // src/server/graph/core/audit-core.ts
3872
+ var import_node_fs16 = require("node:fs");
3873
+ var import_node_path18 = require("node:path");
3874
+ function readGraphFile(rootDir, layer) {
3875
+ const filePath = (0, import_node_path18.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
3876
+ if (!(0, import_node_fs16.existsSync)(filePath)) return null;
3877
+ try {
3878
+ return JSON.parse((0, import_node_fs16.readFileSync)(filePath, "utf-8"));
3879
+ } catch {
3880
+ return null;
3881
+ }
3882
+ }
3883
+ function checkSchemaDrift(rootDir) {
3884
+ const findings = [];
3885
+ const db = readGraphFile(rootDir, "db");
3886
+ if (!db) {
3887
+ findings.push({ id: "no-db-graph", severity: "info", category: "schema_drift", title: "No DB graph", detail: "Run generate_graph first to populate the DB layer." });
3888
+ return buildReport("db", "schema_drift", findings);
3889
+ }
3890
+ for (const c of db.contradictions ?? []) {
3891
+ const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
3892
+ findings.push({
3893
+ id: `drift:${c.entity}`,
3894
+ severity: isTableLevel ? "error" : "warning",
3895
+ category: "schema_drift",
3896
+ title: c.entity,
3897
+ detail: c.detail
3898
+ });
3899
+ }
3900
+ return buildReport("db", "schema_drift", findings);
3901
+ }
3902
+ function checkOrphanFks(rootDir) {
3903
+ const findings = [];
3904
+ const db = readGraphFile(rootDir, "db");
3905
+ if (!db) return buildReport("db", "orphan_fks", findings);
3906
+ for (const f of db.flagged_edges ?? []) {
3907
+ findings.push({
3908
+ id: `fk:${f.source}->${f.target}`,
3909
+ severity: "warning",
3910
+ category: "orphan_fks",
3911
+ title: `${f.source} \u2192 ${f.target}`,
3912
+ detail: f.label
3913
+ });
3914
+ }
3915
+ return buildReport("db", "orphan_fks", findings);
3916
+ }
3917
+ function checkUnprotectedRoutes(rootDir) {
3918
+ const findings = [];
3919
+ const api = readGraphFile(rootDir, "api");
3920
+ const staticGraph = readGraphFile(rootDir, "static");
3921
+ if (!api) return buildReport("api", "unprotected_routes", findings);
3922
+ const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
3923
+ let routePermsContent = "";
3924
+ if ((0, import_node_fs16.existsSync)(routePermsPath)) {
3925
+ routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
3926
+ }
3927
+ const registeredRoutes = /* @__PURE__ */ new Set();
3928
+ const routeEntryRe = /path:\s*'([^']+)'/g;
3929
+ let rm;
3930
+ while ((rm = routeEntryRe.exec(routePermsContent)) !== null) {
3931
+ registeredRoutes.add(rm[1].replace(/:(\w+)/g, "[$1]"));
3932
+ }
3933
+ for (const node of api.nodes) {
3934
+ if (node.type !== "endpoint") continue;
3935
+ const route = node.route ?? "";
3936
+ if (!route) continue;
3937
+ const normalized = "/api" + (route.startsWith("/") ? route : "/" + route);
3938
+ const isRegistered = registeredRoutes.has(normalized) || [...registeredRoutes].some((r) => routeMatchesPattern(normalized, r));
3939
+ if (!isRegistered) {
3940
+ const methods = node.methods ?? [];
3941
+ findings.push({
3942
+ id: `unprotected:${node.id}`,
3943
+ severity: "warning",
3944
+ category: "unprotected_routes",
3945
+ title: `${methods.join(",")} ${route}`,
3946
+ detail: `API endpoint has no entry in ROUTE_PERMISSIONS. Methods: ${methods.join(", ")}`,
3947
+ file: node.id
3948
+ });
3949
+ }
3950
+ }
3951
+ return buildReport("api", "unprotected_routes", findings);
3952
+ }
3953
+ function routeMatchesPattern(route, pattern) {
3954
+ const routeParts = route.split("/");
3955
+ const patternParts = pattern.split("/");
3956
+ if (routeParts.length !== patternParts.length) return false;
3957
+ for (let i = 0; i < routeParts.length; i++) {
3958
+ const rp = routeParts[i];
3959
+ const pp = patternParts[i];
3960
+ if (rp === pp) continue;
3961
+ if (pp.startsWith("[") || pp.startsWith(":")) continue;
3962
+ if (rp.startsWith("[") || rp.startsWith(":")) continue;
3963
+ return false;
3964
+ }
3965
+ return true;
3966
+ }
3967
+ function checkDeadScreens(rootDir) {
3968
+ const findings = [];
3969
+ const ui = readGraphFile(rootDir, "ui");
3970
+ if (!ui) return buildReport("ui", "dead_screens", findings);
3971
+ const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
3972
+ const navTargets = /* @__PURE__ */ new Set();
3973
+ for (const e of ui.edges) {
3974
+ if (e.type === "navigates" || e.type === "renders" || e.type === "imports") {
3975
+ navTargets.add(e.target);
3976
+ }
3977
+ }
3978
+ for (const cr of ui.cross_refs ?? []) {
3979
+ navTargets.add(cr.target);
3980
+ }
3981
+ for (const page of pages) {
3982
+ if (page.id.endsWith("layout.tsx") && page.id.split("/").length <= 2) continue;
3983
+ if (["error.tsx", "loading.tsx", "not-found.tsx", "template.tsx"].some((s) => page.id.endsWith(s))) continue;
3984
+ if (!navTargets.has(page.id)) {
3985
+ findings.push({
3986
+ id: `dead:${page.id}`,
3987
+ severity: "info",
3988
+ category: "dead_screens",
3989
+ title: page.name,
3990
+ detail: `Page "${page.id}" has no incoming navigation, render, or import edges.`,
3991
+ file: page.id
3992
+ });
3993
+ }
3994
+ }
3995
+ return buildReport("ui", "dead_screens", findings);
3996
+ }
3997
+ function checkUnenforcedPermissions(rootDir) {
3998
+ const findings = [];
3999
+ const staticGraph = readGraphFile(rootDir, "static");
4000
+ if (!staticGraph) return buildReport("static", "unenforced_permissions", findings);
4001
+ const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
4002
+ const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
4003
+ let routePermsContent = "";
4004
+ if ((0, import_node_fs16.existsSync)(routePermsPath)) {
4005
+ routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
4006
+ }
4007
+ for (const perm of permissions) {
4008
+ const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
4009
+ if (!regex.test(routePermsContent)) {
4010
+ findings.push({
4011
+ id: `unenforced:${perm.key}`,
4012
+ severity: "warning",
4013
+ category: "unenforced_permissions",
4014
+ title: `${perm.name} (${perm.key})`,
4015
+ detail: `Permission "${perm.key}" exists in seed data but has no entry in ROUTE_PERMISSIONS \u2014 no API route requires it.`
4016
+ });
4017
+ }
4018
+ }
4019
+ return buildReport("static", "unenforced_permissions", findings);
4020
+ }
4021
+ function checkHardcodedValues(rootDir) {
4022
+ const findings = [];
4023
+ const staticGraph = readGraphFile(rootDir, "static");
4024
+ if (!staticGraph) return buildReport("static", "hardcoded_values", findings);
4025
+ const knownValues = /* @__PURE__ */ new Set();
4026
+ for (const n of staticGraph.nodes) {
4027
+ if (n.type === "enum_value") knownValues.add(n.value);
4028
+ }
4029
+ const api = readGraphFile(rootDir, "api");
4030
+ if (!api) return buildReport("static", "hardcoded_values", findings);
4031
+ const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
4032
+ const seen = /* @__PURE__ */ new Set();
4033
+ for (const node of api.nodes) {
4034
+ if (node.type !== "endpoint") continue;
4035
+ const filePath = (0, import_node_path18.join)(rootDir, "src", node.id);
4036
+ if (!(0, import_node_fs16.existsSync)(filePath)) continue;
4037
+ const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
4038
+ let m;
4039
+ allCapsRe.lastIndex = 0;
4040
+ while ((m = allCapsRe.exec(content)) !== null) {
4041
+ const val = m[1];
4042
+ if (knownValues.has(val)) continue;
4043
+ if (["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "UTF", "NULL", "TRUE", "FALSE", "JSON", "HTML", "CSS", "ENV"].includes(val)) continue;
4044
+ const key = `${node.id}:${val}`;
4045
+ if (seen.has(key)) continue;
4046
+ seen.add(key);
4047
+ findings.push({
4048
+ id: `hardcoded:${key}`,
4049
+ severity: "info",
4050
+ category: "hardcoded_values",
4051
+ title: `"${val}" in ${node.id}`,
4052
+ detail: `ALL_CAPS string literal "${val}" appears in API code but is not in the enum/seed inventory. May be an unregistered constant.`,
4053
+ file: node.id
4054
+ });
4055
+ }
4056
+ }
4057
+ return buildReport("static", "hardcoded_values", findings);
4058
+ }
4059
+ function buildReport(layer, check, findings) {
4060
+ return {
4061
+ layer,
4062
+ check,
4063
+ findings,
4064
+ summary: {
4065
+ errors: findings.filter((f) => f.severity === "error").length,
4066
+ warnings: findings.filter((f) => f.severity === "warning").length,
4067
+ info: findings.filter((f) => f.severity === "info").length
4068
+ },
4069
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4070
+ };
4071
+ }
4072
+ var CHECKS = {
4073
+ db: {
4074
+ schema_drift: checkSchemaDrift,
4075
+ orphan_fks: checkOrphanFks
4076
+ },
4077
+ api: {
4078
+ unprotected_routes: checkUnprotectedRoutes
4079
+ },
4080
+ ui: {
4081
+ dead_screens: checkDeadScreens
4082
+ },
4083
+ static: {
4084
+ unenforced_permissions: checkUnenforcedPermissions,
4085
+ hardcoded_values: checkHardcodedValues
4086
+ }
4087
+ };
4088
+ function getAvailableChecks() {
4089
+ const result = {};
4090
+ for (const [layer, checks] of Object.entries(CHECKS)) {
4091
+ result[layer] = Object.keys(checks);
4092
+ }
4093
+ return result;
4094
+ }
4095
+ function runAudit(rootDir, layer, check) {
4096
+ if (layer === "all") {
4097
+ const reports = [];
4098
+ for (const [l, checks] of Object.entries(CHECKS)) {
4099
+ for (const fn of Object.values(checks)) {
4100
+ reports.push(fn(rootDir));
4101
+ }
4102
+ }
4103
+ return reports;
4104
+ }
4105
+ const layerChecks = CHECKS[layer];
4106
+ if (!layerChecks) {
4107
+ return [{
4108
+ layer,
4109
+ check: "unknown",
4110
+ findings: [{ id: "invalid-layer", severity: "error", category: "system", title: "Invalid layer", detail: `Layer "${layer}" has no audit checks. Available: ${Object.keys(CHECKS).join(", ")}` }],
4111
+ summary: { errors: 1, warnings: 0, info: 0 },
4112
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4113
+ }];
4114
+ }
4115
+ if (check) {
4116
+ const fn = layerChecks[check];
4117
+ if (!fn) {
4118
+ return [{
4119
+ layer,
4120
+ check,
4121
+ findings: [{ id: "invalid-check", severity: "error", category: "system", title: "Invalid check", detail: `Check "${check}" not found for layer "${layer}". Available: ${Object.keys(layerChecks).join(", ")}` }],
4122
+ summary: { errors: 1, warnings: 0, info: 0 },
4123
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4124
+ }];
4125
+ }
4126
+ return [fn(rootDir)];
4127
+ }
4128
+ return Object.values(layerChecks).map((fn) => fn(rootDir));
4129
+ }
4130
+ function formatAsPrompt(reports) {
4131
+ const lines = [];
4132
+ for (const report of reports) {
4133
+ if (report.findings.length === 0) continue;
4134
+ lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
4135
+ lines.push("");
4136
+ for (const f of report.findings) {
4137
+ const tag = f.severity === "error" ? "ERROR" : f.severity === "warning" ? "WARNING" : "INFO";
4138
+ const filePart = f.file ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : "";
4139
+ lines.push(`- [${tag}] ${f.title}${filePart}`);
4140
+ lines.push(` ${f.detail}`);
4141
+ }
4142
+ lines.push("");
4143
+ }
4144
+ if (lines.length === 0) return "No audit findings.";
4145
+ return lines.join("\n");
4146
+ }
4147
+
4148
+ // src/server/chart-serve.ts
2679
4149
  var MAX_PORT_SCAN = 3;
2680
4150
  function randomPort() {
2681
4151
  return 49152 + Math.floor(Math.random() * (65535 - 49152));
@@ -2694,31 +4164,39 @@ var MIME_TYPES = {
2694
4164
  function findProjectRoot(startDir) {
2695
4165
  let dir = startDir;
2696
4166
  for (let i = 0; i < 8; i++) {
2697
- const graphsDir2 = import_node_path15.default.join(dir, ".launchsecure", "graphs");
2698
- if (import_node_fs13.default.existsSync(import_node_path15.default.join(graphsDir2, "ui.json")) || import_node_fs13.default.existsSync(import_node_path15.default.join(graphsDir2, "api.json")) || import_node_fs13.default.existsSync(import_node_path15.default.join(graphsDir2, "db.json"))) return dir;
2699
- const parent = import_node_path15.default.dirname(dir);
4167
+ const graphsDir2 = import_node_path19.default.join(dir, ".launchsecure", "graphs");
4168
+ if (import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "ui.json")) || import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "api.json")) || import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "db.json"))) return dir;
4169
+ const parent = import_node_path19.default.dirname(dir);
2700
4170
  if (parent === dir) break;
2701
4171
  dir = parent;
2702
4172
  }
2703
4173
  dir = startDir;
2704
4174
  for (let i = 0; i < 8; i++) {
2705
- if (import_node_fs13.default.existsSync(import_node_path15.default.join(dir, ".git"))) return dir;
2706
- const parent = import_node_path15.default.dirname(dir);
4175
+ if (import_node_fs17.default.existsSync(import_node_path19.default.join(dir, ".git"))) return dir;
4176
+ const parent = import_node_path19.default.dirname(dir);
2707
4177
  if (parent === dir) break;
2708
4178
  dir = parent;
2709
4179
  }
2710
4180
  return startDir;
2711
4181
  }
2712
- async function buildMergedGraph(projectRoot) {
2713
- let graphs = readAllGraphs(projectRoot);
2714
- if (!graphs.ui && !graphs.api && !graphs.db) {
2715
- await generateGraph(projectRoot);
2716
- graphs = readAllGraphs(projectRoot);
4182
+ function resolveRequestRoot(url, monorepoRoot, projects) {
4183
+ const projectParam = url.searchParams.get("project");
4184
+ if (!projectParam || projects.length === 0) return monorepoRoot;
4185
+ const resolved = import_node_path19.default.resolve(monorepoRoot, projectParam);
4186
+ if (!resolved.startsWith(monorepoRoot)) {
4187
+ throw new Error("Project path outside monorepo root");
4188
+ }
4189
+ return resolved;
4190
+ }
4191
+ async function buildMergedGraph(root) {
4192
+ let graphs = readAllGraphs(root);
4193
+ if (Object.keys(graphs).length === 0) {
4194
+ await generateGraph(root);
4195
+ graphs = readAllGraphs(root);
2717
4196
  }
2718
4197
  const nodes = [];
2719
4198
  const rawLinks = [];
2720
- const LAYERS2 = ["ui", "api", "db"];
2721
- for (const layer of LAYERS2) {
4199
+ for (const layer of Object.keys(graphs)) {
2722
4200
  const g = graphs[layer];
2723
4201
  if (!g) continue;
2724
4202
  for (const n of g.nodes) {
@@ -2755,16 +4233,16 @@ async function buildMergedGraph(projectRoot) {
2755
4233
  };
2756
4234
  }
2757
4235
  function serveStatic(res, filePath) {
2758
- if (!import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) return false;
2759
- const ext = import_node_path15.default.extname(filePath).toLowerCase();
4236
+ if (!import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) return false;
4237
+ const ext = import_node_path19.default.extname(filePath).toLowerCase();
2760
4238
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
2761
4239
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
2762
- import_node_fs13.default.createReadStream(filePath).pipe(res);
4240
+ import_node_fs17.default.createReadStream(filePath).pipe(res);
2763
4241
  return true;
2764
4242
  }
2765
4243
  function serveIndex(res, clientDir) {
2766
- const indexPath = import_node_path15.default.join(clientDir, "index.html");
2767
- if (!import_node_fs13.default.existsSync(indexPath)) {
4244
+ const indexPath = import_node_path19.default.join(clientDir, "index.html");
4245
+ if (!import_node_fs17.default.existsSync(indexPath)) {
2768
4246
  res.writeHead(500, { "Content-Type": "text/plain" });
2769
4247
  res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
2770
4248
  return;
@@ -2816,19 +4294,40 @@ async function startChartServer(opts = {}) {
2816
4294
  }
2817
4295
  return { port: existing.port, url: existing.url };
2818
4296
  }
2819
- const clientDir = opts.clientDir ?? import_node_path15.default.join(__dirname, "..", "chart-client");
4297
+ const clientDir = opts.clientDir ?? import_node_path19.default.join(__dirname, "..", "chart-client");
4298
+ const rootConfig = loadConfig(projectRoot);
4299
+ const projects = rootConfig.projects ?? [];
2820
4300
  const server = import_node_http.default.createServer((req, res) => {
2821
4301
  try {
2822
4302
  const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
4303
+ let reqRoot;
4304
+ try {
4305
+ reqRoot = resolveRequestRoot(url2, projectRoot, projects);
4306
+ } catch (err) {
4307
+ res.writeHead(400, { "Content-Type": "application/json" });
4308
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
4309
+ return;
4310
+ }
4311
+ if (req.method === "GET" && url2.pathname === "/api/projects") {
4312
+ const projectList = projects.length > 0 ? projects.map((p) => {
4313
+ const absRoot = import_node_path19.default.resolve(projectRoot, p.root);
4314
+ const hasGraphs = import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, ".launchsecure", "graphs"));
4315
+ const hasNextConfig2 = import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.ts")) || import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.js")) || import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.mjs"));
4316
+ return { name: p.name, root: p.root, hasGraphs, hasNextConfig: hasNextConfig2 };
4317
+ }) : [{ name: import_node_path19.default.basename(projectRoot), root: ".", hasGraphs: import_node_fs17.default.existsSync(import_node_path19.default.join(projectRoot, ".launchsecure", "graphs")), hasNextConfig: true }];
4318
+ res.writeHead(200, { "Content-Type": "application/json" });
4319
+ res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
4320
+ return;
4321
+ }
2823
4322
  if (req.method === "GET" && url2.pathname === "/api/project-graph") {
2824
4323
  const regenerate = url2.searchParams.get("regenerate") === "1";
2825
4324
  (async () => {
2826
- if (regenerate) await generateGraph(projectRoot);
2827
- const merged = await buildMergedGraph(projectRoot);
4325
+ if (regenerate) await generateGraph(reqRoot);
4326
+ const merged = await buildMergedGraph(reqRoot);
2828
4327
  res.writeHead(200, { "Content-Type": "application/json" });
2829
4328
  res.end(JSON.stringify({
2830
4329
  ...merged,
2831
- debug: { cwd, projectRoot }
4330
+ debug: { cwd, projectRoot: reqRoot }
2832
4331
  }));
2833
4332
  })().catch((e) => {
2834
4333
  res.writeHead(500);
@@ -2837,15 +4336,15 @@ async function startChartServer(opts = {}) {
2837
4336
  return;
2838
4337
  }
2839
4338
  if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
2840
- const graphs = readAllGraphs(projectRoot);
4339
+ const graphs = readAllGraphs(reqRoot);
2841
4340
  res.writeHead(200, { "Content-Type": "application/json" });
2842
4341
  res.end(JSON.stringify({ ui: graphs.ui ?? null, api: graphs.api ?? null, db: graphs.db ?? null }));
2843
4342
  return;
2844
4343
  }
2845
4344
  if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
2846
4345
  (async () => {
2847
- await generateGraph(projectRoot);
2848
- const graphs = readAllGraphs(projectRoot);
4346
+ await generateGraph(reqRoot);
4347
+ const graphs = readAllGraphs(reqRoot);
2849
4348
  res.writeHead(200, { "Content-Type": "application/json" });
2850
4349
  res.end(JSON.stringify({
2851
4350
  ok: true,
@@ -2861,41 +4360,43 @@ async function startChartServer(opts = {}) {
2861
4360
  }
2862
4361
  if (req.method === "GET" && url2.pathname === "/api/file-content") {
2863
4362
  const relPath = url2.searchParams.get("path");
2864
- if (!relPath || relPath.includes("..") || import_node_path15.default.isAbsolute(relPath)) {
4363
+ if (!relPath || relPath.includes("..") || import_node_path19.default.isAbsolute(relPath)) {
2865
4364
  res.writeHead(400, { "Content-Type": "application/json" });
2866
4365
  res.end(JSON.stringify({ error: "Invalid path" }));
2867
4366
  return;
2868
4367
  }
2869
- const filePath = import_node_path15.default.join(projectRoot, relPath);
2870
- if (!filePath.startsWith(projectRoot) || !import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) {
4368
+ const filePath = import_node_path19.default.join(reqRoot, relPath);
4369
+ if (!filePath.startsWith(reqRoot) || !import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) {
2871
4370
  res.writeHead(404, { "Content-Type": "application/json" });
2872
4371
  res.end(JSON.stringify({ error: "File not found" }));
2873
4372
  return;
2874
4373
  }
2875
- const ext = import_node_path15.default.extname(filePath).toLowerCase();
4374
+ const ext = import_node_path19.default.extname(filePath).toLowerCase();
2876
4375
  const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
2877
- const content = import_node_fs13.default.readFileSync(filePath, "utf-8");
4376
+ const content = import_node_fs17.default.readFileSync(filePath, "utf-8");
2878
4377
  res.writeHead(200, { "Content-Type": "application/json" });
2879
4378
  res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
2880
4379
  return;
2881
4380
  }
2882
4381
  if (req.method === "GET" && url2.pathname === "/api/health") {
2883
4382
  res.writeHead(200, { "Content-Type": "application/json" });
2884
- res.end(JSON.stringify({ ok: true, projectRoot }));
4383
+ res.end(JSON.stringify({ ok: true, projectRoot: reqRoot }));
2885
4384
  return;
2886
4385
  }
2887
4386
  if (req.method === "GET" && url2.pathname === "/api/parser-config") {
2888
- const config2 = loadConfig(projectRoot);
2889
- const detection = [
2890
- { id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
2891
- { id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
2892
- { id: "prisma-schema", layer: "db", label: "Prisma Schema", detected: prismaSchemaParser.detect(projectRoot) }
2893
- ];
2894
- const crosslayerParsers = [
2895
- { id: "fetch-resolver", label: "Fetch / api.method() calls" },
2896
- { id: "api-annotations", label: "@api annotations" },
2897
- { id: "url-literal-scanner", label: "/api/... URL literals" }
2898
- ];
4387
+ const config2 = loadConfig(reqRoot);
4388
+ const registry = createRegistry(config2, reqRoot);
4389
+ const detection = [];
4390
+ for (const parser of registry.getAll()) {
4391
+ if ("layers" in parser && Array.isArray(parser.layers)) {
4392
+ const mp = parser;
4393
+ detection.push({ id: mp.id, layers: mp.layers, detected: mp.detect(reqRoot) });
4394
+ } else if ("layer" in parser && parser.layer !== "crosslayer") {
4395
+ const sp = parser;
4396
+ detection.push({ id: sp.id, layers: [sp.layer], detected: sp.detect(reqRoot) });
4397
+ }
4398
+ }
4399
+ const crosslayerParsers = registry.getCrossLayerParsers().map((p) => ({ id: p.id }));
2899
4400
  res.writeHead(200, { "Content-Type": "application/json" });
2900
4401
  res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
2901
4402
  return;
@@ -2908,8 +4409,8 @@ async function startChartServer(opts = {}) {
2908
4409
  req.on("end", () => {
2909
4410
  try {
2910
4411
  const newConfig = JSON.parse(body);
2911
- const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
2912
- import_node_fs13.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
4412
+ const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
4413
+ import_node_fs17.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
2913
4414
  res.writeHead(200, { "Content-Type": "application/json" });
2914
4415
  res.end(JSON.stringify({ ok: true }));
2915
4416
  } catch (err) {
@@ -2920,7 +4421,7 @@ async function startChartServer(opts = {}) {
2920
4421
  return;
2921
4422
  }
2922
4423
  if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
2923
- const config2 = loadConfig(projectRoot);
4424
+ const config2 = loadConfig(reqRoot);
2924
4425
  const builtinTaggers = [
2925
4426
  { id: "module", tagKey: "module", trackUntagged: config2.taggers?.trackUntagged?.module ?? true },
2926
4427
  { id: "screen", tagKey: "screen", trackUntagged: config2.taggers?.trackUntagged?.screen ?? true }
@@ -2940,10 +4441,10 @@ async function startChartServer(opts = {}) {
2940
4441
  req.on("end", () => {
2941
4442
  try {
2942
4443
  const taggerConfig = JSON.parse(body);
2943
- const config2 = loadConfig(projectRoot);
4444
+ const config2 = loadConfig(reqRoot);
2944
4445
  config2.taggers = taggerConfig;
2945
- const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
2946
- import_node_fs13.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
4446
+ const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
4447
+ import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
2947
4448
  res.writeHead(200, { "Content-Type": "application/json" });
2948
4449
  res.end(JSON.stringify({ ok: true }));
2949
4450
  } catch (err) {
@@ -2954,7 +4455,7 @@ async function startChartServer(opts = {}) {
2954
4455
  return;
2955
4456
  }
2956
4457
  if (req.method === "GET" && url2.pathname === "/api/tags") {
2957
- const store = readTagStore(projectRoot);
4458
+ const store = readTagStore(reqRoot);
2958
4459
  res.writeHead(200, { "Content-Type": "application/json" });
2959
4460
  res.end(JSON.stringify(store));
2960
4461
  return;
@@ -2972,7 +4473,7 @@ async function startChartServer(opts = {}) {
2972
4473
  res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
2973
4474
  return;
2974
4475
  }
2975
- setTag(projectRoot, nodeId, key, value);
4476
+ setTag(reqRoot, nodeId, key, value);
2976
4477
  res.writeHead(200, { "Content-Type": "application/json" });
2977
4478
  res.end(JSON.stringify({ ok: true }));
2978
4479
  } catch (err) {
@@ -2995,7 +4496,76 @@ async function startChartServer(opts = {}) {
2995
4496
  res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
2996
4497
  return;
2997
4498
  }
2998
- removeTag(projectRoot, nodeId, key);
4499
+ removeTag(reqRoot, nodeId, key);
4500
+ res.writeHead(200, { "Content-Type": "application/json" });
4501
+ res.end(JSON.stringify({ ok: true }));
4502
+ } catch (err) {
4503
+ res.writeHead(400, { "Content-Type": "application/json" });
4504
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
4505
+ }
4506
+ });
4507
+ return;
4508
+ }
4509
+ if (req.method === "GET" && url2.pathname === "/api/detected-paths") {
4510
+ const config2 = loadConfig(reqRoot);
4511
+ const paths = resolveProjectPaths(reqRoot, config2);
4512
+ const isOverride = !!config2.paths?.appDir;
4513
+ res.writeHead(200, { "Content-Type": "application/json" });
4514
+ res.end(JSON.stringify({
4515
+ projectRoot: reqRoot,
4516
+ detected: paths ? {
4517
+ srcDir: import_node_path19.default.relative(reqRoot, paths.srcDir) || ".",
4518
+ appDir: import_node_path19.default.relative(reqRoot, paths.appDir),
4519
+ apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir)
4520
+ } : null,
4521
+ isOverride
4522
+ }));
4523
+ return;
4524
+ }
4525
+ if (req.method === "GET" && url2.pathname === "/api/browse-dir") {
4526
+ const browsePath = url2.searchParams.get("path") || projectRoot;
4527
+ const abs = import_node_path19.default.resolve(browsePath);
4528
+ const twoUp = import_node_path19.default.resolve(projectRoot, "..", "..");
4529
+ if (!abs.startsWith(twoUp)) {
4530
+ res.writeHead(403, { "Content-Type": "application/json" });
4531
+ res.end(JSON.stringify({ ok: false, error: "Path outside allowed range" }));
4532
+ return;
4533
+ }
4534
+ try {
4535
+ const entries = import_node_fs17.default.readdirSync(abs, { withFileTypes: true });
4536
+ const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "dist" && e.name !== ".next").map((e) => e.name).sort();
4537
+ const parent = abs !== twoUp ? import_node_path19.default.dirname(abs) : null;
4538
+ res.writeHead(200, { "Content-Type": "application/json" });
4539
+ res.end(JSON.stringify({ current: abs, parent, dirs, relative: import_node_path19.default.relative(projectRoot, abs) || "." }));
4540
+ } catch (err) {
4541
+ res.writeHead(400, { "Content-Type": "application/json" });
4542
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
4543
+ }
4544
+ return;
4545
+ }
4546
+ if (req.method === "GET" && url2.pathname === "/api/audit") {
4547
+ const layer = url2.searchParams.get("layer") ?? "all";
4548
+ const check = url2.searchParams.get("check") ?? void 0;
4549
+ const reports = runAudit(reqRoot, layer, check);
4550
+ const prompt = formatAsPrompt(reports);
4551
+ res.writeHead(200, { "Content-Type": "application/json" });
4552
+ res.end(JSON.stringify({ reports, prompt, checks: getAvailableChecks() }));
4553
+ return;
4554
+ }
4555
+ if (req.method === "POST" && url2.pathname === "/api/projects") {
4556
+ let body = "";
4557
+ req.on("data", (chunk) => {
4558
+ body += chunk.toString();
4559
+ });
4560
+ req.on("end", () => {
4561
+ try {
4562
+ const { projects: newProjects } = JSON.parse(body);
4563
+ const config2 = loadConfig(projectRoot);
4564
+ config2.projects = newProjects.length > 0 ? newProjects : void 0;
4565
+ const configPath = import_node_path19.default.join(projectRoot, ".launchchart.json");
4566
+ import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
4567
+ projects.length = 0;
4568
+ if (config2.projects) projects.push(...config2.projects);
2999
4569
  res.writeHead(200, { "Content-Type": "application/json" });
3000
4570
  res.end(JSON.stringify({ ok: true }));
3001
4571
  } catch (err) {
@@ -3006,7 +4576,7 @@ async function startChartServer(opts = {}) {
3006
4576
  return;
3007
4577
  }
3008
4578
  if (url2.pathname !== "/") {
3009
- const staticPath = import_node_path15.default.join(clientDir, url2.pathname);
4579
+ const staticPath = import_node_path19.default.join(clientDir, url2.pathname);
3010
4580
  if (serveStatic(res, staticPath)) return;
3011
4581
  }
3012
4582
  serveIndex(res, clientDir);
@@ -3044,6 +4614,10 @@ async function startChartServer(opts = {}) {
3044
4614
  `);
3045
4615
  process.stderr.write(`[launch-chart] project root: ${projectRoot}
3046
4616
  `);
4617
+ if (projects.length > 0) {
4618
+ process.stderr.write(`[launch-chart] multi-project mode: ${projects.length} projects
4619
+ `);
4620
+ }
3047
4621
  }
3048
4622
  return { port, url };
3049
4623
  }