@launchsecure/launch-kit 0.0.15 → 0.0.17

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 (75) hide show
  1. package/dist/chart-client/assets/index-CWRZxjqR.css +1 -0
  2. package/dist/chart-client/index.html +2 -2
  3. package/dist/client/assets/index-DtbN793z.css +32 -0
  4. package/dist/client/index.html +2 -2
  5. package/dist/deck-client/assets/_baseUniq-BbqvoK-V.js +1 -0
  6. package/dist/deck-client/assets/arc-CMtYsIZt.js +1 -0
  7. package/dist/deck-client/assets/architectureDiagram-Q4EWVU46-BEN5hESa.js +36 -0
  8. package/dist/deck-client/assets/blockDiagram-DXYQGD6D-BV4CZ6k8.js +132 -0
  9. package/dist/deck-client/assets/c4Diagram-AHTNJAMY-fLcBXqdD.js +10 -0
  10. package/dist/deck-client/assets/channel-Nf-B3Qor.js +1 -0
  11. package/dist/deck-client/assets/chunk-4BX2VUAB-BO_19zwB.js +1 -0
  12. package/dist/deck-client/assets/chunk-4TB4RGXK-iYegd5fu.js +206 -0
  13. package/dist/deck-client/assets/chunk-55IACEB6-DM3QwYFL.js +1 -0
  14. package/dist/deck-client/assets/chunk-EDXVE4YY-DGznOul1.js +1 -0
  15. package/dist/deck-client/assets/chunk-FMBD7UC4-DsANJqYW.js +15 -0
  16. package/dist/deck-client/assets/chunk-OYMX7WX6-6PGH1F7d.js +231 -0
  17. package/dist/deck-client/assets/chunk-QZHKN3VN-Dihf0Uq7.js +1 -0
  18. package/dist/deck-client/assets/chunk-YZCP3GAM-Cali2wW5.js +1 -0
  19. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-3i3-miMR.js +1 -0
  20. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-3i3-miMR.js +1 -0
  21. package/dist/deck-client/assets/clone-DXBuQlG8.js +1 -0
  22. package/dist/deck-client/assets/cose-bilkent-S5V4N54A-DsRY4vbI.js +1 -0
  23. package/dist/deck-client/assets/cytoscape.esm-BiciSPf8.js +331 -0
  24. package/dist/deck-client/assets/dagre-KV5264BT-DJIKE_pI.js +4 -0
  25. package/dist/deck-client/assets/defaultLocale-DX6XiGOO.js +1 -0
  26. package/dist/deck-client/assets/diagram-5BDNPKRD-Ckgli1SP.js +10 -0
  27. package/dist/deck-client/assets/diagram-G4DWMVQ6-CozcDzae.js +24 -0
  28. package/dist/deck-client/assets/diagram-MMDJMWI5-xVSwW3f_.js +43 -0
  29. package/dist/deck-client/assets/diagram-TYMM5635-CJeZVY-P.js +24 -0
  30. package/dist/deck-client/assets/erDiagram-SMLLAGMA-j4wjAERH.js +85 -0
  31. package/dist/deck-client/assets/flowDiagram-DWJPFMVM-CVLZ1efi.js +162 -0
  32. package/dist/deck-client/assets/ganttDiagram-T4ZO3ILL-CcIJ7pkP.js +292 -0
  33. package/dist/deck-client/assets/gitGraphDiagram-UUTBAWPF-BZRhQX-a.js +106 -0
  34. package/dist/deck-client/assets/graph-D0l25xfo.js +1 -0
  35. package/dist/deck-client/assets/index-BXcoHWVM.js +476 -0
  36. package/dist/deck-client/assets/index-Cdh-f3-c.css +1 -0
  37. package/dist/deck-client/assets/infoDiagram-42DDH7IO-BLwgxnYT.js +2 -0
  38. package/dist/deck-client/assets/init-Gi6I4Gst.js +1 -0
  39. package/dist/deck-client/assets/ishikawaDiagram-UXIWVN3A-BfOLoWv3.js +70 -0
  40. package/dist/deck-client/assets/journeyDiagram-VCZTEJTY-CPuL6C9h.js +139 -0
  41. package/dist/deck-client/assets/kanban-definition-6JOO6SKY-D3uf7_tx.js +89 -0
  42. package/dist/deck-client/assets/katex-DkKDou_j.js +257 -0
  43. package/dist/deck-client/assets/layout-CzToiXdK.js +1 -0
  44. package/dist/deck-client/assets/linear-BU36t460.js +1 -0
  45. package/dist/deck-client/assets/min-DX_q-lqP.js +1 -0
  46. package/dist/deck-client/assets/mindmap-definition-QFDTVHPH-Ccty4O16.js +96 -0
  47. package/dist/deck-client/assets/ordinal-Cboi1Yqb.js +1 -0
  48. package/dist/deck-client/assets/pieDiagram-DEJITSTG-DVjsvH19.js +30 -0
  49. package/dist/deck-client/assets/quadrantDiagram-34T5L4WZ-DtOXFVW9.js +7 -0
  50. package/dist/deck-client/assets/requirementDiagram-MS252O5E-BbO_kKg6.js +84 -0
  51. package/dist/deck-client/assets/sankeyDiagram-XADWPNL6-qbbj-CmC.js +10 -0
  52. package/dist/deck-client/assets/sequenceDiagram-FGHM5R23-JNKZAgfQ.js +157 -0
  53. package/dist/deck-client/assets/stateDiagram-FHFEXIEX-dtFalcNx.js +1 -0
  54. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CviYYulW.js +1 -0
  55. package/dist/deck-client/assets/timeline-definition-GMOUNBTQ-Dpp5nqSJ.js +120 -0
  56. package/dist/deck-client/assets/vennDiagram-DHZGUBPP-D8qEutX7.js +34 -0
  57. package/dist/deck-client/assets/wardley-RL74JXVD-BwMqiNcL.js +162 -0
  58. package/dist/deck-client/assets/wardleyDiagram-NUSXRM2D-Bxl9X3CK.js +20 -0
  59. package/dist/deck-client/assets/xychartDiagram-5P7HB3ND-DYcvxLhi.js +7 -0
  60. package/dist/deck-client/index.html +21 -0
  61. package/dist/server/cli.js +616 -142
  62. package/dist/server/deck-mcp-entry.js +1789 -0
  63. package/dist/server/deck-serve.js +1275 -0
  64. package/dist/server/deck-server/deck-mcp-entry.js +1789 -0
  65. package/dist/server/deck-server/deck-serve.js +1275 -0
  66. package/dist/server/graph-mcp-entry.js +464 -38
  67. package/dist/server/server/chart-serve.js +4643 -0
  68. package/dist/server/server/cli.js +13360 -0
  69. package/dist/server/server/fb-wizard.js +136 -0
  70. package/dist/server/server/graph-mcp-entry.js +6776 -0
  71. package/package.json +13 -6
  72. package/dist/chart-client/assets/index-CbZ13AXL.css +0 -1
  73. package/dist/client/assets/index-BCYw64M7.css +0 -32
  74. /package/dist/chart-client/assets/{index-BpQPtTuo.js → index-BUhuLBaw.js} +0 -0
  75. /package/dist/client/assets/{index-3ENenBk-.js → index-CAAipH3V.js} +0 -0
@@ -4774,13 +4774,221 @@ var init_chart_serve = __esm({
4774
4774
  }
4775
4775
  });
4776
4776
 
4777
+ // src/server/blast-radius-builder.ts
4778
+ function loadDefaults(rootDir) {
4779
+ const filePath = (0, import_node_path20.join)(rootDir, ".launchsecure", "blast-radius-defaults.json");
4780
+ try {
4781
+ if (import_node_fs18.default.existsSync(filePath)) {
4782
+ const raw = import_node_fs18.default.readFileSync(filePath, "utf-8");
4783
+ return JSON.parse(raw);
4784
+ }
4785
+ } catch {
4786
+ }
4787
+ return FALLBACK_DEFAULTS;
4788
+ }
4789
+ function generateAcceptance(node, inspect) {
4790
+ const criteria = [];
4791
+ const t = node.type?.toLowerCase() ?? "";
4792
+ if (t === "endpoint" || t === "mcp-tool") {
4793
+ const methods = inspect?.methods ?? [];
4794
+ const path3 = inspect?.path ?? node.id;
4795
+ if (methods.length > 0) {
4796
+ criteria.push(`${methods.join("/")} ${path3} still returns correct responses for authorized users`);
4797
+ } else {
4798
+ criteria.push(`${path3} still responds correctly`);
4799
+ }
4800
+ if (inspect?.auth && inspect.auth.includes("withAuth")) {
4801
+ criteria.push("Authentication and authorization still enforced");
4802
+ }
4803
+ if (inspect?.db_models && inspect.db_models.length > 0) {
4804
+ criteria.push(`DB operations on ${inspect.db_models.join(", ")} still work correctly`);
4805
+ }
4806
+ } else if (t === "page" || t === "component" || t === "layout") {
4807
+ criteria.push(`${node.name} renders without errors`);
4808
+ if (inspect?.stateVars && inspect.stateVars.length > 0) {
4809
+ criteria.push("State management still works correctly");
4810
+ }
4811
+ if (inspect?.elements && inspect.elements.length > 5) {
4812
+ criteria.push("All child components render correctly");
4813
+ }
4814
+ } else if (t === "table" || t === "enum") {
4815
+ criteria.push(`${node.name} schema unchanged or migration applies cleanly`);
4816
+ criteria.push("Existing queries against this table still work");
4817
+ } else if (t === "hook") {
4818
+ criteria.push(`${node.name} returns expected shape`);
4819
+ if (inspect?.stateVars && inspect.stateVars.length > 0) {
4820
+ criteria.push(`State variables [${inspect.stateVars.map((s) => s.name).join(", ")}] still returned`);
4821
+ }
4822
+ } else if (t === "context") {
4823
+ criteria.push(`${node.name} provides correct context to consumers`);
4824
+ } else if (t === "lib" || t === "config" || t === "types") {
4825
+ criteria.push(`${node.name} exports still conform to expected interface`);
4826
+ } else if (t === "seed" || t === "seed_role" || t === "seed_permission") {
4827
+ criteria.push("Seed runs without errors");
4828
+ criteria.push("Expected rows created in database");
4829
+ } else {
4830
+ criteria.push("Verify no regression");
4831
+ }
4832
+ return criteria;
4833
+ }
4834
+ function buildManifest(input) {
4835
+ const { mode, title, description, subtitle, blastResults, createNodes, inspectData, defaults } = input;
4836
+ const nodeMap = /* @__PURE__ */ new Map();
4837
+ const centerNodeIds = /* @__PURE__ */ new Set();
4838
+ for (const result of blastResults) {
4839
+ centerNodeIds.add(result.center.id);
4840
+ for (const node of result.affected) {
4841
+ const existing = nodeMap.get(node.id);
4842
+ if (!existing || node.hop < existing.hop) {
4843
+ nodeMap.set(node.id, node);
4844
+ }
4845
+ }
4846
+ }
4847
+ for (const id of centerNodeIds) {
4848
+ nodeMap.delete(id);
4849
+ }
4850
+ const manifestNodes = [];
4851
+ for (const result of blastResults) {
4852
+ const c = result.center;
4853
+ if (manifestNodes.some((n) => n.id === c.id)) continue;
4854
+ const inspect = inspectData[c.id];
4855
+ manifestNodes.push({
4856
+ id: c.id,
4857
+ name: c.name,
4858
+ layer: c.layer,
4859
+ ring: "modify",
4860
+ type: c.type,
4861
+ reason: `Direct change target`,
4862
+ acceptance: generateAcceptance(
4863
+ { id: c.id, name: c.name, type: c.type, layer: c.layer, hop: 0 },
4864
+ inspect
4865
+ )
4866
+ });
4867
+ }
4868
+ for (const [, node] of nodeMap) {
4869
+ const ring = node.hop <= 1 ? "modify" : "ripple";
4870
+ const inspect = inspectData[node.id];
4871
+ const reason = node.hop <= 1 ? `Directly depends on changed node` : `Indirect dependency (${node.hop} hops away)`;
4872
+ manifestNodes.push({
4873
+ id: node.id,
4874
+ name: node.name,
4875
+ layer: node.layer,
4876
+ ring,
4877
+ type: node.type,
4878
+ reason,
4879
+ acceptance: generateAcceptance(node, inspect)
4880
+ });
4881
+ }
4882
+ for (const cn of createNodes) {
4883
+ manifestNodes.push({
4884
+ id: cn.id,
4885
+ name: cn.name,
4886
+ layer: cn.layer,
4887
+ ring: "create",
4888
+ type: cn.type ?? "unknown",
4889
+ reason: cn.reason,
4890
+ acceptance: cn.acceptance ?? ["Verify implementation matches spec"]
4891
+ });
4892
+ }
4893
+ const layerIds = /* @__PURE__ */ new Set();
4894
+ for (const n of manifestNodes) {
4895
+ layerIds.add(n.layer);
4896
+ }
4897
+ const layers = [];
4898
+ for (const id of layerIds) {
4899
+ const def = defaults.layers[id];
4900
+ if (def) {
4901
+ layers.push({ id, name: def.name, icon: def.icon, color: def.color });
4902
+ } else {
4903
+ layers.push({ id, name: id, icon: "box", color: "#cbd5e1" });
4904
+ }
4905
+ }
4906
+ const edgeSet = /* @__PURE__ */ new Set();
4907
+ const edges = [];
4908
+ const allNodeIds = new Set(manifestNodes.map((n) => n.id));
4909
+ for (const cId of centerNodeIds) {
4910
+ for (const result of blastResults) {
4911
+ for (const affected of result.affected) {
4912
+ if (affected.hop === 1 && result.center.id === cId && allNodeIds.has(affected.id)) {
4913
+ const key = `${cId}->${affected.id}`;
4914
+ if (!edgeSet.has(key)) {
4915
+ edgeSet.add(key);
4916
+ edges.push({ source: cId, target: affected.id });
4917
+ }
4918
+ }
4919
+ }
4920
+ }
4921
+ }
4922
+ for (const result of blastResults) {
4923
+ if (result.edges) {
4924
+ for (const edge of result.edges) {
4925
+ if (allNodeIds.has(edge.source) && allNodeIds.has(edge.target)) {
4926
+ const key = `${edge.source}->${edge.target}`;
4927
+ if (!edgeSet.has(key)) {
4928
+ edgeSet.add(key);
4929
+ edges.push({ source: edge.source, target: edge.target });
4930
+ }
4931
+ }
4932
+ }
4933
+ }
4934
+ }
4935
+ for (const cn of createNodes) {
4936
+ edges.push({ source: "center", target: cn.id });
4937
+ if (cn.connects_to) {
4938
+ for (const targetId of cn.connects_to) {
4939
+ if (allNodeIds.has(targetId) || createNodes.some((c) => c.id === targetId)) {
4940
+ const key = `${cn.id}->${targetId}`;
4941
+ if (!edgeSet.has(key)) {
4942
+ edgeSet.add(key);
4943
+ edges.push({ source: cn.id, target: targetId });
4944
+ }
4945
+ }
4946
+ }
4947
+ }
4948
+ }
4949
+ return {
4950
+ mode,
4951
+ title,
4952
+ subtitle,
4953
+ layers,
4954
+ rings: defaults.rings,
4955
+ center: { name: title, description },
4956
+ nodes: manifestNodes,
4957
+ edges
4958
+ };
4959
+ }
4960
+ var import_node_fs18, import_node_path20, FALLBACK_DEFAULTS;
4961
+ var init_blast_radius_builder = __esm({
4962
+ "src/server/blast-radius-builder.ts"() {
4963
+ "use strict";
4964
+ import_node_fs18 = __toESM(require("node:fs"));
4965
+ import_node_path20 = require("node:path");
4966
+ FALLBACK_DEFAULTS = {
4967
+ rings: [
4968
+ { id: "modify", name: "Modify", color: "#ff6b00" },
4969
+ { id: "ripple", name: "Ripple (verify)", color: "#ffff00" },
4970
+ { id: "create", name: "Create", color: "#00ff00" }
4971
+ ],
4972
+ layers: {
4973
+ db: { name: "Database", icon: "database", color: "#cbd5e1" },
4974
+ api: { name: "API", icon: "server", color: "#cbd5e1" },
4975
+ middleware: { name: "Middleware", icon: "shield", color: "#cbd5e1" },
4976
+ ui: { name: "UI", icon: "layout-dashboard", color: "#cbd5e1" },
4977
+ config: { name: "Config / Seed", icon: "settings", color: "#cbd5e1" },
4978
+ shared: { name: "Shared Types", icon: "box", color: "#cbd5e1" }
4979
+ },
4980
+ center: { color: "#ff0000" }
4981
+ };
4982
+ }
4983
+ });
4984
+
4777
4985
  // src/server/graph/core/language-detection.ts
