@launchsecure/launch-kit 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -155,7 +155,52 @@ var init_config = __esm({
155
155
  }
156
156
  });
157
157
 
158
+ // src/server/graph/core/resolve-paths.ts
159
+ function detectDbDir(rootDir, config) {
160
+ if (config.paths?.dbDir) return (0, import_node_path3.join)(rootDir, config.paths.dbDir);
161
+ const prismaDir = (0, import_node_path3.join)(rootDir, "prisma");
162
+ if ((0, import_node_fs3.existsSync)(prismaDir)) return prismaDir;
163
+ return null;
164
+ }
165
+ function resolveProjectPaths(rootDir, config) {
166
+ const dbDir = detectDbDir(rootDir, config);
167
+ if (config.paths?.appDir) {
168
+ const appDir = (0, import_node_path3.join)(rootDir, config.paths.appDir);
169
+ const srcDir = config.paths.srcDir ? (0, import_node_path3.join)(rootDir, config.paths.srcDir) : (0, import_node_path3.dirname)(appDir);
170
+ return { srcDir, appDir, apiDir: (0, import_node_path3.join)(appDir, "api"), dbDir };
171
+ }
172
+ const srcApp = (0, import_node_path3.join)(rootDir, "src", "app");
173
+ if ((0, import_node_fs3.existsSync)(srcApp)) {
174
+ return { srcDir: (0, import_node_path3.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path3.join)(srcApp, "api"), dbDir };
175
+ }
176
+ const rootApp = (0, import_node_path3.join)(rootDir, "app");
177
+ if ((0, import_node_fs3.existsSync)(rootApp)) {
178
+ return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path3.join)(rootApp, "api"), dbDir };
179
+ }
180
+ return null;
181
+ }
182
+ var import_node_fs3, import_node_path3;
183
+ var init_resolve_paths = __esm({
184
+ "src/server/graph/core/resolve-paths.ts"() {
185
+ "use strict";
186
+ import_node_fs3 = require("node:fs");
187
+ import_node_path3 = require("node:path");
188
+ }
189
+ });
190
+
158
191
  // src/server/graph/core/ts-extractor.ts
192
+ var ts_extractor_exports = {};
193
+ __export(ts_extractor_exports, {
194
+ classifyFile: () => classifyFile,
195
+ createQuery: () => createQuery,
196
+ extractAuthWrappersTS: () => extractAuthWrappersTS,
197
+ extractDbCallsTS: () => extractDbCallsTS,
198
+ extractDeep: () => extractDeep,
199
+ initTreeSitter: () => initTreeSitter,
200
+ parseCodeTS: () => parseCodeTS,
201
+ parseFileTS: () => parseFileTS,
202
+ setExtractorConfig: () => setExtractorConfig
203
+ });
159
204
  async function initTreeSitter() {
160
205
  if (initialized) return;
161
206
  if (initPromise) return initPromise;
@@ -179,17 +224,25 @@ function getQuery(name) {
179
224
  ensureInit();
180
225
  const cached = queryCache.get(name);
181
226
  if (cached) return cached;
182
- const scmPath = (0, import_node_path3.join)(queriesDir, `${name}.scm`);
183
- const scm = (0, import_node_fs3.readFileSync)(scmPath, "utf-8");
227
+ const scmPath = (0, import_node_path4.join)(queriesDir, `${name}.scm`);
228
+ const scm = (0, import_node_fs4.readFileSync)(scmPath, "utf-8");
184
229
  const query = tsxLanguage.query(scm);
185
230
  queryCache.set(name, query);
186
231
  return query;
187
232
  }
188
233
  function parseSource(absPath) {
189
234
  ensureInit();
190
- const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
235
+ const content = (0, import_node_fs4.readFileSync)(absPath, "utf-8");
191
236
  return parserInstance.parse(content);
192
237
  }
238
+ function parseCodeTS(code) {
239
+ ensureInit();
240
+ return parserInstance.parse(code);
241
+ }
242
+ function createQuery(pattern) {
243
+ ensureInit();
244
+ return tsxLanguage.query(pattern);
245
+ }
193
246
  function setExtractorConfig(config) {
194
247
  extraDbIdentifiers = config.dbIdentifiers ?? [];
195
248
  extraMutationMethods = config.mutationMethods ?? [];
@@ -644,17 +697,17 @@ function extractDeep(absPath) {
644
697
  }
645
698
  return { elements, stateVars, conditions, variables, responses, params };
646
699
  }
647
- var import_node_fs3, import_node_path3, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
700
+ var import_node_fs4, import_node_path4, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
648
701
  var init_ts_extractor = __esm({
649
702
  "src/server/graph/core/ts-extractor.ts"() {
650
703
  "use strict";
651
- import_node_fs3 = require("node:fs");
652
- import_node_path3 = require("node:path");
704
+ import_node_fs4 = require("node:fs");
705
+ import_node_path4 = require("node:path");
653
706
  initialized = false;
654
707
  queriesDir = (() => {
655
- const srcPath = (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "..", "queries");
708
+ const srcPath = (0, import_node_path4.join)((0, import_node_path4.dirname)(__filename), "..", "queries");
656
709
  if (require("fs").existsSync(srcPath)) return srcPath;
657
- return (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "graph", "queries");
710
+ return (0, import_node_path4.join)((0, import_node_path4.dirname)(__filename), "graph", "queries");
658
711
  })();
659
712
  queryCache = /* @__PURE__ */ new Map();
660
713
  PRISMA_MUTATION_METHODS_BUILTIN = [
@@ -674,15 +727,15 @@ var init_ts_extractor = __esm({
674
727
  }
675
728
  });
676
729
 
677
- // src/server/graph/parsers/ui/react-nextjs.ts
730
+ // src/server/graph/parsers/ts/typescript-project.ts
678
731
  function walk(dir, exts) {
679
732
  const results = [];
680
- if (!(0, import_node_fs4.existsSync)(dir)) return results;
681
- for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
682
- const full = (0, import_node_path4.join)(dir, entry.name);
733
+ if (!(0, import_node_fs5.existsSync)(dir)) return results;
734
+ for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
735
+ const full = (0, import_node_path5.join)(dir, entry.name);
683
736
  if (entry.isDirectory()) {
684
737
  results.push(...walk(full, exts));
685
- } else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
738
+ } else if (exts.includes((0, import_node_path5.extname)(entry.name))) {
686
739
  results.push(full);
687
740
  }
688
741
  }
@@ -690,33 +743,33 @@ function walk(dir, exts) {
690
743
  }
691
744
  function walkWithIgnore(dir, exts, ignoreDirs) {
692
745
  const results = [];
693
- if (!(0, import_node_fs4.existsSync)(dir)) return results;
694
- for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
746
+ if (!(0, import_node_fs5.existsSync)(dir)) return results;
747
+ for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
695
748
  if (entry.isDirectory()) {
696
749
  if (ignoreDirs.has(entry.name)) continue;
697
- results.push(...walkWithIgnore((0, import_node_path4.join)(dir, entry.name), exts, ignoreDirs));
698
- } else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
699
- results.push((0, import_node_path4.join)(dir, entry.name));
750
+ results.push(...walkWithIgnore((0, import_node_path5.join)(dir, entry.name), exts, ignoreDirs));
751
+ } else if (exts.includes((0, import_node_path5.extname)(entry.name))) {
752
+ results.push((0, import_node_path5.join)(dir, entry.name));
700
753
  }
701
754
  }
702
755
  return results;
703
756
  }
704
757
  function toNodeId(srcDir, absPath) {
705
- return (0, import_node_path4.relative)(srcDir, absPath).replace(/\\/g, "/");
758
+ return (0, import_node_path5.relative)(srcDir, absPath).replace(/\\/g, "/");
706
759
  }
707
760
  function resolveImport(srcDir, specifier) {
708
761
  if (!specifier.startsWith("@/")) return null;
709
762
  const rel = specifier.slice(2);
710
- const base = (0, import_node_path4.join)(srcDir, rel);
711
- 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")]) {
712
- if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
763
+ const base = (0, import_node_path5.join)(srcDir, rel);
764
+ for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path5.join)(base, "index.ts"), (0, import_node_path5.join)(base, "index.tsx")]) {
765
+ if ((0, import_node_fs5.existsSync)(c) && (0, import_node_fs5.statSync)(c).isFile()) return c;
713
766
  }
714
767
  return null;
715
768
  }
716
769
  function resolveRelativeImport(fromFile, specifier) {
717
- const base = (0, import_node_path4.join)((0, import_node_path4.dirname)(fromFile), specifier);
718
- 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")]) {
719
- if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
770
+ const base = (0, import_node_path5.join)((0, import_node_path5.dirname)(fromFile), specifier);
771
+ for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path5.join)(base, "index.ts"), (0, import_node_path5.join)(base, "index.tsx")]) {
772
+ if ((0, import_node_fs5.existsSync)(c) && (0, import_node_fs5.statSync)(c).isFile()) return c;
720
773
  }
721
774
  return null;
722
775
  }
@@ -737,7 +790,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
737
790
  const resolved = resolveRelativeImport(barrelAbsPath, re.from);
738
791
  if (!resolved) continue;
739
792
  if (re.isWildcard) {
740
- const targetBn = (0, import_node_path4.basename)(resolved);
793
+ const targetBn = (0, import_node_path5.basename)(resolved);
741
794
  const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
742
795
  if (targetIsBarrel) {
743
796
  const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
@@ -764,12 +817,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
764
817
  const barrels = /* @__PURE__ */ new Map();
765
818
  const memo = /* @__PURE__ */ new Map();
766
819
  for (const [absPath, parsed] of parsedByPath) {
767
- const bn = (0, import_node_path4.basename)(absPath);
820
+ const bn = (0, import_node_path5.basename)(absPath);
768
821
  if (bn !== "index.ts" && bn !== "index.tsx") continue;
769
822
  if (parsed.reExports.length === 0) continue;
770
823
  const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
771
824
  if (map.size > 0) {
772
- const barrelId = (0, import_node_path4.relative)(srcDir, (0, import_node_path4.dirname)(absPath)).replace(/\\/g, "/");
825
+ const barrelId = (0, import_node_path5.relative)(srcDir, (0, import_node_path5.dirname)(absPath)).replace(/\\/g, "/");
773
826
  barrels.set(barrelId, map);
774
827
  }
775
828
  }
@@ -790,7 +843,18 @@ function extractRoute(id) {
790
843
  return route || "/";
791
844
  }
792
845
  function nameFromFilename(absPath) {
793
- return (0, import_node_path4.basename)(absPath, (0, import_node_path4.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
846
+ return (0, import_node_path5.basename)(absPath, (0, import_node_path5.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
847
+ }
848
+ function filePathToApiRoute(apiDir, absPath) {
849
+ let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
850
+ route = route.replace(/\[([^\]]+)\]/g, ":$1");
851
+ route = route.replace(/\/+/g, "/");
852
+ if (route === "/") return "/api";
853
+ return "/api" + route;
854
+ }
855
+ function camelToPascal(s) {
856
+ if (!s) return s;
857
+ return s.charAt(0).toUpperCase() + s.slice(1);
794
858
  }
795
859
  function resolveTemplateLiteralRoute(template, routeToNodeId) {
796
860
  const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -871,7 +935,7 @@ function matchRouteToPage(route, routeToNodeId) {
871
935
  if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
872
936
  return null;
873
937
  }
874
- function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
938
+ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps, routeToNodeId) {
875
939
  const edges = [];
876
940
  const flagged = [];
877
941
  const seen = /* @__PURE__ */ new Set();
@@ -883,7 +947,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
883
947
  if (label) edge.label = label;
884
948
  edges.push(edge);
885
949
  }
886
- function edgeTypeFor(_targetId, isTypeOnlyImport, importedNames) {
950
+ function edgeTypeFor(isTypeOnlyImport, importedNames) {
887
951
  if (isTypeOnlyImport) return "imports";
888
952
  const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
889
953
  if (anyRendered) return "renders";
@@ -908,14 +972,14 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
908
972
  }
909
973
  for (const [targetId, targetNames] of byTarget) {
910
974
  const allType = isTypeOnly || targetNames.every((n) => typeNames.has(n));
911
- addEdge(targetId, edgeTypeFor(targetId, allType, targetNames));
975
+ addEdge(targetId, edgeTypeFor(allType, targetNames));
912
976
  }
913
977
  } else {
914
978
  const resolved = resolveImport(srcDir, specifier);
915
979
  if (resolved) {
916
980
  const targetId = toNodeId(srcDir, resolved);
917
981
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
918
- addEdge(targetId, edgeTypeFor(targetId, isTypeOnly, names));
982
+ addEdge(targetId, edgeTypeFor(isTypeOnly, names));
919
983
  }
920
984
  }
921
985
  }
@@ -924,7 +988,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
924
988
  if (resolved) {
925
989
  const targetId = toNodeId(srcDir, resolved);
926
990
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
927
- addEdge(targetId, edgeTypeFor(targetId, isTypeOnly, names));
991
+ addEdge(targetId, edgeTypeFor(isTypeOnly, names));
928
992
  }
929
993
  }
930
994
  }
@@ -966,74 +1030,113 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
966
1030
  }
967
1031
  return { edges, flagged };
968
1032
  }
1033
+ function hasNextConfig(rootDir) {
1034
+ return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "next.config.ts")) || (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "next.config.js")) || (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "next.config.mjs"));
1035
+ }
969
1036
  function detect(rootDir) {
970
- return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app")) && (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"));
1037
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
1038
+ return paths !== null && hasNextConfig(rootDir);
971
1039
  }
