@launchsecure/launch-kit 0.0.13 → 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 ?? [];
@@ -596,16 +563,108 @@ function extractDeep(absPath) {
596
563
  }
597
564
  return { elements, stateVars, conditions, variables, responses, params };
598
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");
599
610
 
600
- // src/server/graph/parsers/ui/react-nextjs.ts
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
+ }
643
+
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
+ };
601
660
  function walk(dir, exts) {
602
661
  const results = [];
603
- if (!(0, import_node_fs3.existsSync)(dir)) return results;
604
- for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
605
- 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);
606
665
  if (entry.isDirectory()) {
607
666
  results.push(...walk(full, exts));
608
- } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
667
+ } else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
609
668
  results.push(full);
610
669
  }
611
670
  }
@@ -613,33 +672,33 @@ function walk(dir, exts) {
613
672
  }
614
673
  function walkWithIgnore(dir, exts, ignoreDirs) {
615
674
  const results = [];
616
- if (!(0, import_node_fs3.existsSync)(dir)) return results;
617
- 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 })) {
618
677
  if (entry.isDirectory()) {
619
678
  if (ignoreDirs.has(entry.name)) continue;
620
- results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, ignoreDirs));
621
- } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
622
- 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));
623
682
  }
624
683
  }
625
684
  return results;
626
685
  }
627
686
  function toNodeId(srcDir, absPath) {
628
- return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
687
+ return (0, import_node_path4.relative)(srcDir, absPath).replace(/\\/g, "/");
629
688
  }
630
689
  function resolveImport(srcDir, specifier) {
631
690
  if (!specifier.startsWith("@/")) return null;
632
691
  const rel = specifier.slice(2);
633
- const base = (0, import_node_path3.join)(srcDir, rel);
634
- 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")]) {
635
- 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;
636
695
  }
637
696
  return null;
638
697
  }
639
698
  function resolveRelativeImport(fromFile, specifier) {
640
- const base = (0, import_node_path3.join)((0, import_node_path3.dirname)(fromFile), specifier);
641
- 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")]) {
642
- 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;
643
702
  }
644
703
  return null;
645
704
  }
@@ -660,7 +719,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
660
719
  const resolved = resolveRelativeImport(barrelAbsPath, re.from);
661
720
  if (!resolved) continue;
662
721
  if (re.isWildcard) {
663
- const targetBn = (0, import_node_path3.basename)(resolved);
722
+ const targetBn = (0, import_node_path4.basename)(resolved);
664
723
  const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
665
724
  if (targetIsBarrel) {
666
725
  const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
@@ -687,12 +746,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
687
746
  const barrels = /* @__PURE__ */ new Map();
688
747
  const memo = /* @__PURE__ */ new Map();
689
748
  for (const [absPath, parsed] of parsedByPath) {
690
- const bn = (0, import_node_path3.basename)(absPath);
749
+ const bn = (0, import_node_path4.basename)(absPath);
691
750
  if (bn !== "index.ts" && bn !== "index.tsx") continue;
692
751
  if (parsed.reExports.length === 0) continue;
693
752
  const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
694
753
  if (map.size > 0) {
695
- 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, "/");
696
755
  barrels.set(barrelId, map);
697
756
  }
698
757
  }
@@ -713,7 +772,18 @@ function extractRoute(id) {
713
772
  return route || "/";
714
773
  }
715
774
  function nameFromFilename(absPath) {
716
- 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);
717
787
  }
718
788
  function resolveTemplateLiteralRoute(template, routeToNodeId) {
719
789
  const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -794,7 +864,7 @@ function matchRouteToPage(route, routeToNodeId) {
794
864
  if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
795
865
  return null;
796
866
  }
797
- function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
867
+ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps, routeToNodeId) {
798
868
  const edges = [];
799
869
  const flagged = [];
800
870
  const seen = /* @__PURE__ */ new Set();
@@ -806,7 +876,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
806
876
  if (label) edge.label = label;
807
877
  edges.push(edge);
808
878
  }
809
- function edgeTypeFor(_targetId, isTypeOnlyImport, importedNames) {
879
+ function edgeTypeFor(isTypeOnlyImport, importedNames) {
810
880
  if (isTypeOnlyImport) return "imports";
811
881
  const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
812
882
  if (anyRendered) return "renders";
@@ -831,14 +901,14 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
831
901
  }
832
902
  for (const [targetId, targetNames] of byTarget) {
833
903
  const allType = isTypeOnly || targetNames.every((n) => typeNames.has(n));
834
- addEdge(targetId, edgeTypeFor(targetId, allType, targetNames));
904
+ addEdge(targetId, edgeTypeFor(allType, targetNames));
835
905
  }
836
906
  } else {
837
907
  const resolved = resolveImport(srcDir, specifier);
838
908
  if (resolved) {
839
909
  const targetId = toNodeId(srcDir, resolved);
840
910
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
841
- addEdge(targetId, edgeTypeFor(targetId, isTypeOnly, names));
911
+ addEdge(targetId, edgeTypeFor(isTypeOnly, names));
842
912
  }
843
913
  }
844
914
  }
@@ -847,7 +917,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
847
917
  if (resolved) {
848
918
  const targetId = toNodeId(srcDir, resolved);
849
919
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
850
- addEdge(targetId, edgeTypeFor(targetId, isTypeOnly, names));
920
+ addEdge(targetId, edgeTypeFor(isTypeOnly, names));
851
921
  }
852
922
  }
853
923
  }
@@ -889,74 +959,113 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
889
959
  }
890
960
  return { edges, flagged };
891
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
+ }
892
965
  function detect(rootDir) {
893
- 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);
894
968
  }
