@launchsecure/launch-kit 0.0.30 → 0.0.31

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 (102) hide show
  1. package/dist/beacon/beacon.mjs +934 -865
  2. package/dist/beacon/beacon.mjs.map +1 -1
  3. package/dist/beacon/beacon.umd.js +9 -9
  4. package/dist/beacon/beacon.umd.js.map +1 -1
  5. package/dist/beacon/types/internal/screenshot.d.ts +19 -1
  6. package/dist/beacon/types/internal/screenshot.d.ts.map +1 -1
  7. package/dist/beacon/types/plugins/domEle.d.ts.map +1 -1
  8. package/dist/chart-client/assets/{index-CJ4mgRRF.css → index-CDIhdgWg.css} +1 -1
  9. package/dist/chart-client/index.html +2 -2
  10. package/dist/client/assets/{index-DI5qSR_w.css → index-CfW4n40I.css} +1 -1
  11. package/dist/client/index.html +2 -2
  12. package/dist/council-client/assets/{index-C_-vAM9L.css → index-CZim6x1u.css} +1 -1
  13. package/dist/council-client/index.html +2 -2
  14. package/dist/deck-client/assets/{_baseUniq-DCt2IMRR.js → _baseUniq-DdHaBFYO.js} +1 -1
  15. package/dist/deck-client/assets/{arc-h-ifqmNR.js → arc-D98e_18X.js} +1 -1
  16. package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-C9dITSPv.js → architectureDiagram-Q4EWVU46-DNFZzh-4.js} +1 -1
  17. package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-BHuJT34t.js → blockDiagram-DXYQGD6D-DeQvGUdX.js} +1 -1
  18. package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CpvMGtDG.js → c4Diagram-AHTNJAMY-B6ekZf1n.js} +1 -1
  19. package/dist/deck-client/assets/channel-DmR7Tyyt.js +1 -0
  20. package/dist/deck-client/assets/{chunk-4BX2VUAB-B6md1VIm.js → chunk-4BX2VUAB-9aDWymq2.js} +1 -1
  21. package/dist/deck-client/assets/{chunk-4TB4RGXK-BmEnX8ik.js → chunk-4TB4RGXK-DtKQqaI7.js} +1 -1
  22. package/dist/deck-client/assets/{chunk-55IACEB6-BZPUyZAZ.js → chunk-55IACEB6-COy9hEae.js} +1 -1
  23. package/dist/deck-client/assets/{chunk-EDXVE4YY-BWwNUK-l.js → chunk-EDXVE4YY-D_f861An.js} +1 -1
  24. package/dist/deck-client/assets/{chunk-FMBD7UC4-o7gSppGI.js → chunk-FMBD7UC4-CmuA5UKn.js} +1 -1
  25. package/dist/deck-client/assets/{chunk-OYMX7WX6-C4KoTL5p.js → chunk-OYMX7WX6-vT8z8D-0.js} +1 -1
  26. package/dist/deck-client/assets/{chunk-QZHKN3VN-jkf68sDs.js → chunk-QZHKN3VN-CTlwwg-R.js} +1 -1
  27. package/dist/deck-client/assets/{chunk-YZCP3GAM-Cd4yBE7o.js → chunk-YZCP3GAM-C44yr620.js} +1 -1
  28. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-Bl4ozQWs.js +1 -0
  29. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-Bl4ozQWs.js +1 -0
  30. package/dist/deck-client/assets/clone-BAy58j24.js +1 -0
  31. package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-DeGFUgAV.js → cose-bilkent-S5V4N54A-DBB2J2nL.js} +1 -1
  32. package/dist/deck-client/assets/{dagre-KV5264BT-ekcYJuUV.js → dagre-KV5264BT-DxDTYbKl.js} +1 -1
  33. package/dist/deck-client/assets/{diagram-5BDNPKRD-YHPk4rV2.js → diagram-5BDNPKRD-DByWrWd1.js} +1 -1
  34. package/dist/deck-client/assets/{diagram-G4DWMVQ6-DM-JCd_B.js → diagram-G4DWMVQ6-B8B6ddMq.js} +1 -1
  35. package/dist/deck-client/assets/{diagram-MMDJMWI5-l5FK1ybk.js → diagram-MMDJMWI5-BMUZ2PWK.js} +1 -1
  36. package/dist/deck-client/assets/{diagram-TYMM5635-CIN4_1-j.js → diagram-TYMM5635-Bk9e8BB-.js} +1 -1
  37. package/dist/deck-client/assets/{erDiagram-SMLLAGMA-MyinSkEl.js → erDiagram-SMLLAGMA-DcOSwSol.js} +1 -1
  38. package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-Dk8nn42x.js → flowDiagram-DWJPFMVM-DI-4BR0F.js} +1 -1
  39. package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-BU1ihicu.js → ganttDiagram-T4ZO3ILL-BeZuXBoU.js} +1 -1
  40. package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-BjsTL13C.js → gitGraphDiagram-UUTBAWPF-Bcki__f-.js} +1 -1
  41. package/dist/deck-client/assets/{graph-DJmh-xi7.js → graph-CifKx6G1.js} +1 -1
  42. package/dist/deck-client/assets/index-6sdqbm2o.js +2 -0
  43. package/dist/deck-client/assets/{index-DsIZ3LqL.css → index-BlTlhxFW.css} +1 -1
  44. package/dist/deck-client/assets/index-CB-qlwRT.js +1195 -0
  45. package/dist/deck-client/assets/{infoDiagram-42DDH7IO-Dxvy_RB4.js → infoDiagram-42DDH7IO-CReN1nFN.js} +1 -1
  46. package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DPOaNF1l.js → ishikawaDiagram-UXIWVN3A-CDF_VLN_.js} +1 -1
  47. package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-DMew3K5c.js → journeyDiagram-VCZTEJTY-DwgGrNVB.js} +1 -1
  48. package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-csciJFuk.js → kanban-definition-6JOO6SKY-DB_zohh5.js} +1 -1
  49. package/dist/deck-client/assets/{layout-Dg4yyms2.js → layout-DFfX1O3z.js} +1 -1
  50. package/dist/deck-client/assets/{linear-BA3zU6gq.js → linear-CtKb4EXj.js} +1 -1
  51. package/dist/deck-client/assets/{min-lz-Ird-p.js → min-DCRRwUZv.js} +1 -1
  52. package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-CCEN8OQV.js → mindmap-definition-QFDTVHPH-D0QBOiFe.js} +1 -1
  53. package/dist/deck-client/assets/{pieDiagram-DEJITSTG-DM6n1HY7.js → pieDiagram-DEJITSTG-CD-EV5WB.js} +1 -1
  54. package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-_ULoR66n.js → quadrantDiagram-34T5L4WZ-B-JXZ8xI.js} +1 -1
  55. package/dist/deck-client/assets/{requirementDiagram-MS252O5E-BuwJs7Tn.js → requirementDiagram-MS252O5E-D2_OK5Dp.js} +1 -1
  56. package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BEsuzkW4.js → sankeyDiagram-XADWPNL6-BbBJqVSC.js} +1 -1
  57. package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-CP2H0YWf.js → sequenceDiagram-FGHM5R23-Db8A-Rkk.js} +1 -1
  58. package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-B5Gw_NNL.js → stateDiagram-FHFEXIEX-DGJnanjS.js} +1 -1
  59. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CR7riiab.js +1 -0
  60. package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-DsoYydQa.js → timeline-definition-GMOUNBTQ-BRkr6T4w.js} +1 -1
  61. package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-Dz8JT_ob.js → vennDiagram-DHZGUBPP-d0rsTqFo.js} +1 -1
  62. package/dist/deck-client/assets/wardley-RL74JXVD-2t7cMqdS.js +162 -0
  63. package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-DN1LJMB1.js → wardleyDiagram-NUSXRM2D-DzboAsHh.js} +1 -1
  64. package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-nb0oSfrQ.js → xychartDiagram-5P7HB3ND-CgTP9u2V.js} +1 -1
  65. package/dist/deck-client/index.html +2 -2
  66. package/dist/server/cli.js +666 -12
  67. package/dist/server/council-entry.js +0 -0
  68. package/dist/server/deck-mcp-entry.js +141 -21
  69. package/dist/server/deck-serve.js +141 -21
  70. package/dist/server/fb-wizard.js +0 -0
  71. package/dist/server/graph-mcp-entry.js +666 -12
  72. package/dist/server/init-entry.js +15 -9
  73. package/package.json +23 -21
  74. package/scaffolds/ls-marketplace/plugins/kit/skills/analyse/SKILL.md +180 -0
  75. package/scaffolds/ls-marketplace/plugins/kit/skills/{blast-radius.md → blast-radius/SKILL.md} +28 -12
  76. package/scaffolds/ls-marketplace/plugins/kit/skills/{debug.md → debug/SKILL.md} +2 -9
  77. package/scaffolds/ls-marketplace/plugins/kit/skills/{diagram.md → diagram/SKILL.md} +27 -9
  78. package/scaffolds/ls-marketplace/plugins/kit/skills/{prototype.md → prototype/SKILL.md} +21 -1
  79. package/scaffolds/ls-marketplace/plugins/kit/skills/recovery/SKILL.md +95 -0
  80. package/scaffolds/ls-marketplace/plugins/kit/skills/{wireframe.md → wireframe/SKILL.md} +21 -1
  81. package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +0 -0
  82. package/scaffolds/recall-hook/scripts/ensure-recall.sh +0 -0
  83. package/dist/deck-client/assets/channel-2PZVMiXf.js +0 -1
  84. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-Bt8xBAof.js +0 -1
  85. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-Bt8xBAof.js +0 -1
  86. package/dist/deck-client/assets/clone-BHQryoDl.js +0 -1
  87. package/dist/deck-client/assets/index-KsShfCV-.js +0 -476
  88. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-4T4wMDXr.js +0 -1
  89. package/dist/deck-client/assets/wardley-RL74JXVD-DGHQ_Ijv.js +0 -162
  90. package/scaffolds/ls-marketplace/plugins/kit/skills/recall.md +0 -83
  91. /package/dist/chart-client/assets/{index-Ccy-DpI-.js → index-B__ARB8k.js} +0 -0
  92. /package/dist/client/assets/{index-Dp0_okva.js → index-h8kMzVtG.js} +0 -0
  93. /package/dist/council-client/assets/{index-Dt4zWKSj.js → index-CWaDcsFR.js} +0 -0
  94. /package/scaffolds/ls-marketplace/plugins/kit/skills/{beacon-array.md → beacon-array/SKILL.md} +0 -0
  95. /package/scaffolds/ls-marketplace/plugins/kit/skills/{beacon-clear.md → beacon-clear/SKILL.md} +0 -0
  96. /package/scaffolds/ls-marketplace/plugins/kit/skills/{beacon-pulse.md → beacon-pulse/SKILL.md} +0 -0
  97. /package/scaffolds/ls-marketplace/plugins/kit/skills/{beacon-scan.md → beacon-scan/SKILL.md} +0 -0
  98. /package/scaffolds/ls-marketplace/plugins/kit/skills/{brief.md → brief/SKILL.md} +0 -0
  99. /package/scaffolds/ls-marketplace/plugins/kit/skills/{course.md → course/SKILL.md} +0 -0
  100. /package/scaffolds/ls-marketplace/plugins/kit/skills/{deploy-check.md → deploy-check/SKILL.md} +0 -0
  101. /package/scaffolds/ls-marketplace/plugins/kit/skills/{orbit.md → orbit/SKILL.md} +0 -0
  102. /package/scaffolds/ls-marketplace/plugins/kit/skills/{show-mcp-status.md → show-mcp-status/SKILL.md} +0 -0
