@launchsecure/launch-kit 0.0.24 → 0.0.26

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.
Files changed (126) hide show
  1. package/README.md +50 -0
  2. package/dist/beacon/beacon.mjs +1016 -0
  3. package/dist/beacon/beacon.mjs.map +1 -0
  4. package/dist/beacon/beacon.umd.js +87 -0
  5. package/dist/beacon/beacon.umd.js.map +1 -0
  6. package/dist/beacon/index-DAIDnjfR.mjs +513 -0
  7. package/dist/beacon/index-DAIDnjfR.mjs.map +1 -0
  8. package/dist/beacon/types/capture/element.d.ts +3 -0
  9. package/dist/beacon/types/capture/element.d.ts.map +1 -0
  10. package/dist/beacon/types/capture/framework.d.ts +3 -0
  11. package/dist/beacon/types/capture/framework.d.ts.map +1 -0
  12. package/dist/beacon/types/capture/metadata.d.ts +3 -0
  13. package/dist/beacon/types/capture/metadata.d.ts.map +1 -0
  14. package/dist/beacon/types/capture/overlay.d.ts +7 -0
  15. package/dist/beacon/types/capture/overlay.d.ts.map +1 -0
  16. package/dist/beacon/types/capture/picker.d.ts +12 -0
  17. package/dist/beacon/types/capture/picker.d.ts.map +1 -0
  18. package/dist/beacon/types/capture/screenshot.d.ts +7 -0
  19. package/dist/beacon/types/capture/screenshot.d.ts.map +1 -0
  20. package/dist/beacon/types/capture/selector.d.ts +2 -0
  21. package/dist/beacon/types/capture/selector.d.ts.map +1 -0
  22. package/dist/beacon/types/element.d.ts +50 -0
  23. package/dist/beacon/types/element.d.ts.map +1 -0
  24. package/dist/beacon/types/index.d.ts +4 -0
  25. package/dist/beacon/types/index.d.ts.map +1 -0
  26. package/dist/beacon/types/transport/submit.d.ts +3 -0
  27. package/dist/beacon/types/transport/submit.d.ts.map +1 -0
  28. package/dist/beacon/types/types.d.ts +88 -0
  29. package/dist/beacon/types/types.d.ts.map +1 -0
  30. package/dist/beacon/types/ui/button.d.ts +2 -0
  31. package/dist/beacon/types/ui/button.d.ts.map +1 -0
  32. package/dist/beacon/types/ui/drawer.d.ts +31 -0
  33. package/dist/beacon/types/ui/drawer.d.ts.map +1 -0
  34. package/dist/beacon/types/ui/icons.d.ts +9 -0
  35. package/dist/beacon/types/ui/icons.d.ts.map +1 -0
  36. package/dist/beacon/types/ui/pick-mode-overlay.d.ts +25 -0
  37. package/dist/beacon/types/ui/pick-mode-overlay.d.ts.map +1 -0
  38. package/dist/beacon/types/ui/pin-popover.d.ts +14 -0
  39. package/dist/beacon/types/ui/pin-popover.d.ts.map +1 -0
  40. package/dist/chart-client/assets/{index-C8ANseEa.js → index-Bk1hawjD.js} +63 -58
  41. package/dist/chart-client/assets/index-DpaGa3bY.css +1 -0
  42. package/dist/chart-client/index.html +2 -2
  43. package/dist/client/assets/index-Bfel4OQ5.css +32 -0
  44. package/dist/client/assets/{index-Ds9UP_cj.js → index-eC-WuUWB.js} +58 -58
  45. package/dist/client/index.html +2 -2
  46. package/dist/council-client/assets/{index-Dc41S-R2.js → index-Cs_MVXHf.js} +14 -14
  47. package/dist/council-client/assets/index-P5kMsT5a.css +1 -0
  48. package/dist/council-client/index.html +2 -2
  49. package/dist/deck-client/assets/{_baseUniq-2gclQXo7.js → _baseUniq-C2xT_eYu.js} +1 -1
  50. package/dist/deck-client/assets/{arc-DcMY5Wm0.js → arc-CmVL9pGd.js} +1 -1
  51. package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-B8iirmmJ.js → architectureDiagram-Q4EWVU46-BSFgdjve.js} +1 -1
  52. package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-B4JBLjmJ.js → blockDiagram-DXYQGD6D-DuLzscvP.js} +1 -1
  53. package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CojrJAk8.js → c4Diagram-AHTNJAMY-CfCJB8eY.js} +1 -1
  54. package/dist/deck-client/assets/channel-B4aNO8ZB.js +1 -0
  55. package/dist/deck-client/assets/{chunk-4BX2VUAB-Bmb_BMDo.js → chunk-4BX2VUAB-DxmLYTWZ.js} +1 -1
  56. package/dist/deck-client/assets/{chunk-4TB4RGXK-CumBy8qe.js → chunk-4TB4RGXK-CCnf7GFE.js} +1 -1
  57. package/dist/deck-client/assets/{chunk-55IACEB6-Ka8Hb1wD.js → chunk-55IACEB6-Db9DApcj.js} +1 -1
  58. package/dist/deck-client/assets/{chunk-EDXVE4YY-B3sIPiQo.js → chunk-EDXVE4YY-DmYDq8ZI.js} +1 -1
  59. package/dist/deck-client/assets/{chunk-FMBD7UC4-C1tYkaqu.js → chunk-FMBD7UC4-BGhUlF20.js} +1 -1
  60. package/dist/deck-client/assets/{chunk-OYMX7WX6-D7Wacbky.js → chunk-OYMX7WX6-CpEnicQZ.js} +1 -1
  61. package/dist/deck-client/assets/{chunk-QZHKN3VN-ChXI0vO3.js → chunk-QZHKN3VN-Doa7LKwf.js} +1 -1
  62. package/dist/deck-client/assets/{chunk-YZCP3GAM-BXhiqf8u.js → chunk-YZCP3GAM-CpkIlH6V.js} +1 -1
  63. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-BHTI0yWz.js +1 -0
  64. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-BHTI0yWz.js +1 -0
  65. package/dist/deck-client/assets/clone-HduFm7qU.js +1 -0
  66. package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-Bqp3p68D.js → cose-bilkent-S5V4N54A-Bkh8Bfcb.js} +1 -1
  67. package/dist/deck-client/assets/{dagre-KV5264BT-BS-rtyhZ.js → dagre-KV5264BT-Bp0XpTgH.js} +1 -1
  68. package/dist/deck-client/assets/{diagram-5BDNPKRD-BIrj9YGI.js → diagram-5BDNPKRD-ZHiyGYPQ.js} +1 -1
  69. package/dist/deck-client/assets/{diagram-G4DWMVQ6-noHWPIg4.js → diagram-G4DWMVQ6-BW-Q8_H5.js} +1 -1
  70. package/dist/deck-client/assets/{diagram-MMDJMWI5-C2qHxvqV.js → diagram-MMDJMWI5-6I3LTafu.js} +1 -1
  71. package/dist/deck-client/assets/{diagram-TYMM5635-BytnGQr-.js → diagram-TYMM5635-CyM5YK28.js} +1 -1
  72. package/dist/deck-client/assets/{erDiagram-SMLLAGMA-BfK5m2YQ.js → erDiagram-SMLLAGMA-CjNxVJHk.js} +1 -1
  73. package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-Cq925G1Z.js → flowDiagram-DWJPFMVM-BDQHuAJR.js} +1 -1
  74. package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-DhhHPAmj.js → ganttDiagram-T4ZO3ILL-B7MnkpbP.js} +1 -1
  75. package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-B3Lc0h9q.js → gitGraphDiagram-UUTBAWPF-C9dZAcYD.js} +1 -1
  76. package/dist/deck-client/assets/{graph-RTawgVWm.js → graph-CjdBnzUy.js} +1 -1
  77. package/dist/deck-client/assets/{index-BfIfJXmS.js → index-DeIVPW63.js} +68 -68
  78. package/dist/deck-client/assets/index-LKZDAS9S.css +1 -0
  79. package/dist/deck-client/assets/{infoDiagram-42DDH7IO-BlR584kX.js → infoDiagram-42DDH7IO-C7d3iRC3.js} +1 -1
  80. package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DygKoNGY.js → ishikawaDiagram-UXIWVN3A-BcYGKj09.js} +1 -1
  81. package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-BnaiYp9N.js → journeyDiagram-VCZTEJTY-DqFlRrOL.js} +1 -1
  82. package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-BQBUBzJC.js → kanban-definition-6JOO6SKY-BJhPp1NR.js} +1 -1
  83. package/dist/deck-client/assets/{layout-DeZ8HI1T.js → layout-DIeS6GvK.js} +1 -1
  84. package/dist/deck-client/assets/{linear-C6roLi_9.js → linear-He_yJy5H.js} +1 -1
  85. package/dist/deck-client/assets/{min-CbUksbuI.js → min-DQ6Kx06t.js} +1 -1
  86. package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-iNxV62yN.js → mindmap-definition-QFDTVHPH-sQ62L8T2.js} +1 -1
  87. package/dist/deck-client/assets/{pieDiagram-DEJITSTG-DHVA0jaG.js → pieDiagram-DEJITSTG-BqCWmU2K.js} +1 -1
  88. package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-DBeKKLUQ.js → quadrantDiagram-34T5L4WZ-rQ1TJOoe.js} +1 -1
  89. package/dist/deck-client/assets/{requirementDiagram-MS252O5E-CBwITx7p.js → requirementDiagram-MS252O5E-BO2MPBOM.js} +1 -1
  90. package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BtE-1YTU.js → sankeyDiagram-XADWPNL6-BgsHEVex.js} +1 -1
  91. package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-DN96yPP2.js → sequenceDiagram-FGHM5R23-B3j1yMLU.js} +1 -1
  92. package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-VUkKC2uJ.js → stateDiagram-FHFEXIEX-C8jFlZou.js} +1 -1
  93. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BoqepHW0.js +1 -0
  94. package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-oUeZhRns.js → timeline-definition-GMOUNBTQ-tM-qo4Zk.js} +1 -1
  95. package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-D87fK90n.js → vennDiagram-DHZGUBPP-B0-6kOEu.js} +1 -1
  96. package/dist/deck-client/assets/wardley-RL74JXVD-HpBk07P-.js +162 -0
  97. package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-Ca_i0QRA.js → wardleyDiagram-NUSXRM2D-BkA1NLDE.js} +1 -1
  98. package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-CUOJVIvq.js → xychartDiagram-5P7HB3ND-CEKGSuI-.js} +1 -1
  99. package/dist/deck-client/index.html +2 -2
  100. package/dist/server/chart-serve.js +1336 -141
  101. package/dist/server/cli.js +28423 -6671
  102. package/dist/server/council-entry.js +0 -0
  103. package/dist/server/deck-mcp-entry.js +332 -3
  104. package/dist/server/deck-serve.js +288 -0
  105. package/dist/server/fb-wizard.js +0 -0
  106. package/dist/server/graph/queries/classify.scm +8 -0
  107. package/dist/server/graph/queries/exports.scm +7 -0
  108. package/dist/server/graph-mcp-entry.js +1987 -224
  109. package/dist/server/recall-entry.js +1112 -0
  110. package/package.json +47 -21
  111. package/dist/chart-client/assets/index--120d9P9.css +0 -1
  112. package/dist/client/assets/index-Bf8zdL3x.css +0 -32
  113. package/dist/council-client/assets/index-CofZh7pS.css +0 -1
  114. package/dist/deck-client/assets/channel-ERh5jKXV.js +0 -1
  115. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-CMi1Gaev.js +0 -1
  116. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-CMi1Gaev.js +0 -1
  117. package/dist/deck-client/assets/clone-DfWhlD4X.js +0 -1
  118. package/dist/deck-client/assets/index-765AIQ9z.css +0 -1
  119. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CA0IjulK.js +0 -1
  120. package/dist/deck-client/assets/wardley-RL74JXVD-DYbYcpDp.js +0 -162
  121. package/dist/server/deck-server/deck-mcp-entry.js +0 -1789
  122. package/dist/server/deck-server/deck-serve.js +0 -1275
  123. package/dist/server/server/chart-serve.js +0 -4643
  124. package/dist/server/server/cli.js +0 -13360
  125. package/dist/server/server/fb-wizard.js +0 -136
  126. package/dist/server/server/graph-mcp-entry.js +0 -6776
@@ -175,6 +175,7 @@ function walkWithIgnore(dir, exts, opts = {}) {
175
175
  const skip = opts.extraIgnore ? /* @__PURE__ */ new Set([...DEFAULT_IGNORE_DIRS, ...opts.extraIgnore]) : DEFAULT_IGNORE_DIRS;
176
176
  for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
177
177
  if (entry.isDirectory()) {
178
+ if (entry.name.startsWith(".")) continue;
178
179
  if (skip.has(entry.name)) continue;
179
180
  results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, opts));
180
181
  } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
@@ -191,15 +192,9 @@ var init_walk = __esm({
191
192
  import_node_path3 = require("node:path");
192
193
  DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
193
194
  "node_modules",
194
- ".git",
195
- ".next",
196
- ".launchsecure",
197
- ".claude",
198
195
  "dist",
199
196
  "build",
200
197
  "out",
201
- ".turbo",
202
- ".vercel",
203
198
  "coverage"
204
199
  ]);
205
200
  }