895
969
  function generate(rootDir) {
896
- const srcDir = (0, import_node_path3.join)(rootDir, "src");
897
- const appFiles = walk((0, import_node_path3.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
898
- (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
899
- );
900
- const clientFiles = walk((0, import_node_path3.join)(srcDir, "client"), [".tsx", ".ts"]);
901
- const serverFiles = walk((0, import_node_path3.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
902
- (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
903
- );
904
- const libFiles = walk((0, import_node_path3.join)(srcDir, "lib"), [".ts", ".tsx"]);
905
- 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"]);
906
979
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
907
980
  const parsedByPath = /* @__PURE__ */ new Map();
908
981
  for (const absPath of allDiscovered) {
909
982
  parsedByPath.set(absPath, parseFileTS(absPath));
910
983
  }
911
984
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
912
- const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
913
- const nodes = [];
985
+ const uiNodes = [];
986
+ const apiNodes = [];
914
987
  const nodeIdSet = /* @__PURE__ */ new Set();
915
- const nodeTypeMap = /* @__PURE__ */ new Map();
916
988
  const routeToNodeId = /* @__PURE__ */ new Map();
989
+ const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
917
990
  for (const absPath of fileSet) {
918
991
  const id = toNodeId(srcDir, absPath);
919
992
  const type = classifyType(absPath, id);
993
+ if (type === "test" || type === "story") continue;
920
994
  const parsed = parsedByPath.get(absPath);
921
995
  const name = parsed.name || nameFromFilename(absPath);
922
- const route = extractRoute(id);
923
- const deep = extractDeep(absPath);
924
- nodes.push({
925
- id,
926
- type,
927
- name,
928
- route,
929
- exports: parsed.exports,
930
- elements: deep.elements,
931
- stateVars: deep.stateVars,
932
- conditions: deep.conditions,
933
- variables: deep.variables
934
- });
996
+ const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
935
997
  nodeIdSet.add(id);
936
- nodeTypeMap.set(id, type);
937
- 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
+ }
938
1046
  }
939
- const allEdges = [];
940
- const allFlagged = [];
1047
+ const uiEdges = [];
1048
+ const uiFlagged = [];
941
1049
  for (const absPath of fileSet) {
942
- const sourceId = toNodeId(srcDir, absPath);
1050
+ const id = toNodeId(srcDir, absPath);
1051
+ if (!nodeIdSet.has(id)) continue;
943
1052
  const parsed = parsedByPath.get(absPath);
944
1053
  const { edges, flagged } = extractEdges(
945
1054
  srcDir,
946
1055
  absPath,
947
- sourceId,
1056
+ id,
948
1057
  parsed,
949
1058
  nodeIdSet,
950
- nodeTypeMap,
951
1059
  barrelMaps,
952
1060
  routeToNodeId
953
1061
  );
954
- allEdges.push(...edges);
955
- allFlagged.push(...flagged);
1062
+ uiEdges.push(...edges);
1063
+ uiFlagged.push(...flagged);
956
1064
  }
957
1065
  const fetchCallEntries = [];
958
1066
  for (const absPath of fileSet) {
959
1067
  const sourceId = toNodeId(srcDir, absPath);
1068
+ if (!nodeIdSet.has(sourceId)) continue;
960
1069
  const parsed = parsedByPath.get(absPath);
961
1070
  if (parsed.fetchCalls.length === 0) continue;
962
1071
  fetchCallEntries.push({
@@ -994,11 +1103,11 @@ function generate(rootDir) {
994
1103
  } catch {
995
1104
  continue;
996
1105
  }
997
- const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
1106
+ const externalId = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
998
1107
  const edgesFromThis = [];
999
1108
  const seen = /* @__PURE__ */ new Set();
1000
1109
  for (const imp of parsed.imports) {
1001
- const { specifier, isTypeOnly, names } = imp;
1110
+ const { specifier, names } = imp;
1002
1111
  let resolved = null;
1003
1112
  if (specifier.startsWith("@/")) {
1004
1113
  const relToSrc = specifier.slice(2);
@@ -1024,25 +1133,52 @@ function generate(rootDir) {
1024
1133
  const targetId = toNodeId(srcDir, resolved);
1025
1134
  if (!nodeIdSet.has(targetId)) continue;
1026
1135
  if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
1027
- const key = `${externalId}\u2192${targetId}\u2192${isTypeOnly ? "type" : "value"}`;
1136
+ const key = `${externalId}\u2192${targetId}`;
1028
1137
  if (seen.has(key)) continue;
1029
1138
  seen.add(key);
1030
1139
  edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
1031
1140
  }
1032
1141
  if (edgesFromThis.length === 0) continue;
1033
- nodes.push({
1142
+ uiNodes.push({
1034
1143
  id: externalId,
1035
1144
  type: "external",
1036
1145
  name: parsed.name || nameFromFilename(absPath),
1146
+ layer: "ui",
1037
1147
  route: null,
1038
1148
  exports: parsed.exports
1039
1149
  });
1040
1150
  nodeIdSet.add(externalId);
1041
- nodeTypeMap.set(externalId, "external");
1042
- 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
+ }
1043
1179
  }
1044
1180
  const flaggedSet = /* @__PURE__ */ new Set();
1045
- const dedupedFlagged = allFlagged.filter((f) => {
1181
+ const dedupedFlagged = uiFlagged.filter((f) => {
1046
1182
  const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
1047
1183
  if (flaggedSet.has(key)) return false;
1048
1184
  flaggedSet.add(key);
@@ -1059,10 +1195,12 @@ function generate(rootDir) {
1059
1195
  hook: 7,
1060
1196
  lib: 8
1061
1197
  };
1062
- nodes.sort((a, b) => (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99) || a.id.localeCompare(b.id));
1063
- allEdges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1064
- const byType = (t) => nodes.filter((n) => n.type === t).length;
1065
- 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 = {
1066
1204
  total_pages: byType("page"),
1067
1205
  total_layouts: byType("layout"),
1068
1206
  total_components: byType("component"),
@@ -1073,182 +1211,99 @@ function generate(rootDir) {
1073
1211
  total_utils: byType("util"),
1074
1212
  total_libs: byType("lib"),
1075
1213
  total_external: byType("external"),
1076
- total_edges: allEdges.length,
1214
+ total_edges: uiOnlyEdges.length,
1077
1215
  total_flagged: dedupedFlagged.length
1078
1216
  };
1079
- return {
1217
+ const stripLayer = (nodes) => nodes.map(({ layer: _, ...rest }) => rest);
1218
+ const result = /* @__PURE__ */ new Map();
1219
+ result.set("ui", {
1080
1220
  metadata: {
1081
1221
  generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1082
1222
  scope: "main-app-only",
1083
1223
  app_root: "src/",
1084
1224
  layer: "ui",
1085
- parser: "react-nextjs-ast",
1086
- ...stats,
1225
+ parser: "typescript-project",
1226
+ ...uiStats,
1087
1227
  notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
1088
1228
  },
1089
- nodes,
1090
- edges: allEdges,
1229
+ nodes: stripLayer(uiNodes),
1230
+ edges: uiOnlyEdges,
1091
1231
  cross_refs: [],
1092
1232
  contradictions: [],
1093
1233
  warnings: [],
1094
1234
  flagged_edges: dedupedFlagged,
1095
1235
  patterns: {
1096
- total_nodes: nodes.length,
1097
- by_type: stats,
1236
+ total_nodes: uiNodes.length,
1237
+ by_type: uiStats,
1098
1238
  by_edge_type: {
1099
- renders: allEdges.filter((e) => e.type === "renders").length,
1100
- imports: allEdges.filter((e) => e.type === "imports").length,
1101
- 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
1102
1242
  },
1103
1243
  fetch_calls: fetchCallEntries
1104
1244
  }
1105
- };
1106
- }
1107
- var reactNextjsParser = {
1108
- id: "react-nextjs",
1109
- layer: "ui",
1110
- detect,
1111
- generate
1112
- };
1113
-
1114
- // src/server/graph/parsers/api/nextjs-routes.ts
1115
- var import_node_fs4 = require("node:fs");
1116
- var import_node_path4 = require("node:path");
1117
- var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
1118
- function walk2(dir) {
1119
- const results = [];
1120
- if (!(0, import_node_fs4.existsSync)(dir)) return results;
1121
- for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
1122
- const full = (0, import_node_path4.join)(dir, entry.name);
1123
- if (entry.isDirectory()) {
1124
- results.push(...walk2(full));
1125
- } else if (entry.name === "route.ts" || entry.name === "route.tsx") {
1126
- 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
+ }
1127
1259
  }
1128
- }
1129
- return results;
1130
- }
1131
- function filePathToRoute(apiDir, absPath) {
1132
- let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
1133
- route = route.replace(/\[([^\]]+)\]/g, ":$1");
1134
- route = route.replace(/\/+/g, "/");
1135
- if (route === "/") return "/api";
1136
- return "/api" + route;
1137
- }
1138
- function camelToPascal(s) {
1139
- if (!s) return s;
1140
- return s.charAt(0).toUpperCase() + s.slice(1);
1141
- }
1142
- function detect2(rootDir) {
1143
- return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
1144
- }
1145
- function generate2(rootDir) {
1146
- const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
1147
- const routeFiles = walk2(apiDir);
1148
- const nodes = [];
1149
- const edges = [];
1150
- const crossRefs = [];
1151
- const mutatorCount = {};
1152
- const authUsage = {};
1153
- let endpointsWithAuth = 0;
1154
- let endpointsWithDbAccess = 0;
1155
- for (const absPath of routeFiles) {
1156
- const parsed = parseFileTS(absPath);
1157
- const dbCalls = extractDbCallsTS(absPath);
1158
- const authWrappers = extractAuthWrappersTS(absPath);
1159
- const methods = [];
1160
- for (const exp of parsed.exports) {
1161
- if (HTTP_METHODS.has(exp)) methods.push(exp);
1162
- }
1163
- const routePath = filePathToRoute(apiDir, absPath);
1164
- const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
1165
- const mutations = dbCalls.filter((c) => c.isMutation);
1166
- const reads = dbCalls.filter((c) => !c.isMutation);
1167
- const mutates = mutations.length > 0;
1168
- if (mutates) {
1169
- for (const m of mutations) {
1170
- mutatorCount[m.method] = (mutatorCount[m.method] ?? 0) + 1;
1171
- }
1172
- }
1173
- if (dbCalls.length > 0) endpointsWithDbAccess++;
1174
- const authStrategy = [];
1175
- for (const w of authWrappers) {
1176
- authStrategy.push(w);
1177
- authUsage[w] = (authUsage[w] ?? 0) + 1;
1178
- }
1179
- if (authStrategy.length > 0) endpointsWithAuth++;
1180
- const deep = extractDeep(absPath);
1181
- nodes.push({
1182
- id: relPath,
1183
- type: "endpoint",
1184
- name: routePath,
1185
- path: routePath,
1186
- methods,
1187
- handler: relPath,
1188
- // Behavioral classification from handler body (AST-derived).
1189
- mutates,
1190
- auth: authStrategy.length > 0 ? authStrategy : ["public"],
1191
- db_models: [...new Set(dbCalls.map((c) => c.model))],
1192
- db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
1193
- // Deep extraction fields
1194
- conditions: deep.conditions,
1195
- variables: deep.variables,
1196
- responses: deep.responses,
1197
- params: deep.params
1198
- });
1199
- const seenModels = /* @__PURE__ */ new Set();
1200
- for (const call of dbCalls) {
1201
- if (seenModels.has(call.model)) continue;
1202
- seenModels.add(call.model);
1203
- crossRefs.push({
1204
- source: relPath,
1205
- target: camelToPascal(call.model),
1206
- type: call.isMutation ? "mutates" : "reads",
1207
- layer: "db"
1208
- });
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
+ }
1209
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
+ });
1210
1299
  }
1211
- nodes.sort((a, b) => a.path.localeCompare(b.path));
1212
- crossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1213
- const mutatorNodes = nodes.filter((n) => n.mutates).length;
1214
- const readOnlyNodes = nodes.filter((n) => !n.mutates).length;
1215
- return {
1216
- metadata: {
1217
- generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1218
- scope: "main-app-only",
1219
- stack: "nextjs-app-router",
1220
- layer: "api",
1221
- parser: "nextjs-routes-ast",
1222
- total_endpoints: nodes.length,
1223
- total_methods: nodes.reduce((sum, n) => sum + n.methods.length, 0),
1224
- endpoints_with_auth: endpointsWithAuth,
1225
- endpoints_with_db_access: endpointsWithDbAccess,
1226
- mutator_endpoints: mutatorNodes,
1227
- read_only_endpoints: readOnlyNodes
1228
- },
1229
- nodes,
1230
- edges,
1231
- cross_refs: crossRefs,
1232
- contradictions: [],
1233
- warnings: [],
1234
- flagged_edges: [],
1235
- patterns: {
1236
- total_endpoints: nodes.length,
1237
- methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
1238
- acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
1239
- return acc;
1240
- }, {}),
1241
- auth_strategies: authUsage,
1242
- mutation_operations: mutatorCount,
1243
- mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
1244
- }
1245
- };
1300
+ return result;
1246
1301
  }
1247
- var nextjsRoutesParser = {
1248
- id: "nextjs-routes",
1249
- layer: "api",
1250
- detect: detect2,
1251
- generate: generate2
1302
+ var typescriptProjectParser = {
1303
+ id: "typescript-project",
1304
+ layers: ["ui", "api"],
1305
+ detect,
1306
+ generate
1252
1307
  };
1253
1308
 
1254
1309
  // src/server/graph/parsers/db/prisma-schema.ts
@@ -1343,10 +1398,10 @@ function parseEnums(content) {
1343
1398
  }
1344
1399
  return nodes;
1345
1400
  }
1346
- function detect3(rootDir) {
1401
+ function detect2(rootDir) {
1347
1402
  return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
1348
1403
  }
1349
- function generate3(rootDir) {
1404
+ function generate2(rootDir) {
1350
1405
  const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
1351
1406
  const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
1352
1407
  const { nodes: modelNodes, relations } = parseModels(content);
@@ -1400,6 +1455,424 @@ function generate3(rootDir) {
1400
1455
  var prismaSchemaParser = {
1401
1456
  id: "prisma-schema",
1402
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",
1403
1876
  detect: detect3,
1404
1877
  generate: generate3
1405
1878
  };
@@ -1617,31 +2090,31 @@ var fetchResolverParser = {
1617
2090
  };
1618
2091
 
1619
2092
  // src/server/graph/parsers/crosslayer/api-annotations.ts
1620
- var import_node_fs6 = require("node:fs");
1621
- var import_node_path6 = require("node:path");
2093
+ var import_node_fs7 = require("node:fs");
2094
+ var import_node_path7 = require("node:path");
1622
2095
  var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
1623
- function walk3(dir, exts) {
1624
- if (!(0, import_node_fs6.existsSync)(dir)) return [];
2096
+ function walk2(dir, exts) {
2097
+ if (!(0, import_node_fs7.existsSync)(dir)) return [];
1625
2098
  const results = [];
1626
- 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 })) {
1627
2100
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1628
- const full = (0, import_node_path6.join)(dir, entry.name);
2101
+ const full = (0, import_node_path7.join)(dir, entry.name);
1629
2102
  if (entry.isDirectory()) {
1630
- results.push(...walk3(full, exts));
1631
- } 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))) {
1632
2105
  results.push(full);
1633
2106
  }
1634
2107
  }
1635
2108
  return results;
1636
2109
  }
1637
2110
  function toNodeId2(srcDir, absPath) {
1638
- return (0, import_node_path6.relative)(srcDir, absPath).replace(/\\/g, "/");
2111
+ return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
1639
2112
  }
1640
2113
  var apiAnnotationsParser = {
1641
2114
  id: "api-annotations",
1642
2115
  layer: "crosslayer",
1643
2116
  detect(rootDir) {
1644
- 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"));
1645
2118
  },
1646
2119
  generate(rootDir, layerOutputs) {
1647
2120
  const apiOutput = layerOutputs.get("api");
@@ -1652,13 +2125,13 @@ var apiAnnotationsParser = {
1652
2125
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1653
2126
  const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1654
2127
  const apiPathMap = buildApiPathMap(apiRoutes);
1655
- const srcDir = (0, import_node_path6.join)(rootDir, "src");
1656
- const files = walk3(srcDir, [".ts", ".tsx"]);
2128
+ const srcDir = (0, import_node_path7.join)(rootDir, "src");
2129
+ const files = walk2(srcDir, [".ts", ".tsx"]);
1657
2130
  const crossRefs = [];
1658
2131
  const flaggedEdges = [];
1659
2132
  const seen = /* @__PURE__ */ new Set();
1660
2133
  for (const absPath of files) {
1661
- const content = (0, import_node_fs6.readFileSync)(absPath, "utf-8");
2134
+ const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
1662
2135
  const sourceId = toNodeId2(srcDir, absPath);
1663
2136
  if (!uiNodeIds.has(sourceId)) continue;
1664
2137
  let match;
@@ -1697,73 +2170,727 @@ var apiAnnotationsParser = {
1697
2170
  annotations_resolved: crossRefs.length,
1698
2171
  annotations_unresolved: flaggedEdges.length
1699
2172
  }
1700
- };
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;
1701
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
1702
2682
  };
1703
2683
 
1704
- // src/server/graph/parsers/crosslayer/url-literal-scanner.ts
1705
- var import_node_fs7 = require("node:fs");
1706
- var import_node_path7 = require("node:path");
1707
- 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();
1708
2688
  function walk4(dir, exts) {
1709
- if (!(0, import_node_fs7.existsSync)(dir)) return [];
2689
+ if (!(0, import_node_fs10.existsSync)(dir)) return [];
1710
2690
  const results = [];
1711
- for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
1712
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1713
- const full = (0, import_node_path7.join)(dir, entry.name);
1714
- if (entry.isDirectory()) {
1715
- results.push(...walk4(full, exts));
1716
- } else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
1717
- 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
+ }
1718
2700
  }
1719
2701
  }
2702
+ recurse(dir);
1720
2703
  return results;
1721
2704
  }
1722
- function toNodeId3(srcDir, absPath) {
1723
- 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;
1724
2735
  }
1725
- var urlLiteralScannerParser = {
1726
- 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",
1727
2812
  layer: "crosslayer",
1728
2813
  detect(rootDir) {
1729
- return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
2814
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2815
+ return paths !== null;
1730
2816
  },
1731
2817
  generate(rootDir, layerOutputs) {
1732
- const apiOutput = layerOutputs.get("api");
1733
- if (!apiOutput) {
2818
+ const staticOutput = layerOutputs.get("static");
2819
+ if (!staticOutput || staticOutput.nodes.length === 0) {
1734
2820
  return { cross_refs: [], flagged_edges: [], warnings: [] };
1735
2821
  }
1736
- const uiOutput = layerOutputs.get("ui");
1737
- const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1738
- const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1739
- const apiPathMap = buildApiPathMap(apiRoutes);
1740
- const srcDir = (0, import_node_path7.join)(rootDir, "src");
1741
- const clientDir = (0, import_node_path7.join)(srcDir, "client");
1742
- 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;
1743
2845
  const files = [
1744
- ...walk4(clientDir, [".ts", ".tsx"]),
1745
- ...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"])
1746
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
+ }
1747
2864
  const crossRefs = [];
1748
2865
  const seen = /* @__PURE__ */ new Set();
2866
+ let filesScanned = 0;
1749
2867
  for (const absPath of files) {
1750
- const sourceId = toNodeId3(srcDir, absPath);
1751
- if (!uiNodeIds.has(sourceId)) continue;
1752
- const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
1753
- let match;
1754
- URL_LITERAL_RE.lastIndex = 0;
1755
- while ((match = URL_LITERAL_RE.exec(content)) !== null) {
1756
- const urlPath = match[1];
1757
- const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
1758
- if (result.kind === "resolved" && result.nodeId) {
1759
- 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}`;
1760
2887
  if (seen.has(key)) continue;
1761
2888
  seen.add(key);
1762
2889
  crossRefs.push({
1763
2890
  source: sourceId,
1764
- target: result.nodeId,
1765
- type: "references_api",
1766
- layer: "api"
2891
+ target: targetId,
2892
+ type: "references_static",
2893
+ layer: "static"
1767
2894
  });
1768
2895
  }
1769
2896
  }
@@ -1773,16 +2900,23 @@ var urlLiteralScannerParser = {
1773
2900
  flagged_edges: [],
1774
2901
  warnings: [],
1775
2902
  patterns: {
1776
- 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"
1777
2907
  }
1778
2908
  };
1779
2909
  }
1780
2910
  };
1781
2911
 
1782
2912
  // src/server/graph/core/parser-registry.ts
2913
+ function isMultiLayerParser(p) {
2914
+ return "layers" in p && Array.isArray(p.layers);
2915
+ }
1783
2916
  var ParserRegistry = class {
1784
2917
  constructor() {
1785
- this.parsers = /* @__PURE__ */ new Map();
2918
+ this.singleLayerParsers = /* @__PURE__ */ new Map();
2919
+ this.multiLayerParsers = [];
1786
2920
  this.ids = /* @__PURE__ */ new Set();
1787
2921
  }
1788
2922
  register(parser) {
@@ -1790,30 +2924,57 @@ var ParserRegistry = class {
1790
2924
  throw new Error(`Duplicate parser id: ${parser.id}`);
1791
2925
  }
1792
2926
  this.ids.add(parser.id);
1793
- const list = this.parsers.get(parser.layer) ?? [];
1794
- list.push(parser);
1795
- 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
+ }
1796
2934
  }
2935
+ /** Get single-layer parsers for a specific layer. */
1797
2936
  getParsers(layer) {
1798
- 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];
1799
2946
  }
1800
2947
  getCrossLayerParsers() {
1801
- 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];
1802
2960
  }
1803
2961
  getAll() {
1804
2962
  const all = [];
1805
- 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);
1806
2965
  return all;
1807
2966
  }
1808
2967
  };
1809
2968
  function registerBuiltins(registry, disabled) {
1810
2969
  const builtins = [
1811
- reactNextjsParser,
1812
- nextjsRoutesParser,
2970
+ typescriptProjectParser,
1813
2971
  prismaSchemaParser,
2972
+ sqlMigrationsParser,
2973
+ staticValuesParser,
1814
2974
  fetchResolverParser,
1815
2975
  apiAnnotationsParser,
1816
- urlLiteralScannerParser
2976
+ urlLiteralScannerParser,
2977
+ staticRefScannerParser
1817
2978
  ];
1818
2979
  for (const parser of builtins) {
1819
2980
  if (disabled.has(parser.id)) continue;
@@ -1823,11 +2984,11 @@ function registerBuiltins(registry, disabled) {
1823
2984
  function loadCustomParsers(registry, config, rootDir, disabled) {
1824
2985
  for (const entry of config.parsers?.custom ?? []) {
1825
2986
  try {
1826
- const absPath = (0, import_node_path8.resolve)(rootDir, entry.path);
2987
+ const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
1827
2988
  const mod = require(absPath);
1828
2989
  const parser = "default" in mod ? mod.default : mod;
1829
2990
  if (disabled.has(parser.id)) continue;
1830
- if (parser.layer !== entry.layer) {
2991
+ if (!isMultiLayerParser(parser) && parser.layer !== entry.layer) {
1831
2992
  process.stderr.write(
1832
2993
  `[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
1833
2994
  `
@@ -1973,10 +3134,10 @@ function applyCrossLayerResults(uiOutput, results, primaryId) {
1973
3134
 
1974
3135
  // src/server/graph/core/graph-builder.ts
1975
3136
  function readGraphFromDisk(rootDir, layer) {
1976
- const filePath = (0, import_node_path9.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
1977
- 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;
1978
3139
  try {
1979
- return JSON.parse((0, import_node_fs8.readFileSync)(filePath, "utf-8"));
3140
+ return JSON.parse((0, import_node_fs11.readFileSync)(filePath, "utf-8"));
1980
3141
  } catch {
1981
3142
  return null;
1982
3143
  }
@@ -1984,18 +3145,24 @@ function readGraphFromDisk(rootDir, layer) {
1984
3145
  function generateLayer(rootDir, layer) {
1985
3146
  const config = loadConfig(rootDir);
1986
3147
  const registry = createRegistry(config, rootDir);
1987
- const parsers = registry.getParsers(layer);
1988
3148
  const outputs = [];
1989
- for (const parser of parsers) {
3149
+ for (const parser of registry.getParsers(layer)) {
1990
3150
  if (!parser.detect(rootDir)) continue;
1991
3151
  outputs.push(parser.generate(rootDir));
1992
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
+ }
1993
3159
  if (outputs.length === 0) return null;
1994
3160
  let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
1995
3161
  if (layer === "ui") {
1996
3162
  const layerOutputs = /* @__PURE__ */ new Map();
1997
3163
  layerOutputs.set("ui", merged);
1998
- for (const otherLayer of ["api", "db"]) {
3164
+ for (const otherLayer of registry.getAvailableLayers()) {
3165
+ if (otherLayer === "ui") continue;
1999
3166
  const existing = readGraphFromDisk(rootDir, otherLayer);
2000
3167
  if (existing) layerOutputs.set(otherLayer, existing);
2001
3168
  }
@@ -2016,16 +3183,29 @@ function generateLayer(rootDir, layer) {
2016
3183
  function generateAll(rootDir) {
2017
3184
  const config = loadConfig(rootDir);
2018
3185
  const registry = createRegistry(config, rootDir);
2019
- 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
+ ];
2020
3191
  const layerOutputs = /* @__PURE__ */ new Map();
2021
3192
  const results = [];
3193
+ const multiLayerResults = /* @__PURE__ */ new Map();
2022
3194
  for (const layer of layerOrder) {
2023
- const parsers = registry.getParsers(layer);
2024
3195
  const outputs = [];
2025
- for (const parser of parsers) {
3196
+ for (const parser of registry.getParsers(layer)) {
2026
3197
  if (!parser.detect(rootDir)) continue;
2027
3198
  outputs.push(parser.generate(rootDir));
2028
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
+ }
2029
3209
  if (outputs.length === 0) continue;
2030
3210
  const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
2031
3211
  layerOutputs.set(layer, merged);
@@ -2051,18 +3231,20 @@ function generateAll(rootDir) {
2051
3231
  }
2052
3232
  }
2053
3233
  const byLayer = new Map(results.map((r) => [r.layer, r]));
2054
- 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);
2055
3237
  }
2056
3238
 
2057
3239
  // src/server/graph/index.ts
2058
3240
  init_config();
2059
3241
 
2060
3242
  // src/server/graph/core/tagger-registry.ts
2061
- var import_node_path11 = require("node:path");
3243
+ var import_node_path14 = require("node:path");
2062
3244
 
2063
3245
  // src/server/graph/taggers/module-tagger.ts
2064
- var import_node_fs9 = require("node:fs");
2065
- var import_node_path10 = require("node:path");
3246
+ var import_node_fs12 = require("node:fs");
3247
+ var import_node_path13 = require("node:path");
2066
3248
  function matchGlob(pattern, id) {
2067
3249
  const patParts = pattern.split("/");
2068
3250
  const idParts = id.split("/");
@@ -2095,18 +3277,18 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
2095
3277
  const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
2096
3278
  const searchDirs = [
2097
3279
  rootDir,
2098
- (0, import_node_path10.join)(rootDir, "src"),
2099
- (0, import_node_path10.join)(rootDir, "app"),
2100
- (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")
2101
3283
  ];
2102
3284
  for (const base of searchDirs) {
2103
3285
  for (const convention of conventionDirs) {
2104
- const dir = (0, import_node_path10.join)(base, convention);
2105
- 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;
2106
3288
  try {
2107
- const stat = (0, import_node_fs9.statSync)(dir);
3289
+ const stat = (0, import_node_fs12.statSync)(dir);
2108
3290
  if (!stat.isDirectory()) continue;
2109
- 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);
2110
3292
  if (entries.length > 0) {
2111
3293
  const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
2112
3294
  result.set(relPath, entries);
@@ -2400,7 +3582,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
2400
3582
  for (const entry of config.taggers?.custom ?? []) {
2401
3583
  if (disabled.has(entry.id)) continue;
2402
3584
  try {
2403
- const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
3585
+ const absPath = (0, import_node_path14.resolve)(rootDir, entry.path);
2404
3586
  const mod = require(absPath);
2405
3587
  const tagger = "default" in mod ? mod.default : mod;
2406
3588
  const override = config.taggers?.trackUntagged?.[tagger.id];
@@ -2423,24 +3605,24 @@ function createTaggerRegistry(config, rootDir) {
2423
3605
  }
2424
3606
 
2425
3607
  // src/server/graph/core/tag-store.ts
2426
- var import_node_fs10 = require("node:fs");
2427
- var import_node_path12 = require("node:path");
3608
+ var import_node_fs13 = require("node:fs");
3609
+ var import_node_path15 = require("node:path");
2428
3610
  var TAGS_FILENAME = "tags.json";
2429
3611
  var GRAPHS_DIR = ".launchsecure/graphs";
2430
3612
  var tagCache = /* @__PURE__ */ new Map();
2431
3613
  function tagsFilePath(rootDir) {
2432
- return (0, import_node_path12.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
3614
+ return (0, import_node_path15.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
2433
3615
  }
2434
3616
  function readTagStore(rootDir) {
2435
3617
  const filePath = tagsFilePath(rootDir);
2436
- if (!(0, import_node_fs10.existsSync)(filePath)) return {};
2437
- 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);
2438
3620
  const cached = tagCache.get(filePath);
2439
3621
  if (cached && cached.mtimeMs === stat.mtimeMs) {
2440
3622
  return cached.store;
2441
3623
  }
2442
3624
  try {
2443
- const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
3625
+ const content = (0, import_node_fs13.readFileSync)(filePath, "utf-8");
2444
3626
  const store = JSON.parse(content);
2445
3627
  tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
2446
3628
  return store;
@@ -2450,15 +3632,15 @@ function readTagStore(rootDir) {
2450
3632
  }
2451
3633
  function writeTagStore(rootDir, store) {
2452
3634
  const filePath = tagsFilePath(rootDir);
2453
- const dir = (0, import_node_path12.dirname)(filePath);
2454
- (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 });
2455
3637
  const cleaned = {};
2456
3638
  for (const [nodeId, tags] of Object.entries(store)) {
2457
3639
  if (Object.keys(tags).length > 0) {
2458
3640
  cleaned[nodeId] = tags;
2459
3641
  }
2460
3642
  }
2461
- (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");
2462
3644
  tagCache.delete(filePath);
2463
3645
  }
2464
3646
  function setTag(rootDir, nodeId, key, value) {
@@ -2478,22 +3660,27 @@ function removeTag(rootDir, nodeId, key) {
2478
3660
  }
2479
3661
 
2480
3662
  // src/server/graph/index.ts
3663
+ init_ts_extractor();
2481
3664
  var GRAPHS_DIR2 = ".launchsecure/graphs";
2482
- 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
+ }
2483
3670
  var graphCache = /* @__PURE__ */ new Map();
2484
3671
  var taggedCache = /* @__PURE__ */ new Map();
2485
3672
  function graphsDir(rootDir) {
2486
- return (0, import_node_path13.join)(rootDir, GRAPHS_DIR2);
3673
+ return (0, import_node_path16.join)(rootDir, GRAPHS_DIR2);
2487
3674
  }
2488
3675
  function graphFilePath(rootDir, layer) {
2489
- return (0, import_node_path13.join)(graphsDir(rootDir), `${layer}.json`);
3676
+ return (0, import_node_path16.join)(graphsDir(rootDir), `${layer}.json`);
2490
3677
  }
2491
3678
  function tagsFilePath2(rootDir) {
2492
- return (0, import_node_path13.join)(graphsDir(rootDir), "tags.json");
3679
+ return (0, import_node_path16.join)(graphsDir(rootDir), "tags.json");
2493
3680
  }
2494
3681
  function getMtimeMs(filePath) {
2495
- if (!(0, import_node_fs11.existsSync)(filePath)) return 0;
2496
- 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;
2497
3684
  }
2498
3685
  function invalidateCache(filePath) {
2499
3686
  graphCache.delete(filePath);
@@ -2532,20 +3719,20 @@ function applyTags(graph, layer, rootDir) {
2532
3719
  }
2533
3720
  function readGraphRaw(rootDir, layer) {
2534
3721
  const filePath = graphFilePath(rootDir, layer);
2535
- if (!(0, import_node_fs11.existsSync)(filePath)) return null;
2536
- 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);
2537
3724
  const cached = graphCache.get(filePath);
2538
3725
  if (cached && cached.mtimeMs === stat.mtimeMs) {
2539
3726
  return cached.graph;
2540
3727
  }
2541
- const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
3728
+ const content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
2542
3729
  const graph = JSON.parse(content);
2543
3730
  graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
2544
3731
  return graph;
2545
3732
  }
2546
3733
  function readGraph(rootDir, layer) {
2547
3734
  const rawFilePath = graphFilePath(rootDir, layer);
2548
- if (!(0, import_node_fs11.existsSync)(rawFilePath)) return null;
3735
+ if (!(0, import_node_fs14.existsSync)(rawFilePath)) return null;
2549
3736
  const rawMtime = getMtimeMs(rawFilePath);
2550
3737
  const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
2551
3738
  const cacheKey = `${rootDir}:${layer}`;
@@ -2561,7 +3748,7 @@ function readGraph(rootDir, layer) {
2561
3748
  }
2562
3749
  function readAllGraphs(rootDir) {
2563
3750
  const result = {};
2564
- for (const layer of LAYERS) {
3751
+ for (const layer of getAvailableLayers(rootDir)) {
2565
3752
  const graph = readGraph(rootDir, layer);
2566
3753
  if (graph) result[layer] = graph;
2567
3754
  }
@@ -2575,11 +3762,11 @@ async function generateGraph(rootDir, layer) {
2575
3762
  mutationMethods: config.parsers?.patterns?.mutationMethods
2576
3763
  });
2577
3764
  const dir = graphsDir(rootDir);
2578
- (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
3765
+ (0, import_node_fs14.mkdirSync)(dir, { recursive: true });
2579
3766
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
2580
3767
  for (const result of results) {
2581
3768
  const filePath = graphFilePath(rootDir, result.layer);
2582
- (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");
2583
3770
  invalidateCache(filePath);
2584
3771
  invalidateTaggedCache(rootDir, result.layer);
2585
3772
  }
@@ -2588,28 +3775,28 @@ async function generateGraph(rootDir, layer) {
2588
3775
 
2589
3776
  // src/server/lockfile.ts
2590
3777
  var import_node_child_process = require("node:child_process");
2591
- var import_node_fs12 = require("node:fs");
3778
+ var import_node_fs15 = require("node:fs");
2592
3779
  var import_node_os = require("node:os");
2593
- var import_node_path14 = require("node:path");
3780
+ var import_node_path17 = require("node:path");
2594
3781
  function lockDir(projectRoot) {
2595
3782
  if (projectRoot) {
2596
- return (0, import_node_path14.join)(projectRoot, ".launchsecure");
3783
+ return (0, import_node_path17.join)(projectRoot, ".launchsecure");
2597
3784
  }
2598
- 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");
2599
3786
  }
2600
3787
  function lockPath(projectRoot) {
2601
- return (0, import_node_path14.join)(lockDir(projectRoot), "launch-chart.lock");
3788
+ return (0, import_node_path17.join)(lockDir(projectRoot), "launch-chart.lock");
2602
3789
  }
2603
3790
  var _activeProjectRoot;
2604
3791
  function readLock(projectRoot) {
2605
3792
  const root = projectRoot ?? _activeProjectRoot;
2606
3793
  const p = lockPath(root);
2607
- if (!(0, import_node_fs12.existsSync)(p)) {
3794
+ if (!(0, import_node_fs15.existsSync)(p)) {
2608
3795
  if (root) {
2609
3796
  const globalP = lockPath();
2610
- if ((0, import_node_fs12.existsSync)(globalP)) {
3797
+ if ((0, import_node_fs15.existsSync)(globalP)) {
2611
3798
  try {
2612
- 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"));
2613
3800
  if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
2614
3801
  return data;
2615
3802
  }
@@ -2620,7 +3807,7 @@ function readLock(projectRoot) {
2620
3807
  return null;
2621
3808
  }
2622
3809
  try {
2623
- 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"));
2624
3811
  if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
2625
3812
  return data;
2626
3813
  } catch {
@@ -2657,7 +3844,7 @@ function getLiveLock(projectRoot) {
2657
3844
  const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
2658
3845
  if (!live) {
2659
3846
  try {
2660
- (0, import_node_fs12.unlinkSync)(lockPath(root));
3847
+ (0, import_node_fs15.unlinkSync)(lockPath(root));
2661
3848
  } catch {
2662
3849
  }
2663
3850
  return null;
@@ -2666,20 +3853,299 @@ function getLiveLock(projectRoot) {
2666
3853
  }
2667
3854
  function writeLock(data, projectRoot) {
2668
3855
  const root = projectRoot ?? _activeProjectRoot;
2669
- (0, import_node_fs12.mkdirSync)(lockDir(root), { recursive: true });
2670
- (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");
2671
3858
  if (root) _activeProjectRoot = root;
2672
3859
  }
2673
3860
  function clearLock(projectRoot) {
2674
3861
  const root = projectRoot ?? _activeProjectRoot;
2675
3862
  try {
2676
- (0, import_node_fs12.unlinkSync)(lockPath(root));
3863
+ (0, import_node_fs15.unlinkSync)(lockPath(root));
2677
3864
  } catch {
2678
3865
  }
2679
3866
  }
2680
3867
 
2681
3868
  // src/server/chart-serve.ts
2682
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
2683
4149
  var MAX_PORT_SCAN = 3;
2684
4150
  function randomPort() {
2685
4151
  return 49152 + Math.floor(Math.random() * (65535 - 49152));
@@ -2698,31 +4164,39 @@ var MIME_TYPES = {
2698
4164
  function findProjectRoot(startDir) {
2699
4165
  let dir = startDir;
2700
4166
  for (let i = 0; i < 8; i++) {
2701
- const graphsDir2 = import_node_path15.default.join(dir, ".launchsecure", "graphs");
2702
- 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;
2703
- 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);
2704
4170
  if (parent === dir) break;
2705
4171
  dir = parent;
2706
4172
  }
2707
4173
  dir = startDir;
2708
4174
  for (let i = 0; i < 8; i++) {
2709
- if (import_node_fs13.default.existsSync(import_node_path15.default.join(dir, ".git"))) return dir;
2710
- 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);
2711
4177
  if (parent === dir) break;
2712
4178
  dir = parent;
2713
4179
  }
2714
4180
  return startDir;
2715
4181
  }
2716
- async function buildMergedGraph(projectRoot) {
2717
- let graphs = readAllGraphs(projectRoot);
2718
- if (!graphs.ui && !graphs.api && !graphs.db) {
2719
- await generateGraph(projectRoot);
2720
- 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);
2721
4196
  }
2722
4197
  const nodes = [];
2723
4198
  const rawLinks = [];
2724
- const LAYERS2 = ["ui", "api", "db"];
2725
- for (const layer of LAYERS2) {
4199
+ for (const layer of Object.keys(graphs)) {
2726
4200
  const g = graphs[layer];
2727
4201
  if (!g) continue;
2728
4202
  for (const n of g.nodes) {
@@ -2759,16 +4233,16 @@ async function buildMergedGraph(projectRoot) {
2759
4233
  };
2760
4234
  }
2761
4235
  function serveStatic(res, filePath) {
2762
- if (!import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) return false;
2763
- 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();
2764
4238
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
2765
4239
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
2766
- import_node_fs13.default.createReadStream(filePath).pipe(res);
4240
+ import_node_fs17.default.createReadStream(filePath).pipe(res);
2767
4241
  return true;
2768
4242
  }
2769
4243
  function serveIndex(res, clientDir) {
2770
- const indexPath = import_node_path15.default.join(clientDir, "index.html");
2771
- 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)) {
2772
4246
  res.writeHead(500, { "Content-Type": "text/plain" });
2773
4247
  res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
2774
4248
  return;
@@ -2820,19 +4294,40 @@ async function startChartServer(opts = {}) {
2820
4294
  }
2821
4295
  return { port: existing.port, url: existing.url };
2822
4296
  }
2823
- 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 ?? [];
2824
4300
  const server = import_node_http.default.createServer((req, res) => {
2825
4301
  try {
2826
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
+ }
2827
4322
  if (req.method === "GET" && url2.pathname === "/api/project-graph") {
2828
4323
  const regenerate = url2.searchParams.get("regenerate") === "1";
2829
4324
  (async () => {
2830
- if (regenerate) await generateGraph(projectRoot);
2831
- const merged = await buildMergedGraph(projectRoot);
4325
+ if (regenerate) await generateGraph(reqRoot);
4326
+ const merged = await buildMergedGraph(reqRoot);
2832
4327
  res.writeHead(200, { "Content-Type": "application/json" });
2833
4328
  res.end(JSON.stringify({
2834
4329
  ...merged,
2835
- debug: { cwd, projectRoot }
4330
+ debug: { cwd, projectRoot: reqRoot }
2836
4331
  }));
2837
4332
  })().catch((e) => {
2838
4333
  res.writeHead(500);
@@ -2841,15 +4336,15 @@ async function startChartServer(opts = {}) {
2841
4336
  return;
2842
4337
  }
2843
4338
  if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
2844
- const graphs = readAllGraphs(projectRoot);
4339
+ const graphs = readAllGraphs(reqRoot);
2845
4340
  res.writeHead(200, { "Content-Type": "application/json" });
2846
4341
  res.end(JSON.stringify({ ui: graphs.ui ?? null, api: graphs.api ?? null, db: graphs.db ?? null }));
2847
4342
  return;
2848
4343
  }
2849
4344
  if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
2850
4345
  (async () => {
2851
- await generateGraph(projectRoot);
2852
- const graphs = readAllGraphs(projectRoot);
4346
+ await generateGraph(reqRoot);
4347
+ const graphs = readAllGraphs(reqRoot);
2853
4348
  res.writeHead(200, { "Content-Type": "application/json" });
2854
4349
  res.end(JSON.stringify({
2855
4350
  ok: true,
@@ -2865,41 +4360,43 @@ async function startChartServer(opts = {}) {
2865
4360
  }
2866
4361
  if (req.method === "GET" && url2.pathname === "/api/file-content") {
2867
4362
  const relPath = url2.searchParams.get("path");
2868
- if (!relPath || relPath.includes("..") || import_node_path15.default.isAbsolute(relPath)) {
4363
+ if (!relPath || relPath.includes("..") || import_node_path19.default.isAbsolute(relPath)) {
2869
4364
  res.writeHead(400, { "Content-Type": "application/json" });
2870
4365
  res.end(JSON.stringify({ error: "Invalid path" }));
2871
4366
  return;
2872
4367
  }
2873
- const filePath = import_node_path15.default.join(projectRoot, relPath);
2874
- 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()) {
2875
4370
  res.writeHead(404, { "Content-Type": "application/json" });
2876
4371
  res.end(JSON.stringify({ error: "File not found" }));
2877
4372
  return;
2878
4373
  }
2879
- const ext = import_node_path15.default.extname(filePath).toLowerCase();
4374
+ const ext = import_node_path19.default.extname(filePath).toLowerCase();
2880
4375
  const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
2881
- const content = import_node_fs13.default.readFileSync(filePath, "utf-8");
4376
+ const content = import_node_fs17.default.readFileSync(filePath, "utf-8");
2882
4377
  res.writeHead(200, { "Content-Type": "application/json" });
2883
4378
  res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
2884
4379
  return;
2885
4380
  }
2886
4381
  if (req.method === "GET" && url2.pathname === "/api/health") {
2887
4382
  res.writeHead(200, { "Content-Type": "application/json" });
2888
- res.end(JSON.stringify({ ok: true, projectRoot }));
4383
+ res.end(JSON.stringify({ ok: true, projectRoot: reqRoot }));
2889
4384
  return;
2890
4385
  }
2891
4386
  if (req.method === "GET" && url2.pathname === "/api/parser-config") {
2892
- const config2 = loadConfig(projectRoot);
2893
- const detection = [
2894
- { id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
2895
- { id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
2896
- { id: "prisma-schema", layer: "db", label: "Prisma Schema", detected: prismaSchemaParser.detect(projectRoot) }
2897
- ];
2898
- const crosslayerParsers = [
2899
- { id: "fetch-resolver", label: "Fetch / api.method() calls" },
2900
- { id: "api-annotations", label: "@api annotations" },
2901
- { id: "url-literal-scanner", label: "/api/... URL literals" }
2902
- ];
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 }));
2903
4400
  res.writeHead(200, { "Content-Type": "application/json" });
2904
4401
  res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
2905
4402
  return;
@@ -2912,8 +4409,8 @@ async function startChartServer(opts = {}) {
2912
4409
  req.on("end", () => {
2913
4410
  try {
2914
4411
  const newConfig = JSON.parse(body);
2915
- const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
2916
- 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");
2917
4414
  res.writeHead(200, { "Content-Type": "application/json" });
2918
4415
  res.end(JSON.stringify({ ok: true }));
2919
4416
  } catch (err) {
@@ -2924,7 +4421,7 @@ async function startChartServer(opts = {}) {
2924
4421
  return;
2925
4422
  }
2926
4423
  if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
2927
- const config2 = loadConfig(projectRoot);
4424
+ const config2 = loadConfig(reqRoot);
2928
4425
  const builtinTaggers = [
2929
4426
  { id: "module", tagKey: "module", trackUntagged: config2.taggers?.trackUntagged?.module ?? true },
2930
4427
  { id: "screen", tagKey: "screen", trackUntagged: config2.taggers?.trackUntagged?.screen ?? true }
@@ -2944,10 +4441,10 @@ async function startChartServer(opts = {}) {
2944
4441
  req.on("end", () => {
2945
4442
  try {
2946
4443
  const taggerConfig = JSON.parse(body);
2947
- const config2 = loadConfig(projectRoot);
4444
+ const config2 = loadConfig(reqRoot);
2948
4445
  config2.taggers = taggerConfig;
2949
- const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
2950
- 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");
2951
4448
  res.writeHead(200, { "Content-Type": "application/json" });
2952
4449
  res.end(JSON.stringify({ ok: true }));
2953
4450
  } catch (err) {
@@ -2958,7 +4455,7 @@ async function startChartServer(opts = {}) {
2958
4455
  return;
2959
4456
  }
2960
4457
  if (req.method === "GET" && url2.pathname === "/api/tags") {
2961
- const store = readTagStore(projectRoot);
4458
+ const store = readTagStore(reqRoot);
2962
4459
  res.writeHead(200, { "Content-Type": "application/json" });
2963
4460
  res.end(JSON.stringify(store));
2964
4461
  return;
@@ -2976,7 +4473,7 @@ async function startChartServer(opts = {}) {
2976
4473
  res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
2977
4474
  return;
2978
4475
  }
2979
- setTag(projectRoot, nodeId, key, value);
4476
+ setTag(reqRoot, nodeId, key, value);
2980
4477
  res.writeHead(200, { "Content-Type": "application/json" });
2981
4478
  res.end(JSON.stringify({ ok: true }));
2982
4479
  } catch (err) {
@@ -2999,7 +4496,76 @@ async function startChartServer(opts = {}) {
2999
4496
  res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
3000
4497
  return;
3001
4498
  }
3002
- 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);
3003
4569
  res.writeHead(200, { "Content-Type": "application/json" });
3004
4570
  res.end(JSON.stringify({ ok: true }));
3005
4571
  } catch (err) {
@@ -3010,7 +4576,7 @@ async function startChartServer(opts = {}) {
3010
4576
  return;
3011
4577
  }
3012
4578
  if (url2.pathname !== "/") {
3013
- const staticPath = import_node_path15.default.join(clientDir, url2.pathname);
4579
+ const staticPath = import_node_path19.default.join(clientDir, url2.pathname);
3014
4580
  if (serveStatic(res, staticPath)) return;
3015
4581
  }
3016
4582
  serveIndex(res, clientDir);
@@ -3048,6 +4614,10 @@ async function startChartServer(opts = {}) {
3048
4614
  `);
3049
4615
  process.stderr.write(`[launch-chart] project root: ${projectRoot}
3050
4616
  `);
4617
+ if (projects.length > 0) {
4618
+ process.stderr.write(`[launch-chart] multi-project mode: ${projects.length} projects
4619
+ `);
4620
+ }
3051
4621
  }
3052
4622
  return { port, url };
3053
4623
  }