4778
4986
  function walkForExtensions(dir, extCounts, depth = 0) {
4779
4987
  if (depth > 10) return;
4780
- if (!(0, import_node_fs18.existsSync)(dir)) return;
4988
+ if (!(0, import_node_fs19.existsSync)(dir)) return;
4781
4989
  let entries;
4782
4990
  try {
4783
- entries = (0, import_node_fs18.readdirSync)(dir, { withFileTypes: true });
4991
+ entries = (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true });
4784
4992
  } catch {
4785
4993
  return;
4786
4994
  }
@@ -4788,9 +4996,9 @@ function walkForExtensions(dir, extCounts, depth = 0) {
4788
4996
  if (entry.name.startsWith(".") && entry.isDirectory()) continue;
4789
4997
  if (entry.isDirectory()) {
4790
4998
  if (IGNORE_DIRS.has(entry.name)) continue;
4791
- walkForExtensions((0, import_node_path20.join)(dir, entry.name), extCounts, depth + 1);
4999
+ walkForExtensions((0, import_node_path21.join)(dir, entry.name), extCounts, depth + 1);
4792
5000
  } else {
4793
- const ext = (0, import_node_path20.extname)(entry.name).toLowerCase();
5001
+ const ext = (0, import_node_path21.extname)(entry.name).toLowerCase();
4794
5002
  if (ext && EXTENSION_TO_LANGUAGE[ext]) {
4795
5003
  extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
4796
5004
  }
@@ -4829,12 +5037,12 @@ function detectLanguages(rootDir, supportedLanguages) {
4829
5037
  });
4830
5038
  return results;
4831
5039
  }
