@launchsecure/launch-kit 0.0.4 → 0.0.5

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.
@@ -105,6 +105,26 @@ var init_lockfile = __esm({
105
105
  }
106
106
  });
107
107
 
108
+ // src/server/graph/core/config.ts
109
+ function loadConfig(rootDir) {
110
+ const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
111
+ if (!(0, import_node_fs2.existsSync)(configPath)) return {};
112
+ try {
113
+ return JSON.parse((0, import_node_fs2.readFileSync)(configPath, "utf-8"));
114
+ } catch {
115
+ return {};
116
+ }
117
+ }
118
+ var import_node_fs2, import_node_path2, CONFIG_FILENAME;
119
+ var init_config = __esm({
120
+ "src/server/graph/core/config.ts"() {
121
+ "use strict";
122
+ import_node_fs2 = require("node:fs");
123
+ import_node_path2 = require("node:path");
124
+ CONFIG_FILENAME = ".launchchart.json";
125
+ }
126
+ });
127
+
108
128
  // src/server/graph/core/ast-helpers.ts
109
129
  function getTs() {
110
130
  if (!tsModule) {
@@ -114,8 +134,8 @@ function getTs() {
114
134
  }
115
135
  function parseFile(absPath) {
116
136
  const ts = getTs();
117
- const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
118
- const ext = (0, import_node_path2.extname)(absPath);
137
+ const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
138
+ const ext = (0, import_node_path3.extname)(absPath);
119
139
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
120
140
  const sourceFile = ts.createSourceFile(
121
141
  absPath,
@@ -382,8 +402,8 @@ function parseFile(absPath) {
382
402
  }
383
403
  function extractDbCalls(absPath) {
384
404
  const ts = getTs();
385
- const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
386
- const ext = (0, import_node_path2.extname)(absPath);
405
+ const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
406
+ const ext = (0, import_node_path3.extname)(absPath);
387
407
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
388
408
  const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
389
409
  const calls = [];
@@ -412,8 +432,8 @@ function extractDbCalls(absPath) {
412
432
  }
413
433
  function extractAuthWrappers(absPath) {
414
434
  const ts = getTs();
415
- const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
416
- const ext = (0, import_node_path2.extname)(absPath);
435
+ const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
436
+ const ext = (0, import_node_path3.extname)(absPath);
417
437
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
418
438
  const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
419
439
  const wrappers = /* @__PURE__ */ new Set();
@@ -429,12 +449,12 @@ function extractAuthWrappers(absPath) {
429
449
  visit(sourceFile);
430
450
  return wrappers;
431
451
  }
432
- var import_node_fs2, import_node_path2, tsModule, HTTP_METHODS, MUTATION_METHODS;
452
+ var import_node_fs3, import_node_path3, tsModule, HTTP_METHODS, MUTATION_METHODS;
433
453
  var init_ast_helpers = __esm({
434
454
  "src/server/graph/core/ast-helpers.ts"() {
435
455
  "use strict";
436
- import_node_fs2 = require("node:fs");
437
- import_node_path2 = require("node:path");
456
+ import_node_fs3 = require("node:fs");
457
+ import_node_path3 = require("node:path");
438
458
  HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
439
459
  MUTATION_METHODS = /* @__PURE__ */ new Set([
440
460
  "create",
@@ -453,12 +473,12 @@ var init_ast_helpers = __esm({
453
473
  // src/server/graph/parsers/ui/react-nextjs.ts
454
474
  function walk(dir, exts) {
455
475
  const results = [];
456
- if (!(0, import_node_fs3.existsSync)(dir)) return results;
457
- for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
458
- const full = (0, import_node_path3.join)(dir, entry.name);
476
+ if (!(0, import_node_fs4.existsSync)(dir)) return results;
477
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
478
+ const full = (0, import_node_path4.join)(dir, entry.name);
459
479
  if (entry.isDirectory()) {
460
480
  results.push(...walk(full, exts));
461
- } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
481
+ } else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
462
482
  results.push(full);
463
483
  }
464
484
  }
@@ -466,33 +486,33 @@ function walk(dir, exts) {
466
486
  }
467
487
  function walkWithIgnore(dir, exts, ignoreDirs) {
468
488
  const results = [];
469
- if (!(0, import_node_fs3.existsSync)(dir)) return results;
470
- for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
489
+ if (!(0, import_node_fs4.existsSync)(dir)) return results;
490
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
471
491
  if (entry.isDirectory()) {
472
492
  if (ignoreDirs.has(entry.name)) continue;
473
- results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, ignoreDirs));
474
- } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
475
- results.push((0, import_node_path3.join)(dir, entry.name));
493
+ results.push(...walkWithIgnore((0, import_node_path4.join)(dir, entry.name), exts, ignoreDirs));
494
+ } else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
495
+ results.push((0, import_node_path4.join)(dir, entry.name));
476
496
  }
477
497
  }
478
498
  return results;
479
499
  }
480
500
  function toNodeId(srcDir, absPath) {
481
- return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
501
+ return (0, import_node_path4.relative)(srcDir, absPath).replace(/\\/g, "/");
482
502
  }
483
503
  function resolveImport(srcDir, specifier) {
484
504
  if (!specifier.startsWith("@/")) return null;
485
505
  const rel = specifier.slice(2);
486
- const base = (0, import_node_path3.join)(srcDir, rel);
487
- 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")]) {
488
- if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
506
+ const base = (0, import_node_path4.join)(srcDir, rel);
507
+ 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")]) {
508
+ if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
489
509
  }
490
510
  return null;
491
511
  }
492
512
  function resolveRelativeImport(fromFile, specifier) {
493
- const base = (0, import_node_path3.join)((0, import_node_path3.dirname)(fromFile), specifier);
494
- 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")]) {
495
- if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
513
+ const base = (0, import_node_path4.join)((0, import_node_path4.dirname)(fromFile), specifier);
514
+ 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")]) {
515
+ if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
496
516
  }
497
517
  return null;
498
518
  }
@@ -513,7 +533,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
513
533
  const resolved = resolveRelativeImport(barrelAbsPath, re.from);
514
534
  if (!resolved) continue;
515
535
  if (re.isWildcard) {
516
- const targetBn = (0, import_node_path3.basename)(resolved);
536
+ const targetBn = (0, import_node_path4.basename)(resolved);
517
537
  const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
518
538
  if (targetIsBarrel) {
519
539
  const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
@@ -540,12 +560,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
540
560
  const barrels = /* @__PURE__ */ new Map();
541
561
  const memo = /* @__PURE__ */ new Map();
542
562
  for (const [absPath, parsed] of parsedByPath) {
543
- const bn = (0, import_node_path3.basename)(absPath);
563
+ const bn = (0, import_node_path4.basename)(absPath);
544
564
  if (bn !== "index.ts" && bn !== "index.tsx") continue;
545
565
  if (parsed.reExports.length === 0) continue;
546
566
  const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
547
567
  if (map.size > 0) {
548
- const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
568
+ const barrelId = (0, import_node_path4.relative)(srcDir, (0, import_node_path4.dirname)(absPath)).replace(/\\/g, "/");
549
569
  barrels.set(barrelId, map);
550
570
  }
551
571
  }
@@ -604,7 +624,7 @@ function extractRoute(id) {
604
624
  return route || "/";
605
625
  }
606
626
  function nameFromFilename(absPath) {
607
- return (0, import_node_path3.basename)(absPath, (0, import_node_path3.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
627
+ return (0, import_node_path4.basename)(absPath, (0, import_node_path4.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
608
628
  }
609
629
  function resolveTemplateLiteralRoute(template, routeToNodeId) {
610
630
  const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -685,105 +705,6 @@ function matchRouteToPage(route, routeToNodeId) {
685
705
  if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
686
706
  return null;
687
707
  }
688
- function loadApiRoutes(rootDir) {
689
- const apiJsonPath = (0, import_node_path3.join)(rootDir, ".launchsecure", "graphs", "api.json");
690
- if (!(0, import_node_fs3.existsSync)(apiJsonPath)) return [];
691
- try {
692
- const parsed = JSON.parse((0, import_node_fs3.readFileSync)(apiJsonPath, "utf-8"));
693
- const routes = [];
694
- for (const n of parsed.nodes ?? []) {
695
- const path3 = n.path;
696
- if (!path3 || typeof path3 !== "string") continue;
697
- routes.push({
698
- path: path3,
699
- nodeId: n.id,
700
- segments: path3.split("/").filter(Boolean)
701
- });
702
- }
703
- return routes;
704
- } catch {
705
- return [];
706
- }
707
- }
708
- function buildApiPathMap(routes) {
709
- const map = /* @__PURE__ */ new Map();
710
- for (const r of routes) {
711
- if (!map.has(r.path)) map.set(r.path, r.nodeId);
712
- }
713
- return map;
714
- }
715
- function normalizeFetchUrl(raw) {
716
- let s = raw.replace(/^`|`$/g, "");
717
- const qIdx = s.indexOf("?");
718
- if (qIdx >= 0) s = s.slice(0, qIdx);
719
- const hIdx = s.indexOf("#");
720
- if (hIdx >= 0) s = s.slice(0, hIdx);
721
- let hadInterpolation = false;
722
- s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
723
- hadInterpolation = true;
724
- const cleaned = expr.trim();
725
- const last = cleaned.split(".").pop() ?? cleaned;
726
- const name = last.replace(/[^\w]/g, "") || "param";
727
- return ":" + name;
728
- });
729
- s = s.replace(/\/+/g, "/");
730
- if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
731
- return { path: s || "/", hadInterpolation };
732
- }
733
- function scoreApiRouteMatch(candidate, known) {
734
- if (candidate.length !== known.length) return -1;
735
- let score = 0;
736
- for (let i = 0; i < candidate.length; i++) {
737
- const a = candidate[i];
738
- const b = known[i];
739
- if (a === b) {
740
- score += 3;
741
- continue;
742
- }
743
- if (a.startsWith(":") && b.startsWith(":")) {
744
- score += 2;
745
- continue;
746
- }
747
- if (a.startsWith(":") || b.startsWith(":")) {
748
- score += 1;
749
- continue;
750
- }
751
- return -1;
752
- }
753
- return score;
754
- }
755
- function resolveFetchCall(call, apiPathMap, apiRoutes) {
756
- const raw = call.url;
757
- if (/^(https?:)?\/\//i.test(raw)) {
758
- return { kind: "external", normalizedUrl: raw };
759
- }
760
- if (call.isConcat) {
761
- return { kind: "dynamic", normalizedUrl: raw };
762
- }
763
- const { path: path3, hadInterpolation } = normalizeFetchUrl(raw);
764
- if (!path3.startsWith("/")) {
765
- return { kind: "unresolved", normalizedUrl: path3 };
766
- }
767
- const segs = path3.split("/").filter(Boolean);
768
- if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
769
- return { kind: "dynamic", normalizedUrl: path3 };
770
- }
771
- const exact = apiPathMap.get(path3);
772
- if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
773
- let bestScore = -1;
774
- let bestId = null;
775
- for (const r of apiRoutes) {
776
- const score = scoreApiRouteMatch(segs, r.segments);
777
- if (score > bestScore) {
778
- bestScore = score;
779
- bestId = r.nodeId;
780
- }
781
- }
782
- if (bestId && bestScore > 0) {
783
- return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
784
- }
785
- return { kind: "unresolved", normalizedUrl: path3 };
786
- }
787
708
  function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
788
709
  const edges = [];
789
710
  const flagged = [];
@@ -883,26 +804,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
883
804
  return { edges, flagged };
884
805
  }
885
806
  function detect(rootDir) {
886
- 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"));
807
+ return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app")) && (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.ts")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.js")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.mjs"));
887
808
  }
888
809
  function generate(rootDir) {
889
- const srcDir = (0, import_node_path3.join)(rootDir, "src");
890
- const appFiles = walk((0, import_node_path3.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
891
- (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
810
+ const srcDir = (0, import_node_path4.join)(rootDir, "src");
811
+ const appFiles = walk((0, import_node_path4.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
812
+ (f) => (0, import_node_path4.basename)(f) !== "route.ts" && (0, import_node_path4.basename)(f) !== "route.tsx"
892
813
  );
893
- const clientFiles = walk((0, import_node_path3.join)(srcDir, "client"), [".tsx", ".ts"]);
894
- const serverFiles = walk((0, import_node_path3.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
895
- (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
814
+ const clientFiles = walk((0, import_node_path4.join)(srcDir, "client"), [".tsx", ".ts"]);
815
+ const serverFiles = walk((0, import_node_path4.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
816
+ (f) => (0, import_node_path4.basename)(f) !== "route.ts" && (0, import_node_path4.basename)(f) !== "route.tsx"
896
817
  );
897
- const libFiles = walk((0, import_node_path3.join)(srcDir, "lib"), [".ts", ".tsx"]);
898
- const configFiles = walk((0, import_node_path3.join)(srcDir, "config"), [".ts", ".tsx"]);
818
+ const libFiles = walk((0, import_node_path4.join)(srcDir, "lib"), [".ts", ".tsx"]);
819
+ const configFiles = walk((0, import_node_path4.join)(srcDir, "config"), [".ts", ".tsx"]);
899
820
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
900
821
  const parsedByPath = /* @__PURE__ */ new Map();
901
822
  for (const absPath of allDiscovered) {
902
823
  parsedByPath.set(absPath, parseFile(absPath));
903
824
  }
904
825
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
905
- const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
826
+ const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
906
827
  const nodes = [];
907
828
  const nodeIdSet = /* @__PURE__ */ new Set();
908
829
  const nodeTypeMap = /* @__PURE__ */ new Map();
@@ -921,7 +842,6 @@ function generate(rootDir) {
921
842
  }
922
843
  const allEdges = [];
923
844
  const allFlagged = [];
924
- const crossRefs = [];
925
845
  for (const absPath of fileSet) {
926
846
  const sourceId = toNodeId(srcDir, absPath);
927
847
  const parsed = parsedByPath.get(absPath);
@@ -938,66 +858,21 @@ function generate(rootDir) {
938
858
  allEdges.push(...edges);
939
859
  allFlagged.push(...flagged);
940
860
  }
941
- const apiRoutes = loadApiRoutes(rootDir);
942
- const apiPathMap = buildApiPathMap(apiRoutes);
943
- const includeExternalFetches = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
944
- const fetchSeen = /* @__PURE__ */ new Set();
945
- let fetchResolvedCount = 0;
946
- let fetchDynamicCount = 0;
947
- let fetchUnresolvedCount = 0;
948
- let fetchExternalCount = 0;
861
+ const fetchCallEntries = [];
949
862
  for (const absPath of fileSet) {
950
863
  const sourceId = toNodeId(srcDir, absPath);
951
864
  const parsed = parsedByPath.get(absPath);
952
865
  if (parsed.fetchCalls.length === 0) continue;
953
- for (const call of parsed.fetchCalls) {
954
- const result = resolveFetchCall(call, apiPathMap, apiRoutes);
955
- const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
956
- if (result.kind === "resolved" && result.nodeId) {
957
- const key = `${sourceId}\u2192${result.nodeId}\u2192calls_api`;
958
- if (fetchSeen.has(key)) continue;
959
- fetchSeen.add(key);
960
- crossRefs.push({
961
- source: sourceId,
962
- target: result.nodeId,
963
- type: "calls_api",
964
- layer: "api"
965
- });
966
- fetchResolvedCount++;
967
- continue;
968
- }
969
- if (result.kind === "dynamic") {
970
- fetchDynamicCount++;
971
- allFlagged.push({
972
- source: sourceId,
973
- target: "DYNAMIC",
974
- type: "calls_api",
975
- label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
976
- confidence: call.isConcat ? "low" : "medium"
977
- });
978
- continue;
979
- }
980
- if (result.kind === "external") {
981
- fetchExternalCount++;
982
- if (!includeExternalFetches) continue;
983
- allFlagged.push({
984
- source: sourceId,
985
- target: "EXTERNAL",
986
- type: "calls_external",
987
- label: `${methodTag} external fetch: ${call.url}`,
988
- confidence: "high"
989
- });
990
- continue;
991
- }
992
- fetchUnresolvedCount++;
993
- allFlagged.push({
994
- source: sourceId,
995
- target: "UNRESOLVED",
996
- type: "calls_api",
997
- label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
998
- confidence: "medium"
999
- });
1000
- }
866
+ fetchCallEntries.push({
867
+ nodeId: sourceId,
868
+ calls: parsed.fetchCalls.map((c) => ({
869
+ url: c.url,
870
+ method: c.method,
871
+ isTemplate: c.isTemplate,
872
+ isConcat: c.isConcat,
873
+ kind: c.kind
874
+ }))
875
+ });
1001
876
  }
1002
877
  const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
1003
878
  const IGNORE_DIRS = /* @__PURE__ */ new Set([
@@ -1023,7 +898,7 @@ function generate(rootDir) {
1023
898
  } catch {
1024
899
  continue;
1025
900
  }
1026
- const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
901
+ const externalId = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
1027
902
  const edgesFromThis = [];
1028
903
  const seen = /* @__PURE__ */ new Set();
1029
904
  for (const imp of parsed.imports) {
@@ -1114,20 +989,11 @@ function generate(rootDir) {
1114
989
  layer: "ui",
1115
990
  parser: "react-nextjs-ast",
1116
991
  ...stats,
1117
- api_call_detection: {
1118
- includeExternalFetches,
1119
- includeConcatFetches: process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1",
1120
- apiRoutesLoaded: apiRoutes.length,
1121
- resolved: fetchResolvedCount,
1122
- dynamic: fetchDynamicCount,
1123
- unresolved: fetchUnresolvedCount,
1124
- external: fetchExternalCount
1125
- },
1126
992
  notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
1127
993
  },
1128
994
  nodes,
1129
995
  edges: allEdges,
1130
- cross_refs: crossRefs,
996
+ cross_refs: [],
1131
997
  contradictions: [],
1132
998
  warnings: [],
1133
999
  flagged_edges: dedupedFlagged,
@@ -1138,16 +1004,17 @@ function generate(rootDir) {
1138
1004
  renders: allEdges.filter((e) => e.type === "renders").length,
1139
1005
  imports: allEdges.filter((e) => e.type === "imports").length,
1140
1006
  navigates: allEdges.filter((e) => e.type === "navigates").length
1141
- }
1007
+ },
1008
+ fetch_calls: fetchCallEntries
1142
1009
  }
1143
1010
  };
1144
1011
  }
1145
- var import_node_fs3, import_node_path3, RENDER_TYPES, reactNextjsParser;
1012
+ var import_node_fs4, import_node_path4, RENDER_TYPES, reactNextjsParser;
1146
1013
  var init_react_nextjs = __esm({
1147
1014
  "src/server/graph/parsers/ui/react-nextjs.ts"() {
1148
1015
  "use strict";
1149
- import_node_fs3 = require("node:fs");
1150
- import_node_path3 = require("node:path");
1016
+ import_node_fs4 = require("node:fs");
1017
+ import_node_path4 = require("node:path");
1151
1018
  init_ast_helpers();
1152
1019
  RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
1153
1020
  reactNextjsParser = {
@@ -1162,9 +1029,9 @@ var init_react_nextjs = __esm({
1162
1029
  // src/server/graph/parsers/api/nextjs-routes.ts
1163
1030
  function walk2(dir) {
1164
1031
  const results = [];
1165
- if (!(0, import_node_fs4.existsSync)(dir)) return results;
1166
- for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
1167
- const full = (0, import_node_path4.join)(dir, entry.name);
1032
+ if (!(0, import_node_fs5.existsSync)(dir)) return results;
1033
+ for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
1034
+ const full = (0, import_node_path5.join)(dir, entry.name);
1168
1035
  if (entry.isDirectory()) {
1169
1036
  results.push(...walk2(full));
1170
1037
  } else if (entry.name === "route.ts" || entry.name === "route.tsx") {
@@ -1174,7 +1041,7 @@ function walk2(dir) {
1174
1041
  return results;
1175
1042
  }
1176
1043
  function filePathToRoute(apiDir, absPath) {
1177
- let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
1044
+ let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
1178
1045
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
1179
1046
  route = route.replace(/\/+/g, "/");
1180
1047
  if (route === "/") return "/api";
@@ -1185,10 +1052,10 @@ function camelToPascal(s) {
1185
1052
  return s.charAt(0).toUpperCase() + s.slice(1);
1186
1053
  }
1187
1054
  function detect2(rootDir) {
1188
- return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
1055
+ return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "src", "app", "api"));
1189
1056
  }
1190
1057
  function generate2(rootDir) {
1191
- const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
1058
+ const apiDir = (0, import_node_path5.join)(rootDir, "src", "app", "api");
1192
1059
  const routeFiles = walk2(apiDir);
1193
1060
  const nodes = [];
1194
1061
  const edges = [];
@@ -1206,7 +1073,7 @@ function generate2(rootDir) {
1206
1073
  if (HTTP_METHODS2.has(exp)) methods.push(exp);
1207
1074
  }
1208
1075
  const routePath = filePathToRoute(apiDir, absPath);
1209
- const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
1076
+ const relPath = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
1210
1077
  const mutations = dbCalls.filter((c) => c.isMutation);
1211
1078
  const reads = dbCalls.filter((c) => !c.isMutation);
1212
1079
  const mutates = mutations.length > 0;
@@ -1283,12 +1150,12 @@ function generate2(rootDir) {
1283
1150
  }
1284
1151
  };
1285
1152
  }
1286
- var import_node_fs4, import_node_path4, HTTP_METHODS2, nextjsRoutesParser;
1153
+ var import_node_fs5, import_node_path5, HTTP_METHODS2, nextjsRoutesParser;
1287
1154
  var init_nextjs_routes = __esm({
1288
1155
  "src/server/graph/parsers/api/nextjs-routes.ts"() {
1289
1156
  "use strict";
1290
- import_node_fs4 = require("node:fs");
1291
- import_node_path4 = require("node:path");
1157
+ import_node_fs5 = require("node:fs");
1158
+ import_node_path5 = require("node:path");
1292
1159
  init_ast_helpers();
1293
1160
  HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
1294
1161
  nextjsRoutesParser = {
@@ -1391,11 +1258,11 @@ function parseEnums(content) {
1391
1258
  return nodes;
1392
1259
  }
1393
1260
  function detect3(rootDir) {
1394
- return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
1261
+ return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "prisma", "schema.prisma"));
1395
1262
  }
1396
1263
  function generate3(rootDir) {
1397
- const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
1398
- const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
1264
+ const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
1265
+ const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
1399
1266
  const { nodes: modelNodes, relations } = parseModels(content);
1400
1267
  const enumNodes = parseEnums(content);
1401
1268
  const allNodes = [...modelNodes, ...enumNodes];
@@ -1444,12 +1311,12 @@ function generate3(rootDir) {
1444
1311
  }
1445
1312
  };
1446
1313
  }
1447
- var import_node_fs5, import_node_path5, prismaSchemaParser;
1314
+ var import_node_fs6, import_node_path6, prismaSchemaParser;
1448
1315
  var init_prisma_schema = __esm({
1449
1316
  "src/server/graph/parsers/db/prisma-schema.ts"() {
1450
1317
  "use strict";
1451
- import_node_fs5 = require("node:fs");
1452
- import_node_path5 = require("node:path");
1318
+ import_node_fs6 = require("node:fs");
1319
+ import_node_path6 = require("node:path");
1453
1320
  prismaSchemaParser = {
1454
1321
  id: "prisma-schema",
1455
1322
  layer: "db",
@@ -1459,66 +1326,730 @@ var init_prisma_schema = __esm({
1459
1326
  }
1460
1327
  });
1461
1328
 
1329
+ // src/server/graph/core/api-route-matching.ts
1330
+ function loadApiRoutesFromOutput(apiOutput) {
1331
+ const routes = [];
1332
+ for (const n of apiOutput.nodes) {
1333
+ const path3 = n.path;
1334
+ if (!path3 || typeof path3 !== "string") continue;
1335
+ routes.push({
1336
+ path: path3,
1337
+ nodeId: n.id,
1338
+ segments: path3.split("/").filter(Boolean)
1339
+ });
1340
+ }
1341
+ return routes;
1342
+ }
1343
+ function buildApiPathMap(routes) {
1344
+ const map = /* @__PURE__ */ new Map();
1345
+ for (const r of routes) {
1346
+ if (!map.has(r.path)) map.set(r.path, r.nodeId);
1347
+ }
1348
+ return map;
1349
+ }
1350
+ function normalizeFetchUrl(raw) {
1351
+ let s = raw.replace(/^`|`$/g, "");
1352
+ const qIdx = s.indexOf("?");
1353
+ if (qIdx >= 0) s = s.slice(0, qIdx);
1354
+ const hIdx = s.indexOf("#");
1355
+ if (hIdx >= 0) s = s.slice(0, hIdx);
1356
+ let hadInterpolation = false;
1357
+ s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
1358
+ hadInterpolation = true;
1359
+ const cleaned = expr.trim();
1360
+ const last = cleaned.split(".").pop() ?? cleaned;
1361
+ const name = last.replace(/[^\w]/g, "") || "param";
1362
+ return ":" + name;
1363
+ });
1364
+ s = s.replace(/\/+/g, "/");
1365
+ if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
1366
+ return { path: s || "/", hadInterpolation };
1367
+ }
1368
+ function scoreApiRouteMatch(candidate, known) {
1369
+ if (candidate.length !== known.length) return -1;
1370
+ let score = 0;
1371
+ for (let i = 0; i < candidate.length; i++) {
1372
+ const a = candidate[i];
1373
+ const b = known[i];
1374
+ if (a === b) {
1375
+ score += 3;
1376
+ continue;
1377
+ }
1378
+ if (a.startsWith(":") && b.startsWith(":")) {
1379
+ score += 2;
1380
+ continue;
1381
+ }
1382
+ if (a.startsWith(":") || b.startsWith(":")) {
1383
+ score += 1;
1384
+ continue;
1385
+ }
1386
+ return -1;
1387
+ }
1388
+ return score;
1389
+ }
1390
+ function resolveFetchCall(call, apiPathMap, apiRoutes) {
1391
+ const raw = call.url;
1392
+ if (/^(https?:)?\/\//i.test(raw)) {
1393
+ return { kind: "external", normalizedUrl: raw };
1394
+ }
1395
+ if (call.isConcat) {
1396
+ return { kind: "dynamic", normalizedUrl: raw };
1397
+ }
1398
+ const { path: path3, hadInterpolation } = normalizeFetchUrl(raw);
1399
+ if (!path3.startsWith("/")) {
1400
+ return { kind: "unresolved", normalizedUrl: path3 };
1401
+ }
1402
+ const segs = path3.split("/").filter(Boolean);
1403
+ if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
1404
+ return { kind: "dynamic", normalizedUrl: path3 };
1405
+ }
1406
+ const exact = apiPathMap.get(path3);
1407
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
1408
+ let bestScore = -1;
1409
+ let bestId = null;
1410
+ for (const r of apiRoutes) {
1411
+ const score = scoreApiRouteMatch(segs, r.segments);
1412
+ if (score > bestScore) {
1413
+ bestScore = score;
1414
+ bestId = r.nodeId;
1415
+ }
1416
+ }
1417
+ if (bestId && bestScore > 0) {
1418
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
1419
+ }
1420
+ return { kind: "unresolved", normalizedUrl: path3 };
1421
+ }
1422
+ function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
1423
+ const { path: path3, hadInterpolation } = normalizeFetchUrl(urlPath);
1424
+ if (!path3.startsWith("/")) {
1425
+ return { kind: "unresolved", normalizedUrl: path3 };
1426
+ }
1427
+ const segs = path3.split("/").filter(Boolean);
1428
+ if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
1429
+ return { kind: "dynamic", normalizedUrl: path3 };
1430
+ }
1431
+ const exact = apiPathMap.get(path3);
1432
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
1433
+ let bestScore = -1;
1434
+ let bestId = null;
1435
+ for (const r of apiRoutes) {
1436
+ const score = scoreApiRouteMatch(segs, r.segments);
1437
+ if (score > bestScore) {
1438
+ bestScore = score;
1439
+ bestId = r.nodeId;
1440
+ }
1441
+ }
1442
+ if (bestId && bestScore > 0) {
1443
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
1444
+ }
1445
+ return { kind: "unresolved", normalizedUrl: path3 };
1446
+ }
1447
+ var init_api_route_matching = __esm({
1448
+ "src/server/graph/core/api-route-matching.ts"() {
1449
+ "use strict";
1450
+ }
1451
+ });
1452
+
1453
+ // src/server/graph/parsers/crosslayer/fetch-resolver.ts
1454
+ var fetchResolverParser;
1455
+ var init_fetch_resolver = __esm({
1456
+ "src/server/graph/parsers/crosslayer/fetch-resolver.ts"() {
1457
+ "use strict";
1458
+ init_api_route_matching();
1459
+ fetchResolverParser = {
1460
+ id: "fetch-resolver",
1461
+ layer: "crosslayer",
1462
+ detect(_rootDir) {
1463
+ return true;
1464
+ },
1465
+ generate(_rootDir, layerOutputs) {
1466
+ const uiOutput = layerOutputs.get("ui");
1467
+ const apiOutput = layerOutputs.get("api");
1468
+ if (!uiOutput || !apiOutput) {
1469
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1470
+ }
1471
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1472
+ const apiPathMap = buildApiPathMap(apiRoutes);
1473
+ const fetchCallEntries = uiOutput.patterns?.fetch_calls ?? [];
1474
+ if (fetchCallEntries.length === 0) {
1475
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1476
+ }
1477
+ const includeExternal = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
1478
+ const crossRefs = [];
1479
+ const flaggedEdges = [];
1480
+ const seen = /* @__PURE__ */ new Set();
1481
+ let resolvedCount = 0;
1482
+ let dynamicCount = 0;
1483
+ let unresolvedCount = 0;
1484
+ let externalCount = 0;
1485
+ for (const entry of fetchCallEntries) {
1486
+ for (const call of entry.calls) {
1487
+ const result = resolveFetchCall(call, apiPathMap, apiRoutes);
1488
+ const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
1489
+ if (result.kind === "resolved" && result.nodeId) {
1490
+ const key = `${entry.nodeId}\u2192${result.nodeId}\u2192calls_api`;
1491
+ if (seen.has(key)) continue;
1492
+ seen.add(key);
1493
+ crossRefs.push({
1494
+ source: entry.nodeId,
1495
+ target: result.nodeId,
1496
+ type: "calls_api",
1497
+ layer: "api"
1498
+ });
1499
+ resolvedCount++;
1500
+ continue;
1501
+ }
1502
+ if (result.kind === "dynamic") {
1503
+ dynamicCount++;
1504
+ flaggedEdges.push({
1505
+ source: entry.nodeId,
1506
+ target: "DYNAMIC",
1507
+ type: "calls_api",
1508
+ label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
1509
+ confidence: call.isConcat ? "low" : "medium"
1510
+ });
1511
+ continue;
1512
+ }
1513
+ if (result.kind === "external") {
1514
+ externalCount++;
1515
+ if (!includeExternal) continue;
1516
+ flaggedEdges.push({
1517
+ source: entry.nodeId,
1518
+ target: "EXTERNAL",
1519
+ type: "calls_external",
1520
+ label: `${methodTag} external fetch: ${call.url}`,
1521
+ confidence: "high"
1522
+ });
1523
+ continue;
1524
+ }
1525
+ unresolvedCount++;
1526
+ flaggedEdges.push({
1527
+ source: entry.nodeId,
1528
+ target: "UNRESOLVED",
1529
+ type: "calls_api",
1530
+ label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
1531
+ confidence: "medium"
1532
+ });
1533
+ }
1534
+ }
1535
+ return {
1536
+ cross_refs: crossRefs,
1537
+ flagged_edges: flaggedEdges,
1538
+ warnings: [],
1539
+ patterns: {
1540
+ api_call_detection: {
1541
+ resolved: resolvedCount,
1542
+ dynamic: dynamicCount,
1543
+ unresolved: unresolvedCount,
1544
+ external: externalCount
1545
+ }
1546
+ }
1547
+ };
1548
+ }
1549
+ };
1550
+ }
1551
+ });
1552
+
1553
+ // src/server/graph/parsers/crosslayer/api-annotations.ts
1554
+ function walk3(dir, exts) {
1555
+ if (!(0, import_node_fs7.existsSync)(dir)) return [];
1556
+ const results = [];
1557
+ for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
1558
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1559
+ const full = (0, import_node_path7.join)(dir, entry.name);
1560
+ if (entry.isDirectory()) {
1561
+ results.push(...walk3(full, exts));
1562
+ } else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
1563
+ results.push(full);
1564
+ }
1565
+ }
1566
+ return results;
1567
+ }
1568
+ function toNodeId2(srcDir, absPath) {
1569
+ return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
1570
+ }
1571
+ var import_node_fs7, import_node_path7, API_ANNOTATION_RE, apiAnnotationsParser;
1572
+ var init_api_annotations = __esm({
1573
+ "src/server/graph/parsers/crosslayer/api-annotations.ts"() {
1574
+ "use strict";
1575
+ import_node_fs7 = require("node:fs");
1576
+ import_node_path7 = require("node:path");
1577
+ init_api_route_matching();
1578
+ API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
1579
+ apiAnnotationsParser = {
1580
+ id: "api-annotations",
1581
+ layer: "crosslayer",
1582
+ detect(rootDir) {
1583
+ return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
1584
+ },
1585
+ generate(rootDir, layerOutputs) {
1586
+ const apiOutput = layerOutputs.get("api");
1587
+ if (!apiOutput) {
1588
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1589
+ }
1590
+ const uiOutput = layerOutputs.get("ui");
1591
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1592
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1593
+ const apiPathMap = buildApiPathMap(apiRoutes);
1594
+ const srcDir = (0, import_node_path7.join)(rootDir, "src");
1595
+ const files = walk3(srcDir, [".ts", ".tsx"]);
1596
+ const crossRefs = [];
1597
+ const flaggedEdges = [];
1598
+ const seen = /* @__PURE__ */ new Set();
1599
+ for (const absPath of files) {
1600
+ const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
1601
+ const sourceId = toNodeId2(srcDir, absPath);
1602
+ if (!uiNodeIds.has(sourceId)) continue;
1603
+ let match;
1604
+ API_ANNOTATION_RE.lastIndex = 0;
1605
+ while ((match = API_ANNOTATION_RE.exec(content)) !== null) {
1606
+ const method = match[1];
1607
+ const urlPath = match[2];
1608
+ const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
1609
+ if (result.kind === "resolved" && result.nodeId) {
1610
+ const key = `${sourceId}|${result.nodeId}|calls_api`;
1611
+ if (seen.has(key)) continue;
1612
+ seen.add(key);
1613
+ crossRefs.push({
1614
+ source: sourceId,
1615
+ target: result.nodeId,
1616
+ type: "calls_api",
1617
+ layer: "api"
1618
+ });
1619
+ } else {
1620
+ flaggedEdges.push({
1621
+ source: sourceId,
1622
+ target: "UNRESOLVED",
1623
+ type: "annotation_unresolved",
1624
+ label: `@api ${method} ${urlPath} \u2014 no matching API route found`,
1625
+ confidence: "high"
1626
+ });
1627
+ }
1628
+ }
1629
+ }
1630
+ return {
1631
+ cross_refs: crossRefs,
1632
+ flagged_edges: flaggedEdges,
1633
+ warnings: [],
1634
+ patterns: {
1635
+ annotations_found: crossRefs.length + flaggedEdges.length,
1636
+ annotations_resolved: crossRefs.length,
1637
+ annotations_unresolved: flaggedEdges.length
1638
+ }
1639
+ };
1640
+ }
1641
+ };
1642
+ }
1643
+ });
1644
+
1645
+ // src/server/graph/parsers/crosslayer/url-literal-scanner.ts
1646
+ function walk4(dir, exts) {
1647
+ if (!(0, import_node_fs8.existsSync)(dir)) return [];
1648
+ const results = [];
1649
+ for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
1650
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1651
+ const full = (0, import_node_path8.join)(dir, entry.name);
1652
+ if (entry.isDirectory()) {
1653
+ results.push(...walk4(full, exts));
1654
+ } else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
1655
+ results.push(full);
1656
+ }
1657
+ }
1658
+ return results;
1659
+ }
1660
+ function toNodeId3(srcDir, absPath) {
1661
+ return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
1662
+ }
1663
+ var import_node_fs8, import_node_path8, URL_LITERAL_RE, urlLiteralScannerParser;
1664
+ var init_url_literal_scanner = __esm({
1665
+ "src/server/graph/parsers/crosslayer/url-literal-scanner.ts"() {
1666
+ "use strict";
1667
+ import_node_fs8 = require("node:fs");
1668
+ import_node_path8 = require("node:path");
1669
+ init_api_route_matching();
1670
+ URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
1671
+ urlLiteralScannerParser = {
1672
+ id: "url-literal-scanner",
1673
+ layer: "crosslayer",
1674
+ detect(rootDir) {
1675
+ return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
1676
+ },
1677
+ generate(rootDir, layerOutputs) {
1678
+ const apiOutput = layerOutputs.get("api");
1679
+ if (!apiOutput) {
1680
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1681
+ }
1682
+ const uiOutput = layerOutputs.get("ui");
1683
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1684
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1685
+ const apiPathMap = buildApiPathMap(apiRoutes);
1686
+ const srcDir = (0, import_node_path8.join)(rootDir, "src");
1687
+ const clientDir = (0, import_node_path8.join)(srcDir, "client");
1688
+ const appDir = (0, import_node_path8.join)(srcDir, "app");
1689
+ const files = [
1690
+ ...walk4(clientDir, [".ts", ".tsx"]),
1691
+ ...walk4(appDir, [".ts", ".tsx"])
1692
+ ];
1693
+ const crossRefs = [];
1694
+ const seen = /* @__PURE__ */ new Set();
1695
+ for (const absPath of files) {
1696
+ const sourceId = toNodeId3(srcDir, absPath);
1697
+ if (!uiNodeIds.has(sourceId)) continue;
1698
+ const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
1699
+ let match;
1700
+ URL_LITERAL_RE.lastIndex = 0;
1701
+ while ((match = URL_LITERAL_RE.exec(content)) !== null) {
1702
+ const urlPath = match[1];
1703
+ const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
1704
+ if (result.kind === "resolved" && result.nodeId) {
1705
+ const key = `${sourceId}|${result.nodeId}|references_api`;
1706
+ if (seen.has(key)) continue;
1707
+ seen.add(key);
1708
+ crossRefs.push({
1709
+ source: sourceId,
1710
+ target: result.nodeId,
1711
+ type: "references_api",
1712
+ layer: "api"
1713
+ });
1714
+ }
1715
+ }
1716
+ }
1717
+ return {
1718
+ cross_refs: crossRefs,
1719
+ flagged_edges: [],
1720
+ warnings: [],
1721
+ patterns: {
1722
+ url_literals_resolved: crossRefs.length
1723
+ }
1724
+ };
1725
+ }
1726
+ };
1727
+ }
1728
+ });
1729
+
1730
+ // src/server/graph/core/parser-registry.ts
1731
+ function registerBuiltins(registry, disabled) {
1732
+ const builtins = [
1733
+ reactNextjsParser,
1734
+ nextjsRoutesParser,
1735
+ prismaSchemaParser,
1736
+ fetchResolverParser,
1737
+ apiAnnotationsParser,
1738
+ urlLiteralScannerParser
1739
+ ];
1740
+ for (const parser of builtins) {
1741
+ if (disabled.has(parser.id)) continue;
1742
+ registry.register(parser);
1743
+ }
1744
+ }
1745
+ function loadCustomParsers(registry, config, rootDir, disabled) {
1746
+ for (const entry of config.parsers?.custom ?? []) {
1747
+ try {
1748
+ const absPath = (0, import_node_path9.resolve)(rootDir, entry.path);
1749
+ const mod = require(absPath);
1750
+ const parser = "default" in mod ? mod.default : mod;
1751
+ if (disabled.has(parser.id)) continue;
1752
+ if (parser.layer !== entry.layer) {
1753
+ process.stderr.write(
1754
+ `[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
1755
+ `
1756
+ );
1757
+ }
1758
+ registry.register(parser);
1759
+ } catch (err2) {
1760
+ process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
1761
+ `);
1762
+ }
1763
+ }
1764
+ }
1765
+ function createRegistry(config, rootDir) {
1766
+ const registry = new ParserRegistry();
1767
+ const disabled = new Set(config.parsers?.disabled ?? []);
1768
+ registerBuiltins(registry, disabled);
1769
+ loadCustomParsers(registry, config, rootDir, disabled);
1770
+ return registry;
1771
+ }
1772
+ var import_node_path9, ParserRegistry;
1773
+ var init_parser_registry = __esm({
1774
+ "src/server/graph/core/parser-registry.ts"() {
1775
+ "use strict";
1776
+ import_node_path9 = require("node:path");
1777
+ init_react_nextjs();
1778
+ init_nextjs_routes();
1779
+ init_prisma_schema();
1780
+ init_fetch_resolver();
1781
+ init_api_annotations();
1782
+ init_url_literal_scanner();
1783
+ ParserRegistry = class {
1784
+ constructor() {
1785
+ this.parsers = /* @__PURE__ */ new Map();
1786
+ this.ids = /* @__PURE__ */ new Set();
1787
+ }
1788
+ register(parser) {
1789
+ if (this.ids.has(parser.id)) {
1790
+ throw new Error(`Duplicate parser id: ${parser.id}`);
1791
+ }
1792
+ this.ids.add(parser.id);
1793
+ const list = this.parsers.get(parser.layer) ?? [];
1794
+ list.push(parser);
1795
+ this.parsers.set(parser.layer, list);
1796
+ }
1797
+ getParsers(layer) {
1798
+ return this.parsers.get(layer) ?? [];
1799
+ }
1800
+ getCrossLayerParsers() {
1801
+ return this.parsers.get("crosslayer") ?? [];
1802
+ }
1803
+ getAll() {
1804
+ const all = [];
1805
+ for (const list of this.parsers.values()) all.push(...list);
1806
+ return all;
1807
+ }
1808
+ };
1809
+ }
1810
+ });
1811
+
1812
+ // src/server/graph/core/merge.ts
1813
+ function mergeGraphOutputs(outputs, layer) {
1814
+ if (outputs.length === 0) {
1815
+ return {
1816
+ metadata: { generated: (/* @__PURE__ */ new Date()).toISOString(), scope: "", layer },
1817
+ nodes: [],
1818
+ edges: [],
1819
+ cross_refs: [],
1820
+ contradictions: [],
1821
+ warnings: [],
1822
+ flagged_edges: []
1823
+ };
1824
+ }
1825
+ if (outputs.length === 1) return outputs[0];
1826
+ const seenNodes = /* @__PURE__ */ new Set();
1827
+ const seenEdges = /* @__PURE__ */ new Set();
1828
+ const seenCrossRefs = /* @__PURE__ */ new Set();
1829
+ const mergedNodes = [];
1830
+ const mergedEdges = [];
1831
+ const mergedCrossRefs = [];
1832
+ const mergedContradictions = [];
1833
+ const mergedWarnings = [];
1834
+ const mergedFlagged = [];
1835
+ const parserIds = [];
1836
+ for (const output of outputs) {
1837
+ if (output.metadata.parser) {
1838
+ parserIds.push(String(output.metadata.parser));
1839
+ }
1840
+ for (const node of output.nodes) {
1841
+ if (seenNodes.has(node.id)) {
1842
+ mergedWarnings.push({
1843
+ type: "merge_conflict",
1844
+ detail: `Node "${node.id}" produced by multiple parsers; keeping first`
1845
+ });
1846
+ continue;
1847
+ }
1848
+ seenNodes.add(node.id);
1849
+ mergedNodes.push(node);
1850
+ }
1851
+ for (const edge of output.edges) {
1852
+ const key = `${edge.source}|${edge.target}|${edge.type}`;
1853
+ if (seenEdges.has(key)) continue;
1854
+ seenEdges.add(key);
1855
+ mergedEdges.push(edge);
1856
+ }
1857
+ for (const ref of output.cross_refs) {
1858
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
1859
+ if (seenCrossRefs.has(key)) continue;
1860
+ seenCrossRefs.add(key);
1861
+ mergedCrossRefs.push(ref);
1862
+ }
1863
+ mergedContradictions.push(...output.contradictions);
1864
+ mergedWarnings.push(...output.warnings);
1865
+ mergedFlagged.push(...output.flagged_edges);
1866
+ }
1867
+ const metadata = {
1868
+ ...outputs[0].metadata,
1869
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
1870
+ parsers: parserIds
1871
+ };
1872
+ return {
1873
+ metadata,
1874
+ nodes: mergedNodes,
1875
+ edges: mergedEdges,
1876
+ cross_refs: mergedCrossRefs,
1877
+ contradictions: mergedContradictions,
1878
+ warnings: mergedWarnings,
1879
+ flagged_edges: mergedFlagged,
1880
+ patterns: outputs[0].patterns
1881
+ };
1882
+ }
1883
+ function dedupCrossRefs(refs) {
1884
+ const seen = /* @__PURE__ */ new Set();
1885
+ const result = [];
1886
+ for (const ref of refs) {
1887
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
1888
+ if (seen.has(key)) continue;
1889
+ seen.add(key);
1890
+ result.push(ref);
1891
+ }
1892
+ return result;
1893
+ }
1894
+ function applyCrossLayerResults(uiOutput, results, primaryId) {
1895
+ const allCrossRefs = [...uiOutput.cross_refs];
1896
+ const allFlagged = [...uiOutput.flagged_edges];
1897
+ const allWarnings = [...uiOutput.warnings];
1898
+ const primaryResult = results.find((r) => r.parserId === primaryId);
1899
+ const secondaryResults = results.filter((r) => r.parserId !== primaryId);
1900
+ if (primaryResult) {
1901
+ allCrossRefs.push(...primaryResult.output.cross_refs);
1902
+ allFlagged.push(...primaryResult.output.flagged_edges);
1903
+ allWarnings.push(...primaryResult.output.warnings);
1904
+ }
1905
+ const primarySet = new Set(
1906
+ (primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
1907
+ );
1908
+ for (const sec of secondaryResults) {
1909
+ for (const ref of sec.output.cross_refs) {
1910
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
1911
+ if (primarySet.has(key)) {
1912
+ allCrossRefs.push(ref);
1913
+ } else {
1914
+ allFlagged.push({
1915
+ source: ref.source,
1916
+ target: ref.target,
1917
+ type: "out_of_pattern",
1918
+ label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
1919
+ confidence: "medium"
1920
+ });
1921
+ allCrossRefs.push(ref);
1922
+ }
1923
+ }
1924
+ allFlagged.push(...sec.output.flagged_edges);
1925
+ allWarnings.push(...sec.output.warnings);
1926
+ }
1927
+ return {
1928
+ ...uiOutput,
1929
+ cross_refs: dedupCrossRefs(allCrossRefs),
1930
+ flagged_edges: allFlagged,
1931
+ warnings: allWarnings
1932
+ };
1933
+ }
1934
+ var init_merge = __esm({
1935
+ "src/server/graph/core/merge.ts"() {
1936
+ "use strict";
1937
+ }
1938
+ });
1939
+
1462
1940
  // src/server/graph/core/graph-builder.ts
1463
- function getParser(layer) {
1464
- return ALL_PARSERS.find((p) => p.layer === layer);
1941
+ function readGraphFromDisk(rootDir, layer) {
1942
+ const filePath = (0, import_node_path10.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
1943
+ if (!(0, import_node_fs9.existsSync)(filePath)) return null;
1944
+ try {
1945
+ return JSON.parse((0, import_node_fs9.readFileSync)(filePath, "utf-8"));
1946
+ } catch {
1947
+ return null;
1948
+ }
1465
1949
  }
1466
1950
  function generateLayer(rootDir, layer) {
1467
- const parser = getParser(layer);
1468
- if (!parser) return null;
1469
- if (!parser.detect(rootDir)) return null;
1470
- const output = parser.generate(rootDir);
1951
+ const config = loadConfig(rootDir);
1952
+ const registry = createRegistry(config, rootDir);
1953
+ const parsers = registry.getParsers(layer);
1954
+ const outputs = [];
1955
+ for (const parser of parsers) {
1956
+ if (!parser.detect(rootDir)) continue;
1957
+ outputs.push(parser.generate(rootDir));
1958
+ }
1959
+ if (outputs.length === 0) return null;
1960
+ let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
1961
+ if (layer === "ui") {
1962
+ const layerOutputs = /* @__PURE__ */ new Map();
1963
+ layerOutputs.set("ui", merged);
1964
+ for (const otherLayer of ["api", "db"]) {
1965
+ const existing = readGraphFromDisk(rootDir, otherLayer);
1966
+ if (existing) layerOutputs.set(otherLayer, existing);
1967
+ }
1968
+ const crossParsers = registry.getCrossLayerParsers();
1969
+ const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
1970
+ const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
1971
+ if (crossResults.length > 0) {
1972
+ merged = applyCrossLayerResults(merged, crossResults, primaryId);
1973
+ }
1974
+ }
1471
1975
  return {
1472
1976
  layer,
1473
- output,
1474
- nodeCount: output.nodes.length,
1475
- edgeCount: output.edges.length
1977
+ output: merged,
1978
+ nodeCount: merged.nodes.length,
1979
+ edgeCount: merged.edges.length
1476
1980
  };
1477
1981
  }
1478
1982
  function generateAll(rootDir) {
1479
- const layers = ["api", "db", "ui"];
1983
+ const config = loadConfig(rootDir);
1984
+ const registry = createRegistry(config, rootDir);
1985
+ const layerOrder = ["api", "db", "ui"];
1986
+ const layerOutputs = /* @__PURE__ */ new Map();
1480
1987
  const results = [];
1481
- for (const layer of layers) {
1482
- const result = generateLayer(rootDir, layer);
1483
- if (result) results.push(result);
1988
+ for (const layer of layerOrder) {
1989
+ const parsers = registry.getParsers(layer);
1990
+ const outputs = [];
1991
+ for (const parser of parsers) {
1992
+ if (!parser.detect(rootDir)) continue;
1993
+ outputs.push(parser.generate(rootDir));
1994
+ }
1995
+ if (outputs.length === 0) continue;
1996
+ const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
1997
+ layerOutputs.set(layer, merged);
1998
+ results.push({
1999
+ layer,
2000
+ output: merged,
2001
+ nodeCount: merged.nodes.length,
2002
+ edgeCount: merged.edges.length
2003
+ });
2004
+ }
2005
+ const crossParsers = registry.getCrossLayerParsers();
2006
+ const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
2007
+ const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
2008
+ if (crossResults.length > 0 && layerOutputs.has("ui")) {
2009
+ const uiOutput = layerOutputs.get("ui");
2010
+ const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
2011
+ layerOutputs.set("ui", merged);
2012
+ const uiResult = results.find((r) => r.layer === "ui");
2013
+ if (uiResult) {
2014
+ uiResult.output = merged;
2015
+ uiResult.nodeCount = merged.nodes.length;
2016
+ uiResult.edgeCount = merged.edges.length;
2017
+ }
1484
2018
  }
1485
2019
  const byLayer = new Map(results.map((r) => [r.layer, r]));
1486
2020
  return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
1487
2021
  }
1488
- var ALL_PARSERS;
2022
+ var import_node_fs9, import_node_path10;
1489
2023
  var init_graph_builder = __esm({
1490
2024
  "src/server/graph/core/graph-builder.ts"() {
1491
2025
  "use strict";
1492
- init_react_nextjs();
1493
- init_nextjs_routes();
1494
- init_prisma_schema();
1495
- ALL_PARSERS = [
1496
- reactNextjsParser,
1497
- nextjsRoutesParser,
1498
- prismaSchemaParser
1499
- ];
2026
+ import_node_fs9 = require("node:fs");
2027
+ import_node_path10 = require("node:path");
2028
+ init_config();
2029
+ init_parser_registry();
2030
+ init_merge();
1500
2031
  }
1501
2032
  });
1502
2033
 
1503
2034
  // src/server/graph/index.ts
1504
2035
  function graphsDir(rootDir) {
1505
- return (0, import_node_path6.join)(rootDir, GRAPHS_DIR);
2036
+ return (0, import_node_path11.join)(rootDir, GRAPHS_DIR);
1506
2037
  }
1507
2038
  function graphFilePath(rootDir, layer) {
1508
- return (0, import_node_path6.join)(graphsDir(rootDir), `${layer}.json`);
2039
+ return (0, import_node_path11.join)(graphsDir(rootDir), `${layer}.json`);
1509
2040
  }
1510
2041
  function invalidateCache(filePath) {
1511
2042
  graphCache.delete(filePath);
1512
2043
  }
1513
2044
  function readGraph(rootDir, layer) {
1514
2045
  const filePath = graphFilePath(rootDir, layer);
1515
- if (!(0, import_node_fs6.existsSync)(filePath)) return null;
1516
- const stat = (0, import_node_fs6.statSync)(filePath);
2046
+ if (!(0, import_node_fs10.existsSync)(filePath)) return null;
2047
+ const stat = (0, import_node_fs10.statSync)(filePath);
1517
2048
  const cached = graphCache.get(filePath);
1518
2049
  if (cached && cached.mtimeMs === stat.mtimeMs) {
1519
2050
  return cached.graph;
1520
2051
  }
1521
- const content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
2052
+ const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
1522
2053
  const graph = JSON.parse(content);
1523
2054
  graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
1524
2055
  return graph;
@@ -1533,21 +2064,21 @@ function readAllGraphs(rootDir) {
1533
2064
  }
1534
2065
  function generateGraph(rootDir, layer) {
1535
2066
  const dir = graphsDir(rootDir);
1536
- (0, import_node_fs6.mkdirSync)(dir, { recursive: true });
2067
+ (0, import_node_fs10.mkdirSync)(dir, { recursive: true });
1537
2068
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
1538
2069
  for (const result of results) {
1539
2070
  const filePath = graphFilePath(rootDir, result.layer);
1540
- (0, import_node_fs6.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
2071
+ (0, import_node_fs10.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
1541
2072
  invalidateCache(filePath);
1542
2073
  }
1543
2074
  return results;
1544
2075
  }
1545
- var import_node_fs6, import_node_path6, GRAPHS_DIR, LAYERS, graphCache;
2076
+ var import_node_fs10, import_node_path11, GRAPHS_DIR, LAYERS, graphCache;
1546
2077
  var init_graph = __esm({
1547
2078
  "src/server/graph/index.ts"() {
1548
2079
  "use strict";
1549
- import_node_fs6 = require("node:fs");
1550
- import_node_path6 = require("node:path");
2080
+ import_node_fs10 = require("node:fs");
2081
+ import_node_path11 = require("node:path");
1551
2082
  init_graph_builder();
1552
2083
  GRAPHS_DIR = ".launchsecure/graphs";
1553
2084
  LAYERS = ["ui", "api", "db"];
@@ -1561,19 +2092,19 @@ __export(chart_serve_exports, {
1561
2092
  runServeCli: () => runServeCli,
1562
2093
  startChartServer: () => startChartServer
1563
2094
  });
1564
- function findProjectRoot(startDir) {
2095
+ function findProjectRoot2(startDir) {
1565
2096
  let dir = startDir;
1566
2097
  for (let i = 0; i < 8; i++) {
1567
- const graphsDir2 = import_node_path7.default.join(dir, ".launchsecure", "graphs");
1568
- if (import_node_fs7.default.existsSync(import_node_path7.default.join(graphsDir2, "ui.json")) || import_node_fs7.default.existsSync(import_node_path7.default.join(graphsDir2, "api.json")) || import_node_fs7.default.existsSync(import_node_path7.default.join(graphsDir2, "db.json"))) return dir;
1569
- const parent = import_node_path7.default.dirname(dir);
2098
+ const graphsDir2 = import_node_path12.default.join(dir, ".launchsecure", "graphs");
2099
+ if (import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "ui.json")) || import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "api.json")) || import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "db.json"))) return dir;
2100
+ const parent = import_node_path12.default.dirname(dir);
1570
2101
  if (parent === dir) break;
1571
2102
  dir = parent;
1572
2103
  }
1573
2104
  dir = startDir;
1574
2105
  for (let i = 0; i < 8; i++) {
1575
- if (import_node_fs7.default.existsSync(import_node_path7.default.join(dir, ".git"))) return dir;
1576
- const parent = import_node_path7.default.dirname(dir);
2106
+ if (import_node_fs11.default.existsSync(import_node_path12.default.join(dir, ".git"))) return dir;
2107
+ const parent = import_node_path12.default.dirname(dir);
1577
2108
  if (parent === dir) break;
1578
2109
  dir = parent;
1579
2110
  }
@@ -1625,16 +2156,16 @@ function buildMergedGraph(projectRoot) {
1625
2156
  };
1626
2157
  }
1627
2158
  function serveStatic(res, filePath) {
1628
- if (!import_node_fs7.default.existsSync(filePath) || !import_node_fs7.default.statSync(filePath).isFile()) return false;
1629
- const ext = import_node_path7.default.extname(filePath).toLowerCase();
2159
+ if (!import_node_fs11.default.existsSync(filePath) || !import_node_fs11.default.statSync(filePath).isFile()) return false;
2160
+ const ext = import_node_path12.default.extname(filePath).toLowerCase();
1630
2161
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
1631
2162
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
1632
- import_node_fs7.default.createReadStream(filePath).pipe(res);
2163
+ import_node_fs11.default.createReadStream(filePath).pipe(res);
1633
2164
  return true;
1634
2165
  }
1635
2166
  function serveIndex(res, clientDir) {
1636
- const indexPath = import_node_path7.default.join(clientDir, "index.html");
1637
- if (!import_node_fs7.default.existsSync(indexPath)) {
2167
+ const indexPath = import_node_path12.default.join(clientDir, "index.html");
2168
+ if (!import_node_fs11.default.existsSync(indexPath)) {
1638
2169
  res.writeHead(500, { "Content-Type": "text/plain" });
1639
2170
  res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
1640
2171
  return;
@@ -1642,14 +2173,14 @@ function serveIndex(res, clientDir) {
1642
2173
  serveStatic(res, indexPath);
1643
2174
  }
1644
2175
  function tryListen(server, port) {
1645
- return new Promise((resolve, reject) => {
2176
+ return new Promise((resolve2, reject) => {
1646
2177
  const onError = (err2) => {
1647
2178
  server.off("listening", onListening);
1648
2179
  reject(err2);
1649
2180
  };
1650
2181
  const onListening = () => {
1651
2182
  server.off("error", onError);
1652
- resolve(port);
2183
+ resolve2(port);
1653
2184
  };
1654
2185
  server.once("error", onError);
1655
2186
  server.once("listening", onListening);
@@ -1675,7 +2206,7 @@ async function bindWithFallback(server, startPort) {
1675
2206
  }
1676
2207
  async function startChartServer(opts = {}) {
1677
2208
  const cwd = opts.cwd ?? process.cwd();
1678
- const projectRoot = findProjectRoot(cwd);
2209
+ const projectRoot = findProjectRoot2(cwd);
1679
2210
  const existing = getLiveLock();
1680
2211
  if (existing) {
1681
2212
  if (!opts.quiet) {
@@ -1686,7 +2217,7 @@ async function startChartServer(opts = {}) {
1686
2217
  }
1687
2218
  return { port: existing.port, url: existing.url };
1688
2219
  }
1689
- const clientDir = opts.clientDir ?? import_node_path7.default.join(__dirname, "..", "chart-client");
2220
+ const clientDir = opts.clientDir ?? import_node_path12.default.join(__dirname, "..", "chart-client");
1690
2221
  const server = import_node_http.default.createServer((req, res) => {
1691
2222
  try {
1692
2223
  const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
@@ -1729,8 +2260,43 @@ async function startChartServer(opts = {}) {
1729
2260
  res.end(JSON.stringify({ ok: true, projectRoot }));
1730
2261
  return;
1731
2262
  }
2263
+ if (req.method === "GET" && url2.pathname === "/api/parser-config") {
2264
+ const config = loadConfig(projectRoot);
2265
+ const detection = [
2266
+ { id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
2267
+ { id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
2268
+ { id: "prisma-schema", layer: "db", label: "Prisma Schema", detected: prismaSchemaParser.detect(projectRoot) }
2269
+ ];
2270
+ const crosslayerParsers = [
2271
+ { id: "fetch-resolver", label: "Fetch / api.method() calls" },
2272
+ { id: "api-annotations", label: "@api annotations" },
2273
+ { id: "url-literal-scanner", label: "/api/... URL literals" }
2274
+ ];
2275
+ res.writeHead(200, { "Content-Type": "application/json" });
2276
+ res.end(JSON.stringify({ config, detection, crosslayerParsers }));
2277
+ return;
2278
+ }
2279
+ if (req.method === "POST" && url2.pathname === "/api/parser-config") {
2280
+ let body = "";
2281
+ req.on("data", (chunk) => {
2282
+ body += chunk.toString();
2283
+ });
2284
+ req.on("end", () => {
2285
+ try {
2286
+ const newConfig = JSON.parse(body);
2287
+ const configPath = import_node_path12.default.join(projectRoot, ".launchchart.json");
2288
+ import_node_fs11.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
2289
+ res.writeHead(200, { "Content-Type": "application/json" });
2290
+ res.end(JSON.stringify({ ok: true }));
2291
+ } catch (err2) {
2292
+ res.writeHead(400, { "Content-Type": "application/json" });
2293
+ res.end(JSON.stringify({ ok: false, error: String(err2) }));
2294
+ }
2295
+ });
2296
+ return;
2297
+ }
1732
2298
  if (url2.pathname !== "/") {
1733
- const staticPath = import_node_path7.default.join(clientDir, url2.pathname);
2299
+ const staticPath = import_node_path12.default.join(clientDir, url2.pathname);
1734
2300
  if (serveStatic(res, staticPath)) return;
1735
2301
  }
1736
2302
  serveIndex(res, clientDir);
@@ -1784,15 +2350,19 @@ function runServeCli(argv) {
1784
2350
  process.exit(1);
1785
2351
  });
1786
2352
  }
1787
- var import_node_http, import_node_fs7, import_node_path7, DEFAULT_PORT, MAX_PORT_SCAN, MIME_TYPES;
2353
+ var import_node_http, import_node_fs11, import_node_path12, DEFAULT_PORT, MAX_PORT_SCAN, MIME_TYPES;
1788
2354
  var init_chart_serve = __esm({
1789
2355
  "src/server/chart-serve.ts"() {
1790
2356
  "use strict";
1791
2357
  import_node_http = __toESM(require("node:http"));
1792
- import_node_fs7 = __toESM(require("node:fs"));
1793
- import_node_path7 = __toESM(require("node:path"));
2358
+ import_node_fs11 = __toESM(require("node:fs"));
2359
+ import_node_path12 = __toESM(require("node:path"));
1794
2360
  init_graph();
1795
2361
  init_lockfile();
2362
+ init_config();
2363
+ init_react_nextjs();
2364
+ init_nextjs_routes();
2365
+ init_prisma_schema();
1796
2366
  DEFAULT_PORT = 52819;
1797
2367
  MAX_PORT_SCAN = 20;
1798
2368
  MIME_TYPES = {
@@ -2118,9 +2688,9 @@ function handleReadGraph(args) {
2118
2688
  return okJson(result);
2119
2689
  }
2120
2690
  function nodeToFilePath(rootDir, layer, nodeId) {
2121
- if (layer === "ui") return (0, import_node_path8.join)(rootDir, "src", nodeId);
2122
- if (layer === "api") return (0, import_node_path8.join)(rootDir, nodeId);
2123
- if (layer === "db") return (0, import_node_path8.join)(rootDir, "prisma", "schema.prisma");
2691
+ if (layer === "ui") return (0, import_node_path13.join)(rootDir, "src", nodeId);
2692
+ if (layer === "api") return (0, import_node_path13.join)(rootDir, nodeId);
2693
+ if (layer === "db") return (0, import_node_path13.join)(rootDir, "prisma", "schema.prisma");
2124
2694
  return null;
2125
2695
  }
2126
2696
  function handleGrepNodes(args) {
@@ -2180,11 +2750,11 @@ function handleGrepNodes(args) {
2180
2750
  let filesSearched = 0;
2181
2751
  let truncated = false;
2182
2752
  for (const [filePath, nodeId] of filePaths) {
2183
- if (!(0, import_node_fs8.existsSync)(filePath)) continue;
2753
+ if (!(0, import_node_fs12.existsSync)(filePath)) continue;
2184
2754
  filesSearched++;
2185
2755
  let content;
2186
2756
  try {
2187
- content = (0, import_node_fs8.readFileSync)(filePath, "utf-8");
2757
+ content = (0, import_node_fs12.readFileSync)(filePath, "utf-8");
2188
2758
  } catch {
2189
2759
  continue;
2190
2760
  }
@@ -2221,13 +2791,10 @@ function handleGrepNodes(args) {
2221
2791
  truncated
2222
2792
  });
2223
2793
  }
2224
- function handleGetGraphUiUrl() {
2794
+ function handleChartServerStatus() {
2225
2795
  const lock = getLiveLock();
2226
2796
  if (!lock) {
2227
- return okJson({
2228
- running: false,
2229
- hint: "No launch-chart UI server is currently running. Start one with `launch-chart serve`, or set LAUNCH_CHART_AUTOSERVE=1 in your MCP config to auto-start it alongside the MCP server."
2230
- });
2797
+ return okJson({ running: false });
2231
2798
  }
2232
2799
  return okJson({
2233
2800
  running: true,
@@ -2238,6 +2805,113 @@ function handleGetGraphUiUrl() {
2238
2805
  startedAt: lock.startedAt
2239
2806
  });
2240
2807
  }
2808
+ function handleStartChartServer(args) {
2809
+ const lock = getLiveLock();
2810
+ if (lock) {
2811
+ return okJson({
2812
+ started: false,
2813
+ reason: "already_running",
2814
+ url: lock.url,
2815
+ port: lock.port,
2816
+ pid: lock.pid
2817
+ });
2818
+ }
2819
+ const entryPath = process.argv[1];
2820
+ const logDir = (0, import_node_path13.join)((0, import_node_os2.homedir)(), ".launchsecure");
2821
+ (0, import_node_fs12.mkdirSync)(logDir, { recursive: true });
2822
+ const logPath = (0, import_node_path13.join)(logDir, "launch-chart.log");
2823
+ const out = (0, import_node_fs12.openSync)(logPath, "a");
2824
+ const err2 = (0, import_node_fs12.openSync)(logPath, "a");
2825
+ const portArgs = args.port ? ["--port", String(args.port)] : [];
2826
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
2827
+ detached: true,
2828
+ stdio: ["ignore", out, err2],
2829
+ env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }
2830
+ });
2831
+ child.unref();
2832
+ return okJson({
2833
+ started: true,
2834
+ pid: child.pid,
2835
+ logPath
2836
+ });
2837
+ }
2838
+ function handleStopChartServer() {
2839
+ const lock = getLiveLock();
2840
+ if (!lock) {
2841
+ return okJson({ stopped: false, reason: "not_running" });
2842
+ }
2843
+ try {
2844
+ process.kill(lock.pid, "SIGTERM");
2845
+ return okJson({ stopped: true, pid: lock.pid });
2846
+ } catch (e) {
2847
+ const code = e.code;
2848
+ if (code === "ESRCH") {
2849
+ clearLock();
2850
+ return okJson({ stopped: true, pid: lock.pid, note: "process was already gone, lock cleaned up" });
2851
+ }
2852
+ return okJson({ stopped: false, reason: `kill failed: ${code ?? e}` });
2853
+ }
2854
+ }
2855
+ function handleDetectProjectStack() {
2856
+ const rootDir = findProjectRoot(process.cwd());
2857
+ const parsers = [
2858
+ { id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
2859
+ { id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
2860
+ { id: "prisma-schema", layer: "db", detected: prismaSchemaParser.detect(rootDir) }
2861
+ ];
2862
+ const config = loadConfig(rootDir);
2863
+ let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
2864
+ const uiGraph = readGraph(rootDir, "ui");
2865
+ if (uiGraph) {
2866
+ for (const ref of uiGraph.cross_refs ?? []) {
2867
+ if (ref.type === "calls_api") stats.calls_api++;
2868
+ if (ref.type === "references_api") stats.references_api++;
2869
+ }
2870
+ for (const f of uiGraph.flagged_edges ?? []) {
2871
+ if (f.type === "out_of_pattern") stats.out_of_pattern++;
2872
+ }
2873
+ }
2874
+ const srcDir = (0, import_node_path13.join)(rootDir, "src");
2875
+ if ((0, import_node_fs12.existsSync)(srcDir)) {
2876
+ const scanDir = (dir) => {
2877
+ if (!(0, import_node_fs12.existsSync)(dir)) return;
2878
+ for (const entry of (0, import_node_fs12.readdirSync)(dir, { withFileTypes: true })) {
2879
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2880
+ const full = (0, import_node_path13.join)(dir, entry.name);
2881
+ if (entry.isDirectory()) {
2882
+ scanDir(full);
2883
+ continue;
2884
+ }
2885
+ if (![".ts", ".tsx"].includes((0, import_node_path13.extname)(entry.name))) continue;
2886
+ try {
2887
+ const content = (0, import_node_fs12.readFileSync)(full, "utf-8");
2888
+ const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
2889
+ if (matches) stats.annotations += matches.length;
2890
+ } catch {
2891
+ }
2892
+ }
2893
+ };
2894
+ scanDir(srcDir);
2895
+ }
2896
+ let recommendedPrimary = "fetch-resolver";
2897
+ if (stats.annotations > 0 && stats.annotations >= stats.calls_api) {
2898
+ recommendedPrimary = "api-annotations";
2899
+ } else if (stats.calls_api === 0 && stats.references_api > 0) {
2900
+ recommendedPrimary = "url-literal-scanner";
2901
+ }
2902
+ return okJson({
2903
+ parsers,
2904
+ crosslayer_parsers: [
2905
+ { id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
2906
+ { id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
2907
+ { id: "url-literal-scanner", description: "Finds /api/... string literals as fallback detection" }
2908
+ ],
2909
+ stats,
2910
+ recommended_primary: recommendedPrimary,
2911
+ current_config: Object.keys(config).length > 0 ? config : null,
2912
+ config_path: ".launchchart.json"
2913
+ });
2914
+ }
2241
2915
  function send(msg) {
2242
2916
  process.stdout.write(JSON.stringify(msg) + "\n");
2243
2917
  }
@@ -2281,8 +2955,20 @@ function handleMessage(msg) {
2281
2955
  respond(id ?? null, handleGrepNodes(args));
2282
2956
  return;
2283
2957
  }
2284
- if (toolName === "get_graph_ui_url") {
2285
- respond(id ?? null, handleGetGraphUiUrl());
2958
+ if (toolName === "chart_server_status") {
2959
+ respond(id ?? null, handleChartServerStatus());
2960
+ return;
2961
+ }
2962
+ if (toolName === "start_chart_server") {
2963
+ respond(id ?? null, handleStartChartServer(args));
2964
+ return;
2965
+ }
2966
+ if (toolName === "stop_chart_server") {
2967
+ respond(id ?? null, handleStopChartServer());
2968
+ return;
2969
+ }
2970
+ if (toolName === "detect_project_stack") {
2971
+ respond(id ?? null, handleDetectProjectStack());
2286
2972
  return;
2287
2973
  }
2288
2974
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
@@ -2320,14 +3006,20 @@ function startGraphMcpServer() {
2320
3006
  process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
2321
3007
  `);
2322
3008
  }
2323
- var import_node_fs8, import_node_path8, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
3009
+ var import_node_fs12, import_node_path13, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
2324
3010
  var init_graph_mcp = __esm({
2325
3011
  "src/server/graph-mcp.ts"() {
2326
3012
  "use strict";
2327
- import_node_fs8 = require("node:fs");
2328
- import_node_path8 = require("node:path");
3013
+ import_node_fs12 = require("node:fs");
3014
+ import_node_path13 = require("node:path");
3015
+ import_node_child_process2 = require("node:child_process");
3016
+ import_node_os2 = require("node:os");
2329
3017
  init_graph();
2330
3018
  init_lockfile();
3019
+ init_config();
3020
+ init_react_nextjs();
3021
+ init_nextjs_routes();
3022
+ init_prisma_schema();
2331
3023
  SERVER_INFO = {
2332
3024
  name: "launchsecure-graph",
2333
3025
  version: "0.0.1"
@@ -2455,8 +3147,39 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
2455
3147
  }
2456
3148
  },
2457
3149
  {
2458
- name: "get_graph_ui_url",
2459
- description: 'Return the URL of a running launch-chart UI server if one exists. The UI is a visual, interactive view of the merged UI+API+DB project graph served by `launch-chart serve` (or auto-started via LAUNCH_CHART_AUTOSERVE=1). \n\nReturns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }. If running is false, no server is currently live \u2014 suggest the user run `launch-chart serve` to start one. \n\nUse this when the user asks "open the graph", "show me the project graph UI", "where\'s the chart", etc.',
3150
+ name: "chart_server_status",
3151
+ description: `Check whether the launch-chart UI server is running. Returns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }.
3152
+
3153
+ Use this when the user asks "is the chart running", "show me the project graph UI", "where's the chart", etc.`,
3154
+ inputSchema: {
3155
+ type: "object",
3156
+ properties: {}
3157
+ }
3158
+ },
3159
+ {
3160
+ name: "start_chart_server",
3161
+ description: 'Start the launch-chart UI server as a detached background process. The server serves the interactive project graph visualization at http://localhost:<port>. If the server is already running, returns the existing URL without spawning a duplicate. \n\nUse this when the user asks to "start the chart", "fire up charts", "open the graph UI", etc.',
3162
+ inputSchema: {
3163
+ type: "object",
3164
+ properties: {
3165
+ port: {
3166
+ type: "number",
3167
+ description: "Port to bind the server to. Defaults to 52819 with automatic fallback if in use."
3168
+ }
3169
+ }
3170
+ }
3171
+ },
3172
+ {
3173
+ name: "stop_chart_server",
3174
+ description: 'Stop the running launch-chart UI server. Sends SIGTERM to the server process and cleans up the lock file. If no server is running, returns a no-op response. \n\nUse this when the user asks to "stop the chart", "cool down charts", "kill the graph server", etc.',
3175
+ inputSchema: {
3176
+ type: "object",
3177
+ properties: {}
3178
+ }
3179
+ },
3180
+ {
3181
+ name: "detect_project_stack",
3182
+ description: "Detect project frameworks, available parsers, and recommend parser configuration. Scans the project to identify the tech stack (Next.js, Prisma, React, etc.), reports which built-in parsers are applicable, and provides cross-layer detection stats (fetch calls, @api annotations, URL literals). Returns a recommended primary parser and current .launchchart.json config if present. \n\nUse this when setting up launch-chart for a new project or reviewing parser configuration.",
2460
3183
  inputSchema: {
2461
3184
  type: "object",
2462
3185
  properties: {}
@@ -2513,11 +3236,11 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
2513
3236
  });
2514
3237
 
2515
3238
  // src/server/graph-mcp-entry.ts
2516
- var import_node_child_process2 = require("node:child_process");
2517
- var import_node_fs9 = require("node:fs");
2518
- var import_node_path9 = __toESM(require("node:path"));
2519
- var import_node_os2 = require("node:os");
2520
- var import_node_fs10 = require("node:fs");
3239
+ var import_node_child_process3 = require("node:child_process");
3240
+ var import_node_fs13 = require("node:fs");
3241
+ var import_node_path14 = __toESM(require("node:path"));
3242
+ var import_node_os3 = require("node:os");
3243
+ var import_node_fs14 = require("node:fs");
2521
3244
  init_lockfile();
2522
3245
  function logStderr(msg) {
2523
3246
  process.stderr.write(`[launch-chart] ${msg}
@@ -2531,13 +3254,13 @@ function maybeAutoServe() {
2531
3254
  return;
2532
3255
  }
2533
3256
  try {
2534
- const logDir = import_node_path9.default.join((0, import_node_os2.homedir)(), ".launchsecure");
2535
- (0, import_node_fs10.mkdirSync)(logDir, { recursive: true });
2536
- const logPath = import_node_path9.default.join(logDir, "launch-chart.log");
2537
- const out = (0, import_node_fs9.openSync)(logPath, "a");
2538
- const err2 = (0, import_node_fs9.openSync)(logPath, "a");
3257
+ const logDir = import_node_path14.default.join((0, import_node_os3.homedir)(), ".launchsecure");
3258
+ (0, import_node_fs14.mkdirSync)(logDir, { recursive: true });
3259
+ const logPath = import_node_path14.default.join(logDir, "launch-chart.log");
3260
+ const out = (0, import_node_fs13.openSync)(logPath, "a");
3261
+ const err2 = (0, import_node_fs13.openSync)(logPath, "a");
2539
3262
  const entryPath = process.argv[1];
2540
- const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve"], {
3263
+ const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
2541
3264
  detached: true,
2542
3265
  stdio: ["ignore", out, err2],
2543
3266
  env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }