@launchsecure/launch-kit 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 ?? [];
@@ -456,6 +502,24 @@ function extractDbCallsTS(absPath) {
456
502
  }
457
503
  return calls;
458
504
  }
505
+ function classifyFile(absPath) {
506
+ const fileName = require("path").basename(absPath);
507
+ if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("__test")) return "test";
508
+ if (fileName.includes(".stories.")) return "story";
509
+ const tree = parseSource(absPath);
510
+ const root = tree.rootNode;
511
+ const classifyQuery = getQuery("classify");
512
+ const captures = classifyQuery.captures(root);
513
+ const capNames = new Set(captures.map((c) => c.name));
514
+ if (capNames.has("http_export") || capNames.has("http_export_fn")) return "endpoint";
515
+ if (fileName === "page.tsx" && capNames.has("has_jsx")) return "page";
516
+ if (fileName === "layout.tsx" && capNames.has("has_jsx")) return "layout";
517
+ if (capNames.has("has_create_context") || capNames.has("has_create_context_bare")) return "context";
518
+ if (capNames.has("has_jsx")) return "component";
519
+ if (capNames.has("hook_decl") || capNames.has("hook_const")) return "hook";
520
+ if (fileName.includes("config") || fileName.includes(".config.")) return "config";
521
+ return "lib";
522
+ }
459
523
  function extractAuthWrappersTS(absPath) {
460
524
  const tree = parseSource(absPath);
461
525
  const root = tree.rootNode;
@@ -626,17 +690,17 @@ function extractDeep(absPath) {
626
690
  }
627
691
  return { elements, stateVars, conditions, variables, responses, params };
628
692
  }
629
- 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;
630
694
  var init_ts_extractor = __esm({
631
695
  "src/server/graph/core/ts-extractor.ts"() {
632
696
  "use strict";
633
- import_node_fs3 = require("node:fs");
634
- import_node_path3 = require("node:path");
697
+ import_node_fs4 = require("node:fs");
698
+ import_node_path4 = require("node:path");
635
699
  initialized = false;
636
700
  queriesDir = (() => {
637
- 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");
638
702
  if (require("fs").existsSync(srcPath)) return srcPath;
639
- 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");
640
704
  })();
641
705
  queryCache = /* @__PURE__ */ new Map();
642
706
  PRISMA_MUTATION_METHODS_BUILTIN = [
@@ -656,15 +720,15 @@ var init_ts_extractor = __esm({
656
720
  }
657
721
  });
658
722
 
659
- // src/server/graph/parsers/ui/react-nextjs.ts
723
+ // src/server/graph/parsers/ts/typescript-project.ts
660
724
  function walk(dir, exts) {
661
725
  const results = [];
662
- if (!(0, import_node_fs4.existsSync)(dir)) return results;
663
- for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
664
- const full = (0, import_node_path4.join)(dir, entry.name);
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);
665
729
  if (entry.isDirectory()) {
666
730
  results.push(...walk(full, exts));
667
- } else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
731
+ } else if (exts.includes((0, import_node_path5.extname)(entry.name))) {
668
732
  results.push(full);
669
733
  }
670
734
  }
@@ -672,33 +736,33 @@ function walk(dir, exts) {
672
736
  }
673
737
  function walkWithIgnore(dir, exts, ignoreDirs) {
674
738
  const results = [];
675
- if (!(0, import_node_fs4.existsSync)(dir)) return results;
676
- 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 })) {
677
741
  if (entry.isDirectory()) {
678
742
  if (ignoreDirs.has(entry.name)) continue;
679
- results.push(...walkWithIgnore((0, import_node_path4.join)(dir, entry.name), exts, ignoreDirs));
680
- } else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
681
- results.push((0, import_node_path4.join)(dir, entry.name));
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));
682
746
  }
683
747
  }
684
748
  return results;
685
749
  }
686
750
  function toNodeId(srcDir, absPath) {
687
- return (0, import_node_path4.relative)(srcDir, absPath).replace(/\\/g, "/");
751
+ return (0, import_node_path5.relative)(srcDir, absPath).replace(/\\/g, "/");
688
752
  }
689
753
  function resolveImport(srcDir, specifier) {
690
754
  if (!specifier.startsWith("@/")) return null;
691
755
  const rel = specifier.slice(2);
692
- const base = (0, import_node_path4.join)(srcDir, rel);
693
- for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
694
- if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
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;
695
759
  }
696
760
  return null;
697
761
  }
698
762
  function resolveRelativeImport(fromFile, specifier) {
699
- const base = (0, import_node_path4.join)((0, import_node_path4.dirname)(fromFile), specifier);
700
- for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
701
- if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
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;
702
766
  }
703
767
  return null;
704
768
  }
@@ -719,7 +783,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
719
783
  const resolved = resolveRelativeImport(barrelAbsPath, re.from);
720
784
  if (!resolved) continue;
721
785
  if (re.isWildcard) {
722
- const targetBn = (0, import_node_path4.basename)(resolved);
786
+ const targetBn = (0, import_node_path5.basename)(resolved);
723
787
  const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
724
788
  if (targetIsBarrel) {
725
789
  const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
@@ -746,31 +810,21 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
746
810
  const barrels = /* @__PURE__ */ new Map();
747
811
  const memo = /* @__PURE__ */ new Map();
748
812
  for (const [absPath, parsed] of parsedByPath) {
749
- const bn = (0, import_node_path4.basename)(absPath);
813
+ const bn = (0, import_node_path5.basename)(absPath);
750
814
  if (bn !== "index.ts" && bn !== "index.tsx") continue;
751
815
  if (parsed.reExports.length === 0) continue;
752
816
  const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
753
817
  if (map.size > 0) {
754
- 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, "/");
755
819
  barrels.set(barrelId, map);
756
820
  }
757
821
  }
758
822
  return barrels;
759
823
  }
760
- function classifyType(id) {
761
- if (id.endsWith("/page.tsx")) return "page";
762
- if (id.endsWith("/layout.tsx")) return "layout";
763
- if (id.startsWith("client/components/ui/")) return "ui";
764
- if (id.startsWith("client/components/")) return "component";
765
- if (id.startsWith("client/hooks/")) return "hook";
766
- if (/client\/lib\/.*-context\./.test(id)) return "context";
767
- if (id.startsWith("client/lib/")) return id.includes("config") ? "config" : "util";
768
- if (id.startsWith("client/api/")) return "util";
769
- if (id.startsWith("server/mcp/")) return "mcp-tool";
770
- if (id.startsWith("server/lib/")) return "lib";
771
- if (id.startsWith("server/")) return "lib";
772
- if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
773
- return "component";
824
+ function classifyType(absPath, id) {
825
+ const contentType = classifyFile(absPath);
826
+ if (contentType === "lib" && id.startsWith("server/mcp/")) return "mcp-tool";
827
+ return contentType;
774
828
  }
775
829
  function extractRoute(id) {
776
830
  if (!id.endsWith("/page.tsx")) return null;
@@ -782,7 +836,18 @@ function extractRoute(id) {
782
836
  return route || "/";
783
837
  }
784
838
  function nameFromFilename(absPath) {
785
- 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);
786
851
  }
787
852
  function resolveTemplateLiteralRoute(template, routeToNodeId) {
788
853
  const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -863,7 +928,7 @@ function matchRouteToPage(route, routeToNodeId) {
863
928
  if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
864
929
  return null;
865
930
  }
866
- function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
931
+ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps, routeToNodeId) {
867
932
  const edges = [];
868
933
  const flagged = [];
869
934
  const seen = /* @__PURE__ */ new Set();
@@ -875,13 +940,10 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
875
940
  if (label) edge.label = label;
876
941
  edges.push(edge);
877
942
  }
878
- function edgeTypeFor(targetId, isTypeOnlyImport, importedNames) {
943
+ function edgeTypeFor(isTypeOnlyImport, importedNames) {
879
944
  if (isTypeOnlyImport) return "imports";
880
- const targetType = nodeTypeMap.get(targetId);
881
- if (targetType && RENDER_TYPES.has(targetType)) {
882
- const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
883
- if (anyRendered) return "renders";
884
- }
945
+ const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
946
+ if (anyRendered) return "renders";
885
947
  return "imports";
886
948
  }
887
949
  for (const imp of parsed.imports) {
@@ -903,14 +965,14 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
903
965
  }
904
966
  for (const [targetId, targetNames] of byTarget) {
905
967
  const allType = isTypeOnly || targetNames.every((n) => typeNames.has(n));
906
- addEdge(targetId, edgeTypeFor(targetId, allType, targetNames));
968
+ addEdge(targetId, edgeTypeFor(allType, targetNames));
907
969
  }
908
970
  } else {
909
971
  const resolved = resolveImport(srcDir, specifier);
910
972
  if (resolved) {
911
973
  const targetId = toNodeId(srcDir, resolved);
912
974
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
913
- addEdge(targetId, edgeTypeFor(targetId, isTypeOnly, names));
975
+ addEdge(targetId, edgeTypeFor(isTypeOnly, names));
914
976
  }
915
977
  }
916
978
  }
@@ -919,7 +981,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
919
981
  if (resolved) {
920
982
  const targetId = toNodeId(srcDir, resolved);
921
983
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
922
- addEdge(targetId, edgeTypeFor(targetId, isTypeOnly, names));
984
+ addEdge(targetId, edgeTypeFor(isTypeOnly, names));
923
985
  }
924
986
  }
925
987
  }