4832
- var import_node_fs18, import_node_path20, EXTENSION_TO_LANGUAGE, IGNORE_DIRS, AUXILIARY_LANGUAGES;
5040
+ var import_node_fs19, import_node_path21, EXTENSION_TO_LANGUAGE, IGNORE_DIRS, AUXILIARY_LANGUAGES;
4833
5041
  var init_language_detection = __esm({
4834
5042
  "src/server/graph/core/language-detection.ts"() {
4835
5043
  "use strict";
4836
- import_node_fs18 = require("node:fs");
4837
- import_node_path20 = require("node:path");
5044
+ import_node_fs19 = require("node:fs");
5045
+ import_node_path21 = require("node:path");
4838
5046
  EXTENSION_TO_LANGUAGE = {
4839
5047
  // Web / Frontend
4840
5048
  ".ts": "typescript",
@@ -5167,6 +5375,144 @@ function handleBlastPoints(args) {
5167
5375
  }
5168
5376
  });
5169
5377
  }
5378
+ function handleGenerateBlastRadius(args) {
5379
+ const rootDir = process.cwd();
5380
+ const mode = args.mode ?? "structural";
5381
+ const title = args.title;
5382
+ const description = args.description ?? title;
5383
+ const subtitle = args.subtitle;
5384
+ const hops = args.hops ?? 2;
5385
+ const defaults = loadDefaults(rootDir);
5386
+ let centerNodeIds = [];
5387
+ if (mode === "structural") {
5388
+ const nodeId = args.node_id;
5389
+ if (!nodeId) return err("structural mode requires node_id");
5390
+ centerNodeIds = [nodeId];
5391
+ } else {
5392
+ centerNodeIds = args.center_nodes ?? [];
5393
+ if (centerNodeIds.length === 0) return err("feature mode requires center_nodes[]");
5394
+ }
5395
+ const createNodes = args.create_nodes ?? [];
5396
+ const blastResults = [];
5397
+ for (const nodeId of centerNodeIds) {
5398
+ let targetLayer;
5399
+ const graphs = readAllGraphs(rootDir);
5400
+ for (const [layer, graph2] of Object.entries(graphs)) {
5401
+ if (graph2 && graph2.nodes.some((n) => n.id === nodeId)) {
5402
+ targetLayer = layer;
5403
+ break;
5404
+ }
5405
+ }
5406
+ if (!targetLayer) continue;
5407
+ const graph = readGraph(rootDir, targetLayer);
5408
+ if (!graph) continue;
5409
+ const center = graph.nodes.find((n) => n.id === nodeId);
5410
+ if (!center) continue;
5411
+ const result2 = reverseNeighborhood(graph, nodeId, hops, "reverse");
5412
+ const affected = [];
5413
+ for (const [id, { node, hop }] of result2.nodes) {
5414
+ if (id === nodeId) continue;
5415
+ const tags = node.tags;
5416
+ affected.push({ id: node.id, name: node.name, type: node.type, layer: targetLayer, hop, module: tags?.module });
5417
+ }
5418
+ const otherLayers = getAvailableLayers(rootDir).filter((l) => l !== targetLayer && l !== "static");
5419
+ for (const otherLayer of otherLayers) {
5420
+ const otherGraph = readGraph(rootDir, otherLayer);
5421
+ if (!otherGraph) continue;
5422
+ for (const edge of otherGraph.edges) {
5423
+ if (edge.target === nodeId || edge.source === nodeId) {
5424
+ const dependentId = edge.target === nodeId ? edge.source : edge.target;
5425
+ if (affected.some((a) => a.id === dependentId)) continue;
5426
+ const depNode = otherGraph.nodes.find((n) => n.id === dependentId);
5427
+ if (depNode) {
5428
+ const tags = depNode.tags;
5429
+ affected.push({ id: depNode.id, name: depNode.name, type: depNode.type, layer: otherLayer, hop: 1, module: tags?.module });
5430
+ }
5431
+ }
5432
+ }
5433
+ }
5434
+ const centerTags = center.tags;
5435
+ const edges = result2.edges.map((e) => ({ source: e.source, target: e.target }));
5436
+ blastResults.push({
5437
+ center: { id: center.id, name: center.name, type: center.type, layer: targetLayer, module: centerTags?.module },
5438
+ affected,
5439
+ edges
5440
+ });
5441
+ }
5442
+ if (blastResults.length === 0) {
5443
+ return err(`None of the center nodes were found in any graph layer: ${centerNodeIds.join(", ")}`);
5444
+ }
5445
+ const inspectData = {};
5446
+ const allAffectedIds = /* @__PURE__ */ new Set();
5447
+ for (const r of blastResults) {
5448
+ allAffectedIds.add(r.center.id);
5449
+ for (const a of r.affected) allAffectedIds.add(a.id);
5450
+ }
5451
+ const allGraphs = readAllGraphs(rootDir);
5452
+ for (const id of allAffectedIds) {
5453
+ for (const [, graph] of Object.entries(allGraphs)) {
5454
+ if (!graph) continue;
5455
+ const node = graph.nodes.find((n) => n.id === id);
5456
+ if (node) {
5457
+ inspectData[id] = {
5458
+ type: node.type,
5459
+ name: node.name,
5460
+ methods: node.methods,
5461
+ path: node.path ?? node.handler,
5462
+ auth: node.auth,
5463
+ db_models: node.db_models
5464
+ };
5465
+ break;
5466
+ }
5467
+ }
5468
+ }
5469
+ const manifest = buildManifest({
5470
+ mode,
5471
+ title,
5472
+ description,
5473
+ subtitle,
5474
+ blastResults,
5475
+ createNodes,
5476
+ inspectData,
5477
+ defaults
5478
+ });
5479
+ const pushToDeck = args.push_to_deck;
5480
+ const session = args.session;
5481
+ let deckResult;
5482
+ if (pushToDeck) {
5483
+ if (!session) return err("push_to_deck requires a session name");
5484
+ const deckLockPath = (0, import_node_path22.join)(rootDir, ".launchsecure", "launch-deck.lock");
5485
+ if (!(0, import_node_fs20.existsSync)(deckLockPath)) {
5486
+ deckResult = { pushed: false, reason: "Deck server not running (no lock file). Push manually via deck tool." };
5487
+ } else {
5488
+ try {
5489
+ const lock = JSON.parse((0, import_node_fs20.readFileSync)(deckLockPath, "utf-8"));
5490
+ const deckUrl = lock.url;
5491
+ const body = JSON.stringify({
5492
+ session,
5493
+ mode: "show",
5494
+ blocks: [{ type: "blast-radius", label: title, manifest }]
5495
+ });
5496
+ (0, import_node_child_process2.execFileSync)("curl", [
5497
+ "-s",
5498
+ "-X",
5499
+ "POST",
5500
+ deckUrl + "/api/deck",
5501
+ "-H",
5502
+ "Content-Type: application/json",
5503
+ "-d",
5504
+ body
5505
+ ], { timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] });
5506
+ deckResult = { pushed: true, session, url: deckUrl };
5507
+ } catch (e) {
5508
+ deckResult = { pushed: false, reason: `Failed to push to deck: ${e}` };
5509
+ }
5510
+ }
5511
+ }
5512
+ const result = { ...manifest };
5513
+ if (deckResult) result._deck = deckResult;
5514
+ return okJson(result);
5515
+ }
5170
5516
  function layerSummary(graph) {
5171
5517
  const typeCounts = {};
5172
5518
  const moduleCounts = {};
@@ -5395,12 +5741,12 @@ function handleReadGraph(args) {
5395
5741
  return okJson(result);
5396
5742
  }
5397
5743
  function nodeToFilePath(rootDir, layer, nodeId) {
5398
- if (layer === "ui" || layer === "api") return (0, import_node_path21.join)(rootDir, "src", nodeId);
5399
- if (layer === "db") return (0, import_node_path21.join)(rootDir, "prisma", "schema.prisma");
5400
- const withSrc = (0, import_node_path21.join)(rootDir, "src", nodeId);
5401
- if ((0, import_node_fs19.existsSync)(withSrc)) return withSrc;
5402
- const direct = (0, import_node_path21.join)(rootDir, nodeId);
5403
- if ((0, import_node_fs19.existsSync)(direct)) return direct;
5744
+ if (layer === "ui" || layer === "api") return (0, import_node_path22.join)(rootDir, "src", nodeId);
5745
+ if (layer === "db") return (0, import_node_path22.join)(rootDir, "prisma", "schema.prisma");
5746
+ const withSrc = (0, import_node_path22.join)(rootDir, "src", nodeId);
5747
+ if ((0, import_node_fs20.existsSync)(withSrc)) return withSrc;
5748
+ const direct = (0, import_node_path22.join)(rootDir, nodeId);
5749
+ if ((0, import_node_fs20.existsSync)(direct)) return direct;
5404
5750
  return null;
5405
5751
  }
5406
5752
  function handleInspectNode(args) {
@@ -5543,11 +5889,11 @@ function handleGrepNodes(args) {
5543
5889
  let filesSearched = 0;
5544
5890
  let truncated = false;
5545
5891
  for (const [filePath, nodeId] of filePaths) {
5546
- if (!(0, import_node_fs19.existsSync)(filePath)) continue;
5892
+ if (!(0, import_node_fs20.existsSync)(filePath)) continue;
5547
5893
  filesSearched++;
5548
5894
  let content;
5549
5895
  try {
5550
- content = (0, import_node_fs19.readFileSync)(filePath, "utf-8");
5896
+ content = (0, import_node_fs20.readFileSync)(filePath, "utf-8");
5551
5897
  } catch {
5552
5898
  continue;
5553
5899
  }
@@ -5612,11 +5958,11 @@ function handleStartChartServer(args) {
5612
5958
  });
5613
5959
  }
5614
5960
  const entryPath = process.argv[1];
5615
- const logDir = (0, import_node_path21.join)((0, import_node_os2.homedir)(), ".launchsecure");
5616
- (0, import_node_fs19.mkdirSync)(logDir, { recursive: true });
5617
- const logPath = (0, import_node_path21.join)(logDir, "launch-chart.log");
5618
- const out = (0, import_node_fs19.openSync)(logPath, "a");
5619
- const err2 = (0, import_node_fs19.openSync)(logPath, "a");
5961
+ const logDir = (0, import_node_path22.join)((0, import_node_os2.homedir)(), ".launchsecure");
5962
+ (0, import_node_fs20.mkdirSync)(logDir, { recursive: true });
5963
+ const logPath = (0, import_node_path22.join)(logDir, "launch-chart.log");
5964
+ const out = (0, import_node_fs20.openSync)(logPath, "a");
5965
+ const err2 = (0, import_node_fs20.openSync)(logPath, "a");
5620
5966
  const portArgs = args.port ? ["--port", String(args.port)] : [];
5621
5967
  const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
5622
5968
  detached: true,
@@ -5736,20 +6082,20 @@ function handleDetectProjectStack() {
5736
6082
  if (ref.type === "references_api") stats.references_api++;
5737
6083
  }
5738
6084
  }
5739
- const srcDir = (0, import_node_path21.join)(rootDir, "src");
5740
- if ((0, import_node_fs19.existsSync)(srcDir)) {
6085
+ const srcDir = (0, import_node_path22.join)(rootDir, "src");
6086
+ if ((0, import_node_fs20.existsSync)(srcDir)) {
5741
6087
  const scanDir = (dir) => {
5742
- if (!(0, import_node_fs19.existsSync)(dir)) return;
5743
- for (const entry of (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true })) {
6088
+ if (!(0, import_node_fs20.existsSync)(dir)) return;
6089
+ for (const entry of (0, import_node_fs20.readdirSync)(dir, { withFileTypes: true })) {
5744
6090
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
5745
- const full = (0, import_node_path21.join)(dir, entry.name);
6091
+ const full = (0, import_node_path22.join)(dir, entry.name);
5746
6092
  if (entry.isDirectory()) {
5747
6093
  scanDir(full);
5748
6094
  continue;
5749
6095
  }
5750
- if (![".ts", ".tsx"].includes((0, import_node_path21.extname)(entry.name))) continue;
6096
+ if (![".ts", ".tsx"].includes((0, import_node_path22.extname)(entry.name))) continue;
5751
6097
  try {
5752
- const content = (0, import_node_fs19.readFileSync)(full, "utf-8");
6098
+ const content = (0, import_node_fs20.readFileSync)(full, "utf-8");
5753
6099
  const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
5754
6100
  if (matches) stats.annotations += matches.length;
5755
6101
  } catch {
@@ -5868,6 +6214,10 @@ async function handleMessage(msg) {
5868
6214
  respond(id ?? null, handleBlastPoints(args));
5869
6215
  return;
5870
6216
  }
6217
+ if (toolName === "generate_blast_radius") {
6218
+ respond(id ?? null, handleGenerateBlastRadius(args));
6219
+ return;
6220
+ }
5871
6221
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
5872
6222
  return;
5873
6223
  }
@@ -5903,15 +6253,16 @@ function startGraphMcpServer() {
5903
6253
  process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
5904
6254
  `);
5905
6255
  }
5906
- var import_node_fs19, 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;
6256
+ var import_node_fs20, import_node_path22, 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;
5907
6257
  var init_graph_mcp = __esm({
5908
6258
  "src/server/graph-mcp.ts"() {
5909
6259
  "use strict";
5910
- import_node_fs19 = require("node:fs");
5911
- import_node_path21 = require("node:path");
6260
+ import_node_fs20 = require("node:fs");
6261
+ import_node_path22 = require("node:path");
5912
6262
  import_node_child_process2 = require("node:child_process");
5913
6263
  import_node_os2 = require("node:os");
5914
6264
  init_graph();
6265
+ init_blast_radius_builder();
5915
6266
  init_lockfile();
5916
6267
  init_config();
5917
6268
  init_parser_registry();
@@ -6228,6 +6579,81 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
6228
6579
  },
6229
6580
  required: ["node_id"]
6230
6581
  }
6582
+ },
6583
+ {
6584
+ name: "generate_blast_radius",
6585
+ description: `Generate a complete BlastRadiusManifest from graph data \u2014 ready to push to deck.
6586
+
6587
+ Two modes:
6588
+ - **Structural**: single node changed \u2192 auto-discover what's affected via reverse BFS
6589
+ Example: generate_blast_radius({ mode: "structural", node_id: "CommentChannel", title: "CommentChannel refactor" })
6590
+ - **Feature**: new feature \u2192 multiple starting nodes + new nodes to create
6591
+ Example: generate_blast_radius({ mode: "feature", title: "Client Role", description: "...", center_nodes: ["CommentChannel", "ProjectMember"], create_nodes: [{ id: "ChannelMember", name: "ChannelMember table", layer: "db", reason: "..." }] })
6592
+
6593
+ Output is a BlastRadiusManifest JSON that passes directly to the deck tool's blast-radius block.
6594
+ Reads ring/layer/center colors from .launchsecure/blast-radius-defaults.json.
6595
+ Auto-generates acceptance criteria per node using inspect_node AST data.`,
6596
+ inputSchema: {
6597
+ type: "object",
6598
+ properties: {
6599
+ mode: {
6600
+ type: "string",
6601
+ enum: ["structural", "feature"],
6602
+ description: '"structural" = single node changed. "feature" = new feature with multiple nodes.'
6603
+ },
6604
+ title: {
6605
+ type: "string",
6606
+ description: "Title for the blast radius (shown in center node and header)."
6607
+ },
6608
+ description: {
6609
+ type: "string",
6610
+ description: "Description of the change or feature."
6611
+ },
6612
+ subtitle: {
6613
+ type: "string",
6614
+ description: "Optional subtitle shown above title in the viz."
6615
+ },
6616
+ node_id: {
6617
+ type: "string",
6618
+ description: "Structural mode only: the node being changed."
6619
+ },
6620
+ center_nodes: {
6621
+ type: "array",
6622
+ items: { type: "string" },
6623
+ description: "Feature mode: existing graph node IDs that are the starting points for traversal."
6624
+ },
6625
+ create_nodes: {
6626
+ type: "array",
6627
+ items: {
6628
+ type: "object",
6629
+ properties: {
6630
+ id: { type: "string" },
6631
+ name: { type: "string" },
6632
+ layer: { type: "string" },
6633
+ type: { type: "string" },
6634
+ reason: { type: "string" },
6635
+ acceptance: { type: "array", items: { type: "string" } },
6636
+ connects_to: { type: "array", items: { type: "string" }, description: "IDs of existing nodes this new node has FK/relationship edges to." }
6637
+ },
6638
+ required: ["id", "name", "layer", "reason"]
6639
+ },
6640
+ description: "Feature mode: new nodes that need to be created (not in graph yet)."
6641
+ },
6642
+ hops: {
6643
+ type: "number",
6644
+ description: "Max hops for traversal. Default 2. Hop 1 = modify ring, hop 2+ = ripple ring."
6645
+ },
6646
+ push_to_deck: {
6647
+ type: "boolean",
6648
+ description: "If true, pushes the manifest directly to LaunchDeck browser (requires deck server running). Default false."
6649
+ },
6650
+ session: {
6651
+ type: "string",
6652
+ description: "Session name for the deck tab. Required when push_to_deck is true."
6653
+ }
6654
+ },
6655
+ required: ["title"]
6656
+ }
6231
6657
  }
6232
6658
  ];
6233
6659
  COMPACT_SCHEMA = {
@@ -6294,10 +6720,10 @@ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 retu
6294
6720
 
6295
6721
  // src/server/graph-mcp-entry.ts
6296
6722
  var import_node_child_process3 = require("node:child_process");
6297
- var import_node_fs20 = require("node:fs");
6298
- var import_node_path22 = __toESM(require("node:path"));
6299
- var import_node_os3 = require("node:os");
6300
6723
  var import_node_fs21 = require("node:fs");
6724
+ var import_node_path23 = __toESM(require("node:path"));
6725
+ var import_node_os3 = require("node:os");
6726
+ var import_node_fs22 = require("node:fs");
6301
6727
  init_lockfile();
6302
6728
  function logStderr(msg) {
6303
6729
  process.stderr.write(`[launch-chart] ${msg}
@@ -6313,11 +6739,11 @@ function maybeAutoServe() {
6313
6739
  return;
6314
6740
  }
6315
6741
  try {
6316
- const logDir = import_node_path22.default.join((0, import_node_os3.homedir)(), ".launchsecure");
6317
- (0, import_node_fs21.mkdirSync)(logDir, { recursive: true });
6318
- const logPath = import_node_path22.default.join(logDir, "launch-chart.log");
6319
- const out = (0, import_node_fs20.openSync)(logPath, "a");
6320
- const err2 = (0, import_node_fs20.openSync)(logPath, "a");
6742
+ const logDir = import_node_path23.default.join((0, import_node_os3.homedir)(), ".launchsecure");
6743
+ (0, import_node_fs22.mkdirSync)(logDir, { recursive: true });
6744
+ const logPath = import_node_path23.default.join(logDir, "launch-chart.log");
6745
+ const out = (0, import_node_fs21.openSync)(logPath, "a");
6746
+ const err2 = (0, import_node_fs21.openSync)(logPath, "a");
6321
6747
  const entryPath = process.argv[1];
6322
6748
  const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
6323
6749
  detached: true,