@@ -403,14 +398,20 @@ var init_resolve_paths = __esm({
403
398
  // src/server/graph/core/ts-extractor.ts
404
399
  var ts_extractor_exports = {};
405
400
  __export(ts_extractor_exports, {
401
+ ParseCascadeError: () => ParseCascadeError,
406
402
  classifyFile: () => classifyFile,
403
+ classifyRouteAgainstMiddleware: () => classifyRouteAgainstMiddleware,
407
404
  createQuery: () => createQuery,
408
405
  extractAuthWrappersTS: () => extractAuthWrappersTS,
409
406
  extractDbCallsTS: () => extractDbCallsTS,
410
407
  extractDeep: () => extractDeep,
408
+ extractEffects: () => extractEffects,
409
+ extractMiddlewareAuthTS: () => extractMiddlewareAuthTS,
411
410
  initTreeSitter: () => initTreeSitter,
411
+ middlewarePatternToRegex: () => middlewarePatternToRegex,
412
412
  parseCodeTS: () => parseCodeTS,
413
413
  parseFileTS: () => parseFileTS,
414
+ parseSource: () => parseSource,
414
415
  setExtractorConfig: () => setExtractorConfig
415
416
  });
416
417
  async function initTreeSitter() {
@@ -444,8 +445,38 @@ function getQuery(name) {
444
445
  }
445
446
  function parseSource(absPath) {
446
447
  ensureInit();
447
- const content = (0, import_node_fs5.readFileSync)(absPath, "utf-8");
448
- return parserInstance.parse(content);
448
+ let content;
449
+ try {
450
+ const stat = (0, import_node_fs5.statSync)(absPath);
451
+ if (stat.size > MAX_PARSEABLE_BYTES) {
452
+ process.stderr.write(`[lc-extractor] skipping ${absPath}: ${stat.size} bytes exceeds max ${MAX_PARSEABLE_BYTES}
453
+ `);
454
+ return null;
455
+ }
456
+ content = (0, import_node_fs5.readFileSync)(absPath, "utf-8");
457
+ } catch (e) {
458
+ process.stderr.write(`[lc-extractor] read failed for ${absPath}: ${e instanceof Error ? e.message : String(e)}
459
+ `);
460
+ return null;
461
+ }
462
+ try {
463
+ const tree = parserInstance.parse(content);
464
+ consecutiveParseFailures = 0;
465
+ return tree;
466
+ } catch (e) {
467
+ consecutiveParseFailures++;
468
+ const msg = e instanceof Error ? e.message : String(e);
469
+ process.stderr.write(
470
+ `[lc-extractor] parse failed for ${absPath}: ${msg} (consecutive failures: ${consecutiveParseFailures}/${MAX_CONSECUTIVE_PARSE_FAILURES})
471
+ `
472
+ );
473
+ if (consecutiveParseFailures >= MAX_CONSECUTIVE_PARSE_FAILURES) {
474
+ const tripCount = consecutiveParseFailures;
475
+ consecutiveParseFailures = 0;
476
+ throw new ParseCascadeError(absPath, tripCount);
477
+ }
478
+ return null;
479
+ }
449
480
  }
450
481
  function parseCodeTS(code) {
451
482
  ensureInit();
@@ -485,8 +516,20 @@ function childrenOfType(node, type) {
485
516
  function childOfType(node, type) {
486
517
  return node.children.find((n) => n.type === type);
487
518
  }
519
+ function emptyParsedFile(absPath) {
520
+ return {
521
+ name: absPath.split("/").pop() ?? absPath,
522
+ exports: [],
523
+ imports: [],
524
+ reExports: [],
525
+ jsxElements: /* @__PURE__ */ new Set(),
526
+ navigations: [],
527
+ fetchCalls: []
528
+ };
529
+ }
488
530
  function parseFileTS(absPath) {
489
531
  const tree = parseSource(absPath);
532
+ if (!tree) return emptyParsedFile(absPath);
490
533
  const root = tree.rootNode;
491
534
  const imports = [];
492
535
  const importStatements = childrenOfType(root, "import_statement");
@@ -681,6 +724,7 @@ function parseFileTS(absPath) {
681
724
  }
682
725
  function extractDbCallsTS(absPath) {
683
726
  const tree = parseSource(absPath);
727
+ if (!tree) return [];
684
728
  const root = tree.rootNode;
685
729
  const dbQuery = getQuery("db-calls");
686
730
  const matches = dbQuery.matches(root);
@@ -741,7 +785,9 @@ function classifyFile(absPath) {
741
785
  const fileName = require("path").basename(absPath);
742
786
  if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("__test")) return "test";
743
787
  if (fileName.includes(".stories.")) return "story";
788
+ if (fileName === "middleware.ts" || fileName === "middleware.tsx") return "middleware";
744
789
  const tree = parseSource(absPath);
790
+ if (!tree) return "util";
745
791
  const root = tree.rootNode;
746
792
  const classifyQuery = getQuery("classify");
747
793
  const captures = classifyQuery.captures(root);
@@ -760,6 +806,7 @@ function classifyFile(absPath) {
760
806
  }
761
807
  function extractAuthWrappersTS(absPath) {
762
808
  const tree = parseSource(absPath);
809
+ if (!tree) return /* @__PURE__ */ new Set();
763
810
  const root = tree.rootNode;
764
811
  const wrapperQuery = getQuery("wrappers");
765
812
  const matches = wrapperQuery.matches(root);
@@ -802,11 +849,354 @@ function extractAuthWrappersTS(absPath) {
802
849
  }
803
850
  return wrappers;
804
851
  }
852
+ function inferIntentFromName(name) {
853
+ if (TRUST_AS_PROTECT_KEYS.has(name)) {
854
+ return { intent: "protect", hint: `Next.js config.${name} \u2014 paths matched run through middleware` };
855
+ }
856
+ for (const re of EXEMPT_NAME_PATTERNS) {
857
+ if (re.test(name)) return { intent: "exempt", hint: `name "${name}" matches /${re.source}/` };
858
+ }
859
+ for (const re of PROTECT_NAME_PATTERNS) {
860
+ if (re.test(name)) return { intent: "protect", hint: `name "${name}" matches /${re.source}/` };
861
+ }
862
+ return { intent: "ambiguous", hint: `name "${name}" has no exempt/protect signal` };
863
+ }
864
+ function looksLikeRoutePattern(s) {
865
+ return s.startsWith("/") && !s.startsWith("//");
866
+ }
867
+ function collectStringsFromArray(arrNode) {
868
+ const out = [];
869
+ for (const child of arrNode.children) {
870
+ if (child.type !== "string") continue;
871
+ const frag = childOfType(child, "string_fragment");
872
+ if (frag) out.push(frag.text);
873
+ }
874
+ return out;
875
+ }
876
+ function findArrayInValue(valueNode) {
877
+ if (!valueNode) return null;
878
+ if (valueNode.type === "array") return valueNode;
879
+ if (valueNode.type === "call_expression") {
880
+ const args = childOfType(valueNode, "arguments");
881
+ if (!args) return null;
882
+ for (const c of args.children) {
883
+ if (c.type === "array") return c;
884
+ }
885
+ }
886
+ return null;
887
+ }
888
+ function extractMatcherFromDeclarator(decl) {
889
+ const nameNode = childOfType(decl, "identifier");
890
+ if (!nameNode) return null;
891
+ let valueNode;
892
+ for (const c of decl.children) {
893
+ if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
894
+ valueNode = c;
895
+ }
896
+ const arr = findArrayInValue(valueNode);
897
+ if (!arr) return null;
898
+ const strings = collectStringsFromArray(arr);
899
+ const routes = strings.filter(looksLikeRoutePattern);
900
+ if (routes.length === 0) return null;
901
+ const { intent, hint } = inferIntentFromName(nameNode.text);
902
+ return { name: nameNode.text, patterns: routes, intent, hint };
903
+ }
904
+ function extractMatchersFromObject(objNode) {
905
+ const out = [];
906
+ for (const pair of childrenOfType(objNode, "pair")) {
907
+ const key = childOfType(pair, "property_identifier");
908
+ if (!key) continue;
909
+ const arr = pair.children.find((c) => c.type === "array");
910
+ if (!arr) continue;
911
+ const routes = collectStringsFromArray(arr).filter(looksLikeRoutePattern);
912
+ if (routes.length === 0) continue;
913
+ const { intent, hint } = inferIntentFromName(key.text);
914
+ out.push({ name: key.text, patterns: routes, intent, hint });
915
+ }
916
+ return out;
917
+ }
918
+ function detectFallthroughProtect(root) {
919
+ const text = root.text;
920
+ const signals = [
921
+ /\bauth\.protect\s*\(/,
922
+ /\bauth\(\)\.protect\s*\(/,
923
+ /\bredirect\s*\(\s*['"`]\/(sign-?in|log-?in|auth)/i,
924
+ /\bNextResponse\.redirect\s*\(/,
925
+ /\bthrow\s+new\s+\w*Unauthorized/i
926
+ ];
927
+ return signals.some((re) => re.test(text));
928
+ }
929
+ function extractMiddlewareAuthTS(absPath) {
930
+ if (!require("node:fs").existsSync(absPath)) return null;
931
+ const tree = parseSource(absPath);
932
+ if (!tree) return null;
933
+ const root = tree.rootNode;
934
+ const matchers = [];
935
+ for (const stmt of root.children) {
936
+ if (stmt.type !== "lexical_declaration" && stmt.type !== "variable_declaration") continue;
937
+ for (const decl of childrenOfType(stmt, "variable_declarator")) {
938
+ const m = extractMatcherFromDeclarator(decl);
939
+ if (m) matchers.push(m);
940
+ let valueNode;
941
+ for (const c of decl.children) {
942
+ if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
943
+ valueNode = c;
944
+ }
945
+ if (valueNode?.type === "object") {
946
+ for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
947
+ }
948
+ }
949
+ }
950
+ for (const stmt of root.children) {
951
+ if (stmt.type !== "export_statement") continue;
952
+ const decl = childOfType(stmt, "lexical_declaration") ?? childOfType(stmt, "variable_declaration");
953
+ if (!decl) continue;
954
+ for (const d of childrenOfType(decl, "variable_declarator")) {
955
+ let valueNode;
956
+ for (const c of d.children) {
957
+ if (c.type === "identifier" || c.type === "=" || c.type === "type_annotation") continue;
958
+ valueNode = c;
959
+ }
960
+ if (valueNode?.type === "object") {
961
+ for (const inner of extractMatchersFromObject(valueNode)) matchers.push(inner);
962
+ }
963
+ const m = extractMatcherFromDeclarator(d);
964
+ if (m && !matchers.some((x) => x.name === m.name && x.patterns.join() === m.patterns.join())) {
965
+ matchers.push(m);
966
+ }
967
+ }
968
+ }
969
+ return {
970
+ file: absPath,
971
+ matchers,
972
+ hasFallthroughProtect: detectFallthroughProtect(root)
973
+ };
974
+ }
975
+ function middlewarePatternToRegex(pattern) {
976
+ const negLookaheadMatch = /^(\/?)\(\(\?\!([^)]+)\)(\.\*|\.\+)\)$/.exec(pattern);
977
+ if (negLookaheadMatch) {
978
+ const [, lead, altsRaw, body] = negLookaheadMatch;
979
+ const escapedAlts = altsRaw.split("|").map((alt) => alt.trim().replace(/[.+*?^${}()|[\]\\]/g, "\\$&")).join("|");
980
+ try {
981
+ return new RegExp(`^${lead}(?!${escapedAlts})${body}$`);
982
+ } catch {
983
+ return null;
984
+ }
985
+ }
986
+ if (/\(\?\!/.test(pattern)) return null;
987
+ if (pattern.startsWith("(")) return null;
988
+ let src = "^";
989
+ let i = 0;
990
+ while (i < pattern.length) {
991
+ const ch = pattern[i];
992
+ if (ch === "(" && pattern.slice(i, i + 4) === "(.*)") {
993
+ src += ".*";
994
+ i += 4;
995
+ continue;
996
+ }
997
+ if (ch === ":") {
998
+ i++;
999
+ while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
1000
+ src += "[^/]+";
1001
+ continue;
1002
+ }
1003
+ if (ch === "*") {
1004
+ i++;
1005
+ while (i < pattern.length && /[a-zA-Z0-9_]/.test(pattern[i])) i++;
1006
+ if (pattern[i] === "?") {
1007
+ i++;
1008
+ src += ".*";
1009
+ continue;
1010
+ }
1011
+ src += ".+";
1012
+ continue;
1013
+ }
1014
+ if (/[.\\+?^${}()|[\]]/.test(ch)) {
1015
+ src += "\\" + ch;
1016
+ i++;
1017
+ continue;
1018
+ }
1019
+ src += ch;
1020
+ i++;
1021
+ }
1022
+ src += "$";
1023
+ try {
1024
+ return new RegExp(src);
1025
+ } catch {
1026
+ return null;
1027
+ }
1028
+ }
1029
+ function classifyRouteAgainstMiddleware(routePath, info) {
1030
+ for (const m of info.matchers) {
1031
+ if (m.intent === "ambiguous") continue;
1032
+ for (const pat of m.patterns) {
1033
+ const re = middlewarePatternToRegex(pat);
1034
+ if (!re) continue;
1035
+ if (re.test(routePath)) return { intent: m.intent, matcher: m.name, pattern: pat };
1036
+ }
1037
+ }
1038
+ return null;
1039
+ }
805
1040
  function trunc(s, max = 120) {
806
1041
  return s.length > max ? s.slice(0, max) + "..." : s;
807
1042
  }
1043
+ function dedup(arr) {
1044
+ return Array.from(new Set(arr));
1045
+ }
1046
+ function firstStringArg(callExpr) {
1047
+ const args = callExpr.childForFieldName("arguments");
1048
+ if (!args) return null;
1049
+ for (const c of args.namedChildren) {
1050
+ if (c.type === "string") {
1051
+ const frag = c.namedChildren.find((cc) => cc.type === "string_fragment");
1052
+ return frag ? frag.text : c.text.replace(/^['"`]|['"`]$/g, "");
1053
+ }
1054
+ if (c.type === "template_string") {
1055
+ return c.text.replace(/^`|`$/g, "");
1056
+ }
1057
+ }
1058
+ return null;
1059
+ }
1060
+ function isFunctionLike(node) {
1061
+ return node.type === "arrow_function" || node.type === "function_expression" || node.type === "function_declaration" || node.type === "method_definition" || node.type === "generator_function" || node.type === "generator_function_declaration";
1062
+ }
1063
+ function stringLiteralValue(n) {
1064
+ if (n.type !== "string") return null;
1065
+ const frag = n.namedChildren.find((c) => c.type === "string_fragment");
1066
+ if (frag) return frag.text;
1067
+ return n.text.replace(/^['"`]|['"`]$/g, "");
1068
+ }
1069
+ function nthStringArg(callExpr, idx) {
1070
+ const args = callExpr.childForFieldName("arguments");
1071
+ if (!args) return null;
1072
+ const child = args.namedChildren[idx];
1073
+ return child ? stringLiteralValue(child) : null;
1074
+ }
1075
+ function objectArgIdValues(callExpr) {
1076
+ const args = callExpr.childForFieldName("arguments");
1077
+ if (!args) return [];
1078
+ const out = [];
1079
+ for (const arg of args.namedChildren) {
1080
+ if (arg.type !== "object") continue;
1081
+ for (const pair of arg.namedChildren) {
1082
+ if (pair.type !== "pair") continue;
1083
+ const key = pair.childForFieldName("key");
1084
+ const val = pair.childForFieldName("value");
1085
+ if (!key || !val) continue;
1086
+ const keyText = key.type === "property_identifier" ? key.text : stringLiteralValue(key) ?? key.text;
1087
+ if (keyText !== "id") continue;
1088
+ const strVal = stringLiteralValue(val);
1089
+ if (strVal) out.push(strVal);
1090
+ }
1091
+ }
1092
+ return out;
1093
+ }
1094
+ function extractEffects(node, moduleOnly = false) {
1095
+ const calls = [];
1096
+ const dom_writes = [];
1097
+ const subscribes = [];
1098
+ const timers = [];
1099
+ const persists = [];
1100
+ const fetches = [];
1101
+ const globals = [];
1102
+ const dom_ids = [];
1103
+ const window_events = [];
1104
+ const storage_keys = [];
1105
+ const fetch_urls = [];
1106
+ function visit(n, depth) {
1107
+ if (moduleOnly && depth > 0 && isFunctionLike(n)) return;
1108
+ if (n.type === "call_expression") {
1109
+ const fn = n.childForFieldName("function");
1110
+ if (fn) {
1111
+ const fnText = fn.text;
1112
+ calls.push(fnText);
1113
+ for (const id of objectArgIdValues(n)) dom_ids.push(id);
1114
+ if (fn.type === "identifier") {
1115
+ if (TIMER_FNS.has(fnText)) timers.push(fnText);
1116
+ if (fnText === "fetch") {
1117
+ const url = firstStringArg(n);
1118
+ fetches.push(url ? `fetch("${url}")` : "fetch(...)");
1119
+ if (url) fetch_urls.push(url);
1120
+ }
1121
+ } else if (fn.type === "member_expression") {
1122
+ const obj = fn.childForFieldName("object");
1123
+ const prop = fn.childForFieldName("property");
1124
+ const objText = obj?.text ?? "";
1125
+ const propText = prop?.text ?? "";
1126
+ if (STORAGE_OBJECTS.has(objText)) {
1127
+ const key = firstStringArg(n);
1128
+ persists.push(key ? `${objText}.${propText}("${key}")` : `${objText}.${propText}(\u2026)`);
1129
+ if (key) storage_keys.push(key);
1130
+ } else if (propText === "addEventListener") {
1131
+ const event = firstStringArg(n);
1132
+ subscribes.push(event ? `${objText}.addEventListener("${event}")` : `${objText}.addEventListener(\u2026)`);
1133
+ if (event && (objText === "window" || objText === "document")) {
1134
+ window_events.push(`${objText}:${event}`);
1135
+ }
1136
+ } else if (propText === "removeEventListener") {
1137
+ const event = firstStringArg(n);
1138
+ subscribes.push(event ? `${objText}.removeEventListener("${event}")` : `${objText}.removeEventListener(\u2026)`);
1139
+ } else if (propText === "subscribe" || propText === "on") {
1140
+ subscribes.push(`${objText}.${propText}(\u2026)`);
1141
+ } else if (DOM_METHOD_NAMES.has(propText)) {
1142
+ dom_writes.push(`${objText}.${propText}(\u2026)`);
1143
+ if (propText === "setAttribute" && nthStringArg(n, 0) === "id") {
1144
+ const idVal = nthStringArg(n, 1);
1145
+ if (idVal) dom_ids.push(idVal);
1146
+ }
1147
+ } else if (objText.endsWith(".classList") && CLASSLIST_METHODS.has(propText)) {
1148
+ dom_writes.push(`${objText}.${propText}(\u2026)`);
1149
+ } else if (objText === "history" && HISTORY_METHODS.has(propText)) {
1150
+ globals.push(`history.${propText}(\u2026)`);
1151
+ } else if (objText === "location" && LOCATION_METHODS.has(propText)) {
1152
+ globals.push(`location.${propText}(\u2026)`);
1153
+ } else if (objText === "window" && propText === "matchMedia") {
1154
+ globals.push("window.matchMedia(\u2026)");
1155
+ }
1156
+ }
1157
+ }
1158
+ } else if (n.type === "assignment_expression") {
1159
+ const lhs = n.children.find((c) => c.type !== "=");
1160
+ const rhs = n.childForFieldName("right");
1161
+ if (lhs?.type === "member_expression") {
1162
+ const prop = lhs.childForFieldName("property");
1163
+ const propText = prop?.text ?? "";
1164
+ const lhsText = lhs.text;
1165
+ if (lhsText.startsWith("document.cookie")) {
1166
+ persists.push("document.cookie =");
1167
+ } else if (lhsText.startsWith("document.title") || lhsText.startsWith("location.")) {
1168
+ globals.push(`${lhsText} =`);
1169
+ } else if (ASSIGN_DOM_PROPS.has(propText) || lhsText.includes(".style.") || lhsText.includes(".dataset.")) {
1170
+ dom_writes.push(`${lhsText} =`);
1171
+ if (propText === "id" && rhs) {
1172
+ const idVal = stringLiteralValue(rhs);
1173
+ if (idVal) dom_ids.push(idVal);
1174
+ }
1175
+ }
1176
+ }
1177
+ }
1178
+ for (const child of n.children) visit(child, depth + 1);
1179
+ }
1180
+ visit(node, 0);
1181
+ const out = {};
1182
+ if (calls.length) out.calls = dedup(calls);
1183
+ if (dom_writes.length) out.dom_writes = dedup(dom_writes);
1184
+ if (subscribes.length) out.subscribes = dedup(subscribes);
1185
+ if (timers.length) out.timers = dedup(timers);
1186
+ if (persists.length) out.persists = dedup(persists);
1187
+ if (fetches.length) out.fetches = dedup(fetches);
1188
+ if (globals.length) out.globals = dedup(globals);
1189
+ if (dom_ids.length) out.dom_ids = dedup(dom_ids);
1190
+ if (window_events.length) out.window_events = dedup(window_events);
1191
+ if (storage_keys.length) out.storage_keys = dedup(storage_keys);
1192
+ if (fetch_urls.length) out.fetch_urls = dedup(fetch_urls);
1193
+ return out;
1194
+ }
808
1195
  function extractDeep(absPath) {
809
1196
  const tree = parseSource(absPath);
1197
+ if (!tree) {
1198
+ return { elements: [], stateVars: [], conditions: [], variables: [], responses: [], params: [] };
1199
+ }
810
1200
  const root = tree.rootNode;
811
1201
  const elements = [];
812
1202
  const elQuery = getQuery("deep/jsx-semantic");
@@ -899,12 +1289,18 @@ function extractDeep(absPath) {
899
1289
  const caps = captureMap(m);
900
1290
  const declNode = m.captures.find((c) => c.name === "var.decl" || c.name === "var.destructured" || c.name === "var.array")?.node;
901
1291
  const kind = declNode?.children.find((n) => n.type === "const" || n.type === "let" || n.type === "var")?.type ?? "const";
1292
+ const initNode = m.captures.find((c) => c.name === "var.init")?.node;
902
1293
  if (caps["var.name"] && caps["var.init"]) {
903
- variables.push({
1294
+ const variable = {
904
1295
  name: caps["var.name"],
905
1296
  kind,
906
1297
  init: trunc(caps["var.init"], 100)
907
- });
1298
+ };
1299
+ if (initNode && isFunctionLike(initNode)) {
1300
+ const eff = extractEffects(initNode);
1301
+ if (Object.keys(eff).length > 0) variable.effects = eff;
1302
+ }
1303
+ variables.push(variable);
908
1304
  }
909
1305
  if (caps["var.destructured.obj"]) {
910
1306
  variables.push({
@@ -956,9 +1352,23 @@ function extractDeep(absPath) {
956
1352
  params.push({ name: caps["param.body"], source: "body" });
957
1353
  }
958
1354
  }
959
- return { elements, stateVars, conditions, variables, responses, params };
1355
+ const fileEffects = extractEffects(
1356
+ root,
1357
+ /* moduleOnly */
1358
+ false
1359
+ );
1360
+ const hasEffects = Object.keys(fileEffects).length > 0;
1361
+ return {
1362
+ elements,
1363
+ stateVars,
1364
+ conditions,
1365
+ variables,
1366
+ responses,
1367
+ params,
1368
+ ...hasEffects ? { effects: fileEffects } : {}
1369
+ };
960
1370
  }
961
- var import_node_fs5, import_node_path5, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, SUPABASE_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods, INLINE_AUTH_IMPORTS;
1371
+ var import_node_fs5, import_node_path5, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, MAX_PARSEABLE_BYTES, MAX_CONSECUTIVE_PARSE_FAILURES, consecutiveParseFailures, ParseCascadeError, PRISMA_MUTATION_METHODS_BUILTIN, SUPABASE_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods, INLINE_AUTH_IMPORTS, EXEMPT_NAME_PATTERNS, PROTECT_NAME_PATTERNS, TRUST_AS_PROTECT_KEYS, TIMER_FNS, DOM_METHOD_NAMES, CLASSLIST_METHODS, STORAGE_OBJECTS, HISTORY_METHODS, LOCATION_METHODS, ASSIGN_DOM_PROPS;
962
1372
  var init_ts_extractor = __esm({
963
1373
  "src/server/graph/core/ts-extractor.ts"() {
964
1374
  "use strict";
@@ -971,6 +1381,19 @@ var init_ts_extractor = __esm({
971
1381
  return (0, import_node_path5.join)((0, import_node_path5.dirname)(__filename), "graph", "queries");
972
1382
  })();
973
1383
  queryCache = /* @__PURE__ */ new Map();
1384
+ MAX_PARSEABLE_BYTES = 2 * 1024 * 1024;
1385
+ MAX_CONSECUTIVE_PARSE_FAILURES = 10;
1386
+ consecutiveParseFailures = 0;
1387
+ ParseCascadeError = class extends Error {
1388
+ constructor(lastPath, failureCount) {
1389
+ super(
1390
+ `tree-sitter parse cascade: ${failureCount} consecutive WASM failures (last file: ${lastPath}). The shared Parser's WASM heap is likely corrupted; aborting regen so the graph isn't silently degraded. Restart the chart server to reinitialize, then re-run generate_graph.`
1391
+ );
1392
+ this.lastPath = lastPath;
1393
+ this.failureCount = failureCount;
1394
+ this.name = "ParseCascadeError";
1395
+ }
1396
+ };
974
1397
  PRISMA_MUTATION_METHODS_BUILTIN = [
975
1398
  "create",
976
1399
  "createMany",
@@ -997,6 +1420,72 @@ var init_ts_extractor = __esm({
997
1420
  { module: /^@auth\//, helpers: ["auth"] },
998
1421
  { module: /^@supabase\/auth-helpers/, helpers: ["createServerClient"] }
999
1422
  ];
1423
+ EXEMPT_NAME_PATTERNS = [
1424
+ /^is_?public/i,
1425
+ /^public_?routes?/i,
1426
+ /^public_?paths?/i,
1427
+ /^whitelist/i,
1428
+ /^allowlist/i,
1429
+ /^unauthenticated/i,
1430
+ /^anonymous/i,
1431
+ /^guest/i,
1432
+ /^skip_?auth/i,
1433
+ /^bypass/i
1434
+ ];
1435
+ PROTECT_NAME_PATTERNS = [
1436
+ /^is_?protected/i,
1437
+ /^protected_?routes?/i,
1438
+ /^protected_?paths?/i,
1439
+ /^require_?auth/i,
1440
+ /^auth_?required/i,
1441
+ /^private_?routes?/i,
1442
+ /^is_?admin/i,
1443
+ /^admin_?routes?/i,
1444
+ /^secured/i
1445
+ ];
1446
+ TRUST_AS_PROTECT_KEYS = /* @__PURE__ */ new Set(["matcher", "matchers"]);
1447
+ TIMER_FNS = /* @__PURE__ */ new Set([
1448
+ "setInterval",
1449
+ "setTimeout",
1450
+ "clearInterval",
1451
+ "clearTimeout",
1452
+ "requestAnimationFrame",
1453
+ "cancelAnimationFrame",
1454
+ "queueMicrotask"
1455
+ ]);
1456
+ DOM_METHOD_NAMES = /* @__PURE__ */ new Set([
1457
+ "setAttribute",
1458
+ "removeAttribute",
1459
+ "appendChild",
1460
+ "removeChild",
1461
+ "replaceChildren",
1462
+ "replaceWith",
1463
+ "insertBefore",
1464
+ "append",
1465
+ "prepend",
1466
+ "remove",
1467
+ "before",
1468
+ "after",
1469
+ "insertAdjacentHTML",
1470
+ "insertAdjacentElement"
1471
+ ]);
1472
+ CLASSLIST_METHODS = /* @__PURE__ */ new Set(["add", "remove", "toggle", "replace"]);
1473
+ STORAGE_OBJECTS = /* @__PURE__ */ new Set(["localStorage", "sessionStorage"]);
1474
+ HISTORY_METHODS = /* @__PURE__ */ new Set(["pushState", "replaceState", "back", "forward", "go"]);
1475
+ LOCATION_METHODS = /* @__PURE__ */ new Set(["assign", "replace", "reload"]);
1476
+ ASSIGN_DOM_PROPS = /* @__PURE__ */ new Set([
1477
+ "textContent",
1478
+ "innerHTML",
1479
+ "innerText",
1480
+ "value",
1481
+ "src",
1482
+ "href",
1483
+ "className",
1484
+ "id",
1485
+ "checked",
1486
+ "selected",
1487
+ "disabled"
1488
+ ]);
1000
1489
  }
1001
1490
  });
1002
1491
 
@@ -1082,6 +1571,8 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
1082
1571
  function classifyType(absPath, id) {
1083
1572
  const contentType = classifyFile(absPath);
1084
1573
  if (contentType === "lib" && id.startsWith("server/mcp/")) return "mcp-tool";
1574
+ if (/^app\/(.+\/)?page\.tsx$/.test(id)) return "page";
1575
+ if (/^app\/(.+\/)?layout\.tsx$/.test(id)) return "layout";
1085
1576
  return contentType;
1086
1577
  }
1087
1578
  function extractRoute(id) {
@@ -1392,6 +1883,7 @@ function generate(rootDir) {
1392
1883
  variables: deep.variables,
1393
1884
  responses: deep.responses,
1394
1885
  params: deep.params,
1886
+ ...deep.effects ? { effects: deep.effects } : {},
1395
1887
  _dbCalls: dbCalls
1396
1888
  // temp: used for cross-ref building below
1397
1889
  });
@@ -1411,6 +1903,7 @@ function generate(rootDir) {
1411
1903
  stateVars: deep.stateVars,
1412
1904
  conditions: deep.conditions,
1413
1905
  variables: deep.variables,
1906
+ ...deep.effects ? { effects: deep.effects } : {},
1414
1907
  ...authWrappers.length > 0 ? { auth: authWrappers } : {},
1415
1908
  ...dbCalls.length > 0 ? { _dbCalls: dbCalls } : {}
1416
1909
  });
@@ -1540,43 +2033,169 @@ function generate(rootDir) {
1540
2033
  nodeIdSet.add(externalId);
1541
2034
  uiEdges.push(...edgesFromThis);
1542
2035
  }
2036
+ const tablesByFile = /* @__PURE__ */ new Map();
2037
+ const allDbNodes = [...apiNodes, ...uiNodes];
2038
+ for (const node of allDbNodes) {
2039
+ const calls = node._dbCalls;
2040
+ if (!calls || calls.length === 0) continue;
2041
+ const map = /* @__PURE__ */ new Map();
2042
+ for (const c of calls) {
2043
+ const key = `${c.kind}:${c.model}:${c.isMutation ? "m" : "r"}`;
2044
+ if (!map.has(key)) {
2045
+ map.set(key, { model: c.model, method: c.method, isMutation: c.isMutation, kind: c.kind, via: [] });
2046
+ }
2047
+ }
2048
+ tablesByFile.set(node.id, map);
2049
+ }
2050
+ const reverseRuntimeImports = /* @__PURE__ */ new Map();
2051
+ for (const edge of uiEdges) {
2052
+ if (edge.type !== "imports" && edge.type !== "renders") continue;
2053
+ if (!reverseRuntimeImports.has(edge.target)) {
2054
+ reverseRuntimeImports.set(edge.target, /* @__PURE__ */ new Set());
2055
+ }
2056
+ reverseRuntimeImports.get(edge.target).add(edge.source);
2057
+ }
2058
+ let changed = true;
2059
+ let iterations = 0;
2060
+ while (changed && iterations < 50) {
2061
+ changed = false;
2062
+ iterations++;
2063
+ for (const [target, tableMap] of [...tablesByFile]) {
2064
+ const importers = reverseRuntimeImports.get(target);
2065
+ if (!importers) continue;
2066
+ for (const importer of importers) {
2067
+ if (importer === target) continue;
2068
+ let importerMap = tablesByFile.get(importer);
2069
+ if (!importerMap) {
2070
+ importerMap = /* @__PURE__ */ new Map();
2071
+ tablesByFile.set(importer, importerMap);
2072
+ }
2073
+ for (const [key, call] of tableMap) {
2074
+ if (importerMap.has(key)) continue;
2075
+ importerMap.set(key, {
2076
+ model: call.model,
2077
+ method: null,
2078
+ isMutation: call.isMutation,
2079
+ kind: call.kind,
2080
+ via: [...call.via, target]
2081
+ });
2082
+ changed = true;
2083
+ }
2084
+ }
2085
+ }
2086
+ }
2087
+ for (const node of apiNodes) {
2088
+ const map = tablesByFile.get(node.id);
2089
+ if (!map) continue;
2090
+ node.db_models = [...new Set([...map.values()].map((c) => c.model))];
2091
+ node.db_operations = [...new Set(
2092
+ [...map.values()].filter((c) => c.via.length === 0 && c.method).map((c) => `${c.model}.${c.method}`)
2093
+ )];
2094
+ node.mutates = [...map.values()].some((c) => c.isMutation);
2095
+ }
1543
2096
  const apiCrossRefs = [];
1544
2097
  for (const node of apiNodes) {
1545
- const dbCalls = node._dbCalls;
1546
- if (!dbCalls) continue;
1547
- const seenModels = /* @__PURE__ */ new Set();
1548
- for (const call of dbCalls) {
2098
+ const map = tablesByFile.get(node.id);
2099
+ if (!map) {
2100
+ delete node._dbCalls;
2101
+ continue;
2102
+ }
2103
+ const seenTargets = /* @__PURE__ */ new Set();
2104
+ for (const call of map.values()) {
1549
2105
  const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
1550
- if (seenModels.has(target)) continue;
1551
- seenModels.add(target);
1552
- apiCrossRefs.push({
2106
+ if (seenTargets.has(target)) continue;
2107
+ seenTargets.add(target);
2108
+ const isTransitive = call.via.length > 0;
2109
+ const ref = {
1553
2110
  source: node.id,
1554
2111
  target,
1555
- type: call.isMutation ? "mutates" : "reads",
2112
+ type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
1556
2113
  layer: "db"
1557
- });
2114
+ };
2115
+ if (isTransitive) ref.via = call.via;
2116
+ apiCrossRefs.push(ref);
1558
2117
  }
1559
2118
  delete node._dbCalls;
1560
2119
  }
1561
2120
  const uiCrossRefs = [];
1562
2121
  for (const node of uiNodes) {
1563
- const dbCalls = node._dbCalls;
1564
- if (!dbCalls) continue;
1565
- const seenModels = /* @__PURE__ */ new Set();
1566
- for (const call of dbCalls) {
2122
+ const map = tablesByFile.get(node.id);
2123
+ if (!map) {
2124
+ if (node._dbCalls) delete node._dbCalls;
2125
+ continue;
2126
+ }
2127
+ const seenTargets = /* @__PURE__ */ new Set();
2128
+ for (const call of map.values()) {
1567
2129
  const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
1568
- if (seenModels.has(target)) continue;
1569
- seenModels.add(target);
1570
- uiCrossRefs.push({
2130
+ if (seenTargets.has(target)) continue;
2131
+ seenTargets.add(target);
2132
+ const isTransitive = call.via.length > 0;
2133
+ const ref = {
1571
2134
  source: node.id,
1572
2135
  target,
1573
- type: call.isMutation ? "mutates" : "reads",
2136
+ type: call.isMutation ? isTransitive ? "mutates_via" : "mutates" : isTransitive ? "reads_via" : "reads",
1574
2137
  layer: "db"
1575
- });
2138
+ };
2139
+ if (isTransitive) ref.via = call.via;
2140
+ uiCrossRefs.push(ref);
1576
2141
  }
1577
- delete node._dbCalls;
2142
+ if (node._dbCalls) delete node._dbCalls;
1578
2143
  }
1579
2144
  uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
2145
+ const middlewareInfos = [];
2146
+ for (const conv of paths.conventionFiles) {
2147
+ if (!/middleware\.tsx?$/.test(conv)) continue;
2148
+ try {
2149
+ const info = extractMiddlewareAuthTS(conv);
2150
+ if (info && info.matchers.length > 0) middlewareInfos.push(info);
2151
+ } catch {
2152
+ }
2153
+ }
2154
+ if (middlewareInfos.length > 0) {
2155
+ let setAuth2 = function(node, newTags, replaceAll) {
2156
+ const existing = node.auth ?? [];
2157
+ const meaningful = existing.filter((a) => a !== "public");
2158
+ const merged = replaceAll ? newTags : [.../* @__PURE__ */ new Set([...newTags, ...meaningful])];
2159
+ node.auth = merged.length > 0 ? merged : ["public"];
2160
+ }, applyMiddleware2 = function(node, routePath) {
2161
+ let resolved = null;
2162
+ let label = "";
2163
+ let hasAnyExemptMatcher = false;
2164
+ let hasAnyFallthrough = false;
2165
+ for (const info of middlewareInfos) {
2166
+ if (info.hasFallthroughProtect) hasAnyFallthrough = true;
2167
+ if (info.matchers.some((m) => m.intent === "exempt")) hasAnyExemptMatcher = true;
2168
+ const c = classifyRouteAgainstMiddleware(routePath, info);
2169
+ if (!c) continue;
2170
+ if (!resolved) {
2171
+ resolved = c.intent;
2172
+ label = c.matcher;
2173
+ }
2174
+ }
2175
+ if (resolved === "exempt") {
2176
+ setAuth2(node, ["public"], true);
2177
+ return;
2178
+ }
2179
+ if (resolved === "protect") {
2180
+ setAuth2(node, [`middleware:${label}`], false);
2181
+ return;
2182
+ }
2183
+ if (hasAnyExemptMatcher && hasAnyFallthrough) {
2184
+ setAuth2(node, ["middleware-protected"], false);
2185
+ }
2186
+ };
2187
+ var setAuth = setAuth2, applyMiddleware = applyMiddleware2;
2188
+ for (const node of apiNodes) {
2189
+ const routePath = node.path;
2190
+ if (!routePath) continue;
2191
+ applyMiddleware2(node, routePath);
2192
+ }
2193
+ for (const node of uiNodes) {
2194
+ const route = node.route;
2195
+ if (!route) continue;
2196
+ applyMiddleware2(node, route);
2197
+ }
2198
+ }
1580
2199
  const apiNodeIds = new Set(apiNodes.map((n) => n.id));
1581
2200
  const apiEdges = [];
1582
2201
  const uiOnlyEdges = [];
@@ -1732,6 +2351,7 @@ var init_typescript_project = __esm({
1732
2351
  config: "ui",
1733
2352
  lib: "ui",
1734
2353
  "mcp-tool": "ui",
2354
+ middleware: "ui",
1735
2355
  external: "ui"
1736
2356
  };
1737
2357
  typescriptProjectParser = {
@@ -3641,6 +4261,119 @@ var init_static_ref_scanner = __esm({
3641
4261
  }
3642
4262
  });
3643
4263
 
4264
+ // src/server/graph/parsers/crosslayer/middleware-gates.ts
4265
+ function toNodeId4(srcDir, rootDir, absPath) {
4266
+ const relFromSrc = (0, import_node_path12.relative)(srcDir, absPath).replace(/\\/g, "/");
4267
+ if (relFromSrc.startsWith("..")) {
4268
+ return (0, import_node_path12.relative)(rootDir, absPath).replace(/\\/g, "/");
4269
+ }
4270
+ return relFromSrc;
4271
+ }
4272
+ function collectTargets(apiOutput, uiOutput) {
4273
+ const out = [];
4274
+ for (const n of apiOutput?.nodes ?? []) {
4275
+ if (n.type !== "endpoint") continue;
4276
+ const path3 = n.path;
4277
+ if (typeof path3 !== "string" || !path3) continue;
4278
+ out.push({ id: n.id, route: path3, layer: "api" });
4279
+ }
4280
+ for (const n of uiOutput?.nodes ?? []) {
4281
+ if (n.type !== "page") continue;
4282
+ const route = n.route;
4283
+ if (typeof route !== "string" || !route) continue;
4284
+ out.push({ id: n.id, route, layer: "ui" });
4285
+ }
4286
+ return out;
4287
+ }
4288
+ var import_node_path12, middlewareGatesParser;
4289
+ var init_middleware_gates = __esm({
4290
+ "src/server/graph/parsers/crosslayer/middleware-gates.ts"() {
4291
+ "use strict";
4292
+ import_node_path12 = require("node:path");
4293
+ init_ts_extractor();
4294
+ init_config();
4295
+ init_resolve_paths();
4296
+ middlewareGatesParser = {
4297
+ id: "middleware-gates",
4298
+ layer: "crosslayer",
4299
+ concern: "auth-gate",
4300
+ detect(rootDir) {
4301
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
4302
+ if (!paths) return false;
4303
+ return paths.conventionFiles.some((f) => /middleware\.tsx?$/.test(f));
4304
+ },
4305
+ generate(rootDir, layerOutputs) {
4306
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
4307
+ if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
4308
+ const middlewareFiles = paths.conventionFiles.filter((f) => /middleware\.tsx?$/.test(f));
4309
+ if (middlewareFiles.length === 0) {
4310
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
4311
+ }
4312
+ const apiOutput = layerOutputs.get("api");
4313
+ const uiOutput = layerOutputs.get("ui");
4314
+ const targets = collectTargets(apiOutput, uiOutput);
4315
+ const crossRefs = [];
4316
+ const flagged = [];
4317
+ const seenEdge = /* @__PURE__ */ new Set();
4318
+ const seenUnparseable = /* @__PURE__ */ new Set();
4319
+ let protectMatcherCount = 0;
4320
+ for (const file of middlewareFiles) {
4321
+ let info = null;
4322
+ try {
4323
+ info = extractMiddlewareAuthTS(file);
4324
+ } catch {
4325
+ }
4326
+ if (!info || info.matchers.length === 0) continue;
4327
+ const middlewareId = toNodeId4(paths.srcDir, rootDir, file);
4328
+ for (const matcher of info.matchers) {
4329
+ if (matcher.intent !== "protect") continue;
4330
+ protectMatcherCount += 1;
4331
+ for (const pattern of matcher.patterns) {
4332
+ const re = middlewarePatternToRegex(pattern);
4333
+ if (!re) {
4334
+ const key = `${middlewareId}|${pattern}`;
4335
+ if (seenUnparseable.has(key)) continue;
4336
+ seenUnparseable.add(key);
4337
+ flagged.push({
4338
+ source: middlewareId,
4339
+ target: "UNRESOLVED",
4340
+ type: "middleware_pattern_unparseable",
4341
+ label: `pattern "${pattern}" in matcher "${matcher.name}" \u2014 coverage unknown`,
4342
+ confidence: "high"
4343
+ });
4344
+ continue;
4345
+ }
4346
+ for (const target of targets) {
4347
+ if (!re.test(target.route)) continue;
4348
+ const key = `${middlewareId}|${target.id}|protects`;
4349
+ if (seenEdge.has(key)) continue;
4350
+ seenEdge.add(key);
4351
+ crossRefs.push({
4352
+ source: middlewareId,
4353
+ target: target.id,
4354
+ type: "protects",
4355
+ layer: target.layer
4356
+ });
4357
+ }
4358
+ }
4359
+ }
4360
+ }
4361
+ return {
4362
+ cross_refs: crossRefs,
4363
+ flagged_edges: flagged,
4364
+ warnings: [],
4365
+ patterns: {
4366
+ middleware_files: middlewareFiles.length,
4367
+ protect_matchers: protectMatcherCount,
4368
+ protects_edges: crossRefs.length,
4369
+ unparseable_patterns: flagged.length
4370
+ }
4371
+ };
4372
+ }
4373
+ };
4374
+ }
4375
+ });
4376
+
3644
4377
  // src/server/graph/core/parser-registry.ts
3645
4378
  function isMultiLayerParser(p) {
3646
4379
  return "layers" in p && Array.isArray(p.layers);
@@ -3654,7 +4387,8 @@ function registerBuiltins(registry, disabled) {
3654
4387
  fetchResolverParser,
3655
4388
  apiAnnotationsParser,
3656
4389
  urlLiteralScannerParser,
3657
- staticRefScannerParser
4390
+ staticRefScannerParser,
4391
+ middlewareGatesParser
3658
4392
  ];
3659
4393
  for (const parser of builtins) {
3660
4394
  if (disabled.has(parser.id)) continue;
@@ -3664,7 +4398,7 @@ function registerBuiltins(registry, disabled) {
3664
4398
  function loadCustomParsers(registry, config, rootDir, disabled) {
3665
4399
  for (const entry of config.parsers?.custom ?? []) {
3666
4400
  try {
3667
- const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
4401
+ const absPath = (0, import_node_path13.resolve)(rootDir, entry.path);
3668
4402
  const mod = require(absPath);
3669
4403
  const parser = "default" in mod ? mod.default : mod;
3670
4404
  if (disabled.has(parser.id)) continue;
@@ -3691,11 +4425,11 @@ function createRegistry(config, rootDir) {
3691
4425
  loadCustomParsers(registry, config, rootDir, disabled);
3692
4426
  return registry;
3693
4427
  }
3694
- var import_node_path12, ParserRegistry;
4428
+ var import_node_path13, ParserRegistry;
3695
4429
  var init_parser_registry = __esm({
3696
4430
  "src/server/graph/core/parser-registry.ts"() {
3697
4431
  "use strict";
3698
- import_node_path12 = require("node:path");
4432
+ import_node_path13 = require("node:path");
3699
4433
  init_typescript_project();
3700
4434
  init_prisma_schema();
3701
4435
  init_sql_migrations();
@@ -3704,6 +4438,7 @@ var init_parser_registry = __esm({
3704
4438
  init_url_literal_scanner();
3705
4439
  init_static_values();
3706
4440
  init_static_ref_scanner();
4441
+ init_middleware_gates();
3707
4442
  ParserRegistry = class {
3708
4443
  constructor() {
3709
4444
  this.singleLayerParsers = /* @__PURE__ */ new Map();
@@ -3866,7 +4601,7 @@ var init_merge = __esm({
3866
4601
 
3867
4602
  // src/server/graph/core/graph-builder.ts
3868
4603
  function readGraphFromDisk(rootDir, layer) {
3869
- const filePath = (0, import_node_path13.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
4604
+ const filePath = (0, import_node_path14.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
3870
4605
  if (!(0, import_node_fs13.existsSync)(filePath)) return null;
3871
4606
  try {
3872
4607
  return JSON.parse((0, import_node_fs13.readFileSync)(filePath, "utf-8"));
@@ -3965,12 +4700,12 @@ function generateAll(rootDir) {
3965
4700
  const extras = [...byLayer.keys()].filter((l) => !wellKnownOrder.includes(l)).sort();
3966
4701
  return [...wellKnownOrder, ...extras].map((l) => byLayer.get(l)).filter((r) => !!r);
3967
4702
  }
3968
- var import_node_fs13, import_node_path13;
4703
+ var import_node_fs13, import_node_path14;
3969
4704
  var init_graph_builder = __esm({
3970
4705
  "src/server/graph/core/graph-builder.ts"() {
3971
4706
  "use strict";
3972
4707
  import_node_fs13 = require("node:fs");
3973
- import_node_path13 = require("node:path");
4708
+ import_node_path14 = require("node:path");
3974
4709
  init_config();
3975
4710
  init_parser_registry();
3976
4711
  init_merge();
@@ -4009,13 +4744,13 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
4009
4744
  const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
4010
4745
  const searchDirs = [
4011
4746
  rootDir,
4012
- (0, import_node_path14.join)(rootDir, "src"),
4013
- (0, import_node_path14.join)(rootDir, "app"),
4014
- (0, import_node_path14.join)(rootDir, "lib")
4747
+ (0, import_node_path15.join)(rootDir, "src"),
4748
+ (0, import_node_path15.join)(rootDir, "app"),
4749
+ (0, import_node_path15.join)(rootDir, "lib")
4015
4750
  ];
4016
4751
  for (const base of searchDirs) {
4017
4752
  for (const convention of conventionDirs) {
4018
- const dir = (0, import_node_path14.join)(base, convention);
4753
+ const dir = (0, import_node_path15.join)(base, convention);
4019
4754
  if (!(0, import_node_fs14.existsSync)(dir)) continue;
4020
4755
  try {
4021
4756
  const stat = (0, import_node_fs14.statSync)(dir);
@@ -4096,12 +4831,12 @@ function extractModuleFromPath(id, extraTrivial, extraSkipSegments, extraGeneric
4096
4831
  }
4097
4832
  return "root";
4098
4833
  }
4099
- var import_node_fs14, import_node_path14, CONVENTION_DIRS_BUILTIN, GENERIC_ROLE_NAMES_BUILTIN, SKIP_SEGMENTS_BUILTIN, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
4834
+ var import_node_fs14, import_node_path15, CONVENTION_DIRS_BUILTIN, GENERIC_ROLE_NAMES_BUILTIN, SKIP_SEGMENTS_BUILTIN, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
4100
4835
  var init_module_tagger = __esm({
4101
4836
  "src/server/graph/taggers/module-tagger.ts"() {
4102
4837
  "use strict";
4103
4838
  import_node_fs14 = require("node:fs");
4104
- import_node_path14 = require("node:path");
4839
+ import_node_path15 = require("node:path");
4105
4840
  CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
4106
4841
  GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
4107
4842
  // JS/TS
@@ -4316,7 +5051,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
4316
5051
  for (const entry of config.taggers?.custom ?? []) {
4317
5052
  if (disabled.has(entry.id)) continue;
4318
5053
  try {
4319
- const absPath = (0, import_node_path15.resolve)(rootDir, entry.path);
5054
+ const absPath = (0, import_node_path16.resolve)(rootDir, entry.path);
4320
5055
  const mod = require(absPath);
4321
5056
  const tagger = "default" in mod ? mod.default : mod;
4322
5057
  const override = config.taggers?.trackUntagged?.[tagger.id];
@@ -4337,11 +5072,11 @@ function createTaggerRegistry(config, rootDir) {
4337
5072
  loadCustomTaggers(registry, config, rootDir, disabled);
4338
5073
  return registry;
4339
5074
  }
4340
- var import_node_path15, TaggerRegistry, BUILTIN_TAGGERS;
5075
+ var import_node_path16, TaggerRegistry, BUILTIN_TAGGERS;
4341
5076
  var init_tagger_registry = __esm({
4342
5077
  "src/server/graph/core/tagger-registry.ts"() {
4343
5078
  "use strict";
4344
- import_node_path15 = require("node:path");
5079
+ import_node_path16 = require("node:path");
4345
5080
  init_module_tagger();
4346
5081
  init_screen_tagger();
4347
5082
  TaggerRegistry = class {
@@ -4369,7 +5104,7 @@ var init_tagger_registry = __esm({
4369
5104
 
4370
5105
  // src/server/graph/core/tag-store.ts
4371
5106
  function tagsFilePath(rootDir) {
4372
- return (0, import_node_path16.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
5107
+ return (0, import_node_path17.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
4373
5108
  }
4374
5109
  function readTagStore(rootDir) {
4375
5110
  const filePath = tagsFilePath(rootDir);
@@ -4390,7 +5125,7 @@ function readTagStore(rootDir) {
4390
5125
  }
4391
5126
  function writeTagStore(rootDir, store) {
4392
5127
  const filePath = tagsFilePath(rootDir);
4393
- const dir = (0, import_node_path16.dirname)(filePath);
5128
+ const dir = (0, import_node_path17.dirname)(filePath);
4394
5129
  (0, import_node_fs15.mkdirSync)(dir, { recursive: true });
4395
5130
  const cleaned = {};
4396
5131
  for (const [nodeId, tags] of Object.entries(store)) {
@@ -4416,36 +5151,102 @@ function removeTag(rootDir, nodeId, key) {
4416
5151
  }
4417
5152
  writeTagStore(rootDir, store);
4418
5153
  }
4419
- var import_node_fs15, import_node_path16, TAGS_FILENAME, GRAPHS_DIR, tagCache;
5154
+ var import_node_fs15, import_node_path17, TAGS_FILENAME, GRAPHS_DIR, tagCache;
4420
5155
  var init_tag_store = __esm({
4421
5156
  "src/server/graph/core/tag-store.ts"() {
4422
5157
  "use strict";
4423
5158
  import_node_fs15 = require("node:fs");
4424
- import_node_path16 = require("node:path");
5159
+ import_node_path17 = require("node:path");
4425
5160
  TAGS_FILENAME = "tags.json";
4426
5161
  GRAPHS_DIR = ".launchsecure/graphs";
4427
5162
  tagCache = /* @__PURE__ */ new Map();
4428
5163
  }
4429
5164
  });
4430
5165
 
5166
+ // src/server/graph/core/effects-index.ts
5167
+ function addTo(map, key, nodeId) {
5168
+ const list = map[key];
5169
+ if (!list) {
5170
+ map[key] = [nodeId];
5171
+ } else if (!list.includes(nodeId)) {
5172
+ list.push(nodeId);
5173
+ }
5174
+ }
5175
+ function collisionList(map) {
5176
+ const out = [];
5177
+ for (const [key, nodes] of Object.entries(map)) {
5178
+ if (nodes.length > 1) out.push({ key, nodes: nodes.slice().sort() });
5179
+ }
5180
+ out.sort((a, b) => b.nodes.length - a.nodes.length || a.key.localeCompare(b.key));
5181
+ return out;
5182
+ }
5183
+ function buildEffectsIndex(layerOutputs) {
5184
+ const idx = {
5185
+ generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
5186
+ dom_ids: {},
5187
+ window_events: {},
5188
+ storage_keys: {},
5189
+ fetch_urls: {},
5190
+ timers: {},
5191
+ singleton_risks: [],
5192
+ collisions: { dom_ids: [], storage_keys: [], window_events: [] }
5193
+ };
5194
+ const singletonSet = /* @__PURE__ */ new Set();
5195
+ for (const output of Object.values(layerOutputs)) {
5196
+ for (const node of output.nodes) {
5197
+ const eff = node.effects;
5198
+ if (!eff) continue;
5199
+ for (const id of eff.dom_ids ?? []) addTo(idx.dom_ids, id, node.id);
5200
+ for (const ev of eff.window_events ?? []) addTo(idx.window_events, ev, node.id);
5201
+ for (const k of eff.storage_keys ?? []) addTo(idx.storage_keys, k, node.id);
5202
+ for (const u of eff.fetch_urls ?? []) addTo(idx.fetch_urls, u, node.id);
5203
+ for (const t of eff.timers ?? []) addTo(idx.timers, t, node.id);
5204
+ const hasTimer = (eff.timers?.length ?? 0) > 0;
5205
+ const hasWindowListener = (eff.window_events?.length ?? 0) > 0;
5206
+ const hasGlobalMutation = (eff.globals?.length ?? 0) > 0;
5207
+ if (hasTimer || hasWindowListener || hasGlobalMutation) {
5208
+ singletonSet.add(node.id);
5209
+ }
5210
+ }
5211
+ }
5212
+ idx.singleton_risks = Array.from(singletonSet).sort();
5213
+ idx.collisions.dom_ids = collisionList(idx.dom_ids);
5214
+ idx.collisions.storage_keys = collisionList(idx.storage_keys);
5215
+ idx.collisions.window_events = collisionList(idx.window_events);
5216
+ return idx;
5217
+ }
5218
+ function writeEffectsIndex(rootDir, idx) {
5219
+ const path3 = (0, import_node_path18.join)(rootDir, ".launchsecure", "graphs", "effects-index.json");
5220
+ (0, import_node_fs16.writeFileSync)(path3, JSON.stringify(idx, null, 2) + "\n", "utf-8");
5221
+ return path3;
5222
+ }
5223
+ var import_node_fs16, import_node_path18;
5224
+ var init_effects_index = __esm({
5225
+ "src/server/graph/core/effects-index.ts"() {
5226
+ "use strict";
5227
+ import_node_fs16 = require("node:fs");
5228
+ import_node_path18 = require("node:path");
5229
+ }
5230
+ });
5231
+
4431
5232
  // src/server/graph/index.ts
4432
5233
  function getAvailableLayers(rootDir) {
4433
- const dir = (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
4434
- if (!(0, import_node_fs16.existsSync)(dir)) return [];
4435
- return (0, import_node_fs16.readdirSync)(dir).filter((f) => f.endsWith(".json") && f !== "tags.json").map((f) => f.replace(".json", ""));
5234
+ const dir = (0, import_node_path19.join)(rootDir, GRAPHS_DIR2);
5235
+ if (!(0, import_node_fs17.existsSync)(dir)) return [];
5236
+ return (0, import_node_fs17.readdirSync)(dir).filter((f) => f.endsWith(".json") && !NON_LAYER_GRAPH_FILES.has(f)).map((f) => f.replace(".json", ""));
4436
5237
  }
4437
5238
  function graphsDir(rootDir) {
4438
- return (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
5239
+ return (0, import_node_path19.join)(rootDir, GRAPHS_DIR2);
4439
5240
  }
4440
5241
  function graphFilePath(rootDir, layer) {
4441
- return (0, import_node_path17.join)(graphsDir(rootDir), `${layer}.json`);
5242
+ return (0, import_node_path19.join)(graphsDir(rootDir), `${layer}.json`);
4442
5243
  }
4443
5244
  function tagsFilePath2(rootDir) {
4444
- return (0, import_node_path17.join)(graphsDir(rootDir), "tags.json");
5245
+ return (0, import_node_path19.join)(graphsDir(rootDir), "tags.json");
4445
5246
  }
4446
5247
  function getMtimeMs(filePath) {
4447
- if (!(0, import_node_fs16.existsSync)(filePath)) return 0;
4448
- return (0, import_node_fs16.statSync)(filePath).mtimeMs;
5248
+ if (!(0, import_node_fs17.existsSync)(filePath)) return 0;
5249
+ return (0, import_node_fs17.statSync)(filePath).mtimeMs;
4449
5250
  }
4450
5251
  function invalidateCache(filePath) {
4451
5252
  graphCache.delete(filePath);
@@ -4484,20 +5285,20 @@ function applyTags(graph, layer, rootDir) {
4484
5285
  }
4485
5286
  function readGraphRaw(rootDir, layer) {
4486
5287
  const filePath = graphFilePath(rootDir, layer);
4487
- if (!(0, import_node_fs16.existsSync)(filePath)) return null;
4488
- const stat = (0, import_node_fs16.statSync)(filePath);
5288
+ if (!(0, import_node_fs17.existsSync)(filePath)) return null;
5289
+ const stat = (0, import_node_fs17.statSync)(filePath);
4489
5290
  const cached = graphCache.get(filePath);
4490
5291
  if (cached && cached.mtimeMs === stat.mtimeMs) {
4491
5292
  return cached.graph;
4492
5293
  }
4493
- const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
5294
+ const content = (0, import_node_fs17.readFileSync)(filePath, "utf-8");
4494
5295
  const graph = JSON.parse(content);
4495
5296
  graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
4496
5297
  return graph;
4497
5298
  }
4498
5299
  function readGraph(rootDir, layer) {
4499
5300
  const rawFilePath = graphFilePath(rootDir, layer);
4500
- if (!(0, import_node_fs16.existsSync)(rawFilePath)) return null;
5301
+ if (!(0, import_node_fs17.existsSync)(rawFilePath)) return null;
4501
5302
  const rawMtime = getMtimeMs(rawFilePath);
4502
5303
  const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
4503
5304
  const cacheKey = `${rootDir}:${layer}`;
@@ -4527,24 +5328,24 @@ async function generateGraph(rootDir, layer) {
4527
5328
  mutationMethods: config.parsers?.patterns?.mutationMethods
4528
5329
  });
4529
5330
  const dir = graphsDir(rootDir);
4530
- (0, import_node_fs16.mkdirSync)(dir, { recursive: true });
5331
+ (0, import_node_fs17.mkdirSync)(dir, { recursive: true });
4531
5332
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
4532
5333
  for (const result of results) {
4533
5334
  const filePath = graphFilePath(rootDir, result.layer);
4534
- (0, import_node_fs16.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
5335
+ (0, import_node_fs17.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
4535
5336
  invalidateCache(filePath);
4536
5337
  invalidateTaggedCache(rootDir, result.layer);
4537
5338
  }
4538
5339
  if (!layer) {
4539
5340
  const producedLayers = new Set(results.map((r) => r.layer));
4540
5341
  try {
4541
- for (const f of (0, import_node_fs16.readdirSync)(dir)) {
4542
- if (!f.endsWith(".json") || f === "tags.json") continue;
5342
+ for (const f of (0, import_node_fs17.readdirSync)(dir)) {
5343
+ if (!f.endsWith(".json") || f === "tags.json" || f === "effects-index.json") continue;
4543
5344
  const layerName = f.replace(/\.json$/, "");
4544
5345
  if (producedLayers.has(layerName)) continue;
4545
- const orphan = (0, import_node_path17.join)(dir, f);
5346
+ const orphan = (0, import_node_path19.join)(dir, f);
4546
5347
  try {
4547
- (0, import_node_fs16.unlinkSync)(orphan);
5348
+ (0, import_node_fs17.unlinkSync)(orphan);
4548
5349
  invalidateCache(orphan);
4549
5350
  invalidateTaggedCache(rootDir, layerName);
4550
5351
  process.stderr.write(`[launch-chart] removed orphan layer file: ${f} (no parser produced ${layerName} this run)
@@ -4555,32 +5356,404 @@ async function generateGraph(rootDir, layer) {
4555
5356
  } catch {
4556
5357
  }
4557
5358
  }
5359
+ try {
5360
+ const allLayers = {};
5361
+ for (const r of results) allLayers[r.layer] = r.output;
5362
+ if (layer) {
5363
+ for (const f of (0, import_node_fs17.readdirSync)(dir)) {
5364
+ if (!f.endsWith(".json") || f === "tags.json" || f === "effects-index.json") continue;
5365
+ const layerName = f.replace(/\.json$/, "");
5366
+ if (allLayers[layerName]) continue;
5367
+ const existing = readGraphRaw(rootDir, layerName);
5368
+ if (existing) allLayers[layerName] = existing;
5369
+ }
5370
+ }
5371
+ const idx = buildEffectsIndex(allLayers);
5372
+ writeEffectsIndex(rootDir, idx);
5373
+ } catch (e) {
5374
+ process.stderr.write(`[launch-chart] effects-index build failed: ${e.message}
5375
+ `);
5376
+ }
4558
5377
  return results;
4559
5378
  }
4560
- var import_node_fs16, import_node_path17, GRAPHS_DIR2, graphCache, taggedCache;
4561
- var init_graph = __esm({
4562
- "src/server/graph/index.ts"() {
5379
+ function readEffectsIndex(rootDir) {
5380
+ const path3 = (0, import_node_path19.join)(rootDir, GRAPHS_DIR2, "effects-index.json");
5381
+ if (!(0, import_node_fs17.existsSync)(path3)) return null;
5382
+ try {
5383
+ return JSON.parse((0, import_node_fs17.readFileSync)(path3, "utf-8"));
5384
+ } catch {
5385
+ return null;
5386
+ }
5387
+ }
5388
+ var import_node_fs17, import_node_path19, GRAPHS_DIR2, NON_LAYER_GRAPH_FILES, graphCache, taggedCache;
5389
+ var init_graph = __esm({
5390
+ "src/server/graph/index.ts"() {
4563
5391
  "use strict";
4564
- import_node_fs16 = require("node:fs");
4565
- import_node_path17 = require("node:path");
5392
+ import_node_fs17 = require("node:fs");
5393
+ import_node_path19 = require("node:path");
4566
5394
  init_graph_builder();
4567
5395
  init_config();
4568
5396
  init_tagger_registry();
4569
5397
  init_tag_store();
4570
5398
  init_ts_extractor();
5399
+ init_effects_index();
4571
5400
  init_tag_store();
4572
5401
  GRAPHS_DIR2 = ".launchsecure/graphs";
5402
+ NON_LAYER_GRAPH_FILES = /* @__PURE__ */ new Set(["tags.json", "effects-index.json"]);
4573
5403
  graphCache = /* @__PURE__ */ new Map();
4574
5404
  taggedCache = /* @__PURE__ */ new Map();
4575
5405
  }
4576
5406
  });
4577
5407
 
5408
+ // src/server/graph/core/audit-security.ts
5409
+ function collectSourceFiles(rootDir) {
5410
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
5411
+ if (!paths) return null;
5412
+ const seen = /* @__PURE__ */ new Set();
5413
+ const out = [];
5414
+ for (const root of paths.srcRoots) {
5415
+ for (const f of walk(root, [".ts", ".tsx"])) {
5416
+ if (!seen.has(f)) {
5417
+ seen.add(f);
5418
+ out.push(f);
5419
+ }
5420
+ }
5421
+ }
5422
+ for (const conv of paths.conventionFiles) {
5423
+ if (!seen.has(conv)) {
5424
+ seen.add(conv);
5425
+ out.push(conv);
5426
+ }
5427
+ }
5428
+ return out;
5429
+ }
5430
+ function toNodeId5(rootDir, srcDir, absPath) {
5431
+ const relFromSrc = (0, import_node_path20.relative)(srcDir, absPath).replace(/\\/g, "/");
5432
+ if (relFromSrc.startsWith("..")) {
5433
+ return (0, import_node_path20.relative)(rootDir, absPath).replace(/\\/g, "/");
5434
+ }
5435
+ return relFromSrc;
5436
+ }
5437
+ function scanResponseSecretsInFile(absPath) {
5438
+ const tree = parseSource(absPath);
5439
+ if (!tree) return [];
5440
+ const query = createQuery(`
5441
+ (call_expression
5442
+ function: (member_expression
5443
+ property: (property_identifier) @method
5444
+ (#eq? @method "json"))
5445
+ arguments: (arguments
5446
+ (object) @payload))
5447
+ `);
5448
+ const matches = query.matches(tree.rootNode);
5449
+ const hits = [];
5450
+ for (const m of matches) {
5451
+ const caps = m.captures;
5452
+ const payload = caps.find((c) => c.name === "payload")?.node;
5453
+ if (!payload) continue;
5454
+ walkPairKeys(payload, (keyName, node) => {
5455
+ const trimmed = keyName.replace(/^["']|["']$/g, "");
5456
+ if (SECRET_KEY_ALLOWLIST.has(trimmed)) return;
5457
+ if (!SECRET_KEY_RE.test(trimmed)) return;
5458
+ hits.push({
5459
+ file: absPath,
5460
+ line: node.startPosition.row + 1,
5461
+ keyName: trimmed,
5462
+ callShape: payload.text.slice(0, 40).replace(/\s+/g, " ")
5463
+ });
5464
+ });
5465
+ }
5466
+ return hits;
5467
+ }
5468
+ function walkPairKeys(node, visit) {
5469
+ for (const child of node.children) {
5470
+ if (child.type === "pair") {
5471
+ const keyChild = child.children[0];
5472
+ if (keyChild && (keyChild.type === "property_identifier" || keyChild.type === "string")) {
5473
+ visit(keyChild.text, keyChild);
5474
+ }
5475
+ const valueChild = child.children[child.children.length - 1];
5476
+ if (valueChild && valueChild.type === "object") walkPairKeys(valueChild, visit);
5477
+ } else if (child.type === "shorthand_property_identifier") {
5478
+ visit(child.text, child);
5479
+ } else if (child.type === "object") {
5480
+ walkPairKeys(child, visit);
5481
+ }
5482
+ }
5483
+ }
5484
+ function checkResponseSecretLeak(rootDir, core) {
5485
+ const files = collectSourceFiles(rootDir);
5486
+ if (files === null || files.length === 0) {
5487
+ return core.buildSkipped(
5488
+ "security",
5489
+ "response_secret_leak",
5490
+ `no source files detected \u2014 not a TypeScript project, or src roots aren't configured`
5491
+ );
5492
+ }
5493
+ const findings = [];
5494
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
5495
+ for (const abs of files) {
5496
+ let hits = [];
5497
+ try {
5498
+ hits = scanResponseSecretsInFile(abs);
5499
+ } catch {
5500
+ }
5501
+ for (const h of hits) {
5502
+ const fileId = toNodeId5(rootDir, paths.srcDir, abs);
5503
+ findings.push({
5504
+ id: `secret-leak:${fileId}:${h.line}:${h.keyName}`,
5505
+ severity: "error",
5506
+ category: "response_secret_leak",
5507
+ title: `${h.keyName} in response payload`,
5508
+ detail: `Response body includes a "${h.keyName}" key \u2014 likely leaking a secret to the client. If this is a public identifier or anti-CSRF token, add it to the allow-list in audit-security.ts.`,
5509
+ file: fileId,
5510
+ line: h.line
5511
+ });
5512
+ }
5513
+ }
5514
+ return core.buildReport("security", "response_secret_leak", findings);
5515
+ }
5516
+ function collectDeclaredEnvKeys(rootDir) {
5517
+ const keys = /* @__PURE__ */ new Set();
5518
+ const files = [];
5519
+ let entries = [];
5520
+ try {
5521
+ entries = (0, import_node_fs18.readdirSync)(rootDir);
5522
+ } catch {
5523
+ return { keys, files };
5524
+ }
5525
+ for (const name of entries) {
5526
+ if (!name.startsWith(".env")) continue;
5527
+ const abs = (0, import_node_path20.join)(rootDir, name);
5528
+ if (!(0, import_node_fs18.existsSync)(abs)) continue;
5529
+ files.push(name);
5530
+ let content = "";
5531
+ try {
5532
+ content = (0, import_node_fs18.readFileSync)(abs, "utf-8");
5533
+ } catch {
5534
+ continue;
5535
+ }
5536
+ for (const line of content.split("\n")) {
5537
+ const trimmed = line.trim();
5538
+ if (!trimmed || trimmed.startsWith("#")) continue;
5539
+ const eq = trimmed.indexOf("=");
5540
+ if (eq < 0) continue;
5541
+ const key = trimmed.slice(0, eq).trim();
5542
+ if (/^[A-Z_][A-Z0-9_]*$/.test(key)) keys.add(key);
5543
+ }
5544
+ }
5545
+ return { keys, files };
5546
+ }
5547
+ function scanEnvRefsInFile(absPath) {
5548
+ const tree = parseSource(absPath);
5549
+ if (!tree) return [];
5550
+ const query = createQuery(`
5551
+ ; process.env.X
5552
+ (member_expression
5553
+ object: (member_expression
5554
+ object: (identifier) @_proc
5555
+ property: (property_identifier) @_env)
5556
+ property: (property_identifier) @env_name
5557
+ (#eq? @_proc "process")
5558
+ (#eq? @_env "env"))
5559
+
5560
+ ; process.env["X"]
5561
+ (subscript_expression
5562
+ object: (member_expression
5563
+ object: (identifier) @_proc2
5564
+ property: (property_identifier) @_env2)
5565
+ index: (string (string_fragment) @env_name_str)
5566
+ (#eq? @_proc2 "process")
5567
+ (#eq? @_env2 "env"))
5568
+ `);
5569
+ const matches = query.matches(tree.rootNode);
5570
+ const refs = [];
5571
+ for (const m of matches) {
5572
+ const caps = m.captures;
5573
+ const key = caps.find((c) => c.name === "env_name" || c.name === "env_name_str");
5574
+ if (!key) continue;
5575
+ refs.push({ file: absPath, line: key.node.startPosition.row + 1, envKey: key.node.text });
5576
+ }
5577
+ return refs;
5578
+ }
5579
+ function checkEnvDeadAlias(rootDir, core) {
5580
+ const { keys: declared, files: envFiles } = collectDeclaredEnvKeys(rootDir);
5581
+ if (envFiles.length === 0) {
5582
+ return core.buildSkipped(
5583
+ "security",
5584
+ "env_dead_alias",
5585
+ `no .env* files in project root \u2014 this check needs a declared-env inventory to compare against`
5586
+ );
5587
+ }
5588
+ const files = collectSourceFiles(rootDir);
5589
+ if (files === null || files.length === 0) {
5590
+ return core.buildSkipped("security", "env_dead_alias", `no source files detected`);
5591
+ }
5592
+ const findings = [];
5593
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
5594
+ const seen = /* @__PURE__ */ new Set();
5595
+ for (const abs of files) {
5596
+ let refs = [];
5597
+ try {
5598
+ refs = scanEnvRefsInFile(abs);
5599
+ } catch {
5600
+ }
5601
+ for (const r of refs) {
5602
+ if (declared.has(r.envKey)) continue;
5603
+ if (FRAMEWORK_ENV_KEYS.has(r.envKey)) continue;
5604
+ if (r.envKey.startsWith("npm_")) continue;
5605
+ const fileId = toNodeId5(rootDir, paths.srcDir, abs);
5606
+ const dedupeKey = `${fileId}:${r.envKey}`;
5607
+ if (seen.has(dedupeKey)) continue;
5608
+ seen.add(dedupeKey);
5609
+ findings.push({
5610
+ id: `dead-env:${fileId}:${r.envKey}`,
5611
+ severity: "warning",
5612
+ category: "env_dead_alias",
5613
+ title: `process.env.${r.envKey} not declared in any .env file`,
5614
+ detail: `Reference to env var "${r.envKey}" but no .env* file declares it (${envFiles.join(", ")}). Likely a rename left behind, a typo, or an env var that's only set in deployment but should be documented in .env.example.`,
5615
+ file: fileId,
5616
+ line: r.line
5617
+ });
5618
+ }
5619
+ }
5620
+ return core.buildReport("security", "env_dead_alias", findings);
5621
+ }
5622
+ function scanUrlFallbacksInFile(absPath) {
5623
+ const tree = parseSource(absPath);
5624
+ if (!tree) return [];
5625
+ const query = createQuery(`
5626
+ ; process.env.X || "https://..." or process.env.X ?? "https://..."
5627
+ (binary_expression
5628
+ left: (member_expression
5629
+ object: (member_expression
5630
+ object: (identifier) @_proc
5631
+ property: (property_identifier) @_env)
5632
+ property: (property_identifier) @env_var)
5633
+ right: (string (string_fragment) @url)
5634
+ (#eq? @_proc "process")
5635
+ (#eq? @_env "env")
5636
+ (#match? @url "^https?://")) @expr
5637
+ `);
5638
+ const matches = query.matches(tree.rootNode);
5639
+ const hits = [];
5640
+ for (const m of matches) {
5641
+ const caps = m.captures;
5642
+ const envVar = caps.find((c) => c.name === "env_var")?.node;
5643
+ const url = caps.find((c) => c.name === "url")?.node;
5644
+ const expr = caps.find((c) => c.name === "expr")?.node;
5645
+ if (!envVar || !url || !expr) continue;
5646
+ const opMatch = / (\|\||\?\?) /.exec(expr.text);
5647
+ const operator = opMatch ? opMatch[1] : "||";
5648
+ hits.push({
5649
+ file: absPath,
5650
+ line: envVar.startPosition.row + 1,
5651
+ envKey: envVar.text,
5652
+ url: url.text,
5653
+ operator
5654
+ });
5655
+ }
5656
+ return hits;
5657
+ }
5658
+ function checkHardcodedUrlFallback(rootDir, core) {
5659
+ const files = collectSourceFiles(rootDir);
5660
+ if (files === null || files.length === 0) {
5661
+ return core.buildSkipped("security", "hardcoded_url_fallback", `no source files detected`);
5662
+ }
5663
+ const findings = [];
5664
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
5665
+ for (const abs of files) {
5666
+ let hits = [];
5667
+ try {
5668
+ hits = scanUrlFallbacksInFile(abs);
5669
+ } catch {
5670
+ }
5671
+ for (const h of hits) {
5672
+ const fileId = toNodeId5(rootDir, paths.srcDir, abs);
5673
+ findings.push({
5674
+ id: `url-fallback:${fileId}:${h.line}:${h.envKey}`,
5675
+ severity: "warning",
5676
+ category: "hardcoded_url_fallback",
5677
+ title: `${h.envKey} ${h.operator} "${h.url}"`,
5678
+ detail: `Hardcoded URL fallback for env var "${h.envKey}". If the env var is unset in prod, requests will silently route to "${h.url}" \u2014 common cause of dev/prod URL leaks and stale localhost ports. Make the env var required, or move the default to a deployment-aware config.`,
5679
+ file: fileId,
5680
+ line: h.line
5681
+ });
5682
+ }
5683
+ }
5684
+ return core.buildReport("security", "hardcoded_url_fallback", findings);
5685
+ }
5686
+ var import_node_fs18, import_node_path20, SECRET_KEY_RE, SECRET_KEY_ALLOWLIST, FRAMEWORK_ENV_KEYS;
5687
+ var init_audit_security = __esm({
5688
+ "src/server/graph/core/audit-security.ts"() {
5689
+ "use strict";
5690
+ import_node_fs18 = require("node:fs");
5691
+ import_node_path20 = require("node:path");
5692
+ init_ts_extractor();
5693
+ init_config();
5694
+ init_resolve_paths();
5695
+ init_walk();
5696
+ SECRET_KEY_RE = /^(.*_)?(token|secret|password|passwd|credential|credentials|apikey|api_key|access_?token|refresh_?token|client_?secret|private_?key|signing_?key|webhook_?secret|auth_?token|bearer)$/i;
5697
+ SECRET_KEY_ALLOWLIST = /* @__PURE__ */ new Set([
5698
+ "csrfToken",
5699
+ "csrf_token",
5700
+ "publicKey",
5701
+ "public_key",
5702
+ "clientId",
5703
+ "idempotencyKey",
5704
+ "idempotency_key",
5705
+ "requestKey",
5706
+ "request_key",
5707
+ "sessionId",
5708
+ "session_id",
5709
+ "apiKeyId",
5710
+ "api_key_id",
5711
+ "tokenType",
5712
+ "token_type",
5713
+ "tokenExpiry",
5714
+ "token_expiry",
5715
+ "isAccessToken",
5716
+ "hasToken"
5717
+ ]);
5718
+ FRAMEWORK_ENV_KEYS = /* @__PURE__ */ new Set([
5719
+ "NODE_ENV",
5720
+ "NODE_OPTIONS",
5721
+ "NODE_PATH",
5722
+ "PATH",
5723
+ "HOME",
5724
+ "USER",
5725
+ "PWD",
5726
+ "CI",
5727
+ "GITHUB_ACTIONS",
5728
+ "GITHUB_WORKFLOW",
5729
+ "GITHUB_RUN_ID",
5730
+ "GITHUB_SHA",
5731
+ "VERCEL",
5732
+ "VERCEL_ENV",
5733
+ "VERCEL_URL",
5734
+ "VERCEL_REGION",
5735
+ "VERCEL_GIT_COMMIT_SHA",
5736
+ "VERCEL_GIT_COMMIT_REF",
5737
+ "VERCEL_GIT_REPO_SLUG",
5738
+ "VERCEL_GIT_REPO_OWNER",
5739
+ "PORT",
5740
+ "TZ",
5741
+ "LANG",
5742
+ "LC_ALL",
5743
+ "TERM",
5744
+ "DEBUG",
5745
+ "NEXT_RUNTIME",
5746
+ "NEXT_PUBLIC_VERCEL_URL"
5747
+ ]);
5748
+ }
5749
+ });
5750
+
4578
5751
  // src/server/graph/core/audit-core.ts
4579
5752
  function readGraphFile(rootDir, layer) {
4580
- const filePath = (0, import_node_path18.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
4581
- if (!(0, import_node_fs17.existsSync)(filePath)) return null;
5753
+ const filePath = (0, import_node_path21.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
5754
+ if (!(0, import_node_fs19.existsSync)(filePath)) return null;
4582
5755
  try {
4583
- return JSON.parse((0, import_node_fs17.readFileSync)(filePath, "utf-8"));
5756
+ return JSON.parse((0, import_node_fs19.readFileSync)(filePath, "utf-8"));
4584
5757
  } catch {
4585
5758
  return null;
4586
5759
  }
@@ -4589,8 +5762,7 @@ function checkSchemaDrift(rootDir) {
4589
5762
  const findings = [];
4590
5763
  const db = readGraphFile(rootDir, "db");
4591
5764
  if (!db) {
4592
- 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." });
4593
- return buildReport("db", "schema_drift", findings);
5765
+ return buildSkipped("db", "schema_drift", "no db graph \u2014 generate_graph first, or this project has no Prisma schema");
4594
5766
  }
4595
5767
  for (const c of db.contradictions ?? []) {
4596
5768
  const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
@@ -4607,7 +5779,7 @@ function checkSchemaDrift(rootDir) {
4607
5779
  function checkOrphanFks(rootDir) {
4608
5780
  const findings = [];
4609
5781
  const db = readGraphFile(rootDir, "db");
4610
- if (!db) return buildReport("db", "orphan_fks", findings);
5782
+ if (!db) return buildSkipped("db", "orphan_fks", "no db graph");
4611
5783
  for (const f of db.flagged_edges ?? []) {
4612
5784
  findings.push({
4613
5785
  id: `fk:${f.source}->${f.target}`,
@@ -4622,13 +5794,16 @@ function checkOrphanFks(rootDir) {
4622
5794
  function checkUnprotectedRoutes(rootDir) {
4623
5795
  const findings = [];
4624
5796
  const api = readGraphFile(rootDir, "api");
4625
- const staticGraph = readGraphFile(rootDir, "static");
4626
- if (!api) return buildReport("api", "unprotected_routes", findings);
4627
- const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
4628
- let routePermsContent = "";
4629
- if ((0, import_node_fs17.existsSync)(routePermsPath)) {
4630
- routePermsContent = (0, import_node_fs17.readFileSync)(routePermsPath, "utf-8");
5797
+ if (!api) return buildSkipped("api", "unprotected_routes", "no api graph");
5798
+ const routePermsPath = (0, import_node_path21.join)(rootDir, "src", "config", "route-permissions.ts");
5799
+ if (!(0, import_node_fs19.existsSync)(routePermsPath)) {
5800
+ return buildSkipped(
5801
+ "api",
5802
+ "unprotected_routes",
5803
+ `no src/config/route-permissions.ts \u2014 this check needs a centralized ROUTE_PERMISSIONS inventory to compare endpoints against`
5804
+ );
4631
5805
  }
5806
+ const routePermsContent = (0, import_node_fs19.readFileSync)(routePermsPath, "utf-8");
4632
5807
  const registeredRoutes = /* @__PURE__ */ new Set();
4633
5808
  const routeEntryRe = /path:\s*'([^']+)'/g;
4634
5809
  let rm;
@@ -4672,7 +5847,7 @@ function routeMatchesPattern(route, pattern) {
4672
5847
  function checkDeadScreens(rootDir) {
4673
5848
  const findings = [];
4674
5849
  const ui = readGraphFile(rootDir, "ui");
4675
- if (!ui) return buildReport("ui", "dead_screens", findings);
5850
+ if (!ui) return buildSkipped("ui", "dead_screens", "no ui graph");
4676
5851
  const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
4677
5852
  const navTargets = /* @__PURE__ */ new Set();
4678
5853
  for (const e of ui.edges) {
@@ -4704,13 +5879,24 @@ function checkDeadScreens(rootDir) {
4704
5879
  function checkUnenforcedPermissions(rootDir) {
4705
5880
  const findings = [];
4706
5881
  const staticGraph = readGraphFile(rootDir, "static");
4707
- if (!staticGraph) return buildReport("static", "unenforced_permissions", findings);
5882
+ if (!staticGraph) return buildSkipped("static", "unenforced_permissions", "no static graph");
4708
5883
  const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
4709
- const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
4710
- let routePermsContent = "";
4711
- if ((0, import_node_fs17.existsSync)(routePermsPath)) {
4712
- routePermsContent = (0, import_node_fs17.readFileSync)(routePermsPath, "utf-8");
5884
+ if (permissions.length === 0) {
5885
+ return buildSkipped(
5886
+ "static",
5887
+ "unenforced_permissions",
5888
+ `no seed_permission nodes \u2014 this project either has no seed permissions or hasn't tagged them in seed.ts`
5889
+ );
4713
5890
  }
5891
+ const routePermsPath = (0, import_node_path21.join)(rootDir, "src", "config", "route-permissions.ts");
5892
+ if (!(0, import_node_fs19.existsSync)(routePermsPath)) {
5893
+ return buildSkipped(
5894
+ "static",
5895
+ "unenforced_permissions",
5896
+ `no src/config/route-permissions.ts to compare seed permissions against`
5897
+ );
5898
+ }
5899
+ const routePermsContent = (0, import_node_fs19.readFileSync)(routePermsPath, "utf-8");
4714
5900
  for (const perm of permissions) {
4715
5901
  const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
4716
5902
  if (!regex.test(routePermsContent)) {
@@ -4728,20 +5914,27 @@ function checkUnenforcedPermissions(rootDir) {
4728
5914
  function checkHardcodedValues(rootDir) {
4729
5915
  const findings = [];
4730
5916
  const staticGraph = readGraphFile(rootDir, "static");
4731
- if (!staticGraph) return buildReport("static", "hardcoded_values", findings);
5917
+ if (!staticGraph) return buildSkipped("static", "hardcoded_values", "no static graph");
4732
5918
  const knownValues = /* @__PURE__ */ new Set();
4733
5919
  for (const n of staticGraph.nodes) {
4734
5920
  if (n.type === "enum_value") knownValues.add(n.value);
4735
5921
  }
5922
+ if (knownValues.size === 0) {
5923
+ return buildSkipped(
5924
+ "static",
5925
+ "hardcoded_values",
5926
+ `no enum_value nodes in static graph \u2014 without an inventory of known ALL_CAPS constants, this scan would flag every legitimate constant in your codebase`
5927
+ );
5928
+ }
4736
5929
  const api = readGraphFile(rootDir, "api");
4737
- if (!api) return buildReport("static", "hardcoded_values", findings);
5930
+ if (!api) return buildSkipped("static", "hardcoded_values", "no api graph");
4738
5931
  const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
4739
5932
  const seen = /* @__PURE__ */ new Set();
4740
5933
  for (const node of api.nodes) {
4741
5934
  if (node.type !== "endpoint") continue;
4742
- const filePath = (0, import_node_path18.join)(rootDir, "src", node.id);
4743
- if (!(0, import_node_fs17.existsSync)(filePath)) continue;
4744
- const content = (0, import_node_fs17.readFileSync)(filePath, "utf-8");
5935
+ const filePath = (0, import_node_path21.join)(rootDir, "src", node.id);
5936
+ if (!(0, import_node_fs19.existsSync)(filePath)) continue;
5937
+ const content = (0, import_node_fs19.readFileSync)(filePath, "utf-8");
4745
5938
  let m;
4746
5939
  allCapsRe.lastIndex = 0;
4747
5940
  while ((m = allCapsRe.exec(content)) !== null) {
@@ -4773,7 +5966,19 @@ function buildReport(layer, check, findings) {
4773
5966
  warnings: findings.filter((f) => f.severity === "warning").length,
4774
5967
  info: findings.filter((f) => f.severity === "info").length
4775
5968
  },
4776
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
5969
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5970
+ status: "ok"
5971
+ };
5972
+ }
5973
+ function buildSkipped(layer, check, reason) {
5974
+ return {
5975
+ layer,
5976
+ check,
5977
+ findings: [],
5978
+ summary: { errors: 0, warnings: 0, info: 0 },
5979
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5980
+ status: "skipped",
5981
+ skipReason: reason
4777
5982
  };
4778
5983
  }
4779
5984
  function getAvailableChecks() {
@@ -4820,7 +6025,12 @@ function runAudit(rootDir, layer, check) {
4820
6025
  }
4821
6026
  function formatAsPrompt(reports) {
4822
6027
  const lines = [];
6028
+ const skipped = [];
4823
6029
  for (const report of reports) {
6030
+ if (report.status === "skipped") {
6031
+ skipped.push(report);
6032
+ continue;
6033
+ }
4824
6034
  if (report.findings.length === 0) continue;
4825
6035
  lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
4826
6036
  lines.push("");
@@ -4832,15 +6042,24 @@ function formatAsPrompt(reports) {
4832
6042
  }
4833
6043
  lines.push("");
4834
6044
  }
6045
+ if (skipped.length > 0) {
6046
+ lines.push("## Skipped checks (no comparison target \u2014 NOT passes)");
6047
+ lines.push("");
6048
+ for (const r of skipped) {
6049
+ lines.push(`- ${r.layer}/${r.check}: ${r.skipReason ?? "comparison target missing"}`);
6050
+ }
6051
+ lines.push("");
6052
+ }
4835
6053
  if (lines.length === 0) return "No audit findings.";
4836
6054
  return lines.join("\n");
4837
6055
  }
4838
- var import_node_fs17, import_node_path18, CHECKS;
6056
+ var import_node_fs19, import_node_path21, CHECKS;
4839
6057
  var init_audit_core = __esm({
4840
6058
  "src/server/graph/core/audit-core.ts"() {
4841
6059
  "use strict";
4842
- import_node_fs17 = require("node:fs");
4843
- import_node_path18 = require("node:path");
6060
+ import_node_fs19 = require("node:fs");
6061
+ import_node_path21 = require("node:path");
6062
+ init_audit_security();
4844
6063
  CHECKS = {
4845
6064
  db: {
4846
6065
  schema_drift: checkSchemaDrift,
@@ -4855,6 +6074,11 @@ var init_audit_core = __esm({
4855
6074
  static: {
4856
6075
  unenforced_permissions: checkUnenforcedPermissions,
4857
6076
  hardcoded_values: checkHardcodedValues
6077
+ },
6078
+ security: {
6079
+ response_secret_leak: (rootDir) => checkResponseSecretLeak(rootDir, { buildReport, buildSkipped }),
6080
+ env_dead_alias: (rootDir) => checkEnvDeadAlias(rootDir, { buildReport, buildSkipped }),
6081
+ hardcoded_url_fallback: (rootDir) => checkHardcodedUrlFallback(rootDir, { buildReport, buildSkipped })
4858
6082
  }
4859
6083
  };
4860
6084
  }
@@ -4872,16 +6096,16 @@ function randomPort() {
4872
6096
  function findProjectRoot(startDir) {
4873
6097
  let dir = startDir;
4874
6098
  for (let i = 0; i < 8; i++) {
4875
- const graphsDir2 = import_node_path19.default.join(dir, ".launchsecure", "graphs");
4876
- if (import_node_fs18.default.existsSync(import_node_path19.default.join(graphsDir2, "ui.json")) || import_node_fs18.default.existsSync(import_node_path19.default.join(graphsDir2, "api.json")) || import_node_fs18.default.existsSync(import_node_path19.default.join(graphsDir2, "db.json"))) return dir;
4877
- const parent = import_node_path19.default.dirname(dir);
6099
+ const graphsDir2 = import_node_path22.default.join(dir, ".launchsecure", "graphs");
6100
+ if (import_node_fs20.default.existsSync(import_node_path22.default.join(graphsDir2, "ui.json")) || import_node_fs20.default.existsSync(import_node_path22.default.join(graphsDir2, "api.json")) || import_node_fs20.default.existsSync(import_node_path22.default.join(graphsDir2, "db.json"))) return dir;
6101
+ const parent = import_node_path22.default.dirname(dir);
4878
6102
  if (parent === dir) break;
4879
6103
  dir = parent;
4880
6104
  }
4881
6105
  dir = startDir;
4882
6106
  for (let i = 0; i < 8; i++) {
4883
- if (import_node_fs18.default.existsSync(import_node_path19.default.join(dir, ".git"))) return dir;
4884
- const parent = import_node_path19.default.dirname(dir);
6107
+ if (import_node_fs20.default.existsSync(import_node_path22.default.join(dir, ".git"))) return dir;
6108
+ const parent = import_node_path22.default.dirname(dir);
4885
6109
  if (parent === dir) break;
4886
6110
  dir = parent;
4887
6111
  }
@@ -4890,7 +6114,7 @@ function findProjectRoot(startDir) {
4890
6114
  function resolveRequestRoot(url, monorepoRoot, projects) {
4891
6115
  const projectParam = url.searchParams.get("project");
4892
6116
  if (!projectParam || projects.length === 0) return monorepoRoot;
4893
- const resolved = import_node_path19.default.resolve(monorepoRoot, projectParam);
6117
+ const resolved = import_node_path22.default.resolve(monorepoRoot, projectParam);
4894
6118
  if (!resolved.startsWith(monorepoRoot)) {
4895
6119
  throw new Error("Project path outside monorepo root");
4896
6120
  }
@@ -4941,16 +6165,16 @@ async function buildMergedGraph(root) {
4941
6165
  };
4942
6166
  }
4943
6167
  function serveStatic(res, filePath) {
4944
- if (!import_node_fs18.default.existsSync(filePath) || !import_node_fs18.default.statSync(filePath).isFile()) return false;
4945
- const ext = import_node_path19.default.extname(filePath).toLowerCase();
6168
+ if (!import_node_fs20.default.existsSync(filePath) || !import_node_fs20.default.statSync(filePath).isFile()) return false;
6169
+ const ext = import_node_path22.default.extname(filePath).toLowerCase();
4946
6170
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
4947
6171
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
4948
- import_node_fs18.default.createReadStream(filePath).pipe(res);
6172
+ import_node_fs20.default.createReadStream(filePath).pipe(res);
4949
6173
  return true;
4950
6174
  }
4951
6175
  function serveIndex(res, clientDir) {
4952
- const indexPath = import_node_path19.default.join(clientDir, "index.html");
4953
- if (!import_node_fs18.default.existsSync(indexPath)) {
6176
+ const indexPath = import_node_path22.default.join(clientDir, "index.html");
6177
+ if (!import_node_fs20.default.existsSync(indexPath)) {
4954
6178
  res.writeHead(500, { "Content-Type": "text/plain" });
4955
6179
  res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
4956
6180
  return;
@@ -4958,14 +6182,14 @@ function serveIndex(res, clientDir) {
4958
6182
  serveStatic(res, indexPath);
4959
6183
  }
4960
6184
  function tryListen(server, port) {
4961
- return new Promise((resolve4, reject) => {
6185
+ return new Promise((resolve5, reject) => {
4962
6186
  const onError = (err2) => {
4963
6187
  server.off("listening", onListening);
4964
6188
  reject(err2);
4965
6189
  };
4966
6190
  const onListening = () => {
4967
6191
  server.off("error", onError);
4968
- resolve4(port);
6192
+ resolve5(port);
4969
6193
  };
4970
6194
  server.once("error", onError);
4971
6195
  server.once("listening", onListening);
@@ -5002,7 +6226,7 @@ async function startChartServer(opts = {}) {
5002
6226
  }
5003
6227
  return { port: existing.port, url: existing.url };
5004
6228
  }
5005
- const clientDir = opts.clientDir ?? import_node_path19.default.join(__dirname, "..", "chart-client");
6229
+ const clientDir = opts.clientDir ?? import_node_path22.default.join(__dirname, "..", "chart-client");
5006
6230
  const rootConfig = loadConfig(projectRoot);
5007
6231
  const projects = rootConfig.projects ?? [];
5008
6232
  const server = import_node_http.default.createServer((req, res) => {
@@ -5018,11 +6242,11 @@ async function startChartServer(opts = {}) {
5018
6242
  }
5019
6243
  if (req.method === "GET" && url2.pathname === "/api/projects") {
5020
6244
  const projectList = projects.length > 0 ? projects.map((p) => {
5021
- const absRoot = import_node_path19.default.resolve(projectRoot, p.root);
5022
- const hasGraphs = import_node_fs18.default.existsSync(import_node_path19.default.join(absRoot, ".launchsecure", "graphs"));
5023
- const hasNextConfig = import_node_fs18.default.existsSync(import_node_path19.default.join(absRoot, "next.config.ts")) || import_node_fs18.default.existsSync(import_node_path19.default.join(absRoot, "next.config.js")) || import_node_fs18.default.existsSync(import_node_path19.default.join(absRoot, "next.config.mjs"));
6245
+ const absRoot = import_node_path22.default.resolve(projectRoot, p.root);
6246
+ const hasGraphs = import_node_fs20.default.existsSync(import_node_path22.default.join(absRoot, ".launchsecure", "graphs"));
6247
+ const hasNextConfig = import_node_fs20.default.existsSync(import_node_path22.default.join(absRoot, "next.config.ts")) || import_node_fs20.default.existsSync(import_node_path22.default.join(absRoot, "next.config.js")) || import_node_fs20.default.existsSync(import_node_path22.default.join(absRoot, "next.config.mjs"));
5024
6248
  return { name: p.name, root: p.root, hasGraphs, hasNextConfig };
5025
- }) : [{ name: import_node_path19.default.basename(projectRoot), root: ".", hasGraphs: import_node_fs18.default.existsSync(import_node_path19.default.join(projectRoot, ".launchsecure", "graphs")), hasNextConfig: true }];
6249
+ }) : [{ name: import_node_path22.default.basename(projectRoot), root: ".", hasGraphs: import_node_fs20.default.existsSync(import_node_path22.default.join(projectRoot, ".launchsecure", "graphs")), hasNextConfig: true }];
5026
6250
  res.writeHead(200, { "Content-Type": "application/json" });
5027
6251
  res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
5028
6252
  return;
@@ -5068,20 +6292,20 @@ async function startChartServer(opts = {}) {
5068
6292
  }
5069
6293
  if (req.method === "GET" && url2.pathname === "/api/file-content") {
5070
6294
  const relPath = url2.searchParams.get("path");
5071
- if (!relPath || relPath.includes("..") || import_node_path19.default.isAbsolute(relPath)) {
6295
+ if (!relPath || relPath.includes("..") || import_node_path22.default.isAbsolute(relPath)) {
5072
6296
  res.writeHead(400, { "Content-Type": "application/json" });
5073
6297
  res.end(JSON.stringify({ error: "Invalid path" }));
5074
6298
  return;
5075
6299
  }
5076
- const filePath = import_node_path19.default.join(reqRoot, relPath);
5077
- if (!filePath.startsWith(reqRoot) || !import_node_fs18.default.existsSync(filePath) || !import_node_fs18.default.statSync(filePath).isFile()) {
6300
+ const filePath = import_node_path22.default.join(reqRoot, relPath);
6301
+ if (!filePath.startsWith(reqRoot) || !import_node_fs20.default.existsSync(filePath) || !import_node_fs20.default.statSync(filePath).isFile()) {
5078
6302
  res.writeHead(404, { "Content-Type": "application/json" });
5079
6303
  res.end(JSON.stringify({ error: "File not found" }));
5080
6304
  return;
5081
6305
  }
5082
- const ext = import_node_path19.default.extname(filePath).toLowerCase();
6306
+ const ext = import_node_path22.default.extname(filePath).toLowerCase();
5083
6307
  const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
5084
- const content = import_node_fs18.default.readFileSync(filePath, "utf-8");
6308
+ const content = import_node_fs20.default.readFileSync(filePath, "utf-8");
5085
6309
  res.writeHead(200, { "Content-Type": "application/json" });
5086
6310
  res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
5087
6311
  return;
@@ -5123,8 +6347,8 @@ async function startChartServer(opts = {}) {
5123
6347
  req.on("end", () => {
5124
6348
  try {
5125
6349
  const newConfig = JSON.parse(body);
5126
- const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
5127
- import_node_fs18.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
6350
+ const configPath = import_node_path22.default.join(reqRoot, ".launchchart.json");
6351
+ import_node_fs20.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
5128
6352
  res.writeHead(200, { "Content-Type": "application/json" });
5129
6353
  res.end(JSON.stringify({ ok: true }));
5130
6354
  } catch (err2) {
@@ -5157,8 +6381,8 @@ async function startChartServer(opts = {}) {
5157
6381
  const taggerConfig = JSON.parse(body);
5158
6382
  const config2 = loadConfig(reqRoot);
5159
6383
  config2.taggers = taggerConfig;
5160
- const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
5161
- import_node_fs18.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
6384
+ const configPath = import_node_path22.default.join(reqRoot, ".launchchart.json");
6385
+ import_node_fs20.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
5162
6386
  res.writeHead(200, { "Content-Type": "application/json" });
5163
6387
  res.end(JSON.stringify({ ok: true }));
5164
6388
  } catch (err2) {
@@ -5228,7 +6452,7 @@ async function startChartServer(opts = {}) {
5228
6452
  dbDir: !!config2.paths?.dbDir,
5229
6453
  srcRoots: !!(config2.paths?.srcRoots && config2.paths.srcRoots.length > 0)
5230
6454
  };
5231
- const relFromRoot = (abs) => import_node_path19.default.relative(reqRoot, abs) || ".";
6455
+ const relFromRoot = (abs) => import_node_path22.default.relative(reqRoot, abs) || ".";
5232
6456
  res.writeHead(200, { "Content-Type": "application/json" });
5233
6457
  res.end(JSON.stringify({
5234
6458
  projectRoot: reqRoot,
@@ -5250,19 +6474,19 @@ async function startChartServer(opts = {}) {
5250
6474
  }
5251
6475
  if (req.method === "GET" && url2.pathname === "/api/browse-dir") {
5252
6476
  const browsePath = url2.searchParams.get("path") || projectRoot;
5253
- const abs = import_node_path19.default.resolve(browsePath);
5254
- const twoUp = import_node_path19.default.resolve(projectRoot, "..", "..");
6477
+ const abs = import_node_path22.default.resolve(browsePath);
6478
+ const twoUp = import_node_path22.default.resolve(projectRoot, "..", "..");
5255
6479
  if (!abs.startsWith(twoUp)) {
5256
6480
  res.writeHead(403, { "Content-Type": "application/json" });
5257
6481
  res.end(JSON.stringify({ ok: false, error: "Path outside allowed range" }));
5258
6482
  return;
5259
6483
  }
5260
6484
  try {
5261
- const entries = import_node_fs18.default.readdirSync(abs, { withFileTypes: true });
6485
+ const entries = import_node_fs20.default.readdirSync(abs, { withFileTypes: true });
5262
6486
  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();
5263
- const parent = abs !== twoUp ? import_node_path19.default.dirname(abs) : null;
6487
+ const parent = abs !== twoUp ? import_node_path22.default.dirname(abs) : null;
5264
6488
  res.writeHead(200, { "Content-Type": "application/json" });
5265
- res.end(JSON.stringify({ current: abs, parent, dirs, relative: import_node_path19.default.relative(projectRoot, abs) || "." }));
6489
+ res.end(JSON.stringify({ current: abs, parent, dirs, relative: import_node_path22.default.relative(projectRoot, abs) || "." }));
5266
6490
  } catch (err2) {
5267
6491
  res.writeHead(400, { "Content-Type": "application/json" });
5268
6492
  res.end(JSON.stringify({ ok: false, error: String(err2) }));
@@ -5288,8 +6512,8 @@ async function startChartServer(opts = {}) {
5288
6512
  const { projects: newProjects } = JSON.parse(body);
5289
6513
  const config2 = loadConfig(projectRoot);
5290
6514
  config2.projects = newProjects.length > 0 ? newProjects : void 0;
5291
- const configPath = import_node_path19.default.join(projectRoot, ".launchchart.json");
5292
- import_node_fs18.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
6515
+ const configPath = import_node_path22.default.join(projectRoot, ".launchchart.json");
6516
+ import_node_fs20.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
5293
6517
  projects.length = 0;
5294
6518
  if (config2.projects) projects.push(...config2.projects);
5295
6519
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -5302,7 +6526,7 @@ async function startChartServer(opts = {}) {
5302
6526
  return;
5303
6527
  }
5304
6528
  if (url2.pathname !== "/") {
5305
- const staticPath = import_node_path19.default.join(clientDir, url2.pathname);
6529
+ const staticPath = import_node_path22.default.join(clientDir, url2.pathname);
5306
6530
  if (serveStatic(res, staticPath)) return;
5307
6531
  }
5308
6532
  serveIndex(res, clientDir);
@@ -5362,13 +6586,13 @@ function runServeCli(argv) {
5362
6586
  process.exit(1);
5363
6587
  });
5364
6588
  }
5365
- var import_node_http, import_node_fs18, import_node_path19, MAX_PORT_SCAN, MIME_TYPES;
6589
+ var import_node_http, import_node_fs20, import_node_path22, MAX_PORT_SCAN, MIME_TYPES;
5366
6590
  var init_chart_serve = __esm({
5367
6591
  "src/server/chart-serve.ts"() {
5368
6592
  "use strict";
5369
6593
  import_node_http = __toESM(require("node:http"));
5370
- import_node_fs18 = __toESM(require("node:fs"));
5371
- import_node_path19 = __toESM(require("node:path"));
6594
+ import_node_fs20 = __toESM(require("node:fs"));
6595
+ import_node_path22 = __toESM(require("node:path"));
5372
6596
  init_graph();
5373
6597
  init_lockfile();
5374
6598
  init_config();
@@ -5390,13 +6614,183 @@ var init_chart_serve = __esm({
5390
6614
  }
5391
6615
  });
5392
6616
 
6617
+ // src/server/graph/core/projects.ts
6618
+ function listProjects(monorepoRoot) {
6619
+ const cfg = loadConfig(monorepoRoot);
6620
+ const entries = cfg.projects ?? [];
6621
+ return entries.map((p) => ({
6622
+ name: p.name,
6623
+ root: p.root,
6624
+ absoluteRoot: (0, import_node_path23.resolve)(monorepoRoot, p.root)
6625
+ }));
6626
+ }
6627
+ function resolveProject(name, projects) {
6628
+ const exact = projects.find((p) => p.name === name || p.root === name);
6629
+ if (exact) return exact;
6630
+ const ci = projects.find((p) => p.name.toLowerCase() === name.toLowerCase());
6631
+ if (ci) return ci;
6632
+ const available = projects.map((p) => `"${p.name}" (${p.root})`).join(", ");
6633
+ throw new Error(`Unknown project "${name}". Available: ${available}.`);
6634
+ }
6635
+ function resolveProjectRoot(project, monorepoRoot) {
6636
+ const raw = typeof project === "string" ? project.trim() : "";
6637
+ if (!raw) return monorepoRoot;
6638
+ const projects = listProjects(monorepoRoot);
6639
+ if (projects.length === 0) {
6640
+ throw new Error(
6641
+ `project="${raw}" requested but .launchchart.json has no projects[] configured. Remove the project arg, or add the project to .launchchart.json.`
6642
+ );
6643
+ }
6644
+ return resolveProject(raw, projects).absoluteRoot;
6645
+ }
6646
+ var import_node_path23, PROJECT_PARAM_DESCRIPTION;
6647
+ var init_projects = __esm({
6648
+ "src/server/graph/core/projects.ts"() {
6649
+ "use strict";
6650
+ import_node_path23 = require("node:path");
6651
+ init_config();
6652
+ PROJECT_PARAM_DESCRIPTION = "Optional sub-project name (or root path) from .launchchart.json projects[]. Defaults to the monorepo root. Run detect_project_stack to list configured projects.";
6653
+ }
6654
+ });
6655
+
6656
+ // src/server/graph-cli.ts
6657
+ var graph_cli_exports = {};
6658
+ __export(graph_cli_exports, {
6659
+ handleGraphCommand: () => handleGraphCommand
6660
+ });
6661
+ function parseLayerFlag(args) {
6662
+ const idx = args.indexOf("--layer");
6663
+ if (idx < 0 || idx + 1 >= args.length) return void 0;
6664
+ return args[idx + 1];
6665
+ }
6666
+ function parseProjectFlag(args) {
6667
+ const idx = args.indexOf("--project");
6668
+ if (idx < 0 || idx + 1 >= args.length) return void 0;
6669
+ return args[idx + 1];
6670
+ }
6671
+ async function handleGraphCommand(subcommand, args) {
6672
+ const monorepoRoot = process.cwd();
6673
+ if (subcommand === "graph:generate") {
6674
+ const layer = parseLayerFlag(args);
6675
+ const projectArg = parseProjectFlag(args);
6676
+ const projects = listProjects(monorepoRoot);
6677
+ if (projectArg) {
6678
+ let target;
6679
+ try {
6680
+ target = resolveProject(projectArg, projects);
6681
+ } catch (e) {
6682
+ console.error(e.message);
6683
+ process.exit(1);
6684
+ }
6685
+ const results2 = await generateGraph(target.absoluteRoot, layer);
6686
+ if (results2.length === 0) {
6687
+ console.error(
6688
+ layer ? `No parser detected for the "${layer}" layer in project "${projectArg}".` : `No parsers detected for project "${projectArg}".`
6689
+ );
6690
+ process.exit(1);
6691
+ }
6692
+ console.log(`\u2713 "${target.name}" (${target.root}):`);
6693
+ for (const r of results2) {
6694
+ const warnings = r.output.warnings.length;
6695
+ console.log(` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`);
6696
+ }
6697
+ console.log(`Output: ${target.root}/.launchsecure/graphs/`);
6698
+ return;
6699
+ }
6700
+ if (projects.length > 0) {
6701
+ let okCount = 0;
6702
+ let failCount = 0;
6703
+ for (const proj of projects) {
6704
+ try {
6705
+ const results2 = await generateGraph(proj.absoluteRoot, layer);
6706
+ if (results2.length === 0) {
6707
+ console.error(` \u2717 "${proj.name}" (${proj.root}): no parsers detected`);
6708
+ failCount++;
6709
+ continue;
6710
+ }
6711
+ console.log(` \u2713 "${proj.name}" (${proj.root}):`);
6712
+ for (const r of results2) {
6713
+ const warnings = r.output.warnings.length;
6714
+ console.log(` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`);
6715
+ }
6716
+ okCount++;
6717
+ } catch (e) {
6718
+ console.error(` \u2717 "${proj.name}" (${proj.root}): ${e.message}`);
6719
+ failCount++;
6720
+ }
6721
+ }
6722
+ console.log(`
6723
+ Regenerated ${okCount}/${projects.length} project graph(s)${failCount ? ` (${failCount} failed)` : ""}.`);
6724
+ if (failCount > 0 && okCount === 0) process.exit(1);
6725
+ return;
6726
+ }
6727
+ const results = await generateGraph(monorepoRoot, layer);
6728
+ if (results.length === 0) {
6729
+ console.error(
6730
+ layer ? `No parser detected for the "${layer}" layer in this project.` : "No parsers detected for this project. Check that the project has the expected structure."
6731
+ );
6732
+ process.exit(1);
6733
+ }
6734
+ for (const r of results) {
6735
+ const warnings = r.output.warnings.length;
6736
+ console.log(` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`);
6737
+ }
6738
+ console.log(`Output: .launchsecure/graphs/`);
6739
+ return;
6740
+ }
6741
+ if (subcommand === "graph:read") {
6742
+ const layer = parseLayerFlag(args);
6743
+ const projectArg = parseProjectFlag(args);
6744
+ const projects = listProjects(monorepoRoot);
6745
+ let rootDir = monorepoRoot;
6746
+ if (projectArg) {
6747
+ try {
6748
+ rootDir = resolveProject(projectArg, projects).absoluteRoot;
6749
+ } catch (e) {
6750
+ console.error(e.message);
6751
+ process.exit(1);
6752
+ }
6753
+ }
6754
+ if (layer) {
6755
+ const available = getAvailableLayers(rootDir);
6756
+ if (available.length > 0 && !available.includes(layer)) {
6757
+ console.error(`No graph found for layer "${layer}". Available: ${available.join(", ")}`);
6758
+ process.exit(1);
6759
+ }
6760
+ const graph = readGraph(rootDir, layer);
6761
+ if (!graph) {
6762
+ console.error(`No ${layer} graph found. Run: launchpod graph:generate${projectArg ? ` --project ${projectArg}` : ""}`);
6763
+ process.exit(1);
6764
+ }
6765
+ console.log(JSON.stringify(graph, null, 2));
6766
+ } else {
6767
+ const graphs = readAllGraphs(rootDir);
6768
+ if (Object.keys(graphs).length === 0) {
6769
+ console.error(`No graphs found. Run: launchpod graph:generate${projectArg ? ` --project ${projectArg}` : ""}`);
6770
+ process.exit(1);
6771
+ }
6772
+ console.log(JSON.stringify(graphs, null, 2));
6773
+ }
6774
+ return;
6775
+ }
6776
+ console.error(`Unknown graph subcommand: ${subcommand}`);
6777
+ process.exit(1);
6778
+ }
6779
+ var init_graph_cli = __esm({
6780
+ "src/server/graph-cli.ts"() {
6781
+ "use strict";
6782
+ init_graph();
6783
+ init_projects();
6784
+ }
6785
+ });
6786
+
5393
6787
  // src/server/graph/core/language-detection.ts
5394
6788
  function walkForExtensions(dir, extCounts, depth = 0) {
5395
6789
  if (depth > 10) return;
5396
- if (!(0, import_node_fs19.existsSync)(dir)) return;
6790
+ if (!(0, import_node_fs21.existsSync)(dir)) return;
5397
6791
  let entries;
5398
6792
  try {
5399
- entries = (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true });
6793
+ entries = (0, import_node_fs21.readdirSync)(dir, { withFileTypes: true });
5400
6794
  } catch {
5401
6795
  return;
5402
6796
  }
@@ -5404,9 +6798,9 @@ function walkForExtensions(dir, extCounts, depth = 0) {
5404
6798
  if (entry.name.startsWith(".") && entry.isDirectory()) continue;
5405
6799
  if (entry.isDirectory()) {
5406
6800
  if (IGNORE_DIRS.has(entry.name)) continue;
5407
- walkForExtensions((0, import_node_path20.join)(dir, entry.name), extCounts, depth + 1);
6801
+ walkForExtensions((0, import_node_path24.join)(dir, entry.name), extCounts, depth + 1);
5408
6802
  } else {
5409
- const ext = (0, import_node_path20.extname)(entry.name).toLowerCase();
6803
+ const ext = (0, import_node_path24.extname)(entry.name).toLowerCase();
5410
6804
  if (ext && EXTENSION_TO_LANGUAGE[ext]) {
5411
6805
  extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
5412
6806
  }
@@ -5445,12 +6839,12 @@ function detectLanguages(rootDir, supportedLanguages) {
5445
6839
  });
5446
6840
  return results;
5447
6841
  }
5448
- var import_node_fs19, import_node_path20, EXTENSION_TO_LANGUAGE, IGNORE_DIRS, AUXILIARY_LANGUAGES;
6842
+ var import_node_fs21, import_node_path24, EXTENSION_TO_LANGUAGE, IGNORE_DIRS, AUXILIARY_LANGUAGES;
5449
6843
  var init_language_detection = __esm({
5450
6844
  "src/server/graph/core/language-detection.ts"() {
5451
6845
  "use strict";
5452
- import_node_fs19 = require("node:fs");
5453
- import_node_path20 = require("node:path");
6846
+ import_node_fs21 = require("node:fs");
6847
+ import_node_path24 = require("node:path");
5454
6848
  EXTENSION_TO_LANGUAGE = {
5455
6849
  // Web / Frontend
5456
6850
  ".ts": "typescript",
@@ -5563,9 +6957,128 @@ var init_language_detection = __esm({
5563
6957
  }
5564
6958
  });
5565
6959
 
6960
+ // src/server/graph/core/watcher.ts
6961
+ var watcher_exports = {};
6962
+ __export(watcher_exports, {
6963
+ startGraphWatcher: () => startGraphWatcher
6964
+ });
6965
+ function isIgnoredPath(rel) {
6966
+ if (rel.startsWith(GRAPHS_RELATIVE)) return true;
6967
+ if (rel.endsWith(".lock") || rel.endsWith(".log")) return true;
6968
+ for (const part of rel.split(import_node_path25.sep)) {
6969
+ if (IGNORE_SEGMENTS.has(part)) return true;
6970
+ }
6971
+ return false;
6972
+ }
6973
+ function isTriggerFile(rel) {
6974
+ if (isIgnoredPath(rel)) return false;
6975
+ const dot = rel.lastIndexOf(".");
6976
+ if (dot < 0) return false;
6977
+ return TRIGGER_EXTENSIONS.has(rel.slice(dot));
6978
+ }
6979
+ function startGraphWatcher(rootDir, opts = {}) {
6980
+ const debounceMs = opts.debounceMs ?? 500;
6981
+ const pending = /* @__PURE__ */ new Set();
6982
+ let timer = null;
6983
+ let regenerating = false;
6984
+ let lastAt = null;
6985
+ let lastErr = null;
6986
+ async function flush() {
6987
+ if (regenerating) {
6988
+ timer = setTimeout(flush, debounceMs);
6989
+ return;
6990
+ }
6991
+ const changedFiles = Array.from(pending);
6992
+ pending.clear();
6993
+ timer = null;
6994
+ if (changedFiles.length === 0) return;
6995
+ regenerating = true;
6996
+ const start = Date.now();
6997
+ try {
6998
+ await generateGraph(rootDir);
6999
+ lastAt = Date.now();
7000
+ lastErr = null;
7001
+ opts.onRegen?.({ changedFiles, durationMs: lastAt - start });
7002
+ } catch (e) {
7003
+ const err2 = e instanceof Error ? e : new Error(String(e));
7004
+ lastErr = err2.message;
7005
+ opts.onError?.(err2);
7006
+ } finally {
7007
+ regenerating = false;
7008
+ }
7009
+ }
7010
+ const watcher = (0, import_node_fs22.watch)(rootDir, { recursive: true }, (event, filename) => {
7011
+ if (!filename) return;
7012
+ const rel = filename.toString();
7013
+ if (process.env.LAUNCH_CHART_WATCH_TRACE === "1") {
7014
+ process.stderr.write(`[lc-watcher trace] event=${event} file=${rel} trigger=${isTriggerFile(rel)}
7015
+ `);
7016
+ }
7017
+ if (!isTriggerFile(rel)) return;
7018
+ pending.add(rel);
7019
+ if (timer) clearTimeout(timer);
7020
+ timer = setTimeout(flush, debounceMs);
7021
+ });
7022
+ watcher.on("error", (e) => {
7023
+ const err2 = e instanceof Error ? e : new Error(String(e));
7024
+ lastErr = `watcher: ${err2.message}`;
7025
+ opts.onError?.(err2);
7026
+ });
7027
+ return {
7028
+ rootDir,
7029
+ stop() {
7030
+ if (timer) {
7031
+ clearTimeout(timer);
7032
+ timer = null;
7033
+ }
7034
+ watcher.close();
7035
+ },
7036
+ isRegenerating: () => regenerating,
7037
+ lastRegenAt: () => lastAt,
7038
+ lastError: () => lastErr,
7039
+ pendingCount: () => pending.size
7040
+ };
7041
+ }
7042
+ var import_node_fs22, import_node_path25, IGNORE_SEGMENTS, TRIGGER_EXTENSIONS, GRAPHS_RELATIVE;
7043
+ var init_watcher = __esm({
7044
+ "src/server/graph/core/watcher.ts"() {
7045
+ "use strict";
7046
+ import_node_fs22 = require("node:fs");
7047
+ import_node_path25 = require("node:path");
7048
+ init_graph();
7049
+ IGNORE_SEGMENTS = /* @__PURE__ */ new Set([
7050
+ "node_modules",
7051
+ ".git",
7052
+ ".claude",
7053
+ ".next",
7054
+ ".turbo",
7055
+ "dist",
7056
+ "build",
7057
+ "coverage",
7058
+ ".cache",
7059
+ ".vite",
7060
+ ".parcel-cache"
7061
+ ]);
7062
+ TRIGGER_EXTENSIONS = /* @__PURE__ */ new Set([
7063
+ ".ts",
7064
+ ".tsx",
7065
+ ".js",
7066
+ ".jsx",
7067
+ ".mts",
7068
+ ".cts",
7069
+ ".mjs",
7070
+ ".cjs",
7071
+ ".prisma",
7072
+ ".sql"
7073
+ ]);
7074
+ GRAPHS_RELATIVE = (0, import_node_path25.join)(".launchsecure", "graphs");
7075
+ }
7076
+ });
7077
+
5566
7078
  // src/server/graph-mcp.ts
5567
7079
  var graph_mcp_exports = {};
5568
7080
  __export(graph_mcp_exports, {
7081
+ getWatcherHandle: () => getWatcherHandle,
5569
7082
  startGraphMcpServer: () => startGraphMcpServer
5570
7083
  });
5571
7084
  function matchesSearch(node, query) {
@@ -5585,6 +7098,14 @@ function toMinimal(nodes) {
5585
7098
  return out;
5586
7099
  });
5587
7100
  }
7101
+ function crossRefsAsEdges(graph) {
7102
+ return (graph.cross_refs ?? []).map((c) => ({
7103
+ source: c.source,
7104
+ target: c.target,
7105
+ type: c.type,
7106
+ target_layer: c.layer
7107
+ }));
7108
+ }
5588
7109
  function toCompactNode(n) {
5589
7110
  const out = { i: n.id, t: n.type, n: n.name };
5590
7111
  const tags = n.tags;
@@ -5609,6 +7130,8 @@ function toCompactEdges(edges, idx) {
5609
7130
  t: e.type
5610
7131
  };
5611
7132
  if (e.label != null) o.l = e.label;
7133
+ const targetLayer = e.target_layer;
7134
+ if (targetLayer != null) o.tl = targetLayer;
5612
7135
  return o;
5613
7136
  });
5614
7137
  }
@@ -5649,6 +7172,9 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
5649
7172
  const dstIn = visited.has(e.target) || next.has(e.target);
5650
7173
  if (srcIn && dstIn) projectedEdges++;
5651
7174
  }
7175
+ for (const c of graph.cross_refs ?? []) {
7176
+ if (visited.has(c.source) || next.has(c.source)) projectedEdges++;
7177
+ }
5652
7178
  const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] ?? DEFAULT_EST_NODE_MIN : EST_CHARS_PER_NODE_FULL[layer] ?? DEFAULT_EST_NODE_FULL;
5653
7179
  const projectedChars = projectedVisited * perNode + projectedEdges * (EST_CHARS_PER_EDGE[layer] ?? DEFAULT_EST_EDGE);
5654
7180
  if (projectedChars > NEIGHBORHOOD_BUDGET_CHARS) {
@@ -5661,8 +7187,9 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
5661
7187
  if (frontier.size === 0) break;
5662
7188
  }
5663
7189
  const nodes = graph.nodes.filter((n) => visited.has(n.id));
5664
- const edges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
5665
- return { nodes, edges, budgetExceeded, stoppedAtHop };
7190
+ const sameLayerEdges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
7191
+ const crossLayerEdges = crossRefsAsEdges(graph).filter((e) => visited.has(e.source));
7192
+ return { nodes, edges: [...sameLayerEdges, ...crossLayerEdges], budgetExceeded, stoppedAtHop };
5666
7193
  }
5667
7194
  function reverseNeighborhood(graph, centerId, hops, direction) {
5668
7195
  const center = graph.nodes.find((n) => n.id === centerId);
@@ -5692,7 +7219,9 @@ function reverseNeighborhood(graph, centerId, hops, direction) {
5692
7219
  return { nodes: visited, edges };
5693
7220
  }
5694
7221
  function handleBlastPoints(args) {
5695
- const rootDir = process.cwd();
7222
+ const __resolved = resolveOrErr(args);
7223
+ if ("content" in __resolved) return __resolved;
7224
+ const { rootDir } = __resolved;
5696
7225
  const nodeId = args.node_id;
5697
7226
  const requestedLayer = args.layer;
5698
7227
  const hops = args.hops ?? 2;
@@ -5736,7 +7265,11 @@ function handleBlastPoints(args) {
5736
7265
  for (const otherLayer of otherLayers) {
5737
7266
  const otherGraph = readGraph(rootDir, otherLayer);
5738
7267
  if (!otherGraph) continue;
5739
- for (const edge of otherGraph.edges) {
7268
+ const candidates = [
7269
+ ...otherGraph.edges,
7270
+ ...otherGraph.cross_refs ?? []
7271
+ ];
7272
+ for (const edge of candidates) {
5740
7273
  if (edge.target === nodeId || edge.source === nodeId) {
5741
7274
  const dependentId = edge.target === nodeId ? edge.source : edge.target;
5742
7275
  if (affected.some((a) => a.id === dependentId)) continue;
@@ -5763,7 +7296,7 @@ function handleBlastPoints(args) {
5763
7296
  byHop[String(a.hop)] = (byHop[String(a.hop)] ?? 0) + 1;
5764
7297
  if (a.module) modulesSet.add(a.module);
5765
7298
  }
5766
- const crossesLayers = Object.keys(byLayer).length > 1;
7299
+ const crossesLayers = Object.keys(byLayer).some((l) => l !== targetLayer);
5767
7300
  const centerTags = center.tags;
5768
7301
  return okJson({
5769
7302
  center: {
@@ -5814,26 +7347,89 @@ function okJson(data) {
5814
7347
  function err(text) {
5815
7348
  return { content: [{ type: "text", text }], isError: true };
5816
7349
  }
7350
+ function resolveOrErr(args) {
7351
+ try {
7352
+ return { rootDir: resolveProjectRoot(args.project, process.cwd()) };
7353
+ } catch (e) {
7354
+ return err(e.message);
7355
+ }
7356
+ }
5817
7357
  async function handleGenerateGraph(args) {
5818
- const rootDir = process.cwd();
7358
+ const monorepoRoot = process.cwd();
5819
7359
  const layer = args.layer;
5820
- const results = await generateGraph(rootDir, layer);
7360
+ const projectArg = typeof args.project === "string" ? args.project.trim() : "";
7361
+ function formatProjectResult(results2, relativeRoot) {
7362
+ return results2.map((r) => {
7363
+ const warnings = r.output.warnings.length;
7364
+ return ` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`;
7365
+ }).join("\n") + `
7366
+ \u2192 ${relativeRoot}/.launchsecure/graphs/`;
7367
+ }
7368
+ if (projectArg) {
7369
+ let rootDir;
7370
+ try {
7371
+ rootDir = resolveProjectRoot(projectArg, monorepoRoot);
7372
+ } catch (e) {
7373
+ return err(e.message);
7374
+ }
7375
+ const results2 = await generateGraph(rootDir, layer);
7376
+ if (results2.length === 0) {
7377
+ return err(
7378
+ layer ? `No parser detected for the "${layer}" layer in project "${projectArg}".` : `No parsers detected for project "${projectArg}". Check that the project root has the expected structure.`
7379
+ );
7380
+ }
7381
+ return ok(
7382
+ `Graph generated successfully for project "${projectArg}".
7383
+
7384
+ ${formatProjectResult(results2, projectArg)}
7385
+
7386
+ Use read_graph (with project="${projectArg}") to query.`
7387
+ );
7388
+ }
7389
+ const projects = listProjects(monorepoRoot);
7390
+ if (projects.length > 0) {
7391
+ const lines = [];
7392
+ let okCount = 0;
7393
+ let failCount = 0;
7394
+ for (const proj of projects) {
7395
+ try {
7396
+ const results2 = await generateGraph(proj.absoluteRoot, layer);
7397
+ if (results2.length === 0) {
7398
+ lines.push(` \u2717 "${proj.name}" (${proj.root}): no parsers detected`);
7399
+ failCount++;
7400
+ continue;
7401
+ }
7402
+ lines.push(` \u2713 "${proj.name}" (${proj.root}):`);
7403
+ for (const r of results2) {
7404
+ const warnings = r.output.warnings.length;
7405
+ lines.push(` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`);
7406
+ }
7407
+ okCount++;
7408
+ } catch (e) {
7409
+ lines.push(` \u2717 "${proj.name}" (${proj.root}): ${e.message}`);
7410
+ failCount++;
7411
+ }
7412
+ }
7413
+ const header = `Regenerated ${okCount}/${projects.length} project graph(s)${failCount ? ` (${failCount} failed)` : ""}.`;
7414
+ return failCount > 0 && okCount === 0 ? err(`${header}
7415
+
7416
+ ${lines.join("\n")}`) : ok(`${header}
7417
+
7418
+ ${lines.join("\n")}
7419
+
7420
+ Use read_graph (with optional project="<name>") to query.`);
7421
+ }
7422
+ const results = await generateGraph(monorepoRoot, layer);
5821
7423
  if (results.length === 0) {
5822
7424
  return err(
5823
7425
  layer ? `No parser detected for the "${layer}" layer in this project.` : "No parsers detected for this project. Check that the project has the expected structure."
5824
7426
  );
5825
7427
  }
5826
- const summary = results.map((r) => {
5827
- const warnings = r.output.warnings.length;
5828
- return ` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`;
5829
- }).join("\n");
5830
7428
  return ok(
5831
7429
  `Graph generated successfully.
5832
7430
 
5833
7431
  Layers:
5834
- ${summary}
5835
-
5836
- Output: .launchsecure/graphs/
7432
+ ${formatProjectResult(results, ".")}
5837
7433
 
5838
7434
  Use read_graph with filters (search/type/module/node_id) to query.`
5839
7435
  );
@@ -5916,8 +7512,6 @@ function runReadGraphQueryRaw(rootDir, args) {
5916
7512
  if (tagKey && tagValue && nodeTags?.[tagKey] !== tagValue) return false;
5917
7513
  return true;
5918
7514
  });
5919
- const matchedIds = new Set(matched.map((n) => n.id));
5920
- const matchedEdges = graph.edges.filter((e) => matchedIds.has(e.source) && matchedIds.has(e.target));
5921
7515
  if (matched.length === 0) {
5922
7516
  return {
5923
7517
  layer,
@@ -5931,7 +7525,9 @@ function runReadGraphQueryRaw(rootDir, args) {
5931
7525
  const hasMore = offset + paginatedNodes.length < totalMatched;
5932
7526
  const wantEdges = includeEdges ?? false;
5933
7527
  const returnedIds = new Set(paginatedNodes.map((n) => n.id));
5934
- const returnedEdges = graph.edges.filter((e) => returnedIds.has(e.source) && returnedIds.has(e.target));
7528
+ const sameLayerReturnedEdges = graph.edges.filter((e) => returnedIds.has(e.source) && returnedIds.has(e.target));
7529
+ const crossLayerReturnedEdges = crossRefsAsEdges(graph).filter((e) => returnedIds.has(e.source));
7530
+ const returnedEdges = [...sameLayerReturnedEdges, ...crossLayerReturnedEdges];
5935
7531
  const result = {
5936
7532
  layer,
5937
7533
  filter: { search, type, module: module_ },
@@ -5946,7 +7542,14 @@ function runReadGraphQueryRaw(rootDir, args) {
5946
7542
  result.next_offset = offset + paginatedNodes.length;
5947
7543
  }
5948
7544
  if (wantEdges) {
5949
- result.edges = returnedEdges;
7545
+ if (returnedEdges.length > MAX_FILTER_EDGES) {
7546
+ result.edges = returnedEdges.slice(0, MAX_FILTER_EDGES);
7547
+ result.edges_truncated = true;
7548
+ result.total_edges = returnedEdges.length;
7549
+ result.edges_hint = `Showing first ${MAX_FILTER_EDGES} of ${returnedEdges.length} edges. Narrow with type/module/node_id, or call read_graph on a single node_id to walk its neighborhood with full edges.`;
7550
+ } else {
7551
+ result.edges = returnedEdges;
7552
+ }
5950
7553
  } else if (returnedEdges.length > 0) {
5951
7554
  result.edges_hint = `${returnedEdges.length} edges between matched nodes omitted. Pass include_edges:true to retrieve them (only do this when you actually need edge data).`;
5952
7555
  }
@@ -5957,12 +7560,13 @@ function runReadGraphQuery(rootDir, args) {
5957
7560
  return compactResult(raw);
5958
7561
  }
5959
7562
  function handleReadGraph(args) {
5960
- const rootDir = process.cwd();
7563
+ const monorepoRoot = process.cwd();
5961
7564
  if (Array.isArray(args.queries)) {
5962
7565
  const queries = args.queries;
5963
7566
  if (queries.length === 0) {
5964
7567
  return err("queries array is empty. Provide at least one query object.");
5965
7568
  }
7569
+ const inheritedProject = typeof args.project === "string" ? args.project : void 0;
5966
7570
  const results = [];
5967
7571
  let cumulativeChars = 0;
5968
7572
  let budgetHit = false;
@@ -5980,7 +7584,15 @@ function handleReadGraph(args) {
5980
7584
  });
5981
7585
  continue;
5982
7586
  }
5983
- const r = runReadGraphQuery(rootDir, q);
7587
+ const qWithProject = inheritedProject && !q.project ? { ...q, project: inheritedProject } : q;
7588
+ let perQueryRoot;
7589
+ try {
7590
+ perQueryRoot = resolveProjectRoot(qWithProject.project, monorepoRoot);
7591
+ } catch (e) {
7592
+ results.push({ index: i, query: q, result: { error: e.message } });
7593
+ continue;
7594
+ }
7595
+ const r = runReadGraphQuery(perQueryRoot, qWithProject);
5984
7596
  const entry = { index: i, query: q, result: r };
5985
7597
  const entrySize = JSON.stringify(entry, null, 2).length;
5986
7598
  if (cumulativeChars + entrySize > BATCH_BUDGET_CHARS && results.length > 0) {
@@ -6007,20 +7619,24 @@ function handleReadGraph(args) {
6007
7619
  results
6008
7620
  });
6009
7621
  }
6010
- const result = runReadGraphQuery(rootDir, args);
7622
+ const __resolved = resolveOrErr(args);
7623
+ if ("content" in __resolved) return __resolved;
7624
+ const result = runReadGraphQuery(__resolved.rootDir, args);
6011
7625
  return okJson(result);
6012
7626
  }
6013
7627
  function nodeToFilePath(rootDir, layer, nodeId) {
6014
- if (layer === "ui" || layer === "api") return (0, import_node_path21.join)(rootDir, "src", nodeId);
6015
- if (layer === "db") return (0, import_node_path21.join)(rootDir, "prisma", "schema.prisma");
6016
- const withSrc = (0, import_node_path21.join)(rootDir, "src", nodeId);
6017
- if ((0, import_node_fs20.existsSync)(withSrc)) return withSrc;
6018
- const direct = (0, import_node_path21.join)(rootDir, nodeId);
6019
- if ((0, import_node_fs20.existsSync)(direct)) return direct;
7628
+ if (layer === "ui" || layer === "api") return (0, import_node_path26.join)(rootDir, "src", nodeId);
7629
+ if (layer === "db") return (0, import_node_path26.join)(rootDir, "prisma", "schema.prisma");
7630
+ const withSrc = (0, import_node_path26.join)(rootDir, "src", nodeId);
7631
+ if ((0, import_node_fs23.existsSync)(withSrc)) return withSrc;
7632
+ const direct = (0, import_node_path26.join)(rootDir, nodeId);
7633
+ if ((0, import_node_fs23.existsSync)(direct)) return direct;
6020
7634
  return null;
6021
7635
  }
6022
7636
  function handleInspectNode(args) {
6023
- const rootDir = process.cwd();
7637
+ const __resolved = resolveOrErr(args);
7638
+ if ("content" in __resolved) return __resolved;
7639
+ const { rootDir } = __resolved;
6024
7640
  const layer = args.layer;
6025
7641
  const nodeId = args.node_id;
6026
7642
  const search = args.search;
@@ -6044,7 +7660,7 @@ function handleInspectNode(args) {
6044
7660
  } else {
6045
7661
  matched = graph.nodes;
6046
7662
  }
6047
- const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params"];
7663
+ const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params", "effects"];
6048
7664
  const requestedFields = fields ?? allDeepFields;
6049
7665
  let filterRegex = null;
6050
7666
  if (filter) {
@@ -6100,7 +7716,9 @@ function handleInspectNode(args) {
6100
7716
  });
6101
7717
  }
6102
7718
  function handleGrepNodes(args) {
6103
- const rootDir = process.cwd();
7719
+ const __resolved = resolveOrErr(args);
7720
+ if ("content" in __resolved) return __resolved;
7721
+ const { rootDir } = __resolved;
6104
7722
  const pattern = args.pattern;
6105
7723
  const layer = args.layer;
6106
7724
  if (!pattern) return err("pattern is required");
@@ -6159,11 +7777,11 @@ function handleGrepNodes(args) {
6159
7777
  let filesSearched = 0;
6160
7778
  let truncated = false;
6161
7779
  for (const [filePath, nodeId] of filePaths) {
6162
- if (!(0, import_node_fs20.existsSync)(filePath)) continue;
7780
+ if (!(0, import_node_fs23.existsSync)(filePath)) continue;
6163
7781
  filesSearched++;
6164
7782
  let content;
6165
7783
  try {
6166
- content = (0, import_node_fs20.readFileSync)(filePath, "utf-8");
7784
+ content = (0, import_node_fs23.readFileSync)(filePath, "utf-8");
6167
7785
  } catch {
6168
7786
  continue;
6169
7787
  }
@@ -6200,11 +7818,61 @@ function handleGrepNodes(args) {
6200
7818
  truncated
6201
7819
  });
6202
7820
  }
7821
+ function handleEffectsIndex(args) {
7822
+ const __resolved = resolveOrErr(args);
7823
+ if ("content" in __resolved) return __resolved;
7824
+ const { rootDir } = __resolved;
7825
+ const idx = readEffectsIndex(rootDir);
7826
+ if (!idx) {
7827
+ return err("No effects-index.json found. Run generate_graph first (or wait for the file-watcher to fire).");
7828
+ }
7829
+ const kind = args.kind ?? "collisions";
7830
+ const key = args.key;
7831
+ const MAX_KEYS = 200;
7832
+ if (kind === "singleton_risks") {
7833
+ return okJson({ kind, count: idx.singleton_risks.length, nodes: idx.singleton_risks });
7834
+ }
7835
+ if (kind === "collisions") {
7836
+ return okJson({
7837
+ kind,
7838
+ dom_ids: idx.collisions.dom_ids,
7839
+ storage_keys: idx.collisions.storage_keys,
7840
+ window_events: idx.collisions.window_events
7841
+ });
7842
+ }
7843
+ const map = idx[kind];
7844
+ if (!map || typeof map !== "object") {
7845
+ return err(`Unknown kind "${kind}". Valid: dom_ids, window_events, storage_keys, fetch_urls, timers, singleton_risks, collisions.`);
7846
+ }
7847
+ if (key) {
7848
+ const nodes = map[key] ?? [];
7849
+ return okJson({ kind, key, nodes });
7850
+ }
7851
+ const entries = Object.entries(map);
7852
+ const truncated = entries.length > MAX_KEYS;
7853
+ const slice = entries.slice(0, MAX_KEYS);
7854
+ const results = {};
7855
+ for (const [k, v] of slice) results[k] = v;
7856
+ return okJson({
7857
+ kind,
7858
+ total_keys: entries.length,
7859
+ ...truncated ? { truncated: true, showing: MAX_KEYS, hint: "Pass `key` to look up a specific value." } : {},
7860
+ results
7861
+ });
7862
+ }
6203
7863
  function handleChartServerStatus() {
6204
7864
  const rootDir = process.cwd();
6205
7865
  const lock = getLiveLock(rootDir);
7866
+ const watcher = getWatcherHandle();
7867
+ const watcherInfo = watcher ? {
7868
+ active: true,
7869
+ regenerating: watcher.isRegenerating(),
7870
+ last_regen_at: watcher.lastRegenAt(),
7871
+ pending_events: watcher.pendingCount(),
7872
+ ...watcher.lastError() ? { last_error: watcher.lastError() } : {}
7873
+ } : { active: false };
6206
7874
  if (!lock) {
6207
- return okJson({ running: false });
7875
+ return okJson({ running: false, watcher: watcherInfo });
6208
7876
  }
6209
7877
  return okJson({
6210
7878
  running: true,
@@ -6212,7 +7880,8 @@ function handleChartServerStatus() {
6212
7880
  port: lock.port,
6213
7881
  pid: lock.pid,
6214
7882
  cwd: lock.cwd,
6215
- startedAt: lock.startedAt
7883
+ startedAt: lock.startedAt,
7884
+ watcher: watcherInfo
6216
7885
  });
6217
7886
  }
6218
7887
  function handleStartChartServer(args) {
@@ -6228,11 +7897,11 @@ function handleStartChartServer(args) {
6228
7897
  });
6229
7898
  }
6230
7899
  const entryPath = process.argv[1];
6231
- const logDir = (0, import_node_path21.join)((0, import_node_os2.homedir)(), ".launchsecure");
6232
- (0, import_node_fs20.mkdirSync)(logDir, { recursive: true });
6233
- const logPath = (0, import_node_path21.join)(logDir, "launch-chart.log");
6234
- const out = (0, import_node_fs20.openSync)(logPath, "a");
6235
- const err2 = (0, import_node_fs20.openSync)(logPath, "a");
7900
+ const logDir = (0, import_node_path26.join)((0, import_node_os2.homedir)(), ".launchsecure");
7901
+ (0, import_node_fs23.mkdirSync)(logDir, { recursive: true });
7902
+ const logPath = (0, import_node_path26.join)(logDir, "launch-chart.log");
7903
+ const out = (0, import_node_fs23.openSync)(logPath, "a");
7904
+ const err2 = (0, import_node_fs23.openSync)(logPath, "a");
6236
7905
  const portArgs = args.port ? ["--port", String(args.port)] : [];
6237
7906
  const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
6238
7907
  detached: true,
@@ -6265,7 +7934,9 @@ function handleStopChartServer() {
6265
7934
  }
6266
7935
  }
6267
7936
  function handleAddTag(args) {
6268
- const rootDir = process.cwd();
7937
+ const __resolved = resolveOrErr(args);
7938
+ if ("content" in __resolved) return __resolved;
7939
+ const { rootDir } = __resolved;
6269
7940
  const nodeId = args.node_id;
6270
7941
  const key = args.key;
6271
7942
  const value = args.value;
@@ -6287,7 +7958,9 @@ function handleAddTag(args) {
6287
7958
  return okJson({ ok: true, node_id: nodeId, tag: { [key]: value } });
6288
7959
  }
6289
7960
  function handleRemoveTag(args) {
6290
- const rootDir = process.cwd();
7961
+ const __resolved = resolveOrErr(args);
7962
+ if ("content" in __resolved) return __resolved;
7963
+ const { rootDir } = __resolved;
6291
7964
  const nodeId = args.node_id;
6292
7965
  const key = args.key;
6293
7966
  if (!nodeId) return err("node_id is required");
@@ -6296,7 +7969,9 @@ function handleRemoveTag(args) {
6296
7969
  return okJson({ ok: true, node_id: nodeId, removed_key: key });
6297
7970
  }
6298
7971
  function handleAuditLayer(args) {
6299
- const rootDir = process.cwd();
7972
+ const __resolved = resolveOrErr(args);
7973
+ if ("content" in __resolved) return __resolved;
7974
+ const { rootDir } = __resolved;
6300
7975
  const layer = args.layer;
6301
7976
  const check = args.check;
6302
7977
  if (!layer) return err("layer is required");
@@ -6309,6 +7984,10 @@ function handleAuditLayer(args) {
6309
7984
  lines.push(`Audit: ${layer}${check ? ` / ${check}` : ""} \u2014 ${totalFindings} findings (${totalErrors} errors, ${totalWarnings} warnings, ${totalInfo} info)`);
6310
7985
  lines.push("");
6311
7986
  for (const report of reports) {
7987
+ if (report.status === "skipped") {
7988
+ lines.push(`\u2298 ${report.check}: skipped \u2014 ${report.skipReason ?? "comparison target missing"} (NOT a pass)`);
7989
+ continue;
7990
+ }
6312
7991
  if (report.findings.length === 0) {
6313
7992
  lines.push(`\u2713 ${report.check}: no issues found`);
6314
7993
  continue;
@@ -6352,20 +8031,20 @@ function handleDetectProjectStack() {
6352
8031
  if (ref.type === "references_api") stats.references_api++;
6353
8032
  }
6354
8033
  }
6355
- const srcDir = (0, import_node_path21.join)(rootDir, "src");
6356
- if ((0, import_node_fs20.existsSync)(srcDir)) {
8034
+ const srcDir = (0, import_node_path26.join)(rootDir, "src");
8035
+ if ((0, import_node_fs23.existsSync)(srcDir)) {
6357
8036
  const scanDir = (dir) => {
6358
- if (!(0, import_node_fs20.existsSync)(dir)) return;
6359
- for (const entry of (0, import_node_fs20.readdirSync)(dir, { withFileTypes: true })) {
8037
+ if (!(0, import_node_fs23.existsSync)(dir)) return;
8038
+ for (const entry of (0, import_node_fs23.readdirSync)(dir, { withFileTypes: true })) {
6360
8039
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
6361
- const full = (0, import_node_path21.join)(dir, entry.name);
8040
+ const full = (0, import_node_path26.join)(dir, entry.name);
6362
8041
  if (entry.isDirectory()) {
6363
8042
  scanDir(full);
6364
8043
  continue;
6365
8044
  }
6366
- if (![".ts", ".tsx"].includes((0, import_node_path21.extname)(entry.name))) continue;
8045
+ if (![".ts", ".tsx"].includes((0, import_node_path26.extname)(entry.name))) continue;
6367
8046
  try {
6368
- const content = (0, import_node_fs20.readFileSync)(full, "utf-8");
8047
+ const content = (0, import_node_fs23.readFileSync)(full, "utf-8");
6369
8048
  const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
6370
8049
  if (matches) stats.annotations += matches.length;
6371
8050
  } catch {
@@ -6380,6 +8059,12 @@ function handleDetectProjectStack() {
6380
8059
  const languages = detectLanguages(rootDir, supportedLanguages);
6381
8060
  const unsupported = languages.filter((l) => !l.supported);
6382
8061
  const unsupportedHint = unsupported.length > 0 ? unsupported.map((l) => `${l.id} (${l.fileCount} files)`).join(", ") + " \u2014 detected but not yet supported" : null;
8062
+ const projects = listProjects(rootDir).map((p) => ({
8063
+ name: p.name,
8064
+ root: p.root,
8065
+ absolute_root: p.absoluteRoot,
8066
+ has_graph: (0, import_node_fs23.existsSync)((0, import_node_path26.join)(p.absoluteRoot, ".launchsecure", "graphs"))
8067
+ }));
6383
8068
  return okJson({
6384
8069
  languages,
6385
8070
  parsers: parserResults,
@@ -6402,7 +8087,11 @@ function handleDetectProjectStack() {
6402
8087
  stats,
6403
8088
  ...unsupportedHint ? { unsupported_hint: unsupportedHint } : {},
6404
8089
  current_config: Object.keys(config).length > 0 ? config : null,
6405
- config_path: ".launchchart.json"
8090
+ config_path: ".launchchart.json",
8091
+ ...projects.length > 0 && {
8092
+ projects,
8093
+ projects_hint: 'Pass `project: "<name>"` (or its root path) to other tools to target a sub-project. Omitting `project` targets the monorepo root. `generate_graph` with no `project` regenerates ALL configured projects.'
8094
+ }
6406
8095
  });
6407
8096
  }
6408
8097
  function send(msg) {
@@ -6452,6 +8141,10 @@ async function handleMessage(msg) {
6452
8141
  respond(id ?? null, handleInspectNode(args));
6453
8142
  return;
6454
8143
  }
8144
+ if (toolName === "effects_index") {
8145
+ respond(id ?? null, handleEffectsIndex(args));
8146
+ return;
8147
+ }
6455
8148
  if (toolName === "chart_server_status") {
6456
8149
  respond(id ?? null, handleChartServerStatus());
6457
8150
  return;
@@ -6495,6 +8188,9 @@ async function handleMessage(msg) {
6495
8188
  respondError(id, -32601, `Method not found: ${method}`);
6496
8189
  }
6497
8190
  }
8191
+ function getWatcherHandle() {
8192
+ return watcherHandle;
8193
+ }
6498
8194
  function startGraphMcpServer() {
6499
8195
  process.stdin.setEncoding("utf-8");
6500
8196
  let buffer = "";
@@ -6514,17 +8210,39 @@ function startGraphMcpServer() {
6514
8210
  }
6515
8211
  });
6516
8212
  process.stdin.on("end", () => {
8213
+ watcherHandle?.stop();
6517
8214
  process.exit(0);
6518
8215
  });
8216
+ if (process.env.LAUNCH_CHART_WATCH !== "0") {
8217
+ try {
8218
+ const { startGraphWatcher: startGraphWatcher2 } = (init_watcher(), __toCommonJS(watcher_exports));
8219
+ watcherHandle = startGraphWatcher2(process.cwd(), {
8220
+ onRegen: ({ changedFiles, durationMs }) => {
8221
+ const sample = changedFiles.slice(0, 3).join(", ") + (changedFiles.length > 3 ? `, +${changedFiles.length - 3} more` : "");
8222
+ process.stderr.write(`[launchsecure-graph] regen ${durationMs}ms (${sample})
8223
+ `);
8224
+ },
8225
+ onError: (err2) => {
8226
+ process.stderr.write(`[launchsecure-graph] watcher error: ${err2.message}
8227
+ `);
8228
+ }
8229
+ });
8230
+ process.stderr.write(`[launchsecure-graph] watcher started (LAUNCH_CHART_WATCH=0 to disable)
8231
+ `);
8232
+ } catch (err2) {
8233
+ process.stderr.write(`[launchsecure-graph] watcher failed to start: ${err2}
8234
+ `);
8235
+ }
8236
+ }
6519
8237
  process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
6520
8238
  `);
6521
8239
  }
6522
- var import_node_fs20, import_node_path21, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, DEEP_FIELDS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, DEFAULT_EST_NODE_FULL, DEFAULT_EST_NODE_MIN, DEFAULT_EST_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
8240
+ var import_node_fs23, import_node_path26, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, DEEP_FIELDS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, DEFAULT_EST_NODE_FULL, DEFAULT_EST_NODE_MIN, DEFAULT_EST_EDGE, NEIGHBORHOOD_BUDGET_CHARS, MAX_FILTER_EDGES, BATCH_BUDGET_CHARS, watcherHandle;
6523
8241
  var init_graph_mcp = __esm({
6524
8242
  "src/server/graph-mcp.ts"() {
6525
8243
  "use strict";
6526
- import_node_fs20 = require("node:fs");
6527
- import_node_path21 = require("node:path");
8244
+ import_node_fs23 = require("node:fs");
8245
+ import_node_path26 = require("node:path");
6528
8246
  import_node_child_process2 = require("node:child_process");
6529
8247
  import_node_os2 = require("node:os");
6530
8248
  init_graph();
@@ -6533,6 +8251,7 @@ var init_graph_mcp = __esm({
6533
8251
  init_parser_registry();
6534
8252
  init_language_detection();
6535
8253
  init_audit_core();
8254
+ init_projects();
6536
8255
  SERVER_INFO = {
6537
8256
  name: "launchsecure-graph",
6538
8257
  version: "0.0.1"
@@ -6540,20 +8259,24 @@ var init_graph_mcp = __esm({
6540
8259
  TOOLS = [
6541
8260
  {
6542
8261
  name: "generate_graph",
6543
- description: "Regenerate the structural project graph by scanning source code in the current working directory. Auto-detects project languages and frameworks, then parses into layers (e.g. ui, api, db) based on content classification. Writes JSON to .launchsecure/graphs/{layer}.json and returns node/edge counts. Run this when project structure has changed (new files, moved components, schema updates). Fast: typically <1s. After generation, use read_graph with filters to query.",
8262
+ description: "Regenerate the structural project graph by scanning source code in the current working directory. Auto-detects project languages and frameworks, then parses into layers (e.g. ui, api, db) based on content classification. Writes JSON to .launchsecure/graphs/{layer}.json and returns node/edge counts. Run this when project structure has changed (new files, moved components, schema updates). Fast: typically <1s. After generation, use read_graph with filters to query.\n\nMONOREPO BEHAVIOR: when .launchchart.json declares projects[] and no `project` arg is given, regenerates EVERY configured project (each writes to its own <root>/.launchsecure/graphs/). Pass `project` to limit regeneration to a single sub-project.",
6544
8263
  inputSchema: {
6545
8264
  type: "object",
6546
8265
  properties: {
6547
8266
  layer: {
6548
8267
  type: "string",
6549
8268
  description: "Specific layer to regenerate (e.g. 'ui', 'api', 'db'). Omit to regenerate all detectable layers. Run detect_project_stack to see available layers."
8269
+ },
8270
+ project: {
8271
+ type: "string",
8272
+ description: PROJECT_PARAM_DESCRIPTION + " Special: omit to regenerate ALL configured projects."
6550
8273
  }
6551
8274
  }
6552
8275
  }
6553
8276
  },
6554
8277
  {
6555
8278
  name: "read_graph",
6556
- description: 'Query the structural project graph \u2014 use INSTEAD of Glob and Grep for locating files, understanding structure, and navigating the codebase. Faster and more accurate than file-system search because it returns typed nodes with metadata and relationships. \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module", "which endpoints touch the User table", "what auth strategy does this endpoint use". \n\nDO NOT USE FOR: understanding what\'s INSIDE a component (use inspect_node for elements, conditions, state, variables, responses), reading actual source code (use Read). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nPAGINATION (filter queries):\n- Use `offset` and `limit` to paginate through large result sets.\n- Response includes: `total` (matched), `returned` (in this page), `has_more`, `next_offset`.\n- If `has_more: true`, call again with `offset: next_offset` to get the next page.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
8279
+ description: 'Query the structural project graph \u2014 use INSTEAD of Glob and Grep for locating files, understanding structure, and navigating the codebase. Faster and more accurate than file-system search because it returns typed nodes with metadata and relationships. \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module", "which endpoints touch the User table", "what auth strategy does this endpoint use". \n\nDO NOT USE FOR: understanding what\'s INSIDE a component (use inspect_node for elements, conditions, state, variables, responses), reading actual source code (use Read). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nPAGINATION (filter queries):\n- Use `offset` and `limit` to paginate through large result sets.\n- Response includes: `total` (matched), `returned` (in this page), `has_more`, `next_offset`.\n- If `has_more: true`, call again with `offset: next_offset` to get the next page.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.\n\nMONOREPOS: pass `project: "<name>"` to query a sub-project graph (defined in .launchchart.json projects[]). Omitting `project` targets the monorepo root. In batch mode the top-level `project` is inherited by sub-queries that do not set their own. Run detect_project_stack to list configured projects.',
6557
8280
  inputSchema: {
6558
8281
  type: "object",
6559
8282
  properties: {
@@ -6607,7 +8330,7 @@ var init_graph_mcp = __esm({
6607
8330
  },
6608
8331
  queries: {
6609
8332
  type: "array",
6610
- description: "Batch mode \u2014 array of query objects to run in a single call. Each uses the same param schema. When set, top-level params are ignored. Subject to an aggregate size budget \u2014 later queries may return a skipped stub.",
8333
+ description: "Batch mode \u2014 array of query objects to run in a single call. Each uses the same param schema (including `project`). When set, top-level params are ignored. Subject to an aggregate size budget \u2014 later queries may return a skipped stub.",
6611
8334
  items: {
6612
8335
  type: "object",
6613
8336
  properties: {
@@ -6618,9 +8341,14 @@ var init_graph_mcp = __esm({
6618
8341
  node_id: { type: "string" },
6619
8342
  hops: { type: "number" },
6620
8343
  minimal: { type: "boolean" },
6621
- include_edges: { type: "boolean" }
8344
+ include_edges: { type: "boolean" },
8345
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
6622
8346
  }
6623
8347
  }
8348
+ },
8349
+ project: {
8350
+ type: "string",
8351
+ description: PROJECT_PARAM_DESCRIPTION
6624
8352
  }
6625
8353
  }
6626
8354
  }
@@ -6667,16 +8395,17 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
6667
8395
  case_insensitive: { type: "boolean", description: "Case-insensitive regex. Default false." },
6668
8396
  context: { type: "number", description: "Context lines around each match. Default 2." },
6669
8397
  max_matches: { type: "number", description: "Max matches to return total. Default 50." },
6670
- max_files: { type: "number", description: "Max files to search. Default 50." }
8398
+ max_files: { type: "number", description: "Max files to search. Default 50." },
8399
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
6671
8400
  },
6672
8401
  required: ["layer", "pattern"]
6673
8402
  }
6674
8403
  },
6675
8404
  {
6676
8405
  name: "inspect_node",
6677
- description: `Get deep AST data for specific graph nodes \u2014 what's INSIDE a component or endpoint. Returns elements (JSX), state hooks, conditions, variables, responses, and request params. Use INSTEAD of Grep/Read when you need to understand component internals without reading source.
8406
+ description: `Get deep AST data for specific graph nodes \u2014 what's INSIDE a component or endpoint. Returns elements (JSX), state hooks, conditions, variables, responses, request params, and effects. Use INSTEAD of Grep/Read when you need to understand component internals without reading source.
6678
8407
 
6679
- USE THIS FOR: "what elements does LoginPage have?", "what conditions does the login endpoint check?", "what state does SettingsPage manage?", "what responses can this endpoint return?", "what validation does this API do?", "what props does this component accept?", "which endpoints check for isAdmin?", "find all conditions mentioning rateLimit"
8408
+ USE THIS FOR: "what elements does LoginPage have?", "what conditions does the login endpoint check?", "what state does SettingsPage manage?", "what responses can this endpoint return?", "what validation does this API do?", "what props does this component accept?", "which endpoints check for isAdmin?", "find all conditions mentioning rateLimit", "is this function safe to instantiate twice?" (check effects.subscribes / .timers / .dom_writes / .persists / .globals)
6680
8409
 
6681
8410
  DO NOT USE FOR: structural queries (use read_graph), content search (use grep_nodes).
6682
8411
 
@@ -6699,7 +8428,7 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
6699
8428
  fields: {
6700
8429
  type: "array",
6701
8430
  items: { type: "string" },
6702
- description: "Specific deep fields to return. Options: elements, stateVars, conditions, variables, responses, params. Omit for all."
8431
+ description: 'Specific deep fields to return. Options: elements, stateVars, conditions, variables, responses, params, effects. Omit for all. "effects" returns the file-level side-effect summary (calls, dom_writes, subscribes, timers, persists, fetches, globals); per-arrow-function effects are also attached to each entry in "variables".'
6703
8432
  },
6704
8433
  filter: {
6705
8434
  type: "string",
@@ -6708,7 +8437,8 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
6708
8437
  case_insensitive: {
6709
8438
  type: "boolean",
6710
8439
  description: "Case-insensitive filter matching. Default true."
6711
- }
8440
+ },
8441
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
6712
8442
  },
6713
8443
  required: ["layer"]
6714
8444
  }
@@ -6744,6 +8474,25 @@ Use this when the user asks "is the chart running", "show me the project graph U
6744
8474
  properties: {}
6745
8475
  }
6746
8476
  },
8477
+ {
8478
+ name: "effects_index",
8479
+ description: 'Cross-layer inverted index over per-node side effects. Answers "who else touches X?" without re-walking every node. Built automatically when generate_graph runs (or whenever the watcher regenerates). \n\nUSE THIS FOR: "is mountFoo safe to instantiate twice?" (kind="singleton_risks"), "who writes DOM id moon-shadow-blur?" (kind="dom_ids", key="moon-shadow-blur"), "which files attach a window keydown listener?" (kind="window_events", key="window:keydown"), "who else writes localStorage key panchang.settings.v1?" (kind="storage_keys", key="..."), "any DOM-id collisions?" (kind="collisions"). \n\nReturns: { kind, results } where results is a {key: [nodeIds]} map for the chosen kind, or a list of multi-writer collisions when kind="collisions", or a flat node-id list when kind="singleton_risks".',
8480
+ inputSchema: {
8481
+ type: "object",
8482
+ properties: {
8483
+ kind: {
8484
+ type: "string",
8485
+ enum: ["dom_ids", "window_events", "storage_keys", "fetch_urls", "timers", "singleton_risks", "collisions"],
8486
+ description: "Which inverted index to query. Default: collisions (most actionable signal)."
8487
+ },
8488
+ key: {
8489
+ type: "string",
8490
+ description: 'Optional specific key to look up within the chosen kind (e.g. "moon-shadow-blur"). When omitted, returns the full {key:nodes} map for the kind.'
8491
+ },
8492
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
8493
+ }
8494
+ }
8495
+ },
6747
8496
  {
6748
8497
  name: "detect_project_stack",
6749
8498
  description: "Detect project languages, frameworks, available parsers, and recommend parser configuration. Scans the project to identify all languages present (TypeScript, Python, Go, etc.) and reports which are supported by registered parsers vs detected-but-unsupported. Also detects frameworks (Next.js, Prisma, React, etc.), provides cross-layer detection stats (fetch calls, @api annotations, URL literals), and returns available graph layers. \n\nUse this when setting up launch-chart for a new project, reviewing parser configuration, or checking what languages are in the project.",
@@ -6769,7 +8518,8 @@ Use this when the user asks "is the chart running", "show me the project graph U
6769
8518
  value: {
6770
8519
  type: "string",
6771
8520
  description: 'Tag value (e.g. "auth", "alice", "true").'
6772
- }
8521
+ },
8522
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
6773
8523
  },
6774
8524
  required: ["node_id", "key", "value"]
6775
8525
  }
@@ -6787,7 +8537,8 @@ Use this when the user asks "is the chart running", "show me the project graph U
6787
8537
  key: {
6788
8538
  type: "string",
6789
8539
  description: "Tag key to remove."
6790
- }
8540
+ },
8541
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
6791
8542
  },
6792
8543
  required: ["node_id", "key"]
6793
8544
  }
@@ -6805,7 +8556,8 @@ Use this when the user asks "is the chart running", "show me the project graph U
6805
8556
  check: {
6806
8557
  type: "string",
6807
8558
  description: "Specific check to run (e.g. 'schema_drift', 'unprotected_routes'). Omit to run all checks for the layer."
6808
- }
8559
+ },
8560
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
6809
8561
  },
6810
8562
  required: ["layer"]
6811
8563
  }
@@ -6840,7 +8592,8 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
6840
8592
  type: "string",
6841
8593
  enum: ["reverse", "both"],
6842
8594
  description: "'reverse' (default) = only what depends on this node. 'both' = full neighborhood."
6843
- }
8595
+ },
8596
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
6844
8597
  },
6845
8598
  required: ["node_id"]
6846
8599
  }
@@ -6862,9 +8615,10 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
6862
8615
  s: "source_node_index",
6863
8616
  d: "target_node_index",
6864
8617
  t: "type",
6865
- l: "label"
8618
+ l: "label",
8619
+ tl: "target_layer (only set on cross-layer edges, e.g. ui\u2192api calls_api)"
6866
8620
  },
6867
- note: "edges.s/d are 0-based indices into this response's nodes array. If a referenced node is outside the response (boundary case), s/d may contain the full node id string instead of an index."
8621
+ note: "edges.s/d are 0-based indices into this response's nodes array. If a referenced node is outside the response (boundary case \u2014 common for cross-layer edges where the target lives in a different layer's graph), s/d may contain the full node id string instead of an index. Use tl to identify which layer the target belongs to."
6868
8622
  };
6869
8623
  COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
6870
8624
  "id",
@@ -6883,7 +8637,8 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
6883
8637
  "conditions",
6884
8638
  "variables",
6885
8639
  "responses",
6886
- "params"
8640
+ "params",
8641
+ "effects"
6887
8642
  ]);
6888
8643
  EST_CHARS_PER_NODE_FULL = {
6889
8644
  ui: 300,
@@ -6904,16 +8659,18 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
6904
8659
  DEFAULT_EST_NODE_MIN = 150;
6905
8660
  DEFAULT_EST_EDGE = 65;
6906
8661
  NEIGHBORHOOD_BUDGET_CHARS = 55e3;
8662
+ MAX_FILTER_EDGES = 200;
6907
8663
  BATCH_BUDGET_CHARS = 6e4;
8664
+ watcherHandle = null;
6908
8665
  }
6909
8666
  });
6910
8667
 
6911
8668
  // src/server/graph-mcp-entry.ts
6912
8669
  var import_node_child_process3 = require("node:child_process");
6913
- var import_node_fs21 = require("node:fs");
6914
- var import_node_path22 = __toESM(require("node:path"));
8670
+ var import_node_fs24 = require("node:fs");
8671
+ var import_node_path27 = __toESM(require("node:path"));
6915
8672
  var import_node_os3 = require("node:os");
6916
- var import_node_fs22 = require("node:fs");
8673
+ var import_node_fs25 = require("node:fs");
6917
8674
  init_lockfile();
6918
8675
  function logStderr(msg) {
6919
8676
  process.stderr.write(`[launch-chart] ${msg}
@@ -6929,11 +8686,11 @@ function maybeAutoServe() {
6929
8686
  return;
6930
8687
  }
6931
8688
  try {
6932
- const logDir = import_node_path22.default.join((0, import_node_os3.homedir)(), ".launchsecure");
6933
- (0, import_node_fs22.mkdirSync)(logDir, { recursive: true });
6934
- const logPath = import_node_path22.default.join(logDir, "launch-chart.log");
6935
- const out = (0, import_node_fs21.openSync)(logPath, "a");
6936
- const err2 = (0, import_node_fs21.openSync)(logPath, "a");
8689
+ const logDir = import_node_path27.default.join((0, import_node_os3.homedir)(), ".launchsecure");
8690
+ (0, import_node_fs25.mkdirSync)(logDir, { recursive: true });
8691
+ const logPath = import_node_path27.default.join(logDir, "launch-chart.log");
8692
+ const out = (0, import_node_fs24.openSync)(logPath, "a");
8693
+ const err2 = (0, import_node_fs24.openSync)(logPath, "a");
6937
8694
  const entryPath = process.argv[1];
6938
8695
  const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
6939
8696
  detached: true,
@@ -6955,6 +8712,12 @@ async function main() {
6955
8712
  runServeCli2(argv.slice(1));
6956
8713
  return;
6957
8714
  }
8715
+ if (subcommand === "generate" || subcommand === "read") {
8716
+ const { handleGraphCommand: handleGraphCommand2 } = await Promise.resolve().then(() => (init_graph_cli(), graph_cli_exports));
8717
+ const mapped = subcommand === "generate" ? "graph:generate" : "graph:read";
8718
+ await handleGraphCommand2(mapped, argv.slice(1));
8719
+ process.exit(0);
8720
+ }
6958
8721
  maybeAutoServe();
6959
8722
  const { startGraphMcpServer: startGraphMcpServer2 } = await Promise.resolve().then(() => (init_graph_mcp(), graph_mcp_exports));
6960
8723
  startGraphMcpServer2();