@@ -8361,9 +8361,13 @@ function runReadGraphQueryRaw(rootDir, args) {
8361
8361
  const minimal = args.minimal ?? layerIsDb;
8362
8362
  const includeEdges = args.include_edges;
8363
8363
  const includeFindings = args.include_findings === true;
8364
+ const tagPredicates = Array.isArray(args.tag_predicates) ? args.tag_predicates : void 0;
8365
+ const includeCrossRefs = args.include_cross_refs === true;
8366
+ const crossRefType = args.cross_ref_type;
8367
+ const edgeConfidence = Array.isArray(args.edge_confidence) ? new Set(args.edge_confidence.map((s) => String(s).toLowerCase())) : void 0;
8364
8368
  const offset = args.offset ?? 0;
8365
8369
  const limit = args.limit;
8366
- const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
8370
+ const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue || tagPredicates && tagPredicates.length > 0);
8367
8371
  if (layer) {
8368
8372
  const available = getAvailableLayers(rootDir);
8369
8373
  if (available.length > 0 && !available.includes(layer)) {
@@ -8414,6 +8418,14 @@ function runReadGraphQueryRaw(rootDir, args) {
8414
8418
  result2.contradictions = graph.contradictions ?? [];
8415
8419
  result2.flagged_edges = graph.flagged_edges ?? [];
8416
8420
  }
8421
+ const neighborhoodIds = new Set(nb.nodes.map((n) => n.id));
8422
+ attachCrossRefsAndFlagged(result2, graph, neighborhoodIds, {
8423
+ includeCrossRefs,
8424
+ crossRefType,
8425
+ edgeConfidence,
8426
+ scopeToIds: true,
8427
+ layer
8428
+ });
8417
8429
  return result2;
8418
8430
  }
8419
8431
  if (!hasFilter) {
@@ -8434,6 +8446,11 @@ function runReadGraphQueryRaw(rootDir, args) {
8434
8446
  const nodeTags = n.tags;
8435
8447
  if (module_ && nodeTags?.module !== module_) return false;
8436
8448
  if (tagKey && tagValue && nodeTags?.[tagKey] !== tagValue) return false;
8449
+ if (tagPredicates && tagPredicates.length > 0) {
8450
+ for (const p of tagPredicates) {
8451
+ if (!evaluatePredicate(n, p)) return false;
8452
+ }
8453
+ }
8437
8454
  return true;
8438
8455
  });
8439
8456
  if (matched.length === 0) {
@@ -8481,8 +8498,89 @@ function runReadGraphQueryRaw(rootDir, args) {
8481
8498
  result.contradictions = graph.contradictions ?? [];
8482
8499
  result.flagged_edges = graph.flagged_edges ?? [];
8483
8500
  }
8501
+ attachCrossRefsAndFlagged(result, graph, returnedIds, {
8502
+ includeCrossRefs,
8503
+ crossRefType,
8504
+ edgeConfidence,
8505
+ scopeToIds: true,
8506
+ layer
8507
+ });
8484
8508
  return result;
8485
8509
  }
8510
+ function attachCrossRefsAndFlagged(result, graph, scopeIds, opts) {
8511
+ if (opts.includeCrossRefs) {
8512
+ const allCrossRefs = graph.cross_refs ?? [];
8513
+ const crossRefsByNode = {};
8514
+ for (const id of scopeIds) {
8515
+ crossRefsByNode[id] = { outgoing: [], incoming: [] };
8516
+ }
8517
+ for (const cr of allCrossRefs) {
8518
+ if (opts.crossRefType && cr.type !== opts.crossRefType) continue;
8519
+ if (crossRefsByNode[cr.source]) crossRefsByNode[cr.source].outgoing.push(cr);
8520
+ if (crossRefsByNode[cr.target]) crossRefsByNode[cr.target].incoming.push(cr);
8521
+ }
8522
+ result.cross_refs = crossRefsByNode;
8523
+ if (opts.crossRefType) result.cross_ref_type = opts.crossRefType;
8524
+ if (allCrossRefs.length === 0) {
8525
+ result.parser_warning = parserWarning([opts.layer], "cross_refs", "cross-layer references parser(s) (fetch-resolver / api-annotations / url-literal-scanner / static-ref-scanner)");
8526
+ }
8527
+ }
8528
+ if (opts.edgeConfidence && opts.edgeConfidence.size > 0) {
8529
+ const allFlagged = graph.flagged_edges ?? [];
8530
+ const useScope = opts.scopeToIds && scopeIds.size > 0;
8531
+ result.flagged_edges = allFlagged.filter((fe) => {
8532
+ if (!opts.edgeConfidence.has(String(fe.confidence).toLowerCase())) return false;
8533
+ if (useScope && !scopeIds.has(fe.source) && !scopeIds.has(fe.target)) return false;
8534
+ return true;
8535
+ });
8536
+ if (allFlagged.length === 0 && !result.parser_warning) {
8537
+ result.parser_warning = parserWarning([opts.layer], "flagged_edges", opts.layer === "db" ? "sql-migrations parser" : "cross-layer parsers");
8538
+ }
8539
+ }
8540
+ }
8541
+ function parserWarning(layers, capability, parserName) {
8542
+ return `The ${capability} data is empty in scanned layer(s): ${layers.join(", ")}. This likely means the ${parserName} did not produce output for this project \u2014 run detect_project_stack to verify parser configuration, then generate_graph to refresh. (If the parser IS configured and the source data is genuinely empty, ignore this warning.)`;
8543
+ }
8544
+ function evaluatePredicate(node, p) {
8545
+ if (!p.field || !p.op) return true;
8546
+ const tags = node.tags;
8547
+ const fromTag = tags?.[p.field];
8548
+ const fromField = node[p.field];
8549
+ const present = fromTag !== void 0 || fromField !== void 0;
8550
+ if (p.op === "exists") return present;
8551
+ if (p.op === "not_exists") return !present;
8552
+ if (!present) return false;
8553
+ const raw = fromTag !== void 0 ? coerceTagValue(fromTag, p.value) : fromField;
8554
+ switch (p.op) {
8555
+ case "eq":
8556
+ return raw === p.value;
8557
+ case "ne":
8558
+ return raw !== p.value;
8559
+ case "gt":
8560
+ return typeof raw === "number" && typeof p.value === "number" && raw > p.value;
8561
+ case "lt":
8562
+ return typeof raw === "number" && typeof p.value === "number" && raw < p.value;
8563
+ case "gte":
8564
+ return typeof raw === "number" && typeof p.value === "number" && raw >= p.value;
8565
+ case "lte":
8566
+ return typeof raw === "number" && typeof p.value === "number" && raw <= p.value;
8567
+ case "in":
8568
+ return Array.isArray(p.value) && p.value.includes(raw);
8569
+ default:
8570
+ return false;
8571
+ }
8572
+ }
8573
+ function coerceTagValue(raw, target) {
8574
+ if (typeof target === "boolean") {
8575
+ if (raw === "true") return true;
8576
+ if (raw === "false") return false;
8577
+ }
8578
+ if (typeof target === "number") {
8579
+ const n = Number(raw);
8580
+ if (!Number.isNaN(n)) return n;
8581
+ }
8582
+ return raw;
8583
+ }
8486
8584
  function runReadGraphQuery(rootDir, args) {
8487
8585
  const raw = runReadGraphQueryRaw(rootDir, args);
8488
8586
  return compactResult(raw);
@@ -8593,8 +8691,23 @@ function handleInspectNode(args) {
8593
8691
  } else {
8594
8692
  matched = graph.nodes;
8595
8693
  }
8596
- const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params", "effects", "ui_labels", "notes"];
8694
+ const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params", "effects", "ui_labels", "notes", "crossRefs"];
8597
8695
  const requestedFields = fields ?? allDeepFields;
8696
+ const crossRefTypeArg = args.cross_ref_type;
8697
+ const wantCrossRefs = requestedFields.includes("crossRefs");
8698
+ let crossRefsBySource = null;
8699
+ let crossRefsByTarget = null;
8700
+ if (wantCrossRefs) {
8701
+ crossRefsBySource = /* @__PURE__ */ new Map();
8702
+ crossRefsByTarget = /* @__PURE__ */ new Map();
8703
+ for (const cr of graph.cross_refs ?? []) {
8704
+ if (crossRefTypeArg && cr.type !== crossRefTypeArg) continue;
8705
+ if (!crossRefsBySource.has(cr.source)) crossRefsBySource.set(cr.source, []);
8706
+ crossRefsBySource.get(cr.source).push(cr);
8707
+ if (!crossRefsByTarget.has(cr.target)) crossRefsByTarget.set(cr.target, []);
8708
+ crossRefsByTarget.get(cr.target).push(cr);
8709
+ }
8710
+ }
8598
8711
  let filterRegex = null;
8599
8712
  if (filter) {
8600
8713
  try {
@@ -8617,7 +8730,14 @@ function handleInspectNode(args) {
8617
8730
  const deep = { id: node.id, name: node.name, type: node.type };
8618
8731
  let hasData = false;
8619
8732
  for (const field of requestedFields) {
8620
- if (allDeepFields.includes(field) && node[field] != null) {
8733
+ if (field === "crossRefs") {
8734
+ const outgoing = crossRefsBySource?.get(node.id) ?? [];
8735
+ const incoming = crossRefsByTarget?.get(node.id) ?? [];
8736
+ if (outgoing.length > 0 || incoming.length > 0) {
8737
+ deep.crossRefs = { outgoing, incoming };
8738
+ hasData = true;
8739
+ }
8740
+ } else if (allDeepFields.includes(field) && node[field] != null) {
8621
8741
  deep[field] = node[field];
8622
8742
  hasData = true;
8623
8743
  }
@@ -8641,10 +8761,14 @@ function handleInspectNode(args) {
8641
8761
  const hint = filter ? `No nodes with deep fields matching /${filter}/${caseInsensitive ? "i" : ""} in ${layer} layer.` : `No nodes matching "${search}" in ${layer} layer.`;
8642
8762
  return err(hint);
8643
8763
  }
8764
+ const wantedCrossRefs = wantCrossRefs;
8765
+ const graphHasNoCrossRefs = (graph.cross_refs ?? []).length === 0;
8766
+ const extraWarning = wantedCrossRefs && graphHasNoCrossRefs ? { parser_warning: parserWarning([layer], "cross_refs", "cross-layer references parser(s) (fetch-resolver / api-annotations / url-literal-scanner / static-ref-scanner)") } : {};
8644
8767
  return okJson({
8645
8768
  layer,
8646
8769
  matched: results.length,
8647
8770
  ...results.length >= maxResults ? { truncated: true, hint: `Showing first ${maxResults} matches. Narrow with search param.` } : {},
8771
+ ...extraWarning,
8648
8772
  nodes: results
8649
8773
  });
8650
8774
  }
@@ -8850,13 +8974,19 @@ function handleEffectsIndex(args) {
8850
8974
  const __resolved = resolveOrErr(args);
8851
8975
  if ("content" in __resolved) return __resolved;
8852
8976
  const { rootDir } = __resolved;
8977
+ const kind = args.kind ?? "collisions";
8978
+ const key = args.key;
8979
+ const MAX_KEYS = 200;
8980
+ if (kind === "fetch_calls") {
8981
+ return okJson(buildFetchCallsIndex(rootDir, key, MAX_KEYS));
8982
+ }
8983
+ if (kind === "api_effects") {
8984
+ return okJson(buildApiEffectsIndex(rootDir, key, MAX_KEYS));
8985
+ }
8853
8986
  const idx = readEffectsIndex(rootDir);
8854
8987
  if (!idx) {
8855
8988
  return err("No effects-index.json found. Run generate_graph first (or wait for the file-watcher to fire).");
8856
8989
  }
8857
- const kind = args.kind ?? "collisions";
8858
- const key = args.key;
8859
- const MAX_KEYS = 200;
8860
8990
  if (kind === "singleton_risks") {
8861
8991
  return okJson({ kind, count: idx.singleton_risks.length, nodes: idx.singleton_risks });
8862
8992
  }
@@ -8870,7 +9000,7 @@ function handleEffectsIndex(args) {
8870
9000
  }
8871
9001
  const map = idx[kind];
8872
9002
  if (!map || typeof map !== "object") {
8873
- return err(`Unknown kind "${kind}". Valid: dom_ids, window_events, storage_keys, fetch_urls, timers, singleton_risks, collisions.`);
9003
+ return err(`Unknown kind "${kind}". Valid: dom_ids, window_events, storage_keys, fetch_urls, timers, singleton_risks, collisions, fetch_calls, api_effects.`);
8874
9004
  }
8875
9005
  if (key) {
8876
9006
  const nodes = map[key] ?? [];
@@ -8888,6 +9018,97 @@ function handleEffectsIndex(args) {
8888
9018
  results
8889
9019
  });
8890
9020
  }
9021
+ function buildFetchCallsIndex(rootDir, key, maxKeys) {
9022
+ const ui = readGraph(rootDir, "ui");
9023
+ if (!ui) return { kind: "fetch_calls", error: "No ui graph found. Run generate_graph first." };
9024
+ const inverted = /* @__PURE__ */ new Map();
9025
+ let callsApiCount = 0;
9026
+ for (const cr of ui.cross_refs ?? []) {
9027
+ if (cr.type !== "calls_api") continue;
9028
+ callsApiCount++;
9029
+ if (!inverted.has(cr.target)) inverted.set(cr.target, /* @__PURE__ */ new Set());
9030
+ inverted.get(cr.target).add(cr.source);
9031
+ }
9032
+ for (const n of ui.nodes) {
9033
+ const eff = n.effects;
9034
+ if (!eff?.fetches) continue;
9035
+ for (const f of eff.fetches) {
9036
+ if (!inverted.has(f)) inverted.set(f, /* @__PURE__ */ new Set());
9037
+ inverted.get(f).add(n.id);
9038
+ }
9039
+ }
9040
+ const allCrossRefs = ui.cross_refs ?? [];
9041
+ const fetchResolverAbsent = callsApiCount === 0 && allCrossRefs.length === 0;
9042
+ const partialResolverGap = callsApiCount === 0 && inverted.size > 0;
9043
+ if (key) {
9044
+ return {
9045
+ kind: "fetch_calls",
9046
+ key,
9047
+ callers: [...inverted.get(key) ?? []],
9048
+ ...fetchResolverAbsent ? { parser_warning: parserWarning(["ui"], "cross_refs", "fetch-resolver cross-layer parser") } : {},
9049
+ ...partialResolverGap ? { parser_warning: "No calls_api cross_refs found \u2014 fetch-resolver may not be enabled. Only raw fetch() strings from ui.node.effects.fetches are indexed; resolved-to-endpoint links are missing." } : {}
9050
+ };
9051
+ }
9052
+ const entries = [...inverted.entries()].sort((a, b) => b[1].size - a[1].size);
9053
+ const truncated = entries.length > maxKeys;
9054
+ const results = {};
9055
+ for (const [k, v] of entries.slice(0, maxKeys)) results[k] = [...v];
9056
+ return {
9057
+ kind: "fetch_calls",
9058
+ total_keys: entries.length,
9059
+ ...truncated ? { truncated: true, showing: maxKeys, hint: "Pass `key` (endpoint path or URL template) to look up callers." } : {},
9060
+ ...fetchResolverAbsent ? { parser_warning: parserWarning(["ui"], "cross_refs", "fetch-resolver cross-layer parser") } : {},
9061
+ ...partialResolverGap ? { parser_warning: "No calls_api cross_refs found \u2014 fetch-resolver may not be enabled. Only raw fetch() strings from ui.node.effects.fetches are indexed; resolved-to-endpoint links are missing." } : {},
9062
+ results
9063
+ };
9064
+ }
9065
+ function buildApiEffectsIndex(rootDir, key, maxKeys) {
9066
+ const api = readGraph(rootDir, "api");
9067
+ if (!api) return { kind: "api_effects", error: "No api graph found. Run generate_graph first." };
9068
+ const categories = ["calls", "fetches", "persists", "subscribes", "timers", "dom_writes", "globals"];
9069
+ const inverted = {};
9070
+ for (const c of categories) inverted[c] = /* @__PURE__ */ new Map();
9071
+ for (const n of api.nodes) {
9072
+ const eff = n.effects;
9073
+ if (!eff) continue;
9074
+ for (const c of categories) {
9075
+ const items = eff[c];
9076
+ if (!Array.isArray(items)) continue;
9077
+ for (const item of items) {
9078
+ if (!inverted[c].has(item)) inverted[c].set(item, /* @__PURE__ */ new Set());
9079
+ inverted[c].get(item).add(n.id);
9080
+ }
9081
+ }
9082
+ }
9083
+ if (key) {
9084
+ const hits = {};
9085
+ for (const c of categories) {
9086
+ const endpoints = inverted[c].get(key);
9087
+ if (endpoints && endpoints.size > 0) hits[c] = [...endpoints];
9088
+ }
9089
+ return { kind: "api_effects", key, hits };
9090
+ }
9091
+ const TOP_CATEGORIES = Math.min(maxKeys, 50);
9092
+ const TOP_CALLERS = 10;
9093
+ const summary = {};
9094
+ for (const c of categories) {
9095
+ const entries = [...inverted[c].entries()].sort((a, b) => b[1].size - a[1].size);
9096
+ const top = {};
9097
+ for (const [k, v] of entries.slice(0, TOP_CATEGORIES)) {
9098
+ const endpoints = [...v];
9099
+ top[k] = {
9100
+ count: endpoints.length,
9101
+ sample_endpoints: endpoints.slice(0, TOP_CALLERS)
9102
+ };
9103
+ }
9104
+ summary[c] = { total_items: entries.length, top };
9105
+ }
9106
+ return {
9107
+ kind: "api_effects",
9108
+ hint: `Summary mode: top ${TOP_CATEGORIES} items per category, top ${TOP_CALLERS} endpoints per item. Pass \`key\` (a specific call/url/event) to get the FULL caller list across all categories.`,
9109
+ summary
9110
+ };
9111
+ }
8891
9112
  function handleChartServerStatus() {
8892
9113
  const rootDir = process.cwd();
8893
9114
  const lock = getLiveLock(rootDir);
@@ -9001,6 +9222,296 @@ function handleRemoveTag(args) {
9001
9222
  removeTag(rootDir, nodeId, key);
9002
9223
  return okJson({ ok: true, node_id: nodeId, removed_key: key });
9003
9224
  }
9225
+ function handleMigrationAudit(args) {
9226
+ const __resolved = resolveOrErr(args);
9227
+ if ("content" in __resolved) return __resolved;
9228
+ const { rootDir } = __resolved;
9229
+ const requireOrphan = args.require_orphan_check !== false;
9230
+ const requireBackup = args.require_sidecar_backup !== false;
9231
+ const requirePreFlight = args.require_pre_flight_notice !== false;
9232
+ const includeSafe = args.include_safe === true;
9233
+ const offset = args.offset ?? 0;
9234
+ const limit = args.limit ?? 10;
9235
+ const db = readGraph(rootDir, "db");
9236
+ if (!db) return err("No db graph found. Run generate_graph first.");
9237
+ const migrations = db.nodes.filter((n) => n.type === "migration");
9238
+ const audit = [];
9239
+ for (const m of migrations) {
9240
+ const isDestructive = m.is_destructive === true;
9241
+ if (!isDestructive && !includeSafe) continue;
9242
+ const violations = [];
9243
+ if (isDestructive) {
9244
+ if (requireOrphan && m.has_orphan_check !== true) violations.push("missing_orphan_check");
9245
+ if (requireBackup && m.has_sidecar_backup !== true) violations.push("missing_sidecar_backup");
9246
+ if (requirePreFlight && m.has_pre_flight_notice !== true) violations.push("missing_pre_flight_notice");
9247
+ }
9248
+ if (!isDestructive && includeSafe && violations.length === 0) {
9249
+ audit.push({ id: m.id, name: m.name, timestamp: m.timestamp, is_destructive: false, status: "safe" });
9250
+ continue;
9251
+ }
9252
+ if (violations.length === 0 && !includeSafe) continue;
9253
+ audit.push({
9254
+ id: m.id,
9255
+ name: m.name,
9256
+ timestamp: m.timestamp,
9257
+ filepath: m.filepath,
9258
+ is_destructive: isDestructive,
9259
+ contains_drop_column: m.contains_drop_column === true,
9260
+ contains_drop_table: m.contains_drop_table === true,
9261
+ contains_backfill: m.contains_backfill === true,
9262
+ has_orphan_check: m.has_orphan_check === true,
9263
+ has_sidecar_backup: m.has_sidecar_backup === true,
9264
+ has_pre_flight_notice: m.has_pre_flight_notice === true,
9265
+ statement_count: m.statement_count,
9266
+ violations,
9267
+ status: violations.length === 0 ? "safe" : "unsafe"
9268
+ });
9269
+ }
9270
+ audit.sort((a, b) => String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")));
9271
+ const total = audit.length;
9272
+ const page = audit.slice(offset, offset + limit);
9273
+ const hasMore = offset + page.length < total;
9274
+ return okJson({
9275
+ total,
9276
+ returned: page.length,
9277
+ offset,
9278
+ has_more: hasMore,
9279
+ ...hasMore ? { next_offset: offset + page.length } : {},
9280
+ items: page,
9281
+ discipline: "Layer 1 (expand-and-contract) + Layer 2 (pg_dump wrapper) + Layer 3 (in-migration SQL guards) per CLAUDE.md. This tool checks Layer 3 only."
9282
+ });
9283
+ }
9284
+ function handleDriftReport(args) {
9285
+ const __resolved = resolveOrErr(args);
9286
+ if ("content" in __resolved) return __resolved;
9287
+ const { rootDir } = __resolved;
9288
+ const layerArg = args.layer;
9289
+ const kind = args.kind ?? "all";
9290
+ const confidenceArg = Array.isArray(args.confidence) ? new Set(args.confidence.map((s) => String(s).toLowerCase())) : void 0;
9291
+ const offset = args.offset ?? 0;
9292
+ const limit = args.limit ?? 10;
9293
+ const layers = layerArg ? [layerArg] : getAvailableLayers(rootDir);
9294
+ const items = [];
9295
+ const layersWithAnyFinding = [];
9296
+ for (const layer of layers) {
9297
+ const g = readGraph(rootDir, layer);
9298
+ if (!g) continue;
9299
+ const cs = g.contradictions ?? [];
9300
+ const fes = g.flagged_edges ?? [];
9301
+ if (cs.length > 0 || fes.length > 0) layersWithAnyFinding.push(layer);
9302
+ if (kind === "all" || kind === "contradictions") {
9303
+ for (const c of cs) {
9304
+ items.push({ layer, kind: "contradiction", entity: c.entity, source_a: c.source_a, source_b: c.source_b, detail: c.detail });
9305
+ }
9306
+ }
9307
+ if (kind === "all" || kind === "flagged_edges") {
9308
+ for (const fe of fes) {
9309
+ if (confidenceArg && !confidenceArg.has(String(fe.confidence).toLowerCase())) continue;
9310
+ items.push({
9311
+ layer,
9312
+ kind: "flagged_edge",
9313
+ source: fe.source,
9314
+ target: fe.target,
9315
+ edge_type: fe.type,
9316
+ confidence: fe.confidence,
9317
+ label: fe.label
9318
+ });
9319
+ }
9320
+ }
9321
+ }
9322
+ const total = items.length;
9323
+ const page = items.slice(offset, offset + limit);
9324
+ const hasMore = offset + page.length < total;
9325
+ const parserAbsent = layers.length > 0 && layersWithAnyFinding.length === 0;
9326
+ return okJson({
9327
+ total,
9328
+ returned: page.length,
9329
+ offset,
9330
+ has_more: hasMore,
9331
+ ...hasMore ? { next_offset: offset + page.length } : {},
9332
+ layers_scanned: layers,
9333
+ ...parserAbsent ? { parser_warning: parserWarning(layers, "contradictions + flagged_edges", layers.includes("db") ? "sql-migrations parser and/or cross-layer parsers" : "cross-layer parsers") } : {},
9334
+ items: page
9335
+ });
9336
+ }
9337
+ function handleWhoUses(args) {
9338
+ const __resolved = resolveOrErr(args);
9339
+ if ("content" in __resolved) return __resolved;
9340
+ const { rootDir } = __resolved;
9341
+ const target = args.target;
9342
+ if (!target) return err("target is required");
9343
+ const filterType = args.cross_ref_type;
9344
+ const offset = args.offset ?? 0;
9345
+ const limit = args.limit ?? 10;
9346
+ const hits = [];
9347
+ const layersScanned = [];
9348
+ const layersWithCrossRefs = [];
9349
+ for (const layer of getAvailableLayers(rootDir)) {
9350
+ const g = readGraph(rootDir, layer);
9351
+ if (!g) continue;
9352
+ layersScanned.push(layer);
9353
+ const crs = g.cross_refs ?? [];
9354
+ if (crs.length > 0) layersWithCrossRefs.push(layer);
9355
+ for (const cr of crs) {
9356
+ if (cr.target !== target) continue;
9357
+ if (filterType && cr.type !== filterType) continue;
9358
+ const entry = {
9359
+ source_layer: layer,
9360
+ source: cr.source,
9361
+ cross_ref_type: cr.type,
9362
+ target_layer: cr.layer
9363
+ };
9364
+ const via = cr.via;
9365
+ if (Array.isArray(via)) entry.via = via;
9366
+ hits.push(entry);
9367
+ }
9368
+ }
9369
+ hits.sort((a, b) => String(a.source).localeCompare(String(b.source)));
9370
+ const total = hits.length;
9371
+ const page = hits.slice(offset, offset + limit);
9372
+ const hasMore = offset + page.length < total;
9373
+ const parserAbsent = layersScanned.length > 0 && layersWithCrossRefs.length === 0;
9374
+ return okJson({
9375
+ target,
9376
+ ...filterType ? { cross_ref_type: filterType } : {},
9377
+ total,
9378
+ returned: page.length,
9379
+ offset,
9380
+ has_more: hasMore,
9381
+ ...hasMore ? { next_offset: offset + page.length } : {},
9382
+ ...parserAbsent ? { parser_warning: parserWarning(layersScanned, "cross_refs", "cross-layer references parser(s) (fetch-resolver / api-annotations / url-literal-scanner / static-ref-scanner)") } : {},
9383
+ items: page
9384
+ });
9385
+ }
9386
+ function handleTracePath(args) {
9387
+ const __resolved = resolveOrErr(args);
9388
+ if ("content" in __resolved) return __resolved;
9389
+ const { rootDir } = __resolved;
9390
+ const from = args.from;
9391
+ const to = args.to;
9392
+ if (!from || !to) return err("from and to are required");
9393
+ const maxHops = args.max_hops ?? 5;
9394
+ const maxPaths = args.max_paths ?? 10;
9395
+ const offset = args.offset ?? 0;
9396
+ const limit = args.limit ?? 10;
9397
+ const outgoing = /* @__PURE__ */ new Map();
9398
+ const layersScanned = [];
9399
+ const layersWithCrossRefs = [];
9400
+ for (const layer of getAvailableLayers(rootDir)) {
9401
+ const g = readGraph(rootDir, layer);
9402
+ if (!g) continue;
9403
+ layersScanned.push(layer);
9404
+ if ((g.cross_refs ?? []).length > 0) layersWithCrossRefs.push(layer);
9405
+ for (const cr of g.cross_refs ?? []) {
9406
+ const via = cr.via;
9407
+ if (Array.isArray(via) && via.length > 0) {
9408
+ let prev = cr.source;
9409
+ for (const step of via) {
9410
+ pushEdge(outgoing, { from: prev, to: step, type: cr.type, layer: cr.layer });
9411
+ prev = step;
9412
+ }
9413
+ pushEdge(outgoing, { from: prev, to: cr.target, type: cr.type, layer: cr.layer });
9414
+ } else {
9415
+ pushEdge(outgoing, { from: cr.source, to: cr.target, type: cr.type, layer: cr.layer });
9416
+ }
9417
+ }
9418
+ for (const e of g.edges ?? []) {
9419
+ pushEdge(outgoing, { from: e.source, to: e.target, type: e.type, layer });
9420
+ }
9421
+ }
9422
+ const paths = [];
9423
+ const queue = [{ nodes: [from], edges: [] }];
9424
+ while (queue.length > 0 && paths.length < maxPaths) {
9425
+ const p = queue.shift();
9426
+ const tail = p.nodes[p.nodes.length - 1];
9427
+ if (tail === to && p.nodes.length > 1) {
9428
+ paths.push(p);
9429
+ continue;
9430
+ }
9431
+ if (p.nodes.length > maxHops) continue;
9432
+ const next = outgoing.get(tail) ?? [];
9433
+ for (const e of next) {
9434
+ if (p.nodes.includes(e.to)) continue;
9435
+ queue.push({ nodes: [...p.nodes, e.to], edges: [...p.edges, e] });
9436
+ }
9437
+ }
9438
+ paths.sort((a, b) => a.nodes.length - b.nodes.length);
9439
+ const total = paths.length;
9440
+ const page = paths.slice(offset, offset + limit);
9441
+ const hasMore = offset + page.length < total;
9442
+ const parserAbsent = layersScanned.length > 0 && layersWithCrossRefs.length === 0;
9443
+ return okJson({
9444
+ from,
9445
+ to,
9446
+ max_hops: maxHops,
9447
+ max_paths: maxPaths,
9448
+ total,
9449
+ returned: page.length,
9450
+ offset,
9451
+ has_more: hasMore,
9452
+ ...hasMore ? { next_offset: offset + page.length } : {},
9453
+ ...total === maxPaths ? { search_capped: true, hint: "Hit max_paths cap \u2014 more paths may exist. Raise max_paths or narrow from/to." } : {},
9454
+ ...parserAbsent ? { parser_warning: parserWarning(layersScanned, "cross_refs", "cross-layer references parser(s)") + " Cross-layer paths are unreachable without cross_refs; only same-layer paths (imports/renders/belongs_to) are visible." } : {},
9455
+ paths: page.map((p) => ({ hops: p.nodes.length - 1, nodes: p.nodes, edges: p.edges }))
9456
+ });
9457
+ }
9458
+ function pushEdge(map, e) {
9459
+ if (!map.has(e.from)) map.set(e.from, []);
9460
+ map.get(e.from).push(e);
9461
+ }
9462
+ function handleAuthCoverageReport(args) {
9463
+ const __resolved = resolveOrErr(args);
9464
+ if ("content" in __resolved) return __resolved;
9465
+ const { rootDir } = __resolved;
9466
+ const moduleFilter = args.module;
9467
+ const strategyFilter = args.strategy;
9468
+ const offset = args.offset ?? 0;
9469
+ const limit = args.limit ?? 10;
9470
+ const api = readGraph(rootDir, "api");
9471
+ if (!api) return err("No api graph found. Run generate_graph first.");
9472
+ const byStrategy = {};
9473
+ const byModule = {};
9474
+ const unauthenticated = [];
9475
+ const matchedEndpoints = [];
9476
+ for (const n of api.nodes) {
9477
+ const tags = n.tags;
9478
+ const moduleTag = tags?.module ?? "root";
9479
+ const auth = n.auth ?? [];
9480
+ const strategyKey = auth.length === 0 ? "none" : auth.slice().sort().join("+");
9481
+ byStrategy[strategyKey] = (byStrategy[strategyKey] ?? 0) + 1;
9482
+ if (!byModule[moduleTag]) byModule[moduleTag] = { total: 0, by_strategy: {} };
9483
+ byModule[moduleTag].total += 1;
9484
+ byModule[moduleTag].by_strategy[strategyKey] = (byModule[moduleTag].by_strategy[strategyKey] ?? 0) + 1;
9485
+ if (auth.length === 0) unauthenticated.push(n.id);
9486
+ if (moduleFilter && moduleTag !== moduleFilter) continue;
9487
+ if (strategyFilter && strategyKey !== strategyFilter) continue;
9488
+ matchedEndpoints.push({
9489
+ id: n.id,
9490
+ path: n.path,
9491
+ methods: n.methods,
9492
+ module: moduleTag,
9493
+ auth,
9494
+ mutates: n.mutates === true
9495
+ });
9496
+ }
9497
+ matchedEndpoints.sort((a, b) => String(a.path ?? a.id).localeCompare(String(b.path ?? b.id)));
9498
+ const total = matchedEndpoints.length;
9499
+ const page = matchedEndpoints.slice(offset, offset + limit);
9500
+ const hasMore = offset + page.length < total;
9501
+ return okJson({
9502
+ total_endpoints: api.nodes.length,
9503
+ by_strategy: byStrategy,
9504
+ unauthenticated_count: unauthenticated.length,
9505
+ by_module: byModule,
9506
+ filter: { module: moduleFilter, strategy: strategyFilter },
9507
+ total,
9508
+ returned: page.length,
9509
+ offset,
9510
+ has_more: hasMore,
9511
+ ...hasMore ? { next_offset: offset + page.length } : {},
9512
+ items: page
9513
+ });
9514
+ }
9004
9515
  function handleAuditLayer(args) {
9005
9516
  const __resolved = resolveOrErr(args);
9006
9517
  if ("content" in __resolved) return __resolved;
@@ -9214,6 +9725,26 @@ async function handleMessage(msg) {
9214
9725
  respond(id ?? null, withFreshnessMeta(handleListNotes(args), args));
9215
9726
  return;
9216
9727
  }
9728
+ if (toolName === "migration_audit") {
9729
+ respond(id ?? null, withFreshnessMeta(handleMigrationAudit(args), args));
9730
+ return;
9731
+ }
9732
+ if (toolName === "drift_report") {
9733
+ respond(id ?? null, withFreshnessMeta(handleDriftReport(args), args));
9734
+ return;
9735
+ }
9736
+ if (toolName === "who_uses") {
9737
+ respond(id ?? null, withFreshnessMeta(handleWhoUses(args), args));
9738
+ return;
9739
+ }
9740
+ if (toolName === "trace_path") {
9741
+ respond(id ?? null, withFreshnessMeta(handleTracePath(args), args));
9742
+ return;
9743
+ }
9744
+ if (toolName === "auth_coverage_report") {
9745
+ respond(id ?? null, withFreshnessMeta(handleAuthCoverageReport(args), args));
9746
+ return;
9747
+ }
9217
9748
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
9218
9749
  return;
9219
9750
  }
@@ -9371,6 +9902,32 @@ var init_graph_mcp = __esm({
9371
9902
  type: "boolean",
9372
9903
  description: "DB layer only. When true, response includes `contradictions[]` and `flagged_edges[]` arrays from the SQL migrations parser \u2014 SQL\u2194ORM schema drift findings (missing columns/tables, type mismatches, nullability drift, FKs in SQL but no ORM @relation). Default false."
9373
9904
  },
9905
+ tag_predicates: {
9906
+ type: "array",
9907
+ description: 'Multi-condition AND filter over node attributes. Each predicate is { field, op, value }. `field` matches either a top-level node attribute (e.g. is_destructive, mutates, statement_count) OR a tag key (tag values take precedence when both exist). `op` \u2208 {eq, ne, gt, lt, gte, lte, exists, not_exists, in}. Example: [{field:"is_destructive",op:"eq",value:true},{field:"has_orphan_check",op:"eq",value:false}] returns destructive migrations missing an orphan check.',
9908
+ items: {
9909
+ type: "object",
9910
+ properties: {
9911
+ field: { type: "string", description: "Node attribute or tag key." },
9912
+ op: { type: "string", enum: ["eq", "ne", "gt", "lt", "gte", "lte", "exists", "not_exists", "in"] },
9913
+ value: { description: "Comparison value. Required for all ops except exists/not_exists. Pass an array for `in`." }
9914
+ },
9915
+ required: ["field", "op"]
9916
+ }
9917
+ },
9918
+ include_cross_refs: {
9919
+ type: "boolean",
9920
+ description: "When true, attach `cross_refs` (incoming + outgoing) to each returned node. Unlocks the cross-layer reference data (references_static, calls_api, reads_via, mutates_via, reads, mutates, references_api) that is otherwise dropped from responses. Combine with `cross_ref_type` to narrow."
9921
+ },
9922
+ cross_ref_type: {
9923
+ type: "string",
9924
+ description: "Narrow attached cross_refs by kind. Common values: calls_api, references_static, reads_via, mutates_via, reads, mutates, references_api. Only applies when include_cross_refs is true."
9925
+ },
9926
+ edge_confidence: {
9927
+ type: "array",
9928
+ description: 'Filter to surface low/medium/high-confidence flagged_edges in the response. Pass e.g. ["medium","high"] to get DYNAMIC navigation targets, FK drift, etc. Scoped to matched nodes when a node filter is in play; otherwise returns all flagged_edges at the requested confidence levels. Layer-independent.',
9929
+ items: { type: "string", enum: ["low", "medium", "high"] }
9930
+ },
9374
9931
  offset: {
9375
9932
  type: "number",
9376
9933
  description: "Skip first N matched nodes (pagination). Default 0. Use next_offset from a previous response to get the next page."
@@ -9394,6 +9951,10 @@ var init_graph_mcp = __esm({
9394
9951
  minimal: { type: "boolean" },
9395
9952
  include_edges: { type: "boolean" },
9396
9953
  include_findings: { type: "boolean" },
9954
+ tag_predicates: { type: "array" },
9955
+ include_cross_refs: { type: "boolean" },
9956
+ cross_ref_type: { type: "string" },
9957
+ edge_confidence: { type: "array" },
9397
9958
  project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
9398
9959
  worktree: { type: "string", description: WORKTREE_PARAM_DESCRIPTION2 },
9399
9960
  project_root: { type: "string", description: PROJECT_ROOT_PARAM_DESCRIPTION2 }
@@ -9492,12 +10053,16 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
9492
10053
  fields: {
9493
10054
  type: "array",
9494
10055
  items: { type: "string" },
9495
- 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".'
10056
+ description: `Specific deep fields to return. Options: elements, stateVars, conditions, variables, responses, params, effects, crossRefs. 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". "crossRefs" returns this node's incoming + outgoing cross-layer references (calls_api, references_static, reads_via, mutates_via, reads, mutates, references_api) \u2014 the cross-layer linking data otherwise dropped from responses.`
9496
10057
  },
9497
10058
  filter: {
9498
10059
  type: "string",
9499
10060
  description: "Regex pattern to search WITHIN deep field values. Only returns nodes where at least one deep field matches. Searches across all string values in the requested fields (condition tests, variable inits, element tags/props, response bodies, etc.). When set, search becomes optional and node limit is raised to 50."
9500
10061
  },
10062
+ cross_ref_type: {
10063
+ type: "string",
10064
+ description: 'Narrow returned crossRefs by kind (e.g. calls_api, references_static, reads_via). Only applies when "crossRefs" is included in fields (or fields is omitted).'
10065
+ },
9501
10066
  case_insensitive: {
9502
10067
  type: "boolean",
9503
10068
  description: "Case-insensitive filter matching. Default true."
@@ -9542,18 +10107,18 @@ Use this when the user asks "is the chart running", "show me the project graph U
9542
10107
  },
9543
10108
  {
9544
10109
  name: "effects_index",
9545
- 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".',
10110
+ description: 'Cross-layer inverted index over per-node side effects. Answers "who else touches X?" without re-walking every node. Most kinds load from a precomputed effects-index.json (built when generate_graph runs); fetch_calls and api_effects are computed on demand from the ui/api graphs so they always reflect current state.\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"), "who calls /api/work-items?" (kind="fetch_calls", key="app/api/.../work-items/route.ts"), "which endpoints call db.user.findUnique?" (kind="api_effects", key="db.user.findUnique"). \n\nReturns vary by kind: {key:[nodeIds]} map for standard kinds; {dom_ids, storage_keys, window_events} for collisions; flat node-id list for singleton_risks; {api_endpoint|url_template: [ui_files]} for fetch_calls; {category: {item: [endpoint_ids]}} summary for api_effects (or {category, key, hits} when key is set).',
9546
10111
  inputSchema: {
9547
10112
  type: "object",
9548
10113
  properties: {
9549
10114
  kind: {
9550
10115
  type: "string",
9551
- enum: ["dom_ids", "window_events", "storage_keys", "fetch_urls", "timers", "singleton_risks", "collisions"],
9552
- description: "Which inverted index to query. Default: collisions (most actionable signal)."
10116
+ enum: ["dom_ids", "window_events", "storage_keys", "fetch_urls", "timers", "singleton_risks", "collisions", "fetch_calls", "api_effects"],
10117
+ description: 'Which inverted index to query. Default: collisions (most actionable signal). fetch_calls = "UI\u2192API call inventory" (resolved via ui cross_refs + raw fetch strings). api_effects = "what does each endpoint touch" inverted across all effect categories (calls / fetches / persists / subscribes / timers / dom_writes / globals).'
9553
10118
  },
9554
10119
  key: {
9555
10120
  type: "string",
9556
- 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.'
10121
+ description: 'Optional specific key to look up within the chosen kind (e.g. "moon-shadow-blur" for dom_ids, "/api/work-items" for fetch_calls, "db.user.findUnique" for api_effects). When omitted, returns the full {key:nodes} map (or per-category summary for api_effects).'
9557
10122
  },
9558
10123
  project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
9559
10124
  worktree: { type: "string", description: WORKTREE_PARAM_DESCRIPTION2 },
@@ -9561,6 +10126,95 @@ Use this when the user asks "is the chart running", "show me the project graph U
9561
10126
  }
9562
10127
  }
9563
10128
  },
10129
+ {
10130
+ name: "migration_audit",
10131
+ description: `List Prisma migrations that violate the migration-safety discipline: destructive migrations missing one or more guards (orphan check, sidecar backup, pre-flight notice). Codifies the three-layer migration safety rules \u2014 derived from each migration node's extracted attributes (is_destructive, has_orphan_check, has_sidecar_backup, has_pre_flight_notice, contains_backfill, contains_drop_column, contains_drop_table).
10132
+
10133
+ USE THIS FOR: "are there unsafe migrations on this branch", "audit the migration history before deploy", "find migrations that drop columns without backup". Paginated. Returns one entry per offending migration with the specific missing guards listed.`,
10134
+ inputSchema: {
10135
+ type: "object",
10136
+ properties: {
10137
+ require_orphan_check: { type: "boolean", description: "Flag destructive migrations missing an orphan-count guard. Default true." },
10138
+ require_sidecar_backup: { type: "boolean", description: "Flag destructive migrations missing a sidecar backup table. Default true." },
10139
+ require_pre_flight_notice: { type: "boolean", description: "Flag destructive migrations missing a RAISE NOTICE pre-flight. Default true." },
10140
+ include_safe: { type: "boolean", description: "Also return migrations that pass all checks (no violations). Default false \u2014 by default only offenders are returned." },
10141
+ offset: { type: "number", description: "Pagination offset. Default 0." },
10142
+ limit: { type: "number", description: "Max migrations to return. Default 10." },
10143
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
10144
+ worktree: { type: "string", description: WORKTREE_PARAM_DESCRIPTION2 },
10145
+ project_root: { type: "string", description: PROJECT_ROOT_PARAM_DESCRIPTION2 }
10146
+ }
10147
+ }
10148
+ },
10149
+ {
10150
+ name: "drift_report",
10151
+ description: 'Aggregate cross-layer drift findings \u2014 contradictions (schema disagreements) + flagged_edges (low-confidence resolutions, FK drift, dynamic routes) \u2014 into one report. Generalizes the db-only `read_graph include_findings` flag to all layers.\n\nUSE THIS FOR: "is the codebase drifting", "show me SQL vs ORM mismatches", "list endpoints/components with unresolved routes", "audit schema health before refactor". Paginated. Returns items with { layer, kind, source, target, detail, confidence }.',
10152
+ inputSchema: {
10153
+ type: "object",
10154
+ properties: {
10155
+ layer: { type: "string", description: "Restrict to one layer (e.g. db, ui). Omit for all layers." },
10156
+ kind: { type: "string", enum: ["contradictions", "flagged_edges", "all"], description: "Which finding type to return. Default all." },
10157
+ confidence: { type: "array", items: { type: "string", enum: ["low", "medium", "high"] }, description: "For flagged_edges: filter by confidence levels. Default all." },
10158
+ offset: { type: "number", description: "Pagination offset. Default 0." },
10159
+ limit: { type: "number", description: "Max items to return. Default 10." },
10160
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
10161
+ worktree: { type: "string", description: WORKTREE_PARAM_DESCRIPTION2 },
10162
+ project_root: { type: "string", description: PROJECT_ROOT_PARAM_DESCRIPTION2 }
10163
+ }
10164
+ }
10165
+ },
10166
+ {
10167
+ name: "who_uses",
10168
+ description: 'List everything that references a target node via cross_refs \u2014 the cross-layer references_static / calls_api / reads_via / mutates_via / reads / mutates / references_api edges that are otherwise dropped from responses.\n\nUSE THIS FOR: "where is permission `manage_billing` used", "which UI components reference enum `WorkItemPriority`", "which endpoints touch the User table", "which files call /api/work-items". Auto-scans all layers\' cross_refs for incoming edges to the target. Paginated.',
10169
+ inputSchema: {
10170
+ type: "object",
10171
+ properties: {
10172
+ target: { type: "string", description: 'Target node id to look up (e.g. "seed:permission:manage_billing", "User", "app/api/.../work-items/route.ts", "WorkItemPriority").' },
10173
+ cross_ref_type: { type: "string", description: "Narrow by cross-ref kind (calls_api / references_static / reads_via / mutates_via / reads / mutates / references_api). Omit for all kinds." },
10174
+ offset: { type: "number", description: "Pagination offset. Default 0." },
10175
+ limit: { type: "number", description: "Max references to return. Default 10." },
10176
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
10177
+ worktree: { type: "string", description: WORKTREE_PARAM_DESCRIPTION2 },
10178
+ project_root: { type: "string", description: PROJECT_ROOT_PARAM_DESCRIPTION2 }
10179
+ },
10180
+ required: ["target"]
10181
+ }
10182
+ },
10183
+ {
10184
+ name: "trace_path",
10185
+ description: 'Find shortest paths between two nodes across layers via cross_refs (including the via[] chains that capture transitive DB access through middleware).\n\nUSE THIS FOR: "how does AdminAnalyticsPage end up reading the User table" \u2014 returns the call chain page \u2192 fetch \u2192 endpoint \u2192 middleware \u2192 db. BFS-bounded by max_hops + max_paths to keep responses tractable on hub nodes. Paginated.',
10186
+ inputSchema: {
10187
+ type: "object",
10188
+ properties: {
10189
+ from: { type: "string", description: "Source node id." },
10190
+ to: { type: "string", description: "Target node id." },
10191
+ max_hops: { type: "number", description: "Hard cap on path length. Default 5." },
10192
+ max_paths: { type: "number", description: "Hard cap on total paths discovered. Default 10. Once hit, BFS stops." },
10193
+ offset: { type: "number", description: "Pagination offset over discovered paths. Default 0." },
10194
+ limit: { type: "number", description: "Max paths returned per response. Default 10." },
10195
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
10196
+ worktree: { type: "string", description: WORKTREE_PARAM_DESCRIPTION2 },
10197
+ project_root: { type: "string", description: PROJECT_ROOT_PARAM_DESCRIPTION2 }
10198
+ },
10199
+ required: ["from", "to"]
10200
+ }
10201
+ },
10202
+ {
10203
+ name: "auth_coverage_report",
10204
+ description: 'Aggregate every API endpoint by its auth[] wrapper(s) \u2014 surfaces endpoints with empty auth, groups by module, shows which auth strategies dominate. Computed from api.json endpoint auth field (100% populated).\n\nUSE THIS FOR: "are there unauthenticated endpoints", "which auth wrappers are in use", "audit auth strategy consistency across modules". Paginated. Returns { total, by_strategy: {strategy: count}, unauthenticated: [endpoint_ids], by_module: {module: {total, by_strategy}} } plus the paginated endpoint list.',
10205
+ inputSchema: {
10206
+ type: "object",
10207
+ properties: {
10208
+ module: { type: "string", description: 'Restrict to one module tag (e.g. "orgs", "admin").' },
10209
+ strategy: { type: "string", description: 'Restrict to endpoints using this auth wrapper (e.g. "withAuth"). Pass "none" to list unauthenticated endpoints.' },
10210
+ offset: { type: "number", description: "Pagination offset over the endpoint list. Default 0." },
10211
+ limit: { type: "number", description: "Max endpoints to return per response. Default 10." },
10212
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
10213
+ worktree: { type: "string", description: WORKTREE_PARAM_DESCRIPTION2 },
10214
+ project_root: { type: "string", description: PROJECT_ROOT_PARAM_DESCRIPTION2 }
10215
+ }
10216
+ }
10217
+ },
9564
10218
  {
9565
10219
  name: "detect_project_stack",
9566
10220
  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.",