@launchsecure/launch-kit 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -155,7 +155,45 @@ var init_config = __esm({
155
155
  }
156
156
  });
157
157
 
158
+ // src/server/graph/core/resolve-paths.ts
159
+ function resolveProjectPaths(rootDir, config) {
160
+ if (config.paths?.appDir) {
161
+ const appDir = (0, import_node_path3.join)(rootDir, config.paths.appDir);
162
+ const srcDir = config.paths.srcDir ? (0, import_node_path3.join)(rootDir, config.paths.srcDir) : (0, import_node_path3.dirname)(appDir);
163
+ return { srcDir, appDir, apiDir: (0, import_node_path3.join)(appDir, "api") };
164
+ }
165
+ const srcApp = (0, import_node_path3.join)(rootDir, "src", "app");
166
+ if ((0, import_node_fs3.existsSync)(srcApp)) {
167
+ return { srcDir: (0, import_node_path3.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path3.join)(srcApp, "api") };
168
+ }
169
+ const rootApp = (0, import_node_path3.join)(rootDir, "app");
170
+ if ((0, import_node_fs3.existsSync)(rootApp)) {
171
+ return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path3.join)(rootApp, "api") };
172
+ }
173
+ return null;
174
+ }
175
+ var import_node_fs3, import_node_path3;
176
+ var init_resolve_paths = __esm({
177
+ "src/server/graph/core/resolve-paths.ts"() {
178
+ "use strict";
179
+ import_node_fs3 = require("node:fs");
180
+ import_node_path3 = require("node:path");
181
+ }
182
+ });
183
+
158
184
  // src/server/graph/core/ts-extractor.ts
185
+ var ts_extractor_exports = {};
186
+ __export(ts_extractor_exports, {
187
+ classifyFile: () => classifyFile,
188
+ createQuery: () => createQuery,
189
+ extractAuthWrappersTS: () => extractAuthWrappersTS,
190
+ extractDbCallsTS: () => extractDbCallsTS,
191
+ extractDeep: () => extractDeep,
192
+ initTreeSitter: () => initTreeSitter,
193
+ parseCodeTS: () => parseCodeTS,
194
+ parseFileTS: () => parseFileTS,
195
+ setExtractorConfig: () => setExtractorConfig
196
+ });
159
197
  async function initTreeSitter() {
160
198
  if (initialized) return;
161
199
  if (initPromise) return initPromise;
@@ -179,17 +217,25 @@ function getQuery(name) {
179
217
  ensureInit();
180
218
  const cached = queryCache.get(name);
181
219
  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");
220
+ const scmPath = (0, import_node_path4.join)(queriesDir, `${name}.scm`);
221
+ const scm = (0, import_node_fs4.readFileSync)(scmPath, "utf-8");
184
222
  const query = tsxLanguage.query(scm);
185
223
  queryCache.set(name, query);
186
224
  return query;
187
225
  }
188
226
  function parseSource(absPath) {
189
227
  ensureInit();
190
- const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
228
+ const content = (0, import_node_fs4.readFileSync)(absPath, "utf-8");
191
229
  return parserInstance.parse(content);
192
230
  }
231
+ function parseCodeTS(code) {
232
+ ensureInit();
233
+ return parserInstance.parse(code);
234
+ }
235
+ function createQuery(pattern) {
236
+ ensureInit();
237
+ return tsxLanguage.query(pattern);
238
+ }
193
239
  function setExtractorConfig(config) {
194
240
  extraDbIdentifiers = config.dbIdentifiers ?? [];
195
241
  extraMutationMethods = config.mutationMethods ?? [];
@@ -644,17 +690,17 @@ function extractDeep(absPath) {
644
690
  }
645
691
  return { elements, stateVars, conditions, variables, responses, params };
646
692
  }
647
- var import_node_fs3, import_node_path3, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
693
+ var import_node_fs4, import_node_path4, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
648
694
  var init_ts_extractor = __esm({
649
695
  "src/server/graph/core/ts-extractor.ts"() {
650
696
  "use strict";
651
- import_node_fs3 = require("node:fs");
652
- import_node_path3 = require("node:path");
697
+ import_node_fs4 = require("node:fs");
698
+ import_node_path4 = require("node:path");
653
699
  initialized = false;
654
700
  queriesDir = (() => {
655
- const srcPath = (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "..", "queries");
701
+ const srcPath = (0, import_node_path4.join)((0, import_node_path4.dirname)(__filename), "..", "queries");
656
702
  if (require("fs").existsSync(srcPath)) return srcPath;
657
- return (0, import_node_path3.join)((0, import_node_path3.dirname)(__filename), "graph", "queries");
703
+ return (0, import_node_path4.join)((0, import_node_path4.dirname)(__filename), "graph", "queries");
658
704
  })();
659
705
  queryCache = /* @__PURE__ */ new Map();
660
706
  PRISMA_MUTATION_METHODS_BUILTIN = [
@@ -674,15 +720,15 @@ var init_ts_extractor = __esm({
674
720
  }
675
721
  });
676
722
 
677
- // src/server/graph/parsers/ui/react-nextjs.ts
723
+ // src/server/graph/parsers/ts/typescript-project.ts
678
724
  function walk(dir, exts) {
679
725
  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);
726
+ if (!(0, import_node_fs5.existsSync)(dir)) return results;
727
+ for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
728
+ const full = (0, import_node_path5.join)(dir, entry.name);
683
729
  if (entry.isDirectory()) {
684
730
  results.push(...walk(full, exts));
685
- } else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
731
+ } else if (exts.includes((0, import_node_path5.extname)(entry.name))) {
686
732
  results.push(full);
687
733
  }
688
734
  }
@@ -690,33 +736,33 @@ function walk(dir, exts) {
690
736
  }
691
737
  function walkWithIgnore(dir, exts, ignoreDirs) {
692
738
  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 })) {
739
+ if (!(0, import_node_fs5.existsSync)(dir)) return results;
740
+ for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
695
741
  if (entry.isDirectory()) {
696
742
  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));
743
+ results.push(...walkWithIgnore((0, import_node_path5.join)(dir, entry.name), exts, ignoreDirs));
744
+ } else if (exts.includes((0, import_node_path5.extname)(entry.name))) {
745
+ results.push((0, import_node_path5.join)(dir, entry.name));
700
746
  }
701
747
  }
702
748
  return results;
703
749
  }
704
750
  function toNodeId(srcDir, absPath) {
705
- return (0, import_node_path4.relative)(srcDir, absPath).replace(/\\/g, "/");
751
+ return (0, import_node_path5.relative)(srcDir, absPath).replace(/\\/g, "/");
706
752
  }
707
753
  function resolveImport(srcDir, specifier) {
708
754
  if (!specifier.startsWith("@/")) return null;
709
755
  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;
756
+ const base = (0, import_node_path5.join)(srcDir, rel);
757
+ 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")]) {
758
+ if ((0, import_node_fs5.existsSync)(c) && (0, import_node_fs5.statSync)(c).isFile()) return c;
713
759
  }
714
760
  return null;
715
761
  }
716
762
  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;
763
+ const base = (0, import_node_path5.join)((0, import_node_path5.dirname)(fromFile), specifier);
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;
720
766
  }
721
767
  return null;
722
768
  }
@@ -737,7 +783,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
737
783
  const resolved = resolveRelativeImport(barrelAbsPath, re.from);
738
784
  if (!resolved) continue;
739
785
  if (re.isWildcard) {
740
- const targetBn = (0, import_node_path4.basename)(resolved);
786
+ const targetBn = (0, import_node_path5.basename)(resolved);
741
787
  const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
742
788
  if (targetIsBarrel) {
743
789
  const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
@@ -764,12 +810,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
764
810
  const barrels = /* @__PURE__ */ new Map();
765
811
  const memo = /* @__PURE__ */ new Map();
766
812
  for (const [absPath, parsed] of parsedByPath) {
767
- const bn = (0, import_node_path4.basename)(absPath);
813
+ const bn = (0, import_node_path5.basename)(absPath);
768
814
  if (bn !== "index.ts" && bn !== "index.tsx") continue;
769
815
  if (parsed.reExports.length === 0) continue;
770
816
  const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
771
817
  if (map.size > 0) {
772
- const barrelId = (0, import_node_path4.relative)(srcDir, (0, import_node_path4.dirname)(absPath)).replace(/\\/g, "/");
818
+ const barrelId = (0, import_node_path5.relative)(srcDir, (0, import_node_path5.dirname)(absPath)).replace(/\\/g, "/");
773
819
  barrels.set(barrelId, map);
774
820
  }
775
821
  }
@@ -790,7 +836,18 @@ function extractRoute(id) {
790
836
  return route || "/";
791
837
  }
792
838
  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());
839
+ return (0, import_node_path5.basename)(absPath, (0, import_node_path5.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
840
+ }
841
+ function filePathToApiRoute(apiDir, absPath) {
842
+ let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
843
+ route = route.replace(/\[([^\]]+)\]/g, ":$1");
844
+ route = route.replace(/\/+/g, "/");
845
+ if (route === "/") return "/api";
846
+ return "/api" + route;
847
+ }
848
+ function camelToPascal(s) {
849
+ if (!s) return s;
850
+ return s.charAt(0).toUpperCase() + s.slice(1);
794
851
  }
795
852
  function resolveTemplateLiteralRoute(template, routeToNodeId) {
796
853
  const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -871,7 +928,7 @@ function matchRouteToPage(route, routeToNodeId) {
871
928
  if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
872
929
  return null;
873
930
  }
874
- function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
931
+ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps, routeToNodeId) {
875
932
  const edges = [];
876
933
  const flagged = [];
877
934
  const seen = /* @__PURE__ */ new Set();
@@ -883,7 +940,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
883
940
  if (label) edge.label = label;
884
941
  edges.push(edge);
885
942
  }
886
- function edgeTypeFor(_targetId, isTypeOnlyImport, importedNames) {
943
+ function edgeTypeFor(isTypeOnlyImport, importedNames) {
887
944
  if (isTypeOnlyImport) return "imports";
888
945
  const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
889
946
  if (anyRendered) return "renders";
@@ -908,14 +965,14 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
908
965
  }
909
966
  for (const [targetId, targetNames] of byTarget) {
910
967
  const allType = isTypeOnly || targetNames.every((n) => typeNames.has(n));
911
- addEdge(targetId, edgeTypeFor(targetId, allType, targetNames));
968
+ addEdge(targetId, edgeTypeFor(allType, targetNames));
912
969
  }
913
970
  } else {
914
971
  const resolved = resolveImport(srcDir, specifier);
915
972
  if (resolved) {
916
973
  const targetId = toNodeId(srcDir, resolved);
917
974
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
918
- addEdge(targetId, edgeTypeFor(targetId, isTypeOnly, names));
975
+ addEdge(targetId, edgeTypeFor(isTypeOnly, names));
919
976
  }
920
977
  }
921
978
  }
@@ -924,7 +981,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
924
981
  if (resolved) {
925
982
  const targetId = toNodeId(srcDir, resolved);
926
983
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
927
- addEdge(targetId, edgeTypeFor(targetId, isTypeOnly, names));
984
+ addEdge(targetId, edgeTypeFor(isTypeOnly, names));
928
985
  }
929
986
  }
930
987
  }
@@ -966,74 +1023,113 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
966
1023
  }
967
1024
  return { edges, flagged };
968
1025
  }
1026
+ function hasNextConfig(rootDir) {
1027
+ 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"));
1028
+ }
969
1029
  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"));
1030
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
1031
+ return paths !== null && hasNextConfig(rootDir);
971
1032
  }
972
1033
  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"]);
1034
+ const config = loadConfig(rootDir);
1035
+ const paths = resolveProjectPaths(rootDir, config);
1036
+ const srcDir = paths.srcDir;
1037
+ const apiDir = paths.apiDir;
1038
+ const appFiles = walk(paths.appDir, [".tsx", ".ts"]);
1039
+ const clientFiles = walk((0, import_node_path5.join)(srcDir, "client"), [".tsx", ".ts"]);
1040
+ const serverFiles = walk((0, import_node_path5.join)(srcDir, "server"), [".ts", ".tsx"]);
1041
+ const libFiles = walk((0, import_node_path5.join)(srcDir, "lib"), [".ts", ".tsx"]);
1042
+ const configFiles = walk((0, import_node_path5.join)(srcDir, "config"), [".ts", ".tsx"]);
983
1043
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
984
1044
  const parsedByPath = /* @__PURE__ */ new Map();
985
1045
  for (const absPath of allDiscovered) {
986
1046
  parsedByPath.set(absPath, parseFileTS(absPath));
987
1047
  }
988
1048
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
989
- const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
990
- const nodes = [];
1049
+ const uiNodes = [];
1050
+ const apiNodes = [];
991
1051
  const nodeIdSet = /* @__PURE__ */ new Set();
992
- const nodeTypeMap = /* @__PURE__ */ new Map();
993
1052
  const routeToNodeId = /* @__PURE__ */ new Map();
1053
+ const fileSet = allDiscovered.filter((f) => !(0, import_node_path5.basename)(f).startsWith("index."));
994
1054
  for (const absPath of fileSet) {
995
1055
  const id = toNodeId(srcDir, absPath);
996
1056
  const type = classifyType(absPath, id);
1057
+ if (type === "test" || type === "story") continue;
997
1058
  const parsed = parsedByPath.get(absPath);
998
1059
  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
- });
1060
+ const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
1012
1061
  nodeIdSet.add(id);
1013
- nodeTypeMap.set(id, type);
1014
- if (route) routeToNodeId.set(route, id);
1062
+ if (layer === "api") {
1063
+ const methods = [];
1064
+ for (const exp of parsed.exports) {
1065
+ if (HTTP_METHODS.has(exp)) methods.push(exp);
1066
+ }
1067
+ const dbCalls = extractDbCallsTS(absPath);
1068
+ const authWrappers = extractAuthWrappersTS(absPath);
1069
+ const deep = extractDeep(absPath);
1070
+ const routePath = (0, import_node_fs5.existsSync)(apiDir) ? filePathToApiRoute(apiDir, absPath) : `/api/${id.replace(/\/route\.tsx?$/, "")}`;
1071
+ const mutations = dbCalls.filter((c) => c.isMutation);
1072
+ const mutates = mutations.length > 0;
1073
+ const authStrategy = [...authWrappers];
1074
+ apiNodes.push({
1075
+ id,
1076
+ type: "endpoint",
1077
+ name: routePath,
1078
+ layer: "api",
1079
+ path: routePath,
1080
+ methods,
1081
+ handler: id,
1082
+ mutates,
1083
+ auth: authStrategy.length > 0 ? authStrategy : ["public"],
1084
+ db_models: [...new Set(dbCalls.map((c) => c.model))],
1085
+ db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
1086
+ conditions: deep.conditions,
1087
+ variables: deep.variables,
1088
+ responses: deep.responses,
1089
+ params: deep.params,
1090
+ _dbCalls: dbCalls
1091
+ // temp: used for cross-ref building below
1092
+ });
1093
+ } else {
1094
+ const route = extractRoute(id);
1095
+ const deep = extractDeep(absPath);
1096
+ uiNodes.push({
1097
+ id,
1098
+ type,
1099
+ name,
1100
+ layer: "ui",
1101
+ route,
1102
+ exports: parsed.exports,
1103
+ elements: deep.elements,
1104
+ stateVars: deep.stateVars,
1105
+ conditions: deep.conditions,
1106
+ variables: deep.variables
1107
+ });
1108
+ if (route) routeToNodeId.set(route, id);
1109
+ }
1015
1110
  }
1016
- const allEdges = [];
1017
- const allFlagged = [];
1111
+ const uiEdges = [];
1112
+ const uiFlagged = [];
1018
1113
  for (const absPath of fileSet) {
1019
- const sourceId = toNodeId(srcDir, absPath);
1114
+ const id = toNodeId(srcDir, absPath);
1115
+ if (!nodeIdSet.has(id)) continue;
1020
1116
  const parsed = parsedByPath.get(absPath);
1021
1117
  const { edges, flagged } = extractEdges(
1022
1118
  srcDir,
1023
1119
  absPath,
1024
- sourceId,
1120
+ id,
1025
1121
  parsed,
1026
1122
  nodeIdSet,
1027
- nodeTypeMap,
1028
1123
  barrelMaps,
1029
1124
  routeToNodeId
1030
1125
  );
1031
- allEdges.push(...edges);
1032
- allFlagged.push(...flagged);
1126
+ uiEdges.push(...edges);
1127
+ uiFlagged.push(...flagged);
1033
1128
  }
1034
1129
  const fetchCallEntries = [];