972
1040
  function generate(rootDir) {
973
- const srcDir = (0, import_node_path4.join)(rootDir, "src");
974
- const appFiles = walk((0, import_node_path4.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
975
- (f) => (0, import_node_path4.basename)(f) !== "route.ts" && (0, import_node_path4.basename)(f) !== "route.tsx"
976
- );
977
- const clientFiles = walk((0, import_node_path4.join)(srcDir, "client"), [".tsx", ".ts"]);
978
- const serverFiles = walk((0, import_node_path4.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
979
- (f) => (0, import_node_path4.basename)(f) !== "route.ts" && (0, import_node_path4.basename)(f) !== "route.tsx"
980
- );
981
- const libFiles = walk((0, import_node_path4.join)(srcDir, "lib"), [".ts", ".tsx"]);
982
- const configFiles = walk((0, import_node_path4.join)(srcDir, "config"), [".ts", ".tsx"]);
1041
+ const config = loadConfig(rootDir);
1042
+ const paths = resolveProjectPaths(rootDir, config);
1043
+ const srcDir = paths.srcDir;
1044
+ const apiDir = paths.apiDir;
1045
+ const appFiles = walk(paths.appDir, [".tsx", ".ts"]);
1046
+ const clientFiles = walk((0, import_node_path5.join)(srcDir, "client"), [".tsx", ".ts"]);
1047
+ const serverFiles = walk((0, import_node_path5.join)(srcDir, "server"), [".ts", ".tsx"]);
1048
+ const libFiles = walk((0, import_node_path5.join)(srcDir, "lib"), [".ts", ".tsx"]);
1049
+ const configFiles = walk((0, import_node_path5.join)(srcDir, "config"), [".ts", ".tsx"]);
983
1050
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
984
1051
  const parsedByPath = /* @__PURE__ */ new Map();
985
1052
  for (const absPath of allDiscovered) {
986
1053
  parsedByPath.set(absPath, parseFileTS(absPath));
987
1054
  }
988
1055
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
989
- const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
990
- const nodes = [];
1056
+ const uiNodes = [];
1057
+ const apiNodes = [];
991
1058
  const nodeIdSet = /* @__PURE__ */ new Set();
992
- const nodeTypeMap = /* @__PURE__ */ new Map();
993
1059
  const routeToNodeId = /* @__PURE__ */ new Map();
1060
+ const fileSet = allDiscovered.filter((f) => !(0, import_node_path5.basename)(f).startsWith("index."));
994
1061
  for (const absPath of fileSet) {
995
1062
  const id = toNodeId(srcDir, absPath);
996
1063
  const type = classifyType(absPath, id);
1064
+ if (type === "test" || type === "story") continue;
997
1065
  const parsed = parsedByPath.get(absPath);
998
1066
  const name = parsed.name || nameFromFilename(absPath);
999
- const route = extractRoute(id);
1000
- const deep = extractDeep(absPath);
1001
- nodes.push({
1002
- id,
1003
- type,
1004
- name,
1005
- route,
1006
- exports: parsed.exports,
1007
- elements: deep.elements,
1008
- stateVars: deep.stateVars,
1009
- conditions: deep.conditions,
1010
- variables: deep.variables
1011
- });
1067
+ const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
1012
1068
  nodeIdSet.add(id);
1013
- nodeTypeMap.set(id, type);
1014
- if (route) routeToNodeId.set(route, id);
1069
+ if (layer === "api") {
1070
+ const methods = [];
1071
+ for (const exp of parsed.exports) {
1072
+ if (HTTP_METHODS.has(exp)) methods.push(exp);
1073
+ }
1074
+ const dbCalls = extractDbCallsTS(absPath);
1075
+ const authWrappers = extractAuthWrappersTS(absPath);
1076
+ const deep = extractDeep(absPath);
1077
+ const routePath = (0, import_node_fs5.existsSync)(apiDir) ? filePathToApiRoute(apiDir, absPath) : `/api/${id.replace(/\/route\.tsx?$/, "")}`;
1078
+ const mutations = dbCalls.filter((c) => c.isMutation);
1079
+ const mutates = mutations.length > 0;
1080
+ const authStrategy = [...authWrappers];
1081
+ apiNodes.push({
1082
+ id,
1083
+ type: "endpoint",
1084
+ name: routePath,
1085
+ layer: "api",
1086
+ path: routePath,
1087
+ methods,
1088
+ handler: id,
1089
+ mutates,
1090
+ auth: authStrategy.length > 0 ? authStrategy : ["public"],
1091
+ db_models: [...new Set(dbCalls.map((c) => c.model))],
1092
+ db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
1093
+ conditions: deep.conditions,
1094
+ variables: deep.variables,
1095
+ responses: deep.responses,
1096
+ params: deep.params,
1097
+ _dbCalls: dbCalls
1098
+ // temp: used for cross-ref building below
1099
+ });
1100
+ } else {
1101
+ const route = extractRoute(id);
1102
+ const deep = extractDeep(absPath);
1103
+ uiNodes.push({
1104
+ id,
1105
+ type,
1106
+ name,
1107
+ layer: "ui",
1108
+ route,
1109
+ exports: parsed.exports,
1110
+ elements: deep.elements,
1111
+ stateVars: deep.stateVars,
1112
+ conditions: deep.conditions,
1113
+ variables: deep.variables
1114
+ });
1115
+ if (route) routeToNodeId.set(route, id);
1116
+ }
1015
1117
  }
1016
- const allEdges = [];
1017
- const allFlagged = [];
1118
+ const uiEdges = [];
1119
+ const uiFlagged = [];
1018
1120
  for (const absPath of fileSet) {
1019
- const sourceId = toNodeId(srcDir, absPath);
1121
+ const id = toNodeId(srcDir, absPath);
1122
+ if (!nodeIdSet.has(id)) continue;
1020
1123
  const parsed = parsedByPath.get(absPath);
1021
1124
  const { edges, flagged } = extractEdges(
1022
1125
  srcDir,
1023
1126
  absPath,
1024
- sourceId,
1127
+ id,
1025
1128
  parsed,
1026
1129
  nodeIdSet,
1027
- nodeTypeMap,
1028
1130
  barrelMaps,
1029
1131
  routeToNodeId
1030
1132
  );
1031
- allEdges.push(...edges);
1032
- allFlagged.push(...flagged);
1133
+ uiEdges.push(...edges);
1134
+ uiFlagged.push(...flagged);
1033
1135
  }
1034
1136
  const fetchCallEntries = [];
1035
1137
  for (const absPath of fileSet) {
1036
1138
  const sourceId = toNodeId(srcDir, absPath);
1139
+ if (!nodeIdSet.has(sourceId)) continue;
1037
1140
  const parsed = parsedByPath.get(absPath);
1038
1141
  if (parsed.fetchCalls.length === 0) continue;
1039
1142
  fetchCallEntries.push({
@@ -1048,7 +1151,7 @@ function generate(rootDir) {
1048
1151
  });
1049
1152
  }
1050
1153
  const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
1051
- const IGNORE_DIRS = /* @__PURE__ */ new Set([
1154
+ const IGNORE_DIRS2 = /* @__PURE__ */ new Set([
1052
1155
  "node_modules",
1053
1156
  ".next",
1054
1157
  "dist",
@@ -1061,7 +1164,7 @@ function generate(rootDir) {
1061
1164
  "out",
1062
1165
  ".vercel"
1063
1166
  ]);
1064
- const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS);
1167
+ const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS2);
1065
1168
  for (const absPath of externalCandidates) {
1066
1169
  const normalized = absPath.replace(/\\/g, "/");
1067
1170
  if (externalScanned.has(normalized)) continue;
@@ -1071,11 +1174,11 @@ function generate(rootDir) {
1071
1174
  } catch {
1072
1175
  continue;
1073
1176
  }
1074
- const externalId = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
1177
+ const externalId = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
1075
1178
  const edgesFromThis = [];
1076
1179
  const seen = /* @__PURE__ */ new Set();
1077
1180
  for (const imp of parsed.imports) {
1078
- const { specifier, isTypeOnly, names } = imp;
1181
+ const { specifier, names } = imp;
1079
1182
  let resolved = null;
1080
1183
  if (specifier.startsWith("@/")) {
1081
1184
  const relToSrc = specifier.slice(2);
@@ -1101,25 +1204,52 @@ function generate(rootDir) {
1101
1204
  const targetId = toNodeId(srcDir, resolved);
1102
1205
  if (!nodeIdSet.has(targetId)) continue;
1103
1206
  if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
1104
- const key = `${externalId}\u2192${targetId}\u2192${isTypeOnly ? "type" : "value"}`;
1207
+ const key = `${externalId}\u2192${targetId}`;
1105
1208
  if (seen.has(key)) continue;
1106
1209
  seen.add(key);
1107
1210
  edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
1108
1211
  }
1109
1212
  if (edgesFromThis.length === 0) continue;
1110
- nodes.push({
1213
+ uiNodes.push({
1111
1214
  id: externalId,
1112
1215
  type: "external",
1113
1216
  name: parsed.name || nameFromFilename(absPath),
1217
+ layer: "ui",
1114
1218
  route: null,
1115
1219
  exports: parsed.exports
1116
1220
  });
1117
1221
  nodeIdSet.add(externalId);
1118
- nodeTypeMap.set(externalId, "external");
1119
- allEdges.push(...edgesFromThis);
1222
+ uiEdges.push(...edgesFromThis);
1223
+ }
1224
+ const apiCrossRefs = [];
1225
+ for (const node of apiNodes) {
1226
+ const dbCalls = node._dbCalls;
1227
+ if (!dbCalls) continue;
1228
+ const seenModels = /* @__PURE__ */ new Set();
1229
+ for (const call of dbCalls) {
1230
+ if (seenModels.has(call.model)) continue;
1231
+ seenModels.add(call.model);
1232
+ apiCrossRefs.push({
1233
+ source: node.id,
1234
+ target: camelToPascal(call.model),
1235
+ type: call.isMutation ? "mutates" : "reads",
1236
+ layer: "db"
1237
+ });
1238
+ }
1239
+ delete node._dbCalls;
1240
+ }
1241
+ const apiNodeIds = new Set(apiNodes.map((n) => n.id));
1242
+ const apiEdges = [];
1243
+ const uiOnlyEdges = [];
1244
+ for (const edge of uiEdges) {
1245
+ if (apiNodeIds.has(edge.source)) {
1246
+ apiEdges.push(edge);
1247
+ } else {
1248
+ uiOnlyEdges.push(edge);
1249
+ }
1120
1250
  }
1121
1251
  const flaggedSet = /* @__PURE__ */ new Set();
1122
- const dedupedFlagged = allFlagged.filter((f) => {
1252
+ const dedupedFlagged = uiFlagged.filter((f) => {
1123
1253
  const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
1124
1254
  if (flaggedSet.has(key)) return false;
1125
1255
  flaggedSet.add(key);
@@ -1136,10 +1266,12 @@ function generate(rootDir) {
1136
1266
  hook: 7,
1137
1267
  lib: 8
1138
1268
  };
1139
- nodes.sort((a, b) => (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99) || a.id.localeCompare(b.id));
1140
- allEdges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1141
- const byType = (t) => nodes.filter((n) => n.type === t).length;
1142
- const stats = {
1269
+ uiNodes.sort((a, b) => (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99) || a.id.localeCompare(b.id));
1270
+ uiOnlyEdges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1271
+ apiNodes.sort((a, b) => (a.path ?? "").localeCompare(b.path ?? ""));
1272
+ apiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1273
+ const byType = (t) => uiNodes.filter((n) => n.type === t).length;
1274
+ const uiStats = {
1143
1275
  total_pages: byType("page"),
1144
1276
  total_layouts: byType("layout"),
1145
1277
  total_components: byType("component"),
@@ -1150,196 +1282,122 @@ function generate(rootDir) {
1150
1282
  total_utils: byType("util"),
1151
1283
  total_libs: byType("lib"),
1152
1284
  total_external: byType("external"),
1153
- total_edges: allEdges.length,
1285
+ total_edges: uiOnlyEdges.length,
1154
1286
  total_flagged: dedupedFlagged.length
1155
1287
  };
1156
- return {
1288
+ const stripLayer = (nodes) => nodes.map(({ layer: _, ...rest }) => rest);
1289
+ const result = /* @__PURE__ */ new Map();
1290
+ result.set("ui", {
1157
1291
  metadata: {
1158
1292
  generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1159
1293
  scope: "main-app-only",
1160
1294
  app_root: "src/",
1161
1295
  layer: "ui",
1162
- parser: "react-nextjs-ast",
1163
- ...stats,
1296
+ parser: "typescript-project",
1297
+ ...uiStats,
1164
1298
  notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
1165
1299
  },
1166
- nodes,
1167
- edges: allEdges,
1300
+ nodes: stripLayer(uiNodes),
1301
+ edges: uiOnlyEdges,
1168
1302
  cross_refs: [],
1169
1303
  contradictions: [],
1170
1304
  warnings: [],
1171
1305
  flagged_edges: dedupedFlagged,
1172
1306
  patterns: {
1173
- total_nodes: nodes.length,
1174
- by_type: stats,
1307
+ total_nodes: uiNodes.length,
1308
+ by_type: uiStats,
1175
1309
  by_edge_type: {
1176
- renders: allEdges.filter((e) => e.type === "renders").length,
1177
- imports: allEdges.filter((e) => e.type === "imports").length,
1178
- navigates: allEdges.filter((e) => e.type === "navigates").length
1310
+ renders: uiOnlyEdges.filter((e) => e.type === "renders").length,
1311
+ imports: uiOnlyEdges.filter((e) => e.type === "imports").length,
1312
+ navigates: uiOnlyEdges.filter((e) => e.type === "navigates").length
1179
1313
  },
1180
1314
  fetch_calls: fetchCallEntries
1181
1315
  }
1182
- };
1183
- }
1184
- var import_node_fs4, import_node_path4, reactNextjsParser;
1185
- var init_react_nextjs = __esm({
1186
- "src/server/graph/parsers/ui/react-nextjs.ts"() {
1187
- "use strict";
1188
- import_node_fs4 = require("node:fs");
1189
- import_node_path4 = require("node:path");
1190
- init_ts_extractor();
1191
- reactNextjsParser = {
1192
- id: "react-nextjs",
1193
- layer: "ui",
1194
- detect,
1195
- generate
1196
- };
1197
- }
1198
- });
1199
-
1200
- // src/server/graph/parsers/api/nextjs-routes.ts
1201
- function walk2(dir) {
1202
- const results = [];
1203
- if (!(0, import_node_fs5.existsSync)(dir)) return results;
1204
- for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
1205
- const full = (0, import_node_path5.join)(dir, entry.name);
1206
- if (entry.isDirectory()) {
1207
- results.push(...walk2(full));
1208
- } else if (entry.name === "route.ts" || entry.name === "route.tsx") {
1209
- results.push(full);
1210
- }
1211
- }
1212
- return results;
1213
- }
1214
- function filePathToRoute(apiDir, absPath) {
1215
- let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
1216
- route = route.replace(/\[([^\]]+)\]/g, ":$1");
1217
- route = route.replace(/\/+/g, "/");
1218
- if (route === "/") return "/api";
1219
- return "/api" + route;
1220
- }
1221
- function camelToPascal(s) {
1222
- if (!s) return s;
1223
- return s.charAt(0).toUpperCase() + s.slice(1);
1224
- }
1225
- function detect2(rootDir) {
1226
- return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "src", "app", "api"));
1227
- }
1228
- function generate2(rootDir) {
1229
- const apiDir = (0, import_node_path5.join)(rootDir, "src", "app", "api");
1230
- const routeFiles = walk2(apiDir);
1231
- const nodes = [];
1232
- const edges = [];
1233
- const crossRefs = [];
1234
- const mutatorCount = {};
1235
- const authUsage = {};
1236
- let endpointsWithAuth = 0;
1237
- let endpointsWithDbAccess = 0;
1238
- for (const absPath of routeFiles) {
1239
- const parsed = parseFileTS(absPath);
1240
- const dbCalls = extractDbCallsTS(absPath);
1241
- const authWrappers = extractAuthWrappersTS(absPath);
1242
- const methods = [];
1243
- for (const exp of parsed.exports) {
1244
- if (HTTP_METHODS.has(exp)) methods.push(exp);
1245
- }
1246
- const routePath = filePathToRoute(apiDir, absPath);
1247
- const relPath = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
1248
- const mutations = dbCalls.filter((c) => c.isMutation);
1249
- const reads = dbCalls.filter((c) => !c.isMutation);
1250
- const mutates = mutations.length > 0;
1251
- if (mutates) {
1252
- for (const m of mutations) {
1253
- mutatorCount[m.method] = (mutatorCount[m.method] ?? 0) + 1;
1316
+ });
1317
+ if (apiNodes.length > 0) {
1318
+ const mutatorNodes = apiNodes.filter((n) => n.mutates).length;
1319
+ const readOnlyNodes = apiNodes.filter((n) => !n.mutates).length;
1320
+ const authUsage = {};
1321
+ let endpointsWithAuth = 0;
1322
+ for (const n of apiNodes) {
1323
+ const auth = n.auth;
1324
+ if (auth.length > 0 && auth[0] !== "public") {
1325
+ endpointsWithAuth++;
1326
+ for (const w of auth) {
1327
+ authUsage[w] = (authUsage[w] ?? 0) + 1;
1328
+ }
1254
1329
  }
1255
1330
  }
1256
- if (dbCalls.length > 0) endpointsWithDbAccess++;
1257
- const authStrategy = [];
1258
- for (const w of authWrappers) {
1259
- authStrategy.push(w);
1260
- authUsage[w] = (authUsage[w] ?? 0) + 1;
1331
+ const mutatorCount = {};
1332
+ for (const n of apiNodes) {
1333
+ const ops = n.db_operations;
1334
+ for (const op of ops) {
1335
+ const method = op.split(".")[1];
1336
+ if (method) mutatorCount[method] = (mutatorCount[method] ?? 0) + 1;
1337
+ }
1261
1338
  }
1262
- if (authStrategy.length > 0) endpointsWithAuth++;
1263
- const deep = extractDeep(absPath);
1264
- nodes.push({
1265
- id: relPath,
1266
- type: "endpoint",
1267
- name: routePath,
1268
- path: routePath,
1269
- methods,
1270
- handler: relPath,
1271
- // Behavioral classification from handler body (AST-derived).
1272
- mutates,
1273
- auth: authStrategy.length > 0 ? authStrategy : ["public"],
1274
- db_models: [...new Set(dbCalls.map((c) => c.model))],
1275
- db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
1276
- // Deep extraction fields
1277
- conditions: deep.conditions,
1278
- variables: deep.variables,
1279
- responses: deep.responses,
1280
- params: deep.params
1339
+ result.set("api", {
1340
+ metadata: {
1341
+ generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1342
+ scope: "main-app-only",
1343
+ stack: "nextjs-app-router",
1344
+ layer: "api",
1345
+ parser: "typescript-project",
1346
+ total_endpoints: apiNodes.length,
1347
+ total_methods: apiNodes.reduce((sum, n) => sum + n.methods.length, 0),
1348
+ endpoints_with_auth: endpointsWithAuth,
1349
+ endpoints_with_db_access: apiNodes.filter((n) => n.db_models.length > 0).length,
1350
+ mutator_endpoints: mutatorNodes,
1351
+ read_only_endpoints: readOnlyNodes
1352
+ },
1353
+ nodes: stripLayer(apiNodes),
1354
+ edges: apiEdges,
1355
+ cross_refs: apiCrossRefs,
1356
+ contradictions: [],
1357
+ warnings: [],
1358
+ flagged_edges: [],
1359
+ patterns: {
1360
+ total_endpoints: apiNodes.length,
1361
+ methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
1362
+ acc[m] = apiNodes.filter((n) => n.methods.includes(m)).length;
1363
+ return acc;
1364
+ }, {}),
1365
+ auth_strategies: authUsage,
1366
+ mutation_operations: mutatorCount,
1367
+ mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
1368
+ }
1281
1369
  });
1282
- const seenModels = /* @__PURE__ */ new Set();
1283
- for (const call of dbCalls) {
1284
- if (seenModels.has(call.model)) continue;
1285
- seenModels.add(call.model);
1286
- crossRefs.push({
1287
- source: relPath,
1288
- target: camelToPascal(call.model),
1289
- type: call.isMutation ? "mutates" : "reads",
1290
- layer: "db"
1291
- });
1292
- }
1293
1370
  }
1294
- nodes.sort((a, b) => a.path.localeCompare(b.path));
1295
- crossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1296
- const mutatorNodes = nodes.filter((n) => n.mutates).length;
1297
- const readOnlyNodes = nodes.filter((n) => !n.mutates).length;
1298
- return {
1299
- metadata: {
1300
- generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1301
- scope: "main-app-only",
1302
- stack: "nextjs-app-router",
1303
- layer: "api",
1304
- parser: "nextjs-routes-ast",
1305
- total_endpoints: nodes.length,
1306
- total_methods: nodes.reduce((sum, n) => sum + n.methods.length, 0),
1307
- endpoints_with_auth: endpointsWithAuth,
1308
- endpoints_with_db_access: endpointsWithDbAccess,
1309
- mutator_endpoints: mutatorNodes,
1310
- read_only_endpoints: readOnlyNodes
1311
- },
1312
- nodes,
1313
- edges,
1314
- cross_refs: crossRefs,
1315
- contradictions: [],
1316
- warnings: [],
1317
- flagged_edges: [],
1318
- patterns: {
1319
- total_endpoints: nodes.length,
1320
- methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
1321
- acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
1322
- return acc;
1323
- }, {}),
1324
- auth_strategies: authUsage,
1325
- mutation_operations: mutatorCount,
1326
- mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
1327
- }
1328
- };
1371
+ return result;
1329
1372
  }
1330
- var import_node_fs5, import_node_path5, HTTP_METHODS, nextjsRoutesParser;
1331
- var init_nextjs_routes = __esm({
1332
- "src/server/graph/parsers/api/nextjs-routes.ts"() {
1373
+ var import_node_fs5, import_node_path5, HTTP_METHODS, CLASSIFICATION_TO_LAYER, typescriptProjectParser;
1374
+ var init_typescript_project = __esm({
1375
+ "src/server/graph/parsers/ts/typescript-project.ts"() {
1333
1376
  "use strict";
1334
1377
  import_node_fs5 = require("node:fs");
1335
1378
  import_node_path5 = require("node:path");
1379
+ init_config();
1380
+ init_resolve_paths();
1336
1381
  init_ts_extractor();
1337
1382
  HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
1338
- nextjsRoutesParser = {
1339
- id: "nextjs-routes",
1340
- layer: "api",
1341
- detect: detect2,
1342
- generate: generate2
1383
+ CLASSIFICATION_TO_LAYER = {
1384
+ endpoint: "api",
1385
+ page: "ui",
1386
+ layout: "ui",
1387
+ component: "ui",
1388
+ ui: "ui",
1389
+ hook: "ui",
1390
+ context: "ui",
1391
+ config: "ui",
1392
+ lib: "ui",
1393
+ "mcp-tool": "ui",
1394
+ external: "ui"
1395
+ };
1396
+ typescriptProjectParser = {
1397
+ id: "typescript-project",
1398
+ layers: ["ui", "api"],
1399
+ detect,
1400
+ generate
1343
1401
  };
1344
1402
  }
1345
1403
  });
@@ -1434,10 +1492,10 @@ function parseEnums(content) {
1434
1492
  }
1435
1493
  return nodes;
1436
1494
  }
1437
- function detect3(rootDir) {
1495
+ function detect2(rootDir) {
1438
1496
  return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "prisma", "schema.prisma"));
1439
1497
  }
1440
- function generate3(rootDir) {
1498
+ function generate2(rootDir) {
1441
1499
  const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
1442
1500
  const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
1443
1501
  const { nodes: modelNodes, relations } = parseModels(content);
@@ -1497,108 +1555,532 @@ var init_prisma_schema = __esm({
1497
1555
  prismaSchemaParser = {
1498
1556
  id: "prisma-schema",
1499
1557
  layer: "db",
1500
- detect: detect3,
1501
- generate: generate3
1558
+ detect: detect2,
1559
+ generate: generate2
1502
1560
  };
1503
1561
  }
1504
1562
  });
1505
1563
 
1506
- // src/server/graph/core/api-route-matching.ts
1507
- function loadApiRoutesFromOutput(apiOutput) {
1508
- const routes = [];
1509
- for (const n of apiOutput.nodes) {
1510
- const path3 = n.path;
1511
- if (!path3 || typeof path3 !== "string") continue;
1512
- routes.push({
1513
- path: path3,
1514
- nodeId: n.id,
1515
- segments: path3.split("/").filter(Boolean)
1516
- });
1517
- }
1518
- return routes;
1519
- }
1520
- function buildApiPathMap(routes) {
1521
- const map = /* @__PURE__ */ new Map();
1522
- for (const r of routes) {
1523
- if (!map.has(r.path)) map.set(r.path, r.nodeId);
1524
- }
1525
- return map;
1526
- }
1527
- function normalizeFetchUrl(raw) {
1528
- let s = raw.replace(/^`|`$/g, "");
1529
- const qIdx = s.indexOf("?");
1530
- if (qIdx >= 0) s = s.slice(0, qIdx);
1531
- const hIdx = s.indexOf("#");
1532
- if (hIdx >= 0) s = s.slice(0, hIdx);
1533
- let hadInterpolation = false;
1534
- s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
1535
- hadInterpolation = true;
1536
- const cleaned = expr.trim();
1537
- const last = cleaned.split(".").pop() ?? cleaned;
1538
- const name = last.replace(/[^\w]/g, "") || "param";
1539
- return ":" + name;
1540
- });
1541
- s = s.replace(/\/+/g, "/");
1542
- if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
1543
- return { path: s || "/", hadInterpolation };
1564
+ // src/server/graph/parsers/db/sql-migrations.ts
1565
+ function pgTypeToPrisma(pgType) {
1566
+ const upper = pgType.toUpperCase().trim();
1567
+ return PG_TO_PRISMA[upper] ?? upper;
1544
1568
  }
1545
- function scoreApiRouteMatch(candidate, known) {
1546
- if (candidate.length !== known.length) return -1;
1547
- let score = 0;
1548
- for (let i = 0; i < candidate.length; i++) {
1549
- const a = candidate[i];
1550
- const b = known[i];
1551
- if (a === b) {
1552
- score += 3;
1553
- continue;
1554
- }
1555
- if (a.startsWith(":") && b.startsWith(":")) {
1556
- score += 2;
1557
- continue;
1569
+ function parseCreateTable(sql, state) {
1570
+ const re = /CREATE\s+TABLE\s+"(\w+)"\s*\(([\s\S]*?)\);/gi;
1571
+ let m;
1572
+ while ((m = re.exec(sql)) !== null) {
1573
+ const tableName = m[1];
1574
+ const body = m[2];
1575
+ const columns = /* @__PURE__ */ new Map();
1576
+ let primaryCol = null;
1577
+ for (const line of body.split("\n")) {
1578
+ const trimmed = line.trim().replace(/,\s*$/, "");
1579
+ if (!trimmed || trimmed.startsWith("--")) continue;
1580
+ const pkMatch = trimmed.match(/CONSTRAINT\s+"[^"]+"\s+PRIMARY\s+KEY\s*\("(\w+)"\)/i);
1581
+ if (pkMatch) {
1582
+ primaryCol = pkMatch[1];
1583
+ continue;
1584
+ }
1585
+ if (/^\s*CONSTRAINT\s/i.test(trimmed)) continue;
1586
+ const colMatch = trimmed.match(/^"(\w+)"\s+(.+)/);
1587
+ if (!colMatch) continue;
1588
+ const colName = colMatch[1];
1589
+ let rest = colMatch[2];
1590
+ const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
1591
+ const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)(?:\s*,?\s*$)/i);
1592
+ const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
1593
+ let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim().replace(/,\s*$/, "").trim();
1594
+ columns.set(colName, {
1595
+ name: colName,
1596
+ type: colType,
1597
+ nullable: !isNotNull,
1598
+ primary: false,
1599
+ unique: false,
1600
+ default: defaultVal
1601
+ });
1558
1602
  }
1559
- if (a.startsWith(":") || b.startsWith(":")) {
1560
- score += 1;
1561
- continue;
1603
+ if (primaryCol && columns.has(primaryCol)) {
1604
+ columns.get(primaryCol).primary = true;
1562
1605
  }
1563
- return -1;
1606
+ state.tables.set(tableName, { name: tableName, columns });
1564
1607
  }
1565
- return score;
1566
1608
  }
1567
- function resolveFetchCall(call, apiPathMap, apiRoutes) {
1568
- const raw = call.url;
1569
- if (/^(https?:)?\/\//i.test(raw)) {
1570
- return { kind: "external", normalizedUrl: raw };
1571
- }
1572
- if (call.isConcat) {
1573
- return { kind: "dynamic", normalizedUrl: raw };
1609
+ function parseCreateEnum(sql, state) {
1610
+ const re = /CREATE\s+TYPE\s+"(\w+)"\s+AS\s+ENUM\s*\(([^)]+)\)/gi;
1611
+ let m;
1612
+ while ((m = re.exec(sql)) !== null) {
1613
+ const enumName = m[1];
1614
+ const valuesStr = m[2];
1615
+ const values = new Set(
1616
+ valuesStr.split(",").map((v) => v.trim().replace(/^'(.*)'$/, "$1")).filter(Boolean)
1617
+ );
1618
+ state.enums.set(enumName, { name: enumName, values });
1574
1619
  }
1575
- const { path: path3, hadInterpolation } = normalizeFetchUrl(raw);
1576
- if (!path3.startsWith("/")) {
1577
- return { kind: "unresolved", normalizedUrl: path3 };
1620
+ }
1621
+ function parseAlterTable(sql, state) {
1622
+ const addColRe = /ALTER\s+TABLE\s+"(\w+)"\s+ADD\s+COLUMN\s+"(\w+)"\s+(.+?);/gi;
1623
+ let m;
1624
+ while ((m = addColRe.exec(sql)) !== null) {
1625
+ const tableName = m[1];
1626
+ const colName = m[2];
1627
+ let rest = m[3];
1628
+ const table = state.tables.get(tableName);
1629
+ if (!table) continue;
1630
+ const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
1631
+ const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)$/i);
1632
+ const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
1633
+ let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim();
1634
+ table.columns.set(colName, {
1635
+ name: colName,
1636
+ type: colType,
1637
+ nullable: !isNotNull,
1638
+ primary: false,
1639
+ unique: false,
1640
+ default: defaultVal
1641
+ });
1578
1642
  }
1579
- const segs = path3.split("/").filter(Boolean);
1580
- if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
1581
- return { kind: "dynamic", normalizedUrl: path3 };
1643
+ const dropColRe = /ALTER\s+TABLE\s+"(\w+)"\s+DROP\s+COLUMN\s+"(\w+)"/gi;
1644
+ while ((m = dropColRe.exec(sql)) !== null) {
1645
+ const table = state.tables.get(m[1]);
1646
+ if (table) table.columns.delete(m[2]);
1647
+ }
1648
+ 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;
1649
+ while ((m = fkRe.exec(sql)) !== null) {
1650
+ state.fks.push({
1651
+ constraintName: m[2],
1652
+ sourceTable: m[1],
1653
+ sourceColumn: m[3],
1654
+ targetTable: m[4],
1655
+ targetColumn: m[5],
1656
+ onDelete: m[6] ?? null
1657
+ });
1582
1658
  }
1583
- const exact = apiPathMap.get(path3);
1584
- if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
1585
- let bestScore = -1;
1586
- let bestId = null;
1587
- for (const r of apiRoutes) {
1588
- const score = scoreApiRouteMatch(segs, r.segments);
1589
- if (score > bestScore) {
1590
- bestScore = score;
1591
- bestId = r.nodeId;
1592
- }
1659
+ }
1660
+ function parseAlterEnum(sql, state) {
1661
+ const re = /ALTER\s+TYPE\s+"(\w+)"\s+ADD\s+VALUE\s+'([^']+)'/gi;
1662
+ let m;
1663
+ while ((m = re.exec(sql)) !== null) {
1664
+ const en = state.enums.get(m[1]);
1665
+ if (en) en.values.add(m[2]);
1593
1666
  }
1594
- if (bestId && bestScore > 0) {
1595
- return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
1667
+ }
1668
+ function parseDropTable(sql, state) {
1669
+ const re = /DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?"(\w+)"/gi;
1670
+ let m;
1671
+ while ((m = re.exec(sql)) !== null) {
1672
+ state.tables.delete(m[1]);
1673
+ state.fks = state.fks.filter((fk) => fk.sourceTable !== m[1] && fk.targetTable !== m[1]);
1596
1674
  }
1597
- return { kind: "unresolved", normalizedUrl: path3 };
1598
1675
  }
1599
- function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
1600
- const { path: path3, hadInterpolation } = normalizeFetchUrl(urlPath);
1601
- if (!path3.startsWith("/")) {
1676
+ function parseUniqueIndex(sql, state) {
1677
+ const re = /CREATE\s+UNIQUE\s+INDEX\s+"[^"]+"\s+ON\s+"(\w+)"\("(\w+)"\)/gi;
1678
+ let m;
1679
+ while ((m = re.exec(sql)) !== null) {
1680
+ const table = state.tables.get(m[1]);
1681
+ const col = table?.columns.get(m[2]);
1682
+ if (col) col.unique = true;
1683
+ if (!state.uniqueIndexes.has(m[1])) state.uniqueIndexes.set(m[1], /* @__PURE__ */ new Set());
1684
+ state.uniqueIndexes.get(m[1]).add(m[2]);
1685
+ }
1686
+ }
1687
+ function parseMigrations(rootDir) {
1688
+ const migrationsDir = (0, import_node_path7.join)(rootDir, "prisma", "migrations");
1689
+ const state = {
1690
+ tables: /* @__PURE__ */ new Map(),
1691
+ enums: /* @__PURE__ */ new Map(),
1692
+ fks: [],
1693
+ uniqueIndexes: /* @__PURE__ */ new Map()
1694
+ };
1695
+ if (!(0, import_node_fs7.existsSync)(migrationsDir)) return state;
1696
+ const dirs = (0, import_node_fs7.readdirSync)(migrationsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
1697
+ for (const dir of dirs) {
1698
+ const sqlPath = (0, import_node_path7.join)(migrationsDir, dir, "migration.sql");
1699
+ if (!(0, import_node_fs7.existsSync)(sqlPath)) continue;
1700
+ const sql = (0, import_node_fs7.readFileSync)(sqlPath, "utf-8");
1701
+ parseCreateEnum(sql, state);
1702
+ parseCreateTable(sql, state);
1703
+ parseAlterTable(sql, state);
1704
+ parseAlterEnum(sql, state);
1705
+ parseDropTable(sql, state);
1706
+ parseUniqueIndex(sql, state);
1707
+ }
1708
+ return state;
1709
+ }
1710
+ function loadPrismaState(rootDir) {
1711
+ const schemaPath = (0, import_node_path7.join)(rootDir, "prisma", "schema.prisma");
1712
+ if (!(0, import_node_fs7.existsSync)(schemaPath)) return null;
1713
+ const content = (0, import_node_fs7.readFileSync)(schemaPath, "utf-8");
1714
+ const tables = /* @__PURE__ */ new Map();
1715
+ const enums = /* @__PURE__ */ new Map();
1716
+ const relations = [];
1717
+ const modelRe = /model\s+(\w+)\s*\{([^}]+)\}/g;
1718
+ let m;
1719
+ while ((m = modelRe.exec(content)) !== null) {
1720
+ const modelName = m[1];
1721
+ const body = m[2];
1722
+ const cols = [];
1723
+ for (const line of body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("@@"))) {
1724
+ const fm = line.match(/^(\w+)\s+(\S+)(.*)/);
1725
+ if (!fm) continue;
1726
+ const fieldName = fm[1];
1727
+ const fieldType = fm[2];
1728
+ const rest = fm[3] ?? "";
1729
+ const baseType = fieldType.replace(/[?\[\]]/g, "");
1730
+ const isRelation = rest.includes("@relation");
1731
+ if (isRelation) {
1732
+ const relArgs = rest.match(/@relation\(([^)]*)\)/)?.[1] ?? "";
1733
+ const fieldsMatch = relArgs.match(/fields:\s*\[([^\]]+)\]/);
1734
+ if (fieldsMatch) {
1735
+ relations.push({ source: modelName, target: baseType, fk: fieldsMatch[1].trim() });
1736
+ }
1737
+ continue;
1738
+ }
1739
+ if (fieldType.endsWith("[]") || fieldType.endsWith("?") && content.includes(`model ${baseType}`)) {
1740
+ if (new RegExp(`model\\s+${baseType}\\s*\\{`).test(content)) continue;
1741
+ }
1742
+ cols.push({
1743
+ name: fieldName,
1744
+ type: fieldType.replace("?", ""),
1745
+ nullable: fieldType.endsWith("?") || fieldType.includes("?"),
1746
+ primary: rest.includes("@id"),
1747
+ unique: rest.includes("@unique")
1748
+ });
1749
+ }
1750
+ tables.set(modelName, { columns: cols });
1751
+ }
1752
+ const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
1753
+ while ((m = enumRe.exec(content)) !== null) {
1754
+ const values = m[2].split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
1755
+ enums.set(m[1], values);
1756
+ }
1757
+ return { tables, enums, relations };
1758
+ }
1759
+ function verify(sqlState, prisma) {
1760
+ const contradictions = [];
1761
+ const flaggedEdges = [];
1762
+ for (const [name] of sqlState.tables) {
1763
+ if (!prisma.tables.has(name)) {
1764
+ contradictions.push({
1765
+ entity: name,
1766
+ source_a: "sql-migrations",
1767
+ source_b: "prisma-schema",
1768
+ detail: `Table "${name}" exists in migrations but not in schema.prisma`
1769
+ });
1770
+ }
1771
+ }
1772
+ for (const [name] of prisma.tables) {
1773
+ if (!sqlState.tables.has(name)) {
1774
+ contradictions.push({
1775
+ entity: name,
1776
+ source_a: "prisma-schema",
1777
+ source_b: "sql-migrations",
1778
+ detail: `Table "${name}" in schema.prisma has no CREATE TABLE in migrations`
1779
+ });
1780
+ }
1781
+ }
1782
+ for (const [tableName, prismaTable] of prisma.tables) {
1783
+ const sqlTable = sqlState.tables.get(tableName);
1784
+ if (!sqlTable) continue;
1785
+ for (const prismaCol of prismaTable.columns) {
1786
+ const sqlCol = sqlTable.columns.get(prismaCol.name);
1787
+ if (!sqlCol) {
1788
+ contradictions.push({
1789
+ entity: `${tableName}.${prismaCol.name}`,
1790
+ source_a: "prisma-schema",
1791
+ source_b: "sql-migrations",
1792
+ detail: `Column "${tableName}.${prismaCol.name}" in schema.prisma but not in migrations`
1793
+ });
1794
+ continue;
1795
+ }
1796
+ const mappedSqlType = pgTypeToPrisma(sqlCol.type);
1797
+ const prismaBaseType = prismaCol.type.replace(/[?\[\]]/g, "");
1798
+ if (mappedSqlType !== prismaBaseType && mappedSqlType !== prismaCol.type) {
1799
+ if (!sqlState.enums.has(prismaBaseType)) {
1800
+ contradictions.push({
1801
+ entity: `${tableName}.${prismaCol.name}`,
1802
+ source_a: "sql-migrations",
1803
+ source_b: "prisma-schema",
1804
+ detail: `Column "${tableName}.${prismaCol.name}": SQL type "${sqlCol.type}" (\u2192${mappedSqlType}) vs Prisma type "${prismaCol.type}"`
1805
+ });
1806
+ }
1807
+ }
1808
+ if (sqlCol.nullable !== prismaCol.nullable) {
1809
+ contradictions.push({
1810
+ entity: `${tableName}.${prismaCol.name}`,
1811
+ source_a: "sql-migrations",
1812
+ source_b: "prisma-schema",
1813
+ detail: `Column "${tableName}.${prismaCol.name}": ${sqlCol.nullable ? "nullable" : "NOT NULL"} in SQL vs ${prismaCol.nullable ? "optional" : "required"} in Prisma`
1814
+ });
1815
+ }
1816
+ }
1817
+ for (const [colName] of sqlTable.columns) {
1818
+ const inPrisma = prismaTable.columns.some((c) => c.name === colName);
1819
+ if (!inPrisma) {
1820
+ contradictions.push({
1821
+ entity: `${tableName}.${colName}`,
1822
+ source_a: "sql-migrations",
1823
+ source_b: "prisma-schema",
1824
+ detail: `Column "${tableName}.${colName}" in migrations but not in schema.prisma`
1825
+ });
1826
+ }
1827
+ }
1828
+ }
1829
+ for (const [name, sqlEnum] of sqlState.enums) {
1830
+ const prismaValues = prisma.enums.get(name);
1831
+ if (!prismaValues) {
1832
+ contradictions.push({
1833
+ entity: name,
1834
+ source_a: "sql-migrations",
1835
+ source_b: "prisma-schema",
1836
+ detail: `Enum "${name}" exists in migrations but not in schema.prisma`
1837
+ });
1838
+ continue;
1839
+ }
1840
+ const prismaSet = new Set(prismaValues);
1841
+ for (const val of sqlEnum.values) {
1842
+ if (!prismaSet.has(val)) {
1843
+ contradictions.push({
1844
+ entity: `${name}.${val}`,
1845
+ source_a: "sql-migrations",
1846
+ source_b: "prisma-schema",
1847
+ detail: `Enum "${name}": value "${val}" in migrations but not in schema.prisma`
1848
+ });
1849
+ }
1850
+ }
1851
+ for (const val of prismaValues) {
1852
+ if (!sqlEnum.values.has(val)) {
1853
+ contradictions.push({
1854
+ entity: `${name}.${val}`,
1855
+ source_a: "prisma-schema",
1856
+ source_b: "sql-migrations",
1857
+ detail: `Enum "${name}": value "${val}" in schema.prisma but not in migrations`
1858
+ });
1859
+ }
1860
+ }
1861
+ }
1862
+ const prismaFkSet = new Set(prisma.relations.map((r) => `${r.source}|${r.target}|${r.fk}`));
1863
+ for (const fk of sqlState.fks) {
1864
+ const key = `${fk.sourceTable}|${fk.targetTable}|${fk.sourceColumn}`;
1865
+ if (!prismaFkSet.has(key)) {
1866
+ flaggedEdges.push({
1867
+ source: fk.sourceTable,
1868
+ target: fk.targetTable,
1869
+ type: "belongs_to",
1870
+ label: `FK "${fk.constraintName}" (${fk.sourceColumn}\u2192${fk.targetTable}) in migrations but no @relation in schema.prisma`,
1871
+ confidence: "high"
1872
+ });
1873
+ }
1874
+ }
1875
+ return { contradictions, flaggedEdges };
1876
+ }
1877
+ function detect3(rootDir) {
1878
+ const migrationsDir = (0, import_node_path7.join)(rootDir, "prisma", "migrations");
1879
+ if (!(0, import_node_fs7.existsSync)(migrationsDir)) return false;
1880
+ return (0, import_node_fs7.readdirSync)(migrationsDir, { withFileTypes: true }).some((d) => d.isDirectory() && (0, import_node_fs7.existsSync)((0, import_node_path7.join)(migrationsDir, d.name, "migration.sql")));
1881
+ }
1882
+ function generate3(rootDir) {
1883
+ const sqlState = parseMigrations(rootDir);
1884
+ const prisma = loadPrismaState(rootDir);
1885
+ const prismaTableIds = prisma ? new Set(prisma.tables.keys()) : /* @__PURE__ */ new Set();
1886
+ const prismaEnumIds = prisma ? new Set(prisma.enums.keys()) : /* @__PURE__ */ new Set();
1887
+ const nodes = [];
1888
+ for (const [name, table] of sqlState.tables) {
1889
+ if (prismaTableIds.has(name)) continue;
1890
+ nodes.push({
1891
+ id: name,
1892
+ type: "table",
1893
+ name,
1894
+ source: "sql",
1895
+ columns: [...table.columns.values()].map((c) => ({
1896
+ name: c.name,
1897
+ type: c.type,
1898
+ primary: c.primary,
1899
+ unique: c.unique,
1900
+ nullable: c.nullable,
1901
+ default: c.default,
1902
+ isRelation: false,
1903
+ comment: null
1904
+ }))
1905
+ });
1906
+ }
1907
+ for (const [name, sqlEnum] of sqlState.enums) {
1908
+ if (prismaEnumIds.has(name)) continue;
1909
+ nodes.push({
1910
+ id: name,
1911
+ type: "enum",
1912
+ name,
1913
+ source: "sql",
1914
+ values: [...sqlEnum.values]
1915
+ });
1916
+ }
1917
+ const sqlOnlyTables = new Set(nodes.filter((n) => n.type === "table").map((n) => n.id));
1918
+ const edges = sqlState.fks.filter((fk) => sqlOnlyTables.has(fk.sourceTable)).map((fk) => ({
1919
+ source: fk.sourceTable,
1920
+ target: fk.targetTable,
1921
+ type: "belongs_to",
1922
+ fk: fk.sourceColumn,
1923
+ onDelete: fk.onDelete
1924
+ }));
1925
+ const { contradictions, flaggedEdges } = prisma ? verify(sqlState, prisma) : { contradictions: [], flaggedEdges: [] };
1926
+ return {
1927
+ metadata: {
1928
+ generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1929
+ scope: "sql-migrations",
1930
+ source: "prisma/migrations/",
1931
+ layer: "db",
1932
+ sql_tables: sqlState.tables.size,
1933
+ sql_enums: sqlState.enums.size,
1934
+ sql_fks: sqlState.fks.length,
1935
+ additive_nodes: nodes.length,
1936
+ contradictions_found: contradictions.length,
1937
+ flagged_edges_found: flaggedEdges.length
1938
+ },
1939
+ nodes,
1940
+ edges,
1941
+ cross_refs: [],
1942
+ contradictions,
1943
+ warnings: [],
1944
+ flagged_edges: flaggedEdges
1945
+ };
1946
+ }
1947
+ var import_node_fs7, import_node_path7, PG_TO_PRISMA, sqlMigrationsParser;
1948
+ var init_sql_migrations = __esm({
1949
+ "src/server/graph/parsers/db/sql-migrations.ts"() {
1950
+ "use strict";
1951
+ import_node_fs7 = require("node:fs");
1952
+ import_node_path7 = require("node:path");
1953
+ PG_TO_PRISMA = {
1954
+ "TEXT": "String",
1955
+ "VARCHAR": "String",
1956
+ "CHAR": "String",
1957
+ "INTEGER": "Int",
1958
+ "INT": "Int",
1959
+ "SMALLINT": "Int",
1960
+ "BIGINT": "BigInt",
1961
+ "SERIAL": "Int",
1962
+ "BOOLEAN": "Boolean",
1963
+ "BOOL": "Boolean",
1964
+ "TIMESTAMP(3)": "DateTime",
1965
+ "TIMESTAMP": "DateTime",
1966
+ "TIMESTAMPTZ": "DateTime",
1967
+ "DATE": "DateTime",
1968
+ "DOUBLE PRECISION": "Float",
1969
+ "FLOAT": "Float",
1970
+ "REAL": "Float",
1971
+ "DECIMAL": "Decimal",
1972
+ "NUMERIC": "Decimal",
1973
+ "JSONB": "Json",
1974
+ "JSON": "Json",
1975
+ "BYTEA": "Bytes",
1976
+ "UUID": "String",
1977
+ "TEXT[]": "String[]"
1978
+ };
1979
+ sqlMigrationsParser = {
1980
+ id: "sql-migrations",
1981
+ layer: "db",
1982
+ detect: detect3,
1983
+ generate: generate3
1984
+ };
1985
+ }
1986
+ });
1987
+
1988
+ // src/server/graph/core/api-route-matching.ts
1989
+ function loadApiRoutesFromOutput(apiOutput) {
1990
+ const routes = [];
1991
+ for (const n of apiOutput.nodes) {
1992
+ const path3 = n.path;
1993
+ if (!path3 || typeof path3 !== "string") continue;
1994
+ routes.push({
1995
+ path: path3,
1996
+ nodeId: n.id,
1997
+ segments: path3.split("/").filter(Boolean)
1998
+ });
1999
+ }
2000
+ return routes;
2001
+ }
2002
+ function buildApiPathMap(routes) {
2003
+ const map = /* @__PURE__ */ new Map();
2004
+ for (const r of routes) {
2005
+ if (!map.has(r.path)) map.set(r.path, r.nodeId);
2006
+ }
2007
+ return map;
2008
+ }
2009
+ function normalizeFetchUrl(raw) {
2010
+ let s = raw.replace(/^`|`$/g, "");
2011
+ const qIdx = s.indexOf("?");
2012
+ if (qIdx >= 0) s = s.slice(0, qIdx);
2013
+ const hIdx = s.indexOf("#");
2014
+ if (hIdx >= 0) s = s.slice(0, hIdx);
2015
+ let hadInterpolation = false;
2016
+ s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
2017
+ hadInterpolation = true;
2018
+ const cleaned = expr.trim();
2019
+ const last = cleaned.split(".").pop() ?? cleaned;
2020
+ const name = last.replace(/[^\w]/g, "") || "param";
2021
+ return ":" + name;
2022
+ });
2023
+ s = s.replace(/\/+/g, "/");
2024
+ if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
2025
+ return { path: s || "/", hadInterpolation };
2026
+ }
2027
+ function scoreApiRouteMatch(candidate, known) {
2028
+ if (candidate.length !== known.length) return -1;
2029
+ let score = 0;
2030
+ for (let i = 0; i < candidate.length; i++) {
2031
+ const a = candidate[i];
2032
+ const b = known[i];
2033
+ if (a === b) {
2034
+ score += 3;
2035
+ continue;
2036
+ }
2037
+ if (a.startsWith(":") && b.startsWith(":")) {
2038
+ score += 2;
2039
+ continue;
2040
+ }
2041
+ if (a.startsWith(":") || b.startsWith(":")) {
2042
+ score += 1;
2043
+ continue;
2044
+ }
2045
+ return -1;
2046
+ }
2047
+ return score;
2048
+ }
2049
+ function resolveFetchCall(call, apiPathMap, apiRoutes) {
2050
+ const raw = call.url;
2051
+ if (/^(https?:)?\/\//i.test(raw)) {
2052
+ return { kind: "external", normalizedUrl: raw };
2053
+ }
2054
+ if (call.isConcat) {
2055
+ return { kind: "dynamic", normalizedUrl: raw };
2056
+ }
2057
+ const { path: path3, hadInterpolation } = normalizeFetchUrl(raw);
2058
+ if (!path3.startsWith("/")) {
2059
+ return { kind: "unresolved", normalizedUrl: path3 };
2060
+ }
2061
+ const segs = path3.split("/").filter(Boolean);
2062
+ if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
2063
+ return { kind: "dynamic", normalizedUrl: path3 };
2064
+ }
2065
+ const exact = apiPathMap.get(path3);
2066
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
2067
+ let bestScore = -1;
2068
+ let bestId = null;
2069
+ for (const r of apiRoutes) {
2070
+ const score = scoreApiRouteMatch(segs, r.segments);
2071
+ if (score > bestScore) {
2072
+ bestScore = score;
2073
+ bestId = r.nodeId;
2074
+ }
2075
+ }
2076
+ if (bestId && bestScore > 0) {
2077
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
2078
+ }
2079
+ return { kind: "unresolved", normalizedUrl: path3 };
2080
+ }
2081
+ function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
2082
+ const { path: path3, hadInterpolation } = normalizeFetchUrl(urlPath);
2083
+ if (!path3.startsWith("/")) {
1602
2084
  return { kind: "unresolved", normalizedUrl: path3 };
1603
2085
  }
1604
2086
  const segs = path3.split("/").filter(Boolean);
@@ -1636,6 +2118,7 @@ var init_fetch_resolver = __esm({
1636
2118
  fetchResolverParser = {
1637
2119
  id: "fetch-resolver",
1638
2120
  layer: "crosslayer",
2121
+ concern: "api-binding",
1639
2122
  detect(_rootDir) {
1640
2123
  return true;
1641
2124
  },
@@ -1728,36 +2211,37 @@ var init_fetch_resolver = __esm({
1728
2211
  });
1729
2212
 
1730
2213
  // src/server/graph/parsers/crosslayer/api-annotations.ts
1731
- function walk3(dir, exts) {
1732
- if (!(0, import_node_fs7.existsSync)(dir)) return [];
2214
+ function walk2(dir, exts) {
2215
+ if (!(0, import_node_fs8.existsSync)(dir)) return [];
1733
2216
  const results = [];
1734
- for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
2217
+ for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
1735
2218
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1736
- const full = (0, import_node_path7.join)(dir, entry.name);
2219
+ const full = (0, import_node_path8.join)(dir, entry.name);
1737
2220
  if (entry.isDirectory()) {
1738
- results.push(...walk3(full, exts));
1739
- } else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
2221
+ results.push(...walk2(full, exts));
2222
+ } else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
1740
2223
  results.push(full);
1741
2224
  }
1742
2225
  }
1743
2226
  return results;
1744
2227
  }
1745
2228
  function toNodeId2(srcDir, absPath) {
1746
- return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
2229
+ return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
1747
2230
  }
1748
- var import_node_fs7, import_node_path7, API_ANNOTATION_RE, apiAnnotationsParser;
2231
+ var import_node_fs8, import_node_path8, API_ANNOTATION_RE, apiAnnotationsParser;
1749
2232
  var init_api_annotations = __esm({
1750
2233
  "src/server/graph/parsers/crosslayer/api-annotations.ts"() {
1751
2234
  "use strict";
1752
- import_node_fs7 = require("node:fs");
1753
- import_node_path7 = require("node:path");
2235
+ import_node_fs8 = require("node:fs");
2236
+ import_node_path8 = require("node:path");
1754
2237
  init_api_route_matching();
1755
2238
  API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
1756
2239
  apiAnnotationsParser = {
1757
2240
  id: "api-annotations",
1758
2241
  layer: "crosslayer",
2242
+ concern: "api-binding",
1759
2243
  detect(rootDir) {
1760
- return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
2244
+ return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
1761
2245
  },
1762
2246
  generate(rootDir, layerOutputs) {
1763
2247
  const apiOutput = layerOutputs.get("api");
@@ -1768,13 +2252,13 @@ var init_api_annotations = __esm({
1768
2252
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1769
2253
  const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1770
2254
  const apiPathMap = buildApiPathMap(apiRoutes);
1771
- const srcDir = (0, import_node_path7.join)(rootDir, "src");
1772
- const files = walk3(srcDir, [".ts", ".tsx"]);
2255
+ const srcDir = (0, import_node_path8.join)(rootDir, "src");
2256
+ const files = walk2(srcDir, [".ts", ".tsx"]);
1773
2257
  const crossRefs = [];
1774
2258
  const flaggedEdges = [];
1775
2259
  const seen = /* @__PURE__ */ new Set();
1776
2260
  for (const absPath of files) {
1777
- const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
2261
+ const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
1778
2262
  const sourceId = toNodeId2(srcDir, absPath);
1779
2263
  if (!uiNodeIds.has(sourceId)) continue;
1780
2264
  let match;
@@ -1820,36 +2304,40 @@ var init_api_annotations = __esm({
1820
2304
  });
1821
2305
 
1822
2306
  // src/server/graph/parsers/crosslayer/url-literal-scanner.ts
1823
- function walk4(dir, exts) {
1824
- if (!(0, import_node_fs8.existsSync)(dir)) return [];
2307
+ function walk3(dir, exts) {
2308
+ if (!(0, import_node_fs9.existsSync)(dir)) return [];
1825
2309
  const results = [];
1826
- for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
2310
+ for (const entry of (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true })) {
1827
2311
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1828
- const full = (0, import_node_path8.join)(dir, entry.name);
2312
+ const full = (0, import_node_path9.join)(dir, entry.name);
1829
2313
  if (entry.isDirectory()) {
1830
- results.push(...walk4(full, exts));
1831
- } else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
2314
+ results.push(...walk3(full, exts));
2315
+ } else if (exts.includes((0, import_node_path9.extname)(entry.name))) {
1832
2316
  results.push(full);
1833
2317
  }
1834
2318
  }
1835
2319
  return results;
1836
2320
  }
1837
2321
  function toNodeId3(srcDir, absPath) {
1838
- return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
2322
+ return (0, import_node_path9.relative)(srcDir, absPath).replace(/\\/g, "/");
1839
2323
  }
1840
- var import_node_fs8, import_node_path8, URL_LITERAL_RE, urlLiteralScannerParser;
2324
+ var import_node_fs9, import_node_path9, URL_LITERAL_RE, urlLiteralScannerParser;
1841
2325
  var init_url_literal_scanner = __esm({
1842
2326
  "src/server/graph/parsers/crosslayer/url-literal-scanner.ts"() {
1843
2327
  "use strict";
1844
- import_node_fs8 = require("node:fs");
1845
- import_node_path8 = require("node:path");
2328
+ import_node_fs9 = require("node:fs");
2329
+ import_node_path9 = require("node:path");
1846
2330
  init_api_route_matching();
2331
+ init_config();
2332
+ init_resolve_paths();
1847
2333
  URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
1848
2334
  urlLiteralScannerParser = {
1849
2335
  id: "url-literal-scanner",
1850
2336
  layer: "crosslayer",
2337
+ concern: "api-binding",
1851
2338
  detect(rootDir) {
1852
- return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
2339
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2340
+ return paths !== null;
1853
2341
  },
1854
2342
  generate(rootDir, layerOutputs) {
1855
2343
  const apiOutput = layerOutputs.get("api");
@@ -1860,19 +2348,19 @@ var init_url_literal_scanner = __esm({
1860
2348
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1861
2349
  const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1862
2350
  const apiPathMap = buildApiPathMap(apiRoutes);
1863
- const srcDir = (0, import_node_path8.join)(rootDir, "src");
1864
- const clientDir = (0, import_node_path8.join)(srcDir, "client");
1865
- const appDir = (0, import_node_path8.join)(srcDir, "app");
2351
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2352
+ const srcDir = paths.srcDir;
2353
+ const clientDir = (0, import_node_path9.join)(srcDir, "client");
1866
2354
  const files = [
1867
- ...walk4(clientDir, [".ts", ".tsx"]),
1868
- ...walk4(appDir, [".ts", ".tsx"])
2355
+ ...walk3(clientDir, [".ts", ".tsx"]),
2356
+ ...walk3(paths.appDir, [".ts", ".tsx"])
1869
2357
  ];
1870
2358
  const crossRefs = [];
1871
2359
  const seen = /* @__PURE__ */ new Set();
1872
2360
  for (const absPath of files) {
1873
2361
  const sourceId = toNodeId3(srcDir, absPath);
1874
2362
  if (!uiNodeIds.has(sourceId)) continue;
1875
- const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
2363
+ const content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
1876
2364
  let match;
1877
2365
  URL_LITERAL_RE.lastIndex = 0;
1878
2366
  while ((match = URL_LITERAL_RE.exec(content)) !== null) {
@@ -1904,15 +2392,689 @@ var init_url_literal_scanner = __esm({
1904
2392
  }
1905
2393
  });
1906
2394
 
2395
+ // src/server/graph/parsers/static/static-values.ts
2396
+ function tryLoadTreeSitter() {
2397
+ if (parseCode) return true;
2398
+ try {
2399
+ const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
2400
+ if (typeof extractor.parseCodeTS === "function") {
2401
+ parseCode = extractor.parseCodeTS;
2402
+ return true;
2403
+ }
2404
+ } catch {
2405
+ }
2406
+ return false;
2407
+ }
2408
+ function classifyScope(source, model) {
2409
+ if (source.includes("prisma/schema.prisma")) return "shared";
2410
+ if (source.includes("prisma/seed") && model) {
2411
+ if (SHARED_MODELS.has(model)) return "shared";
2412
+ if (DB_MODELS.has(model)) return "db";
2413
+ return "shared";
2414
+ }
2415
+ if (source.startsWith("src/client/") || source.startsWith("src/app/")) return "fe";
2416
+ if (source.startsWith("src/server/")) return "be";
2417
+ if (source.startsWith("src/config/")) return "be";
2418
+ if (source.startsWith("src/lib/")) return "shared";
2419
+ return "shared";
2420
+ }
2421
+ function extractEnumValues(rootDir) {
2422
+ const nodes = [];
2423
+ const edges = [];
2424
+ const schemaPaths = [
2425
+ (0, import_node_path10.join)(rootDir, "prisma", "schema.prisma"),
2426
+ (0, import_node_path10.join)(rootDir, "prisma", "schema")
2427
+ ];
2428
+ let content = "";
2429
+ for (const p of schemaPaths) {
2430
+ if ((0, import_node_fs10.existsSync)(p)) {
2431
+ try {
2432
+ const stat = (0, import_node_fs10.statSync)(p);
2433
+ if (stat.isFile()) {
2434
+ content = (0, import_node_fs10.readFileSync)(p, "utf-8");
2435
+ } else if (stat.isDirectory()) {
2436
+ const files = (0, import_node_fs10.readdirSync)(p).filter((f) => f.endsWith(".prisma"));
2437
+ content = files.map((f) => (0, import_node_fs10.readFileSync)((0, import_node_path10.join)(p, f), "utf-8")).join("\n");
2438
+ }
2439
+ } catch {
2440
+ continue;
2441
+ }
2442
+ break;
2443
+ }
2444
+ }
2445
+ if (!content) return { nodes, edges };
2446
+ const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
2447
+ let m;
2448
+ while ((m = enumRe.exec(content)) !== null) {
2449
+ const enumName = m[1];
2450
+ const body = m[2];
2451
+ const values = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
2452
+ const scope = classifyScope("prisma/schema.prisma");
2453
+ for (const val of values) {
2454
+ const nodeId = `${enumName}.${val}`;
2455
+ nodes.push({
2456
+ id: nodeId,
2457
+ type: "enum_value",
2458
+ name: val,
2459
+ parentEnum: enumName,
2460
+ value: val,
2461
+ scope,
2462
+ source: "prisma/schema.prisma"
2463
+ });
2464
+ edges.push({
2465
+ source: nodeId,
2466
+ target: `enum:${enumName}`,
2467
+ type: "member_of"
2468
+ });
2469
+ }
2470
+ nodes.push({
2471
+ id: `enum:${enumName}`,
2472
+ type: "enum_group",
2473
+ name: enumName,
2474
+ valueCount: values.length,
2475
+ scope,
2476
+ source: "prisma/schema.prisma"
2477
+ });
2478
+ }
2479
+ return { nodes, edges };
2480
+ }
2481
+ function extractPropsFromObjectNode(node) {
2482
+ const props = {};
2483
+ for (const child of node.namedChildren) {
2484
+ if (child.type !== "pair") continue;
2485
+ const keyNode = child.childForFieldName("key");
2486
+ const valNode = child.childForFieldName("value");
2487
+ if (!keyNode || !valNode) continue;
2488
+ const key = keyNode.type === "property_identifier" ? keyNode.text : keyNode.text.replace(/['"]/g, "");
2489
+ if (valNode.type === "string" || valNode.type === "template_string") {
2490
+ const fragment = valNode.namedChildren.find((c) => c.type === "string_fragment" || c.type === "template_substitution");
2491
+ props[key] = fragment ? fragment.text : valNode.text.replace(/^['"`]|['"`]$/g, "");
2492
+ } else if (valNode.type === "number") {
2493
+ props[key] = valNode.text;
2494
+ } else if (valNode.type === "true" || valNode.type === "false") {
2495
+ props[key] = valNode.text;
2496
+ }
2497
+ }
2498
+ return props;
2499
+ }
2500
+ function extractStringArrayFromNode(node) {
2501
+ const values = [];
2502
+ for (const child of node.namedChildren) {
2503
+ if (child.type === "string") {
2504
+ const frag = child.namedChildren.find((c) => c.type === "string_fragment");
2505
+ if (frag) values.push(frag.text);
2506
+ }
2507
+ }
2508
+ return values;
2509
+ }
2510
+ function findArrayDecl(root, varName) {
2511
+ function walk5(node) {
2512
+ if (node.type === "variable_declarator") {
2513
+ const nameNode = node.childForFieldName("name");
2514
+ const valueNode = node.childForFieldName("value");
2515
+ if (nameNode?.text === varName && valueNode?.type === "array") {
2516
+ return valueNode;
2517
+ }
2518
+ if (nameNode?.text === varName && valueNode?.type === "as_expression") {
2519
+ const inner = valueNode.namedChildren.find((c) => c.type === "array");
2520
+ if (inner) return inner;
2521
+ }
2522
+ }
2523
+ for (const child of node.namedChildren) {
2524
+ const found = walk5(child);
2525
+ if (found) return found;
2526
+ }
2527
+ return null;
2528
+ }
2529
+ return walk5(root);
2530
+ }
2531
+ function extractObjectPropsRegex(objStr) {
2532
+ const props = {};
2533
+ const propRe = /(\w+):\s*['"]([^'"]*)['"]/g;
2534
+ let m;
2535
+ while ((m = propRe.exec(objStr)) !== null) props[m[1]] = m[2];
2536
+ const numRe = /(\w+):\s*(-?\d+(?:\.\d+)?)\s*[,\n}]/g;
2537
+ while ((m = numRe.exec(objStr)) !== null) if (!props[m[1]]) props[m[1]] = m[2];
2538
+ return props;
2539
+ }
2540
+ function extractStringArrayRegex(arrStr) {
2541
+ return (arrStr.match(/'([^']+)'/g) ?? []).map((s) => s.replace(/'/g, ""));
2542
+ }
2543
+ function splitArrayObjectsRegex(arrayBody) {
2544
+ const objects = [];
2545
+ let depth = 0;
2546
+ let start = -1;
2547
+ for (let i = 0; i < arrayBody.length; i++) {
2548
+ if (arrayBody[i] === "{") {
2549
+ if (depth === 0) start = i;
2550
+ depth++;
2551
+ } else if (arrayBody[i] === "}") {
2552
+ depth--;
2553
+ if (depth === 0 && start >= 0) {
2554
+ objects.push(arrayBody.slice(start, i + 1));
2555
+ start = -1;
2556
+ }
2557
+ }
2558
+ }
2559
+ return objects;
2560
+ }
2561
+ function detectSeededArrays(content, sourceFile) {
2562
+ const results = [];
2563
+ const forOfRe = /for\s*\(\s*const\s+\w+\s+of\s+(\w+)\s*\)\s*\{/g;
2564
+ let fm;
2565
+ while ((fm = forOfRe.exec(content)) !== null) {
2566
+ const arrayName = fm[1];
2567
+ const lookahead = content.slice(fm.index + fm[0].length, fm.index + fm[0].length + 500);
2568
+ const prismaMatch = lookahead.match(/prisma\.(\w+)\.(create|upsert|update|createMany|findFirst)/);
2569
+ if (!prismaMatch) continue;
2570
+ results.push({ arrayName, prismaModel: prismaMatch[1], sourceFile });
2571
+ }
2572
+ return results;
2573
+ }
2574
+ function pickIdField(props) {
2575
+ for (const key of ["key", "slug", "id", "name", "templateId"]) {
2576
+ if (props[key]) return props[key];
2577
+ }
2578
+ return null;
2579
+ }
2580
+ function pickNameField(props) {
2581
+ for (const key of ["name", "slug", "key", "id"]) {
2582
+ if (props[key]) return props[key];
2583
+ }
2584
+ return null;
2585
+ }
2586
+ function modelToNodeType(model) {
2587
+ return `seed_${model.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "")}`;
2588
+ }
2589
+ function extractSeedData(rootDir) {
2590
+ const nodes = [];
2591
+ const edges = [];
2592
+ const seedFiles = [
2593
+ (0, import_node_path10.join)(rootDir, "prisma", "seed.ts"),
2594
+ (0, import_node_path10.join)(rootDir, "prisma", "seed.js"),
2595
+ (0, import_node_path10.join)(rootDir, "src", "server", "lib", "system-tags.ts")
2596
+ ].filter(import_node_fs10.existsSync);
2597
+ const useTreeSitter = tryLoadTreeSitter();
2598
+ for (const filePath of seedFiles) {
2599
+ const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
2600
+ const relPath = (0, import_node_path10.relative)(rootDir, filePath);
2601
+ const seeded = detectSeededArrays(content, relPath);
2602
+ let astRoot = null;
2603
+ if (useTreeSitter && parseCode) {
2604
+ try {
2605
+ const tree = parseCode(content);
2606
+ astRoot = tree.rootNode;
2607
+ } catch {
2608
+ }
2609
+ }
2610
+ for (const { arrayName, prismaModel, sourceFile } of seeded) {
2611
+ const nodeType = modelToNodeType(prismaModel);
2612
+ const scope = classifyScope(sourceFile, prismaModel);
2613
+ if (astRoot) {
2614
+ const arrayNode = findArrayDecl(astRoot, arrayName);
2615
+ if (!arrayNode) continue;
2616
+ for (const child of arrayNode.namedChildren) {
2617
+ if (child.type !== "object") continue;
2618
+ const props = extractPropsFromObjectNode(child);
2619
+ const idVal = pickIdField(props);
2620
+ if (!idVal) continue;
2621
+ const nameVal = pickNameField(props) ?? idVal;
2622
+ const nodeId = `seed:${prismaModel}:${idVal}`;
2623
+ const { scope: _seedScope, ...safeProps } = props;
2624
+ nodes.push({
2625
+ id: nodeId,
2626
+ type: nodeType,
2627
+ name: nameVal,
2628
+ value: idVal,
2629
+ model: prismaModel,
2630
+ ...safeProps,
2631
+ seedScope: _seedScope ?? void 0,
2632
+ // preserve as seedScope
2633
+ scope,
2634
+ source: sourceFile
2635
+ });
2636
+ const permsArrayNode = child.namedChildren.filter((c) => c.type === "pair").find((c) => c.childForFieldName("key")?.text === "permissions");
2637
+ if (permsArrayNode) {
2638
+ const valNode = permsArrayNode.childForFieldName("value");
2639
+ if (valNode?.type === "array") {
2640
+ const permKeys = extractStringArrayFromNode(valNode);
2641
+ for (const pk of permKeys) {
2642
+ if (pk === "*") continue;
2643
+ edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
2644
+ }
2645
+ }
2646
+ }
2647
+ }
2648
+ } else {
2649
+ const constRe = new RegExp(`const\\s+${arrayName}\\s*(?::[^=]+)?=\\s*\\[`, "g");
2650
+ const cm = constRe.exec(content);
2651
+ if (!cm) continue;
2652
+ const bracketStart = cm.index + cm[0].length - 1;
2653
+ let depth = 1, i = bracketStart + 1;
2654
+ while (i < content.length && depth > 0) {
2655
+ if (content[i] === "[") depth++;
2656
+ else if (content[i] === "]") depth--;
2657
+ i++;
2658
+ }
2659
+ if (depth !== 0) continue;
2660
+ const body = content.slice(bracketStart + 1, i - 1);
2661
+ const objects = splitArrayObjectsRegex(body);
2662
+ for (const objStr of objects) {
2663
+ const props = extractObjectPropsRegex(objStr);
2664
+ const idVal = pickIdField(props);
2665
+ if (!idVal) continue;
2666
+ const nameVal = pickNameField(props) ?? idVal;
2667
+ const nodeId = `seed:${prismaModel}:${idVal}`;
2668
+ const { scope: _rScope, ...rSafeProps } = props;
2669
+ nodes.push({
2670
+ id: nodeId,
2671
+ type: nodeType,
2672
+ name: nameVal,
2673
+ value: idVal,
2674
+ model: prismaModel,
2675
+ ...rSafeProps,
2676
+ seedScope: _rScope ?? void 0,
2677
+ scope,
2678
+ source: sourceFile
2679
+ });
2680
+ const permArrayMatch = objStr.match(/permissions:\s*\[([^\]]*)\]/);
2681
+ if (permArrayMatch) {
2682
+ for (const pk of extractStringArrayRegex(permArrayMatch[1])) {
2683
+ if (pk === "*") continue;
2684
+ edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
2685
+ }
2686
+ }
2687
+ }
2688
+ }
2689
+ }
2690
+ }
2691
+ return { nodes, edges };
2692
+ }
2693
+ function walkDir(dir, exts) {
2694
+ if (!(0, import_node_fs10.existsSync)(dir)) return [];
2695
+ const results = [];
2696
+ for (const entry of (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true })) {
2697
+ if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
2698
+ const full = (0, import_node_path10.join)(dir, entry.name);
2699
+ if (entry.isDirectory()) results.push(...walkDir(full, exts));
2700
+ else if (exts.some((ext) => entry.name.endsWith(ext))) results.push(full);
2701
+ }
2702
+ return results;
2703
+ }
2704
+ function extractConstants(rootDir) {
2705
+ const nodes = [];
2706
+ const srcDir = (0, import_node_path10.join)(rootDir, "src");
2707
+ if (!(0, import_node_fs10.existsSync)(srcDir)) return { nodes };
2708
+ for (const filePath of walkDir(srcDir, [".ts", ".tsx"])) {
2709
+ const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
2710
+ const relPath = (0, import_node_path10.relative)(rootDir, filePath);
2711
+ const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
2712
+ let cm;
2713
+ while ((cm = constArrayRe.exec(content)) !== null) {
2714
+ const constName = cm[1];
2715
+ const bracketStart = cm.index + cm[0].length - 1;
2716
+ let depth = 1, i = bracketStart + 1;
2717
+ while (i < content.length && depth > 0) {
2718
+ if (content[i] === "[") depth++;
2719
+ else if (content[i] === "]") depth--;
2720
+ i++;
2721
+ }
2722
+ if (depth !== 0) continue;
2723
+ const body = content.slice(bracketStart + 1, i - 1);
2724
+ const stringValues = extractStringArrayRegex(body);
2725
+ const objectCount = splitArrayObjectsRegex(body).length;
2726
+ const valueCount = Math.max(stringValues.length, objectCount);
2727
+ if (valueCount < 2) continue;
2728
+ const scope = classifyScope(relPath);
2729
+ nodes.push({
2730
+ id: `const:${constName}`,
2731
+ type: "constant",
2732
+ name: constName,
2733
+ valueCount,
2734
+ values: stringValues.length > 0 && stringValues.length <= 30 ? stringValues : void 0,
2735
+ scope,
2736
+ source: relPath
2737
+ });
2738
+ }
2739
+ }
2740
+ return { nodes };
2741
+ }
2742
+ function detect4(rootDir) {
2743
+ return (0, import_node_fs10.existsSync)((0, import_node_path10.join)(rootDir, "prisma", "schema.prisma")) || (0, import_node_fs10.existsSync)((0, import_node_path10.join)(rootDir, "prisma", "seed.ts"));
2744
+ }
2745
+ function generate4(rootDir) {
2746
+ const enumResult = extractEnumValues(rootDir);
2747
+ const seedResult = extractSeedData(rootDir);
2748
+ const constResult = extractConstants(rootDir);
2749
+ const allNodes = [...enumResult.nodes, ...seedResult.nodes, ...constResult.nodes];
2750
+ const allEdges = [...enumResult.edges, ...seedResult.edges];
2751
+ const typeOrder = {
2752
+ enum_group: 0,
2753
+ enum_value: 1,
2754
+ seed_permission: 2,
2755
+ seed_role: 3,
2756
+ seed_tag: 4,
2757
+ seed_plan: 5,
2758
+ seed_provider: 6,
2759
+ constant: 7
2760
+ };
2761
+ allNodes.sort((a, b) => {
2762
+ const ta = typeOrder[a.type] ?? 8;
2763
+ const tb = typeOrder[b.type] ?? 8;
2764
+ if (ta !== tb) return ta - tb;
2765
+ return a.name.localeCompare(b.name);
2766
+ });
2767
+ const enumGroups = allNodes.filter((n) => n.type === "enum_group").length;
2768
+ const enumValues = allNodes.filter((n) => n.type === "enum_value").length;
2769
+ const seedNodes = allNodes.filter((n) => n.type.startsWith("seed_")).length;
2770
+ const constNodes = allNodes.filter((n) => n.type === "constant").length;
2771
+ const byScope = {};
2772
+ for (const n of allNodes) {
2773
+ const s = n.scope ?? "unknown";
2774
+ byScope[s] = (byScope[s] ?? 0) + 1;
2775
+ }
2776
+ return {
2777
+ metadata: {
2778
+ generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
2779
+ scope: "static-values",
2780
+ layer: "static",
2781
+ enum_groups: enumGroups,
2782
+ enum_values: enumValues,
2783
+ seed_records: seedNodes,
2784
+ constants: constNodes,
2785
+ total_nodes: allNodes.length,
2786
+ total_edges: allEdges.length,
2787
+ parser: tryLoadTreeSitter() ? "tree-sitter" : "regex-fallback"
2788
+ },
2789
+ nodes: allNodes,
2790
+ edges: allEdges,
2791
+ cross_refs: [],
2792
+ contradictions: [],
2793
+ warnings: [],
2794
+ flagged_edges: [],
2795
+ patterns: {
2796
+ by_type: {
2797
+ enum_group: enumGroups,
2798
+ enum_value: enumValues,
2799
+ seed_permission: allNodes.filter((n) => n.type === "seed_permission").length,
2800
+ seed_role: allNodes.filter((n) => n.type === "seed_role").length,
2801
+ seed_tag: allNodes.filter((n) => n.type === "seed_tag").length,
2802
+ seed_plan: allNodes.filter((n) => n.type === "seed_plan").length,
2803
+ seed_provider: allNodes.filter((n) => n.type === "seed_provider").length,
2804
+ constant: constNodes
2805
+ },
2806
+ by_scope: byScope
2807
+ }
2808
+ };
2809
+ }
2810
+ var import_node_fs10, import_node_path10, parseCode, SHARED_MODELS, DB_MODELS, staticValuesParser;
2811
+ var init_static_values = __esm({
2812
+ "src/server/graph/parsers/static/static-values.ts"() {
2813
+ "use strict";
2814
+ import_node_fs10 = require("node:fs");
2815
+ import_node_path10 = require("node:path");
2816
+ parseCode = null;
2817
+ SHARED_MODELS = /* @__PURE__ */ new Set(["permission", "role", "tag"]);
2818
+ DB_MODELS = /* @__PURE__ */ new Set(["subscriptionPlan", "providerDefinition", "pipelineMasterTemplate"]);
2819
+ staticValuesParser = {
2820
+ id: "static-values",
2821
+ layer: "static",
2822
+ detect: detect4,
2823
+ generate: generate4
2824
+ };
2825
+ }
2826
+ });
2827
+
2828
+ // src/server/graph/parsers/crosslayer/static-ref-scanner.ts
2829
+ function walk4(dir, exts) {
2830
+ if (!(0, import_node_fs11.existsSync)(dir)) return [];
2831
+ const results = [];
2832
+ function recurse(d) {
2833
+ for (const entry of (0, import_node_fs11.readdirSync)(d, { withFileTypes: true })) {
2834
+ const full = (0, import_node_path11.join)(d, entry.name);
2835
+ if (entry.isDirectory()) {
2836
+ if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
2837
+ recurse(full);
2838
+ } else if (exts.some((ext) => entry.name.endsWith(ext))) {
2839
+ results.push(full);
2840
+ }
2841
+ }
2842
+ }
2843
+ recurse(dir);
2844
+ return results;
2845
+ }
2846
+ function isInCommentOrType(node) {
2847
+ let current = node.parent;
2848
+ while (current) {
2849
+ if (current.type === "comment" || current.type === "type_annotation" || current.type === "type_alias_declaration" || current.type === "interface_declaration" || current.type === "jsdoc") {
2850
+ return true;
2851
+ }
2852
+ current = current.parent;
2853
+ }
2854
+ return false;
2855
+ }
2856
+ function collectStaticRefs(root, valueLookup) {
2857
+ const refs = [];
2858
+ const seen = /* @__PURE__ */ new Set();
2859
+ function visit(node) {
2860
+ if (node.type === "string_fragment") {
2861
+ const val = node.text;
2862
+ if (val.length >= MIN_VALUE_LENGTH && !SKIP_VALUES.has(val.toLowerCase())) {
2863
+ const targets = valueLookup.get(val);
2864
+ if (targets && !isInCommentOrType(node)) {
2865
+ for (const t of targets) {
2866
+ const key = t;
2867
+ if (!seen.has(key)) {
2868
+ seen.add(key);
2869
+ refs.push({ value: val, targetIds: [t] });
2870
+ }
2871
+ }
2872
+ }
2873
+ }
2874
+ }
2875
+ if (node.type === "member_expression") {
2876
+ const objNode = node.namedChildren.find((c) => c.type === "identifier");
2877
+ const propNode = node.namedChildren.find((c) => c.type === "property_identifier");
2878
+ if (objNode && propNode) {
2879
+ const combined = `${objNode.text}.${propNode.text}`;
2880
+ const targets = valueLookup.get(propNode.text);
2881
+ const directTarget = valueLookup.get(combined);
2882
+ if (directTarget && !isInCommentOrType(node)) {
2883
+ for (const t of directTarget) {
2884
+ if (!seen.has(t)) {
2885
+ seen.add(t);
2886
+ refs.push({ value: combined, targetIds: [t] });
2887
+ }
2888
+ }
2889
+ } else if (targets && !isInCommentOrType(node)) {
2890
+ for (const t of targets) {
2891
+ if (!seen.has(t)) {
2892
+ seen.add(t);
2893
+ refs.push({ value: propNode.text, targetIds: [t] });
2894
+ }
2895
+ }
2896
+ }
2897
+ }
2898
+ }
2899
+ for (const child of node.namedChildren) {
2900
+ visit(child);
2901
+ }
2902
+ }
2903
+ visit(root);
2904
+ return refs;
2905
+ }
2906
+ function collectStaticRefsRegex(content, valueLookup, allValues) {
2907
+ const refs = [];
2908
+ const seen = /* @__PURE__ */ new Set();
2909
+ const escaped = allValues.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
2910
+ const pattern = new RegExp(
2911
+ `(?:['"\`])(${escaped.join("|")})(?:['"\`])|\\b(\\w+)\\.(${escaped.join("|")})\\b`,
2912
+ "g"
2913
+ );
2914
+ let match;
2915
+ pattern.lastIndex = 0;
2916
+ while ((match = pattern.exec(content)) !== null) {
2917
+ const val = match[1] ?? match[3];
2918
+ if (!val) continue;
2919
+ const targets = valueLookup.get(val);
2920
+ if (!targets) continue;
2921
+ for (const t of targets) {
2922
+ if (!seen.has(t)) {
2923
+ seen.add(t);
2924
+ refs.push({ value: val, targetIds: [t] });
2925
+ }
2926
+ }
2927
+ }
2928
+ return refs;
2929
+ }
2930
+ var import_node_fs11, import_node_path11, MIN_VALUE_LENGTH, SKIP_VALUES, staticRefScannerParser;
2931
+ var init_static_ref_scanner = __esm({
2932
+ "src/server/graph/parsers/crosslayer/static-ref-scanner.ts"() {
2933
+ "use strict";
2934
+ import_node_fs11 = require("node:fs");
2935
+ import_node_path11 = require("node:path");
2936
+ init_config();
2937
+ init_resolve_paths();
2938
+ MIN_VALUE_LENGTH = 4;
2939
+ SKIP_VALUES = /* @__PURE__ */ new Set([
2940
+ "true",
2941
+ "false",
2942
+ "null",
2943
+ "undefined",
2944
+ "none",
2945
+ "default",
2946
+ "name",
2947
+ "type",
2948
+ "data",
2949
+ "text",
2950
+ "info",
2951
+ "error",
2952
+ "open",
2953
+ "read",
2954
+ "user",
2955
+ "test",
2956
+ "json",
2957
+ "form"
2958
+ ]);
2959
+ staticRefScannerParser = {
2960
+ id: "static-ref-scanner",
2961
+ layer: "crosslayer",
2962
+ concern: "static-ref",
2963
+ detect(rootDir) {
2964
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2965
+ return paths !== null;
2966
+ },
2967
+ generate(rootDir, layerOutputs) {
2968
+ const staticOutput = layerOutputs.get("static");
2969
+ if (!staticOutput || staticOutput.nodes.length === 0) {
2970
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
2971
+ }
2972
+ const valueLookup = /* @__PURE__ */ new Map();
2973
+ for (const node of staticOutput.nodes) {
2974
+ const type = node.type;
2975
+ let valueStr = null;
2976
+ if (type === "enum_value") {
2977
+ valueStr = node.value;
2978
+ const fullId = node.id;
2979
+ if (!valueLookup.has(fullId)) valueLookup.set(fullId, []);
2980
+ valueLookup.get(fullId).push(node.id);
2981
+ } else if (type.startsWith("seed_")) {
2982
+ valueStr = node.value;
2983
+ }
2984
+ if (!valueStr || valueStr.length < MIN_VALUE_LENGTH || SKIP_VALUES.has(valueStr.toLowerCase())) continue;
2985
+ if (!valueLookup.has(valueStr)) valueLookup.set(valueStr, []);
2986
+ valueLookup.get(valueStr).push(node.id);
2987
+ }
2988
+ const allValues = [...valueLookup.keys()].sort((a, b) => b.length - a.length);
2989
+ if (allValues.length === 0) {
2990
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
2991
+ }
2992
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2993
+ if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
2994
+ const srcDir = paths.srcDir;
2995
+ const files = [
2996
+ ...walk4((0, import_node_path11.join)(srcDir, "client"), [".ts", ".tsx"]),
2997
+ ...walk4(paths.appDir, [".ts", ".tsx"]),
2998
+ ...walk4((0, import_node_path11.join)(srcDir, "server"), [".ts", ".tsx"]),
2999
+ ...walk4((0, import_node_path11.join)(srcDir, "lib"), [".ts", ".tsx"]),
3000
+ ...walk4((0, import_node_path11.join)(srcDir, "config"), [".ts", ".tsx"])
3001
+ ];
3002
+ const uiOutput = layerOutputs.get("ui");
3003
+ const apiOutput = layerOutputs.get("api");
3004
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
3005
+ const apiNodeIds = new Set(apiOutput?.nodes.map((n) => n.id) ?? []);
3006
+ let parseCode2 = null;
3007
+ try {
3008
+ const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
3009
+ if (typeof extractor.parseCodeTS === "function") {
3010
+ parseCode2 = extractor.parseCodeTS;
3011
+ }
3012
+ } catch {
3013
+ }
3014
+ const crossRefs = [];
3015
+ const seen = /* @__PURE__ */ new Set();
3016
+ let filesScanned = 0;
3017
+ for (const absPath of files) {
3018
+ const sourceId = (0, import_node_path11.relative)(srcDir, absPath).replace(/\\/g, "/");
3019
+ const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
3020
+ if (!sourceLayer) continue;
3021
+ const content = (0, import_node_fs11.readFileSync)(absPath, "utf-8");
3022
+ filesScanned++;
3023
+ let fileRefs;
3024
+ if (parseCode2) {
3025
+ try {
3026
+ const tree = parseCode2(content);
3027
+ fileRefs = collectStaticRefs(tree.rootNode, valueLookup);
3028
+ } catch {
3029
+ fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
3030
+ }
3031
+ } else {
3032
+ fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
3033
+ }
3034
+ for (const ref of fileRefs) {
3035
+ for (const targetId of ref.targetIds) {
3036
+ const key = `${sourceId}|${targetId}`;
3037
+ if (seen.has(key)) continue;
3038
+ seen.add(key);
3039
+ crossRefs.push({
3040
+ source: sourceId,
3041
+ target: targetId,
3042
+ type: "references_static",
3043
+ layer: "static"
3044
+ });
3045
+ }
3046
+ }
3047
+ }
3048
+ return {
3049
+ cross_refs: crossRefs,
3050
+ flagged_edges: [],
3051
+ warnings: [],
3052
+ patterns: {
3053
+ files_scanned: filesScanned,
3054
+ static_values_tracked: valueLookup.size,
3055
+ references_found: crossRefs.length,
3056
+ parser: parseCode2 ? "tree-sitter" : "regex-fallback"
3057
+ }
3058
+ };
3059
+ }
3060
+ };
3061
+ }
3062
+ });
3063
+
1907
3064
  // src/server/graph/core/parser-registry.ts
3065
+ function isMultiLayerParser(p) {
3066
+ return "layers" in p && Array.isArray(p.layers);
3067
+ }
1908
3068
  function registerBuiltins(registry, disabled) {
1909
3069
  const builtins = [
1910
- reactNextjsParser,
1911
- nextjsRoutesParser,
3070
+ typescriptProjectParser,
1912
3071
  prismaSchemaParser,
3072
+ sqlMigrationsParser,
3073
+ staticValuesParser,
1913
3074
  fetchResolverParser,
1914
3075
  apiAnnotationsParser,
1915
- urlLiteralScannerParser
3076
+ urlLiteralScannerParser,
3077
+ staticRefScannerParser
1916
3078
  ];
1917
3079
  for (const parser of builtins) {
1918
3080
  if (disabled.has(parser.id)) continue;
@@ -1922,16 +3084,19 @@ function registerBuiltins(registry, disabled) {
1922
3084
  function loadCustomParsers(registry, config, rootDir, disabled) {
1923
3085
  for (const entry of config.parsers?.custom ?? []) {
1924
3086
  try {
1925
- const absPath = (0, import_node_path9.resolve)(rootDir, entry.path);
3087
+ const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
1926
3088
  const mod = require(absPath);
1927
3089
  const parser = "default" in mod ? mod.default : mod;
1928
3090
  if (disabled.has(parser.id)) continue;
1929
- if (parser.layer !== entry.layer) {
3091
+ if (!isMultiLayerParser(parser) && parser.layer !== entry.layer) {
1930
3092
  process.stderr.write(
1931
3093
  `[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
1932
3094
  `
1933
3095
  );
1934
3096
  }
3097
+ if (parser.layer === "crosslayer" && entry.concern && !("concern" in parser && parser.concern)) {
3098
+ parser.concern = entry.concern;
3099
+ }
1935
3100
  registry.register(parser);
1936
3101
  } catch (err2) {
1937
3102
  process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
@@ -1946,20 +3111,23 @@ function createRegistry(config, rootDir) {
1946
3111
  loadCustomParsers(registry, config, rootDir, disabled);
1947
3112
  return registry;
1948
3113
  }
1949
- var import_node_path9, ParserRegistry;
3114
+ var import_node_path12, ParserRegistry;
1950
3115
  var init_parser_registry = __esm({
1951
3116
  "src/server/graph/core/parser-registry.ts"() {
1952
3117
  "use strict";
1953
- import_node_path9 = require("node:path");
1954
- init_react_nextjs();
1955
- init_nextjs_routes();
3118
+ import_node_path12 = require("node:path");
3119
+ init_typescript_project();
1956
3120
  init_prisma_schema();
3121
+ init_sql_migrations();
1957
3122
  init_fetch_resolver();
1958
3123
  init_api_annotations();
1959
3124
  init_url_literal_scanner();
3125
+ init_static_values();
3126
+ init_static_ref_scanner();
1960
3127
  ParserRegistry = class {
1961
3128
  constructor() {
1962
- this.parsers = /* @__PURE__ */ new Map();
3129
+ this.singleLayerParsers = /* @__PURE__ */ new Map();
3130
+ this.multiLayerParsers = [];
1963
3131
  this.ids = /* @__PURE__ */ new Set();
1964
3132
  }
1965
3133
  register(parser) {
@@ -1967,19 +3135,44 @@ var init_parser_registry = __esm({
1967
3135
  throw new Error(`Duplicate parser id: ${parser.id}`);
1968
3136
  }
1969
3137
  this.ids.add(parser.id);
1970
- const list = this.parsers.get(parser.layer) ?? [];
1971
- list.push(parser);
1972
- this.parsers.set(parser.layer, list);
3138
+ if (isMultiLayerParser(parser)) {
3139
+ this.multiLayerParsers.push(parser);
3140
+ } else {
3141
+ const list = this.singleLayerParsers.get(parser.layer) ?? [];
3142
+ list.push(parser);
3143
+ this.singleLayerParsers.set(parser.layer, list);
3144
+ }
1973
3145
  }
3146
+ /** Get single-layer parsers for a specific layer. */
1974
3147
  getParsers(layer) {
1975
- return this.parsers.get(layer) ?? [];
3148
+ return this.singleLayerParsers.get(layer) ?? [];
3149
+ }
3150
+ /** Get multi-layer parsers that can produce output for the given layer. */
3151
+ getMultiLayerParsersFor(layer) {
3152
+ return this.multiLayerParsers.filter((p) => p.layers.includes(layer));
3153
+ }
3154
+ /** Get all multi-layer parsers. */
3155
+ getMultiLayerParsers() {
3156
+ return [...this.multiLayerParsers];
1976
3157
  }
1977
3158
  getCrossLayerParsers() {
1978
- return this.parsers.get("crosslayer") ?? [];
3159
+ return this.singleLayerParsers.get("crosslayer") ?? [];
3160
+ }
3161
+ /** All layers that registered parsers can produce (single + multi). */
3162
+ getAvailableLayers() {
3163
+ const layers = /* @__PURE__ */ new Set();
3164
+ for (const key of this.singleLayerParsers.keys()) {
3165
+ if (key !== "crosslayer") layers.add(key);
3166
+ }
3167
+ for (const mp of this.multiLayerParsers) {
3168
+ for (const l of mp.layers) layers.add(l);
3169
+ }
3170
+ return [...layers];
1979
3171
  }
1980
3172
  getAll() {
1981
3173
  const all = [];
1982
- for (const list of this.parsers.values()) all.push(...list);
3174
+ for (const list of this.singleLayerParsers.values()) all.push(...list);
3175
+ all.push(...this.multiLayerParsers);
1983
3176
  return all;
1984
3177
  }
1985
3178
  };
@@ -2068,44 +3261,21 @@ function dedupCrossRefs(refs) {
2068
3261
  }
2069
3262
  return result;
2070
3263
  }
2071
- function applyCrossLayerResults(uiOutput, results, primaryId) {
2072
- const allCrossRefs = [...uiOutput.cross_refs];
2073
- const allFlagged = [...uiOutput.flagged_edges];
2074
- const allWarnings = [...uiOutput.warnings];
2075
- const primaryResult = results.find((r) => r.parserId === primaryId);
2076
- const secondaryResults = results.filter((r) => r.parserId !== primaryId);
2077
- if (primaryResult) {
2078
- allCrossRefs.push(...primaryResult.output.cross_refs);
2079
- allFlagged.push(...primaryResult.output.flagged_edges);
2080
- allWarnings.push(...primaryResult.output.warnings);
2081
- }
2082
- const primarySet = new Set(
2083
- (primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
2084
- );
2085
- for (const sec of secondaryResults) {
2086
- for (const ref of sec.output.cross_refs) {
2087
- const key = `${ref.source}|${ref.target}|${ref.type}`;
2088
- if (primarySet.has(key)) {
2089
- allCrossRefs.push(ref);
2090
- } else {
2091
- allFlagged.push({
2092
- source: ref.source,
2093
- target: ref.target,
2094
- type: "out_of_pattern",
2095
- label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
2096
- confidence: "medium"
2097
- });
2098
- allCrossRefs.push(ref);
2099
- }
2100
- }
2101
- allFlagged.push(...sec.output.flagged_edges);
2102
- allWarnings.push(...sec.output.warnings);
2103
- }
3264
+ function applyCrossLayerResults(uiOutput, results) {
2104
3265
  return {
2105
3266
  ...uiOutput,
2106
- cross_refs: dedupCrossRefs(allCrossRefs),
2107
- flagged_edges: allFlagged,
2108
- warnings: allWarnings
3267
+ cross_refs: dedupCrossRefs([
3268
+ ...uiOutput.cross_refs,
3269
+ ...results.flatMap((r) => r.output.cross_refs)
3270
+ ]),
3271
+ flagged_edges: [
3272
+ ...uiOutput.flagged_edges,
3273
+ ...results.flatMap((r) => r.output.flagged_edges)
3274
+ ],
3275
+ warnings: [
3276
+ ...uiOutput.warnings,
3277
+ ...results.flatMap((r) => r.output.warnings)
3278
+ ]
2109
3279
  };
2110
3280
  }
2111
3281
  var init_merge = __esm({
@@ -2116,10 +3286,10 @@ var init_merge = __esm({
2116
3286
 
2117
3287
  // src/server/graph/core/graph-builder.ts
2118
3288
  function readGraphFromDisk(rootDir, layer) {
2119
- const filePath = (0, import_node_path10.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
2120
- if (!(0, import_node_fs9.existsSync)(filePath)) return null;
3289
+ const filePath = (0, import_node_path13.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
3290
+ if (!(0, import_node_fs12.existsSync)(filePath)) return null;
2121
3291
  try {
2122
- return JSON.parse((0, import_node_fs9.readFileSync)(filePath, "utf-8"));
3292
+ return JSON.parse((0, import_node_fs12.readFileSync)(filePath, "utf-8"));
2123
3293
  } catch {
2124
3294
  return null;
2125
3295
  }
@@ -2127,26 +3297,31 @@ function readGraphFromDisk(rootDir, layer) {
2127
3297
  function generateLayer(rootDir, layer) {
2128
3298
  const config = loadConfig(rootDir);
2129
3299
  const registry = createRegistry(config, rootDir);
2130
- const parsers = registry.getParsers(layer);
2131
3300
  const outputs = [];
2132
- for (const parser of parsers) {
3301
+ for (const parser of registry.getParsers(layer)) {
2133
3302
  if (!parser.detect(rootDir)) continue;
2134
3303
  outputs.push(parser.generate(rootDir));
2135
3304
  }
3305
+ for (const mp of registry.getMultiLayerParsersFor(layer)) {
3306
+ if (!mp.detect(rootDir)) continue;
3307
+ const multiOutput = mp.generate(rootDir);
3308
+ const layerOutput = multiOutput.get(layer);
3309
+ if (layerOutput) outputs.push(layerOutput);
3310
+ }
2136
3311
  if (outputs.length === 0) return null;
2137
3312
  let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
2138
3313
  if (layer === "ui") {
2139
3314
  const layerOutputs = /* @__PURE__ */ new Map();
2140
3315
  layerOutputs.set("ui", merged);
2141
- for (const otherLayer of ["api", "db"]) {
3316
+ for (const otherLayer of registry.getAvailableLayers()) {
3317
+ if (otherLayer === "ui") continue;
2142
3318
  const existing = readGraphFromDisk(rootDir, otherLayer);
2143
3319
  if (existing) layerOutputs.set(otherLayer, existing);
2144
3320
  }
2145
3321
  const crossParsers = registry.getCrossLayerParsers();
2146
- const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
2147
3322
  const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
2148
3323
  if (crossResults.length > 0) {
2149
- merged = applyCrossLayerResults(merged, crossResults, primaryId);
3324
+ merged = applyCrossLayerResults(merged, crossResults);
2150
3325
  }
2151
3326
  }
2152
3327
  return {
@@ -2159,16 +3334,29 @@ function generateLayer(rootDir, layer) {
2159
3334
  function generateAll(rootDir) {
2160
3335
  const config = loadConfig(rootDir);
2161
3336
  const registry = createRegistry(config, rootDir);
2162
- const layerOrder = ["api", "db", "ui"];
3337
+ const allLayers = registry.getAvailableLayers();
3338
+ const layerOrder = [
3339
+ ...allLayers.filter((l) => l !== "ui"),
3340
+ ...allLayers.filter((l) => l === "ui")
3341
+ ];
2163
3342
  const layerOutputs = /* @__PURE__ */ new Map();
2164
3343
  const results = [];
3344
+ const multiLayerResults = /* @__PURE__ */ new Map();
2165
3345
  for (const layer of layerOrder) {
2166
- const parsers = registry.getParsers(layer);
2167
3346
  const outputs = [];
2168
- for (const parser of parsers) {
3347
+ for (const parser of registry.getParsers(layer)) {
2169
3348
  if (!parser.detect(rootDir)) continue;
2170
3349
  outputs.push(parser.generate(rootDir));
2171
3350
  }
3351
+ for (const mp of registry.getMultiLayerParsersFor(layer)) {
3352
+ if (!mp.detect(rootDir)) continue;
3353
+ if (!multiLayerResults.has(mp.id)) {
3354
+ multiLayerResults.set(mp.id, mp.generate(rootDir));
3355
+ }
3356
+ const cached = multiLayerResults.get(mp.id);
3357
+ const layerOutput = cached.get(layer);
3358
+ if (layerOutput) outputs.push(layerOutput);
3359
+ }
2172
3360
  if (outputs.length === 0) continue;
2173
3361
  const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
2174
3362
  layerOutputs.set(layer, merged);
@@ -2180,11 +3368,10 @@ function generateAll(rootDir) {
2180
3368
  });
2181
3369
  }
2182
3370
  const crossParsers = registry.getCrossLayerParsers();
2183
- const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
2184
3371
  const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
2185
3372
  if (crossResults.length > 0 && layerOutputs.has("ui")) {
2186
3373
  const uiOutput = layerOutputs.get("ui");
2187
- const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
3374
+ const merged = applyCrossLayerResults(uiOutput, crossResults);
2188
3375
  layerOutputs.set("ui", merged);
2189
3376
  const uiResult = results.find((r) => r.layer === "ui");
2190
3377
  if (uiResult) {
@@ -2194,14 +3381,16 @@ function generateAll(rootDir) {
2194
3381
  }
2195
3382
  }
2196
3383
  const byLayer = new Map(results.map((r) => [r.layer, r]));
2197
- return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
3384
+ const wellKnownOrder = ["ui", "api", "db"];
3385
+ const extras = [...byLayer.keys()].filter((l) => !wellKnownOrder.includes(l)).sort();
3386
+ return [...wellKnownOrder, ...extras].map((l) => byLayer.get(l)).filter((r) => !!r);
2198
3387
  }
2199
- var import_node_fs9, import_node_path10;
3388
+ var import_node_fs12, import_node_path13;
2200
3389
  var init_graph_builder = __esm({
2201
3390
  "src/server/graph/core/graph-builder.ts"() {
2202
3391
  "use strict";
2203
- import_node_fs9 = require("node:fs");
2204
- import_node_path10 = require("node:path");
3392
+ import_node_fs12 = require("node:fs");
3393
+ import_node_path13 = require("node:path");
2205
3394
  init_config();
2206
3395
  init_parser_registry();
2207
3396
  init_merge();
@@ -2240,18 +3429,18 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
2240
3429
  const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
2241
3430
  const searchDirs = [
2242
3431
  rootDir,
2243
- (0, import_node_path11.join)(rootDir, "src"),
2244
- (0, import_node_path11.join)(rootDir, "app"),
2245
- (0, import_node_path11.join)(rootDir, "lib")
3432
+ (0, import_node_path14.join)(rootDir, "src"),
3433
+ (0, import_node_path14.join)(rootDir, "app"),
3434
+ (0, import_node_path14.join)(rootDir, "lib")
2246
3435
  ];
2247
3436
  for (const base of searchDirs) {
2248
3437
  for (const convention of conventionDirs) {
2249
- const dir = (0, import_node_path11.join)(base, convention);
2250
- if (!(0, import_node_fs10.existsSync)(dir)) continue;
3438
+ const dir = (0, import_node_path14.join)(base, convention);
3439
+ if (!(0, import_node_fs13.existsSync)(dir)) continue;
2251
3440
  try {
2252
- const stat = (0, import_node_fs10.statSync)(dir);
3441
+ const stat = (0, import_node_fs13.statSync)(dir);
2253
3442
  if (!stat.isDirectory()) continue;
2254
- const entries = (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
3443
+ const entries = (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
2255
3444
  if (entries.length > 0) {
2256
3445
  const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
2257
3446
  result.set(relPath, entries);
@@ -2323,12 +3512,12 @@ function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
2323
3512
  }
2324
3513
  return "root";
2325
3514
  }
2326
- var import_node_fs10, import_node_path11, CONVENTION_DIRS_BUILTIN, GENERIC_ROLE_NAMES_BUILTIN, SKIP_SEGMENTS_BUILTIN, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
3515
+ var import_node_fs13, import_node_path14, CONVENTION_DIRS_BUILTIN, GENERIC_ROLE_NAMES_BUILTIN, SKIP_SEGMENTS_BUILTIN, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
2327
3516
  var init_module_tagger = __esm({
2328
3517
  "src/server/graph/taggers/module-tagger.ts"() {
2329
3518
  "use strict";
2330
- import_node_fs10 = require("node:fs");
2331
- import_node_path11 = require("node:path");
3519
+ import_node_fs13 = require("node:fs");
3520
+ import_node_path14 = require("node:path");
2332
3521
  CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
2333
3522
  GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
2334
3523
  // JS/TS
@@ -2540,7 +3729,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
2540
3729
  for (const entry of config.taggers?.custom ?? []) {
2541
3730
  if (disabled.has(entry.id)) continue;
2542
3731
  try {
2543
- const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
3732
+ const absPath = (0, import_node_path15.resolve)(rootDir, entry.path);
2544
3733
  const mod = require(absPath);
2545
3734
  const tagger = "default" in mod ? mod.default : mod;
2546
3735
  const override = config.taggers?.trackUntagged?.[tagger.id];
@@ -2561,11 +3750,11 @@ function createTaggerRegistry(config, rootDir) {
2561
3750
  loadCustomTaggers(registry, config, rootDir, disabled);
2562
3751
  return registry;
2563
3752
  }
2564
- var import_node_path12, TaggerRegistry, BUILTIN_TAGGERS;
3753
+ var import_node_path15, TaggerRegistry, BUILTIN_TAGGERS;
2565
3754
  var init_tagger_registry = __esm({
2566
3755
  "src/server/graph/core/tagger-registry.ts"() {
2567
3756
  "use strict";
2568
- import_node_path12 = require("node:path");
3757
+ import_node_path15 = require("node:path");
2569
3758
  init_module_tagger();
2570
3759
  init_screen_tagger();
2571
3760
  TaggerRegistry = class {
@@ -2593,18 +3782,18 @@ var init_tagger_registry = __esm({
2593
3782
 
2594
3783
  // src/server/graph/core/tag-store.ts
2595
3784
  function tagsFilePath(rootDir) {
2596
- return (0, import_node_path13.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
3785
+ return (0, import_node_path16.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
2597
3786
  }
2598
3787
  function readTagStore(rootDir) {
2599
3788
  const filePath = tagsFilePath(rootDir);
2600
- if (!(0, import_node_fs11.existsSync)(filePath)) return {};
2601
- const stat = (0, import_node_fs11.statSync)(filePath);
3789
+ if (!(0, import_node_fs14.existsSync)(filePath)) return {};
3790
+ const stat = (0, import_node_fs14.statSync)(filePath);
2602
3791
  const cached = tagCache.get(filePath);
2603
3792
  if (cached && cached.mtimeMs === stat.mtimeMs) {
2604
3793
  return cached.store;
2605
3794
  }
2606
3795
  try {
2607
- const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
3796
+ const content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
2608
3797
  const store = JSON.parse(content);
2609
3798
  tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
2610
3799
  return store;
@@ -2614,15 +3803,15 @@ function readTagStore(rootDir) {
2614
3803
  }
2615
3804
  function writeTagStore(rootDir, store) {
2616
3805
  const filePath = tagsFilePath(rootDir);
2617
- const dir = (0, import_node_path13.dirname)(filePath);
2618
- (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
3806
+ const dir = (0, import_node_path16.dirname)(filePath);
3807
+ (0, import_node_fs14.mkdirSync)(dir, { recursive: true });
2619
3808
  const cleaned = {};
2620
3809
  for (const [nodeId, tags] of Object.entries(store)) {
2621
3810
  if (Object.keys(tags).length > 0) {
2622
3811
  cleaned[nodeId] = tags;
2623
3812
  }
2624
3813
  }
2625
- (0, import_node_fs11.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
3814
+ (0, import_node_fs14.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
2626
3815
  tagCache.delete(filePath);
2627
3816
  }
2628
3817
  function setTag(rootDir, nodeId, key, value) {
@@ -2640,12 +3829,12 @@ function removeTag(rootDir, nodeId, key) {
2640
3829
  }
2641
3830
  writeTagStore(rootDir, store);
2642
3831
  }
2643
- var import_node_fs11, import_node_path13, TAGS_FILENAME, GRAPHS_DIR, tagCache;
3832
+ var import_node_fs14, import_node_path16, TAGS_FILENAME, GRAPHS_DIR, tagCache;
2644
3833
  var init_tag_store = __esm({
2645
3834
  "src/server/graph/core/tag-store.ts"() {
2646
3835
  "use strict";
2647
- import_node_fs11 = require("node:fs");
2648
- import_node_path13 = require("node:path");
3836
+ import_node_fs14 = require("node:fs");
3837
+ import_node_path16 = require("node:path");
2649
3838
  TAGS_FILENAME = "tags.json";
2650
3839
  GRAPHS_DIR = ".launchsecure/graphs";
2651
3840
  tagCache = /* @__PURE__ */ new Map();
@@ -2653,18 +3842,23 @@ var init_tag_store = __esm({
2653
3842
  });
2654
3843
 
2655
3844
  // src/server/graph/index.ts
3845
+ function getAvailableLayers(rootDir) {
3846
+ const dir = (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
3847
+ if (!(0, import_node_fs15.existsSync)(dir)) return [];
3848
+ return (0, import_node_fs15.readdirSync)(dir).filter((f) => f.endsWith(".json") && f !== "tags.json").map((f) => f.replace(".json", ""));
3849
+ }
2656
3850
  function graphsDir(rootDir) {
2657
- return (0, import_node_path14.join)(rootDir, GRAPHS_DIR2);
3851
+ return (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
2658
3852
  }
2659
3853
  function graphFilePath(rootDir, layer) {
2660
- return (0, import_node_path14.join)(graphsDir(rootDir), `${layer}.json`);
3854
+ return (0, import_node_path17.join)(graphsDir(rootDir), `${layer}.json`);
2661
3855
  }
2662
3856
  function tagsFilePath2(rootDir) {
2663
- return (0, import_node_path14.join)(graphsDir(rootDir), "tags.json");
3857
+ return (0, import_node_path17.join)(graphsDir(rootDir), "tags.json");
2664
3858
  }
2665
3859
  function getMtimeMs(filePath) {
2666
- if (!(0, import_node_fs12.existsSync)(filePath)) return 0;
2667
- return (0, import_node_fs12.statSync)(filePath).mtimeMs;
3860
+ if (!(0, import_node_fs15.existsSync)(filePath)) return 0;
3861
+ return (0, import_node_fs15.statSync)(filePath).mtimeMs;
2668
3862
  }
2669
3863
  function invalidateCache(filePath) {
2670
3864
  graphCache.delete(filePath);
@@ -2703,20 +3897,20 @@ function applyTags(graph, layer, rootDir) {
2703
3897
  }
2704
3898
  function readGraphRaw(rootDir, layer) {
2705
3899
  const filePath = graphFilePath(rootDir, layer);
2706
- if (!(0, import_node_fs12.existsSync)(filePath)) return null;
2707
- const stat = (0, import_node_fs12.statSync)(filePath);
3900
+ if (!(0, import_node_fs15.existsSync)(filePath)) return null;
3901
+ const stat = (0, import_node_fs15.statSync)(filePath);
2708
3902
  const cached = graphCache.get(filePath);
2709
3903
  if (cached && cached.mtimeMs === stat.mtimeMs) {
2710
3904
  return cached.graph;
2711
3905
  }
2712
- const content = (0, import_node_fs12.readFileSync)(filePath, "utf-8");
3906
+ const content = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
2713
3907
  const graph = JSON.parse(content);
2714
3908
  graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
2715
3909
  return graph;
2716
3910
  }
2717
3911
  function readGraph(rootDir, layer) {
2718
3912
  const rawFilePath = graphFilePath(rootDir, layer);
2719
- if (!(0, import_node_fs12.existsSync)(rawFilePath)) return null;
3913
+ if (!(0, import_node_fs15.existsSync)(rawFilePath)) return null;
2720
3914
  const rawMtime = getMtimeMs(rawFilePath);
2721
3915
  const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
2722
3916
  const cacheKey = `${rootDir}:${layer}`;
@@ -2732,7 +3926,7 @@ function readGraph(rootDir, layer) {
2732
3926
  }
2733
3927
  function readAllGraphs(rootDir) {
2734
3928
  const result = {};
2735
- for (const layer of LAYERS) {
3929
+ for (const layer of getAvailableLayers(rootDir)) {
2736
3930
  const graph = readGraph(rootDir, layer);
2737
3931
  if (graph) result[layer] = graph;
2738
3932
  }
@@ -2746,22 +3940,22 @@ async function generateGraph(rootDir, layer) {
2746
3940
  mutationMethods: config.parsers?.patterns?.mutationMethods
2747
3941
  });
2748
3942
  const dir = graphsDir(rootDir);
2749
- (0, import_node_fs12.mkdirSync)(dir, { recursive: true });
3943
+ (0, import_node_fs15.mkdirSync)(dir, { recursive: true });
2750
3944
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
2751
3945
  for (const result of results) {
2752
3946
  const filePath = graphFilePath(rootDir, result.layer);
2753
- (0, import_node_fs12.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
3947
+ (0, import_node_fs15.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
2754
3948
  invalidateCache(filePath);
2755
3949
  invalidateTaggedCache(rootDir, result.layer);
2756
3950
  }
2757
3951
  return results;
2758
3952
  }
2759
- var import_node_fs12, import_node_path14, GRAPHS_DIR2, LAYERS, graphCache, taggedCache;
3953
+ var import_node_fs15, import_node_path17, GRAPHS_DIR2, graphCache, taggedCache;
2760
3954
  var init_graph = __esm({
2761
3955
  "src/server/graph/index.ts"() {
2762
3956
  "use strict";
2763
- import_node_fs12 = require("node:fs");
2764
- import_node_path14 = require("node:path");
3957
+ import_node_fs15 = require("node:fs");
3958
+ import_node_path17 = require("node:path");
2765
3959
  init_graph_builder();
2766
3960
  init_config();
2767
3961
  init_tagger_registry();
@@ -2769,12 +3963,294 @@ var init_graph = __esm({
2769
3963
  init_ts_extractor();
2770
3964
  init_tag_store();
2771
3965
  GRAPHS_DIR2 = ".launchsecure/graphs";
2772
- LAYERS = ["ui", "api", "db"];
2773
3966
  graphCache = /* @__PURE__ */ new Map();
2774
3967
  taggedCache = /* @__PURE__ */ new Map();
2775
3968
  }
2776
3969
  });
2777
3970
 
3971
+ // src/server/graph/core/audit-core.ts
3972
+ function readGraphFile(rootDir, layer) {
3973
+ const filePath = (0, import_node_path18.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
3974
+ if (!(0, import_node_fs16.existsSync)(filePath)) return null;
3975
+ try {
3976
+ return JSON.parse((0, import_node_fs16.readFileSync)(filePath, "utf-8"));
3977
+ } catch {
3978
+ return null;
3979
+ }
3980
+ }
3981
+ function checkSchemaDrift(rootDir) {
3982
+ const findings = [];
3983
+ const db = readGraphFile(rootDir, "db");
3984
+ if (!db) {
3985
+ 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." });
3986
+ return buildReport("db", "schema_drift", findings);
3987
+ }
3988
+ for (const c of db.contradictions ?? []) {
3989
+ const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
3990
+ findings.push({
3991
+ id: `drift:${c.entity}`,
3992
+ severity: isTableLevel ? "error" : "warning",
3993
+ category: "schema_drift",
3994
+ title: c.entity,
3995
+ detail: c.detail
3996
+ });
3997
+ }
3998
+ return buildReport("db", "schema_drift", findings);
3999
+ }
4000
+ function checkOrphanFks(rootDir) {
4001
+ const findings = [];
4002
+ const db = readGraphFile(rootDir, "db");
4003
+ if (!db) return buildReport("db", "orphan_fks", findings);
4004
+ for (const f of db.flagged_edges ?? []) {
4005
+ findings.push({
4006
+ id: `fk:${f.source}->${f.target}`,
4007
+ severity: "warning",
4008
+ category: "orphan_fks",
4009
+ title: `${f.source} \u2192 ${f.target}`,
4010
+ detail: f.label
4011
+ });
4012
+ }
4013
+ return buildReport("db", "orphan_fks", findings);
4014
+ }
4015
+ function checkUnprotectedRoutes(rootDir) {
4016
+ const findings = [];
4017
+ const api = readGraphFile(rootDir, "api");
4018
+ const staticGraph = readGraphFile(rootDir, "static");
4019
+ if (!api) return buildReport("api", "unprotected_routes", findings);
4020
+ const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
4021
+ let routePermsContent = "";
4022
+ if ((0, import_node_fs16.existsSync)(routePermsPath)) {
4023
+ routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
4024
+ }
4025
+ const registeredRoutes = /* @__PURE__ */ new Set();
4026
+ const routeEntryRe = /path:\s*'([^']+)'/g;
4027
+ let rm;
4028
+ while ((rm = routeEntryRe.exec(routePermsContent)) !== null) {
4029
+ registeredRoutes.add(rm[1].replace(/:(\w+)/g, "[$1]"));
4030
+ }
4031
+ for (const node of api.nodes) {
4032
+ if (node.type !== "endpoint") continue;
4033
+ const route = node.route ?? "";
4034
+ if (!route) continue;
4035
+ const normalized = "/api" + (route.startsWith("/") ? route : "/" + route);
4036
+ const isRegistered = registeredRoutes.has(normalized) || [...registeredRoutes].some((r) => routeMatchesPattern(normalized, r));
4037
+ if (!isRegistered) {
4038
+ const methods = node.methods ?? [];
4039
+ findings.push({
4040
+ id: `unprotected:${node.id}`,
4041
+ severity: "warning",
4042
+ category: "unprotected_routes",
4043
+ title: `${methods.join(",")} ${route}`,
4044
+ detail: `API endpoint has no entry in ROUTE_PERMISSIONS. Methods: ${methods.join(", ")}`,
4045
+ file: node.id
4046
+ });
4047
+ }
4048
+ }
4049
+ return buildReport("api", "unprotected_routes", findings);
4050
+ }
4051
+ function routeMatchesPattern(route, pattern) {
4052
+ const routeParts = route.split("/");
4053
+ const patternParts = pattern.split("/");
4054
+ if (routeParts.length !== patternParts.length) return false;
4055
+ for (let i = 0; i < routeParts.length; i++) {
4056
+ const rp = routeParts[i];
4057
+ const pp = patternParts[i];
4058
+ if (rp === pp) continue;
4059
+ if (pp.startsWith("[") || pp.startsWith(":")) continue;
4060
+ if (rp.startsWith("[") || rp.startsWith(":")) continue;
4061
+ return false;
4062
+ }
4063
+ return true;
4064
+ }
4065
+ function checkDeadScreens(rootDir) {
4066
+ const findings = [];
4067
+ const ui = readGraphFile(rootDir, "ui");
4068
+ if (!ui) return buildReport("ui", "dead_screens", findings);
4069
+ const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
4070
+ const navTargets = /* @__PURE__ */ new Set();
4071
+ for (const e of ui.edges) {
4072
+ if (e.type === "navigates" || e.type === "renders" || e.type === "imports") {
4073
+ navTargets.add(e.target);
4074
+ }
4075
+ }
4076
+ for (const cr of ui.cross_refs ?? []) {
4077
+ navTargets.add(cr.target);
4078
+ }
4079
+ for (const page of pages) {
4080
+ if (page.id.endsWith("layout.tsx") && page.id.split("/").length <= 2) continue;
4081
+ if (["error.tsx", "loading.tsx", "not-found.tsx", "template.tsx"].some((s) => page.id.endsWith(s))) continue;
4082
+ if (!navTargets.has(page.id)) {
4083
+ findings.push({
4084
+ id: `dead:${page.id}`,
4085
+ severity: "info",
4086
+ category: "dead_screens",
4087
+ title: page.name,
4088
+ detail: `Page "${page.id}" has no incoming navigation, render, or import edges.`,
4089
+ file: page.id
4090
+ });
4091
+ }
4092
+ }
4093
+ return buildReport("ui", "dead_screens", findings);
4094
+ }
4095
+ function checkUnenforcedPermissions(rootDir) {
4096
+ const findings = [];
4097
+ const staticGraph = readGraphFile(rootDir, "static");
4098
+ if (!staticGraph) return buildReport("static", "unenforced_permissions", findings);
4099
+ const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
4100
+ const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
4101
+ let routePermsContent = "";
4102
+ if ((0, import_node_fs16.existsSync)(routePermsPath)) {
4103
+ routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
4104
+ }
4105
+ for (const perm of permissions) {
4106
+ const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
4107
+ if (!regex.test(routePermsContent)) {
4108
+ findings.push({
4109
+ id: `unenforced:${perm.key}`,
4110
+ severity: "warning",
4111
+ category: "unenforced_permissions",
4112
+ title: `${perm.name} (${perm.key})`,
4113
+ detail: `Permission "${perm.key}" exists in seed data but has no entry in ROUTE_PERMISSIONS \u2014 no API route requires it.`
4114
+ });
4115
+ }
4116
+ }
4117
+ return buildReport("static", "unenforced_permissions", findings);
4118
+ }
4119
+ function checkHardcodedValues(rootDir) {
4120
+ const findings = [];
4121
+ const staticGraph = readGraphFile(rootDir, "static");
4122
+ if (!staticGraph) return buildReport("static", "hardcoded_values", findings);
4123
+ const knownValues = /* @__PURE__ */ new Set();
4124
+ for (const n of staticGraph.nodes) {
4125
+ if (n.type === "enum_value") knownValues.add(n.value);
4126
+ }
4127
+ const api = readGraphFile(rootDir, "api");
4128
+ if (!api) return buildReport("static", "hardcoded_values", findings);
4129
+ const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
4130
+ const seen = /* @__PURE__ */ new Set();
4131
+ for (const node of api.nodes) {
4132
+ if (node.type !== "endpoint") continue;
4133
+ const filePath = (0, import_node_path18.join)(rootDir, "src", node.id);
4134
+ if (!(0, import_node_fs16.existsSync)(filePath)) continue;
4135
+ const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
4136
+ let m;
4137
+ allCapsRe.lastIndex = 0;
4138
+ while ((m = allCapsRe.exec(content)) !== null) {
4139
+ const val = m[1];
4140
+ if (knownValues.has(val)) continue;
4141
+ if (["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "UTF", "NULL", "TRUE", "FALSE", "JSON", "HTML", "CSS", "ENV"].includes(val)) continue;
4142
+ const key = `${node.id}:${val}`;
4143
+ if (seen.has(key)) continue;
4144
+ seen.add(key);
4145
+ findings.push({
4146
+ id: `hardcoded:${key}`,
4147
+ severity: "info",
4148
+ category: "hardcoded_values",
4149
+ title: `"${val}" in ${node.id}`,
4150
+ detail: `ALL_CAPS string literal "${val}" appears in API code but is not in the enum/seed inventory. May be an unregistered constant.`,
4151
+ file: node.id
4152
+ });
4153
+ }
4154
+ }
4155
+ return buildReport("static", "hardcoded_values", findings);
4156
+ }
4157
+ function buildReport(layer, check, findings) {
4158
+ return {
4159
+ layer,
4160
+ check,
4161
+ findings,
4162
+ summary: {
4163
+ errors: findings.filter((f) => f.severity === "error").length,
4164
+ warnings: findings.filter((f) => f.severity === "warning").length,
4165
+ info: findings.filter((f) => f.severity === "info").length
4166
+ },
4167
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4168
+ };
4169
+ }
4170
+ function getAvailableChecks() {
4171
+ const result = {};
4172
+ for (const [layer, checks] of Object.entries(CHECKS)) {
4173
+ result[layer] = Object.keys(checks);
4174
+ }
4175
+ return result;
4176
+ }
4177
+ function runAudit(rootDir, layer, check) {
4178
+ if (layer === "all") {
4179
+ const reports = [];
4180
+ for (const [l, checks] of Object.entries(CHECKS)) {
4181
+ for (const fn of Object.values(checks)) {
4182
+ reports.push(fn(rootDir));
4183
+ }
4184
+ }
4185
+ return reports;
4186
+ }
4187
+ const layerChecks = CHECKS[layer];
4188
+ if (!layerChecks) {
4189
+ return [{
4190
+ layer,
4191
+ check: "unknown",
4192
+ findings: [{ id: "invalid-layer", severity: "error", category: "system", title: "Invalid layer", detail: `Layer "${layer}" has no audit checks. Available: ${Object.keys(CHECKS).join(", ")}` }],
4193
+ summary: { errors: 1, warnings: 0, info: 0 },
4194
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4195
+ }];
4196
+ }
4197
+ if (check) {
4198
+ const fn = layerChecks[check];
4199
+ if (!fn) {
4200
+ return [{
4201
+ layer,
4202
+ check,
4203
+ findings: [{ id: "invalid-check", severity: "error", category: "system", title: "Invalid check", detail: `Check "${check}" not found for layer "${layer}". Available: ${Object.keys(layerChecks).join(", ")}` }],
4204
+ summary: { errors: 1, warnings: 0, info: 0 },
4205
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4206
+ }];
4207
+ }
4208
+ return [fn(rootDir)];
4209
+ }
4210
+ return Object.values(layerChecks).map((fn) => fn(rootDir));
4211
+ }
4212
+ function formatAsPrompt(reports) {
4213
+ const lines = [];
4214
+ for (const report of reports) {
4215
+ if (report.findings.length === 0) continue;
4216
+ lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
4217
+ lines.push("");
4218
+ for (const f of report.findings) {
4219
+ const tag = f.severity === "error" ? "ERROR" : f.severity === "warning" ? "WARNING" : "INFO";
4220
+ const filePart = f.file ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : "";
4221
+ lines.push(`- [${tag}] ${f.title}${filePart}`);
4222
+ lines.push(` ${f.detail}`);
4223
+ }
4224
+ lines.push("");
4225
+ }
4226
+ if (lines.length === 0) return "No audit findings.";
4227
+ return lines.join("\n");
4228
+ }
4229
+ var import_node_fs16, import_node_path18, CHECKS;
4230
+ var init_audit_core = __esm({
4231
+ "src/server/graph/core/audit-core.ts"() {
4232
+ "use strict";
4233
+ import_node_fs16 = require("node:fs");
4234
+ import_node_path18 = require("node:path");
4235
+ CHECKS = {
4236
+ db: {
4237
+ schema_drift: checkSchemaDrift,
4238
+ orphan_fks: checkOrphanFks
4239
+ },
4240
+ api: {
4241
+ unprotected_routes: checkUnprotectedRoutes
4242
+ },
4243
+ ui: {
4244
+ dead_screens: checkDeadScreens
4245
+ },
4246
+ static: {
4247
+ unenforced_permissions: checkUnenforcedPermissions,
4248
+ hardcoded_values: checkHardcodedValues
4249
+ }
4250
+ };
4251
+ }
4252
+ });
4253
+
2778
4254
  // src/server/chart-serve.ts
2779
4255
  var chart_serve_exports = {};
2780
4256
  __export(chart_serve_exports, {
@@ -2787,31 +4263,39 @@ function randomPort() {
2787
4263
  function findProjectRoot(startDir) {
2788
4264
  let dir = startDir;
2789
4265
  for (let i = 0; i < 8; i++) {
2790
- const graphsDir2 = import_node_path15.default.join(dir, ".launchsecure", "graphs");
2791
- 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;
2792
- const parent = import_node_path15.default.dirname(dir);
4266
+ const graphsDir2 = import_node_path19.default.join(dir, ".launchsecure", "graphs");
4267
+ 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;
4268
+ const parent = import_node_path19.default.dirname(dir);
2793
4269
  if (parent === dir) break;
2794
4270
  dir = parent;
2795
4271
  }
2796
4272
  dir = startDir;
2797
4273
  for (let i = 0; i < 8; i++) {
2798
- if (import_node_fs13.default.existsSync(import_node_path15.default.join(dir, ".git"))) return dir;
2799
- const parent = import_node_path15.default.dirname(dir);
4274
+ if (import_node_fs17.default.existsSync(import_node_path19.default.join(dir, ".git"))) return dir;
4275
+ const parent = import_node_path19.default.dirname(dir);
2800
4276
  if (parent === dir) break;
2801
4277
  dir = parent;
2802
4278
  }
2803
4279
  return startDir;
2804
4280
  }
2805
- async function buildMergedGraph(projectRoot) {
2806
- let graphs = readAllGraphs(projectRoot);
2807
- if (!graphs.ui && !graphs.api && !graphs.db) {
2808
- await generateGraph(projectRoot);
2809
- graphs = readAllGraphs(projectRoot);
4281
+ function resolveRequestRoot(url, monorepoRoot, projects) {
4282
+ const projectParam = url.searchParams.get("project");
4283
+ if (!projectParam || projects.length === 0) return monorepoRoot;
4284
+ const resolved = import_node_path19.default.resolve(monorepoRoot, projectParam);
4285
+ if (!resolved.startsWith(monorepoRoot)) {
4286
+ throw new Error("Project path outside monorepo root");
4287
+ }
4288
+ return resolved;
4289
+ }
4290
+ async function buildMergedGraph(root) {
4291
+ let graphs = readAllGraphs(root);
4292
+ if (Object.keys(graphs).length === 0) {
4293
+ await generateGraph(root);
4294
+ graphs = readAllGraphs(root);
2810
4295
  }
2811
4296
  const nodes = [];
2812
4297
  const rawLinks = [];
2813
- const LAYERS2 = ["ui", "api", "db"];
2814
- for (const layer of LAYERS2) {
4298
+ for (const layer of Object.keys(graphs)) {
2815
4299
  const g = graphs[layer];
2816
4300
  if (!g) continue;
2817
4301
  for (const n of g.nodes) {
@@ -2848,16 +4332,16 @@ async function buildMergedGraph(projectRoot) {
2848
4332
  };
2849
4333
  }
2850
4334
  function serveStatic(res, filePath) {
2851
- if (!import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) return false;
2852
- const ext = import_node_path15.default.extname(filePath).toLowerCase();
4335
+ if (!import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) return false;
4336
+ const ext = import_node_path19.default.extname(filePath).toLowerCase();
2853
4337
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
2854
4338
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
2855
- import_node_fs13.default.createReadStream(filePath).pipe(res);
4339
+ import_node_fs17.default.createReadStream(filePath).pipe(res);
2856
4340
  return true;
2857
4341
  }
2858
4342
  function serveIndex(res, clientDir) {
2859
- const indexPath = import_node_path15.default.join(clientDir, "index.html");
2860
- if (!import_node_fs13.default.existsSync(indexPath)) {
4343
+ const indexPath = import_node_path19.default.join(clientDir, "index.html");
4344
+ if (!import_node_fs17.default.existsSync(indexPath)) {
2861
4345
  res.writeHead(500, { "Content-Type": "text/plain" });
2862
4346
  res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
2863
4347
  return;
@@ -2909,19 +4393,40 @@ async function startChartServer(opts = {}) {
2909
4393
  }
2910
4394
  return { port: existing.port, url: existing.url };
2911
4395
  }
2912
- const clientDir = opts.clientDir ?? import_node_path15.default.join(__dirname, "..", "chart-client");
4396
+ const clientDir = opts.clientDir ?? import_node_path19.default.join(__dirname, "..", "chart-client");
4397
+ const rootConfig = loadConfig(projectRoot);
4398
+ const projects = rootConfig.projects ?? [];
2913
4399
  const server = import_node_http.default.createServer((req, res) => {
2914
4400
  try {
2915
4401
  const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
4402
+ let reqRoot;
4403
+ try {
4404
+ reqRoot = resolveRequestRoot(url2, projectRoot, projects);
4405
+ } catch (err2) {
4406
+ res.writeHead(400, { "Content-Type": "application/json" });
4407
+ res.end(JSON.stringify({ ok: false, error: String(err2) }));
4408
+ return;
4409
+ }
4410
+ if (req.method === "GET" && url2.pathname === "/api/projects") {
4411
+ const projectList = projects.length > 0 ? projects.map((p) => {
4412
+ const absRoot = import_node_path19.default.resolve(projectRoot, p.root);
4413
+ const hasGraphs = import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, ".launchsecure", "graphs"));
4414
+ 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"));
4415
+ return { name: p.name, root: p.root, hasGraphs, hasNextConfig: hasNextConfig2 };
4416
+ }) : [{ name: import_node_path19.default.basename(projectRoot), root: ".", hasGraphs: import_node_fs17.default.existsSync(import_node_path19.default.join(projectRoot, ".launchsecure", "graphs")), hasNextConfig: true }];
4417
+ res.writeHead(200, { "Content-Type": "application/json" });
4418
+ res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
4419
+ return;
4420
+ }
2916
4421
  if (req.method === "GET" && url2.pathname === "/api/project-graph") {
2917
4422
  const regenerate = url2.searchParams.get("regenerate") === "1";
2918
4423
  (async () => {
2919
- if (regenerate) await generateGraph(projectRoot);
2920
- const merged = await buildMergedGraph(projectRoot);
4424
+ if (regenerate) await generateGraph(reqRoot);
4425
+ const merged = await buildMergedGraph(reqRoot);
2921
4426
  res.writeHead(200, { "Content-Type": "application/json" });
2922
4427
  res.end(JSON.stringify({
2923
4428
  ...merged,
2924
- debug: { cwd, projectRoot }
4429
+ debug: { cwd, projectRoot: reqRoot }
2925
4430
  }));
2926
4431
  })().catch((e) => {
2927
4432
  res.writeHead(500);
@@ -2930,15 +4435,15 @@ async function startChartServer(opts = {}) {
2930
4435
  return;
2931
4436
  }
2932
4437
  if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
2933
- const graphs = readAllGraphs(projectRoot);
4438
+ const graphs = readAllGraphs(reqRoot);
2934
4439
  res.writeHead(200, { "Content-Type": "application/json" });
2935
4440
  res.end(JSON.stringify({ ui: graphs.ui ?? null, api: graphs.api ?? null, db: graphs.db ?? null }));
2936
4441
  return;
2937
4442
  }
2938
4443
  if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
2939
4444
  (async () => {
2940
- await generateGraph(projectRoot);
2941
- const graphs = readAllGraphs(projectRoot);
4445
+ await generateGraph(reqRoot);
4446
+ const graphs = readAllGraphs(reqRoot);
2942
4447
  res.writeHead(200, { "Content-Type": "application/json" });
2943
4448
  res.end(JSON.stringify({
2944
4449
  ok: true,
@@ -2954,41 +4459,49 @@ async function startChartServer(opts = {}) {
2954
4459
  }
2955
4460
  if (req.method === "GET" && url2.pathname === "/api/file-content") {
2956
4461
  const relPath = url2.searchParams.get("path");
2957
- if (!relPath || relPath.includes("..") || import_node_path15.default.isAbsolute(relPath)) {
4462
+ if (!relPath || relPath.includes("..") || import_node_path19.default.isAbsolute(relPath)) {
2958
4463
  res.writeHead(400, { "Content-Type": "application/json" });
2959
4464
  res.end(JSON.stringify({ error: "Invalid path" }));
2960
4465
  return;
2961
4466
  }
2962
- const filePath = import_node_path15.default.join(projectRoot, relPath);
2963
- if (!filePath.startsWith(projectRoot) || !import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) {
4467
+ const filePath = import_node_path19.default.join(reqRoot, relPath);
4468
+ if (!filePath.startsWith(reqRoot) || !import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) {
2964
4469
  res.writeHead(404, { "Content-Type": "application/json" });
2965
4470
  res.end(JSON.stringify({ error: "File not found" }));
2966
4471
  return;
2967
4472
  }
2968
- const ext = import_node_path15.default.extname(filePath).toLowerCase();
4473
+ const ext = import_node_path19.default.extname(filePath).toLowerCase();
2969
4474
  const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
2970
- const content = import_node_fs13.default.readFileSync(filePath, "utf-8");
4475
+ const content = import_node_fs17.default.readFileSync(filePath, "utf-8");
2971
4476
  res.writeHead(200, { "Content-Type": "application/json" });
2972
4477
  res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
2973
4478
  return;
2974
4479
  }
2975
4480
  if (req.method === "GET" && url2.pathname === "/api/health") {
2976
4481
  res.writeHead(200, { "Content-Type": "application/json" });
2977
- res.end(JSON.stringify({ ok: true, projectRoot }));
4482
+ res.end(JSON.stringify({ ok: true, projectRoot: reqRoot }));
2978
4483
  return;
2979
4484
  }
2980
4485
  if (req.method === "GET" && url2.pathname === "/api/parser-config") {
2981
- const config2 = loadConfig(projectRoot);
2982
- const detection = [
2983
- { id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
2984
- { id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
2985
- { id: "prisma-schema", layer: "db", label: "Prisma Schema", detected: prismaSchemaParser.detect(projectRoot) }
2986
- ];
2987
- const crosslayerParsers = [
2988
- { id: "fetch-resolver", label: "Fetch / api.method() calls" },
2989
- { id: "api-annotations", label: "@api annotations" },
2990
- { id: "url-literal-scanner", label: "/api/... URL literals" }
2991
- ];
4486
+ const config2 = loadConfig(reqRoot);
4487
+ const registry = createRegistry(config2, reqRoot);
4488
+ const toLabel = (id) => id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
4489
+ const detection = [];
4490
+ for (const parser of registry.getAll()) {
4491
+ if ("layers" in parser && Array.isArray(parser.layers)) {
4492
+ const mp = parser;
4493
+ detection.push({ id: mp.id, layers: mp.layers, label: toLabel(mp.id), detected: mp.detect(reqRoot) });
4494
+ } else if ("layer" in parser && parser.layer !== "crosslayer") {
4495
+ const sp = parser;
4496
+ detection.push({ id: sp.id, layers: [sp.layer], label: toLabel(sp.id), detected: sp.detect(reqRoot) });
4497
+ }
4498
+ }
4499
+ const crosslayerParsers = {};
4500
+ for (const p of registry.getCrossLayerParsers()) {
4501
+ const concern = p.concern ?? "api-binding";
4502
+ if (!crosslayerParsers[concern]) crosslayerParsers[concern] = [];
4503
+ crosslayerParsers[concern].push({ id: p.id, label: toLabel(p.id) });
4504
+ }
2992
4505
  res.writeHead(200, { "Content-Type": "application/json" });
2993
4506
  res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
2994
4507
  return;
@@ -3001,8 +4514,8 @@ async function startChartServer(opts = {}) {
3001
4514
  req.on("end", () => {
3002
4515
  try {
3003
4516
  const newConfig = JSON.parse(body);
3004
- const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
3005
- import_node_fs13.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
4517
+ const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
4518
+ import_node_fs17.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
3006
4519
  res.writeHead(200, { "Content-Type": "application/json" });
3007
4520
  res.end(JSON.stringify({ ok: true }));
3008
4521
  } catch (err2) {
@@ -3013,7 +4526,7 @@ async function startChartServer(opts = {}) {
3013
4526
  return;
3014
4527
  }
3015
4528
  if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
3016
- const config2 = loadConfig(projectRoot);
4529
+ const config2 = loadConfig(reqRoot);
3017
4530
  const builtinTaggers = [
3018
4531
  { id: "module", tagKey: "module", trackUntagged: config2.taggers?.trackUntagged?.module ?? true },
3019
4532
  { id: "screen", tagKey: "screen", trackUntagged: config2.taggers?.trackUntagged?.screen ?? true }
@@ -3033,10 +4546,10 @@ async function startChartServer(opts = {}) {
3033
4546
  req.on("end", () => {
3034
4547
  try {
3035
4548
  const taggerConfig = JSON.parse(body);
3036
- const config2 = loadConfig(projectRoot);
4549
+ const config2 = loadConfig(reqRoot);
3037
4550
  config2.taggers = taggerConfig;
3038
- const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
3039
- import_node_fs13.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
4551
+ const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
4552
+ import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
3040
4553
  res.writeHead(200, { "Content-Type": "application/json" });
3041
4554
  res.end(JSON.stringify({ ok: true }));
3042
4555
  } catch (err2) {
@@ -3047,7 +4560,7 @@ async function startChartServer(opts = {}) {
3047
4560
  return;
3048
4561
  }
3049
4562
  if (req.method === "GET" && url2.pathname === "/api/tags") {
3050
- const store = readTagStore(projectRoot);
4563
+ const store = readTagStore(reqRoot);
3051
4564
  res.writeHead(200, { "Content-Type": "application/json" });
3052
4565
  res.end(JSON.stringify(store));
3053
4566
  return;
@@ -3065,7 +4578,7 @@ async function startChartServer(opts = {}) {
3065
4578
  res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
3066
4579
  return;
3067
4580
  }
3068
- setTag(projectRoot, nodeId, key, value);
4581
+ setTag(reqRoot, nodeId, key, value);
3069
4582
  res.writeHead(200, { "Content-Type": "application/json" });
3070
4583
  res.end(JSON.stringify({ ok: true }));
3071
4584
  } catch (err2) {
@@ -3088,7 +4601,81 @@ async function startChartServer(opts = {}) {
3088
4601
  res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
3089
4602
  return;
3090
4603
  }
3091
- removeTag(projectRoot, nodeId, key);
4604
+ removeTag(reqRoot, nodeId, key);
4605
+ res.writeHead(200, { "Content-Type": "application/json" });
4606
+ res.end(JSON.stringify({ ok: true }));
4607
+ } catch (err2) {
4608
+ res.writeHead(400, { "Content-Type": "application/json" });
4609
+ res.end(JSON.stringify({ ok: false, error: String(err2) }));
4610
+ }
4611
+ });
4612
+ return;
4613
+ }
4614
+ if (req.method === "GET" && url2.pathname === "/api/detected-paths") {
4615
+ const config2 = loadConfig(reqRoot);
4616
+ const paths = resolveProjectPaths(reqRoot, config2);
4617
+ const overrides = {
4618
+ appDir: !!config2.paths?.appDir,
4619
+ dbDir: !!config2.paths?.dbDir
4620
+ };
4621
+ res.writeHead(200, { "Content-Type": "application/json" });
4622
+ res.end(JSON.stringify({
4623
+ projectRoot: reqRoot,
4624
+ detected: paths ? {
4625
+ srcDir: import_node_path19.default.relative(reqRoot, paths.srcDir) || ".",
4626
+ appDir: import_node_path19.default.relative(reqRoot, paths.appDir),
4627
+ apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir),
4628
+ dbDir: paths.dbDir ? import_node_path19.default.relative(reqRoot, paths.dbDir) : null
4629
+ } : null,
4630
+ overrides,
4631
+ isOverride: overrides.appDir
4632
+ }));
4633
+ return;
4634
+ }
4635
+ if (req.method === "GET" && url2.pathname === "/api/browse-dir") {
4636
+ const browsePath = url2.searchParams.get("path") || projectRoot;
4637
+ const abs = import_node_path19.default.resolve(browsePath);
4638
+ const twoUp = import_node_path19.default.resolve(projectRoot, "..", "..");
4639
+ if (!abs.startsWith(twoUp)) {
4640
+ res.writeHead(403, { "Content-Type": "application/json" });
4641
+ res.end(JSON.stringify({ ok: false, error: "Path outside allowed range" }));
4642
+ return;
4643
+ }
4644
+ try {
4645
+ const entries = import_node_fs17.default.readdirSync(abs, { withFileTypes: true });
4646
+ 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();
4647
+ const parent = abs !== twoUp ? import_node_path19.default.dirname(abs) : null;
4648
+ res.writeHead(200, { "Content-Type": "application/json" });
4649
+ res.end(JSON.stringify({ current: abs, parent, dirs, relative: import_node_path19.default.relative(projectRoot, abs) || "." }));
4650
+ } catch (err2) {
4651
+ res.writeHead(400, { "Content-Type": "application/json" });
4652
+ res.end(JSON.stringify({ ok: false, error: String(err2) }));
4653
+ }
4654
+ return;
4655
+ }
4656
+ if (req.method === "GET" && url2.pathname === "/api/audit") {
4657
+ const layer = url2.searchParams.get("layer") ?? "all";
4658
+ const check = url2.searchParams.get("check") ?? void 0;
4659
+ const reports = runAudit(reqRoot, layer, check);
4660
+ const prompt = formatAsPrompt(reports);
4661
+ res.writeHead(200, { "Content-Type": "application/json" });
4662
+ res.end(JSON.stringify({ reports, prompt, checks: getAvailableChecks() }));
4663
+ return;
4664
+ }
4665
+ if (req.method === "POST" && url2.pathname === "/api/projects") {
4666
+ let body = "";
4667
+ req.on("data", (chunk) => {
4668
+ body += chunk.toString();
4669
+ });
4670
+ req.on("end", () => {
4671
+ try {
4672
+ const { projects: newProjects } = JSON.parse(body);
4673
+ const config2 = loadConfig(projectRoot);
4674
+ config2.projects = newProjects.length > 0 ? newProjects : void 0;
4675
+ const configPath = import_node_path19.default.join(projectRoot, ".launchchart.json");
4676
+ import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
4677
+ projects.length = 0;
4678
+ if (config2.projects) projects.push(...config2.projects);
3092
4679
  res.writeHead(200, { "Content-Type": "application/json" });
3093
4680
  res.end(JSON.stringify({ ok: true }));
3094
4681
  } catch (err2) {
@@ -3099,7 +4686,7 @@ async function startChartServer(opts = {}) {
3099
4686
  return;
3100
4687
  }
3101
4688
  if (url2.pathname !== "/") {
3102
- const staticPath = import_node_path15.default.join(clientDir, url2.pathname);
4689
+ const staticPath = import_node_path19.default.join(clientDir, url2.pathname);
3103
4690
  if (serveStatic(res, staticPath)) return;
3104
4691
  }
3105
4692
  serveIndex(res, clientDir);
@@ -3137,6 +4724,10 @@ async function startChartServer(opts = {}) {
3137
4724
  `);
3138
4725
  process.stderr.write(`[launch-chart] project root: ${projectRoot}
3139
4726
  `);
4727
+ if (projects.length > 0) {
4728
+ process.stderr.write(`[launch-chart] multi-project mode: ${projects.length} projects
4729
+ `);
4730
+ }
3140
4731
  }
3141
4732
  return { port, url };
3142
4733
  }
@@ -3155,19 +4746,19 @@ function runServeCli(argv) {
3155
4746
  process.exit(1);
3156
4747
  });
3157
4748
  }
3158
- var import_node_http, import_node_fs13, import_node_path15, MAX_PORT_SCAN, MIME_TYPES;
4749
+ var import_node_http, import_node_fs17, import_node_path19, MAX_PORT_SCAN, MIME_TYPES;
3159
4750
  var init_chart_serve = __esm({
3160
4751
  "src/server/chart-serve.ts"() {
3161
4752
  "use strict";
3162
4753
  import_node_http = __toESM(require("node:http"));
3163
- import_node_fs13 = __toESM(require("node:fs"));
3164
- import_node_path15 = __toESM(require("node:path"));
4754
+ import_node_fs17 = __toESM(require("node:fs"));
4755
+ import_node_path19 = __toESM(require("node:path"));
3165
4756
  init_graph();
3166
4757
  init_lockfile();
3167
4758
  init_config();
3168
- init_react_nextjs();
3169
- init_nextjs_routes();
3170
- init_prisma_schema();
4759
+ init_parser_registry();
4760
+ init_resolve_paths();
4761
+ init_audit_core();
3171
4762
  MAX_PORT_SCAN = 3;
3172
4763
  MIME_TYPES = {
3173
4764
  ".html": "text/html; charset=utf-8",
@@ -3183,6 +4774,179 @@ var init_chart_serve = __esm({
3183
4774
  }
3184
4775
  });
3185
4776
 
4777
+ // src/server/graph/core/language-detection.ts
4778
+ function walkForExtensions(dir, extCounts, depth = 0) {
4779
+ if (depth > 10) return;
4780
+ if (!(0, import_node_fs18.existsSync)(dir)) return;
4781
+ let entries;
4782
+ try {
4783
+ entries = (0, import_node_fs18.readdirSync)(dir, { withFileTypes: true });
4784
+ } catch {
4785
+ return;
4786
+ }
4787
+ for (const entry of entries) {
4788
+ if (entry.name.startsWith(".") && entry.isDirectory()) continue;
4789
+ if (entry.isDirectory()) {
4790
+ if (IGNORE_DIRS.has(entry.name)) continue;
4791
+ walkForExtensions((0, import_node_path20.join)(dir, entry.name), extCounts, depth + 1);
4792
+ } else {
4793
+ const ext = (0, import_node_path20.extname)(entry.name).toLowerCase();
4794
+ if (ext && EXTENSION_TO_LANGUAGE[ext]) {
4795
+ extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
4796
+ }
4797
+ }
4798
+ }
4799
+ }
4800
+ function detectLanguages(rootDir, supportedLanguages) {
4801
+ const extCounts = /* @__PURE__ */ new Map();
4802
+ walkForExtensions(rootDir, extCounts);
4803
+ const langData = /* @__PURE__ */ new Map();
4804
+ for (const [ext, count] of extCounts) {
4805
+ const lang = EXTENSION_TO_LANGUAGE[ext];
4806
+ if (!lang) continue;
4807
+ if (!langData.has(lang)) {
4808
+ langData.set(lang, { extensions: /* @__PURE__ */ new Set(), fileCount: 0 });
4809
+ }
4810
+ const data = langData.get(lang);
4811
+ data.extensions.add(ext);
4812
+ data.fileCount += count;
4813
+ }
4814
+ const results = [];
4815
+ for (const [lang, data] of langData) {
4816
+ if (AUXILIARY_LANGUAGES.has(lang)) continue;
4817
+ const parserIds = supportedLanguages.get(lang) ?? [];
4818
+ results.push({
4819
+ id: lang,
4820
+ extensions: [...data.extensions].sort(),
4821
+ fileCount: data.fileCount,
4822
+ supported: parserIds.length > 0,
4823
+ parserIds
4824
+ });
4825
+ }
4826
+ results.sort((a, b) => {
4827
+ if (a.supported !== b.supported) return a.supported ? -1 : 1;
4828
+ return b.fileCount - a.fileCount;
4829
+ });
4830
+ return results;
4831
+ }
4832
+ var import_node_fs18, import_node_path20, EXTENSION_TO_LANGUAGE, IGNORE_DIRS, AUXILIARY_LANGUAGES;
4833
+ var init_language_detection = __esm({
4834
+ "src/server/graph/core/language-detection.ts"() {
4835
+ "use strict";
4836
+ import_node_fs18 = require("node:fs");
4837
+ import_node_path20 = require("node:path");
4838
+ EXTENSION_TO_LANGUAGE = {
4839
+ // Web / Frontend
4840
+ ".ts": "typescript",
4841
+ ".tsx": "typescript",
4842
+ ".js": "javascript",
4843
+ ".jsx": "javascript",
4844
+ ".mjs": "javascript",
4845
+ ".cjs": "javascript",
4846
+ ".vue": "vue",
4847
+ ".svelte": "svelte",
4848
+ ".html": "html",
4849
+ ".css": "css",
4850
+ ".scss": "scss",
4851
+ ".less": "less",
4852
+ // Backend
4853
+ ".py": "python",
4854
+ ".go": "go",
4855
+ ".rs": "rust",
4856
+ ".rb": "ruby",
4857
+ ".java": "java",
4858
+ ".kt": "kotlin",
4859
+ ".kts": "kotlin",
4860
+ ".scala": "scala",
4861
+ ".cs": "csharp",
4862
+ ".fs": "fsharp",
4863
+ ".php": "php",
4864
+ ".ex": "elixir",
4865
+ ".exs": "elixir",
4866
+ ".erl": "erlang",
4867
+ ".hs": "haskell",
4868
+ ".lua": "lua",
4869
+ ".pl": "perl",
4870
+ ".r": "r",
4871
+ ".R": "r",
4872
+ ".jl": "julia",
4873
+ ".dart": "dart",
4874
+ ".clj": "clojure",
4875
+ ".cljs": "clojure",
4876
+ // Systems
4877
+ ".c": "c",
4878
+ ".h": "c",
4879
+ ".cpp": "cpp",
4880
+ ".cc": "cpp",
4881
+ ".cxx": "cpp",
4882
+ ".hpp": "cpp",
4883
+ ".swift": "swift",
4884
+ ".m": "objective-c",
4885
+ ".mm": "objective-c",
4886
+ ".zig": "zig",
4887
+ ".nim": "nim",
4888
+ // Shell / Config
4889
+ ".sh": "shell",
4890
+ ".bash": "shell",
4891
+ ".zsh": "shell",
4892
+ ".fish": "shell",
4893
+ // Data / Schema
4894
+ ".prisma": "prisma",
4895
+ ".sql": "sql",
4896
+ ".graphql": "graphql",
4897
+ ".gql": "graphql",
4898
+ ".proto": "protobuf",
4899
+ // Config
4900
+ ".json": "json",
4901
+ ".yaml": "yaml",
4902
+ ".yml": "yaml",
4903
+ ".toml": "toml",
4904
+ ".xml": "xml",
4905
+ // Docs
4906
+ ".md": "markdown",
4907
+ ".mdx": "mdx",
4908
+ ".rst": "restructuredtext"
4909
+ };
4910
+ IGNORE_DIRS = /* @__PURE__ */ new Set([
4911
+ "node_modules",
4912
+ ".next",
4913
+ "dist",
4914
+ ".launchsecure",
4915
+ ".git",
4916
+ "coverage",
4917
+ ".turbo",
4918
+ "build",
4919
+ "out",
4920
+ ".vercel",
4921
+ ".cache",
4922
+ "__pycache__",
4923
+ ".tox",
4924
+ "venv",
4925
+ ".venv",
4926
+ "target",
4927
+ "vendor",
4928
+ "Pods",
4929
+ ".gradle",
4930
+ ".idea",
4931
+ ".vscode"
4932
+ ]);
4933
+ AUXILIARY_LANGUAGES = /* @__PURE__ */ new Set([
4934
+ "json",
4935
+ "yaml",
4936
+ "toml",
4937
+ "xml",
4938
+ "markdown",
4939
+ "mdx",
4940
+ "restructuredtext",
4941
+ "html",
4942
+ "css",
4943
+ "scss",
4944
+ "less",
4945
+ "shell"
4946
+ ]);
4947
+ }
4948
+ });
4949
+
3186
4950
  // src/server/graph-mcp.ts
3187
4951
  var graph_mcp_exports = {};
3188
4952
  __export(graph_mcp_exports, {
@@ -3269,8 +5033,8 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
3269
5033
  const dstIn = visited.has(e.target) || next.has(e.target);
3270
5034
  if (srcIn && dstIn) projectedEdges++;
3271
5035
  }
3272
- const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] : EST_CHARS_PER_NODE_FULL[layer];
3273
- const projectedChars = projectedVisited * perNode + projectedEdges * EST_CHARS_PER_EDGE[layer];
5036
+ const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] ?? DEFAULT_EST_NODE_MIN : EST_CHARS_PER_NODE_FULL[layer] ?? DEFAULT_EST_NODE_FULL;
5037
+ const projectedChars = projectedVisited * perNode + projectedEdges * (EST_CHARS_PER_EDGE[layer] ?? DEFAULT_EST_EDGE);
3274
5038
  if (projectedChars > NEIGHBORHOOD_BUDGET_CHARS) {
3275
5039
  budgetExceeded = true;
3276
5040
  break;
@@ -3284,6 +5048,125 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
3284
5048
  const edges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
3285
5049
  return { nodes, edges, budgetExceeded, stoppedAtHop };
3286
5050
  }
5051
+ function reverseNeighborhood(graph, centerId, hops, direction) {
5052
+ const center = graph.nodes.find((n) => n.id === centerId);
5053
+ if (!center) return { nodes: /* @__PURE__ */ new Map(), edges: [] };
5054
+ const visited = /* @__PURE__ */ new Map();
5055
+ visited.set(centerId, { node: center, hop: 0 });
5056
+ let frontier = /* @__PURE__ */ new Set([centerId]);
5057
+ for (let h = 0; h < hops; h++) {
5058
+ const next = /* @__PURE__ */ new Set();
5059
+ for (const edge of graph.edges) {
5060
+ if (direction === "reverse") {
5061
+ if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
5062
+ } else {
5063
+ if (frontier.has(edge.source) && !visited.has(edge.target)) next.add(edge.target);
5064
+ if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
5065
+ }
5066
+ }
5067
+ for (const id of next) {
5068
+ const node = graph.nodes.find((n) => n.id === id);
5069
+ if (node) visited.set(id, { node, hop: h + 1 });
5070
+ }
5071
+ frontier = next;
5072
+ if (frontier.size === 0) break;
5073
+ }
5074
+ const nodeIds = new Set(visited.keys());
5075
+ const edges = graph.edges.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target));
5076
+ return { nodes: visited, edges };
5077
+ }
5078
+ function handleBlastPoints(args) {
5079
+ const rootDir = process.cwd();
5080
+ const nodeId = args.node_id;
5081
+ const requestedLayer = args.layer;
5082
+ const hops = args.hops ?? 2;
5083
+ const direction = args.direction ?? "reverse";
5084
+ let targetLayer = requestedLayer;
5085
+ if (!targetLayer) {
5086
+ const graphs = readAllGraphs(rootDir);
5087
+ for (const [layer, graph2] of Object.entries(graphs)) {
5088
+ if (graph2 && graph2.nodes.some((n) => n.id === nodeId)) {
5089
+ targetLayer = layer;
5090
+ break;
5091
+ }
5092
+ }
5093
+ if (!targetLayer) {
5094
+ return err(`Node "${nodeId}" not found in any layer. Available layers: ${getAvailableLayers(rootDir).join(", ")}`);
5095
+ }
5096
+ }
5097
+ const graph = readGraph(rootDir, targetLayer);
5098
+ if (!graph) {
5099
+ return err(`No graph for layer "${targetLayer}". Run generate_graph first.`);
5100
+ }
5101
+ const center = graph.nodes.find((n) => n.id === nodeId);
5102
+ if (!center) {
5103
+ return err(`Node "${nodeId}" not found in ${targetLayer} layer.`);
5104
+ }
5105
+ const result = reverseNeighborhood(graph, nodeId, hops, direction);
5106
+ const affected = [];
5107
+ for (const [id, { node, hop }] of result.nodes) {
5108
+ if (id === nodeId) continue;
5109
+ const tags = node.tags;
5110
+ affected.push({
5111
+ id: node.id,
5112
+ name: node.name,
5113
+ type: node.type,
5114
+ layer: targetLayer,
5115
+ hop,
5116
+ module: tags?.module
5117
+ });
5118
+ }
5119
+ const otherLayers = getAvailableLayers(rootDir).filter((l) => l !== targetLayer && l !== "static");
5120
+ for (const otherLayer of otherLayers) {
5121
+ const otherGraph = readGraph(rootDir, otherLayer);
5122
+ if (!otherGraph) continue;
5123
+ for (const edge of otherGraph.edges) {
5124
+ if (edge.target === nodeId || edge.source === nodeId) {
5125
+ const dependentId = edge.target === nodeId ? edge.source : edge.target;
5126
+ if (affected.some((a) => a.id === dependentId)) continue;
5127
+ const depNode = otherGraph.nodes.find((n) => n.id === dependentId);
5128
+ if (depNode) {
5129
+ const tags = depNode.tags;
5130
+ affected.push({
5131
+ id: depNode.id,
5132
+ name: depNode.name,
5133
+ type: depNode.type,
5134
+ layer: otherLayer,
5135
+ hop: 1,
5136
+ module: tags?.module
5137
+ });
5138
+ }
5139
+ }
5140
+ }
5141
+ }
5142
+ const byLayer = {};
5143
+ const byHop = {};
5144
+ const modulesSet = /* @__PURE__ */ new Set();
5145
+ for (const a of affected) {
5146
+ byLayer[a.layer] = (byLayer[a.layer] ?? 0) + 1;
5147
+ byHop[String(a.hop)] = (byHop[String(a.hop)] ?? 0) + 1;
5148
+ if (a.module) modulesSet.add(a.module);
5149
+ }
5150
+ const crossesLayers = Object.keys(byLayer).length > 1;
5151
+ const centerTags = center.tags;
5152
+ return okJson({
5153
+ center: {
5154
+ id: center.id,
5155
+ name: center.name,
5156
+ type: center.type,
5157
+ layer: targetLayer,
5158
+ module: centerTags?.module
5159
+ },
5160
+ affected,
5161
+ summary: {
5162
+ total: affected.length,
5163
+ by_layer: byLayer,
5164
+ by_hop: byHop,
5165
+ modules_touched: Array.from(modulesSet).sort(),
5166
+ crosses_layers: crossesLayers
5167
+ }
5168
+ });
5169
+ }
3287
5170
  function layerSummary(graph) {
3288
5171
  const typeCounts = {};
3289
5172
  const moduleCounts = {};
@@ -3318,9 +5201,6 @@ function err(text) {
3318
5201
  async function handleGenerateGraph(args) {
3319
5202
  const rootDir = process.cwd();
3320
5203
  const layer = args.layer;
3321
- if (layer && !["ui", "api", "db"].includes(layer)) {
3322
- return err(`Invalid layer "${layer}". Must be one of: ui, api, db`);
3323
- }
3324
5204
  const results = await generateGraph(rootDir, layer);
3325
5205
  if (results.length === 0) {
3326
5206
  return err(
@@ -3357,8 +5237,11 @@ function runReadGraphQueryRaw(rootDir, args) {
3357
5237
  const offset = args.offset ?? 0;
3358
5238
  const limit = args.limit;
3359
5239
  const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
3360
- if (layer && !["ui", "api", "db"].includes(layer)) {
3361
- return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
5240
+ if (layer) {
5241
+ const available = getAvailableLayers(rootDir);
5242
+ if (available.length > 0 && !available.includes(layer)) {
5243
+ return { error: `No graph found for layer "${layer}". Available: ${available.join(", ")}. Run generate_graph first.` };
5244
+ }
3362
5245
  }
3363
5246
  if (!layer) {
3364
5247
  const graphs = readAllGraphs(rootDir);
@@ -3512,9 +5395,12 @@ function handleReadGraph(args) {
3512
5395
  return okJson(result);
3513
5396
  }
3514
5397
  function nodeToFilePath(rootDir, layer, nodeId) {
3515
- if (layer === "ui") return (0, import_node_path16.join)(rootDir, "src", nodeId);
3516
- if (layer === "api") return (0, import_node_path16.join)(rootDir, nodeId);
3517
- if (layer === "db") return (0, import_node_path16.join)(rootDir, "prisma", "schema.prisma");
5398
+ if (layer === "ui" || layer === "api") return (0, import_node_path21.join)(rootDir, "src", nodeId);
5399
+ if (layer === "db") return (0, import_node_path21.join)(rootDir, "prisma", "schema.prisma");
5400
+ const withSrc = (0, import_node_path21.join)(rootDir, "src", nodeId);
5401
+ if ((0, import_node_fs19.existsSync)(withSrc)) return withSrc;
5402
+ const direct = (0, import_node_path21.join)(rootDir, nodeId);
5403
+ if ((0, import_node_fs19.existsSync)(direct)) return direct;
3518
5404
  return null;
3519
5405
  }
3520
5406
  function handleInspectNode(args) {
@@ -3603,7 +5489,10 @@ function handleGrepNodes(args) {
3603
5489
  const layer = args.layer;
3604
5490
  if (!pattern) return err("pattern is required");
3605
5491
  if (!layer) return err("layer is required (ui, api, or db)");
3606
- if (!["ui", "api", "db"].includes(layer)) return err(`Invalid layer "${layer}". Must be one of: ui, api, db`);
5492
+ const grepAvailable = getAvailableLayers(rootDir);
5493
+ if (grepAvailable.length > 0 && !grepAvailable.includes(layer)) {
5494
+ return err(`No graph found for layer "${layer}". Available: ${grepAvailable.join(", ")}. Run generate_graph first.`);
5495
+ }
3607
5496
  const hasFilter = !!(args.search || args.type || args.module || args.node_id);
3608
5497
  if (!hasFilter) {
3609
5498
  return err(
@@ -3654,11 +5543,11 @@ function handleGrepNodes(args) {
3654
5543
  let filesSearched = 0;
3655
5544
  let truncated = false;
3656
5545
  for (const [filePath, nodeId] of filePaths) {
3657
- if (!(0, import_node_fs14.existsSync)(filePath)) continue;
5546
+ if (!(0, import_node_fs19.existsSync)(filePath)) continue;
3658
5547
  filesSearched++;
3659
5548
  let content;
3660
5549
  try {
3661
- content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
5550
+ content = (0, import_node_fs19.readFileSync)(filePath, "utf-8");
3662
5551
  } catch {
3663
5552
  continue;
3664
5553
  }
@@ -3723,11 +5612,11 @@ function handleStartChartServer(args) {
3723
5612
  });
3724
5613
  }
3725
5614
  const entryPath = process.argv[1];
3726
- const logDir = (0, import_node_path16.join)((0, import_node_os2.homedir)(), ".launchsecure");
3727
- (0, import_node_fs14.mkdirSync)(logDir, { recursive: true });
3728
- const logPath = (0, import_node_path16.join)(logDir, "launch-chart.log");
3729
- const out = (0, import_node_fs14.openSync)(logPath, "a");
3730
- const err2 = (0, import_node_fs14.openSync)(logPath, "a");
5615
+ const logDir = (0, import_node_path21.join)((0, import_node_os2.homedir)(), ".launchsecure");
5616
+ (0, import_node_fs19.mkdirSync)(logDir, { recursive: true });
5617
+ const logPath = (0, import_node_path21.join)(logDir, "launch-chart.log");
5618
+ const out = (0, import_node_fs19.openSync)(logPath, "a");
5619
+ const err2 = (0, import_node_fs19.openSync)(logPath, "a");
3731
5620
  const portArgs = args.port ? ["--port", String(args.port)] : [];
3732
5621
  const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
3733
5622
  detached: true,
@@ -3790,39 +5679,77 @@ function handleRemoveTag(args) {
3790
5679
  removeTag(rootDir, nodeId, key);
3791
5680
  return okJson({ ok: true, node_id: nodeId, removed_key: key });
3792
5681
  }
5682
+ function handleAuditLayer(args) {
5683
+ const rootDir = process.cwd();
5684
+ const layer = args.layer;
5685
+ const check = args.check;
5686
+ if (!layer) return err("layer is required");
5687
+ const reports = runAudit(rootDir, layer, check);
5688
+ const totalFindings = reports.reduce((acc, r) => acc + r.findings.length, 0);
5689
+ const totalErrors = reports.reduce((acc, r) => acc + r.summary.errors, 0);
5690
+ const totalWarnings = reports.reduce((acc, r) => acc + r.summary.warnings, 0);
5691
+ const totalInfo = reports.reduce((acc, r) => acc + r.summary.info, 0);
5692
+ const lines = [];
5693
+ lines.push(`Audit: ${layer}${check ? ` / ${check}` : ""} \u2014 ${totalFindings} findings (${totalErrors} errors, ${totalWarnings} warnings, ${totalInfo} info)`);
5694
+ lines.push("");
5695
+ for (const report of reports) {
5696
+ if (report.findings.length === 0) {
5697
+ lines.push(`\u2713 ${report.check}: no issues found`);
5698
+ continue;
5699
+ }
5700
+ lines.push(`\u2500\u2500 ${report.check} (${report.findings.length}) \u2500\u2500`);
5701
+ for (const f of report.findings) {
5702
+ const tag = f.severity === "error" ? "\u2717" : f.severity === "warning" ? "!" : "\xB7";
5703
+ const filePart = f.file ? ` [${f.file}]` : "";
5704
+ lines.push(` ${tag} ${f.title}${filePart}`);
5705
+ lines.push(` ${f.detail}`);
5706
+ }
5707
+ lines.push("");
5708
+ }
5709
+ return okText(lines.join("\n"));
5710
+ }
3793
5711
  function handleDetectProjectStack() {
3794
5712
  const rootDir = process.cwd();
3795
- const parsers = [
3796
- { id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
3797
- { id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
3798
- { id: "prisma-schema", layer: "db", detected: prismaSchemaParser.detect(rootDir) }
3799
- ];
3800
5713
  const config = loadConfig(rootDir);
3801
- let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
5714
+ const registry = createRegistry(config, rootDir);
5715
+ const parserResults = [];
5716
+ for (const parser of registry.getAll()) {
5717
+ if ("layers" in parser && Array.isArray(parser.layers)) {
5718
+ const mp = parser;
5719
+ parserResults.push({ id: mp.id, layers: mp.layers, detected: mp.detect(rootDir) });
5720
+ } else if ("layer" in parser && parser.layer !== "crosslayer") {
5721
+ const sp = parser;
5722
+ parserResults.push({ id: sp.id, layers: [sp.layer], detected: sp.detect(rootDir) });
5723
+ }
5724
+ }
5725
+ const availableLayers = /* @__PURE__ */ new Set();
5726
+ for (const p of parserResults) {
5727
+ if (p.detected) {
5728
+ for (const l of p.layers) availableLayers.add(l);
5729
+ }
5730
+ }
5731
+ const stats = { calls_api: 0, references_api: 0, annotations: 0 };
3802
5732
  const uiGraph = readGraph(rootDir, "ui");
3803
5733
  if (uiGraph) {
3804
5734
  for (const ref of uiGraph.cross_refs ?? []) {
3805
5735
  if (ref.type === "calls_api") stats.calls_api++;
3806
5736
  if (ref.type === "references_api") stats.references_api++;
3807
5737
  }
3808
- for (const f of uiGraph.flagged_edges ?? []) {
3809
- if (f.type === "out_of_pattern") stats.out_of_pattern++;
3810
- }
3811
5738
  }
3812
- const srcDir = (0, import_node_path16.join)(rootDir, "src");
3813
- if ((0, import_node_fs14.existsSync)(srcDir)) {
5739
+ const srcDir = (0, import_node_path21.join)(rootDir, "src");
5740
+ if ((0, import_node_fs19.existsSync)(srcDir)) {
3814
5741
  const scanDir = (dir) => {
3815
- if (!(0, import_node_fs14.existsSync)(dir)) return;
3816
- for (const entry of (0, import_node_fs14.readdirSync)(dir, { withFileTypes: true })) {
5742
+ if (!(0, import_node_fs19.existsSync)(dir)) return;
5743
+ for (const entry of (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true })) {
3817
5744
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
3818
- const full = (0, import_node_path16.join)(dir, entry.name);
5745
+ const full = (0, import_node_path21.join)(dir, entry.name);
3819
5746
  if (entry.isDirectory()) {
3820
5747
  scanDir(full);
3821
5748
  continue;
3822
5749
  }
3823
- if (![".ts", ".tsx"].includes((0, import_node_path16.extname)(entry.name))) continue;
5750
+ if (![".ts", ".tsx"].includes((0, import_node_path21.extname)(entry.name))) continue;
3824
5751
  try {
3825
- const content = (0, import_node_fs14.readFileSync)(full, "utf-8");
5752
+ const content = (0, import_node_fs19.readFileSync)(full, "utf-8");
3826
5753
  const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
3827
5754
  if (matches) stats.annotations += matches.length;
3828
5755
  } catch {
@@ -3831,21 +5758,33 @@ function handleDetectProjectStack() {
3831
5758
  };
3832
5759
  scanDir(srcDir);
3833
5760
  }
3834
- let recommendedPrimary = "fetch-resolver";
3835
- if (stats.annotations > 0 && stats.annotations >= stats.calls_api) {
3836
- recommendedPrimary = "api-annotations";
3837
- } else if (stats.calls_api === 0 && stats.references_api > 0) {
3838
- recommendedPrimary = "url-literal-scanner";
3839
- }
5761
+ const supportedLanguages = /* @__PURE__ */ new Map();
5762
+ supportedLanguages.set("typescript", parserResults.filter((p) => p.detected && p.layers.some((l) => l === "ui" || l === "api")).map((p) => p.id));
5763
+ supportedLanguages.set("prisma", parserResults.filter((p) => p.detected && p.layers.includes("db")).map((p) => p.id));
5764
+ const languages = detectLanguages(rootDir, supportedLanguages);
5765
+ const unsupported = languages.filter((l) => !l.supported);
5766
+ const unsupportedHint = unsupported.length > 0 ? unsupported.map((l) => `${l.id} (${l.fileCount} files)`).join(", ") + " \u2014 detected but not yet supported" : null;
3840
5767
  return okJson({
3841
- parsers,
3842
- crosslayer_parsers: [
3843
- { id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
3844
- { id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
3845
- { id: "url-literal-scanner", description: "Finds /api/... string literals as fallback detection" }
3846
- ],
5768
+ languages,
5769
+ parsers: parserResults,
5770
+ available_layers: [...availableLayers],
5771
+ crosslayer_parsers: (() => {
5772
+ const descriptions = {
5773
+ "fetch-resolver": "Detects direct fetch()/api.get() calls with inline URLs",
5774
+ "api-annotations": "Scans for @api METHOD /path annotations in JSDoc/comments",
5775
+ "url-literal-scanner": "Finds /api/... string literals as fallback detection",
5776
+ "static-ref-scanner": "Finds references to static values (enums, permissions, roles)"
5777
+ };
5778
+ const grouped = {};
5779
+ for (const p of registry.getCrossLayerParsers()) {
5780
+ const concern = p.concern ?? "api-binding";
5781
+ if (!grouped[concern]) grouped[concern] = [];
5782
+ grouped[concern].push({ id: p.id, description: descriptions[p.id] ?? p.id });
5783
+ }
5784
+ return grouped;
5785
+ })(),
3847
5786
  stats,
3848
- recommended_primary: recommendedPrimary,
5787
+ ...unsupportedHint ? { unsupported_hint: unsupportedHint } : {},
3849
5788
  current_config: Object.keys(config).length > 0 ? config : null,
3850
5789
  config_path: ".launchchart.json"
3851
5790
  });
@@ -3921,6 +5860,14 @@ async function handleMessage(msg) {
3921
5860
  respond(id ?? null, handleRemoveTag(args));
3922
5861
  return;
3923
5862
  }
5863
+ if (toolName === "audit_layer") {
5864
+ respond(id ?? null, handleAuditLayer(args));
5865
+ return;
5866
+ }
5867
+ if (toolName === "blast_points") {
5868
+ respond(id ?? null, handleBlastPoints(args));
5869
+ return;
5870
+ }
3924
5871
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
3925
5872
  return;
3926
5873
  }
@@ -3956,20 +5903,20 @@ function startGraphMcpServer() {
3956
5903
  process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
3957
5904
  `);
3958
5905
  }
3959
- var import_node_fs14, import_node_path16, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, DEEP_FIELDS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
5906
+ var import_node_fs19, import_node_path21, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, DEEP_FIELDS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, DEFAULT_EST_NODE_FULL, DEFAULT_EST_NODE_MIN, DEFAULT_EST_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
3960
5907
  var init_graph_mcp = __esm({
3961
5908
  "src/server/graph-mcp.ts"() {
3962
5909
  "use strict";
3963
- import_node_fs14 = require("node:fs");
3964
- import_node_path16 = require("node:path");
5910
+ import_node_fs19 = require("node:fs");
5911
+ import_node_path21 = require("node:path");
3965
5912
  import_node_child_process2 = require("node:child_process");
3966
5913
  import_node_os2 = require("node:os");
3967
5914
  init_graph();
3968
5915
  init_lockfile();
3969
5916
  init_config();
3970
- init_react_nextjs();
3971
- init_nextjs_routes();
3972
- init_prisma_schema();
5917
+ init_parser_registry();
5918
+ init_language_detection();
5919
+ init_audit_core();
3973
5920
  SERVER_INFO = {
3974
5921
  name: "launchsecure-graph",
3975
5922
  version: "0.0.1"
@@ -3977,14 +5924,13 @@ var init_graph_mcp = __esm({
3977
5924
  TOOLS = [
3978
5925
  {
3979
5926
  name: "generate_graph",
3980
- description: "Regenerate the structural project graph by scanning source code in the current working directory. Parses three layers: UI (React/Next.js pages, layouts, components, hooks, imports/renders/navigation edges), API (Next.js App Router endpoints with HTTP methods), DB (Prisma schema models, enums, belongs_to/has_many relations). Writes JSON to .launchsecure/graphs/{layer}.json and returns node/edge counts. Run this when project structure has changed (new files, moved components, schema updates). Fast: typically <1s. After generation, use read_graph with filters to query.",
5927
+ description: "Regenerate the structural project graph by scanning source code in the current working directory. Auto-detects project languages and frameworks, then parses into layers (e.g. ui, api, db) based on content classification. Writes JSON to .launchsecure/graphs/{layer}.json and returns node/edge counts. Run this when project structure has changed (new files, moved components, schema updates). Fast: typically <1s. After generation, use read_graph with filters to query.",
3981
5928
  inputSchema: {
3982
5929
  type: "object",
3983
5930
  properties: {
3984
5931
  layer: {
3985
5932
  type: "string",
3986
- enum: ["ui", "api", "db"],
3987
- description: "Specific layer to regenerate. Omit to regenerate all detectable layers."
5933
+ description: "Specific layer to regenerate (e.g. 'ui', 'api', 'db'). Omit to regenerate all detectable layers. Run detect_project_stack to see available layers."
3988
5934
  }
3989
5935
  }
3990
5936
  }
@@ -3997,8 +5943,7 @@ var init_graph_mcp = __esm({
3997
5943
  properties: {
3998
5944
  layer: {
3999
5945
  type: "string",
4000
- enum: ["ui", "api", "db"],
4001
- description: "Graph layer to query: ui, api, or db. Required if any filter is provided."
5946
+ description: "Graph layer to query (e.g. 'ui', 'api', 'db'). Required if any filter is provided. Run detect_project_stack to see available layers."
4002
5947
  },
4003
5948
  search: {
4004
5949
  type: "string",
@@ -4050,7 +5995,7 @@ var init_graph_mcp = __esm({
4050
5995
  items: {
4051
5996
  type: "object",
4052
5997
  properties: {
4053
- layer: { type: "string", enum: ["ui", "api", "db"] },
5998
+ layer: { type: "string" },
4054
5999
  search: { type: "string" },
4055
6000
  type: { type: "string" },
4056
6001
  module: { type: "string" },
@@ -4092,8 +6037,7 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
4092
6037
  properties: {
4093
6038
  layer: {
4094
6039
  type: "string",
4095
- enum: ["ui", "api", "db"],
4096
- description: "Graph layer to scope files (required)."
6040
+ description: "Graph layer to scope files (required, e.g. 'ui', 'api', 'db')."
4097
6041
  },
4098
6042
  pattern: {
4099
6043
  type: "string",
@@ -4126,8 +6070,7 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
4126
6070
  properties: {
4127
6071
  layer: {
4128
6072
  type: "string",
4129
- enum: ["ui", "api", "db"],
4130
- description: "Graph layer (required)."
6073
+ description: "Graph layer (required, e.g. 'ui', 'api', 'db')."
4131
6074
  },
4132
6075
  node_id: {
4133
6076
  type: "string",
@@ -4187,7 +6130,7 @@ Use this when the user asks "is the chart running", "show me the project graph U
4187
6130
  },
4188
6131
  {
4189
6132
  name: "detect_project_stack",
4190
- description: "Detect project frameworks, available parsers, and recommend parser configuration. Scans the project to identify the tech stack (Next.js, Prisma, React, etc.), reports which built-in parsers are applicable, and provides cross-layer detection stats (fetch calls, @api annotations, URL literals). Returns a recommended primary parser and current .launchchart.json config if present. \n\nUse this when setting up launch-chart for a new project or reviewing parser configuration.",
6133
+ description: "Detect project languages, frameworks, available parsers, and recommend parser configuration. Scans the project to identify all languages present (TypeScript, Python, Go, etc.) and reports which are supported by registered parsers vs detected-but-unsupported. Also detects frameworks (Next.js, Prisma, React, etc.), provides cross-layer detection stats (fetch calls, @api annotations, URL literals), and returns available graph layers. \n\nUse this when setting up launch-chart for a new project, reviewing parser configuration, or checking what languages are in the project.",
4191
6134
  inputSchema: {
4192
6135
  type: "object",
4193
6136
  properties: {}
@@ -4232,6 +6175,59 @@ Use this when the user asks "is the chart running", "show me the project graph U
4232
6175
  },
4233
6176
  required: ["node_id", "key"]
4234
6177
  }
6178
+ },
6179
+ {
6180
+ name: "audit_layer",
6181
+ description: "Run audit checks on a graph layer. Surfaces schema drift, unprotected routes, hardcoded values, dead screens, and other discrepancies. Returns findings with severity and file locations.\n\nAvailable layers and checks:\n- db: schema_drift (Prisma vs SQL migration mismatches), orphan_fks (FKs without @relation)\n- api: unprotected_routes (endpoints missing from ROUTE_PERMISSIONS)\n- ui: dead_screens (pages with no incoming navigation)\n- static: unenforced_permissions (seed permissions with no route enforcement), hardcoded_values (ALL_CAPS strings not in inventory)\n- all: run every check across all layers",
6182
+ inputSchema: {
6183
+ type: "object",
6184
+ properties: {
6185
+ layer: {
6186
+ type: "string",
6187
+ description: "Layer to audit: 'db', 'api', 'ui', 'static', or 'all'."
6188
+ },
6189
+ check: {
6190
+ type: "string",
6191
+ description: "Specific check to run (e.g. 'schema_drift', 'unprotected_routes'). Omit to run all checks for the layer."
6192
+ }
6193
+ },
6194
+ required: ["layer"]
6195
+ }
6196
+ },
6197
+ {
6198
+ name: "blast_points",
6199
+ description: `Calculate the blast radius for a node \u2014 what depends on it across all project layers. Returns reverse dependencies aggregated with hop distance and summary stats.
6200
+
6201
+ USE THIS when assessing the impact of changing a file, table, or endpoint. Replaces multiple read_graph calls with a single query that:
6202
+ - Traverses REVERSE edges (who imports/depends on this node)
6203
+ - Searches across ALL layers if layer is omitted
6204
+ - Returns affected nodes with hop distance, type, layer, and module
6205
+ - Provides a summary with counts by layer, by hop, and risk assessment
6206
+
6207
+ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 returns all files that import middleware.ts, and all files that import THOSE files.`,
6208
+ inputSchema: {
6209
+ type: "object",
6210
+ properties: {
6211
+ node_id: {
6212
+ type: "string",
6213
+ description: "The node to analyze (file path, table name, etc.)"
6214
+ },
6215
+ layer: {
6216
+ type: "string",
6217
+ description: "Layer the node lives in (e.g. 'ui', 'api', 'db'). Omit to auto-detect by searching all layers."
6218
+ },
6219
+ hops: {
6220
+ type: "number",
6221
+ description: "Max hops to traverse outward. Default 2."
6222
+ },
6223
+ direction: {
6224
+ type: "string",
6225
+ enum: ["reverse", "both"],
6226
+ description: "'reverse' (default) = only what depends on this node. 'both' = full neighborhood."
6227
+ }
6228
+ },
6229
+ required: ["node_id"]
6230
+ }
4235
6231
  }
4236
6232
  ];
4237
6233
  COMPACT_SCHEMA = {
@@ -4288,6 +6284,9 @@ Use this when the user asks "is the chart running", "show me the project graph U
4288
6284
  api: 65,
4289
6285
  db: 65
4290
6286
  };
6287
+ DEFAULT_EST_NODE_FULL = 300;
6288
+ DEFAULT_EST_NODE_MIN = 150;
6289
+ DEFAULT_EST_EDGE = 65;
4291
6290
  NEIGHBORHOOD_BUDGET_CHARS = 55e3;
4292
6291
  BATCH_BUDGET_CHARS = 6e4;
4293
6292
  }
@@ -4295,10 +6294,10 @@ Use this when the user asks "is the chart running", "show me the project graph U
4295
6294
 
4296
6295
  // src/server/graph-mcp-entry.ts
4297
6296
  var import_node_child_process3 = require("node:child_process");
4298
- var import_node_fs15 = require("node:fs");
4299
- var import_node_path17 = __toESM(require("node:path"));
6297
+ var import_node_fs20 = require("node:fs");
6298
+ var import_node_path22 = __toESM(require("node:path"));
4300
6299
  var import_node_os3 = require("node:os");
4301
- var import_node_fs16 = require("node:fs");
6300
+ var import_node_fs21 = require("node:fs");
4302
6301
  init_lockfile();
4303
6302
  function logStderr(msg) {
4304
6303
  process.stderr.write(`[launch-chart] ${msg}
@@ -4314,11 +6313,11 @@ function maybeAutoServe() {
4314
6313
  return;
4315
6314
  }
4316
6315
  try {
4317
- const logDir = import_node_path17.default.join((0, import_node_os3.homedir)(), ".launchsecure");
4318
- (0, import_node_fs16.mkdirSync)(logDir, { recursive: true });
4319
- const logPath = import_node_path17.default.join(logDir, "launch-chart.log");
4320
- const out = (0, import_node_fs15.openSync)(logPath, "a");
4321
- const err2 = (0, import_node_fs15.openSync)(logPath, "a");
6316
+ const logDir = import_node_path22.default.join((0, import_node_os3.homedir)(), ".launchsecure");
6317
+ (0, import_node_fs21.mkdirSync)(logDir, { recursive: true });
6318
+ const logPath = import_node_path22.default.join(logDir, "launch-chart.log");
6319
+ const out = (0, import_node_fs20.openSync)(logPath, "a");
6320
+ const err2 = (0, import_node_fs20.openSync)(logPath, "a");
4322
6321
  const entryPath = process.argv[1];
4323
6322
  const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
4324
6323
  detached: true,