@launchsecure/launch-kit 0.0.13 → 0.0.15

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