1035
1130
  for (const absPath of fileSet) {
1036
1131
  const sourceId = toNodeId(srcDir, absPath);
1132
+ if (!nodeIdSet.has(sourceId)) continue;
1037
1133
  const parsed = parsedByPath.get(absPath);
1038
1134
  if (parsed.fetchCalls.length === 0) continue;
1039
1135
  fetchCallEntries.push({
@@ -1048,7 +1144,7 @@ function generate(rootDir) {
1048
1144
  });
1049
1145
  }
1050
1146
  const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
1051
- const IGNORE_DIRS = /* @__PURE__ */ new Set([
1147
+ const IGNORE_DIRS2 = /* @__PURE__ */ new Set([
1052
1148
  "node_modules",
1053
1149
  ".next",
1054
1150
  "dist",
@@ -1061,7 +1157,7 @@ function generate(rootDir) {
1061
1157
  "out",
1062
1158
  ".vercel"
1063
1159
  ]);
1064
- const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS);
1160
+ const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS2);
1065
1161
  for (const absPath of externalCandidates) {
1066
1162
  const normalized = absPath.replace(/\\/g, "/");
1067
1163
  if (externalScanned.has(normalized)) continue;
@@ -1071,11 +1167,11 @@ function generate(rootDir) {
1071
1167
  } catch {
1072
1168
  continue;
1073
1169
  }
1074
- const externalId = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
1170
+ const externalId = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
1075
1171
  const edgesFromThis = [];
1076
1172
  const seen = /* @__PURE__ */ new Set();
1077
1173
  for (const imp of parsed.imports) {
1078
- const { specifier, isTypeOnly, names } = imp;
1174
+ const { specifier, names } = imp;
1079
1175
  let resolved = null;
1080
1176
  if (specifier.startsWith("@/")) {
1081
1177
  const relToSrc = specifier.slice(2);
@@ -1101,25 +1197,52 @@ function generate(rootDir) {
1101
1197
  const targetId = toNodeId(srcDir, resolved);
1102
1198
  if (!nodeIdSet.has(targetId)) continue;
1103
1199
  if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
1104
- const key = `${externalId}\u2192${targetId}\u2192${isTypeOnly ? "type" : "value"}`;
1200
+ const key = `${externalId}\u2192${targetId}`;
1105
1201
  if (seen.has(key)) continue;
1106
1202
  seen.add(key);
1107
1203
  edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
1108
1204
  }
1109
1205
  if (edgesFromThis.length === 0) continue;
1110
- nodes.push({
1206
+ uiNodes.push({
1111
1207
  id: externalId,
1112
1208
  type: "external",
1113
1209
  name: parsed.name || nameFromFilename(absPath),
1210
+ layer: "ui",
1114
1211
  route: null,
1115
1212
  exports: parsed.exports
1116
1213
  });
1117
1214
  nodeIdSet.add(externalId);
1118
- nodeTypeMap.set(externalId, "external");
1119
- allEdges.push(...edgesFromThis);
1215
+ uiEdges.push(...edgesFromThis);
1216
+ }
1217
+ const apiCrossRefs = [];
1218
+ for (const node of apiNodes) {
1219
+ const dbCalls = node._dbCalls;
1220
+ if (!dbCalls) continue;
1221
+ const seenModels = /* @__PURE__ */ new Set();
1222
+ for (const call of dbCalls) {
1223
+ if (seenModels.has(call.model)) continue;
1224
+ seenModels.add(call.model);
1225
+ apiCrossRefs.push({
1226
+ source: node.id,
1227
+ target: camelToPascal(call.model),
1228
+ type: call.isMutation ? "mutates" : "reads",
1229
+ layer: "db"
1230
+ });
1231
+ }
1232
+ delete node._dbCalls;
1233
+ }
1234
+ const apiNodeIds = new Set(apiNodes.map((n) => n.id));
1235
+ const apiEdges = [];
1236
+ const uiOnlyEdges = [];
1237
+ for (const edge of uiEdges) {
1238
+ if (apiNodeIds.has(edge.source)) {
1239
+ apiEdges.push(edge);
1240
+ } else {
1241
+ uiOnlyEdges.push(edge);
1242
+ }
1120
1243
  }
1121
1244
  const flaggedSet = /* @__PURE__ */ new Set();
1122
- const dedupedFlagged = allFlagged.filter((f) => {
1245
+ const dedupedFlagged = uiFlagged.filter((f) => {
1123
1246
  const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
1124
1247
  if (flaggedSet.has(key)) return false;
1125
1248
  flaggedSet.add(key);
@@ -1136,10 +1259,12 @@ function generate(rootDir) {
1136
1259
  hook: 7,
1137
1260
  lib: 8
1138
1261
  };
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 = {
1262
+ uiNodes.sort((a, b) => (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99) || a.id.localeCompare(b.id));
1263
+ uiOnlyEdges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1264
+ apiNodes.sort((a, b) => (a.path ?? "").localeCompare(b.path ?? ""));
1265
+ apiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1266
+ const byType = (t) => uiNodes.filter((n) => n.type === t).length;
1267
+ const uiStats = {
1143
1268
  total_pages: byType("page"),
1144
1269
  total_layouts: byType("layout"),
1145
1270
  total_components: byType("component"),
@@ -1150,196 +1275,122 @@ function generate(rootDir) {
1150
1275
  total_utils: byType("util"),
1151
1276
  total_libs: byType("lib"),
1152
1277
  total_external: byType("external"),
1153
- total_edges: allEdges.length,
1278
+ total_edges: uiOnlyEdges.length,
1154
1279
  total_flagged: dedupedFlagged.length
1155
1280
  };
1156
- return {
1281
+ const stripLayer = (nodes) => nodes.map(({ layer: _, ...rest }) => rest);
1282
+ const result = /* @__PURE__ */ new Map();
1283
+ result.set("ui", {
1157
1284
  metadata: {
1158
1285
  generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1159
1286
  scope: "main-app-only",
1160
1287
  app_root: "src/",
1161
1288
  layer: "ui",
1162
- parser: "react-nextjs-ast",
1163
- ...stats,
1289
+ parser: "typescript-project",
1290
+ ...uiStats,
1164
1291
  notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
1165
1292
  },
1166
- nodes,
1167
- edges: allEdges,
1293
+ nodes: stripLayer(uiNodes),
1294
+ edges: uiOnlyEdges,
1168
1295
  cross_refs: [],
1169
1296
  contradictions: [],
1170
1297
  warnings: [],
1171
1298
  flagged_edges: dedupedFlagged,
1172
1299
  patterns: {
1173
- total_nodes: nodes.length,
1174
- by_type: stats,
1300
+ total_nodes: uiNodes.length,
1301
+ by_type: uiStats,
1175
1302
  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
1303
+ renders: uiOnlyEdges.filter((e) => e.type === "renders").length,
1304
+ imports: uiOnlyEdges.filter((e) => e.type === "imports").length,
1305
+ navigates: uiOnlyEdges.filter((e) => e.type === "navigates").length
1179
1306
  },
1180
1307
  fetch_calls: fetchCallEntries
1181
1308
  }
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;
1309
+ });
1310
+ if (apiNodes.length > 0) {
1311
+ const mutatorNodes = apiNodes.filter((n) => n.mutates).length;
1312
+ const readOnlyNodes = apiNodes.filter((n) => !n.mutates).length;
1313
+ const authUsage = {};
1314
+ let endpointsWithAuth = 0;
1315
+ for (const n of apiNodes) {
1316
+ const auth = n.auth;
1317
+ if (auth.length > 0 && auth[0] !== "public") {
1318
+ endpointsWithAuth++;
1319
+ for (const w of auth) {
1320
+ authUsage[w] = (authUsage[w] ?? 0) + 1;
1321
+ }
1254
1322
  }
1255
1323
  }
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;
1324
+ const mutatorCount = {};
1325
+ for (const n of apiNodes) {
1326
+ const ops = n.db_operations;
1327
+ for (const op of ops) {
1328
+ const method = op.split(".")[1];
1329
+ if (method) mutatorCount[method] = (mutatorCount[method] ?? 0) + 1;
1330
+ }
1261
1331
  }
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
1332
+ result.set("api", {
1333
+ metadata: {
1334
+ generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1335
+ scope: "main-app-only",
1336
+ stack: "nextjs-app-router",
1337
+ layer: "api",
1338
+ parser: "typescript-project",
1339
+ total_endpoints: apiNodes.length,
1340
+ total_methods: apiNodes.reduce((sum, n) => sum + n.methods.length, 0),
1341
+ endpoints_with_auth: endpointsWithAuth,
1342
+ endpoints_with_db_access: apiNodes.filter((n) => n.db_models.length > 0).length,
1343
+ mutator_endpoints: mutatorNodes,
1344
+ read_only_endpoints: readOnlyNodes
1345
+ },
1346
+ nodes: stripLayer(apiNodes),
1347
+ edges: apiEdges,
1348
+ cross_refs: apiCrossRefs,
1349
+ contradictions: [],
1350
+ warnings: [],
1351
+ flagged_edges: [],
1352
+ patterns: {
1353
+ total_endpoints: apiNodes.length,
1354
+ methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
1355
+ acc[m] = apiNodes.filter((n) => n.methods.includes(m)).length;
1356
+ return acc;
1357
+ }, {}),
1358
+ auth_strategies: authUsage,
1359
+ mutation_operations: mutatorCount,
1360
+ mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
1361
+ }
1281
1362
  });
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
1363
  }
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
- };
1364
+ return result;
1329
1365
  }
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"() {
1366
+ var import_node_fs5, import_node_path5, HTTP_METHODS, CLASSIFICATION_TO_LAYER, typescriptProjectParser;
1367
+ var init_typescript_project = __esm({
1368
+ "src/server/graph/parsers/ts/typescript-project.ts"() {
1333
1369
  "use strict";
1334
1370
  import_node_fs5 = require("node:fs");
1335
1371
  import_node_path5 = require("node:path");
1372
+ init_config();
1373
+ init_resolve_paths();
1336
1374
  init_ts_extractor();
1337
1375
  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
1376
+ CLASSIFICATION_TO_LAYER = {
1377
+ endpoint: "api",
1378
+ page: "ui",
1379
+ layout: "ui",
1380
+ component: "ui",
1381
+ ui: "ui",
1382
+ hook: "ui",
1383
+ context: "ui",
1384
+ config: "ui",
1385
+ lib: "ui",
1386
+ "mcp-tool": "ui",
1387
+ external: "ui"
1388
+ };
1389
+ typescriptProjectParser = {
1390
+ id: "typescript-project",
1391
+ layers: ["ui", "api"],
1392
+ detect,
1393
+ generate
1343
1394
  };
1344
1395
  }
1345
1396
  });
@@ -1434,10 +1485,10 @@ function parseEnums(content) {
1434
1485
  }
1435
1486
  return nodes;
1436
1487
  }
1437
- function detect3(rootDir) {
1488
+ function detect2(rootDir) {
1438
1489
  return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "prisma", "schema.prisma"));
1439
1490
  }
1440
- function generate3(rootDir) {
1491
+ function generate2(rootDir) {
1441
1492
  const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
1442
1493
  const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
1443
1494
  const { nodes: modelNodes, relations } = parseModels(content);
@@ -1497,6 +1548,430 @@ var init_prisma_schema = __esm({
1497
1548
  prismaSchemaParser = {
1498
1549
  id: "prisma-schema",
1499
1550
  layer: "db",
1551
+ detect: detect2,
1552
+ generate: generate2
1553
+ };
1554
+ }
1555
+ });
1556
+
1557
+ // src/server/graph/parsers/db/sql-migrations.ts
1558
+ function pgTypeToPrisma(pgType) {
1559
+ const upper = pgType.toUpperCase().trim();
1560
+ return PG_TO_PRISMA[upper] ?? upper;
1561
+ }
1562
+ function parseCreateTable(sql, state) {
1563
+ const re = /CREATE\s+TABLE\s+"(\w+)"\s*\(([\s\S]*?)\);/gi;
1564
+ let m;
1565
+ while ((m = re.exec(sql)) !== null) {
1566
+ const tableName = m[1];
1567
+ const body = m[2];
1568
+ const columns = /* @__PURE__ */ new Map();
1569
+ let primaryCol = null;
1570
+ for (const line of body.split("\n")) {
1571
+ const trimmed = line.trim().replace(/,\s*$/, "");
1572
+ if (!trimmed || trimmed.startsWith("--")) continue;
1573
+ const pkMatch = trimmed.match(/CONSTRAINT\s+"[^"]+"\s+PRIMARY\s+KEY\s*\("(\w+)"\)/i);
1574
+ if (pkMatch) {
1575
+ primaryCol = pkMatch[1];
1576
+ continue;
1577
+ }
1578
+ if (/^\s*CONSTRAINT\s/i.test(trimmed)) continue;
1579
+ const colMatch = trimmed.match(/^"(\w+)"\s+(.+)/);
1580
+ if (!colMatch) continue;
1581
+ const colName = colMatch[1];
1582
+ let rest = colMatch[2];
1583
+ const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
1584
+ const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)(?:\s*,?\s*$)/i);
1585
+ const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
1586
+ let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim().replace(/,\s*$/, "").trim();
1587
+ columns.set(colName, {
1588
+ name: colName,
1589
+ type: colType,
1590
+ nullable: !isNotNull,
1591
+ primary: false,
1592
+ unique: false,
1593
+ default: defaultVal
1594
+ });
1595
+ }
1596
+ if (primaryCol && columns.has(primaryCol)) {
1597
+ columns.get(primaryCol).primary = true;
1598
+ }
1599
+ state.tables.set(tableName, { name: tableName, columns });
1600
+ }
1601
+ }
1602
+ function parseCreateEnum(sql, state) {
1603
+ const re = /CREATE\s+TYPE\s+"(\w+)"\s+AS\s+ENUM\s*\(([^)]+)\)/gi;
1604
+ let m;
1605
+ while ((m = re.exec(sql)) !== null) {
1606
+ const enumName = m[1];
1607
+ const valuesStr = m[2];
1608
+ const values = new Set(
1609
+ valuesStr.split(",").map((v) => v.trim().replace(/^'(.*)'$/, "$1")).filter(Boolean)
1610
+ );
1611
+ state.enums.set(enumName, { name: enumName, values });
1612
+ }
1613
+ }
1614
+ function parseAlterTable(sql, state) {
1615
+ const addColRe = /ALTER\s+TABLE\s+"(\w+)"\s+ADD\s+COLUMN\s+"(\w+)"\s+(.+?);/gi;
1616
+ let m;
1617
+ while ((m = addColRe.exec(sql)) !== null) {
1618
+ const tableName = m[1];
1619
+ const colName = m[2];
1620
+ let rest = m[3];
1621
+ const table = state.tables.get(tableName);
1622
+ if (!table) continue;
1623
+ const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
1624
+ const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)$/i);
1625
+ const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
1626
+ let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim();
1627
+ table.columns.set(colName, {
1628
+ name: colName,
1629
+ type: colType,
1630
+ nullable: !isNotNull,
1631
+ primary: false,
1632
+ unique: false,
1633
+ default: defaultVal
1634
+ });
1635
+ }
1636
+ const dropColRe = /ALTER\s+TABLE\s+"(\w+)"\s+DROP\s+COLUMN\s+"(\w+)"/gi;
1637
+ while ((m = dropColRe.exec(sql)) !== null) {
1638
+ const table = state.tables.get(m[1]);
1639
+ if (table) table.columns.delete(m[2]);
1640
+ }
1641
+ 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;
1642
+ while ((m = fkRe.exec(sql)) !== null) {
1643
+ state.fks.push({
1644
+ constraintName: m[2],
1645
+ sourceTable: m[1],
1646
+ sourceColumn: m[3],
1647
+ targetTable: m[4],
1648
+ targetColumn: m[5],
1649
+ onDelete: m[6] ?? null
1650
+ });
1651
+ }
1652
+ }
1653
+ function parseAlterEnum(sql, state) {
1654
+ const re = /ALTER\s+TYPE\s+"(\w+)"\s+ADD\s+VALUE\s+'([^']+)'/gi;
1655
+ let m;
1656
+ while ((m = re.exec(sql)) !== null) {
1657
+ const en = state.enums.get(m[1]);
1658
+ if (en) en.values.add(m[2]);
1659
+ }
1660
+ }
1661
+ function parseDropTable(sql, state) {
1662
+ const re = /DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?"(\w+)"/gi;
1663
+ let m;
1664
+ while ((m = re.exec(sql)) !== null) {
1665
+ state.tables.delete(m[1]);
1666
+ state.fks = state.fks.filter((fk) => fk.sourceTable !== m[1] && fk.targetTable !== m[1]);
1667
+ }
1668
+ }
1669
+ function parseUniqueIndex(sql, state) {
1670
+ const re = /CREATE\s+UNIQUE\s+INDEX\s+"[^"]+"\s+ON\s+"(\w+)"\("(\w+)"\)/gi;
1671
+ let m;
1672
+ while ((m = re.exec(sql)) !== null) {
1673
+ const table = state.tables.get(m[1]);
1674
+ const col = table?.columns.get(m[2]);
1675
+ if (col) col.unique = true;
1676
+ if (!state.uniqueIndexes.has(m[1])) state.uniqueIndexes.set(m[1], /* @__PURE__ */ new Set());
1677
+ state.uniqueIndexes.get(m[1]).add(m[2]);
1678
+ }
1679
+ }
1680
+ function parseMigrations(rootDir) {
1681
+ const migrationsDir = (0, import_node_path7.join)(rootDir, "prisma", "migrations");
1682
+ const state = {
1683
+ tables: /* @__PURE__ */ new Map(),
1684
+ enums: /* @__PURE__ */ new Map(),
1685
+ fks: [],
1686
+ uniqueIndexes: /* @__PURE__ */ new Map()
1687
+ };
1688
+ if (!(0, import_node_fs7.existsSync)(migrationsDir)) return state;
1689
+ const dirs = (0, import_node_fs7.readdirSync)(migrationsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
1690
+ for (const dir of dirs) {
1691
+ const sqlPath = (0, import_node_path7.join)(migrationsDir, dir, "migration.sql");
1692
+ if (!(0, import_node_fs7.existsSync)(sqlPath)) continue;
1693
+ const sql = (0, import_node_fs7.readFileSync)(sqlPath, "utf-8");
1694
+ parseCreateEnum(sql, state);
1695
+ parseCreateTable(sql, state);
1696
+ parseAlterTable(sql, state);
1697
+ parseAlterEnum(sql, state);
1698
+ parseDropTable(sql, state);
1699
+ parseUniqueIndex(sql, state);
1700
+ }
1701
+ return state;
1702
+ }
1703
+ function loadPrismaState(rootDir) {
1704
+ const schemaPath = (0, import_node_path7.join)(rootDir, "prisma", "schema.prisma");
1705
+ if (!(0, import_node_fs7.existsSync)(schemaPath)) return null;
1706
+ const content = (0, import_node_fs7.readFileSync)(schemaPath, "utf-8");
1707
+ const tables = /* @__PURE__ */ new Map();
1708
+ const enums = /* @__PURE__ */ new Map();
1709
+ const relations = [];
1710
+ const modelRe = /model\s+(\w+)\s*\{([^}]+)\}/g;
1711
+ let m;
1712
+ while ((m = modelRe.exec(content)) !== null) {
1713
+ const modelName = m[1];
1714
+ const body = m[2];
1715
+ const cols = [];
1716
+ for (const line of body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("@@"))) {
1717
+ const fm = line.match(/^(\w+)\s+(\S+)(.*)/);
1718
+ if (!fm) continue;
1719
+ const fieldName = fm[1];
1720
+ const fieldType = fm[2];
1721
+ const rest = fm[3] ?? "";
1722
+ const baseType = fieldType.replace(/[?\[\]]/g, "");
1723
+ const isRelation = rest.includes("@relation");
1724
+ if (isRelation) {
1725
+ const relArgs = rest.match(/@relation\(([^)]*)\)/)?.[1] ?? "";
1726
+ const fieldsMatch = relArgs.match(/fields:\s*\[([^\]]+)\]/);
1727
+ if (fieldsMatch) {
1728
+ relations.push({ source: modelName, target: baseType, fk: fieldsMatch[1].trim() });
1729
+ }
1730
+ continue;
1731
+ }
1732
+ if (fieldType.endsWith("[]") || fieldType.endsWith("?") && content.includes(`model ${baseType}`)) {
1733
+ if (new RegExp(`model\\s+${baseType}\\s*\\{`).test(content)) continue;
1734
+ }
1735
+ cols.push({
1736
+ name: fieldName,
1737
+ type: fieldType.replace("?", ""),
1738
+ nullable: fieldType.endsWith("?") || fieldType.includes("?"),
1739
+ primary: rest.includes("@id"),
1740
+ unique: rest.includes("@unique")
1741
+ });
1742
+ }
1743
+ tables.set(modelName, { columns: cols });
1744
+ }
1745
+ const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
1746
+ while ((m = enumRe.exec(content)) !== null) {
1747
+ const values = m[2].split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
1748
+ enums.set(m[1], values);
1749
+ }
1750
+ return { tables, enums, relations };
1751
+ }
1752
+ function verify(sqlState, prisma) {
1753
+ const contradictions = [];
1754
+ const flaggedEdges = [];
1755
+ for (const [name] of sqlState.tables) {
1756
+ if (!prisma.tables.has(name)) {
1757
+ contradictions.push({
1758
+ entity: name,
1759
+ source_a: "sql-migrations",
1760
+ source_b: "prisma-schema",
1761
+ detail: `Table "${name}" exists in migrations but not in schema.prisma`
1762
+ });
1763
+ }
1764
+ }
1765
+ for (const [name] of prisma.tables) {
1766
+ if (!sqlState.tables.has(name)) {
1767
+ contradictions.push({
1768
+ entity: name,
1769
+ source_a: "prisma-schema",
1770
+ source_b: "sql-migrations",
1771
+ detail: `Table "${name}" in schema.prisma has no CREATE TABLE in migrations`
1772
+ });
1773
+ }
1774
+ }
1775
+ for (const [tableName, prismaTable] of prisma.tables) {
1776
+ const sqlTable = sqlState.tables.get(tableName);
1777
+ if (!sqlTable) continue;
1778
+ for (const prismaCol of prismaTable.columns) {
1779
+ const sqlCol = sqlTable.columns.get(prismaCol.name);
1780
+ if (!sqlCol) {
1781
+ contradictions.push({
1782
+ entity: `${tableName}.${prismaCol.name}`,
1783
+ source_a: "prisma-schema",
1784
+ source_b: "sql-migrations",
1785
+ detail: `Column "${tableName}.${prismaCol.name}" in schema.prisma but not in migrations`
1786
+ });
1787
+ continue;
1788
+ }
1789
+ const mappedSqlType = pgTypeToPrisma(sqlCol.type);
1790
+ const prismaBaseType = prismaCol.type.replace(/[?\[\]]/g, "");
1791
+ if (mappedSqlType !== prismaBaseType && mappedSqlType !== prismaCol.type) {
1792
+ if (!sqlState.enums.has(prismaBaseType)) {
1793
+ contradictions.push({
1794
+ entity: `${tableName}.${prismaCol.name}`,
1795
+ source_a: "sql-migrations",
1796
+ source_b: "prisma-schema",
1797
+ detail: `Column "${tableName}.${prismaCol.name}": SQL type "${sqlCol.type}" (\u2192${mappedSqlType}) vs Prisma type "${prismaCol.type}"`
1798
+ });
1799
+ }
1800
+ }
1801
+ if (sqlCol.nullable !== prismaCol.nullable) {
1802
+ contradictions.push({
1803
+ entity: `${tableName}.${prismaCol.name}`,
1804
+ source_a: "sql-migrations",
1805
+ source_b: "prisma-schema",
1806
+ detail: `Column "${tableName}.${prismaCol.name}": ${sqlCol.nullable ? "nullable" : "NOT NULL"} in SQL vs ${prismaCol.nullable ? "optional" : "required"} in Prisma`
1807
+ });
1808
+ }
1809
+ }
1810
+ for (const [colName] of sqlTable.columns) {
1811
+ const inPrisma = prismaTable.columns.some((c) => c.name === colName);
1812
+ if (!inPrisma) {
1813
+ contradictions.push({
1814
+ entity: `${tableName}.${colName}`,
1815
+ source_a: "sql-migrations",
1816
+ source_b: "prisma-schema",
1817
+ detail: `Column "${tableName}.${colName}" in migrations but not in schema.prisma`
1818
+ });
1819
+ }
1820
+ }
1821
+ }
1822
+ for (const [name, sqlEnum] of sqlState.enums) {
1823
+ const prismaValues = prisma.enums.get(name);
1824
+ if (!prismaValues) {
1825
+ contradictions.push({
1826
+ entity: name,
1827
+ source_a: "sql-migrations",
1828
+ source_b: "prisma-schema",
1829
+ detail: `Enum "${name}" exists in migrations but not in schema.prisma`
1830
+ });
1831
+ continue;
1832
+ }
1833
+ const prismaSet = new Set(prismaValues);
1834
+ for (const val of sqlEnum.values) {
1835
+ if (!prismaSet.has(val)) {
1836
+ contradictions.push({
1837
+ entity: `${name}.${val}`,
1838
+ source_a: "sql-migrations",
1839
+ source_b: "prisma-schema",
1840
+ detail: `Enum "${name}": value "${val}" in migrations but not in schema.prisma`
1841
+ });
1842
+ }
1843
+ }
1844
+ for (const val of prismaValues) {
1845
+ if (!sqlEnum.values.has(val)) {
1846
+ contradictions.push({
1847
+ entity: `${name}.${val}`,
1848
+ source_a: "prisma-schema",
1849
+ source_b: "sql-migrations",
1850
+ detail: `Enum "${name}": value "${val}" in schema.prisma but not in migrations`
1851
+ });
1852
+ }
1853
+ }
1854
+ }
1855
+ const prismaFkSet = new Set(prisma.relations.map((r) => `${r.source}|${r.target}|${r.fk}`));
1856
+ for (const fk of sqlState.fks) {
1857
+ const key = `${fk.sourceTable}|${fk.targetTable}|${fk.sourceColumn}`;
1858
+ if (!prismaFkSet.has(key)) {
1859
+ flaggedEdges.push({
1860
+ source: fk.sourceTable,
1861
+ target: fk.targetTable,
1862
+ type: "belongs_to",
1863
+ label: `FK "${fk.constraintName}" (${fk.sourceColumn}\u2192${fk.targetTable}) in migrations but no @relation in schema.prisma`,
1864
+ confidence: "high"
1865
+ });
1866
+ }
1867
+ }
1868
+ return { contradictions, flaggedEdges };
1869
+ }
1870
+ function detect3(rootDir) {
1871
+ const migrationsDir = (0, import_node_path7.join)(rootDir, "prisma", "migrations");
1872
+ if (!(0, import_node_fs7.existsSync)(migrationsDir)) return false;
1873
+ 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")));
1874
+ }
1875
+ function generate3(rootDir) {
1876
+ const sqlState = parseMigrations(rootDir);
1877
+ const prisma = loadPrismaState(rootDir);
1878
+ const prismaTableIds = prisma ? new Set(prisma.tables.keys()) : /* @__PURE__ */ new Set();
1879
+ const prismaEnumIds = prisma ? new Set(prisma.enums.keys()) : /* @__PURE__ */ new Set();
1880
+ const nodes = [];
1881
+ for (const [name, table] of sqlState.tables) {
1882
+ if (prismaTableIds.has(name)) continue;
1883
+ nodes.push({
1884
+ id: name,
1885
+ type: "table",
1886
+ name,
1887
+ source: "sql",
1888
+ columns: [...table.columns.values()].map((c) => ({
1889
+ name: c.name,
1890
+ type: c.type,
1891
+ primary: c.primary,
1892
+ unique: c.unique,
1893
+ nullable: c.nullable,
1894
+ default: c.default,
1895
+ isRelation: false,
1896
+ comment: null
1897
+ }))
1898
+ });
1899
+ }
1900
+ for (const [name, sqlEnum] of sqlState.enums) {
1901
+ if (prismaEnumIds.has(name)) continue;
1902
+ nodes.push({
1903
+ id: name,
1904
+ type: "enum",
1905
+ name,
1906
+ source: "sql",
1907
+ values: [...sqlEnum.values]
1908
+ });
1909
+ }
1910
+ const sqlOnlyTables = new Set(nodes.filter((n) => n.type === "table").map((n) => n.id));
1911
+ const edges = sqlState.fks.filter((fk) => sqlOnlyTables.has(fk.sourceTable)).map((fk) => ({
1912
+ source: fk.sourceTable,
1913
+ target: fk.targetTable,
1914
+ type: "belongs_to",
1915
+ fk: fk.sourceColumn,
1916
+ onDelete: fk.onDelete
1917
+ }));
1918
+ const { contradictions, flaggedEdges } = prisma ? verify(sqlState, prisma) : { contradictions: [], flaggedEdges: [] };
1919
+ return {
1920
+ metadata: {
1921
+ generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1922
+ scope: "sql-migrations",
1923
+ source: "prisma/migrations/",
1924
+ layer: "db",
1925
+ sql_tables: sqlState.tables.size,
1926
+ sql_enums: sqlState.enums.size,
1927
+ sql_fks: sqlState.fks.length,
1928
+ additive_nodes: nodes.length,
1929
+ contradictions_found: contradictions.length,
1930
+ flagged_edges_found: flaggedEdges.length
1931
+ },
1932
+ nodes,
1933
+ edges,
1934
+ cross_refs: [],
1935
+ contradictions,
1936
+ warnings: [],
1937
+ flagged_edges: flaggedEdges
1938
+ };
1939
+ }
1940
+ var import_node_fs7, import_node_path7, PG_TO_PRISMA, sqlMigrationsParser;
1941
+ var init_sql_migrations = __esm({
1942
+ "src/server/graph/parsers/db/sql-migrations.ts"() {
1943
+ "use strict";
1944
+ import_node_fs7 = require("node:fs");
1945
+ import_node_path7 = require("node:path");
1946
+ PG_TO_PRISMA = {
1947
+ "TEXT": "String",
1948
+ "VARCHAR": "String",
1949
+ "CHAR": "String",
1950
+ "INTEGER": "Int",
1951
+ "INT": "Int",
1952
+ "SMALLINT": "Int",
1953
+ "BIGINT": "BigInt",
1954
+ "SERIAL": "Int",
1955
+ "BOOLEAN": "Boolean",
1956
+ "BOOL": "Boolean",
1957
+ "TIMESTAMP(3)": "DateTime",
1958
+ "TIMESTAMP": "DateTime",
1959
+ "TIMESTAMPTZ": "DateTime",
1960
+ "DATE": "DateTime",
1961
+ "DOUBLE PRECISION": "Float",
1962
+ "FLOAT": "Float",
1963
+ "REAL": "Float",
1964
+ "DECIMAL": "Decimal",
1965
+ "NUMERIC": "Decimal",
1966
+ "JSONB": "Json",
1967
+ "JSON": "Json",
1968
+ "BYTEA": "Bytes",
1969
+ "UUID": "String",
1970
+ "TEXT[]": "String[]"
1971
+ };
1972
+ sqlMigrationsParser = {
1973
+ id: "sql-migrations",
1974
+ layer: "db",
1500
1975
  detect: detect3,
1501
1976
  generate: generate3
1502
1977
  };
@@ -1728,36 +2203,36 @@ var init_fetch_resolver = __esm({
1728
2203
  });
1729
2204
 
1730
2205
  // src/server/graph/parsers/crosslayer/api-annotations.ts
1731
- function walk3(dir, exts) {
1732
- if (!(0, import_node_fs7.existsSync)(dir)) return [];
2206
+ function walk2(dir, exts) {
2207
+ if (!(0, import_node_fs8.existsSync)(dir)) return [];
1733
2208
  const results = [];
1734
- for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
2209
+ for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
1735
2210
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1736
- const full = (0, import_node_path7.join)(dir, entry.name);
2211
+ const full = (0, import_node_path8.join)(dir, entry.name);
1737
2212
  if (entry.isDirectory()) {
1738
- results.push(...walk3(full, exts));
1739
- } else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
2213
+ results.push(...walk2(full, exts));
2214
+ } else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
1740
2215
  results.push(full);
1741
2216
  }
1742
2217
  }
1743
2218
  return results;
1744
2219
  }
1745
2220
  function toNodeId2(srcDir, absPath) {
1746
- return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
2221
+ return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
1747
2222
  }
1748
- var import_node_fs7, import_node_path7, API_ANNOTATION_RE, apiAnnotationsParser;
2223
+ var import_node_fs8, import_node_path8, API_ANNOTATION_RE, apiAnnotationsParser;
1749
2224
  var init_api_annotations = __esm({
1750
2225
  "src/server/graph/parsers/crosslayer/api-annotations.ts"() {
1751
2226
  "use strict";
1752
- import_node_fs7 = require("node:fs");
1753
- import_node_path7 = require("node:path");
2227
+ import_node_fs8 = require("node:fs");
2228
+ import_node_path8 = require("node:path");
1754
2229
  init_api_route_matching();
1755
2230
  API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
1756
2231
  apiAnnotationsParser = {
1757
2232
  id: "api-annotations",
1758
2233
  layer: "crosslayer",
1759
2234
  detect(rootDir) {
1760
- return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
2235
+ return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
1761
2236
  },
1762
2237
  generate(rootDir, layerOutputs) {
1763
2238
  const apiOutput = layerOutputs.get("api");
@@ -1768,13 +2243,13 @@ var init_api_annotations = __esm({
1768
2243
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1769
2244
  const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1770
2245
  const apiPathMap = buildApiPathMap(apiRoutes);
1771
- const srcDir = (0, import_node_path7.join)(rootDir, "src");
1772
- const files = walk3(srcDir, [".ts", ".tsx"]);
2246
+ const srcDir = (0, import_node_path8.join)(rootDir, "src");
2247
+ const files = walk2(srcDir, [".ts", ".tsx"]);
1773
2248
  const crossRefs = [];
1774
2249
  const flaggedEdges = [];
1775
2250
  const seen = /* @__PURE__ */ new Set();
1776
2251
  for (const absPath of files) {
1777
- const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
2252
+ const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
1778
2253
  const sourceId = toNodeId2(srcDir, absPath);
1779
2254
  if (!uiNodeIds.has(sourceId)) continue;
1780
2255
  let match;
@@ -1820,73 +2295,741 @@ var init_api_annotations = __esm({
1820
2295
  });
1821
2296
 
1822
2297
  // src/server/graph/parsers/crosslayer/url-literal-scanner.ts
1823
- function walk4(dir, exts) {
1824
- if (!(0, import_node_fs8.existsSync)(dir)) return [];
2298
+ function walk3(dir, exts) {
2299
+ if (!(0, import_node_fs9.existsSync)(dir)) return [];
1825
2300
  const results = [];
1826
- for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
2301
+ for (const entry of (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true })) {
1827
2302
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1828
- const full = (0, import_node_path8.join)(dir, entry.name);
2303
+ const full = (0, import_node_path9.join)(dir, entry.name);
1829
2304
  if (entry.isDirectory()) {
1830
- results.push(...walk4(full, exts));
1831
- } else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
2305
+ results.push(...walk3(full, exts));
2306
+ } else if (exts.includes((0, import_node_path9.extname)(entry.name))) {
1832
2307
  results.push(full);
1833
2308
  }
1834
2309
  }
1835
- return results;
1836
- }
1837
- function toNodeId3(srcDir, absPath) {
1838
- return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
2310
+ return results;
2311
+ }
2312
+ function toNodeId3(srcDir, absPath) {
2313
+ return (0, import_node_path9.relative)(srcDir, absPath).replace(/\\/g, "/");
2314
+ }
2315
+ var import_node_fs9, import_node_path9, URL_LITERAL_RE, urlLiteralScannerParser;
2316
+ var init_url_literal_scanner = __esm({
2317
+ "src/server/graph/parsers/crosslayer/url-literal-scanner.ts"() {
2318
+ "use strict";
2319
+ import_node_fs9 = require("node:fs");
2320
+ import_node_path9 = require("node:path");
2321
+ init_api_route_matching();
2322
+ init_config();
2323
+ init_resolve_paths();
2324
+ URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
2325
+ urlLiteralScannerParser = {
2326
+ id: "url-literal-scanner",
2327
+ layer: "crosslayer",
2328
+ detect(rootDir) {
2329
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2330
+ return paths !== null;
2331
+ },
2332
+ generate(rootDir, layerOutputs) {
2333
+ const apiOutput = layerOutputs.get("api");
2334
+ if (!apiOutput) {
2335
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
2336
+ }
2337
+ const uiOutput = layerOutputs.get("ui");
2338
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
2339
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
2340
+ const apiPathMap = buildApiPathMap(apiRoutes);
2341
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2342
+ const srcDir = paths.srcDir;
2343
+ const clientDir = (0, import_node_path9.join)(srcDir, "client");
2344
+ const files = [
2345
+ ...walk3(clientDir, [".ts", ".tsx"]),
2346
+ ...walk3(paths.appDir, [".ts", ".tsx"])
2347
+ ];
2348
+ const crossRefs = [];
2349
+ const seen = /* @__PURE__ */ new Set();
2350
+ for (const absPath of files) {
2351
+ const sourceId = toNodeId3(srcDir, absPath);
2352
+ if (!uiNodeIds.has(sourceId)) continue;
2353
+ const content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
2354
+ let match;
2355
+ URL_LITERAL_RE.lastIndex = 0;
2356
+ while ((match = URL_LITERAL_RE.exec(content)) !== null) {
2357
+ const urlPath = match[1];
2358
+ const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
2359
+ if (result.kind === "resolved" && result.nodeId) {
2360
+ const key = `${sourceId}|${result.nodeId}|references_api`;
2361
+ if (seen.has(key)) continue;
2362
+ seen.add(key);
2363
+ crossRefs.push({
2364
+ source: sourceId,
2365
+ target: result.nodeId,
2366
+ type: "references_api",
2367
+ layer: "api"
2368
+ });
2369
+ }
2370
+ }
2371
+ }
2372
+ return {
2373
+ cross_refs: crossRefs,
2374
+ flagged_edges: [],
2375
+ warnings: [],
2376
+ patterns: {
2377
+ url_literals_resolved: crossRefs.length
2378
+ }
2379
+ };
2380
+ }
2381
+ };
2382
+ }
2383
+ });
2384
+
2385
+ // src/server/graph/parsers/static/static-values.ts
2386
+ function tryLoadTreeSitter() {
2387
+ if (parseCode) return true;
2388
+ try {
2389
+ const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
2390
+ if (typeof extractor.parseCodeTS === "function") {
2391
+ parseCode = extractor.parseCodeTS;
2392
+ return true;
2393
+ }
2394
+ } catch {
2395
+ }
2396
+ return false;
2397
+ }
2398
+ function classifyScope(source, model) {
2399
+ if (source.includes("prisma/schema.prisma")) return "shared";
2400
+ if (source.includes("prisma/seed") && model) {
2401
+ if (SHARED_MODELS.has(model)) return "shared";
2402
+ if (DB_MODELS.has(model)) return "db";
2403
+ return "shared";
2404
+ }
2405
+ if (source.startsWith("src/client/") || source.startsWith("src/app/")) return "fe";
2406
+ if (source.startsWith("src/server/")) return "be";
2407
+ if (source.startsWith("src/config/")) return "be";
2408
+ if (source.startsWith("src/lib/")) return "shared";
2409
+ return "shared";
2410
+ }
2411
+ function extractEnumValues(rootDir) {
2412
+ const nodes = [];
2413
+ const edges = [];
2414
+ const schemaPaths = [
2415
+ (0, import_node_path10.join)(rootDir, "prisma", "schema.prisma"),
2416
+ (0, import_node_path10.join)(rootDir, "prisma", "schema")
2417
+ ];
2418
+ let content = "";
2419
+ for (const p of schemaPaths) {
2420
+ if ((0, import_node_fs10.existsSync)(p)) {
2421
+ try {
2422
+ const stat = (0, import_node_fs10.statSync)(p);
2423
+ if (stat.isFile()) {
2424
+ content = (0, import_node_fs10.readFileSync)(p, "utf-8");
2425
+ } else if (stat.isDirectory()) {
2426
+ const files = (0, import_node_fs10.readdirSync)(p).filter((f) => f.endsWith(".prisma"));
2427
+ content = files.map((f) => (0, import_node_fs10.readFileSync)((0, import_node_path10.join)(p, f), "utf-8")).join("\n");
2428
+ }
2429
+ } catch {
2430
+ continue;
2431
+ }
2432
+ break;
2433
+ }
2434
+ }
2435
+ if (!content) return { nodes, edges };
2436
+ const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
2437
+ let m;
2438
+ while ((m = enumRe.exec(content)) !== null) {
2439
+ const enumName = m[1];
2440
+ const body = m[2];
2441
+ const values = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
2442
+ const scope = classifyScope("prisma/schema.prisma");
2443
+ for (const val of values) {
2444
+ const nodeId = `${enumName}.${val}`;
2445
+ nodes.push({
2446
+ id: nodeId,
2447
+ type: "enum_value",
2448
+ name: val,
2449
+ parentEnum: enumName,
2450
+ value: val,
2451
+ scope,
2452
+ source: "prisma/schema.prisma"
2453
+ });
2454
+ edges.push({
2455
+ source: nodeId,
2456
+ target: `enum:${enumName}`,
2457
+ type: "member_of"
2458
+ });
2459
+ }
2460
+ nodes.push({
2461
+ id: `enum:${enumName}`,
2462
+ type: "enum_group",
2463
+ name: enumName,
2464
+ valueCount: values.length,
2465
+ scope,
2466
+ source: "prisma/schema.prisma"
2467
+ });
2468
+ }
2469
+ return { nodes, edges };
2470
+ }
2471
+ function extractPropsFromObjectNode(node) {
2472
+ const props = {};
2473
+ for (const child of node.namedChildren) {
2474
+ if (child.type !== "pair") continue;
2475
+ const keyNode = child.childForFieldName("key");
2476
+ const valNode = child.childForFieldName("value");
2477
+ if (!keyNode || !valNode) continue;
2478
+ const key = keyNode.type === "property_identifier" ? keyNode.text : keyNode.text.replace(/['"]/g, "");
2479
+ if (valNode.type === "string" || valNode.type === "template_string") {
2480
+ const fragment = valNode.namedChildren.find((c) => c.type === "string_fragment" || c.type === "template_substitution");
2481
+ props[key] = fragment ? fragment.text : valNode.text.replace(/^['"`]|['"`]$/g, "");
2482
+ } else if (valNode.type === "number") {
2483
+ props[key] = valNode.text;
2484
+ } else if (valNode.type === "true" || valNode.type === "false") {
2485
+ props[key] = valNode.text;
2486
+ }
2487
+ }
2488
+ return props;
2489
+ }
2490
+ function extractStringArrayFromNode(node) {
2491
+ const values = [];
2492
+ for (const child of node.namedChildren) {
2493
+ if (child.type === "string") {
2494
+ const frag = child.namedChildren.find((c) => c.type === "string_fragment");
2495
+ if (frag) values.push(frag.text);
2496
+ }
2497
+ }
2498
+ return values;
2499
+ }
2500
+ function findArrayDecl(root, varName) {
2501
+ function walk5(node) {
2502
+ if (node.type === "variable_declarator") {
2503
+ const nameNode = node.childForFieldName("name");
2504
+ const valueNode = node.childForFieldName("value");
2505
+ if (nameNode?.text === varName && valueNode?.type === "array") {
2506
+ return valueNode;
2507
+ }
2508
+ if (nameNode?.text === varName && valueNode?.type === "as_expression") {
2509
+ const inner = valueNode.namedChildren.find((c) => c.type === "array");
2510
+ if (inner) return inner;
2511
+ }
2512
+ }
2513
+ for (const child of node.namedChildren) {
2514
+ const found = walk5(child);
2515
+ if (found) return found;
2516
+ }
2517
+ return null;
2518
+ }
2519
+ return walk5(root);
2520
+ }
2521
+ function extractObjectPropsRegex(objStr) {
2522
+ const props = {};
2523
+ const propRe = /(\w+):\s*['"]([^'"]*)['"]/g;
2524
+ let m;
2525
+ while ((m = propRe.exec(objStr)) !== null) props[m[1]] = m[2];
2526
+ const numRe = /(\w+):\s*(-?\d+(?:\.\d+)?)\s*[,\n}]/g;
2527
+ while ((m = numRe.exec(objStr)) !== null) if (!props[m[1]]) props[m[1]] = m[2];
2528
+ return props;
2529
+ }
2530
+ function extractStringArrayRegex(arrStr) {
2531
+ return (arrStr.match(/'([^']+)'/g) ?? []).map((s) => s.replace(/'/g, ""));
2532
+ }
2533
+ function splitArrayObjectsRegex(arrayBody) {
2534
+ const objects = [];
2535
+ let depth = 0;
2536
+ let start = -1;
2537
+ for (let i = 0; i < arrayBody.length; i++) {
2538
+ if (arrayBody[i] === "{") {
2539
+ if (depth === 0) start = i;
2540
+ depth++;
2541
+ } else if (arrayBody[i] === "}") {
2542
+ depth--;
2543
+ if (depth === 0 && start >= 0) {
2544
+ objects.push(arrayBody.slice(start, i + 1));
2545
+ start = -1;
2546
+ }
2547
+ }
2548
+ }
2549
+ return objects;
2550
+ }
2551
+ function detectSeededArrays(content, sourceFile) {
2552
+ const results = [];
2553
+ const forOfRe = /for\s*\(\s*const\s+\w+\s+of\s+(\w+)\s*\)\s*\{/g;
2554
+ let fm;
2555
+ while ((fm = forOfRe.exec(content)) !== null) {
2556
+ const arrayName = fm[1];
2557
+ const lookahead = content.slice(fm.index + fm[0].length, fm.index + fm[0].length + 500);
2558
+ const prismaMatch = lookahead.match(/prisma\.(\w+)\.(create|upsert|update|createMany|findFirst)/);
2559
+ if (!prismaMatch) continue;
2560
+ results.push({ arrayName, prismaModel: prismaMatch[1], sourceFile });
2561
+ }
2562
+ return results;
2563
+ }
2564
+ function pickIdField(props) {
2565
+ for (const key of ["key", "slug", "id", "name", "templateId"]) {
2566
+ if (props[key]) return props[key];
2567
+ }
2568
+ return null;
2569
+ }
2570
+ function pickNameField(props) {
2571
+ for (const key of ["name", "slug", "key", "id"]) {
2572
+ if (props[key]) return props[key];
2573
+ }
2574
+ return null;
2575
+ }
2576
+ function modelToNodeType(model) {
2577
+ return `seed_${model.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "")}`;
2578
+ }
2579
+ function extractSeedData(rootDir) {
2580
+ const nodes = [];
2581
+ const edges = [];
2582
+ const seedFiles = [
2583
+ (0, import_node_path10.join)(rootDir, "prisma", "seed.ts"),
2584
+ (0, import_node_path10.join)(rootDir, "prisma", "seed.js"),
2585
+ (0, import_node_path10.join)(rootDir, "src", "server", "lib", "system-tags.ts")
2586
+ ].filter(import_node_fs10.existsSync);
2587
+ const useTreeSitter = tryLoadTreeSitter();
2588
+ for (const filePath of seedFiles) {
2589
+ const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
2590
+ const relPath = (0, import_node_path10.relative)(rootDir, filePath);
2591
+ const seeded = detectSeededArrays(content, relPath);
2592
+ let astRoot = null;
2593
+ if (useTreeSitter && parseCode) {
2594
+ try {
2595
+ const tree = parseCode(content);
2596
+ astRoot = tree.rootNode;
2597
+ } catch {
2598
+ }
2599
+ }
2600
+ for (const { arrayName, prismaModel, sourceFile } of seeded) {
2601
+ const nodeType = modelToNodeType(prismaModel);
2602
+ const scope = classifyScope(sourceFile, prismaModel);
2603
+ if (astRoot) {
2604
+ const arrayNode = findArrayDecl(astRoot, arrayName);
2605
+ if (!arrayNode) continue;
2606
+ for (const child of arrayNode.namedChildren) {
2607
+ if (child.type !== "object") continue;
2608
+ const props = extractPropsFromObjectNode(child);
2609
+ const idVal = pickIdField(props);
2610
+ if (!idVal) continue;
2611
+ const nameVal = pickNameField(props) ?? idVal;
2612
+ const nodeId = `seed:${prismaModel}:${idVal}`;
2613
+ const { scope: _seedScope, ...safeProps } = props;
2614
+ nodes.push({
2615
+ id: nodeId,
2616
+ type: nodeType,
2617
+ name: nameVal,
2618
+ value: idVal,
2619
+ model: prismaModel,
2620
+ ...safeProps,
2621
+ seedScope: _seedScope ?? void 0,
2622
+ // preserve as seedScope
2623
+ scope,
2624
+ source: sourceFile
2625
+ });
2626
+ const permsArrayNode = child.namedChildren.filter((c) => c.type === "pair").find((c) => c.childForFieldName("key")?.text === "permissions");
2627
+ if (permsArrayNode) {
2628
+ const valNode = permsArrayNode.childForFieldName("value");
2629
+ if (valNode?.type === "array") {
2630
+ const permKeys = extractStringArrayFromNode(valNode);
2631
+ for (const pk of permKeys) {
2632
+ if (pk === "*") continue;
2633
+ edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
2634
+ }
2635
+ }
2636
+ }
2637
+ }
2638
+ } else {
2639
+ const constRe = new RegExp(`const\\s+${arrayName}\\s*(?::[^=]+)?=\\s*\\[`, "g");
2640
+ const cm = constRe.exec(content);
2641
+ if (!cm) continue;
2642
+ const bracketStart = cm.index + cm[0].length - 1;
2643
+ let depth = 1, i = bracketStart + 1;
2644
+ while (i < content.length && depth > 0) {
2645
+ if (content[i] === "[") depth++;
2646
+ else if (content[i] === "]") depth--;
2647
+ i++;
2648
+ }
2649
+ if (depth !== 0) continue;
2650
+ const body = content.slice(bracketStart + 1, i - 1);
2651
+ const objects = splitArrayObjectsRegex(body);
2652
+ for (const objStr of objects) {
2653
+ const props = extractObjectPropsRegex(objStr);
2654
+ const idVal = pickIdField(props);
2655
+ if (!idVal) continue;
2656
+ const nameVal = pickNameField(props) ?? idVal;
2657
+ const nodeId = `seed:${prismaModel}:${idVal}`;
2658
+ const { scope: _rScope, ...rSafeProps } = props;
2659
+ nodes.push({
2660
+ id: nodeId,
2661
+ type: nodeType,
2662
+ name: nameVal,
2663
+ value: idVal,
2664
+ model: prismaModel,
2665
+ ...rSafeProps,
2666
+ seedScope: _rScope ?? void 0,
2667
+ scope,
2668
+ source: sourceFile
2669
+ });
2670
+ const permArrayMatch = objStr.match(/permissions:\s*\[([^\]]*)\]/);
2671
+ if (permArrayMatch) {
2672
+ for (const pk of extractStringArrayRegex(permArrayMatch[1])) {
2673
+ if (pk === "*") continue;
2674
+ edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
2675
+ }
2676
+ }
2677
+ }
2678
+ }
2679
+ }
2680
+ }
2681
+ return { nodes, edges };
2682
+ }
2683
+ function walkDir(dir, exts) {
2684
+ if (!(0, import_node_fs10.existsSync)(dir)) return [];
2685
+ const results = [];
2686
+ for (const entry of (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true })) {
2687
+ if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
2688
+ const full = (0, import_node_path10.join)(dir, entry.name);
2689
+ if (entry.isDirectory()) results.push(...walkDir(full, exts));
2690
+ else if (exts.some((ext) => entry.name.endsWith(ext))) results.push(full);
2691
+ }
2692
+ return results;
2693
+ }
2694
+ function extractConstants(rootDir) {
2695
+ const nodes = [];
2696
+ const srcDir = (0, import_node_path10.join)(rootDir, "src");
2697
+ if (!(0, import_node_fs10.existsSync)(srcDir)) return { nodes };
2698
+ for (const filePath of walkDir(srcDir, [".ts", ".tsx"])) {
2699
+ const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
2700
+ const relPath = (0, import_node_path10.relative)(rootDir, filePath);
2701
+ const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
2702
+ let cm;
2703
+ while ((cm = constArrayRe.exec(content)) !== null) {
2704
+ const constName = cm[1];
2705
+ const bracketStart = cm.index + cm[0].length - 1;
2706
+ let depth = 1, i = bracketStart + 1;
2707
+ while (i < content.length && depth > 0) {
2708
+ if (content[i] === "[") depth++;
2709
+ else if (content[i] === "]") depth--;
2710
+ i++;
2711
+ }
2712
+ if (depth !== 0) continue;
2713
+ const body = content.slice(bracketStart + 1, i - 1);
2714
+ const stringValues = extractStringArrayRegex(body);
2715
+ const objectCount = splitArrayObjectsRegex(body).length;
2716
+ const valueCount = Math.max(stringValues.length, objectCount);
2717
+ if (valueCount < 2) continue;
2718
+ const scope = classifyScope(relPath);
2719
+ nodes.push({
2720
+ id: `const:${constName}`,
2721
+ type: "constant",
2722
+ name: constName,
2723
+ valueCount,
2724
+ values: stringValues.length > 0 && stringValues.length <= 30 ? stringValues : void 0,
2725
+ scope,
2726
+ source: relPath
2727
+ });
2728
+ }
2729
+ }
2730
+ return { nodes };
2731
+ }
2732
+ function detect4(rootDir) {
2733
+ 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"));
2734
+ }
2735
+ function generate4(rootDir) {
2736
+ const enumResult = extractEnumValues(rootDir);
2737
+ const seedResult = extractSeedData(rootDir);
2738
+ const constResult = extractConstants(rootDir);
2739
+ const allNodes = [...enumResult.nodes, ...seedResult.nodes, ...constResult.nodes];
2740
+ const allEdges = [...enumResult.edges, ...seedResult.edges];
2741
+ const typeOrder = {
2742
+ enum_group: 0,
2743
+ enum_value: 1,
2744
+ seed_permission: 2,
2745
+ seed_role: 3,
2746
+ seed_tag: 4,
2747
+ seed_plan: 5,
2748
+ seed_provider: 6,
2749
+ constant: 7
2750
+ };
2751
+ allNodes.sort((a, b) => {
2752
+ const ta = typeOrder[a.type] ?? 8;
2753
+ const tb = typeOrder[b.type] ?? 8;
2754
+ if (ta !== tb) return ta - tb;
2755
+ return a.name.localeCompare(b.name);
2756
+ });
2757
+ const enumGroups = allNodes.filter((n) => n.type === "enum_group").length;
2758
+ const enumValues = allNodes.filter((n) => n.type === "enum_value").length;
2759
+ const seedNodes = allNodes.filter((n) => n.type.startsWith("seed_")).length;
2760
+ const constNodes = allNodes.filter((n) => n.type === "constant").length;
2761
+ const byScope = {};
2762
+ for (const n of allNodes) {
2763
+ const s = n.scope ?? "unknown";
2764
+ byScope[s] = (byScope[s] ?? 0) + 1;
2765
+ }
2766
+ return {
2767
+ metadata: {
2768
+ generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
2769
+ scope: "static-values",
2770
+ layer: "static",
2771
+ enum_groups: enumGroups,
2772
+ enum_values: enumValues,
2773
+ seed_records: seedNodes,
2774
+ constants: constNodes,
2775
+ total_nodes: allNodes.length,
2776
+ total_edges: allEdges.length,
2777
+ parser: tryLoadTreeSitter() ? "tree-sitter" : "regex-fallback"
2778
+ },
2779
+ nodes: allNodes,
2780
+ edges: allEdges,
2781
+ cross_refs: [],
2782
+ contradictions: [],
2783
+ warnings: [],
2784
+ flagged_edges: [],
2785
+ patterns: {
2786
+ by_type: {
2787
+ enum_group: enumGroups,
2788
+ enum_value: enumValues,
2789
+ seed_permission: allNodes.filter((n) => n.type === "seed_permission").length,
2790
+ seed_role: allNodes.filter((n) => n.type === "seed_role").length,
2791
+ seed_tag: allNodes.filter((n) => n.type === "seed_tag").length,
2792
+ seed_plan: allNodes.filter((n) => n.type === "seed_plan").length,
2793
+ seed_provider: allNodes.filter((n) => n.type === "seed_provider").length,
2794
+ constant: constNodes
2795
+ },
2796
+ by_scope: byScope
2797
+ }
2798
+ };
2799
+ }
2800
+ var import_node_fs10, import_node_path10, parseCode, SHARED_MODELS, DB_MODELS, staticValuesParser;
2801
+ var init_static_values = __esm({
2802
+ "src/server/graph/parsers/static/static-values.ts"() {
2803
+ "use strict";
2804
+ import_node_fs10 = require("node:fs");
2805
+ import_node_path10 = require("node:path");
2806
+ parseCode = null;
2807
+ SHARED_MODELS = /* @__PURE__ */ new Set(["permission", "role", "tag"]);
2808
+ DB_MODELS = /* @__PURE__ */ new Set(["subscriptionPlan", "providerDefinition", "pipelineMasterTemplate"]);
2809
+ staticValuesParser = {
2810
+ id: "static-values",
2811
+ layer: "static",
2812
+ detect: detect4,
2813
+ generate: generate4
2814
+ };
2815
+ }
2816
+ });
2817
+
2818
+ // src/server/graph/parsers/crosslayer/static-ref-scanner.ts
2819
+ function walk4(dir, exts) {
2820
+ if (!(0, import_node_fs11.existsSync)(dir)) return [];
2821
+ const results = [];
2822
+ function recurse(d) {
2823
+ for (const entry of (0, import_node_fs11.readdirSync)(d, { withFileTypes: true })) {
2824
+ const full = (0, import_node_path11.join)(d, entry.name);
2825
+ if (entry.isDirectory()) {
2826
+ if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
2827
+ recurse(full);
2828
+ } else if (exts.some((ext) => entry.name.endsWith(ext))) {
2829
+ results.push(full);
2830
+ }
2831
+ }
2832
+ }
2833
+ recurse(dir);
2834
+ return results;
2835
+ }
2836
+ function isInCommentOrType(node) {
2837
+ let current = node.parent;
2838
+ while (current) {
2839
+ if (current.type === "comment" || current.type === "type_annotation" || current.type === "type_alias_declaration" || current.type === "interface_declaration" || current.type === "jsdoc") {
2840
+ return true;
2841
+ }
2842
+ current = current.parent;
2843
+ }
2844
+ return false;
2845
+ }
2846
+ function collectStaticRefs(root, valueLookup) {
2847
+ const refs = [];
2848
+ const seen = /* @__PURE__ */ new Set();
2849
+ function visit(node) {
2850
+ if (node.type === "string_fragment") {
2851
+ const val = node.text;
2852
+ if (val.length >= MIN_VALUE_LENGTH && !SKIP_VALUES.has(val.toLowerCase())) {
2853
+ const targets = valueLookup.get(val);
2854
+ if (targets && !isInCommentOrType(node)) {
2855
+ for (const t of targets) {
2856
+ const key = t;
2857
+ if (!seen.has(key)) {
2858
+ seen.add(key);
2859
+ refs.push({ value: val, targetIds: [t] });
2860
+ }
2861
+ }
2862
+ }
2863
+ }
2864
+ }
2865
+ if (node.type === "member_expression") {
2866
+ const objNode = node.namedChildren.find((c) => c.type === "identifier");
2867
+ const propNode = node.namedChildren.find((c) => c.type === "property_identifier");
2868
+ if (objNode && propNode) {
2869
+ const combined = `${objNode.text}.${propNode.text}`;
2870
+ const targets = valueLookup.get(propNode.text);
2871
+ const directTarget = valueLookup.get(combined);
2872
+ if (directTarget && !isInCommentOrType(node)) {
2873
+ for (const t of directTarget) {
2874
+ if (!seen.has(t)) {
2875
+ seen.add(t);
2876
+ refs.push({ value: combined, targetIds: [t] });
2877
+ }
2878
+ }
2879
+ } else if (targets && !isInCommentOrType(node)) {
2880
+ for (const t of targets) {
2881
+ if (!seen.has(t)) {
2882
+ seen.add(t);
2883
+ refs.push({ value: propNode.text, targetIds: [t] });
2884
+ }
2885
+ }
2886
+ }
2887
+ }
2888
+ }
2889
+ for (const child of node.namedChildren) {
2890
+ visit(child);
2891
+ }
2892
+ }
2893
+ visit(root);
2894
+ return refs;
2895
+ }
2896
+ function collectStaticRefsRegex(content, valueLookup, allValues) {
2897
+ const refs = [];
2898
+ const seen = /* @__PURE__ */ new Set();
2899
+ const escaped = allValues.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
2900
+ const pattern = new RegExp(
2901
+ `(?:['"\`])(${escaped.join("|")})(?:['"\`])|\\b(\\w+)\\.(${escaped.join("|")})\\b`,
2902
+ "g"
2903
+ );
2904
+ let match;
2905
+ pattern.lastIndex = 0;
2906
+ while ((match = pattern.exec(content)) !== null) {
2907
+ const val = match[1] ?? match[3];
2908
+ if (!val) continue;
2909
+ const targets = valueLookup.get(val);
2910
+ if (!targets) continue;
2911
+ for (const t of targets) {
2912
+ if (!seen.has(t)) {
2913
+ seen.add(t);
2914
+ refs.push({ value: val, targetIds: [t] });
2915
+ }
2916
+ }
2917
+ }
2918
+ return refs;
1839
2919
  }
1840
- var import_node_fs8, import_node_path8, URL_LITERAL_RE, urlLiteralScannerParser;
1841
- var init_url_literal_scanner = __esm({
1842
- "src/server/graph/parsers/crosslayer/url-literal-scanner.ts"() {
2920
+ var import_node_fs11, import_node_path11, MIN_VALUE_LENGTH, SKIP_VALUES, staticRefScannerParser;
2921
+ var init_static_ref_scanner = __esm({
2922
+ "src/server/graph/parsers/crosslayer/static-ref-scanner.ts"() {
1843
2923
  "use strict";
1844
- import_node_fs8 = require("node:fs");
1845
- import_node_path8 = require("node:path");
1846
- init_api_route_matching();
1847
- URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
1848
- urlLiteralScannerParser = {
1849
- id: "url-literal-scanner",
2924
+ import_node_fs11 = require("node:fs");
2925
+ import_node_path11 = require("node:path");
2926
+ init_config();
2927
+ init_resolve_paths();
2928
+ MIN_VALUE_LENGTH = 4;
2929
+ SKIP_VALUES = /* @__PURE__ */ new Set([
2930
+ "true",
2931
+ "false",
2932
+ "null",
2933
+ "undefined",
2934
+ "none",
2935
+ "default",
2936
+ "name",
2937
+ "type",
2938
+ "data",
2939
+ "text",
2940
+ "info",
2941
+ "error",
2942
+ "open",
2943
+ "read",
2944
+ "user",
2945
+ "test",
2946
+ "json",
2947
+ "form"
2948
+ ]);
2949
+ staticRefScannerParser = {
2950
+ id: "static-ref-scanner",
1850
2951
  layer: "crosslayer",
1851
2952
  detect(rootDir) {
1852
- return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
2953
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2954
+ return paths !== null;
1853
2955
  },
1854
2956
  generate(rootDir, layerOutputs) {
1855
- const apiOutput = layerOutputs.get("api");
1856
- if (!apiOutput) {
2957
+ const staticOutput = layerOutputs.get("static");
2958
+ if (!staticOutput || staticOutput.nodes.length === 0) {
1857
2959
  return { cross_refs: [], flagged_edges: [], warnings: [] };
1858
2960
  }
1859
- const uiOutput = layerOutputs.get("ui");
1860
- const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1861
- const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1862
- 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");
2961
+ const valueLookup = /* @__PURE__ */ new Map();
2962
+ for (const node of staticOutput.nodes) {
2963
+ const type = node.type;
2964
+ let valueStr = null;
2965
+ if (type === "enum_value") {
2966
+ valueStr = node.value;
2967
+ const fullId = node.id;
2968
+ if (!valueLookup.has(fullId)) valueLookup.set(fullId, []);
2969
+ valueLookup.get(fullId).push(node.id);
2970
+ } else if (type.startsWith("seed_")) {
2971
+ valueStr = node.value;
2972
+ }
2973
+ if (!valueStr || valueStr.length < MIN_VALUE_LENGTH || SKIP_VALUES.has(valueStr.toLowerCase())) continue;
2974
+ if (!valueLookup.has(valueStr)) valueLookup.set(valueStr, []);
2975
+ valueLookup.get(valueStr).push(node.id);
2976
+ }
2977
+ const allValues = [...valueLookup.keys()].sort((a, b) => b.length - a.length);
2978
+ if (allValues.length === 0) {
2979
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
2980
+ }
2981
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2982
+ if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
2983
+ const srcDir = paths.srcDir;
1866
2984
  const files = [
1867
- ...walk4(clientDir, [".ts", ".tsx"]),
1868
- ...walk4(appDir, [".ts", ".tsx"])
2985
+ ...walk4((0, import_node_path11.join)(srcDir, "client"), [".ts", ".tsx"]),
2986
+ ...walk4(paths.appDir, [".ts", ".tsx"]),
2987
+ ...walk4((0, import_node_path11.join)(srcDir, "server"), [".ts", ".tsx"]),
2988
+ ...walk4((0, import_node_path11.join)(srcDir, "lib"), [".ts", ".tsx"]),
2989
+ ...walk4((0, import_node_path11.join)(srcDir, "config"), [".ts", ".tsx"])
1869
2990
  ];
2991
+ const uiOutput = layerOutputs.get("ui");
2992
+ const apiOutput = layerOutputs.get("api");
2993
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
2994
+ const apiNodeIds = new Set(apiOutput?.nodes.map((n) => n.id) ?? []);
2995
+ let parseCode2 = null;
2996
+ try {
2997
+ const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
2998
+ if (typeof extractor.parseCodeTS === "function") {
2999
+ parseCode2 = extractor.parseCodeTS;
3000
+ }
3001
+ } catch {
3002
+ }
1870
3003
  const crossRefs = [];
1871
3004
  const seen = /* @__PURE__ */ new Set();
3005
+ let filesScanned = 0;
1872
3006
  for (const absPath of files) {
1873
- const sourceId = toNodeId3(srcDir, absPath);
1874
- if (!uiNodeIds.has(sourceId)) continue;
1875
- const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
1876
- let match;
1877
- URL_LITERAL_RE.lastIndex = 0;
1878
- while ((match = URL_LITERAL_RE.exec(content)) !== null) {
1879
- const urlPath = match[1];
1880
- const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
1881
- if (result.kind === "resolved" && result.nodeId) {
1882
- const key = `${sourceId}|${result.nodeId}|references_api`;
3007
+ const sourceId = (0, import_node_path11.relative)(srcDir, absPath).replace(/\\/g, "/");
3008
+ const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
3009
+ if (!sourceLayer) continue;
3010
+ const content = (0, import_node_fs11.readFileSync)(absPath, "utf-8");
3011
+ filesScanned++;
3012
+ let fileRefs;
3013
+ if (parseCode2) {
3014
+ try {
3015
+ const tree = parseCode2(content);
3016
+ fileRefs = collectStaticRefs(tree.rootNode, valueLookup);
3017
+ } catch {
3018
+ fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
3019
+ }
3020
+ } else {
3021
+ fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
3022
+ }
3023
+ for (const ref of fileRefs) {
3024
+ for (const targetId of ref.targetIds) {
3025
+ const key = `${sourceId}|${targetId}`;
1883
3026
  if (seen.has(key)) continue;
1884
3027
  seen.add(key);
1885
3028
  crossRefs.push({
1886
3029
  source: sourceId,
1887
- target: result.nodeId,
1888
- type: "references_api",
1889
- layer: "api"
3030
+ target: targetId,
3031
+ type: "references_static",
3032
+ layer: "static"
1890
3033
  });
1891
3034
  }
1892
3035
  }
@@ -1896,7 +3039,10 @@ var init_url_literal_scanner = __esm({
1896
3039
  flagged_edges: [],
1897
3040
  warnings: [],
1898
3041
  patterns: {
1899
- url_literals_resolved: crossRefs.length
3042
+ files_scanned: filesScanned,
3043
+ static_values_tracked: valueLookup.size,
3044
+ references_found: crossRefs.length,
3045
+ parser: parseCode2 ? "tree-sitter" : "regex-fallback"
1900
3046
  }
1901
3047
  };
1902
3048
  }
@@ -1905,14 +3051,19 @@ var init_url_literal_scanner = __esm({
1905
3051
  });
1906
3052
 
1907
3053
  // src/server/graph/core/parser-registry.ts
3054
+ function isMultiLayerParser(p) {
3055
+ return "layers" in p && Array.isArray(p.layers);
3056
+ }
1908
3057
  function registerBuiltins(registry, disabled) {
1909
3058
  const builtins = [
1910
- reactNextjsParser,
1911
- nextjsRoutesParser,
3059
+ typescriptProjectParser,
1912
3060
  prismaSchemaParser,
3061
+ sqlMigrationsParser,
3062
+ staticValuesParser,
1913
3063
  fetchResolverParser,
1914
3064
  apiAnnotationsParser,
1915
- urlLiteralScannerParser
3065
+ urlLiteralScannerParser,
3066
+ staticRefScannerParser
1916
3067
  ];
1917
3068
  for (const parser of builtins) {
1918
3069
  if (disabled.has(parser.id)) continue;
@@ -1922,11 +3073,11 @@ function registerBuiltins(registry, disabled) {
1922
3073
  function loadCustomParsers(registry, config, rootDir, disabled) {
1923
3074
  for (const entry of config.parsers?.custom ?? []) {
1924
3075
  try {
1925
- const absPath = (0, import_node_path9.resolve)(rootDir, entry.path);
3076
+ const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
1926
3077
  const mod = require(absPath);
1927
3078
  const parser = "default" in mod ? mod.default : mod;
1928
3079
  if (disabled.has(parser.id)) continue;
1929
- if (parser.layer !== entry.layer) {
3080
+ if (!isMultiLayerParser(parser) && parser.layer !== entry.layer) {
1930
3081
  process.stderr.write(
1931
3082
  `[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
1932
3083
  `
@@ -1946,20 +3097,23 @@ function createRegistry(config, rootDir) {
1946
3097
  loadCustomParsers(registry, config, rootDir, disabled);
1947
3098
  return registry;
1948
3099
  }
1949
- var import_node_path9, ParserRegistry;
3100
+ var import_node_path12, ParserRegistry;
1950
3101
  var init_parser_registry = __esm({
1951
3102
  "src/server/graph/core/parser-registry.ts"() {
1952
3103
  "use strict";
1953
- import_node_path9 = require("node:path");
1954
- init_react_nextjs();
1955
- init_nextjs_routes();
3104
+ import_node_path12 = require("node:path");
3105
+ init_typescript_project();
1956
3106
  init_prisma_schema();
3107
+ init_sql_migrations();
1957
3108
  init_fetch_resolver();
1958
3109
  init_api_annotations();
1959
3110
  init_url_literal_scanner();
3111
+ init_static_values();
3112
+ init_static_ref_scanner();
1960
3113
  ParserRegistry = class {
1961
3114
  constructor() {
1962
- this.parsers = /* @__PURE__ */ new Map();
3115
+ this.singleLayerParsers = /* @__PURE__ */ new Map();
3116
+ this.multiLayerParsers = [];
1963
3117
  this.ids = /* @__PURE__ */ new Set();
1964
3118
  }
1965
3119
  register(parser) {
@@ -1967,19 +3121,44 @@ var init_parser_registry = __esm({
1967
3121
  throw new Error(`Duplicate parser id: ${parser.id}`);
1968
3122
  }
1969
3123
  this.ids.add(parser.id);
1970
- const list = this.parsers.get(parser.layer) ?? [];
1971
- list.push(parser);
1972
- this.parsers.set(parser.layer, list);
3124
+ if (isMultiLayerParser(parser)) {
3125
+ this.multiLayerParsers.push(parser);
3126
+ } else {
3127
+ const list = this.singleLayerParsers.get(parser.layer) ?? [];
3128
+ list.push(parser);
3129
+ this.singleLayerParsers.set(parser.layer, list);
3130
+ }
1973
3131
  }
3132
+ /** Get single-layer parsers for a specific layer. */
1974
3133
  getParsers(layer) {
1975
- return this.parsers.get(layer) ?? [];
3134
+ return this.singleLayerParsers.get(layer) ?? [];
3135
+ }
3136
+ /** Get multi-layer parsers that can produce output for the given layer. */
3137
+ getMultiLayerParsersFor(layer) {
3138
+ return this.multiLayerParsers.filter((p) => p.layers.includes(layer));
3139
+ }
3140
+ /** Get all multi-layer parsers. */
3141
+ getMultiLayerParsers() {
3142
+ return [...this.multiLayerParsers];
1976
3143
  }
1977
3144
  getCrossLayerParsers() {
1978
- return this.parsers.get("crosslayer") ?? [];
3145
+ return this.singleLayerParsers.get("crosslayer") ?? [];
3146
+ }
3147
+ /** All layers that registered parsers can produce (single + multi). */
3148
+ getAvailableLayers() {
3149
+ const layers = /* @__PURE__ */ new Set();
3150
+ for (const key of this.singleLayerParsers.keys()) {
3151
+ if (key !== "crosslayer") layers.add(key);
3152
+ }
3153
+ for (const mp of this.multiLayerParsers) {
3154
+ for (const l of mp.layers) layers.add(l);
3155
+ }
3156
+ return [...layers];
1979
3157
  }
1980
3158
  getAll() {
1981
3159
  const all = [];
1982
- for (const list of this.parsers.values()) all.push(...list);
3160
+ for (const list of this.singleLayerParsers.values()) all.push(...list);
3161
+ all.push(...this.multiLayerParsers);
1983
3162
  return all;
1984
3163
  }
1985
3164
  };
@@ -2116,10 +3295,10 @@ var init_merge = __esm({
2116
3295
 
2117
3296
  // src/server/graph/core/graph-builder.ts
2118
3297
  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;
3298
+ const filePath = (0, import_node_path13.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
3299
+ if (!(0, import_node_fs12.existsSync)(filePath)) return null;
2121
3300
  try {
2122
- return JSON.parse((0, import_node_fs9.readFileSync)(filePath, "utf-8"));
3301
+ return JSON.parse((0, import_node_fs12.readFileSync)(filePath, "utf-8"));
2123
3302
  } catch {
2124
3303
  return null;
2125
3304
  }
@@ -2127,18 +3306,24 @@ function readGraphFromDisk(rootDir, layer) {
2127
3306
  function generateLayer(rootDir, layer) {
2128
3307
  const config = loadConfig(rootDir);
2129
3308
  const registry = createRegistry(config, rootDir);
2130
- const parsers = registry.getParsers(layer);
2131
3309
  const outputs = [];
2132
- for (const parser of parsers) {
3310
+ for (const parser of registry.getParsers(layer)) {
2133
3311
  if (!parser.detect(rootDir)) continue;
2134
3312
  outputs.push(parser.generate(rootDir));
2135
3313
  }
3314
+ for (const mp of registry.getMultiLayerParsersFor(layer)) {
3315
+ if (!mp.detect(rootDir)) continue;
3316
+ const multiOutput = mp.generate(rootDir);
3317
+ const layerOutput = multiOutput.get(layer);
3318
+ if (layerOutput) outputs.push(layerOutput);
3319
+ }
2136
3320
  if (outputs.length === 0) return null;
2137
3321
  let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
2138
3322
  if (layer === "ui") {
2139
3323
  const layerOutputs = /* @__PURE__ */ new Map();
2140
3324
  layerOutputs.set("ui", merged);
2141
- for (const otherLayer of ["api", "db"]) {
3325
+ for (const otherLayer of registry.getAvailableLayers()) {
3326
+ if (otherLayer === "ui") continue;
2142
3327
  const existing = readGraphFromDisk(rootDir, otherLayer);
2143
3328
  if (existing) layerOutputs.set(otherLayer, existing);
2144
3329
  }
@@ -2159,16 +3344,29 @@ function generateLayer(rootDir, layer) {
2159
3344
  function generateAll(rootDir) {
2160
3345
  const config = loadConfig(rootDir);
2161
3346
  const registry = createRegistry(config, rootDir);
2162
- const layerOrder = ["api", "db", "ui"];
3347
+ const allLayers = registry.getAvailableLayers();
3348
+ const layerOrder = [
3349
+ ...allLayers.filter((l) => l !== "ui"),
3350
+ ...allLayers.filter((l) => l === "ui")
3351
+ ];
2163
3352
  const layerOutputs = /* @__PURE__ */ new Map();
2164
3353
  const results = [];
3354
+ const multiLayerResults = /* @__PURE__ */ new Map();
2165
3355
  for (const layer of layerOrder) {
2166
- const parsers = registry.getParsers(layer);
2167
3356
  const outputs = [];
2168
- for (const parser of parsers) {
3357
+ for (const parser of registry.getParsers(layer)) {
2169
3358
  if (!parser.detect(rootDir)) continue;
2170
3359
  outputs.push(parser.generate(rootDir));
2171
3360
  }
3361
+ for (const mp of registry.getMultiLayerParsersFor(layer)) {
3362
+ if (!mp.detect(rootDir)) continue;
3363
+ if (!multiLayerResults.has(mp.id)) {
3364
+ multiLayerResults.set(mp.id, mp.generate(rootDir));
3365
+ }
3366
+ const cached = multiLayerResults.get(mp.id);
3367
+ const layerOutput = cached.get(layer);
3368
+ if (layerOutput) outputs.push(layerOutput);
3369
+ }
2172
3370
  if (outputs.length === 0) continue;
2173
3371
  const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
2174
3372
  layerOutputs.set(layer, merged);
@@ -2194,14 +3392,16 @@ function generateAll(rootDir) {
2194
3392
  }
2195
3393
  }
2196
3394
  const byLayer = new Map(results.map((r) => [r.layer, r]));
2197
- return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
3395
+ const wellKnownOrder = ["ui", "api", "db"];
3396
+ const extras = [...byLayer.keys()].filter((l) => !wellKnownOrder.includes(l)).sort();
3397
+ return [...wellKnownOrder, ...extras].map((l) => byLayer.get(l)).filter((r) => !!r);
2198
3398
  }
2199
- var import_node_fs9, import_node_path10;
3399
+ var import_node_fs12, import_node_path13;
2200
3400
  var init_graph_builder = __esm({
2201
3401
  "src/server/graph/core/graph-builder.ts"() {
2202
3402
  "use strict";
2203
- import_node_fs9 = require("node:fs");
2204
- import_node_path10 = require("node:path");
3403
+ import_node_fs12 = require("node:fs");
3404
+ import_node_path13 = require("node:path");
2205
3405
  init_config();
2206
3406
  init_parser_registry();
2207
3407
  init_merge();
@@ -2240,18 +3440,18 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
2240
3440
  const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
2241
3441
  const searchDirs = [
2242
3442
  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")
3443
+ (0, import_node_path14.join)(rootDir, "src"),
3444
+ (0, import_node_path14.join)(rootDir, "app"),
3445
+ (0, import_node_path14.join)(rootDir, "lib")
2246
3446
  ];
2247
3447
  for (const base of searchDirs) {
2248
3448
  for (const convention of conventionDirs) {
2249
- const dir = (0, import_node_path11.join)(base, convention);
2250
- if (!(0, import_node_fs10.existsSync)(dir)) continue;
3449
+ const dir = (0, import_node_path14.join)(base, convention);
3450
+ if (!(0, import_node_fs13.existsSync)(dir)) continue;
2251
3451
  try {
2252
- const stat = (0, import_node_fs10.statSync)(dir);
3452
+ const stat = (0, import_node_fs13.statSync)(dir);
2253
3453
  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);
3454
+ const entries = (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
2255
3455
  if (entries.length > 0) {
2256
3456
  const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
2257
3457
  result.set(relPath, entries);
@@ -2323,12 +3523,12 @@ function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
2323
3523
  }
2324
3524
  return "root";
2325
3525
  }
2326
- var import_node_fs10, import_node_path11, CONVENTION_DIRS_BUILTIN, GENERIC_ROLE_NAMES_BUILTIN, SKIP_SEGMENTS_BUILTIN, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
3526
+ var import_node_fs13, import_node_path14, CONVENTION_DIRS_BUILTIN, GENERIC_ROLE_NAMES_BUILTIN, SKIP_SEGMENTS_BUILTIN, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
2327
3527
  var init_module_tagger = __esm({
2328
3528
  "src/server/graph/taggers/module-tagger.ts"() {
2329
3529
  "use strict";
2330
- import_node_fs10 = require("node:fs");
2331
- import_node_path11 = require("node:path");
3530
+ import_node_fs13 = require("node:fs");
3531
+ import_node_path14 = require("node:path");
2332
3532
  CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
2333
3533
  GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
2334
3534
  // JS/TS
@@ -2540,7 +3740,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
2540
3740
  for (const entry of config.taggers?.custom ?? []) {
2541
3741
  if (disabled.has(entry.id)) continue;
2542
3742
  try {
2543
- const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
3743
+ const absPath = (0, import_node_path15.resolve)(rootDir, entry.path);
2544
3744
  const mod = require(absPath);
2545
3745
  const tagger = "default" in mod ? mod.default : mod;
2546
3746
  const override = config.taggers?.trackUntagged?.[tagger.id];
@@ -2561,11 +3761,11 @@ function createTaggerRegistry(config, rootDir) {
2561
3761
  loadCustomTaggers(registry, config, rootDir, disabled);
2562
3762
  return registry;
2563
3763
  }
2564
- var import_node_path12, TaggerRegistry, BUILTIN_TAGGERS;
3764
+ var import_node_path15, TaggerRegistry, BUILTIN_TAGGERS;
2565
3765
  var init_tagger_registry = __esm({
2566
3766
  "src/server/graph/core/tagger-registry.ts"() {
2567
3767
  "use strict";
2568
- import_node_path12 = require("node:path");
3768
+ import_node_path15 = require("node:path");
2569
3769
  init_module_tagger();
2570
3770
  init_screen_tagger();
2571
3771
  TaggerRegistry = class {
@@ -2593,18 +3793,18 @@ var init_tagger_registry = __esm({
2593
3793
 
2594
3794
  // src/server/graph/core/tag-store.ts
2595
3795
  function tagsFilePath(rootDir) {
2596
- return (0, import_node_path13.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
3796
+ return (0, import_node_path16.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
2597
3797
  }
2598
3798
  function readTagStore(rootDir) {
2599
3799
  const filePath = tagsFilePath(rootDir);
2600
- if (!(0, import_node_fs11.existsSync)(filePath)) return {};
2601
- const stat = (0, import_node_fs11.statSync)(filePath);
3800
+ if (!(0, import_node_fs14.existsSync)(filePath)) return {};
3801
+ const stat = (0, import_node_fs14.statSync)(filePath);
2602
3802
  const cached = tagCache.get(filePath);
2603
3803
  if (cached && cached.mtimeMs === stat.mtimeMs) {
2604
3804
  return cached.store;
2605
3805
  }
2606
3806
  try {
2607
- const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
3807
+ const content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
2608
3808
  const store = JSON.parse(content);
2609
3809
  tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
2610
3810
  return store;
@@ -2614,15 +3814,15 @@ function readTagStore(rootDir) {
2614
3814
  }
2615
3815
  function writeTagStore(rootDir, store) {
2616
3816
  const filePath = tagsFilePath(rootDir);
2617
- const dir = (0, import_node_path13.dirname)(filePath);
2618
- (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
3817
+ const dir = (0, import_node_path16.dirname)(filePath);
3818
+ (0, import_node_fs14.mkdirSync)(dir, { recursive: true });
2619
3819
  const cleaned = {};
2620
3820
  for (const [nodeId, tags] of Object.entries(store)) {
2621
3821
  if (Object.keys(tags).length > 0) {
2622
3822
  cleaned[nodeId] = tags;
2623
3823
  }
2624
3824
  }
2625
- (0, import_node_fs11.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
3825
+ (0, import_node_fs14.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
2626
3826
  tagCache.delete(filePath);
2627
3827
  }
2628
3828
  function setTag(rootDir, nodeId, key, value) {
@@ -2640,12 +3840,12 @@ function removeTag(rootDir, nodeId, key) {
2640
3840
  }
2641
3841
  writeTagStore(rootDir, store);
2642
3842
  }
2643
- var import_node_fs11, import_node_path13, TAGS_FILENAME, GRAPHS_DIR, tagCache;
3843
+ var import_node_fs14, import_node_path16, TAGS_FILENAME, GRAPHS_DIR, tagCache;
2644
3844
  var init_tag_store = __esm({
2645
3845
  "src/server/graph/core/tag-store.ts"() {
2646
3846
  "use strict";
2647
- import_node_fs11 = require("node:fs");
2648
- import_node_path13 = require("node:path");
3847
+ import_node_fs14 = require("node:fs");
3848
+ import_node_path16 = require("node:path");
2649
3849
  TAGS_FILENAME = "tags.json";
2650
3850
  GRAPHS_DIR = ".launchsecure/graphs";
2651
3851
  tagCache = /* @__PURE__ */ new Map();
@@ -2653,18 +3853,23 @@ var init_tag_store = __esm({
2653
3853
  });
2654
3854
 
2655
3855
  // src/server/graph/index.ts
3856
+ function getAvailableLayers(rootDir) {
3857
+ const dir = (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
3858
+ if (!(0, import_node_fs15.existsSync)(dir)) return [];
3859
+ return (0, import_node_fs15.readdirSync)(dir).filter((f) => f.endsWith(".json") && f !== "tags.json").map((f) => f.replace(".json", ""));
3860
+ }
2656
3861
  function graphsDir(rootDir) {
2657
- return (0, import_node_path14.join)(rootDir, GRAPHS_DIR2);
3862
+ return (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
2658
3863
  }
2659
3864
  function graphFilePath(rootDir, layer) {
2660
- return (0, import_node_path14.join)(graphsDir(rootDir), `${layer}.json`);
3865
+ return (0, import_node_path17.join)(graphsDir(rootDir), `${layer}.json`);
2661
3866
  }
2662
3867
  function tagsFilePath2(rootDir) {
2663
- return (0, import_node_path14.join)(graphsDir(rootDir), "tags.json");
3868
+ return (0, import_node_path17.join)(graphsDir(rootDir), "tags.json");
2664
3869
  }
2665
3870
  function getMtimeMs(filePath) {
2666
- if (!(0, import_node_fs12.existsSync)(filePath)) return 0;
2667
- return (0, import_node_fs12.statSync)(filePath).mtimeMs;
3871
+ if (!(0, import_node_fs15.existsSync)(filePath)) return 0;
3872
+ return (0, import_node_fs15.statSync)(filePath).mtimeMs;
2668
3873
  }
2669
3874
  function invalidateCache(filePath) {
2670
3875
  graphCache.delete(filePath);
@@ -2703,20 +3908,20 @@ function applyTags(graph, layer, rootDir) {
2703
3908
  }
2704
3909
  function readGraphRaw(rootDir, layer) {
2705
3910
  const filePath = graphFilePath(rootDir, layer);
2706
- if (!(0, import_node_fs12.existsSync)(filePath)) return null;
2707
- const stat = (0, import_node_fs12.statSync)(filePath);
3911
+ if (!(0, import_node_fs15.existsSync)(filePath)) return null;
3912
+ const stat = (0, import_node_fs15.statSync)(filePath);
2708
3913
  const cached = graphCache.get(filePath);
2709
3914
  if (cached && cached.mtimeMs === stat.mtimeMs) {
2710
3915
  return cached.graph;
2711
3916
  }
2712
- const content = (0, import_node_fs12.readFileSync)(filePath, "utf-8");
3917
+ const content = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
2713
3918
  const graph = JSON.parse(content);
2714
3919
  graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
2715
3920
  return graph;
2716
3921
  }
2717
3922
  function readGraph(rootDir, layer) {
2718
3923
  const rawFilePath = graphFilePath(rootDir, layer);
2719
- if (!(0, import_node_fs12.existsSync)(rawFilePath)) return null;
3924
+ if (!(0, import_node_fs15.existsSync)(rawFilePath)) return null;
2720
3925
  const rawMtime = getMtimeMs(rawFilePath);
2721
3926
  const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
2722
3927
  const cacheKey = `${rootDir}:${layer}`;
@@ -2732,7 +3937,7 @@ function readGraph(rootDir, layer) {
2732
3937
  }
2733
3938
  function readAllGraphs(rootDir) {
2734
3939
  const result = {};
2735
- for (const layer of LAYERS) {
3940
+ for (const layer of getAvailableLayers(rootDir)) {
2736
3941
  const graph = readGraph(rootDir, layer);
2737
3942
  if (graph) result[layer] = graph;
2738
3943
  }
@@ -2746,22 +3951,22 @@ async function generateGraph(rootDir, layer) {
2746
3951
  mutationMethods: config.parsers?.patterns?.mutationMethods
2747
3952
  });
2748
3953
  const dir = graphsDir(rootDir);
2749
- (0, import_node_fs12.mkdirSync)(dir, { recursive: true });
3954
+ (0, import_node_fs15.mkdirSync)(dir, { recursive: true });
2750
3955
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
2751
3956
  for (const result of results) {
2752
3957
  const filePath = graphFilePath(rootDir, result.layer);
2753
- (0, import_node_fs12.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
3958
+ (0, import_node_fs15.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
2754
3959
  invalidateCache(filePath);
2755
3960
  invalidateTaggedCache(rootDir, result.layer);
2756
3961
  }
2757
3962
  return results;
2758
3963
  }
2759
- var import_node_fs12, import_node_path14, GRAPHS_DIR2, LAYERS, graphCache, taggedCache;
3964
+ var import_node_fs15, import_node_path17, GRAPHS_DIR2, graphCache, taggedCache;
2760
3965
  var init_graph = __esm({
2761
3966
  "src/server/graph/index.ts"() {
2762
3967
  "use strict";
2763
- import_node_fs12 = require("node:fs");
2764
- import_node_path14 = require("node:path");
3968
+ import_node_fs15 = require("node:fs");
3969
+ import_node_path17 = require("node:path");
2765
3970
  init_graph_builder();
2766
3971
  init_config();
2767
3972
  init_tagger_registry();
@@ -2769,12 +3974,294 @@ var init_graph = __esm({
2769
3974
  init_ts_extractor();
2770
3975
  init_tag_store();
2771
3976
  GRAPHS_DIR2 = ".launchsecure/graphs";
2772
- LAYERS = ["ui", "api", "db"];
2773
3977
  graphCache = /* @__PURE__ */ new Map();
2774
3978
  taggedCache = /* @__PURE__ */ new Map();
2775
3979
  }
2776
3980
  });
2777
3981
 
3982
+ // src/server/graph/core/audit-core.ts
3983
+ function readGraphFile(rootDir, layer) {
3984
+ const filePath = (0, import_node_path18.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
3985
+ if (!(0, import_node_fs16.existsSync)(filePath)) return null;
3986
+ try {
3987
+ return JSON.parse((0, import_node_fs16.readFileSync)(filePath, "utf-8"));
3988
+ } catch {
3989
+ return null;
3990
+ }
3991
+ }
3992
+ function checkSchemaDrift(rootDir) {
3993
+ const findings = [];
3994
+ const db = readGraphFile(rootDir, "db");
3995
+ if (!db) {
3996
+ 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." });
3997
+ return buildReport("db", "schema_drift", findings);
3998
+ }
3999
+ for (const c of db.contradictions ?? []) {
4000
+ const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
4001
+ findings.push({
4002
+ id: `drift:${c.entity}`,
4003
+ severity: isTableLevel ? "error" : "warning",
4004
+ category: "schema_drift",
4005
+ title: c.entity,
4006
+ detail: c.detail
4007
+ });
4008
+ }
4009
+ return buildReport("db", "schema_drift", findings);
4010
+ }
4011
+ function checkOrphanFks(rootDir) {
4012
+ const findings = [];
4013
+ const db = readGraphFile(rootDir, "db");
4014
+ if (!db) return buildReport("db", "orphan_fks", findings);
4015
+ for (const f of db.flagged_edges ?? []) {
4016
+ findings.push({
4017
+ id: `fk:${f.source}->${f.target}`,
4018
+ severity: "warning",
4019
+ category: "orphan_fks",
4020
+ title: `${f.source} \u2192 ${f.target}`,
4021
+ detail: f.label
4022
+ });
4023
+ }
4024
+ return buildReport("db", "orphan_fks", findings);
4025
+ }
4026
+ function checkUnprotectedRoutes(rootDir) {
4027
+ const findings = [];
4028
+ const api = readGraphFile(rootDir, "api");
4029
+ const staticGraph = readGraphFile(rootDir, "static");
4030
+ if (!api) return buildReport("api", "unprotected_routes", findings);
4031
+ const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
4032
+ let routePermsContent = "";
4033
+ if ((0, import_node_fs16.existsSync)(routePermsPath)) {
4034
+ routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
4035
+ }
4036
+ const registeredRoutes = /* @__PURE__ */ new Set();
4037
+ const routeEntryRe = /path:\s*'([^']+)'/g;
4038
+ let rm;
4039
+ while ((rm = routeEntryRe.exec(routePermsContent)) !== null) {
4040
+ registeredRoutes.add(rm[1].replace(/:(\w+)/g, "[$1]"));
4041
+ }
4042
+ for (const node of api.nodes) {
4043
+ if (node.type !== "endpoint") continue;
4044
+ const route = node.route ?? "";
4045
+ if (!route) continue;
4046
+ const normalized = "/api" + (route.startsWith("/") ? route : "/" + route);
4047
+ const isRegistered = registeredRoutes.has(normalized) || [...registeredRoutes].some((r) => routeMatchesPattern(normalized, r));
4048
+ if (!isRegistered) {
4049
+ const methods = node.methods ?? [];
4050
+ findings.push({
4051
+ id: `unprotected:${node.id}`,
4052
+ severity: "warning",
4053
+ category: "unprotected_routes",
4054
+ title: `${methods.join(",")} ${route}`,
4055
+ detail: `API endpoint has no entry in ROUTE_PERMISSIONS. Methods: ${methods.join(", ")}`,
4056
+ file: node.id
4057
+ });
4058
+ }
4059
+ }
4060
+ return buildReport("api", "unprotected_routes", findings);
4061
+ }
4062
+ function routeMatchesPattern(route, pattern) {
4063
+ const routeParts = route.split("/");
4064
+ const patternParts = pattern.split("/");
4065
+ if (routeParts.length !== patternParts.length) return false;
4066
+ for (let i = 0; i < routeParts.length; i++) {
4067
+ const rp = routeParts[i];
4068
+ const pp = patternParts[i];
4069
+ if (rp === pp) continue;
4070
+ if (pp.startsWith("[") || pp.startsWith(":")) continue;
4071
+ if (rp.startsWith("[") || rp.startsWith(":")) continue;
4072
+ return false;
4073
+ }
4074
+ return true;
4075
+ }
4076
+ function checkDeadScreens(rootDir) {
4077
+ const findings = [];
4078
+ const ui = readGraphFile(rootDir, "ui");
4079
+ if (!ui) return buildReport("ui", "dead_screens", findings);
4080
+ const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
4081
+ const navTargets = /* @__PURE__ */ new Set();
4082
+ for (const e of ui.edges) {
4083
+ if (e.type === "navigates" || e.type === "renders" || e.type === "imports") {
4084
+ navTargets.add(e.target);
4085
+ }
4086
+ }
4087
+ for (const cr of ui.cross_refs ?? []) {
4088
+ navTargets.add(cr.target);
4089
+ }
4090
+ for (const page of pages) {
4091
+ if (page.id.endsWith("layout.tsx") && page.id.split("/").length <= 2) continue;
4092
+ if (["error.tsx", "loading.tsx", "not-found.tsx", "template.tsx"].some((s) => page.id.endsWith(s))) continue;
4093
+ if (!navTargets.has(page.id)) {
4094
+ findings.push({
4095
+ id: `dead:${page.id}`,
4096
+ severity: "info",
4097
+ category: "dead_screens",
4098
+ title: page.name,
4099
+ detail: `Page "${page.id}" has no incoming navigation, render, or import edges.`,
4100
+ file: page.id
4101
+ });
4102
+ }
4103
+ }
4104
+ return buildReport("ui", "dead_screens", findings);
4105
+ }
4106
+ function checkUnenforcedPermissions(rootDir) {
4107
+ const findings = [];
4108
+ const staticGraph = readGraphFile(rootDir, "static");
4109
+ if (!staticGraph) return buildReport("static", "unenforced_permissions", findings);
4110
+ const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
4111
+ const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
4112
+ let routePermsContent = "";
4113
+ if ((0, import_node_fs16.existsSync)(routePermsPath)) {
4114
+ routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
4115
+ }
4116
+ for (const perm of permissions) {
4117
+ const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
4118
+ if (!regex.test(routePermsContent)) {
4119
+ findings.push({
4120
+ id: `unenforced:${perm.key}`,
4121
+ severity: "warning",
4122
+ category: "unenforced_permissions",
4123
+ title: `${perm.name} (${perm.key})`,
4124
+ detail: `Permission "${perm.key}" exists in seed data but has no entry in ROUTE_PERMISSIONS \u2014 no API route requires it.`
4125
+ });
4126
+ }
4127
+ }
4128
+ return buildReport("static", "unenforced_permissions", findings);
4129
+ }
4130
+ function checkHardcodedValues(rootDir) {
4131
+ const findings = [];
4132
+ const staticGraph = readGraphFile(rootDir, "static");
4133
+ if (!staticGraph) return buildReport("static", "hardcoded_values", findings);
4134
+ const knownValues = /* @__PURE__ */ new Set();
4135
+ for (const n of staticGraph.nodes) {
4136
+ if (n.type === "enum_value") knownValues.add(n.value);
4137
+ }
4138
+ const api = readGraphFile(rootDir, "api");
4139
+ if (!api) return buildReport("static", "hardcoded_values", findings);
4140
+ const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
4141
+ const seen = /* @__PURE__ */ new Set();
4142
+ for (const node of api.nodes) {
4143
+ if (node.type !== "endpoint") continue;
4144
+ const filePath = (0, import_node_path18.join)(rootDir, "src", node.id);
4145
+ if (!(0, import_node_fs16.existsSync)(filePath)) continue;
4146
+ const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
4147
+ let m;
4148
+ allCapsRe.lastIndex = 0;
4149
+ while ((m = allCapsRe.exec(content)) !== null) {
4150
+ const val = m[1];
4151
+ if (knownValues.has(val)) continue;
4152
+ if (["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "UTF", "NULL", "TRUE", "FALSE", "JSON", "HTML", "CSS", "ENV"].includes(val)) continue;
4153
+ const key = `${node.id}:${val}`;
4154
+ if (seen.has(key)) continue;
4155
+ seen.add(key);
4156
+ findings.push({
4157
+ id: `hardcoded:${key}`,
4158
+ severity: "info",
4159
+ category: "hardcoded_values",
4160
+ title: `"${val}" in ${node.id}`,
4161
+ detail: `ALL_CAPS string literal "${val}" appears in API code but is not in the enum/seed inventory. May be an unregistered constant.`,
4162
+ file: node.id
4163
+ });
4164
+ }
4165
+ }
4166
+ return buildReport("static", "hardcoded_values", findings);
4167
+ }
4168
+ function buildReport(layer, check, findings) {
4169
+ return {
4170
+ layer,
4171
+ check,
4172
+ findings,
4173
+ summary: {
4174
+ errors: findings.filter((f) => f.severity === "error").length,
4175
+ warnings: findings.filter((f) => f.severity === "warning").length,
4176
+ info: findings.filter((f) => f.severity === "info").length
4177
+ },
4178
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4179
+ };
4180
+ }
4181
+ function getAvailableChecks() {
4182
+ const result = {};
4183
+ for (const [layer, checks] of Object.entries(CHECKS)) {
4184
+ result[layer] = Object.keys(checks);
4185
+ }
4186
+ return result;
4187
+ }
4188
+ function runAudit(rootDir, layer, check) {
4189
+ if (layer === "all") {
4190
+ const reports = [];
4191
+ for (const [l, checks] of Object.entries(CHECKS)) {
4192
+ for (const fn of Object.values(checks)) {
4193
+ reports.push(fn(rootDir));
4194
+ }
4195
+ }
4196
+ return reports;
4197
+ }
4198
+ const layerChecks = CHECKS[layer];
4199
+ if (!layerChecks) {
4200
+ return [{
4201
+ layer,
4202
+ check: "unknown",
4203
+ findings: [{ id: "invalid-layer", severity: "error", category: "system", title: "Invalid layer", detail: `Layer "${layer}" has no audit checks. Available: ${Object.keys(CHECKS).join(", ")}` }],
4204
+ summary: { errors: 1, warnings: 0, info: 0 },
4205
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4206
+ }];
4207
+ }
4208
+ if (check) {
4209
+ const fn = layerChecks[check];
4210
+ if (!fn) {
4211
+ return [{
4212
+ layer,
4213
+ check,
4214
+ findings: [{ id: "invalid-check", severity: "error", category: "system", title: "Invalid check", detail: `Check "${check}" not found for layer "${layer}". Available: ${Object.keys(layerChecks).join(", ")}` }],
4215
+ summary: { errors: 1, warnings: 0, info: 0 },
4216
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
4217
+ }];
4218
+ }
4219
+ return [fn(rootDir)];
4220
+ }
4221
+ return Object.values(layerChecks).map((fn) => fn(rootDir));
4222
+ }
4223
+ function formatAsPrompt(reports) {
4224
+ const lines = [];
4225
+ for (const report of reports) {
4226
+ if (report.findings.length === 0) continue;
4227
+ lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
4228
+ lines.push("");
4229
+ for (const f of report.findings) {
4230
+ const tag = f.severity === "error" ? "ERROR" : f.severity === "warning" ? "WARNING" : "INFO";
4231
+ const filePart = f.file ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : "";
4232
+ lines.push(`- [${tag}] ${f.title}${filePart}`);
4233
+ lines.push(` ${f.detail}`);
4234
+ }
4235
+ lines.push("");
4236
+ }
4237
+ if (lines.length === 0) return "No audit findings.";
4238
+ return lines.join("\n");
4239
+ }
4240
+ var import_node_fs16, import_node_path18, CHECKS;
4241
+ var init_audit_core = __esm({
4242
+ "src/server/graph/core/audit-core.ts"() {
4243
+ "use strict";
4244
+ import_node_fs16 = require("node:fs");
4245
+ import_node_path18 = require("node:path");
4246
+ CHECKS = {
4247
+ db: {
4248
+ schema_drift: checkSchemaDrift,
4249
+ orphan_fks: checkOrphanFks
4250
+ },
4251
+ api: {
4252
+ unprotected_routes: checkUnprotectedRoutes
4253
+ },
4254
+ ui: {
4255
+ dead_screens: checkDeadScreens
4256
+ },
4257
+ static: {
4258
+ unenforced_permissions: checkUnenforcedPermissions,
4259
+ hardcoded_values: checkHardcodedValues
4260
+ }
4261
+ };
4262
+ }
4263
+ });
4264
+
2778
4265
  // src/server/chart-serve.ts
2779
4266
  var chart_serve_exports = {};
2780
4267
  __export(chart_serve_exports, {
@@ -2787,31 +4274,39 @@ function randomPort() {
2787
4274
  function findProjectRoot(startDir) {
2788
4275
  let dir = startDir;
2789
4276
  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);
4277
+ const graphsDir2 = import_node_path19.default.join(dir, ".launchsecure", "graphs");
4278
+ 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;
4279
+ const parent = import_node_path19.default.dirname(dir);
2793
4280
  if (parent === dir) break;
2794
4281
  dir = parent;
2795
4282
  }
2796
4283
  dir = startDir;
2797
4284
  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);
4285
+ if (import_node_fs17.default.existsSync(import_node_path19.default.join(dir, ".git"))) return dir;
4286
+ const parent = import_node_path19.default.dirname(dir);
2800
4287
  if (parent === dir) break;
2801
4288
  dir = parent;
2802
4289
  }
2803
4290
  return startDir;
2804
4291
  }
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);
4292
+ function resolveRequestRoot(url, monorepoRoot, projects) {
4293
+ const projectParam = url.searchParams.get("project");
4294
+ if (!projectParam || projects.length === 0) return monorepoRoot;
4295
+ const resolved = import_node_path19.default.resolve(monorepoRoot, projectParam);
4296
+ if (!resolved.startsWith(monorepoRoot)) {
4297
+ throw new Error("Project path outside monorepo root");
4298
+ }
4299
+ return resolved;
4300
+ }
4301
+ async function buildMergedGraph(root) {
4302
+ let graphs = readAllGraphs(root);
4303
+ if (Object.keys(graphs).length === 0) {
4304
+ await generateGraph(root);
4305
+ graphs = readAllGraphs(root);
2810
4306
  }
2811
4307
  const nodes = [];
2812
4308
  const rawLinks = [];
2813
- const LAYERS2 = ["ui", "api", "db"];
2814
- for (const layer of LAYERS2) {
4309
+ for (const layer of Object.keys(graphs)) {
2815
4310
  const g = graphs[layer];
2816
4311
  if (!g) continue;
2817
4312
  for (const n of g.nodes) {
@@ -2848,16 +4343,16 @@ async function buildMergedGraph(projectRoot) {
2848
4343
  };
2849
4344
  }
2850
4345
  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();
4346
+ if (!import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) return false;
4347
+ const ext = import_node_path19.default.extname(filePath).toLowerCase();
2853
4348
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
2854
4349
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
2855
- import_node_fs13.default.createReadStream(filePath).pipe(res);
4350
+ import_node_fs17.default.createReadStream(filePath).pipe(res);
2856
4351
  return true;
2857
4352
  }
2858
4353
  function serveIndex(res, clientDir) {
2859
- const indexPath = import_node_path15.default.join(clientDir, "index.html");
2860
- if (!import_node_fs13.default.existsSync(indexPath)) {
4354
+ const indexPath = import_node_path19.default.join(clientDir, "index.html");
4355
+ if (!import_node_fs17.default.existsSync(indexPath)) {
2861
4356
  res.writeHead(500, { "Content-Type": "text/plain" });
2862
4357
  res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
2863
4358
  return;
@@ -2909,19 +4404,40 @@ async function startChartServer(opts = {}) {
2909
4404
  }
2910
4405
  return { port: existing.port, url: existing.url };
2911
4406
  }
2912
- const clientDir = opts.clientDir ?? import_node_path15.default.join(__dirname, "..", "chart-client");
4407
+ const clientDir = opts.clientDir ?? import_node_path19.default.join(__dirname, "..", "chart-client");
4408
+ const rootConfig = loadConfig(projectRoot);
4409
+ const projects = rootConfig.projects ?? [];
2913
4410
  const server = import_node_http.default.createServer((req, res) => {
2914
4411
  try {
2915
4412
  const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
4413
+ let reqRoot;
4414
+ try {
4415
+ reqRoot = resolveRequestRoot(url2, projectRoot, projects);
4416
+ } catch (err2) {
4417
+ res.writeHead(400, { "Content-Type": "application/json" });
4418
+ res.end(JSON.stringify({ ok: false, error: String(err2) }));
4419
+ return;
4420
+ }
4421
+ if (req.method === "GET" && url2.pathname === "/api/projects") {
4422
+ const projectList = projects.length > 0 ? projects.map((p) => {
4423
+ const absRoot = import_node_path19.default.resolve(projectRoot, p.root);
4424
+ const hasGraphs = import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, ".launchsecure", "graphs"));
4425
+ 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"));
4426
+ return { name: p.name, root: p.root, hasGraphs, hasNextConfig: hasNextConfig2 };
4427
+ }) : [{ name: import_node_path19.default.basename(projectRoot), root: ".", hasGraphs: import_node_fs17.default.existsSync(import_node_path19.default.join(projectRoot, ".launchsecure", "graphs")), hasNextConfig: true }];
4428
+ res.writeHead(200, { "Content-Type": "application/json" });
4429
+ res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
4430
+ return;
4431
+ }
2916
4432
  if (req.method === "GET" && url2.pathname === "/api/project-graph") {
2917
4433
  const regenerate = url2.searchParams.get("regenerate") === "1";
2918
4434
  (async () => {
2919
- if (regenerate) await generateGraph(projectRoot);
2920
- const merged = await buildMergedGraph(projectRoot);
4435
+ if (regenerate) await generateGraph(reqRoot);
4436
+ const merged = await buildMergedGraph(reqRoot);
2921
4437
  res.writeHead(200, { "Content-Type": "application/json" });
2922
4438
  res.end(JSON.stringify({
2923
4439
  ...merged,
2924
- debug: { cwd, projectRoot }
4440
+ debug: { cwd, projectRoot: reqRoot }
2925
4441
  }));
2926
4442
  })().catch((e) => {
2927
4443
  res.writeHead(500);
@@ -2930,15 +4446,15 @@ async function startChartServer(opts = {}) {
2930
4446
  return;
2931
4447
  }
2932
4448
  if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
2933
- const graphs = readAllGraphs(projectRoot);
4449
+ const graphs = readAllGraphs(reqRoot);
2934
4450
  res.writeHead(200, { "Content-Type": "application/json" });
2935
4451
  res.end(JSON.stringify({ ui: graphs.ui ?? null, api: graphs.api ?? null, db: graphs.db ?? null }));
2936
4452
  return;
2937
4453
  }
2938
4454
  if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
2939
4455
  (async () => {
2940
- await generateGraph(projectRoot);
2941
- const graphs = readAllGraphs(projectRoot);
4456
+ await generateGraph(reqRoot);
4457
+ const graphs = readAllGraphs(reqRoot);
2942
4458
  res.writeHead(200, { "Content-Type": "application/json" });
2943
4459
  res.end(JSON.stringify({
2944
4460
  ok: true,
@@ -2954,41 +4470,43 @@ async function startChartServer(opts = {}) {
2954
4470
  }
2955
4471
  if (req.method === "GET" && url2.pathname === "/api/file-content") {
2956
4472
  const relPath = url2.searchParams.get("path");
2957
- if (!relPath || relPath.includes("..") || import_node_path15.default.isAbsolute(relPath)) {
4473
+ if (!relPath || relPath.includes("..") || import_node_path19.default.isAbsolute(relPath)) {
2958
4474
  res.writeHead(400, { "Content-Type": "application/json" });
2959
4475
  res.end(JSON.stringify({ error: "Invalid path" }));
2960
4476
  return;
2961
4477
  }
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()) {
4478
+ const filePath = import_node_path19.default.join(reqRoot, relPath);
4479
+ if (!filePath.startsWith(reqRoot) || !import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) {
2964
4480
  res.writeHead(404, { "Content-Type": "application/json" });
2965
4481
  res.end(JSON.stringify({ error: "File not found" }));
2966
4482
  return;
2967
4483
  }
2968
- const ext = import_node_path15.default.extname(filePath).toLowerCase();
4484
+ const ext = import_node_path19.default.extname(filePath).toLowerCase();
2969
4485
  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");
4486
+ const content = import_node_fs17.default.readFileSync(filePath, "utf-8");
2971
4487
  res.writeHead(200, { "Content-Type": "application/json" });
2972
4488
  res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
2973
4489
  return;
2974
4490
  }
2975
4491
  if (req.method === "GET" && url2.pathname === "/api/health") {
2976
4492
  res.writeHead(200, { "Content-Type": "application/json" });
2977
- res.end(JSON.stringify({ ok: true, projectRoot }));
4493
+ res.end(JSON.stringify({ ok: true, projectRoot: reqRoot }));
2978
4494
  return;
2979
4495
  }
2980
4496
  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
- ];
4497
+ const config2 = loadConfig(reqRoot);
4498
+ const registry = createRegistry(config2, reqRoot);
4499
+ const detection = [];
4500
+ for (const parser of registry.getAll()) {
4501
+ if ("layers" in parser && Array.isArray(parser.layers)) {
4502
+ const mp = parser;
4503
+ detection.push({ id: mp.id, layers: mp.layers, detected: mp.detect(reqRoot) });
4504
+ } else if ("layer" in parser && parser.layer !== "crosslayer") {
4505
+ const sp = parser;
4506
+ detection.push({ id: sp.id, layers: [sp.layer], detected: sp.detect(reqRoot) });
4507
+ }
4508
+ }
4509
+ const crosslayerParsers = registry.getCrossLayerParsers().map((p) => ({ id: p.id }));
2992
4510
  res.writeHead(200, { "Content-Type": "application/json" });
2993
4511
  res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
2994
4512
  return;
@@ -3001,8 +4519,8 @@ async function startChartServer(opts = {}) {
3001
4519
  req.on("end", () => {
3002
4520
  try {
3003
4521
  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");
4522
+ const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
4523
+ import_node_fs17.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
3006
4524
  res.writeHead(200, { "Content-Type": "application/json" });
3007
4525
  res.end(JSON.stringify({ ok: true }));
3008
4526
  } catch (err2) {
@@ -3013,7 +4531,7 @@ async function startChartServer(opts = {}) {
3013
4531
  return;
3014
4532
  }
3015
4533
  if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
3016
- const config2 = loadConfig(projectRoot);
4534
+ const config2 = loadConfig(reqRoot);
3017
4535
  const builtinTaggers = [
3018
4536
  { id: "module", tagKey: "module", trackUntagged: config2.taggers?.trackUntagged?.module ?? true },
3019
4537
  { id: "screen", tagKey: "screen", trackUntagged: config2.taggers?.trackUntagged?.screen ?? true }
@@ -3033,10 +4551,10 @@ async function startChartServer(opts = {}) {
3033
4551
  req.on("end", () => {
3034
4552
  try {
3035
4553
  const taggerConfig = JSON.parse(body);
3036
- const config2 = loadConfig(projectRoot);
4554
+ const config2 = loadConfig(reqRoot);
3037
4555
  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");
4556
+ const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
4557
+ import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
3040
4558
  res.writeHead(200, { "Content-Type": "application/json" });
3041
4559
  res.end(JSON.stringify({ ok: true }));
3042
4560
  } catch (err2) {
@@ -3047,7 +4565,7 @@ async function startChartServer(opts = {}) {
3047
4565
  return;
3048
4566
  }
3049
4567
  if (req.method === "GET" && url2.pathname === "/api/tags") {
3050
- const store = readTagStore(projectRoot);
4568
+ const store = readTagStore(reqRoot);
3051
4569
  res.writeHead(200, { "Content-Type": "application/json" });
3052
4570
  res.end(JSON.stringify(store));
3053
4571
  return;
@@ -3065,7 +4583,7 @@ async function startChartServer(opts = {}) {
3065
4583
  res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
3066
4584
  return;
3067
4585
  }
3068
- setTag(projectRoot, nodeId, key, value);
4586
+ setTag(reqRoot, nodeId, key, value);
3069
4587
  res.writeHead(200, { "Content-Type": "application/json" });
3070
4588
  res.end(JSON.stringify({ ok: true }));
3071
4589
  } catch (err2) {
@@ -3088,7 +4606,76 @@ async function startChartServer(opts = {}) {
3088
4606
  res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
3089
4607
  return;
3090
4608
  }
3091
- removeTag(projectRoot, nodeId, key);
4609
+ removeTag(reqRoot, nodeId, key);
4610
+ res.writeHead(200, { "Content-Type": "application/json" });
4611
+ res.end(JSON.stringify({ ok: true }));
4612
+ } catch (err2) {
4613
+ res.writeHead(400, { "Content-Type": "application/json" });
4614
+ res.end(JSON.stringify({ ok: false, error: String(err2) }));
4615
+ }
4616
+ });
4617
+ return;
4618
+ }
4619
+ if (req.method === "GET" && url2.pathname === "/api/detected-paths") {
4620
+ const config2 = loadConfig(reqRoot);
4621
+ const paths = resolveProjectPaths(reqRoot, config2);
4622
+ const isOverride = !!config2.paths?.appDir;
4623
+ res.writeHead(200, { "Content-Type": "application/json" });
4624
+ res.end(JSON.stringify({
4625
+ projectRoot: reqRoot,
4626
+ detected: paths ? {
4627
+ srcDir: import_node_path19.default.relative(reqRoot, paths.srcDir) || ".",
4628
+ appDir: import_node_path19.default.relative(reqRoot, paths.appDir),
4629
+ apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir)
4630
+ } : null,
4631
+ isOverride
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;
@@ -3318,9 +5082,6 @@ function err(text) {
3318
5082
  async function handleGenerateGraph(args) {
3319
5083
  const rootDir = process.cwd();
3320
5084
  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
5085
  const results = await generateGraph(rootDir, layer);
3325
5086
  if (results.length === 0) {
3326
5087
  return err(
@@ -3357,8 +5118,11 @@ function runReadGraphQueryRaw(rootDir, args) {
3357
5118
  const offset = args.offset ?? 0;
3358
5119
  const limit = args.limit;
3359
5120
  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` };
5121
+ if (layer) {
5122
+ const available = getAvailableLayers(rootDir);
5123
+ if (available.length > 0 && !available.includes(layer)) {
5124
+ return { error: `No graph found for layer "${layer}". Available: ${available.join(", ")}. Run generate_graph first.` };
5125
+ }
3362
5126
  }
3363
5127
  if (!layer) {
3364
5128
  const graphs = readAllGraphs(rootDir);
@@ -3512,9 +5276,12 @@ function handleReadGraph(args) {
3512
5276
  return okJson(result);
3513
5277
  }
3514
5278
  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");
5279
+ if (layer === "ui" || layer === "api") return (0, import_node_path21.join)(rootDir, "src", nodeId);
5280
+ if (layer === "db") return (0, import_node_path21.join)(rootDir, "prisma", "schema.prisma");
5281
+ const withSrc = (0, import_node_path21.join)(rootDir, "src", nodeId);
5282
+ if ((0, import_node_fs19.existsSync)(withSrc)) return withSrc;
5283
+ const direct = (0, import_node_path21.join)(rootDir, nodeId);
5284
+ if ((0, import_node_fs19.existsSync)(direct)) return direct;
3518
5285
  return null;
3519
5286
  }
3520
5287
  function handleInspectNode(args) {
@@ -3603,7 +5370,10 @@ function handleGrepNodes(args) {
3603
5370
  const layer = args.layer;
3604
5371
  if (!pattern) return err("pattern is required");
3605
5372
  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`);
5373
+ const grepAvailable = getAvailableLayers(rootDir);
5374
+ if (grepAvailable.length > 0 && !grepAvailable.includes(layer)) {
5375
+ return err(`No graph found for layer "${layer}". Available: ${grepAvailable.join(", ")}. Run generate_graph first.`);
5376
+ }
3607
5377
  const hasFilter = !!(args.search || args.type || args.module || args.node_id);
3608
5378
  if (!hasFilter) {
3609
5379
  return err(
@@ -3654,11 +5424,11 @@ function handleGrepNodes(args) {
3654
5424
  let filesSearched = 0;
3655
5425
  let truncated = false;
3656
5426
  for (const [filePath, nodeId] of filePaths) {
3657
- if (!(0, import_node_fs14.existsSync)(filePath)) continue;
5427
+ if (!(0, import_node_fs19.existsSync)(filePath)) continue;
3658
5428
  filesSearched++;
3659
5429
  let content;
3660
5430
  try {
3661
- content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
5431
+ content = (0, import_node_fs19.readFileSync)(filePath, "utf-8");
3662
5432
  } catch {
3663
5433
  continue;
3664
5434
  }
@@ -3723,11 +5493,11 @@ function handleStartChartServer(args) {
3723
5493
  });
3724
5494
  }
3725
5495
  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");
5496
+ const logDir = (0, import_node_path21.join)((0, import_node_os2.homedir)(), ".launchsecure");
5497
+ (0, import_node_fs19.mkdirSync)(logDir, { recursive: true });
5498
+ const logPath = (0, import_node_path21.join)(logDir, "launch-chart.log");
5499
+ const out = (0, import_node_fs19.openSync)(logPath, "a");
5500
+ const err2 = (0, import_node_fs19.openSync)(logPath, "a");
3731
5501
  const portArgs = args.port ? ["--port", String(args.port)] : [];
3732
5502
  const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
3733
5503
  detached: true,
@@ -3790,14 +5560,55 @@ function handleRemoveTag(args) {
3790
5560
  removeTag(rootDir, nodeId, key);
3791
5561
  return okJson({ ok: true, node_id: nodeId, removed_key: key });
3792
5562
  }
5563
+ function handleAuditLayer(args) {
5564
+ const rootDir = process.cwd();
5565
+ const layer = args.layer;
5566
+ const check = args.check;
5567
+ if (!layer) return err("layer is required");
5568
+ const reports = runAudit(rootDir, layer, check);
5569
+ const totalFindings = reports.reduce((acc, r) => acc + r.findings.length, 0);
5570
+ const totalErrors = reports.reduce((acc, r) => acc + r.summary.errors, 0);
5571
+ const totalWarnings = reports.reduce((acc, r) => acc + r.summary.warnings, 0);
5572
+ const totalInfo = reports.reduce((acc, r) => acc + r.summary.info, 0);
5573
+ const lines = [];
5574
+ lines.push(`Audit: ${layer}${check ? ` / ${check}` : ""} \u2014 ${totalFindings} findings (${totalErrors} errors, ${totalWarnings} warnings, ${totalInfo} info)`);
5575
+ lines.push("");
5576
+ for (const report of reports) {
5577
+ if (report.findings.length === 0) {
5578
+ lines.push(`\u2713 ${report.check}: no issues found`);
5579
+ continue;
5580
+ }
5581
+ lines.push(`\u2500\u2500 ${report.check} (${report.findings.length}) \u2500\u2500`);
5582
+ for (const f of report.findings) {
5583
+ const tag = f.severity === "error" ? "\u2717" : f.severity === "warning" ? "!" : "\xB7";
5584
+ const filePart = f.file ? ` [${f.file}]` : "";
5585
+ lines.push(` ${tag} ${f.title}${filePart}`);
5586
+ lines.push(` ${f.detail}`);
5587
+ }
5588
+ lines.push("");
5589
+ }
5590
+ return okText(lines.join("\n"));
5591
+ }
3793
5592
  function handleDetectProjectStack() {
3794
5593
  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
5594
  const config = loadConfig(rootDir);
5595
+ const registry = createRegistry(config, rootDir);
5596
+ const parserResults = [];
5597
+ for (const parser of registry.getAll()) {
5598
+ if ("layers" in parser && Array.isArray(parser.layers)) {
5599
+ const mp = parser;
5600
+ parserResults.push({ id: mp.id, layers: mp.layers, detected: mp.detect(rootDir) });
5601
+ } else if ("layer" in parser && parser.layer !== "crosslayer") {
5602
+ const sp = parser;
5603
+ parserResults.push({ id: sp.id, layers: [sp.layer], detected: sp.detect(rootDir) });
5604
+ }
5605
+ }
5606
+ const availableLayers = /* @__PURE__ */ new Set();
5607
+ for (const p of parserResults) {
5608
+ if (p.detected) {
5609
+ for (const l of p.layers) availableLayers.add(l);
5610
+ }
5611
+ }
3801
5612
  let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
3802
5613
  const uiGraph = readGraph(rootDir, "ui");
3803
5614
  if (uiGraph) {
@@ -3809,20 +5620,20 @@ function handleDetectProjectStack() {
3809
5620
  if (f.type === "out_of_pattern") stats.out_of_pattern++;
3810
5621
  }
3811
5622
  }
3812
- const srcDir = (0, import_node_path16.join)(rootDir, "src");
3813
- if ((0, import_node_fs14.existsSync)(srcDir)) {
5623
+ const srcDir = (0, import_node_path21.join)(rootDir, "src");
5624
+ if ((0, import_node_fs19.existsSync)(srcDir)) {
3814
5625
  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 })) {
5626
+ if (!(0, import_node_fs19.existsSync)(dir)) return;
5627
+ for (const entry of (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true })) {
3817
5628
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
3818
- const full = (0, import_node_path16.join)(dir, entry.name);
5629
+ const full = (0, import_node_path21.join)(dir, entry.name);
3819
5630
  if (entry.isDirectory()) {
3820
5631
  scanDir(full);
3821
5632
  continue;
3822
5633
  }
3823
- if (![".ts", ".tsx"].includes((0, import_node_path16.extname)(entry.name))) continue;
5634
+ if (![".ts", ".tsx"].includes((0, import_node_path21.extname)(entry.name))) continue;
3824
5635
  try {
3825
- const content = (0, import_node_fs14.readFileSync)(full, "utf-8");
5636
+ const content = (0, import_node_fs19.readFileSync)(full, "utf-8");
3826
5637
  const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
3827
5638
  if (matches) stats.annotations += matches.length;
3828
5639
  } catch {
@@ -3837,8 +5648,16 @@ function handleDetectProjectStack() {
3837
5648
  } else if (stats.calls_api === 0 && stats.references_api > 0) {
3838
5649
  recommendedPrimary = "url-literal-scanner";
3839
5650
  }
5651
+ const supportedLanguages = /* @__PURE__ */ new Map();
5652
+ supportedLanguages.set("typescript", parserResults.filter((p) => p.detected && p.layers.some((l) => l === "ui" || l === "api")).map((p) => p.id));
5653
+ supportedLanguages.set("prisma", parserResults.filter((p) => p.detected && p.layers.includes("db")).map((p) => p.id));
5654
+ const languages = detectLanguages(rootDir, supportedLanguages);
5655
+ const unsupported = languages.filter((l) => !l.supported);
5656
+ const unsupportedHint = unsupported.length > 0 ? unsupported.map((l) => `${l.id} (${l.fileCount} files)`).join(", ") + " \u2014 detected but not yet supported" : null;
3840
5657
  return okJson({
3841
- parsers,
5658
+ languages,
5659
+ parsers: parserResults,
5660
+ available_layers: [...availableLayers],
3842
5661
  crosslayer_parsers: [
3843
5662
  { id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
3844
5663
  { id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
@@ -3846,6 +5665,7 @@ function handleDetectProjectStack() {
3846
5665
  ],
3847
5666
  stats,
3848
5667
  recommended_primary: recommendedPrimary,
5668
+ ...unsupportedHint ? { unsupported_hint: unsupportedHint } : {},
3849
5669
  current_config: Object.keys(config).length > 0 ? config : null,
3850
5670
  config_path: ".launchchart.json"
3851
5671
  });
@@ -3921,6 +5741,10 @@ async function handleMessage(msg) {
3921
5741
  respond(id ?? null, handleRemoveTag(args));
3922
5742
  return;
3923
5743
  }
5744
+ if (toolName === "audit_layer") {
5745
+ respond(id ?? null, handleAuditLayer(args));
5746
+ return;
5747
+ }
3924
5748
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
3925
5749
  return;
3926
5750
  }
@@ -3956,20 +5780,20 @@ function startGraphMcpServer() {
3956
5780
  process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
3957
5781
  `);
3958
5782
  }
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;
5783
+ 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
5784
  var init_graph_mcp = __esm({
3961
5785
  "src/server/graph-mcp.ts"() {
3962
5786
  "use strict";
3963
- import_node_fs14 = require("node:fs");
3964
- import_node_path16 = require("node:path");
5787
+ import_node_fs19 = require("node:fs");
5788
+ import_node_path21 = require("node:path");
3965
5789
  import_node_child_process2 = require("node:child_process");
3966
5790
  import_node_os2 = require("node:os");
3967
5791
  init_graph();
3968
5792
  init_lockfile();
3969
5793
  init_config();
3970
- init_react_nextjs();
3971
- init_nextjs_routes();
3972
- init_prisma_schema();
5794
+ init_parser_registry();
5795
+ init_language_detection();
5796
+ init_audit_core();
3973
5797
  SERVER_INFO = {
3974
5798
  name: "launchsecure-graph",
3975
5799
  version: "0.0.1"
@@ -3977,14 +5801,13 @@ var init_graph_mcp = __esm({
3977
5801
  TOOLS = [
3978
5802
  {
3979
5803
  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.",
5804
+ 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
5805
  inputSchema: {
3982
5806
  type: "object",
3983
5807
  properties: {
3984
5808
  layer: {
3985
5809
  type: "string",
3986
- enum: ["ui", "api", "db"],
3987
- description: "Specific layer to regenerate. Omit to regenerate all detectable layers."
5810
+ 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
5811
  }
3989
5812
  }
3990
5813
  }
@@ -3997,8 +5820,7 @@ var init_graph_mcp = __esm({
3997
5820
  properties: {
3998
5821
  layer: {
3999
5822
  type: "string",
4000
- enum: ["ui", "api", "db"],
4001
- description: "Graph layer to query: ui, api, or db. Required if any filter is provided."
5823
+ 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
5824
  },
4003
5825
  search: {
4004
5826
  type: "string",
@@ -4050,7 +5872,7 @@ var init_graph_mcp = __esm({
4050
5872
  items: {
4051
5873
  type: "object",
4052
5874
  properties: {
4053
- layer: { type: "string", enum: ["ui", "api", "db"] },
5875
+ layer: { type: "string" },
4054
5876
  search: { type: "string" },
4055
5877
  type: { type: "string" },
4056
5878
  module: { type: "string" },
@@ -4092,8 +5914,7 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
4092
5914
  properties: {
4093
5915
  layer: {
4094
5916
  type: "string",
4095
- enum: ["ui", "api", "db"],
4096
- description: "Graph layer to scope files (required)."
5917
+ description: "Graph layer to scope files (required, e.g. 'ui', 'api', 'db')."
4097
5918
  },
4098
5919
  pattern: {
4099
5920
  type: "string",
@@ -4126,8 +5947,7 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
4126
5947
  properties: {
4127
5948
  layer: {
4128
5949
  type: "string",
4129
- enum: ["ui", "api", "db"],
4130
- description: "Graph layer (required)."
5950
+ description: "Graph layer (required, e.g. 'ui', 'api', 'db')."
4131
5951
  },
4132
5952
  node_id: {
4133
5953
  type: "string",
@@ -4187,7 +6007,7 @@ Use this when the user asks "is the chart running", "show me the project graph U
4187
6007
  },
4188
6008
  {
4189
6009
  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.",
6010
+ 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
6011
  inputSchema: {
4192
6012
  type: "object",
4193
6013
  properties: {}
@@ -4232,6 +6052,24 @@ Use this when the user asks "is the chart running", "show me the project graph U
4232
6052
  },
4233
6053
  required: ["node_id", "key"]
4234
6054
  }
6055
+ },
6056
+ {
6057
+ name: "audit_layer",
6058
+ 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",
6059
+ inputSchema: {
6060
+ type: "object",
6061
+ properties: {
6062
+ layer: {
6063
+ type: "string",
6064
+ description: "Layer to audit: 'db', 'api', 'ui', 'static', or 'all'."
6065
+ },
6066
+ check: {
6067
+ type: "string",
6068
+ description: "Specific check to run (e.g. 'schema_drift', 'unprotected_routes'). Omit to run all checks for the layer."
6069
+ }
6070
+ },
6071
+ required: ["layer"]
6072
+ }
4235
6073
  }
4236
6074
  ];
4237
6075
  COMPACT_SCHEMA = {
@@ -4288,6 +6126,9 @@ Use this when the user asks "is the chart running", "show me the project graph U
4288
6126
  api: 65,
4289
6127
  db: 65
4290
6128
  };
6129
+ DEFAULT_EST_NODE_FULL = 300;
6130
+ DEFAULT_EST_NODE_MIN = 150;
6131
+ DEFAULT_EST_EDGE = 65;
4291
6132
  NEIGHBORHOOD_BUDGET_CHARS = 55e3;
4292
6133
  BATCH_BUDGET_CHARS = 6e4;
4293
6134
  }
@@ -4295,10 +6136,10 @@ Use this when the user asks "is the chart running", "show me the project graph U
4295
6136
 
4296
6137
  // src/server/graph-mcp-entry.ts
4297
6138
  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"));
6139
+ var import_node_fs20 = require("node:fs");
6140
+ var import_node_path22 = __toESM(require("node:path"));
4300
6141
  var import_node_os3 = require("node:os");
4301
- var import_node_fs16 = require("node:fs");
6142
+ var import_node_fs21 = require("node:fs");
4302
6143
  init_lockfile();
4303
6144
  function logStderr(msg) {
4304
6145
  process.stderr.write(`[launch-chart] ${msg}
@@ -4314,11 +6155,11 @@ function maybeAutoServe() {
4314
6155
  return;
4315
6156
  }
4316
6157
  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");
6158
+ const logDir = import_node_path22.default.join((0, import_node_os3.homedir)(), ".launchsecure");
6159
+ (0, import_node_fs21.mkdirSync)(logDir, { recursive: true });
6160
+ const logPath = import_node_path22.default.join(logDir, "launch-chart.log");
6161
+ const out = (0, import_node_fs20.openSync)(logPath, "a");
6162
+ const err2 = (0, import_node_fs20.openSync)(logPath, "a");
4322
6163
  const entryPath = process.argv[1];
4323
6164
  const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
4324
6165
  detached: true,