@@ -961,74 +1023,113 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
961
1023
  }
962
1024
  return { edges, flagged };
963
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
+ }
964
1029
  function detect(rootDir) {
965
- 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);
966
1032
  }
967
1033
  function generate(rootDir) {
968
- const srcDir = (0, import_node_path4.join)(rootDir, "src");
969
- const appFiles = walk((0, import_node_path4.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
970
- (f) => (0, import_node_path4.basename)(f) !== "route.ts" && (0, import_node_path4.basename)(f) !== "route.tsx"
971
- );
972
- const clientFiles = walk((0, import_node_path4.join)(srcDir, "client"), [".tsx", ".ts"]);
973
- const serverFiles = walk((0, import_node_path4.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
974
- (f) => (0, import_node_path4.basename)(f) !== "route.ts" && (0, import_node_path4.basename)(f) !== "route.tsx"
975
- );
976
- const libFiles = walk((0, import_node_path4.join)(srcDir, "lib"), [".ts", ".tsx"]);
977
- 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"]);
978
1043
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
979
1044
  const parsedByPath = /* @__PURE__ */ new Map();
980
1045
  for (const absPath of allDiscovered) {
981
1046
  parsedByPath.set(absPath, parseFileTS(absPath));
982
1047
  }
983
1048
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
984
- const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
985
- const nodes = [];
1049
+ const uiNodes = [];
1050
+ const apiNodes = [];
986
1051
  const nodeIdSet = /* @__PURE__ */ new Set();
987
- const nodeTypeMap = /* @__PURE__ */ new Map();
988
1052
  const routeToNodeId = /* @__PURE__ */ new Map();
1053
+ const fileSet = allDiscovered.filter((f) => !(0, import_node_path5.basename)(f).startsWith("index."));
989
1054
  for (const absPath of fileSet) {
990
1055
  const id = toNodeId(srcDir, absPath);
991
- const type = classifyType(id);
1056
+ const type = classifyType(absPath, id);
1057
+ if (type === "test" || type === "story") continue;
992
1058
  const parsed = parsedByPath.get(absPath);
993
1059
  const name = parsed.name || nameFromFilename(absPath);
994
- const route = extractRoute(id);
995
- const deep = extractDeep(absPath);
996
- nodes.push({
997
- id,
998
- type,
999
- name,
1000
- route,
1001
- exports: parsed.exports,
1002
- elements: deep.elements,
1003
- stateVars: deep.stateVars,
1004
- conditions: deep.conditions,
1005
- variables: deep.variables
1006
- });
1060
+ const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
1007
1061
  nodeIdSet.add(id);
1008
- nodeTypeMap.set(id, type);
1009
- 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
+ }
1010
1110
  }
1011
- const allEdges = [];
1012
- const allFlagged = [];
1111
+ const uiEdges = [];
1112
+ const uiFlagged = [];
1013
1113
  for (const absPath of fileSet) {
1014
- const sourceId = toNodeId(srcDir, absPath);
1114
+ const id = toNodeId(srcDir, absPath);
1115
+ if (!nodeIdSet.has(id)) continue;
1015
1116
  const parsed = parsedByPath.get(absPath);
1016
1117
  const { edges, flagged } = extractEdges(
1017
1118
  srcDir,
1018
1119
  absPath,
1019
- sourceId,
1120
+ id,
1020
1121
  parsed,
1021
1122
  nodeIdSet,
1022
- nodeTypeMap,
1023
1123
  barrelMaps,
1024
1124
  routeToNodeId
1025
1125
  );
1026
- allEdges.push(...edges);
1027
- allFlagged.push(...flagged);
1126
+ uiEdges.push(...edges);
1127
+ uiFlagged.push(...flagged);
1028
1128
  }
1029
1129
  const fetchCallEntries = [];
1030
1130
  for (const absPath of fileSet) {
1031
1131
  const sourceId = toNodeId(srcDir, absPath);
1132
+ if (!nodeIdSet.has(sourceId)) continue;
1032
1133
  const parsed = parsedByPath.get(absPath);
1033
1134
  if (parsed.fetchCalls.length === 0) continue;
1034
1135
  fetchCallEntries.push({
@@ -1043,7 +1144,7 @@ function generate(rootDir) {
1043
1144
  });
1044
1145
  }
1045
1146
  const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
1046
- const IGNORE_DIRS = /* @__PURE__ */ new Set([
1147
+ const IGNORE_DIRS2 = /* @__PURE__ */ new Set([
1047
1148
  "node_modules",
1048
1149
  ".next",
1049
1150
  "dist",
@@ -1056,7 +1157,7 @@ function generate(rootDir) {
1056
1157
  "out",
1057
1158
  ".vercel"
1058
1159
  ]);
1059
- const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS);
1160
+ const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS2);
1060
1161
  for (const absPath of externalCandidates) {
1061
1162
  const normalized = absPath.replace(/\\/g, "/");
1062
1163
  if (externalScanned.has(normalized)) continue;
@@ -1066,11 +1167,11 @@ function generate(rootDir) {
1066
1167
  } catch {
1067
1168
  continue;
1068
1169
  }
1069
- const externalId = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
1170
+ const externalId = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
1070
1171
  const edgesFromThis = [];
1071
1172
  const seen = /* @__PURE__ */ new Set();
1072
1173
  for (const imp of parsed.imports) {
1073
- const { specifier, isTypeOnly, names } = imp;
1174
+ const { specifier, names } = imp;
1074
1175
  let resolved = null;
1075
1176
  if (specifier.startsWith("@/")) {
1076
1177
  const relToSrc = specifier.slice(2);
@@ -1096,25 +1197,52 @@ function generate(rootDir) {
1096
1197
  const targetId = toNodeId(srcDir, resolved);
1097
1198
  if (!nodeIdSet.has(targetId)) continue;
1098
1199
  if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
1099
- const key = `${externalId}\u2192${targetId}\u2192${isTypeOnly ? "type" : "value"}`;
1200
+ const key = `${externalId}\u2192${targetId}`;
1100
1201
  if (seen.has(key)) continue;
1101
1202
  seen.add(key);
1102
1203
  edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
1103
1204
  }
1104
1205
  if (edgesFromThis.length === 0) continue;
1105
- nodes.push({
1206
+ uiNodes.push({
1106
1207
  id: externalId,
1107
1208
  type: "external",
1108
1209
  name: parsed.name || nameFromFilename(absPath),
1210
+ layer: "ui",
1109
1211
  route: null,
1110
1212
  exports: parsed.exports
1111
1213
  });
1112
1214
  nodeIdSet.add(externalId);
1113
- nodeTypeMap.set(externalId, "external");
1114
- 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
+ }
1115
1243
  }
1116
1244
  const flaggedSet = /* @__PURE__ */ new Set();
1117
- const dedupedFlagged = allFlagged.filter((f) => {
1245
+ const dedupedFlagged = uiFlagged.filter((f) => {
1118
1246
  const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
1119
1247
  if (flaggedSet.has(key)) return false;
1120
1248
  flaggedSet.add(key);
@@ -1131,10 +1259,12 @@ function generate(rootDir) {
1131
1259
  hook: 7,
1132
1260
  lib: 8
1133
1261
  };
1134
- nodes.sort((a, b) => (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99) || a.id.localeCompare(b.id));
1135
- allEdges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1136
- const byType = (t) => nodes.filter((n) => n.type === t).length;
1137
- 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 = {
1138
1268
  total_pages: byType("page"),
1139
1269
  total_layouts: byType("layout"),
1140
1270
  total_components: byType("component"),
@@ -1145,197 +1275,122 @@ function generate(rootDir) {
1145
1275
  total_utils: byType("util"),
1146
1276
  total_libs: byType("lib"),
1147
1277
  total_external: byType("external"),
1148
- total_edges: allEdges.length,
1278
+ total_edges: uiOnlyEdges.length,
1149
1279
  total_flagged: dedupedFlagged.length
1150
1280
  };
1151
- return {
1281
+ const stripLayer = (nodes) => nodes.map(({ layer: _, ...rest }) => rest);
1282
+ const result = /* @__PURE__ */ new Map();
1283
+ result.set("ui", {
1152
1284
  metadata: {
1153
1285
  generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1154
1286
  scope: "main-app-only",
1155
1287
  app_root: "src/",
1156
1288
  layer: "ui",
1157
- parser: "react-nextjs-ast",
1158
- ...stats,
1289
+ parser: "typescript-project",
1290
+ ...uiStats,
1159
1291
  notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
1160
1292
  },
1161
- nodes,
1162
- edges: allEdges,
1293
+ nodes: stripLayer(uiNodes),
1294
+ edges: uiOnlyEdges,
1163
1295
  cross_refs: [],
1164
1296
  contradictions: [],
1165
1297
  warnings: [],
1166
1298
  flagged_edges: dedupedFlagged,
1167
1299
  patterns: {
1168
- total_nodes: nodes.length,
1169
- by_type: stats,
1300
+ total_nodes: uiNodes.length,
1301
+ by_type: uiStats,
1170
1302
  by_edge_type: {
1171
- renders: allEdges.filter((e) => e.type === "renders").length,
1172
- imports: allEdges.filter((e) => e.type === "imports").length,
1173
- 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
1174
1306
  },
1175
1307
  fetch_calls: fetchCallEntries
1176
1308
  }
1177
- };
1178
- }
1179
- var import_node_fs4, import_node_path4, RENDER_TYPES, reactNextjsParser;
1180
- var init_react_nextjs = __esm({
1181
- "src/server/graph/parsers/ui/react-nextjs.ts"() {
1182
- "use strict";
1183
- import_node_fs4 = require("node:fs");
1184
- import_node_path4 = require("node:path");
1185
- init_ts_extractor();
1186
- RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
1187
- reactNextjsParser = {
1188
- id: "react-nextjs",
1189
- layer: "ui",
1190
- detect,
1191
- generate
1192
- };
1193
- }
1194
- });
1195
-
1196
- // src/server/graph/parsers/api/nextjs-routes.ts
1197
- function walk2(dir) {
1198
- const results = [];
1199
- if (!(0, import_node_fs5.existsSync)(dir)) return results;
1200
- for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
1201
- const full = (0, import_node_path5.join)(dir, entry.name);
1202
- if (entry.isDirectory()) {
1203
- results.push(...walk2(full));
1204
- } else if (entry.name === "route.ts" || entry.name === "route.tsx") {
1205
- results.push(full);
1206
- }
1207
- }
1208
- return results;
1209
- }
1210
- function filePathToRoute(apiDir, absPath) {
1211
- let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
1212
- route = route.replace(/\[([^\]]+)\]/g, ":$1");
1213
- route = route.replace(/\/+/g, "/");
1214
- if (route === "/") return "/api";
1215
- return "/api" + route;
1216
- }
1217
- function camelToPascal(s) {
1218
- if (!s) return s;
1219
- return s.charAt(0).toUpperCase() + s.slice(1);
1220
- }
1221
- function detect2(rootDir) {
1222
- return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "src", "app", "api"));
1223
- }
1224
- function generate2(rootDir) {
1225
- const apiDir = (0, import_node_path5.join)(rootDir, "src", "app", "api");
1226
- const routeFiles = walk2(apiDir);
1227
- const nodes = [];
1228
- const edges = [];
1229
- const crossRefs = [];
1230
- const mutatorCount = {};
1231
- const authUsage = {};
1232
- let endpointsWithAuth = 0;
1233
- let endpointsWithDbAccess = 0;
1234
- for (const absPath of routeFiles) {
1235
- const parsed = parseFileTS(absPath);
1236
- const dbCalls = extractDbCallsTS(absPath);
1237
- const authWrappers = extractAuthWrappersTS(absPath);
1238
- const methods = [];
1239
- for (const exp of parsed.exports) {
1240
- if (HTTP_METHODS.has(exp)) methods.push(exp);
1241
- }
1242
- const routePath = filePathToRoute(apiDir, absPath);
1243
- const relPath = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
1244
- const mutations = dbCalls.filter((c) => c.isMutation);
1245
- const reads = dbCalls.filter((c) => !c.isMutation);
1246
- const mutates = mutations.length > 0;
1247
- if (mutates) {
1248
- for (const m of mutations) {
1249
- 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
+ }
1250
1322
  }
1251
1323
  }
1252
- if (dbCalls.length > 0) endpointsWithDbAccess++;
1253
- const authStrategy = [];
1254
- for (const w of authWrappers) {
1255
- authStrategy.push(w);
1256
- 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
+ }
1257
1331
  }
1258
- if (authStrategy.length > 0) endpointsWithAuth++;
1259
- const deep = extractDeep(absPath);
1260
- nodes.push({
1261
- id: relPath,
1262
- type: "endpoint",
1263
- name: routePath,
1264
- path: routePath,
1265
- methods,
1266
- handler: relPath,
1267
- // Behavioral classification from handler body (AST-derived).
1268
- mutates,
1269
- auth: authStrategy.length > 0 ? authStrategy : ["public"],
1270
- db_models: [...new Set(dbCalls.map((c) => c.model))],
1271
- db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
1272
- // Deep extraction fields
1273
- conditions: deep.conditions,
1274
- variables: deep.variables,
1275
- responses: deep.responses,
1276
- 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
+ }
1277
1362
  });
1278
- const seenModels = /* @__PURE__ */ new Set();
1279
- for (const call of dbCalls) {
1280
- if (seenModels.has(call.model)) continue;
1281
- seenModels.add(call.model);
1282
- crossRefs.push({
1283
- source: relPath,
1284
- target: camelToPascal(call.model),
1285
- type: call.isMutation ? "mutates" : "reads",
1286
- layer: "db"
1287
- });
1288
- }
1289
1363
  }
1290
- nodes.sort((a, b) => a.path.localeCompare(b.path));
1291
- crossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1292
- const mutatorNodes = nodes.filter((n) => n.mutates).length;
1293
- const readOnlyNodes = nodes.filter((n) => !n.mutates).length;
1294
- return {
1295
- metadata: {
1296
- generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
1297
- scope: "main-app-only",
1298
- stack: "nextjs-app-router",
1299
- layer: "api",
1300
- parser: "nextjs-routes-ast",
1301
- total_endpoints: nodes.length,
1302
- total_methods: nodes.reduce((sum, n) => sum + n.methods.length, 0),
1303
- endpoints_with_auth: endpointsWithAuth,
1304
- endpoints_with_db_access: endpointsWithDbAccess,
1305
- mutator_endpoints: mutatorNodes,
1306
- read_only_endpoints: readOnlyNodes
1307
- },
1308
- nodes,
1309
- edges,
1310
- cross_refs: crossRefs,
1311
- contradictions: [],
1312
- warnings: [],
1313
- flagged_edges: [],
1314
- patterns: {
1315
- total_endpoints: nodes.length,
1316
- methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
1317
- acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
1318
- return acc;
1319
- }, {}),
1320
- auth_strategies: authUsage,
1321
- mutation_operations: mutatorCount,
1322
- mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
1323
- }
1324
- };
1364
+ return result;
1325
1365
  }
1326
- var import_node_fs5, import_node_path5, HTTP_METHODS, nextjsRoutesParser;
1327
- var init_nextjs_routes = __esm({
1328
- "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"() {
1329
1369
  "use strict";
1330
1370
  import_node_fs5 = require("node:fs");
1331
1371
  import_node_path5 = require("node:path");
1372
+ init_config();
1373
+ init_resolve_paths();
1332
1374
  init_ts_extractor();
1333
1375
  HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
1334
- nextjsRoutesParser = {
1335
- id: "nextjs-routes",
1336
- layer: "api",
1337
- detect: detect2,
1338
- 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
1339
1394
  };
1340
1395
  }
1341
1396
  });
@@ -1430,10 +1485,10 @@ function parseEnums(content) {
1430
1485
  }
1431
1486
  return nodes;
1432
1487
  }
1433
- function detect3(rootDir) {
1488
+ function detect2(rootDir) {
1434
1489
  return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "prisma", "schema.prisma"));
1435
1490
  }
1436
- function generate3(rootDir) {
1491
+ function generate2(rootDir) {
1437
1492
  const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
1438
1493
  const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
1439
1494
  const { nodes: modelNodes, relations } = parseModels(content);
@@ -1493,6 +1548,430 @@ var init_prisma_schema = __esm({
1493
1548
  prismaSchemaParser = {
1494
1549
  id: "prisma-schema",
1495
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",
1496
1975
  detect: detect3,
1497
1976
  generate: generate3
1498
1977
  };
@@ -1724,36 +2203,36 @@ var init_fetch_resolver = __esm({
1724
2203
  });
1725
2204
 
1726
2205
  // src/server/graph/parsers/crosslayer/api-annotations.ts
1727
- function walk3(dir, exts) {
1728
- if (!(0, import_node_fs7.existsSync)(dir)) return [];
2206
+ function walk2(dir, exts) {
2207
+ if (!(0, import_node_fs8.existsSync)(dir)) return [];
1729
2208
  const results = [];
1730
- 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 })) {
1731
2210
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1732
- const full = (0, import_node_path7.join)(dir, entry.name);
2211
+ const full = (0, import_node_path8.join)(dir, entry.name);
1733
2212
  if (entry.isDirectory()) {
1734
- results.push(...walk3(full, exts));
1735
- } 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))) {
1736
2215
  results.push(full);
1737
2216
  }
1738
2217
  }
1739
2218
  return results;
1740
2219
  }
1741
2220
  function toNodeId2(srcDir, absPath) {
1742
- return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
2221
+ return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
1743
2222
  }
1744
- var import_node_fs7, import_node_path7, API_ANNOTATION_RE, apiAnnotationsParser;
2223
+ var import_node_fs8, import_node_path8, API_ANNOTATION_RE, apiAnnotationsParser;
1745
2224
  var init_api_annotations = __esm({
1746
2225
  "src/server/graph/parsers/crosslayer/api-annotations.ts"() {
1747
2226
  "use strict";
1748
- import_node_fs7 = require("node:fs");
1749
- import_node_path7 = require("node:path");
2227
+ import_node_fs8 = require("node:fs");
2228
+ import_node_path8 = require("node:path");
1750
2229
  init_api_route_matching();
1751
2230
  API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
1752
2231
  apiAnnotationsParser = {
1753
2232
  id: "api-annotations",
1754
2233
  layer: "crosslayer",
1755
2234
  detect(rootDir) {
1756
- 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"));
1757
2236
  },
1758
2237
  generate(rootDir, layerOutputs) {
1759
2238
  const apiOutput = layerOutputs.get("api");
@@ -1764,13 +2243,13 @@ var init_api_annotations = __esm({
1764
2243
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1765
2244
  const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1766
2245
  const apiPathMap = buildApiPathMap(apiRoutes);
1767
- const srcDir = (0, import_node_path7.join)(rootDir, "src");
1768
- const files = walk3(srcDir, [".ts", ".tsx"]);
2246
+ const srcDir = (0, import_node_path8.join)(rootDir, "src");
2247
+ const files = walk2(srcDir, [".ts", ".tsx"]);
1769
2248
  const crossRefs = [];
1770
2249
  const flaggedEdges = [];
1771
2250
  const seen = /* @__PURE__ */ new Set();
1772
2251
  for (const absPath of files) {
1773
- const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
2252
+ const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
1774
2253
  const sourceId = toNodeId2(srcDir, absPath);
1775
2254
  if (!uiNodeIds.has(sourceId)) continue;
1776
2255
  let match;
@@ -1816,73 +2295,741 @@ var init_api_annotations = __esm({
1816
2295
  });
1817
2296
 
1818
2297
  // src/server/graph/parsers/crosslayer/url-literal-scanner.ts
1819
- function walk4(dir, exts) {
1820
- if (!(0, import_node_fs8.existsSync)(dir)) return [];
2298
+ function walk3(dir, exts) {
2299
+ if (!(0, import_node_fs9.existsSync)(dir)) return [];
1821
2300
  const results = [];
1822
- 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 })) {
1823
2302
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1824
- const full = (0, import_node_path8.join)(dir, entry.name);
2303
+ const full = (0, import_node_path9.join)(dir, entry.name);
1825
2304
  if (entry.isDirectory()) {
1826
- results.push(...walk4(full, exts));
1827
- } 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))) {
1828
2307
  results.push(full);
1829
2308
  }
1830
2309
  }
1831
- return results;
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;
1832
2895
  }
1833
- function toNodeId3(srcDir, absPath) {
1834
- return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
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;
1835
2919
  }
1836
- var import_node_fs8, import_node_path8, URL_LITERAL_RE, urlLiteralScannerParser;
1837
- var init_url_literal_scanner = __esm({
1838
- "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"() {
1839
2923
  "use strict";
1840
- import_node_fs8 = require("node:fs");
1841
- import_node_path8 = require("node:path");
1842
- init_api_route_matching();
1843
- URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
1844
- urlLiteralScannerParser = {
1845
- 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",
1846
2951
  layer: "crosslayer",
1847
2952
  detect(rootDir) {
1848
- return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
2953
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2954
+ return paths !== null;
1849
2955
  },
1850
2956
  generate(rootDir, layerOutputs) {
1851
- const apiOutput = layerOutputs.get("api");
1852
- if (!apiOutput) {
2957
+ const staticOutput = layerOutputs.get("static");
2958
+ if (!staticOutput || staticOutput.nodes.length === 0) {
1853
2959
  return { cross_refs: [], flagged_edges: [], warnings: [] };
1854
2960
  }
1855
- const uiOutput = layerOutputs.get("ui");
1856
- const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1857
- const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1858
- const apiPathMap = buildApiPathMap(apiRoutes);
1859
- const srcDir = (0, import_node_path8.join)(rootDir, "src");
1860
- const clientDir = (0, import_node_path8.join)(srcDir, "client");
1861
- 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;
1862
2984
  const files = [
1863
- ...walk4(clientDir, [".ts", ".tsx"]),
1864
- ...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"])
1865
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
+ }
1866
3003
  const crossRefs = [];
1867
3004
  const seen = /* @__PURE__ */ new Set();
3005
+ let filesScanned = 0;
1868
3006
  for (const absPath of files) {
1869
- const sourceId = toNodeId3(srcDir, absPath);
1870
- if (!uiNodeIds.has(sourceId)) continue;
1871
- const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
1872
- let match;
1873
- URL_LITERAL_RE.lastIndex = 0;
1874
- while ((match = URL_LITERAL_RE.exec(content)) !== null) {
1875
- const urlPath = match[1];
1876
- const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
1877
- if (result.kind === "resolved" && result.nodeId) {
1878
- 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}`;
1879
3026
  if (seen.has(key)) continue;
1880
3027
  seen.add(key);
1881
3028
  crossRefs.push({
1882
3029
  source: sourceId,
1883
- target: result.nodeId,
1884
- type: "references_api",
1885
- layer: "api"
3030
+ target: targetId,
3031
+ type: "references_static",
3032
+ layer: "static"
1886
3033
  });
1887
3034
  }
1888
3035
  }
@@ -1892,7 +3039,10 @@ var init_url_literal_scanner = __esm({
1892
3039
  flagged_edges: [],
1893
3040
  warnings: [],
1894
3041
  patterns: {
1895
- 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"
1896
3046
  }
1897
3047
  };
1898
3048
  }
@@ -1901,14 +3051,19 @@ var init_url_literal_scanner = __esm({
1901
3051
  });
1902
3052
 
1903
3053
  // src/server/graph/core/parser-registry.ts
3054
+ function isMultiLayerParser(p) {
3055
+ return "layers" in p && Array.isArray(p.layers);
3056
+ }
1904
3057
  function registerBuiltins(registry, disabled) {
1905
3058
  const builtins = [
1906
- reactNextjsParser,
1907
- nextjsRoutesParser,
3059
+ typescriptProjectParser,
1908
3060
  prismaSchemaParser,
3061
+ sqlMigrationsParser,
3062
+ staticValuesParser,
1909
3063
  fetchResolverParser,
1910
3064
  apiAnnotationsParser,
1911
- urlLiteralScannerParser
3065
+ urlLiteralScannerParser,
3066
+ staticRefScannerParser
1912
3067
  ];
1913
3068
  for (const parser of builtins) {
1914
3069
  if (disabled.has(parser.id)) continue;
@@ -1918,11 +3073,11 @@ function registerBuiltins(registry, disabled) {
1918
3073
  function loadCustomParsers(registry, config, rootDir, disabled) {
1919
3074
  for (const entry of config.parsers?.custom ?? []) {
1920
3075
  try {
1921
- const absPath = (0, import_node_path9.resolve)(rootDir, entry.path);
3076
+ const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
1922
3077
  const mod = require(absPath);
1923
3078
  const parser = "default" in mod ? mod.default : mod;
1924
3079
  if (disabled.has(parser.id)) continue;
1925
- if (parser.layer !== entry.layer) {
3080
+ if (!isMultiLayerParser(parser) && parser.layer !== entry.layer) {
1926
3081
  process.stderr.write(
1927
3082
  `[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
1928
3083
  `
@@ -1942,20 +3097,23 @@ function createRegistry(config, rootDir) {
1942
3097
  loadCustomParsers(registry, config, rootDir, disabled);
1943
3098
  return registry;
1944
3099
  }
1945
- var import_node_path9, ParserRegistry;
3100
+ var import_node_path12, ParserRegistry;
1946
3101
  var init_parser_registry = __esm({
1947
3102
  "src/server/graph/core/parser-registry.ts"() {
1948
3103
  "use strict";
1949
- import_node_path9 = require("node:path");
1950
- init_react_nextjs();
1951
- init_nextjs_routes();
3104
+ import_node_path12 = require("node:path");
3105
+ init_typescript_project();
1952
3106
  init_prisma_schema();
3107
+ init_sql_migrations();
1953
3108
  init_fetch_resolver();
1954
3109
  init_api_annotations();
1955
3110
  init_url_literal_scanner();
3111
+ init_static_values();
3112
+ init_static_ref_scanner();
1956
3113
  ParserRegistry = class {
1957
3114
  constructor() {
1958
- this.parsers = /* @__PURE__ */ new Map();
3115
+ this.singleLayerParsers = /* @__PURE__ */ new Map();
3116
+ this.multiLayerParsers = [];
1959
3117
  this.ids = /* @__PURE__ */ new Set();
1960
3118
  }
1961
3119
  register(parser) {
@@ -1963,19 +3121,44 @@ var init_parser_registry = __esm({
1963
3121
  throw new Error(`Duplicate parser id: ${parser.id}`);
1964
3122
  }
1965
3123
  this.ids.add(parser.id);
1966
- const list = this.parsers.get(parser.layer) ?? [];
1967
- list.push(parser);
1968
- 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
+ }
1969
3131
  }
3132
+ /** Get single-layer parsers for a specific layer. */
1970
3133
  getParsers(layer) {
1971
- 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];
1972
3143
  }
1973
3144
  getCrossLayerParsers() {
1974
- 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];
1975
3157
  }
1976
3158
  getAll() {
1977
3159
  const all = [];
1978
- 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);
1979
3162
  return all;
1980
3163
  }
1981
3164
  };
@@ -2112,10 +3295,10 @@ var init_merge = __esm({
2112
3295
 
2113
3296
  // src/server/graph/core/graph-builder.ts
2114
3297
  function readGraphFromDisk(rootDir, layer) {
2115
- const filePath = (0, import_node_path10.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
2116
- 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;
2117
3300
  try {
2118
- return JSON.parse((0, import_node_fs9.readFileSync)(filePath, "utf-8"));
3301
+ return JSON.parse((0, import_node_fs12.readFileSync)(filePath, "utf-8"));
2119
3302
  } catch {
2120
3303
  return null;
2121
3304
  }
@@ -2123,18 +3306,24 @@ function readGraphFromDisk(rootDir, layer) {
2123
3306
  function generateLayer(rootDir, layer) {
2124
3307
  const config = loadConfig(rootDir);
2125
3308
  const registry = createRegistry(config, rootDir);
2126
- const parsers = registry.getParsers(layer);
2127
3309
  const outputs = [];
2128
- for (const parser of parsers) {
3310
+ for (const parser of registry.getParsers(layer)) {
2129
3311
  if (!parser.detect(rootDir)) continue;
2130
3312
  outputs.push(parser.generate(rootDir));
2131
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
+ }
2132
3320
  if (outputs.length === 0) return null;
2133
3321
  let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
2134
3322
  if (layer === "ui") {
2135
3323
  const layerOutputs = /* @__PURE__ */ new Map();
2136
3324
  layerOutputs.set("ui", merged);
2137
- for (const otherLayer of ["api", "db"]) {
3325
+ for (const otherLayer of registry.getAvailableLayers()) {
3326
+ if (otherLayer === "ui") continue;
2138
3327
  const existing = readGraphFromDisk(rootDir, otherLayer);
2139
3328
  if (existing) layerOutputs.set(otherLayer, existing);
2140
3329
  }
@@ -2155,16 +3344,29 @@ function generateLayer(rootDir, layer) {
2155
3344
  function generateAll(rootDir) {
2156
3345
  const config = loadConfig(rootDir);
2157
3346
  const registry = createRegistry(config, rootDir);
2158
- 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
+ ];
2159
3352
  const layerOutputs = /* @__PURE__ */ new Map();
2160
3353
  const results = [];
3354
+ const multiLayerResults = /* @__PURE__ */ new Map();
2161
3355
  for (const layer of layerOrder) {
2162
- const parsers = registry.getParsers(layer);
2163
3356
  const outputs = [];
2164
- for (const parser of parsers) {
3357
+ for (const parser of registry.getParsers(layer)) {
2165
3358
  if (!parser.detect(rootDir)) continue;
2166
3359
  outputs.push(parser.generate(rootDir));
2167
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
+ }
2168
3370
  if (outputs.length === 0) continue;
2169
3371
  const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
2170
3372
  layerOutputs.set(layer, merged);
@@ -2190,14 +3392,16 @@ function generateAll(rootDir) {
2190
3392
  }
2191
3393
  }
2192
3394
  const byLayer = new Map(results.map((r) => [r.layer, r]));
2193
- 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);
2194
3398
  }
2195
- var import_node_fs9, import_node_path10;
3399
+ var import_node_fs12, import_node_path13;
2196
3400
  var init_graph_builder = __esm({
2197
3401
  "src/server/graph/core/graph-builder.ts"() {
2198
3402
  "use strict";
2199
- import_node_fs9 = require("node:fs");
2200
- import_node_path10 = require("node:path");
3403
+ import_node_fs12 = require("node:fs");
3404
+ import_node_path13 = require("node:path");
2201
3405
  init_config();
2202
3406
  init_parser_registry();
2203
3407
  init_merge();
@@ -2236,18 +3440,18 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
2236
3440
  const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
2237
3441
  const searchDirs = [
2238
3442
  rootDir,
2239
- (0, import_node_path11.join)(rootDir, "src"),
2240
- (0, import_node_path11.join)(rootDir, "app"),
2241
- (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")
2242
3446
  ];
2243
3447
  for (const base of searchDirs) {
2244
3448
  for (const convention of conventionDirs) {
2245
- const dir = (0, import_node_path11.join)(base, convention);
2246
- 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;
2247
3451
  try {
2248
- const stat = (0, import_node_fs10.statSync)(dir);
3452
+ const stat = (0, import_node_fs13.statSync)(dir);
2249
3453
  if (!stat.isDirectory()) continue;
2250
- 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);
2251
3455
  if (entries.length > 0) {
2252
3456
  const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
2253
3457
  result.set(relPath, entries);
@@ -2319,12 +3523,12 @@ function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
2319
3523
  }
2320
3524
  return "root";
2321
3525
  }
2322
- 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;
2323
3527
  var init_module_tagger = __esm({
2324
3528
  "src/server/graph/taggers/module-tagger.ts"() {
2325
3529
  "use strict";
2326
- import_node_fs10 = require("node:fs");
2327
- import_node_path11 = require("node:path");
3530
+ import_node_fs13 = require("node:fs");
3531
+ import_node_path14 = require("node:path");
2328
3532
  CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
2329
3533
  GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
2330
3534
  // JS/TS
@@ -2536,7 +3740,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
2536
3740
  for (const entry of config.taggers?.custom ?? []) {
2537
3741
  if (disabled.has(entry.id)) continue;
2538
3742
  try {
2539
- const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
3743
+ const absPath = (0, import_node_path15.resolve)(rootDir, entry.path);
2540
3744
  const mod = require(absPath);
2541
3745
  const tagger = "default" in mod ? mod.default : mod;
2542
3746
  const override = config.taggers?.trackUntagged?.[tagger.id];
@@ -2557,11 +3761,11 @@ function createTaggerRegistry(config, rootDir) {
2557
3761
  loadCustomTaggers(registry, config, rootDir, disabled);
2558
3762
  return registry;
2559
3763
  }
2560
- var import_node_path12, TaggerRegistry, BUILTIN_TAGGERS;
3764
+ var import_node_path15, TaggerRegistry, BUILTIN_TAGGERS;
2561
3765
  var init_tagger_registry = __esm({
2562
3766
  "src/server/graph/core/tagger-registry.ts"() {
2563
3767
  "use strict";
2564
- import_node_path12 = require("node:path");
3768
+ import_node_path15 = require("node:path");
2565
3769
  init_module_tagger();
2566
3770
  init_screen_tagger();
2567
3771
  TaggerRegistry = class {
@@ -2589,18 +3793,18 @@ var init_tagger_registry = __esm({
2589
3793
 
2590
3794
  // src/server/graph/core/tag-store.ts
2591
3795
  function tagsFilePath(rootDir) {
2592
- return (0, import_node_path13.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
3796
+ return (0, import_node_path16.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
2593
3797
  }
2594
3798
  function readTagStore(rootDir) {
2595
3799
  const filePath = tagsFilePath(rootDir);
2596
- if (!(0, import_node_fs11.existsSync)(filePath)) return {};
2597
- 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);
2598
3802
  const cached = tagCache.get(filePath);
2599
3803
  if (cached && cached.mtimeMs === stat.mtimeMs) {
2600
3804
  return cached.store;
2601
3805
  }
2602
3806
  try {
2603
- const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
3807
+ const content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
2604
3808
  const store = JSON.parse(content);
2605
3809
  tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
2606
3810
  return store;
@@ -2610,15 +3814,15 @@ function readTagStore(rootDir) {
2610
3814
  }
2611
3815
  function writeTagStore(rootDir, store) {
2612
3816
  const filePath = tagsFilePath(rootDir);
2613
- const dir = (0, import_node_path13.dirname)(filePath);
2614
- (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 });
2615
3819
  const cleaned = {};
2616
3820
  for (const [nodeId, tags] of Object.entries(store)) {
2617
3821
  if (Object.keys(tags).length > 0) {
2618
3822
  cleaned[nodeId] = tags;
2619
3823
  }
2620
3824
  }
2621
- (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");
2622
3826
  tagCache.delete(filePath);
2623
3827
  }
2624
3828
  function setTag(rootDir, nodeId, key, value) {
@@ -2636,12 +3840,12 @@ function removeTag(rootDir, nodeId, key) {
2636
3840
  }
2637
3841
  writeTagStore(rootDir, store);
2638
3842
  }
2639
- 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;
2640
3844
  var init_tag_store = __esm({
2641
3845
  "src/server/graph/core/tag-store.ts"() {
2642
3846
  "use strict";
2643
- import_node_fs11 = require("node:fs");
2644
- import_node_path13 = require("node:path");
3847
+ import_node_fs14 = require("node:fs");
3848
+ import_node_path16 = require("node:path");
2645
3849
  TAGS_FILENAME = "tags.json";
2646
3850
  GRAPHS_DIR = ".launchsecure/graphs";
2647
3851
  tagCache = /* @__PURE__ */ new Map();
@@ -2649,18 +3853,23 @@ var init_tag_store = __esm({
2649
3853
  });
2650
3854
 
2651
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
+ }
2652
3861
  function graphsDir(rootDir) {
2653
- return (0, import_node_path14.join)(rootDir, GRAPHS_DIR2);
3862
+ return (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
2654
3863
  }
2655
3864
  function graphFilePath(rootDir, layer) {
2656
- return (0, import_node_path14.join)(graphsDir(rootDir), `${layer}.json`);
3865
+ return (0, import_node_path17.join)(graphsDir(rootDir), `${layer}.json`);
2657
3866
  }
2658
3867
  function tagsFilePath2(rootDir) {
2659
- return (0, import_node_path14.join)(graphsDir(rootDir), "tags.json");
3868
+ return (0, import_node_path17.join)(graphsDir(rootDir), "tags.json");
2660
3869
  }
2661
3870
  function getMtimeMs(filePath) {
2662
- if (!(0, import_node_fs12.existsSync)(filePath)) return 0;
2663
- 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;
2664
3873
  }
2665
3874
  function invalidateCache(filePath) {
2666
3875
  graphCache.delete(filePath);
@@ -2699,20 +3908,20 @@ function applyTags(graph, layer, rootDir) {
2699
3908
  }
2700
3909
  function readGraphRaw(rootDir, layer) {
2701
3910
  const filePath = graphFilePath(rootDir, layer);
2702
- if (!(0, import_node_fs12.existsSync)(filePath)) return null;
2703
- 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);
2704
3913
  const cached = graphCache.get(filePath);
2705
3914
  if (cached && cached.mtimeMs === stat.mtimeMs) {
2706
3915
  return cached.graph;
2707
3916
  }
2708
- const content = (0, import_node_fs12.readFileSync)(filePath, "utf-8");
3917
+ const content = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
2709
3918
  const graph = JSON.parse(content);
2710
3919
  graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
2711
3920
  return graph;
2712
3921
  }
2713
3922
  function readGraph(rootDir, layer) {
2714
3923
  const rawFilePath = graphFilePath(rootDir, layer);
2715
- if (!(0, import_node_fs12.existsSync)(rawFilePath)) return null;
3924
+ if (!(0, import_node_fs15.existsSync)(rawFilePath)) return null;
2716
3925
  const rawMtime = getMtimeMs(rawFilePath);
2717
3926
  const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
2718
3927
  const cacheKey = `${rootDir}:${layer}`;
@@ -2728,7 +3937,7 @@ function readGraph(rootDir, layer) {
2728
3937
  }
2729
3938
  function readAllGraphs(rootDir) {
2730
3939
  const result = {};
2731
- for (const layer of LAYERS) {
3940
+ for (const layer of getAvailableLayers(rootDir)) {
2732
3941
  const graph = readGraph(rootDir, layer);
2733
3942
  if (graph) result[layer] = graph;
2734
3943
  }
@@ -2742,22 +3951,22 @@ async function generateGraph(rootDir, layer) {
2742
3951
  mutationMethods: config.parsers?.patterns?.mutationMethods
2743
3952
  });
2744
3953
  const dir = graphsDir(rootDir);
2745
- (0, import_node_fs12.mkdirSync)(dir, { recursive: true });
3954
+ (0, import_node_fs15.mkdirSync)(dir, { recursive: true });
2746
3955
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
2747
3956
  for (const result of results) {
2748
3957
  const filePath = graphFilePath(rootDir, result.layer);
2749
- (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");
2750
3959
  invalidateCache(filePath);
2751
3960
  invalidateTaggedCache(rootDir, result.layer);
2752
3961
  }
2753
3962
  return results;
2754
3963
  }
2755
- var import_node_fs12, import_node_path14, GRAPHS_DIR2, LAYERS, graphCache, taggedCache;
3964
+ var import_node_fs15, import_node_path17, GRAPHS_DIR2, graphCache, taggedCache;
2756
3965
  var init_graph = __esm({
2757
3966
  "src/server/graph/index.ts"() {
2758
3967
  "use strict";
2759
- import_node_fs12 = require("node:fs");
2760
- import_node_path14 = require("node:path");
3968
+ import_node_fs15 = require("node:fs");
3969
+ import_node_path17 = require("node:path");
2761
3970
  init_graph_builder();
2762
3971
  init_config();
2763
3972
  init_tagger_registry();
@@ -2765,12 +3974,294 @@ var init_graph = __esm({
2765
3974
  init_ts_extractor();
2766
3975
  init_tag_store();
2767
3976
  GRAPHS_DIR2 = ".launchsecure/graphs";
2768
- LAYERS = ["ui", "api", "db"];
2769
3977
  graphCache = /* @__PURE__ */ new Map();
2770
3978
  taggedCache = /* @__PURE__ */ new Map();
2771
3979
  }
2772
3980
  });
2773
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
+
2774
4265
  // src/server/chart-serve.ts
2775
4266
  var chart_serve_exports = {};
2776
4267
  __export(chart_serve_exports, {
@@ -2783,31 +4274,39 @@ function randomPort() {
2783
4274
  function findProjectRoot(startDir) {
2784
4275
  let dir = startDir;
2785
4276
  for (let i = 0; i < 8; i++) {
2786
- const graphsDir2 = import_node_path15.default.join(dir, ".launchsecure", "graphs");
2787
- 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;
2788
- 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);
2789
4280
  if (parent === dir) break;
2790
4281
  dir = parent;
2791
4282
  }
2792
4283
  dir = startDir;
2793
4284
  for (let i = 0; i < 8; i++) {
2794
- if (import_node_fs13.default.existsSync(import_node_path15.default.join(dir, ".git"))) return dir;
2795
- 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);
2796
4287
  if (parent === dir) break;
2797
4288
  dir = parent;
2798
4289
  }
2799
4290
  return startDir;
2800
4291
  }
2801
- async function buildMergedGraph(projectRoot) {
2802
- let graphs = readAllGraphs(projectRoot);
2803
- if (!graphs.ui && !graphs.api && !graphs.db) {
2804
- await generateGraph(projectRoot);
2805
- 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);
2806
4306
  }
2807
4307
  const nodes = [];
2808
4308
  const rawLinks = [];
2809
- const LAYERS2 = ["ui", "api", "db"];
2810
- for (const layer of LAYERS2) {
4309
+ for (const layer of Object.keys(graphs)) {
2811
4310
  const g = graphs[layer];
2812
4311
  if (!g) continue;
2813
4312
  for (const n of g.nodes) {
@@ -2844,16 +4343,16 @@ async function buildMergedGraph(projectRoot) {
2844
4343
  };
2845
4344
  }
2846
4345
  function serveStatic(res, filePath) {
2847
- if (!import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) return false;
2848
- 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();
2849
4348
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
2850
4349
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
2851
- import_node_fs13.default.createReadStream(filePath).pipe(res);
4350
+ import_node_fs17.default.createReadStream(filePath).pipe(res);
2852
4351
  return true;
2853
4352
  }
2854
4353
  function serveIndex(res, clientDir) {
2855
- const indexPath = import_node_path15.default.join(clientDir, "index.html");
2856
- 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)) {
2857
4356
  res.writeHead(500, { "Content-Type": "text/plain" });
2858
4357
  res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
2859
4358
  return;
@@ -2905,19 +4404,40 @@ async function startChartServer(opts = {}) {
2905
4404
  }
2906
4405
  return { port: existing.port, url: existing.url };
2907
4406
  }
2908
- 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 ?? [];
2909
4410
  const server = import_node_http.default.createServer((req, res) => {
2910
4411
  try {
2911
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
+ }
2912
4432
  if (req.method === "GET" && url2.pathname === "/api/project-graph") {
2913
4433
  const regenerate = url2.searchParams.get("regenerate") === "1";
2914
4434
  (async () => {
2915
- if (regenerate) await generateGraph(projectRoot);
2916
- const merged = await buildMergedGraph(projectRoot);
4435
+ if (regenerate) await generateGraph(reqRoot);
4436
+ const merged = await buildMergedGraph(reqRoot);
2917
4437
  res.writeHead(200, { "Content-Type": "application/json" });
2918
4438
  res.end(JSON.stringify({
2919
4439
  ...merged,
2920
- debug: { cwd, projectRoot }
4440
+ debug: { cwd, projectRoot: reqRoot }
2921
4441
  }));
2922
4442
  })().catch((e) => {
2923
4443
  res.writeHead(500);
@@ -2926,15 +4446,15 @@ async function startChartServer(opts = {}) {
2926
4446
  return;
2927
4447
  }
2928
4448
  if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
2929
- const graphs = readAllGraphs(projectRoot);
4449
+ const graphs = readAllGraphs(reqRoot);
2930
4450
  res.writeHead(200, { "Content-Type": "application/json" });
2931
4451
  res.end(JSON.stringify({ ui: graphs.ui ?? null, api: graphs.api ?? null, db: graphs.db ?? null }));
2932
4452
  return;
2933
4453
  }
2934
4454
  if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
2935
4455
  (async () => {
2936
- await generateGraph(projectRoot);
2937
- const graphs = readAllGraphs(projectRoot);
4456
+ await generateGraph(reqRoot);
4457
+ const graphs = readAllGraphs(reqRoot);
2938
4458
  res.writeHead(200, { "Content-Type": "application/json" });
2939
4459
  res.end(JSON.stringify({
2940
4460
  ok: true,
@@ -2950,41 +4470,43 @@ async function startChartServer(opts = {}) {
2950
4470
  }
2951
4471
  if (req.method === "GET" && url2.pathname === "/api/file-content") {
2952
4472
  const relPath = url2.searchParams.get("path");
2953
- if (!relPath || relPath.includes("..") || import_node_path15.default.isAbsolute(relPath)) {
4473
+ if (!relPath || relPath.includes("..") || import_node_path19.default.isAbsolute(relPath)) {
2954
4474
  res.writeHead(400, { "Content-Type": "application/json" });
2955
4475
  res.end(JSON.stringify({ error: "Invalid path" }));
2956
4476
  return;
2957
4477
  }
2958
- const filePath = import_node_path15.default.join(projectRoot, relPath);
2959
- 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()) {
2960
4480
  res.writeHead(404, { "Content-Type": "application/json" });
2961
4481
  res.end(JSON.stringify({ error: "File not found" }));
2962
4482
  return;
2963
4483
  }
2964
- const ext = import_node_path15.default.extname(filePath).toLowerCase();
4484
+ const ext = import_node_path19.default.extname(filePath).toLowerCase();
2965
4485
  const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
2966
- const content = import_node_fs13.default.readFileSync(filePath, "utf-8");
4486
+ const content = import_node_fs17.default.readFileSync(filePath, "utf-8");
2967
4487
  res.writeHead(200, { "Content-Type": "application/json" });
2968
4488
  res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
2969
4489
  return;
2970
4490
  }
2971
4491
  if (req.method === "GET" && url2.pathname === "/api/health") {
2972
4492
  res.writeHead(200, { "Content-Type": "application/json" });
2973
- res.end(JSON.stringify({ ok: true, projectRoot }));
4493
+ res.end(JSON.stringify({ ok: true, projectRoot: reqRoot }));
2974
4494
  return;
2975
4495
  }
2976
4496
  if (req.method === "GET" && url2.pathname === "/api/parser-config") {
2977
- const config2 = loadConfig(projectRoot);
2978
- const detection = [
2979
- { id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
2980
- { id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
2981
- { id: "prisma-schema", layer: "db", label: "Prisma Schema", detected: prismaSchemaParser.detect(projectRoot) }
2982
- ];
2983
- const crosslayerParsers = [
2984
- { id: "fetch-resolver", label: "Fetch / api.method() calls" },
2985
- { id: "api-annotations", label: "@api annotations" },
2986
- { id: "url-literal-scanner", label: "/api/... URL literals" }
2987
- ];
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 }));
2988
4510
  res.writeHead(200, { "Content-Type": "application/json" });
2989
4511
  res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
2990
4512
  return;
@@ -2997,8 +4519,8 @@ async function startChartServer(opts = {}) {
2997
4519
  req.on("end", () => {
2998
4520
  try {
2999
4521
  const newConfig = JSON.parse(body);
3000
- const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
3001
- 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");
3002
4524
  res.writeHead(200, { "Content-Type": "application/json" });
3003
4525
  res.end(JSON.stringify({ ok: true }));
3004
4526
  } catch (err2) {
@@ -3009,7 +4531,7 @@ async function startChartServer(opts = {}) {
3009
4531
  return;
3010
4532
  }
3011
4533
  if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
3012
- const config2 = loadConfig(projectRoot);
4534
+ const config2 = loadConfig(reqRoot);
3013
4535
  const builtinTaggers = [
3014
4536
  { id: "module", tagKey: "module", trackUntagged: config2.taggers?.trackUntagged?.module ?? true },
3015
4537
  { id: "screen", tagKey: "screen", trackUntagged: config2.taggers?.trackUntagged?.screen ?? true }
@@ -3029,10 +4551,10 @@ async function startChartServer(opts = {}) {
3029
4551
  req.on("end", () => {
3030
4552
  try {
3031
4553
  const taggerConfig = JSON.parse(body);
3032
- const config2 = loadConfig(projectRoot);
4554
+ const config2 = loadConfig(reqRoot);
3033
4555
  config2.taggers = taggerConfig;
3034
- const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
3035
- 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");
3036
4558
  res.writeHead(200, { "Content-Type": "application/json" });
3037
4559
  res.end(JSON.stringify({ ok: true }));
3038
4560
  } catch (err2) {
@@ -3043,7 +4565,7 @@ async function startChartServer(opts = {}) {
3043
4565
  return;
3044
4566
  }
3045
4567
  if (req.method === "GET" && url2.pathname === "/api/tags") {
3046
- const store = readTagStore(projectRoot);
4568
+ const store = readTagStore(reqRoot);
3047
4569
  res.writeHead(200, { "Content-Type": "application/json" });
3048
4570
  res.end(JSON.stringify(store));
3049
4571
  return;
@@ -3061,7 +4583,7 @@ async function startChartServer(opts = {}) {
3061
4583
  res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
3062
4584
  return;
3063
4585
  }
3064
- setTag(projectRoot, nodeId, key, value);
4586
+ setTag(reqRoot, nodeId, key, value);
3065
4587
  res.writeHead(200, { "Content-Type": "application/json" });
3066
4588
  res.end(JSON.stringify({ ok: true }));
3067
4589
  } catch (err2) {
@@ -3084,7 +4606,76 @@ async function startChartServer(opts = {}) {
3084
4606
  res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
3085
4607
  return;
3086
4608
  }
3087
- 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);
3088
4679
  res.writeHead(200, { "Content-Type": "application/json" });
3089
4680
  res.end(JSON.stringify({ ok: true }));
3090
4681
  } catch (err2) {
@@ -3095,7 +4686,7 @@ async function startChartServer(opts = {}) {
3095
4686
  return;
3096
4687
  }
3097
4688
  if (url2.pathname !== "/") {
3098
- const staticPath = import_node_path15.default.join(clientDir, url2.pathname);
4689
+ const staticPath = import_node_path19.default.join(clientDir, url2.pathname);
3099
4690
  if (serveStatic(res, staticPath)) return;
3100
4691
  }
3101
4692
  serveIndex(res, clientDir);
@@ -3133,6 +4724,10 @@ async function startChartServer(opts = {}) {
3133
4724
  `);
3134
4725
  process.stderr.write(`[launch-chart] project root: ${projectRoot}
3135
4726
  `);
4727
+ if (projects.length > 0) {
4728
+ process.stderr.write(`[launch-chart] multi-project mode: ${projects.length} projects
4729
+ `);
4730
+ }
3136
4731
  }
3137
4732
  return { port, url };
3138
4733
  }
@@ -3151,19 +4746,19 @@ function runServeCli(argv) {
3151
4746
  process.exit(1);
3152
4747
  });
3153
4748
  }
3154
- 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;
3155
4750
  var init_chart_serve = __esm({
3156
4751
  "src/server/chart-serve.ts"() {
3157
4752
  "use strict";
3158
4753
  import_node_http = __toESM(require("node:http"));
3159
- import_node_fs13 = __toESM(require("node:fs"));
3160
- import_node_path15 = __toESM(require("node:path"));
4754
+ import_node_fs17 = __toESM(require("node:fs"));
4755
+ import_node_path19 = __toESM(require("node:path"));
3161
4756
  init_graph();
3162
4757
  init_lockfile();
3163
4758
  init_config();
3164
- init_react_nextjs();
3165
- init_nextjs_routes();
3166
- init_prisma_schema();
4759
+ init_parser_registry();
4760
+ init_resolve_paths();
4761
+ init_audit_core();
3167
4762
  MAX_PORT_SCAN = 3;
3168
4763
  MIME_TYPES = {
3169
4764
  ".html": "text/html; charset=utf-8",
@@ -3179,6 +4774,179 @@ var init_chart_serve = __esm({
3179
4774
  }
3180
4775
  });
3181
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
+
3182
4950
  // src/server/graph-mcp.ts
3183
4951
  var graph_mcp_exports = {};
3184
4952
  __export(graph_mcp_exports, {
@@ -3265,8 +5033,8 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
3265
5033
  const dstIn = visited.has(e.target) || next.has(e.target);
3266
5034
  if (srcIn && dstIn) projectedEdges++;
3267
5035
  }
3268
- const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] : EST_CHARS_PER_NODE_FULL[layer];
3269
- 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);
3270
5038
  if (projectedChars > NEIGHBORHOOD_BUDGET_CHARS) {
3271
5039
  budgetExceeded = true;
3272
5040
  break;
@@ -3314,9 +5082,6 @@ function err(text) {
3314
5082
  async function handleGenerateGraph(args) {
3315
5083
  const rootDir = process.cwd();
3316
5084
  const layer = args.layer;
3317
- if (layer && !["ui", "api", "db"].includes(layer)) {
3318
- return err(`Invalid layer "${layer}". Must be one of: ui, api, db`);
3319
- }
3320
5085
  const results = await generateGraph(rootDir, layer);
3321
5086
  if (results.length === 0) {
3322
5087
  return err(
@@ -3353,8 +5118,11 @@ function runReadGraphQueryRaw(rootDir, args) {
3353
5118
  const offset = args.offset ?? 0;
3354
5119
  const limit = args.limit;
3355
5120
  const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
3356
- if (layer && !["ui", "api", "db"].includes(layer)) {
3357
- 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
+ }
3358
5126
  }
3359
5127
  if (!layer) {
3360
5128
  const graphs = readAllGraphs(rootDir);
@@ -3508,9 +5276,12 @@ function handleReadGraph(args) {
3508
5276
  return okJson(result);
3509
5277
  }
3510
5278
  function nodeToFilePath(rootDir, layer, nodeId) {
3511
- if (layer === "ui") return (0, import_node_path16.join)(rootDir, "src", nodeId);
3512
- if (layer === "api") return (0, import_node_path16.join)(rootDir, nodeId);
3513
- 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;
3514
5285
  return null;
3515
5286
  }
3516
5287
  function handleInspectNode(args) {
@@ -3599,7 +5370,10 @@ function handleGrepNodes(args) {
3599
5370
  const layer = args.layer;
3600
5371
  if (!pattern) return err("pattern is required");
3601
5372
  if (!layer) return err("layer is required (ui, api, or db)");
3602
- 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
+ }
3603
5377
  const hasFilter = !!(args.search || args.type || args.module || args.node_id);
3604
5378
  if (!hasFilter) {
3605
5379
  return err(
@@ -3650,11 +5424,11 @@ function handleGrepNodes(args) {
3650
5424
  let filesSearched = 0;
3651
5425
  let truncated = false;
3652
5426
  for (const [filePath, nodeId] of filePaths) {
3653
- if (!(0, import_node_fs14.existsSync)(filePath)) continue;
5427
+ if (!(0, import_node_fs19.existsSync)(filePath)) continue;
3654
5428
  filesSearched++;
3655
5429
  let content;
3656
5430
  try {
3657
- content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
5431
+ content = (0, import_node_fs19.readFileSync)(filePath, "utf-8");
3658
5432
  } catch {
3659
5433
  continue;
3660
5434
  }
@@ -3719,11 +5493,11 @@ function handleStartChartServer(args) {
3719
5493
  });
3720
5494
  }
3721
5495
  const entryPath = process.argv[1];
3722
- const logDir = (0, import_node_path16.join)((0, import_node_os2.homedir)(), ".launchsecure");
3723
- (0, import_node_fs14.mkdirSync)(logDir, { recursive: true });
3724
- const logPath = (0, import_node_path16.join)(logDir, "launch-chart.log");
3725
- const out = (0, import_node_fs14.openSync)(logPath, "a");
3726
- 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");
3727
5501
  const portArgs = args.port ? ["--port", String(args.port)] : [];
3728
5502
  const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
3729
5503
  detached: true,
@@ -3786,14 +5560,55 @@ function handleRemoveTag(args) {
3786
5560
  removeTag(rootDir, nodeId, key);
3787
5561
  return okJson({ ok: true, node_id: nodeId, removed_key: key });
3788
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
+ }
3789
5592
  function handleDetectProjectStack() {
3790
5593
  const rootDir = process.cwd();
3791
- const parsers = [
3792
- { id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
3793
- { id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
3794
- { id: "prisma-schema", layer: "db", detected: prismaSchemaParser.detect(rootDir) }
3795
- ];
3796
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
+ }
3797
5612
  let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
3798
5613
  const uiGraph = readGraph(rootDir, "ui");
3799
5614
  if (uiGraph) {
@@ -3805,20 +5620,20 @@ function handleDetectProjectStack() {
3805
5620
  if (f.type === "out_of_pattern") stats.out_of_pattern++;
3806
5621
  }
3807
5622
  }
3808
- const srcDir = (0, import_node_path16.join)(rootDir, "src");
3809
- 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)) {
3810
5625
  const scanDir = (dir) => {
3811
- if (!(0, import_node_fs14.existsSync)(dir)) return;
3812
- 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 })) {
3813
5628
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
3814
- const full = (0, import_node_path16.join)(dir, entry.name);
5629
+ const full = (0, import_node_path21.join)(dir, entry.name);
3815
5630
  if (entry.isDirectory()) {
3816
5631
  scanDir(full);
3817
5632
  continue;
3818
5633
  }
3819
- 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;
3820
5635
  try {
3821
- const content = (0, import_node_fs14.readFileSync)(full, "utf-8");
5636
+ const content = (0, import_node_fs19.readFileSync)(full, "utf-8");
3822
5637
  const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
3823
5638
  if (matches) stats.annotations += matches.length;
3824
5639
  } catch {
@@ -3833,8 +5648,16 @@ function handleDetectProjectStack() {
3833
5648
  } else if (stats.calls_api === 0 && stats.references_api > 0) {
3834
5649
  recommendedPrimary = "url-literal-scanner";
3835
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;
3836
5657
  return okJson({
3837
- parsers,
5658
+ languages,
5659
+ parsers: parserResults,
5660
+ available_layers: [...availableLayers],
3838
5661
  crosslayer_parsers: [
3839
5662
  { id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
3840
5663
  { id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
@@ -3842,6 +5665,7 @@ function handleDetectProjectStack() {
3842
5665
  ],
3843
5666
  stats,
3844
5667
  recommended_primary: recommendedPrimary,
5668
+ ...unsupportedHint ? { unsupported_hint: unsupportedHint } : {},
3845
5669
  current_config: Object.keys(config).length > 0 ? config : null,
3846
5670
  config_path: ".launchchart.json"
3847
5671
  });
@@ -3917,6 +5741,10 @@ async function handleMessage(msg) {
3917
5741
  respond(id ?? null, handleRemoveTag(args));
3918
5742
  return;
3919
5743
  }
5744
+ if (toolName === "audit_layer") {
5745
+ respond(id ?? null, handleAuditLayer(args));
5746
+ return;
5747
+ }
3920
5748
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
3921
5749
  return;
3922
5750
  }
@@ -3952,20 +5780,20 @@ function startGraphMcpServer() {
3952
5780
  process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
3953
5781
  `);
3954
5782
  }
3955
- 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;
3956
5784
  var init_graph_mcp = __esm({
3957
5785
  "src/server/graph-mcp.ts"() {
3958
5786
  "use strict";
3959
- import_node_fs14 = require("node:fs");
3960
- import_node_path16 = require("node:path");
5787
+ import_node_fs19 = require("node:fs");
5788
+ import_node_path21 = require("node:path");
3961
5789
  import_node_child_process2 = require("node:child_process");
3962
5790
  import_node_os2 = require("node:os");
3963
5791
  init_graph();
3964
5792
  init_lockfile();
3965
5793
  init_config();
3966
- init_react_nextjs();
3967
- init_nextjs_routes();
3968
- init_prisma_schema();
5794
+ init_parser_registry();
5795
+ init_language_detection();
5796
+ init_audit_core();
3969
5797
  SERVER_INFO = {
3970
5798
  name: "launchsecure-graph",
3971
5799
  version: "0.0.1"
@@ -3973,14 +5801,13 @@ var init_graph_mcp = __esm({
3973
5801
  TOOLS = [
3974
5802
  {
3975
5803
  name: "generate_graph",
3976
- 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.",
3977
5805
  inputSchema: {
3978
5806
  type: "object",
3979
5807
  properties: {
3980
5808
  layer: {
3981
5809
  type: "string",
3982
- enum: ["ui", "api", "db"],
3983
- 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."
3984
5811
  }
3985
5812
  }
3986
5813
  }
@@ -3993,8 +5820,7 @@ var init_graph_mcp = __esm({
3993
5820
  properties: {
3994
5821
  layer: {
3995
5822
  type: "string",
3996
- enum: ["ui", "api", "db"],
3997
- 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."
3998
5824
  },
3999
5825
  search: {
4000
5826
  type: "string",
@@ -4046,7 +5872,7 @@ var init_graph_mcp = __esm({
4046
5872
  items: {
4047
5873
  type: "object",
4048
5874
  properties: {
4049
- layer: { type: "string", enum: ["ui", "api", "db"] },
5875
+ layer: { type: "string" },
4050
5876
  search: { type: "string" },
4051
5877
  type: { type: "string" },
4052
5878
  module: { type: "string" },
@@ -4088,8 +5914,7 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
4088
5914
  properties: {
4089
5915
  layer: {
4090
5916
  type: "string",
4091
- enum: ["ui", "api", "db"],
4092
- description: "Graph layer to scope files (required)."
5917
+ description: "Graph layer to scope files (required, e.g. 'ui', 'api', 'db')."
4093
5918
  },
4094
5919
  pattern: {
4095
5920
  type: "string",
@@ -4122,8 +5947,7 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
4122
5947
  properties: {
4123
5948
  layer: {
4124
5949
  type: "string",
4125
- enum: ["ui", "api", "db"],
4126
- description: "Graph layer (required)."
5950
+ description: "Graph layer (required, e.g. 'ui', 'api', 'db')."
4127
5951
  },
4128
5952
  node_id: {
4129
5953
  type: "string",
@@ -4183,7 +6007,7 @@ Use this when the user asks "is the chart running", "show me the project graph U
4183
6007
  },
4184
6008
  {
4185
6009
  name: "detect_project_stack",
4186
- 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.",
4187
6011
  inputSchema: {
4188
6012
  type: "object",
4189
6013
  properties: {}
@@ -4228,6 +6052,24 @@ Use this when the user asks "is the chart running", "show me the project graph U
4228
6052
  },
4229
6053
  required: ["node_id", "key"]
4230
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
+ }
4231
6073
  }
4232
6074
  ];
4233
6075
  COMPACT_SCHEMA = {
@@ -4284,6 +6126,9 @@ Use this when the user asks "is the chart running", "show me the project graph U
4284
6126
  api: 65,
4285
6127
  db: 65
4286
6128
  };
6129
+ DEFAULT_EST_NODE_FULL = 300;
6130
+ DEFAULT_EST_NODE_MIN = 150;
6131
+ DEFAULT_EST_EDGE = 65;
4287
6132
  NEIGHBORHOOD_BUDGET_CHARS = 55e3;
4288
6133
  BATCH_BUDGET_CHARS = 6e4;
4289
6134
  }
@@ -4291,10 +6136,10 @@ Use this when the user asks "is the chart running", "show me the project graph U
4291
6136
 
4292
6137
  // src/server/graph-mcp-entry.ts
4293
6138
  var import_node_child_process3 = require("node:child_process");
4294
- var import_node_fs15 = require("node:fs");
4295
- 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"));
4296
6141
  var import_node_os3 = require("node:os");
4297
- var import_node_fs16 = require("node:fs");
6142
+ var import_node_fs21 = require("node:fs");
4298
6143
  init_lockfile();
4299
6144
  function logStderr(msg) {
4300
6145
  process.stderr.write(`[launch-chart] ${msg}
@@ -4310,11 +6155,11 @@ function maybeAutoServe() {
4310
6155
  return;
4311
6156
  }
4312
6157
  try {
4313
- const logDir = import_node_path17.default.join((0, import_node_os3.homedir)(), ".launchsecure");
4314
- (0, import_node_fs16.mkdirSync)(logDir, { recursive: true });
4315
- const logPath = import_node_path17.default.join(logDir, "launch-chart.log");
4316
- const out = (0, import_node_fs15.openSync)(logPath, "a");
4317
- 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");
4318
6163
  const entryPath = process.argv[1];
4319
6164
  const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
4320
6165
  detached: true,