@launchsecure/launch-kit 0.0.25 → 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 +990 -116
  101. package/dist/server/cli.js +28413 -6982
  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 +1639 -197
  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
@@ -398,17 +398,20 @@ var init_resolve_paths = __esm({
398
398
  // src/server/graph/core/ts-extractor.ts
399
399
  var ts_extractor_exports = {};
400
400
  __export(ts_extractor_exports, {
401
+ ParseCascadeError: () => ParseCascadeError,
401
402
  classifyFile: () => classifyFile,
402
403
  classifyRouteAgainstMiddleware: () => classifyRouteAgainstMiddleware,
403
404
  createQuery: () => createQuery,
404
405
  extractAuthWrappersTS: () => extractAuthWrappersTS,
405
406
  extractDbCallsTS: () => extractDbCallsTS,
406
407
  extractDeep: () => extractDeep,
408
+ extractEffects: () => extractEffects,
407
409
  extractMiddlewareAuthTS: () => extractMiddlewareAuthTS,
408
410
  initTreeSitter: () => initTreeSitter,
409
411
  middlewarePatternToRegex: () => middlewarePatternToRegex,
410
412
  parseCodeTS: () => parseCodeTS,
411
413
  parseFileTS: () => parseFileTS,
414
+ parseSource: () => parseSource,
412
415
  setExtractorConfig: () => setExtractorConfig
413
416
  });
414
417
  async function initTreeSitter() {
@@ -442,8 +445,38 @@ function getQuery(name) {
442
445
  }
443
446
  function parseSource(absPath) {
444
447
  ensureInit();
445
- const content = (0, import_node_fs5.readFileSync)(absPath, "utf-8");
446
- 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
+ }
447
480
  }
448
481
  function parseCodeTS(code) {
449
482
  ensureInit();
@@ -483,8 +516,20 @@ function childrenOfType(node, type) {
483
516
  function childOfType(node, type) {
484
517
  return node.children.find((n) => n.type === type);
485
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
+ }
486
530
  function parseFileTS(absPath) {
487
531
  const tree = parseSource(absPath);
532
+ if (!tree) return emptyParsedFile(absPath);
488
533
  const root = tree.rootNode;
489
534
  const imports = [];
490
535
  const importStatements = childrenOfType(root, "import_statement");
@@ -679,6 +724,7 @@ function parseFileTS(absPath) {
679
724
  }
680
725
  function extractDbCallsTS(absPath) {
681
726
  const tree = parseSource(absPath);
727
+ if (!tree) return [];
682
728
  const root = tree.rootNode;
683
729
  const dbQuery = getQuery("db-calls");
684
730
  const matches = dbQuery.matches(root);
@@ -739,7 +785,9 @@ function classifyFile(absPath) {
739
785
  const fileName = require("path").basename(absPath);
740
786
  if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("__test")) return "test";
741
787
  if (fileName.includes(".stories.")) return "story";
788
+ if (fileName === "middleware.ts" || fileName === "middleware.tsx") return "middleware";
742
789
  const tree = parseSource(absPath);
790
+ if (!tree) return "util";
743
791
  const root = tree.rootNode;
744
792
  const classifyQuery = getQuery("classify");
745
793
  const captures = classifyQuery.captures(root);
@@ -758,6 +806,7 @@ function classifyFile(absPath) {
758
806
  }
759
807
  function extractAuthWrappersTS(absPath) {
760
808
  const tree = parseSource(absPath);
809
+ if (!tree) return /* @__PURE__ */ new Set();
761
810
  const root = tree.rootNode;
762
811
  const wrapperQuery = getQuery("wrappers");
763
812
  const matches = wrapperQuery.matches(root);
@@ -801,6 +850,9 @@ function extractAuthWrappersTS(absPath) {
801
850
  return wrappers;
802
851
  }
803
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
+ }
804
856
  for (const re of EXEMPT_NAME_PATTERNS) {
805
857
  if (re.test(name)) return { intent: "exempt", hint: `name "${name}" matches /${re.source}/` };
806
858
  }
@@ -877,6 +929,7 @@ function detectFallthroughProtect(root) {
877
929
  function extractMiddlewareAuthTS(absPath) {
878
930
  if (!require("node:fs").existsSync(absPath)) return null;
879
931
  const tree = parseSource(absPath);
932
+ if (!tree) return null;
880
933
  const root = tree.rootNode;
881
934
  const matchers = [];
882
935
  for (const stmt of root.children) {
@@ -920,6 +973,16 @@ function extractMiddlewareAuthTS(absPath) {
920
973
  };
921
974
  }
922
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
+ }
923
986
  if (/\(\?\!/.test(pattern)) return null;
924
987
  if (pattern.startsWith("(")) return null;
925
988
  let src = "^";
@@ -977,8 +1040,163 @@ function classifyRouteAgainstMiddleware(routePath, info) {
977
1040
  function trunc(s, max = 120) {
978
1041
  return s.length > max ? s.slice(0, max) + "..." : s;
979
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
+ }
980
1195
  function extractDeep(absPath) {
981
1196
  const tree = parseSource(absPath);
1197
+ if (!tree) {
1198
+ return { elements: [], stateVars: [], conditions: [], variables: [], responses: [], params: [] };
1199
+ }
982
1200
  const root = tree.rootNode;
983
1201
  const elements = [];
984
1202
  const elQuery = getQuery("deep/jsx-semantic");
@@ -1071,12 +1289,18 @@ function extractDeep(absPath) {
1071
1289
  const caps = captureMap(m);
1072
1290
  const declNode = m.captures.find((c) => c.name === "var.decl" || c.name === "var.destructured" || c.name === "var.array")?.node;
1073
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;
1074
1293
  if (caps["var.name"] && caps["var.init"]) {
1075
- variables.push({
1294
+ const variable = {
1076
1295
  name: caps["var.name"],
1077
1296
  kind,
1078
1297
  init: trunc(caps["var.init"], 100)
1079
- });
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);
1080
1304
  }
1081
1305
  if (caps["var.destructured.obj"]) {
1082
1306
  variables.push({
@@ -1128,9 +1352,23 @@ function extractDeep(absPath) {
1128
1352
  params.push({ name: caps["param.body"], source: "body" });
1129
1353
  }
1130
1354
  }
1131
- 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
+ };
1132
1370
  }
1133
- 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, EXEMPT_NAME_PATTERNS, PROTECT_NAME_PATTERNS;
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;
1134
1372
  var init_ts_extractor = __esm({
1135
1373
  "src/server/graph/core/ts-extractor.ts"() {
1136
1374
  "use strict";
@@ -1143,6 +1381,19 @@ var init_ts_extractor = __esm({
1143
1381
  return (0, import_node_path5.join)((0, import_node_path5.dirname)(__filename), "graph", "queries");
1144
1382
  })();
1145
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
+ };
1146
1397
  PRISMA_MUTATION_METHODS_BUILTIN = [
1147
1398
  "create",
1148
1399
  "createMany",
@@ -1192,6 +1443,49 @@ var init_ts_extractor = __esm({
1192
1443
  /^admin_?routes?/i,
1193
1444
  /^secured/i
1194
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
+ ]);
1195
1489
  }
1196
1490
  });
1197
1491
 
@@ -1277,6 +1571,8 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
1277
1571
  function classifyType(absPath, id) {
1278
1572
  const contentType = classifyFile(absPath);
1279
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";
1280
1576
  return contentType;
1281
1577
  }
1282
1578
  function extractRoute(id) {
@@ -1587,6 +1883,7 @@ function generate(rootDir) {
1587
1883
  variables: deep.variables,
1588
1884
  responses: deep.responses,
1589
1885
  params: deep.params,
1886
+ ...deep.effects ? { effects: deep.effects } : {},
1590
1887
  _dbCalls: dbCalls
1591
1888
  // temp: used for cross-ref building below
1592
1889
  });
@@ -1606,6 +1903,7 @@ function generate(rootDir) {
1606
1903
  stateVars: deep.stateVars,
1607
1904
  conditions: deep.conditions,
1608
1905
  variables: deep.variables,
1906
+ ...deep.effects ? { effects: deep.effects } : {},
1609
1907
  ...authWrappers.length > 0 ? { auth: authWrappers } : {},
1610
1908
  ...dbCalls.length > 0 ? { _dbCalls: dbCalls } : {}
1611
1909
  });
@@ -2053,6 +2351,7 @@ var init_typescript_project = __esm({
2053
2351
  config: "ui",
2054
2352
  lib: "ui",
2055
2353
  "mcp-tool": "ui",
2354
+ middleware: "ui",
2056
2355
  external: "ui"
2057
2356
  };
2058
2357
  typescriptProjectParser = {
@@ -3962,6 +4261,119 @@ var init_static_ref_scanner = __esm({
3962
4261
  }
3963
4262
  });
3964
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
+
3965
4377
  // src/server/graph/core/parser-registry.ts
3966
4378
  function isMultiLayerParser(p) {
3967
4379
  return "layers" in p && Array.isArray(p.layers);
@@ -3975,7 +4387,8 @@ function registerBuiltins(registry, disabled) {
3975
4387
  fetchResolverParser,
3976
4388
  apiAnnotationsParser,
3977
4389
  urlLiteralScannerParser,
3978
- staticRefScannerParser
4390
+ staticRefScannerParser,
4391
+ middlewareGatesParser
3979
4392
  ];
3980
4393
  for (const parser of builtins) {
3981
4394
  if (disabled.has(parser.id)) continue;
@@ -3985,7 +4398,7 @@ function registerBuiltins(registry, disabled) {
3985
4398
  function loadCustomParsers(registry, config, rootDir, disabled) {
3986
4399
  for (const entry of config.parsers?.custom ?? []) {
3987
4400
  try {
3988
- const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
4401
+ const absPath = (0, import_node_path13.resolve)(rootDir, entry.path);
3989
4402
  const mod = require(absPath);
3990
4403
  const parser = "default" in mod ? mod.default : mod;
3991
4404
  if (disabled.has(parser.id)) continue;
@@ -4012,11 +4425,11 @@ function createRegistry(config, rootDir) {
4012
4425
  loadCustomParsers(registry, config, rootDir, disabled);
4013
4426
  return registry;
4014
4427
  }
4015
- var import_node_path12, ParserRegistry;
4428
+ var import_node_path13, ParserRegistry;
4016
4429
  var init_parser_registry = __esm({
4017
4430
  "src/server/graph/core/parser-registry.ts"() {
4018
4431
  "use strict";
4019
- import_node_path12 = require("node:path");
4432
+ import_node_path13 = require("node:path");
4020
4433
  init_typescript_project();
4021
4434
  init_prisma_schema();
4022
4435
  init_sql_migrations();
@@ -4025,6 +4438,7 @@ var init_parser_registry = __esm({
4025
4438
  init_url_literal_scanner();
4026
4439
  init_static_values();
4027
4440
  init_static_ref_scanner();
4441
+ init_middleware_gates();
4028
4442
  ParserRegistry = class {
4029
4443
  constructor() {
4030
4444
  this.singleLayerParsers = /* @__PURE__ */ new Map();
@@ -4187,7 +4601,7 @@ var init_merge = __esm({
4187
4601
 
4188
4602
  // src/server/graph/core/graph-builder.ts
4189
4603
  function readGraphFromDisk(rootDir, layer) {
4190
- 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`);
4191
4605
  if (!(0, import_node_fs13.existsSync)(filePath)) return null;
4192
4606
  try {
4193
4607
  return JSON.parse((0, import_node_fs13.readFileSync)(filePath, "utf-8"));
@@ -4286,12 +4700,12 @@ function generateAll(rootDir) {
4286
4700
  const extras = [...byLayer.keys()].filter((l) => !wellKnownOrder.includes(l)).sort();
4287
4701
  return [...wellKnownOrder, ...extras].map((l) => byLayer.get(l)).filter((r) => !!r);
4288
4702
  }
4289
- var import_node_fs13, import_node_path13;
4703
+ var import_node_fs13, import_node_path14;
4290
4704
  var init_graph_builder = __esm({
4291
4705
  "src/server/graph/core/graph-builder.ts"() {
4292
4706
  "use strict";
4293
4707
  import_node_fs13 = require("node:fs");
4294
- import_node_path13 = require("node:path");
4708
+ import_node_path14 = require("node:path");
4295
4709
  init_config();
4296
4710
  init_parser_registry();
4297
4711
  init_merge();
@@ -4330,13 +4744,13 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
4330
4744
  const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
4331
4745
  const searchDirs = [
4332
4746
  rootDir,
4333
- (0, import_node_path14.join)(rootDir, "src"),
4334
- (0, import_node_path14.join)(rootDir, "app"),
4335
- (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")
4336
4750
  ];
4337
4751
  for (const base of searchDirs) {
4338
4752
  for (const convention of conventionDirs) {
4339
- const dir = (0, import_node_path14.join)(base, convention);
4753
+ const dir = (0, import_node_path15.join)(base, convention);
4340
4754
  if (!(0, import_node_fs14.existsSync)(dir)) continue;
4341
4755
  try {
4342
4756
  const stat = (0, import_node_fs14.statSync)(dir);
@@ -4417,12 +4831,12 @@ function extractModuleFromPath(id, extraTrivial, extraSkipSegments, extraGeneric
4417
4831
  }
4418
4832
  return "root";
4419
4833
  }
4420
- 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;
4421
4835
  var init_module_tagger = __esm({
4422
4836
  "src/server/graph/taggers/module-tagger.ts"() {
4423
4837
  "use strict";
4424
4838
  import_node_fs14 = require("node:fs");
4425
- import_node_path14 = require("node:path");
4839
+ import_node_path15 = require("node:path");
4426
4840
  CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
4427
4841
  GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
4428
4842
  // JS/TS
@@ -4637,7 +5051,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
4637
5051
  for (const entry of config.taggers?.custom ?? []) {
4638
5052
  if (disabled.has(entry.id)) continue;
4639
5053
  try {
4640
- const absPath = (0, import_node_path15.resolve)(rootDir, entry.path);
5054
+ const absPath = (0, import_node_path16.resolve)(rootDir, entry.path);
4641
5055
  const mod = require(absPath);
4642
5056
  const tagger = "default" in mod ? mod.default : mod;
4643
5057
  const override = config.taggers?.trackUntagged?.[tagger.id];
@@ -4658,11 +5072,11 @@ function createTaggerRegistry(config, rootDir) {
4658
5072
  loadCustomTaggers(registry, config, rootDir, disabled);
4659
5073
  return registry;
4660
5074
  }
4661
- var import_node_path15, TaggerRegistry, BUILTIN_TAGGERS;
5075
+ var import_node_path16, TaggerRegistry, BUILTIN_TAGGERS;
4662
5076
  var init_tagger_registry = __esm({
4663
5077
  "src/server/graph/core/tagger-registry.ts"() {
4664
5078
  "use strict";
4665
- import_node_path15 = require("node:path");
5079
+ import_node_path16 = require("node:path");
4666
5080
  init_module_tagger();
4667
5081
  init_screen_tagger();
4668
5082
  TaggerRegistry = class {
@@ -4690,7 +5104,7 @@ var init_tagger_registry = __esm({
4690
5104
 
4691
5105
  // src/server/graph/core/tag-store.ts
4692
5106
  function tagsFilePath(rootDir) {
4693
- return (0, import_node_path16.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
5107
+ return (0, import_node_path17.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
4694
5108
  }
4695
5109
  function readTagStore(rootDir) {
4696
5110
  const filePath = tagsFilePath(rootDir);
@@ -4711,7 +5125,7 @@ function readTagStore(rootDir) {
4711
5125
  }
4712
5126
  function writeTagStore(rootDir, store) {
4713
5127
  const filePath = tagsFilePath(rootDir);
4714
- const dir = (0, import_node_path16.dirname)(filePath);
5128
+ const dir = (0, import_node_path17.dirname)(filePath);
4715
5129
  (0, import_node_fs15.mkdirSync)(dir, { recursive: true });
4716
5130
  const cleaned = {};
4717
5131
  for (const [nodeId, tags] of Object.entries(store)) {
@@ -4737,36 +5151,102 @@ function removeTag(rootDir, nodeId, key) {
4737
5151
  }
4738
5152
  writeTagStore(rootDir, store);
4739
5153
  }
4740
- 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;
4741
5155
  var init_tag_store = __esm({
4742
5156
  "src/server/graph/core/tag-store.ts"() {
4743
5157
  "use strict";
4744
5158
  import_node_fs15 = require("node:fs");
4745
- import_node_path16 = require("node:path");
5159
+ import_node_path17 = require("node:path");
4746
5160
  TAGS_FILENAME = "tags.json";
4747
5161
  GRAPHS_DIR = ".launchsecure/graphs";
4748
5162
  tagCache = /* @__PURE__ */ new Map();
4749
5163
  }
4750
5164
  });
4751
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
+
4752
5232
  // src/server/graph/index.ts
4753
5233
  function getAvailableLayers(rootDir) {
4754
- const dir = (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
4755
- if (!(0, import_node_fs16.existsSync)(dir)) return [];
4756
- 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", ""));
4757
5237
  }
4758
5238
  function graphsDir(rootDir) {
4759
- return (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
5239
+ return (0, import_node_path19.join)(rootDir, GRAPHS_DIR2);
4760
5240
  }
4761
5241
  function graphFilePath(rootDir, layer) {
4762
- return (0, import_node_path17.join)(graphsDir(rootDir), `${layer}.json`);
5242
+ return (0, import_node_path19.join)(graphsDir(rootDir), `${layer}.json`);
4763
5243
  }
4764
5244
  function tagsFilePath2(rootDir) {
4765
- return (0, import_node_path17.join)(graphsDir(rootDir), "tags.json");
5245
+ return (0, import_node_path19.join)(graphsDir(rootDir), "tags.json");
4766
5246
  }
4767
5247
  function getMtimeMs(filePath) {
4768
- if (!(0, import_node_fs16.existsSync)(filePath)) return 0;
4769
- 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;
4770
5250
  }
4771
5251
  function invalidateCache(filePath) {
4772
5252
  graphCache.delete(filePath);
@@ -4805,20 +5285,20 @@ function applyTags(graph, layer, rootDir) {
4805
5285
  }
4806
5286
  function readGraphRaw(rootDir, layer) {
4807
5287
  const filePath = graphFilePath(rootDir, layer);
4808
- if (!(0, import_node_fs16.existsSync)(filePath)) return null;
4809
- 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);
4810
5290
  const cached = graphCache.get(filePath);
4811
5291
  if (cached && cached.mtimeMs === stat.mtimeMs) {
4812
5292
  return cached.graph;
4813
5293
  }
4814
- const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
5294
+ const content = (0, import_node_fs17.readFileSync)(filePath, "utf-8");
4815
5295
  const graph = JSON.parse(content);
4816
5296
  graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
4817
5297
  return graph;
4818
5298
  }
4819
5299
  function readGraph(rootDir, layer) {
4820
5300
  const rawFilePath = graphFilePath(rootDir, layer);
4821
- if (!(0, import_node_fs16.existsSync)(rawFilePath)) return null;
5301
+ if (!(0, import_node_fs17.existsSync)(rawFilePath)) return null;
4822
5302
  const rawMtime = getMtimeMs(rawFilePath);
4823
5303
  const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
4824
5304
  const cacheKey = `${rootDir}:${layer}`;
@@ -4848,24 +5328,24 @@ async function generateGraph(rootDir, layer) {
4848
5328
  mutationMethods: config.parsers?.patterns?.mutationMethods
4849
5329
  });
4850
5330
  const dir = graphsDir(rootDir);
4851
- (0, import_node_fs16.mkdirSync)(dir, { recursive: true });
5331
+ (0, import_node_fs17.mkdirSync)(dir, { recursive: true });
4852
5332
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
4853
5333
  for (const result of results) {
4854
5334
  const filePath = graphFilePath(rootDir, result.layer);
4855
- (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");
4856
5336
  invalidateCache(filePath);
4857
5337
  invalidateTaggedCache(rootDir, result.layer);
4858
5338
  }
4859
5339
  if (!layer) {
4860
5340
  const producedLayers = new Set(results.map((r) => r.layer));
4861
5341
  try {
4862
- for (const f of (0, import_node_fs16.readdirSync)(dir)) {
4863
- 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;
4864
5344
  const layerName = f.replace(/\.json$/, "");
4865
5345
  if (producedLayers.has(layerName)) continue;
4866
- const orphan = (0, import_node_path17.join)(dir, f);
5346
+ const orphan = (0, import_node_path19.join)(dir, f);
4867
5347
  try {
4868
- (0, import_node_fs16.unlinkSync)(orphan);
5348
+ (0, import_node_fs17.unlinkSync)(orphan);
4869
5349
  invalidateCache(orphan);
4870
5350
  invalidateTaggedCache(rootDir, layerName);
4871
5351
  process.stderr.write(`[launch-chart] removed orphan layer file: ${f} (no parser produced ${layerName} this run)
@@ -4876,32 +5356,404 @@ async function generateGraph(rootDir, layer) {
4876
5356
  } catch {
4877
5357
  }
4878
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
+ }
4879
5377
  return results;
4880
5378
  }
4881
- var import_node_fs16, import_node_path17, GRAPHS_DIR2, graphCache, taggedCache;
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;
4882
5389
  var init_graph = __esm({
4883
5390
  "src/server/graph/index.ts"() {
4884
5391
  "use strict";
4885
- import_node_fs16 = require("node:fs");
4886
- import_node_path17 = require("node:path");
5392
+ import_node_fs17 = require("node:fs");
5393
+ import_node_path19 = require("node:path");
4887
5394
  init_graph_builder();
4888
5395
  init_config();
4889
5396
  init_tagger_registry();
4890
5397
  init_tag_store();
4891
5398
  init_ts_extractor();
5399
+ init_effects_index();
4892
5400
  init_tag_store();
4893
5401
  GRAPHS_DIR2 = ".launchsecure/graphs";
5402
+ NON_LAYER_GRAPH_FILES = /* @__PURE__ */ new Set(["tags.json", "effects-index.json"]);
4894
5403
  graphCache = /* @__PURE__ */ new Map();
4895
5404
  taggedCache = /* @__PURE__ */ new Map();
4896
5405
  }
4897
5406
  });
4898
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
+
4899
5751
  // src/server/graph/core/audit-core.ts
4900
5752
  function readGraphFile(rootDir, layer) {
4901
- const filePath = (0, import_node_path18.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
4902
- 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;
4903
5755
  try {
4904
- return JSON.parse((0, import_node_fs17.readFileSync)(filePath, "utf-8"));
5756
+ return JSON.parse((0, import_node_fs19.readFileSync)(filePath, "utf-8"));
4905
5757
  } catch {
4906
5758
  return null;
4907
5759
  }
@@ -4910,8 +5762,7 @@ function checkSchemaDrift(rootDir) {
4910
5762
  const findings = [];
4911
5763
  const db = readGraphFile(rootDir, "db");
4912
5764
  if (!db) {
4913
- 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." });
4914
- 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");
4915
5766
  }
4916
5767
  for (const c of db.contradictions ?? []) {
4917
5768
  const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
@@ -4928,7 +5779,7 @@ function checkSchemaDrift(rootDir) {
4928
5779
  function checkOrphanFks(rootDir) {
4929
5780
  const findings = [];
4930
5781
  const db = readGraphFile(rootDir, "db");
4931
- if (!db) return buildReport("db", "orphan_fks", findings);
5782
+ if (!db) return buildSkipped("db", "orphan_fks", "no db graph");
4932
5783
  for (const f of db.flagged_edges ?? []) {
4933
5784
  findings.push({
4934
5785
  id: `fk:${f.source}->${f.target}`,
@@ -4943,13 +5794,16 @@ function checkOrphanFks(rootDir) {
4943
5794
  function checkUnprotectedRoutes(rootDir) {
4944
5795
  const findings = [];
4945
5796
  const api = readGraphFile(rootDir, "api");
4946
- const staticGraph = readGraphFile(rootDir, "static");
4947
- if (!api) return buildReport("api", "unprotected_routes", findings);
4948
- const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
4949
- let routePermsContent = "";
4950
- if ((0, import_node_fs17.existsSync)(routePermsPath)) {
4951
- 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
+ );
4952
5805
  }
5806
+ const routePermsContent = (0, import_node_fs19.readFileSync)(routePermsPath, "utf-8");
4953
5807
  const registeredRoutes = /* @__PURE__ */ new Set();
4954
5808
  const routeEntryRe = /path:\s*'([^']+)'/g;
4955
5809
  let rm;
@@ -4993,7 +5847,7 @@ function routeMatchesPattern(route, pattern) {
4993
5847
  function checkDeadScreens(rootDir) {
4994
5848
  const findings = [];
4995
5849
  const ui = readGraphFile(rootDir, "ui");
4996
- if (!ui) return buildReport("ui", "dead_screens", findings);
5850
+ if (!ui) return buildSkipped("ui", "dead_screens", "no ui graph");
4997
5851
  const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
4998
5852
  const navTargets = /* @__PURE__ */ new Set();
4999
5853
  for (const e of ui.edges) {
@@ -5025,13 +5879,24 @@ function checkDeadScreens(rootDir) {
5025
5879
  function checkUnenforcedPermissions(rootDir) {
5026
5880
  const findings = [];
5027
5881
  const staticGraph = readGraphFile(rootDir, "static");
5028
- if (!staticGraph) return buildReport("static", "unenforced_permissions", findings);
5882
+ if (!staticGraph) return buildSkipped("static", "unenforced_permissions", "no static graph");
5029
5883
  const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
5030
- const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
5031
- let routePermsContent = "";
5032
- if ((0, import_node_fs17.existsSync)(routePermsPath)) {
5033
- 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
+ );
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
+ );
5034
5898
  }
5899
+ const routePermsContent = (0, import_node_fs19.readFileSync)(routePermsPath, "utf-8");
5035
5900
  for (const perm of permissions) {
5036
5901
  const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
5037
5902
  if (!regex.test(routePermsContent)) {
@@ -5049,20 +5914,27 @@ function checkUnenforcedPermissions(rootDir) {
5049
5914
  function checkHardcodedValues(rootDir) {
5050
5915
  const findings = [];
5051
5916
  const staticGraph = readGraphFile(rootDir, "static");
5052
- if (!staticGraph) return buildReport("static", "hardcoded_values", findings);
5917
+ if (!staticGraph) return buildSkipped("static", "hardcoded_values", "no static graph");
5053
5918
  const knownValues = /* @__PURE__ */ new Set();
5054
5919
  for (const n of staticGraph.nodes) {
5055
5920
  if (n.type === "enum_value") knownValues.add(n.value);
5056
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
+ }
5057
5929
  const api = readGraphFile(rootDir, "api");
5058
- if (!api) return buildReport("static", "hardcoded_values", findings);
5930
+ if (!api) return buildSkipped("static", "hardcoded_values", "no api graph");
5059
5931
  const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
5060
5932
  const seen = /* @__PURE__ */ new Set();
5061
5933
  for (const node of api.nodes) {
5062
5934
  if (node.type !== "endpoint") continue;
5063
- const filePath = (0, import_node_path18.join)(rootDir, "src", node.id);
5064
- if (!(0, import_node_fs17.existsSync)(filePath)) continue;
5065
- 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");
5066
5938
  let m;
5067
5939
  allCapsRe.lastIndex = 0;
5068
5940
  while ((m = allCapsRe.exec(content)) !== null) {
@@ -5094,7 +5966,19 @@ function buildReport(layer, check, findings) {
5094
5966
  warnings: findings.filter((f) => f.severity === "warning").length,
5095
5967
  info: findings.filter((f) => f.severity === "info").length
5096
5968
  },
5097
- 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
5098
5982
  };
5099
5983
  }
5100
5984
  function getAvailableChecks() {
@@ -5141,7 +6025,12 @@ function runAudit(rootDir, layer, check) {
5141
6025
  }
5142
6026
  function formatAsPrompt(reports) {
5143
6027
  const lines = [];
6028
+ const skipped = [];
5144
6029
  for (const report of reports) {
6030
+ if (report.status === "skipped") {
6031
+ skipped.push(report);
6032
+ continue;
6033
+ }
5145
6034
  if (report.findings.length === 0) continue;
5146
6035
  lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
5147
6036
  lines.push("");
@@ -5153,15 +6042,24 @@ function formatAsPrompt(reports) {
5153
6042
  }
5154
6043
  lines.push("");
5155
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
+ }
5156
6053
  if (lines.length === 0) return "No audit findings.";
5157
6054
  return lines.join("\n");
5158
6055
  }
5159
- var import_node_fs17, import_node_path18, CHECKS;
6056
+ var import_node_fs19, import_node_path21, CHECKS;
5160
6057
  var init_audit_core = __esm({
5161
6058
  "src/server/graph/core/audit-core.ts"() {
5162
6059
  "use strict";
5163
- import_node_fs17 = require("node:fs");
5164
- import_node_path18 = require("node:path");
6060
+ import_node_fs19 = require("node:fs");
6061
+ import_node_path21 = require("node:path");
6062
+ init_audit_security();
5165
6063
  CHECKS = {
5166
6064
  db: {
5167
6065
  schema_drift: checkSchemaDrift,
@@ -5176,6 +6074,11 @@ var init_audit_core = __esm({
5176
6074
  static: {
5177
6075
  unenforced_permissions: checkUnenforcedPermissions,
5178
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 })
5179
6082
  }
5180
6083
  };
5181
6084
  }
@@ -5193,16 +6096,16 @@ function randomPort() {
5193
6096
  function findProjectRoot(startDir) {
5194
6097
  let dir = startDir;
5195
6098
  for (let i = 0; i < 8; i++) {
5196
- const graphsDir2 = import_node_path19.default.join(dir, ".launchsecure", "graphs");
5197
- 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;
5198
- 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);
5199
6102
  if (parent === dir) break;
5200
6103
  dir = parent;
5201
6104
  }
5202
6105
  dir = startDir;
5203
6106
  for (let i = 0; i < 8; i++) {
5204
- if (import_node_fs18.default.existsSync(import_node_path19.default.join(dir, ".git"))) return dir;
5205
- 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);
5206
6109
  if (parent === dir) break;
5207
6110
  dir = parent;
5208
6111
  }
@@ -5211,7 +6114,7 @@ function findProjectRoot(startDir) {
5211
6114
  function resolveRequestRoot(url, monorepoRoot, projects) {
5212
6115
  const projectParam = url.searchParams.get("project");
5213
6116
  if (!projectParam || projects.length === 0) return monorepoRoot;
5214
- const resolved = import_node_path19.default.resolve(monorepoRoot, projectParam);
6117
+ const resolved = import_node_path22.default.resolve(monorepoRoot, projectParam);
5215
6118
  if (!resolved.startsWith(monorepoRoot)) {
5216
6119
  throw new Error("Project path outside monorepo root");
5217
6120
  }
@@ -5262,16 +6165,16 @@ async function buildMergedGraph(root) {
5262
6165
  };
5263
6166
  }
5264
6167
  function serveStatic(res, filePath) {
5265
- if (!import_node_fs18.default.existsSync(filePath) || !import_node_fs18.default.statSync(filePath).isFile()) return false;
5266
- 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();
5267
6170
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
5268
6171
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
5269
- import_node_fs18.default.createReadStream(filePath).pipe(res);
6172
+ import_node_fs20.default.createReadStream(filePath).pipe(res);
5270
6173
  return true;
5271
6174
  }
5272
6175
  function serveIndex(res, clientDir) {
5273
- const indexPath = import_node_path19.default.join(clientDir, "index.html");
5274
- 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)) {
5275
6178
  res.writeHead(500, { "Content-Type": "text/plain" });
5276
6179
  res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
5277
6180
  return;
@@ -5279,14 +6182,14 @@ function serveIndex(res, clientDir) {
5279
6182
  serveStatic(res, indexPath);
5280
6183
  }
5281
6184
  function tryListen(server, port) {
5282
- return new Promise((resolve4, reject) => {
6185
+ return new Promise((resolve5, reject) => {
5283
6186
  const onError = (err2) => {
5284
6187
  server.off("listening", onListening);
5285
6188
  reject(err2);
5286
6189
  };
5287
6190
  const onListening = () => {
5288
6191
  server.off("error", onError);
5289
- resolve4(port);
6192
+ resolve5(port);
5290
6193
  };
5291
6194
  server.once("error", onError);
5292
6195
  server.once("listening", onListening);
@@ -5323,7 +6226,7 @@ async function startChartServer(opts = {}) {
5323
6226
  }
5324
6227
  return { port: existing.port, url: existing.url };
5325
6228
  }
5326
- 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");
5327
6230
  const rootConfig = loadConfig(projectRoot);
5328
6231
  const projects = rootConfig.projects ?? [];
5329
6232
  const server = import_node_http.default.createServer((req, res) => {
@@ -5339,11 +6242,11 @@ async function startChartServer(opts = {}) {
5339
6242
  }
5340
6243
  if (req.method === "GET" && url2.pathname === "/api/projects") {
5341
6244
  const projectList = projects.length > 0 ? projects.map((p) => {
5342
- const absRoot = import_node_path19.default.resolve(projectRoot, p.root);
5343
- const hasGraphs = import_node_fs18.default.existsSync(import_node_path19.default.join(absRoot, ".launchsecure", "graphs"));
5344
- 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"));
5345
6248
  return { name: p.name, root: p.root, hasGraphs, hasNextConfig };
5346
- }) : [{ 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 }];
5347
6250
  res.writeHead(200, { "Content-Type": "application/json" });
5348
6251
  res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
5349
6252
  return;
@@ -5389,20 +6292,20 @@ async function startChartServer(opts = {}) {
5389
6292
  }
5390
6293
  if (req.method === "GET" && url2.pathname === "/api/file-content") {
5391
6294
  const relPath = url2.searchParams.get("path");
5392
- if (!relPath || relPath.includes("..") || import_node_path19.default.isAbsolute(relPath)) {
6295
+ if (!relPath || relPath.includes("..") || import_node_path22.default.isAbsolute(relPath)) {
5393
6296
  res.writeHead(400, { "Content-Type": "application/json" });
5394
6297
  res.end(JSON.stringify({ error: "Invalid path" }));
5395
6298
  return;
5396
6299
  }
5397
- const filePath = import_node_path19.default.join(reqRoot, relPath);
5398
- 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()) {
5399
6302
  res.writeHead(404, { "Content-Type": "application/json" });
5400
6303
  res.end(JSON.stringify({ error: "File not found" }));
5401
6304
  return;
5402
6305
  }
5403
- const ext = import_node_path19.default.extname(filePath).toLowerCase();
6306
+ const ext = import_node_path22.default.extname(filePath).toLowerCase();
5404
6307
  const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
5405
- const content = import_node_fs18.default.readFileSync(filePath, "utf-8");
6308
+ const content = import_node_fs20.default.readFileSync(filePath, "utf-8");
5406
6309
  res.writeHead(200, { "Content-Type": "application/json" });
5407
6310
  res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
5408
6311
  return;
@@ -5444,8 +6347,8 @@ async function startChartServer(opts = {}) {
5444
6347
  req.on("end", () => {
5445
6348
  try {
5446
6349
  const newConfig = JSON.parse(body);
5447
- const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
5448
- 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");
5449
6352
  res.writeHead(200, { "Content-Type": "application/json" });
5450
6353
  res.end(JSON.stringify({ ok: true }));
5451
6354
  } catch (err2) {
@@ -5478,8 +6381,8 @@ async function startChartServer(opts = {}) {
5478
6381
  const taggerConfig = JSON.parse(body);
5479
6382
  const config2 = loadConfig(reqRoot);
5480
6383
  config2.taggers = taggerConfig;
5481
- const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
5482
- 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");
5483
6386
  res.writeHead(200, { "Content-Type": "application/json" });
5484
6387
  res.end(JSON.stringify({ ok: true }));
5485
6388
  } catch (err2) {
@@ -5549,7 +6452,7 @@ async function startChartServer(opts = {}) {
5549
6452
  dbDir: !!config2.paths?.dbDir,
5550
6453
  srcRoots: !!(config2.paths?.srcRoots && config2.paths.srcRoots.length > 0)
5551
6454
  };
5552
- const relFromRoot = (abs) => import_node_path19.default.relative(reqRoot, abs) || ".";
6455
+ const relFromRoot = (abs) => import_node_path22.default.relative(reqRoot, abs) || ".";
5553
6456
  res.writeHead(200, { "Content-Type": "application/json" });
5554
6457
  res.end(JSON.stringify({
5555
6458
  projectRoot: reqRoot,
@@ -5571,19 +6474,19 @@ async function startChartServer(opts = {}) {
5571
6474
  }
5572
6475
  if (req.method === "GET" && url2.pathname === "/api/browse-dir") {
5573
6476
  const browsePath = url2.searchParams.get("path") || projectRoot;
5574
- const abs = import_node_path19.default.resolve(browsePath);
5575
- 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, "..", "..");
5576
6479
  if (!abs.startsWith(twoUp)) {
5577
6480
  res.writeHead(403, { "Content-Type": "application/json" });
5578
6481
  res.end(JSON.stringify({ ok: false, error: "Path outside allowed range" }));
5579
6482
  return;
5580
6483
  }
5581
6484
  try {
5582
- const entries = import_node_fs18.default.readdirSync(abs, { withFileTypes: true });
6485
+ const entries = import_node_fs20.default.readdirSync(abs, { withFileTypes: true });
5583
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();
5584
- const parent = abs !== twoUp ? import_node_path19.default.dirname(abs) : null;
6487
+ const parent = abs !== twoUp ? import_node_path22.default.dirname(abs) : null;
5585
6488
  res.writeHead(200, { "Content-Type": "application/json" });
5586
- 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) || "." }));
5587
6490
  } catch (err2) {
5588
6491
  res.writeHead(400, { "Content-Type": "application/json" });
5589
6492
  res.end(JSON.stringify({ ok: false, error: String(err2) }));
@@ -5609,8 +6512,8 @@ async function startChartServer(opts = {}) {
5609
6512
  const { projects: newProjects } = JSON.parse(body);
5610
6513
  const config2 = loadConfig(projectRoot);
5611
6514
  config2.projects = newProjects.length > 0 ? newProjects : void 0;
5612
- const configPath = import_node_path19.default.join(projectRoot, ".launchchart.json");
5613
- 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");
5614
6517
  projects.length = 0;
5615
6518
  if (config2.projects) projects.push(...config2.projects);
5616
6519
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -5623,7 +6526,7 @@ async function startChartServer(opts = {}) {
5623
6526
  return;
5624
6527
  }
5625
6528
  if (url2.pathname !== "/") {
5626
- const staticPath = import_node_path19.default.join(clientDir, url2.pathname);
6529
+ const staticPath = import_node_path22.default.join(clientDir, url2.pathname);
5627
6530
  if (serveStatic(res, staticPath)) return;
5628
6531
  }
5629
6532
  serveIndex(res, clientDir);
@@ -5683,13 +6586,13 @@ function runServeCli(argv) {
5683
6586
  process.exit(1);
5684
6587
  });
5685
6588
  }
5686
- 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;
5687
6590
  var init_chart_serve = __esm({
5688
6591
  "src/server/chart-serve.ts"() {
5689
6592
  "use strict";
5690
6593
  import_node_http = __toESM(require("node:http"));
5691
- import_node_fs18 = __toESM(require("node:fs"));
5692
- import_node_path19 = __toESM(require("node:path"));
6594
+ import_node_fs20 = __toESM(require("node:fs"));
6595
+ import_node_path22 = __toESM(require("node:path"));
5693
6596
  init_graph();
5694
6597
  init_lockfile();
5695
6598
  init_config();
@@ -5711,13 +6614,183 @@ var init_chart_serve = __esm({
5711
6614
  }
5712
6615
  });
5713
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
+
5714
6787
  // src/server/graph/core/language-detection.ts
5715
6788
  function walkForExtensions(dir, extCounts, depth = 0) {
5716
6789
  if (depth > 10) return;
5717
- if (!(0, import_node_fs19.existsSync)(dir)) return;
6790
+ if (!(0, import_node_fs21.existsSync)(dir)) return;
5718
6791
  let entries;
5719
6792
  try {
5720
- entries = (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true });
6793
+ entries = (0, import_node_fs21.readdirSync)(dir, { withFileTypes: true });
5721
6794
  } catch {
5722
6795
  return;
5723
6796
  }
@@ -5725,9 +6798,9 @@ function walkForExtensions(dir, extCounts, depth = 0) {
5725
6798
  if (entry.name.startsWith(".") && entry.isDirectory()) continue;
5726
6799
  if (entry.isDirectory()) {
5727
6800
  if (IGNORE_DIRS.has(entry.name)) continue;
5728
- 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);
5729
6802
  } else {
5730
- const ext = (0, import_node_path20.extname)(entry.name).toLowerCase();
6803
+ const ext = (0, import_node_path24.extname)(entry.name).toLowerCase();
5731
6804
  if (ext && EXTENSION_TO_LANGUAGE[ext]) {
5732
6805
  extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
5733
6806
  }
@@ -5766,12 +6839,12 @@ function detectLanguages(rootDir, supportedLanguages) {
5766
6839
  });
5767
6840
  return results;
5768
6841
  }
5769
- 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;
5770
6843
  var init_language_detection = __esm({
5771
6844
  "src/server/graph/core/language-detection.ts"() {
5772
6845
  "use strict";
5773
- import_node_fs19 = require("node:fs");
5774
- import_node_path20 = require("node:path");
6846
+ import_node_fs21 = require("node:fs");
6847
+ import_node_path24 = require("node:path");
5775
6848
  EXTENSION_TO_LANGUAGE = {
5776
6849
  // Web / Frontend
5777
6850
  ".ts": "typescript",
@@ -5884,9 +6957,128 @@ var init_language_detection = __esm({
5884
6957
  }
5885
6958
  });
5886
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
+
5887
7078
  // src/server/graph-mcp.ts
5888
7079
  var graph_mcp_exports = {};
5889
7080
  __export(graph_mcp_exports, {
7081
+ getWatcherHandle: () => getWatcherHandle,
5890
7082
  startGraphMcpServer: () => startGraphMcpServer
5891
7083
  });
5892
7084
  function matchesSearch(node, query) {
@@ -5906,6 +7098,14 @@ function toMinimal(nodes) {
5906
7098
  return out;
5907
7099
  });
5908
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
+ }
5909
7109
  function toCompactNode(n) {
5910
7110
  const out = { i: n.id, t: n.type, n: n.name };
5911
7111
  const tags = n.tags;
@@ -5930,6 +7130,8 @@ function toCompactEdges(edges, idx) {
5930
7130
  t: e.type
5931
7131
  };
5932
7132
  if (e.label != null) o.l = e.label;
7133
+ const targetLayer = e.target_layer;
7134
+ if (targetLayer != null) o.tl = targetLayer;
5933
7135
  return o;
5934
7136
  });
5935
7137
  }
@@ -5970,6 +7172,9 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
5970
7172
  const dstIn = visited.has(e.target) || next.has(e.target);
5971
7173
  if (srcIn && dstIn) projectedEdges++;
5972
7174
  }
7175
+ for (const c of graph.cross_refs ?? []) {
7176
+ if (visited.has(c.source) || next.has(c.source)) projectedEdges++;
7177
+ }
5973
7178
  const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] ?? DEFAULT_EST_NODE_MIN : EST_CHARS_PER_NODE_FULL[layer] ?? DEFAULT_EST_NODE_FULL;
5974
7179
  const projectedChars = projectedVisited * perNode + projectedEdges * (EST_CHARS_PER_EDGE[layer] ?? DEFAULT_EST_EDGE);
5975
7180
  if (projectedChars > NEIGHBORHOOD_BUDGET_CHARS) {
@@ -5982,8 +7187,9 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
5982
7187
  if (frontier.size === 0) break;
5983
7188
  }
5984
7189
  const nodes = graph.nodes.filter((n) => visited.has(n.id));
5985
- const edges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
5986
- 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 };
5987
7193
  }
5988
7194
  function reverseNeighborhood(graph, centerId, hops, direction) {
5989
7195
  const center = graph.nodes.find((n) => n.id === centerId);
@@ -6013,7 +7219,9 @@ function reverseNeighborhood(graph, centerId, hops, direction) {
6013
7219
  return { nodes: visited, edges };
6014
7220
  }
6015
7221
  function handleBlastPoints(args) {
6016
- const rootDir = process.cwd();
7222
+ const __resolved = resolveOrErr(args);
7223
+ if ("content" in __resolved) return __resolved;
7224
+ const { rootDir } = __resolved;
6017
7225
  const nodeId = args.node_id;
6018
7226
  const requestedLayer = args.layer;
6019
7227
  const hops = args.hops ?? 2;
@@ -6057,7 +7265,11 @@ function handleBlastPoints(args) {
6057
7265
  for (const otherLayer of otherLayers) {
6058
7266
  const otherGraph = readGraph(rootDir, otherLayer);
6059
7267
  if (!otherGraph) continue;
6060
- for (const edge of otherGraph.edges) {
7268
+ const candidates = [
7269
+ ...otherGraph.edges,
7270
+ ...otherGraph.cross_refs ?? []
7271
+ ];
7272
+ for (const edge of candidates) {
6061
7273
  if (edge.target === nodeId || edge.source === nodeId) {
6062
7274
  const dependentId = edge.target === nodeId ? edge.source : edge.target;
6063
7275
  if (affected.some((a) => a.id === dependentId)) continue;
@@ -6084,7 +7296,7 @@ function handleBlastPoints(args) {
6084
7296
  byHop[String(a.hop)] = (byHop[String(a.hop)] ?? 0) + 1;
6085
7297
  if (a.module) modulesSet.add(a.module);
6086
7298
  }
6087
- const crossesLayers = Object.keys(byLayer).length > 1;
7299
+ const crossesLayers = Object.keys(byLayer).some((l) => l !== targetLayer);
6088
7300
  const centerTags = center.tags;
6089
7301
  return okJson({
6090
7302
  center: {
@@ -6135,26 +7347,89 @@ function okJson(data) {
6135
7347
  function err(text) {
6136
7348
  return { content: [{ type: "text", text }], isError: true };
6137
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
+ }
6138
7357
  async function handleGenerateGraph(args) {
6139
- const rootDir = process.cwd();
7358
+ const monorepoRoot = process.cwd();
6140
7359
  const layer = args.layer;
6141
- 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);
6142
7423
  if (results.length === 0) {
6143
7424
  return err(
6144
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."
6145
7426
  );
6146
7427
  }
6147
- const summary = results.map((r) => {
6148
- const warnings = r.output.warnings.length;
6149
- return ` ${r.layer}: ${r.nodeCount} nodes, ${r.edgeCount} edges${warnings ? ` (${warnings} warnings)` : ""}`;
6150
- }).join("\n");
6151
7428
  return ok(
6152
7429
  `Graph generated successfully.
6153
7430
 
6154
7431
  Layers:
6155
- ${summary}
6156
-
6157
- Output: .launchsecure/graphs/
7432
+ ${formatProjectResult(results, ".")}
6158
7433
 
6159
7434
  Use read_graph with filters (search/type/module/node_id) to query.`
6160
7435
  );
@@ -6237,8 +7512,6 @@ function runReadGraphQueryRaw(rootDir, args) {
6237
7512
  if (tagKey && tagValue && nodeTags?.[tagKey] !== tagValue) return false;
6238
7513
  return true;
6239
7514
  });
6240
- const matchedIds = new Set(matched.map((n) => n.id));
6241
- const matchedEdges = graph.edges.filter((e) => matchedIds.has(e.source) && matchedIds.has(e.target));
6242
7515
  if (matched.length === 0) {
6243
7516
  return {
6244
7517
  layer,
@@ -6252,7 +7525,9 @@ function runReadGraphQueryRaw(rootDir, args) {
6252
7525
  const hasMore = offset + paginatedNodes.length < totalMatched;
6253
7526
  const wantEdges = includeEdges ?? false;
6254
7527
  const returnedIds = new Set(paginatedNodes.map((n) => n.id));
6255
- 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];
6256
7531
  const result = {
6257
7532
  layer,
6258
7533
  filter: { search, type, module: module_ },
@@ -6267,7 +7542,14 @@ function runReadGraphQueryRaw(rootDir, args) {
6267
7542
  result.next_offset = offset + paginatedNodes.length;
6268
7543
  }
6269
7544
  if (wantEdges) {
6270
- 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
+ }
6271
7553
  } else if (returnedEdges.length > 0) {
6272
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).`;
6273
7555
  }
@@ -6278,12 +7560,13 @@ function runReadGraphQuery(rootDir, args) {
6278
7560
  return compactResult(raw);
6279
7561
  }
6280
7562
  function handleReadGraph(args) {
6281
- const rootDir = process.cwd();
7563
+ const monorepoRoot = process.cwd();
6282
7564
  if (Array.isArray(args.queries)) {
6283
7565
  const queries = args.queries;
6284
7566
  if (queries.length === 0) {
6285
7567
  return err("queries array is empty. Provide at least one query object.");
6286
7568
  }
7569
+ const inheritedProject = typeof args.project === "string" ? args.project : void 0;
6287
7570
  const results = [];
6288
7571
  let cumulativeChars = 0;
6289
7572
  let budgetHit = false;
@@ -6301,7 +7584,15 @@ function handleReadGraph(args) {
6301
7584
  });
6302
7585
  continue;
6303
7586
  }
6304
- 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);
6305
7596
  const entry = { index: i, query: q, result: r };
6306
7597
  const entrySize = JSON.stringify(entry, null, 2).length;
6307
7598
  if (cumulativeChars + entrySize > BATCH_BUDGET_CHARS && results.length > 0) {
@@ -6328,20 +7619,24 @@ function handleReadGraph(args) {
6328
7619
  results
6329
7620
  });
6330
7621
  }
6331
- const result = runReadGraphQuery(rootDir, args);
7622
+ const __resolved = resolveOrErr(args);
7623
+ if ("content" in __resolved) return __resolved;
7624
+ const result = runReadGraphQuery(__resolved.rootDir, args);
6332
7625
  return okJson(result);
6333
7626
  }
6334
7627
  function nodeToFilePath(rootDir, layer, nodeId) {
6335
- if (layer === "ui" || layer === "api") return (0, import_node_path21.join)(rootDir, "src", nodeId);
6336
- if (layer === "db") return (0, import_node_path21.join)(rootDir, "prisma", "schema.prisma");
6337
- const withSrc = (0, import_node_path21.join)(rootDir, "src", nodeId);
6338
- if ((0, import_node_fs20.existsSync)(withSrc)) return withSrc;
6339
- const direct = (0, import_node_path21.join)(rootDir, nodeId);
6340
- 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;
6341
7634
  return null;
6342
7635
  }
6343
7636
  function handleInspectNode(args) {
6344
- const rootDir = process.cwd();
7637
+ const __resolved = resolveOrErr(args);
7638
+ if ("content" in __resolved) return __resolved;
7639
+ const { rootDir } = __resolved;
6345
7640
  const layer = args.layer;
6346
7641
  const nodeId = args.node_id;
6347
7642
  const search = args.search;
@@ -6365,7 +7660,7 @@ function handleInspectNode(args) {
6365
7660
  } else {
6366
7661
  matched = graph.nodes;
6367
7662
  }
6368
- const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params"];
7663
+ const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params", "effects"];
6369
7664
  const requestedFields = fields ?? allDeepFields;
6370
7665
  let filterRegex = null;
6371
7666
  if (filter) {
@@ -6421,7 +7716,9 @@ function handleInspectNode(args) {
6421
7716
  });
6422
7717
  }
6423
7718
  function handleGrepNodes(args) {
6424
- const rootDir = process.cwd();
7719
+ const __resolved = resolveOrErr(args);
7720
+ if ("content" in __resolved) return __resolved;
7721
+ const { rootDir } = __resolved;
6425
7722
  const pattern = args.pattern;
6426
7723
  const layer = args.layer;
6427
7724
  if (!pattern) return err("pattern is required");
@@ -6480,11 +7777,11 @@ function handleGrepNodes(args) {
6480
7777
  let filesSearched = 0;
6481
7778
  let truncated = false;
6482
7779
  for (const [filePath, nodeId] of filePaths) {
6483
- if (!(0, import_node_fs20.existsSync)(filePath)) continue;
7780
+ if (!(0, import_node_fs23.existsSync)(filePath)) continue;
6484
7781
  filesSearched++;
6485
7782
  let content;
6486
7783
  try {
6487
- content = (0, import_node_fs20.readFileSync)(filePath, "utf-8");
7784
+ content = (0, import_node_fs23.readFileSync)(filePath, "utf-8");
6488
7785
  } catch {
6489
7786
  continue;
6490
7787
  }
@@ -6521,11 +7818,61 @@ function handleGrepNodes(args) {
6521
7818
  truncated
6522
7819
  });
6523
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
+ }
6524
7863
  function handleChartServerStatus() {
6525
7864
  const rootDir = process.cwd();
6526
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 };
6527
7874
  if (!lock) {
6528
- return okJson({ running: false });
7875
+ return okJson({ running: false, watcher: watcherInfo });
6529
7876
  }
6530
7877
  return okJson({
6531
7878
  running: true,
@@ -6533,7 +7880,8 @@ function handleChartServerStatus() {
6533
7880
  port: lock.port,
6534
7881
  pid: lock.pid,
6535
7882
  cwd: lock.cwd,
6536
- startedAt: lock.startedAt
7883
+ startedAt: lock.startedAt,
7884
+ watcher: watcherInfo
6537
7885
  });
6538
7886
  }
6539
7887
  function handleStartChartServer(args) {
@@ -6549,11 +7897,11 @@ function handleStartChartServer(args) {
6549
7897
  });
6550
7898
  }
6551
7899
  const entryPath = process.argv[1];
6552
- const logDir = (0, import_node_path21.join)((0, import_node_os2.homedir)(), ".launchsecure");
6553
- (0, import_node_fs20.mkdirSync)(logDir, { recursive: true });
6554
- const logPath = (0, import_node_path21.join)(logDir, "launch-chart.log");
6555
- const out = (0, import_node_fs20.openSync)(logPath, "a");
6556
- 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");
6557
7905
  const portArgs = args.port ? ["--port", String(args.port)] : [];
6558
7906
  const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
6559
7907
  detached: true,
@@ -6586,7 +7934,9 @@ function handleStopChartServer() {
6586
7934
  }
6587
7935
  }
6588
7936
  function handleAddTag(args) {
6589
- const rootDir = process.cwd();
7937
+ const __resolved = resolveOrErr(args);
7938
+ if ("content" in __resolved) return __resolved;
7939
+ const { rootDir } = __resolved;
6590
7940
  const nodeId = args.node_id;
6591
7941
  const key = args.key;
6592
7942
  const value = args.value;
@@ -6608,7 +7958,9 @@ function handleAddTag(args) {
6608
7958
  return okJson({ ok: true, node_id: nodeId, tag: { [key]: value } });
6609
7959
  }
6610
7960
  function handleRemoveTag(args) {
6611
- const rootDir = process.cwd();
7961
+ const __resolved = resolveOrErr(args);
7962
+ if ("content" in __resolved) return __resolved;
7963
+ const { rootDir } = __resolved;
6612
7964
  const nodeId = args.node_id;
6613
7965
  const key = args.key;
6614
7966
  if (!nodeId) return err("node_id is required");
@@ -6617,7 +7969,9 @@ function handleRemoveTag(args) {
6617
7969
  return okJson({ ok: true, node_id: nodeId, removed_key: key });
6618
7970
  }
6619
7971
  function handleAuditLayer(args) {
6620
- const rootDir = process.cwd();
7972
+ const __resolved = resolveOrErr(args);
7973
+ if ("content" in __resolved) return __resolved;
7974
+ const { rootDir } = __resolved;
6621
7975
  const layer = args.layer;
6622
7976
  const check = args.check;
6623
7977
  if (!layer) return err("layer is required");
@@ -6630,6 +7984,10 @@ function handleAuditLayer(args) {
6630
7984
  lines.push(`Audit: ${layer}${check ? ` / ${check}` : ""} \u2014 ${totalFindings} findings (${totalErrors} errors, ${totalWarnings} warnings, ${totalInfo} info)`);
6631
7985
  lines.push("");
6632
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
+ }
6633
7991
  if (report.findings.length === 0) {
6634
7992
  lines.push(`\u2713 ${report.check}: no issues found`);
6635
7993
  continue;
@@ -6673,20 +8031,20 @@ function handleDetectProjectStack() {
6673
8031
  if (ref.type === "references_api") stats.references_api++;
6674
8032
  }
6675
8033
  }
6676
- const srcDir = (0, import_node_path21.join)(rootDir, "src");
6677
- 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)) {
6678
8036
  const scanDir = (dir) => {
6679
- if (!(0, import_node_fs20.existsSync)(dir)) return;
6680
- 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 })) {
6681
8039
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
6682
- const full = (0, import_node_path21.join)(dir, entry.name);
8040
+ const full = (0, import_node_path26.join)(dir, entry.name);
6683
8041
  if (entry.isDirectory()) {
6684
8042
  scanDir(full);
6685
8043
  continue;
6686
8044
  }
6687
- 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;
6688
8046
  try {
6689
- const content = (0, import_node_fs20.readFileSync)(full, "utf-8");
8047
+ const content = (0, import_node_fs23.readFileSync)(full, "utf-8");
6690
8048
  const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
6691
8049
  if (matches) stats.annotations += matches.length;
6692
8050
  } catch {
@@ -6701,6 +8059,12 @@ function handleDetectProjectStack() {
6701
8059
  const languages = detectLanguages(rootDir, supportedLanguages);
6702
8060
  const unsupported = languages.filter((l) => !l.supported);
6703
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
+ }));
6704
8068
  return okJson({
6705
8069
  languages,
6706
8070
  parsers: parserResults,
@@ -6723,7 +8087,11 @@ function handleDetectProjectStack() {
6723
8087
  stats,
6724
8088
  ...unsupportedHint ? { unsupported_hint: unsupportedHint } : {},
6725
8089
  current_config: Object.keys(config).length > 0 ? config : null,
6726
- 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
+ }
6727
8095
  });
6728
8096
  }
6729
8097
  function send(msg) {
@@ -6773,6 +8141,10 @@ async function handleMessage(msg) {
6773
8141
  respond(id ?? null, handleInspectNode(args));
6774
8142
  return;
6775
8143
  }
8144
+ if (toolName === "effects_index") {
8145
+ respond(id ?? null, handleEffectsIndex(args));
8146
+ return;
8147
+ }
6776
8148
  if (toolName === "chart_server_status") {
6777
8149
  respond(id ?? null, handleChartServerStatus());
6778
8150
  return;
@@ -6816,6 +8188,9 @@ async function handleMessage(msg) {
6816
8188
  respondError(id, -32601, `Method not found: ${method}`);
6817
8189
  }
6818
8190
  }
8191
+ function getWatcherHandle() {
8192
+ return watcherHandle;
8193
+ }
6819
8194
  function startGraphMcpServer() {
6820
8195
  process.stdin.setEncoding("utf-8");
6821
8196
  let buffer = "";
@@ -6835,17 +8210,39 @@ function startGraphMcpServer() {
6835
8210
  }
6836
8211
  });
6837
8212
  process.stdin.on("end", () => {
8213
+ watcherHandle?.stop();
6838
8214
  process.exit(0);
6839
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
+ }
6840
8237
  process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
6841
8238
  `);
6842
8239
  }
6843
- 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;
6844
8241
  var init_graph_mcp = __esm({
6845
8242
  "src/server/graph-mcp.ts"() {
6846
8243
  "use strict";
6847
- import_node_fs20 = require("node:fs");
6848
- import_node_path21 = require("node:path");
8244
+ import_node_fs23 = require("node:fs");
8245
+ import_node_path26 = require("node:path");
6849
8246
  import_node_child_process2 = require("node:child_process");
6850
8247
  import_node_os2 = require("node:os");
6851
8248
  init_graph();
@@ -6854,6 +8251,7 @@ var init_graph_mcp = __esm({
6854
8251
  init_parser_registry();
6855
8252
  init_language_detection();
6856
8253
  init_audit_core();
8254
+ init_projects();
6857
8255
  SERVER_INFO = {
6858
8256
  name: "launchsecure-graph",
6859
8257
  version: "0.0.1"
@@ -6861,20 +8259,24 @@ var init_graph_mcp = __esm({
6861
8259
  TOOLS = [
6862
8260
  {
6863
8261
  name: "generate_graph",
6864
- 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.",
6865
8263
  inputSchema: {
6866
8264
  type: "object",
6867
8265
  properties: {
6868
8266
  layer: {
6869
8267
  type: "string",
6870
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."
6871
8273
  }
6872
8274
  }
6873
8275
  }
6874
8276
  },
6875
8277
  {
6876
8278
  name: "read_graph",
6877
- 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.',
6878
8280
  inputSchema: {
6879
8281
  type: "object",
6880
8282
  properties: {
@@ -6928,7 +8330,7 @@ var init_graph_mcp = __esm({
6928
8330
  },
6929
8331
  queries: {
6930
8332
  type: "array",
6931
- 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.",
6932
8334
  items: {
6933
8335
  type: "object",
6934
8336
  properties: {
@@ -6939,9 +8341,14 @@ var init_graph_mcp = __esm({
6939
8341
  node_id: { type: "string" },
6940
8342
  hops: { type: "number" },
6941
8343
  minimal: { type: "boolean" },
6942
- include_edges: { type: "boolean" }
8344
+ include_edges: { type: "boolean" },
8345
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
6943
8346
  }
6944
8347
  }
8348
+ },
8349
+ project: {
8350
+ type: "string",
8351
+ description: PROJECT_PARAM_DESCRIPTION
6945
8352
  }
6946
8353
  }
6947
8354
  }
@@ -6988,16 +8395,17 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
6988
8395
  case_insensitive: { type: "boolean", description: "Case-insensitive regex. Default false." },
6989
8396
  context: { type: "number", description: "Context lines around each match. Default 2." },
6990
8397
  max_matches: { type: "number", description: "Max matches to return total. Default 50." },
6991
- 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 }
6992
8400
  },
6993
8401
  required: ["layer", "pattern"]
6994
8402
  }
6995
8403
  },
6996
8404
  {
6997
8405
  name: "inspect_node",
6998
- 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.
6999
8407
 
7000
- 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)
7001
8409
 
7002
8410
  DO NOT USE FOR: structural queries (use read_graph), content search (use grep_nodes).
7003
8411
 
@@ -7020,7 +8428,7 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
7020
8428
  fields: {
7021
8429
  type: "array",
7022
8430
  items: { type: "string" },
7023
- 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".'
7024
8432
  },
7025
8433
  filter: {
7026
8434
  type: "string",
@@ -7029,7 +8437,8 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
7029
8437
  case_insensitive: {
7030
8438
  type: "boolean",
7031
8439
  description: "Case-insensitive filter matching. Default true."
7032
- }
8440
+ },
8441
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
7033
8442
  },
7034
8443
  required: ["layer"]
7035
8444
  }
@@ -7065,6 +8474,25 @@ Use this when the user asks "is the chart running", "show me the project graph U
7065
8474
  properties: {}
7066
8475
  }
7067
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
+ },
7068
8496
  {
7069
8497
  name: "detect_project_stack",
7070
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.",
@@ -7090,7 +8518,8 @@ Use this when the user asks "is the chart running", "show me the project graph U
7090
8518
  value: {
7091
8519
  type: "string",
7092
8520
  description: 'Tag value (e.g. "auth", "alice", "true").'
7093
- }
8521
+ },
8522
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
7094
8523
  },
7095
8524
  required: ["node_id", "key", "value"]
7096
8525
  }
@@ -7108,7 +8537,8 @@ Use this when the user asks "is the chart running", "show me the project graph U
7108
8537
  key: {
7109
8538
  type: "string",
7110
8539
  description: "Tag key to remove."
7111
- }
8540
+ },
8541
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
7112
8542
  },
7113
8543
  required: ["node_id", "key"]
7114
8544
  }
@@ -7126,7 +8556,8 @@ Use this when the user asks "is the chart running", "show me the project graph U
7126
8556
  check: {
7127
8557
  type: "string",
7128
8558
  description: "Specific check to run (e.g. 'schema_drift', 'unprotected_routes'). Omit to run all checks for the layer."
7129
- }
8559
+ },
8560
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
7130
8561
  },
7131
8562
  required: ["layer"]
7132
8563
  }
@@ -7161,7 +8592,8 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
7161
8592
  type: "string",
7162
8593
  enum: ["reverse", "both"],
7163
8594
  description: "'reverse' (default) = only what depends on this node. 'both' = full neighborhood."
7164
- }
8595
+ },
8596
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION }
7165
8597
  },
7166
8598
  required: ["node_id"]
7167
8599
  }
@@ -7183,9 +8615,10 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
7183
8615
  s: "source_node_index",
7184
8616
  d: "target_node_index",
7185
8617
  t: "type",
7186
- l: "label"
8618
+ l: "label",
8619
+ tl: "target_layer (only set on cross-layer edges, e.g. ui\u2192api calls_api)"
7187
8620
  },
7188
- 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."
7189
8622
  };
7190
8623
  COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
7191
8624
  "id",
@@ -7204,7 +8637,8 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
7204
8637
  "conditions",
7205
8638
  "variables",
7206
8639
  "responses",
7207
- "params"
8640
+ "params",
8641
+ "effects"
7208
8642
  ]);
7209
8643
  EST_CHARS_PER_NODE_FULL = {
7210
8644
  ui: 300,
@@ -7225,16 +8659,18 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
7225
8659
  DEFAULT_EST_NODE_MIN = 150;
7226
8660
  DEFAULT_EST_EDGE = 65;
7227
8661
  NEIGHBORHOOD_BUDGET_CHARS = 55e3;
8662
+ MAX_FILTER_EDGES = 200;
7228
8663
  BATCH_BUDGET_CHARS = 6e4;
8664
+ watcherHandle = null;
7229
8665
  }
7230
8666
  });
7231
8667
 
7232
8668
  // src/server/graph-mcp-entry.ts
7233
8669
  var import_node_child_process3 = require("node:child_process");
7234
- var import_node_fs21 = require("node:fs");
7235
- 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"));
7236
8672
  var import_node_os3 = require("node:os");
7237
- var import_node_fs22 = require("node:fs");
8673
+ var import_node_fs25 = require("node:fs");
7238
8674
  init_lockfile();
7239
8675
  function logStderr(msg) {
7240
8676
  process.stderr.write(`[launch-chart] ${msg}
@@ -7250,11 +8686,11 @@ function maybeAutoServe() {
7250
8686
  return;
7251
8687
  }
7252
8688
  try {
7253
- const logDir = import_node_path22.default.join((0, import_node_os3.homedir)(), ".launchsecure");
7254
- (0, import_node_fs22.mkdirSync)(logDir, { recursive: true });
7255
- const logPath = import_node_path22.default.join(logDir, "launch-chart.log");
7256
- const out = (0, import_node_fs21.openSync)(logPath, "a");
7257
- 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");
7258
8694
  const entryPath = process.argv[1];
7259
8695
  const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
7260
8696
  detached: true,
@@ -7276,6 +8712,12 @@ async function main() {
7276
8712
  runServeCli2(argv.slice(1));
7277
8713
  return;
7278
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
+ }
7279
8721
  maybeAutoServe();
7280
8722
  const { startGraphMcpServer: startGraphMcpServer2 } = await Promise.resolve().then(() => (init_graph_mcp(), graph_mcp_exports));
7281
8723
  startGraphMcpServer2();