@launchsecure/launch-kit 0.0.14 → 0.0.16

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.
@@ -156,19 +156,26 @@ var init_config = __esm({
156
156
  });
157
157
 
158
158
  // src/server/graph/core/resolve-paths.ts
159
+ function detectDbDir(rootDir, config) {
160
+ if (config.paths?.dbDir) return (0, import_node_path3.join)(rootDir, config.paths.dbDir);
161
+ const prismaDir = (0, import_node_path3.join)(rootDir, "prisma");
162
+ if ((0, import_node_fs3.existsSync)(prismaDir)) return prismaDir;
163
+ return null;
164
+ }
159
165
  function resolveProjectPaths(rootDir, config) {
166
+ const dbDir = detectDbDir(rootDir, config);
160
167
  if (config.paths?.appDir) {
161
168
  const appDir = (0, import_node_path3.join)(rootDir, config.paths.appDir);
162
169
  const srcDir = config.paths.srcDir ? (0, import_node_path3.join)(rootDir, config.paths.srcDir) : (0, import_node_path3.dirname)(appDir);
163
- return { srcDir, appDir, apiDir: (0, import_node_path3.join)(appDir, "api") };
170
+ return { srcDir, appDir, apiDir: (0, import_node_path3.join)(appDir, "api"), dbDir };
164
171
  }
165
172
  const srcApp = (0, import_node_path3.join)(rootDir, "src", "app");
166
173
  if ((0, import_node_fs3.existsSync)(srcApp)) {
167
- return { srcDir: (0, import_node_path3.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path3.join)(srcApp, "api") };
174
+ return { srcDir: (0, import_node_path3.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path3.join)(srcApp, "api"), dbDir };
168
175
  }
169
176
  const rootApp = (0, import_node_path3.join)(rootDir, "app");
170
177
  if ((0, import_node_fs3.existsSync)(rootApp)) {
171
- return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path3.join)(rootApp, "api") };
178
+ return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path3.join)(rootApp, "api"), dbDir };
172
179
  }
173
180
  return null;
174
181
  }
@@ -2111,6 +2118,7 @@ var init_fetch_resolver = __esm({
2111
2118
  fetchResolverParser = {
2112
2119
  id: "fetch-resolver",
2113
2120
  layer: "crosslayer",
2121
+ concern: "api-binding",
2114
2122
  detect(_rootDir) {
2115
2123
  return true;
2116
2124
  },
@@ -2231,6 +2239,7 @@ var init_api_annotations = __esm({
2231
2239
  apiAnnotationsParser = {
2232
2240
  id: "api-annotations",
2233
2241
  layer: "crosslayer",
2242
+ concern: "api-binding",
2234
2243
  detect(rootDir) {
2235
2244
  return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
2236
2245
  },
@@ -2325,6 +2334,7 @@ var init_url_literal_scanner = __esm({
2325
2334
  urlLiteralScannerParser = {
2326
2335
  id: "url-literal-scanner",
2327
2336
  layer: "crosslayer",
2337
+ concern: "api-binding",
2328
2338
  detect(rootDir) {
2329
2339
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2330
2340
  return paths !== null;
@@ -2949,6 +2959,7 @@ var init_static_ref_scanner = __esm({
2949
2959
  staticRefScannerParser = {
2950
2960
  id: "static-ref-scanner",
2951
2961
  layer: "crosslayer",
2962
+ concern: "static-ref",
2952
2963
  detect(rootDir) {
2953
2964
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2954
2965
  return paths !== null;
@@ -3083,6 +3094,9 @@ function loadCustomParsers(registry, config, rootDir, disabled) {
3083
3094
  `
3084
3095
  );
3085
3096
  }
3097
+ if (parser.layer === "crosslayer" && entry.concern && !("concern" in parser && parser.concern)) {
3098
+ parser.concern = entry.concern;
3099
+ }
3086
3100
  registry.register(parser);
3087
3101
  } catch (err2) {
3088
3102
  process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
@@ -3247,44 +3261,21 @@ function dedupCrossRefs(refs) {
3247
3261
  }
3248
3262
  return result;
3249
3263
  }
3250
- function applyCrossLayerResults(uiOutput, results, primaryId) {
3251
- const allCrossRefs = [...uiOutput.cross_refs];
3252
- const allFlagged = [...uiOutput.flagged_edges];
3253
- const allWarnings = [...uiOutput.warnings];
3254
- const primaryResult = results.find((r) => r.parserId === primaryId);
3255
- const secondaryResults = results.filter((r) => r.parserId !== primaryId);
3256
- if (primaryResult) {
3257
- allCrossRefs.push(...primaryResult.output.cross_refs);
3258
- allFlagged.push(...primaryResult.output.flagged_edges);
3259
- allWarnings.push(...primaryResult.output.warnings);
3260
- }
3261
- const primarySet = new Set(
3262
- (primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
3263
- );
3264
- for (const sec of secondaryResults) {
3265
- for (const ref of sec.output.cross_refs) {
3266
- const key = `${ref.source}|${ref.target}|${ref.type}`;
3267
- if (primarySet.has(key)) {
3268
- allCrossRefs.push(ref);
3269
- } else {
3270
- allFlagged.push({
3271
- source: ref.source,
3272
- target: ref.target,
3273
- type: "out_of_pattern",
3274
- label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
3275
- confidence: "medium"
3276
- });
3277
- allCrossRefs.push(ref);
3278
- }
3279
- }
3280
- allFlagged.push(...sec.output.flagged_edges);
3281
- allWarnings.push(...sec.output.warnings);
3282
- }
3264
+ function applyCrossLayerResults(uiOutput, results) {
3283
3265
  return {
3284
3266
  ...uiOutput,
3285
- cross_refs: dedupCrossRefs(allCrossRefs),
3286
- flagged_edges: allFlagged,
3287
- warnings: allWarnings
3267
+ cross_refs: dedupCrossRefs([
3268
+ ...uiOutput.cross_refs,
3269
+ ...results.flatMap((r) => r.output.cross_refs)
3270
+ ]),
3271
+ flagged_edges: [
3272
+ ...uiOutput.flagged_edges,
3273
+ ...results.flatMap((r) => r.output.flagged_edges)
3274
+ ],
3275
+ warnings: [
3276
+ ...uiOutput.warnings,
3277
+ ...results.flatMap((r) => r.output.warnings)
3278
+ ]
3288
3279
  };
3289
3280
  }
3290
3281
  var init_merge = __esm({
@@ -3328,10 +3319,9 @@ function generateLayer(rootDir, layer) {
3328
3319
  if (existing) layerOutputs.set(otherLayer, existing);
3329
3320
  }
3330
3321
  const crossParsers = registry.getCrossLayerParsers();
3331
- const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
3332
3322
  const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
3333
3323
  if (crossResults.length > 0) {
3334
- merged = applyCrossLayerResults(merged, crossResults, primaryId);
3324
+ merged = applyCrossLayerResults(merged, crossResults);
3335
3325
  }
3336
3326
  }
3337
3327
  return {
@@ -3378,11 +3368,10 @@ function generateAll(rootDir) {
3378
3368
  });
3379
3369
  }
3380
3370
  const crossParsers = registry.getCrossLayerParsers();
3381
- const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
3382
3371
  const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
3383
3372
  if (crossResults.length > 0 && layerOutputs.has("ui")) {
3384
3373
  const uiOutput = layerOutputs.get("ui");
3385
- const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
3374
+ const merged = applyCrossLayerResults(uiOutput, crossResults);
3386
3375
  layerOutputs.set("ui", merged);
3387
3376
  const uiResult = results.find((r) => r.layer === "ui");
3388
3377
  if (uiResult) {
@@ -4496,17 +4485,23 @@ async function startChartServer(opts = {}) {
4496
4485
  if (req.method === "GET" && url2.pathname === "/api/parser-config") {
4497
4486
  const config2 = loadConfig(reqRoot);
4498
4487
  const registry = createRegistry(config2, reqRoot);
4488
+ const toLabel = (id) => id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
4499
4489
  const detection = [];
4500
4490
  for (const parser of registry.getAll()) {
4501
4491
  if ("layers" in parser && Array.isArray(parser.layers)) {
4502
4492
  const mp = parser;
4503
- detection.push({ id: mp.id, layers: mp.layers, detected: mp.detect(reqRoot) });
4493
+ detection.push({ id: mp.id, layers: mp.layers, label: toLabel(mp.id), detected: mp.detect(reqRoot) });
4504
4494
  } else if ("layer" in parser && parser.layer !== "crosslayer") {
4505
4495
  const sp = parser;
4506
- detection.push({ id: sp.id, layers: [sp.layer], detected: sp.detect(reqRoot) });
4496
+ detection.push({ id: sp.id, layers: [sp.layer], label: toLabel(sp.id), detected: sp.detect(reqRoot) });
4507
4497
  }
4508
4498
  }
4509
- const crosslayerParsers = registry.getCrossLayerParsers().map((p) => ({ id: p.id }));
4499
+ const crosslayerParsers = {};
4500
+ for (const p of registry.getCrossLayerParsers()) {
4501
+ const concern = p.concern ?? "api-binding";
4502
+ if (!crosslayerParsers[concern]) crosslayerParsers[concern] = [];
4503
+ crosslayerParsers[concern].push({ id: p.id, label: toLabel(p.id) });
4504
+ }
4510
4505
  res.writeHead(200, { "Content-Type": "application/json" });
4511
4506
  res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
4512
4507
  return;
@@ -4619,16 +4614,21 @@ async function startChartServer(opts = {}) {
4619
4614
  if (req.method === "GET" && url2.pathname === "/api/detected-paths") {
4620
4615
  const config2 = loadConfig(reqRoot);
4621
4616
  const paths = resolveProjectPaths(reqRoot, config2);
4622
- const isOverride = !!config2.paths?.appDir;
4617
+ const overrides = {
4618
+ appDir: !!config2.paths?.appDir,
4619
+ dbDir: !!config2.paths?.dbDir
4620
+ };
4623
4621
  res.writeHead(200, { "Content-Type": "application/json" });
4624
4622
  res.end(JSON.stringify({
4625
4623
  projectRoot: reqRoot,
4626
4624
  detected: paths ? {
4627
4625
  srcDir: import_node_path19.default.relative(reqRoot, paths.srcDir) || ".",
4628
4626
  appDir: import_node_path19.default.relative(reqRoot, paths.appDir),
4629
- apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir)
4627
+ apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir),
4628
+ dbDir: paths.dbDir ? import_node_path19.default.relative(reqRoot, paths.dbDir) : null
4630
4629
  } : null,
4631
- isOverride
4630
+ overrides,
4631
+ isOverride: overrides.appDir
4632
4632
  }));
4633
4633
  return;
4634
4634
  }
@@ -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",
@@ -5048,6 +5256,263 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
5048
5256
  const edges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
5049
5257
  return { nodes, edges, budgetExceeded, stoppedAtHop };
5050
5258
  }
5259
+ function reverseNeighborhood(graph, centerId, hops, direction) {
5260
+ const center = graph.nodes.find((n) => n.id === centerId);
5261
+ if (!center) return { nodes: /* @__PURE__ */ new Map(), edges: [] };
5262
+ const visited = /* @__PURE__ */ new Map();
5263
+ visited.set(centerId, { node: center, hop: 0 });
5264
+ let frontier = /* @__PURE__ */ new Set([centerId]);
5265
+ for (let h = 0; h < hops; h++) {
5266
+ const next = /* @__PURE__ */ new Set();
5267
+ for (const edge of graph.edges) {
5268
+ if (direction === "reverse") {
5269
+ if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
5270
+ } else {
5271
+ if (frontier.has(edge.source) && !visited.has(edge.target)) next.add(edge.target);
5272
+ if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
5273
+ }
5274
+ }
5275
+ for (const id of next) {
5276
+ const node = graph.nodes.find((n) => n.id === id);
5277
+ if (node) visited.set(id, { node, hop: h + 1 });
5278
+ }
5279
+ frontier = next;
5280
+ if (frontier.size === 0) break;
5281
+ }
5282
+ const nodeIds = new Set(visited.keys());
5283
+ const edges = graph.edges.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target));
5284
+ return { nodes: visited, edges };
5285
+ }
5286
+ function handleBlastPoints(args) {
5287
+ const rootDir = process.cwd();
5288
+ const nodeId = args.node_id;
5289
+ const requestedLayer = args.layer;
5290
+ const hops = args.hops ?? 2;
5291
+ const direction = args.direction ?? "reverse";
5292
+ let targetLayer = requestedLayer;
5293
+ if (!targetLayer) {
5294
+ const graphs = readAllGraphs(rootDir);
5295
+ for (const [layer, graph2] of Object.entries(graphs)) {
5296
+ if (graph2 && graph2.nodes.some((n) => n.id === nodeId)) {
5297
+ targetLayer = layer;
5298
+ break;
5299
+ }
5300
+ }
5301
+ if (!targetLayer) {
5302
+ return err(`Node "${nodeId}" not found in any layer. Available layers: ${getAvailableLayers(rootDir).join(", ")}`);
5303
+ }
5304
+ }
5305
+ const graph = readGraph(rootDir, targetLayer);
5306
+ if (!graph) {
5307
+ return err(`No graph for layer "${targetLayer}". Run generate_graph first.`);
5308
+ }
5309
+ const center = graph.nodes.find((n) => n.id === nodeId);
5310
+ if (!center) {
5311
+ return err(`Node "${nodeId}" not found in ${targetLayer} layer.`);
5312
+ }
5313
+ const result = reverseNeighborhood(graph, nodeId, hops, direction);
5314
+ const affected = [];
5315
+ for (const [id, { node, hop }] of result.nodes) {
5316
+ if (id === nodeId) continue;
5317
+ const tags = node.tags;
5318
+ affected.push({
5319
+ id: node.id,
5320
+ name: node.name,
5321
+ type: node.type,
5322
+ layer: targetLayer,
5323
+ hop,
5324
+ module: tags?.module
5325
+ });
5326
+ }
5327
+ const otherLayers = getAvailableLayers(rootDir).filter((l) => l !== targetLayer && l !== "static");
5328
+ for (const otherLayer of otherLayers) {
5329
+ const otherGraph = readGraph(rootDir, otherLayer);
5330
+ if (!otherGraph) continue;
5331
+ for (const edge of otherGraph.edges) {
5332
+ if (edge.target === nodeId || edge.source === nodeId) {
5333
+ const dependentId = edge.target === nodeId ? edge.source : edge.target;
5334
+ if (affected.some((a) => a.id === dependentId)) continue;
5335
+ const depNode = otherGraph.nodes.find((n) => n.id === dependentId);
5336
+ if (depNode) {
5337
+ const tags = depNode.tags;
5338
+ affected.push({
5339
+ id: depNode.id,
5340
+ name: depNode.name,
5341
+ type: depNode.type,
5342
+ layer: otherLayer,
5343
+ hop: 1,
5344
+ module: tags?.module
5345
+ });
5346
+ }
5347
+ }
5348
+ }
5349
+ }
5350
+ const byLayer = {};
5351
+ const byHop = {};
5352
+ const modulesSet = /* @__PURE__ */ new Set();
5353
+ for (const a of affected) {
5354
+ byLayer[a.layer] = (byLayer[a.layer] ?? 0) + 1;
5355
+ byHop[String(a.hop)] = (byHop[String(a.hop)] ?? 0) + 1;
5356
+ if (a.module) modulesSet.add(a.module);
5357
+ }
5358
+ const crossesLayers = Object.keys(byLayer).length > 1;
5359
+ const centerTags = center.tags;
5360
+ return okJson({
5361
+ center: {
5362
+ id: center.id,
5363
+ name: center.name,
5364
+ type: center.type,
5365
+ layer: targetLayer,
5366
+ module: centerTags?.module
5367
+ },
5368
+ affected,
5369
+ summary: {
5370
+ total: affected.length,
5371
+ by_layer: byLayer,
5372
+ by_hop: byHop,
5373
+ modules_touched: Array.from(modulesSet).sort(),
5374
+ crosses_layers: crossesLayers
5375
+ }
5376
+ });
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
+ }
5051
5516
  function layerSummary(graph) {
5052
5517
  const typeCounts = {};
5053
5518
  const moduleCounts = {};
@@ -5276,12 +5741,12 @@ function handleReadGraph(args) {
5276
5741
  return okJson(result);
5277
5742
  }
5278
5743
  function nodeToFilePath(rootDir, layer, nodeId) {
5279
- if (layer === "ui" || layer === "api") return (0, import_node_path21.join)(rootDir, "src", nodeId);
5280
- if (layer === "db") return (0, import_node_path21.join)(rootDir, "prisma", "schema.prisma");
5281
- const withSrc = (0, import_node_path21.join)(rootDir, "src", nodeId);
5282
- if ((0, import_node_fs19.existsSync)(withSrc)) return withSrc;
5283
- const direct = (0, import_node_path21.join)(rootDir, nodeId);
5284
- 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;
5285
5750
  return null;
5286
5751
  }
5287
5752
  function handleInspectNode(args) {
@@ -5424,11 +5889,11 @@ function handleGrepNodes(args) {
5424
5889
  let filesSearched = 0;
5425
5890
  let truncated = false;
5426
5891
  for (const [filePath, nodeId] of filePaths) {
5427
- if (!(0, import_node_fs19.existsSync)(filePath)) continue;
5892
+ if (!(0, import_node_fs20.existsSync)(filePath)) continue;
5428
5893
  filesSearched++;
5429
5894
  let content;
5430
5895
  try {
5431
- content = (0, import_node_fs19.readFileSync)(filePath, "utf-8");
5896
+ content = (0, import_node_fs20.readFileSync)(filePath, "utf-8");
5432
5897
  } catch {
5433
5898
  continue;
5434
5899
  }
@@ -5493,11 +5958,11 @@ function handleStartChartServer(args) {
5493
5958
  });
5494
5959
  }
5495
5960
  const entryPath = process.argv[1];
5496
- const logDir = (0, import_node_path21.join)((0, import_node_os2.homedir)(), ".launchsecure");
5497
- (0, import_node_fs19.mkdirSync)(logDir, { recursive: true });
5498
- const logPath = (0, import_node_path21.join)(logDir, "launch-chart.log");
5499
- const out = (0, import_node_fs19.openSync)(logPath, "a");
5500
- 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");
5501
5966
  const portArgs = args.port ? ["--port", String(args.port)] : [];
5502
5967
  const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
5503
5968
  detached: true,
@@ -5609,31 +6074,28 @@ function handleDetectProjectStack() {
5609
6074
  for (const l of p.layers) availableLayers.add(l);
5610
6075
  }
5611
6076
  }
5612
- let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
6077
+ const stats = { calls_api: 0, references_api: 0, annotations: 0 };
5613
6078
  const uiGraph = readGraph(rootDir, "ui");
5614
6079
  if (uiGraph) {
5615
6080
  for (const ref of uiGraph.cross_refs ?? []) {
5616
6081
  if (ref.type === "calls_api") stats.calls_api++;
5617
6082
  if (ref.type === "references_api") stats.references_api++;
5618
6083
  }
5619
- for (const f of uiGraph.flagged_edges ?? []) {
5620
- if (f.type === "out_of_pattern") stats.out_of_pattern++;
5621
- }
5622
6084
  }
5623
- const srcDir = (0, import_node_path21.join)(rootDir, "src");
5624
- 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)) {
5625
6087
  const scanDir = (dir) => {
5626
- if (!(0, import_node_fs19.existsSync)(dir)) return;
5627
- 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 })) {
5628
6090
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
5629
- const full = (0, import_node_path21.join)(dir, entry.name);
6091
+ const full = (0, import_node_path22.join)(dir, entry.name);
5630
6092
  if (entry.isDirectory()) {
5631
6093
  scanDir(full);
5632
6094
  continue;
5633
6095
  }
5634
- 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;
5635
6097
  try {
5636
- const content = (0, import_node_fs19.readFileSync)(full, "utf-8");
6098
+ const content = (0, import_node_fs20.readFileSync)(full, "utf-8");
5637
6099
  const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
5638
6100
  if (matches) stats.annotations += matches.length;
5639
6101
  } catch {
@@ -5642,12 +6104,6 @@ function handleDetectProjectStack() {
5642
6104
  };
5643
6105
  scanDir(srcDir);
5644
6106
  }
5645
- let recommendedPrimary = "fetch-resolver";
5646
- if (stats.annotations > 0 && stats.annotations >= stats.calls_api) {
5647
- recommendedPrimary = "api-annotations";
5648
- } else if (stats.calls_api === 0 && stats.references_api > 0) {
5649
- recommendedPrimary = "url-literal-scanner";
5650
- }
5651
6107
  const supportedLanguages = /* @__PURE__ */ new Map();
5652
6108
  supportedLanguages.set("typescript", parserResults.filter((p) => p.detected && p.layers.some((l) => l === "ui" || l === "api")).map((p) => p.id));
5653
6109
  supportedLanguages.set("prisma", parserResults.filter((p) => p.detected && p.layers.includes("db")).map((p) => p.id));
@@ -5658,13 +6114,22 @@ function handleDetectProjectStack() {
5658
6114
  languages,
5659
6115
  parsers: parserResults,
5660
6116
  available_layers: [...availableLayers],
5661
- crosslayer_parsers: [
5662
- { id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
5663
- { id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
5664
- { id: "url-literal-scanner", description: "Finds /api/... string literals as fallback detection" }
5665
- ],
6117
+ crosslayer_parsers: (() => {
6118
+ const descriptions = {
6119
+ "fetch-resolver": "Detects direct fetch()/api.get() calls with inline URLs",
6120
+ "api-annotations": "Scans for @api METHOD /path annotations in JSDoc/comments",
6121
+ "url-literal-scanner": "Finds /api/... string literals as fallback detection",
6122
+ "static-ref-scanner": "Finds references to static values (enums, permissions, roles)"
6123
+ };
6124
+ const grouped = {};
6125
+ for (const p of registry.getCrossLayerParsers()) {
6126
+ const concern = p.concern ?? "api-binding";
6127
+ if (!grouped[concern]) grouped[concern] = [];
6128
+ grouped[concern].push({ id: p.id, description: descriptions[p.id] ?? p.id });
6129
+ }
6130
+ return grouped;
6131
+ })(),
5666
6132
  stats,
5667
- recommended_primary: recommendedPrimary,
5668
6133
  ...unsupportedHint ? { unsupported_hint: unsupportedHint } : {},
5669
6134
  current_config: Object.keys(config).length > 0 ? config : null,
5670
6135
  config_path: ".launchchart.json"
@@ -5745,6 +6210,14 @@ async function handleMessage(msg) {
5745
6210
  respond(id ?? null, handleAuditLayer(args));
5746
6211
  return;
5747
6212
  }
6213
+ if (toolName === "blast_points") {
6214
+ respond(id ?? null, handleBlastPoints(args));
6215
+ return;
6216
+ }
6217
+ if (toolName === "generate_blast_radius") {
6218
+ respond(id ?? null, handleGenerateBlastRadius(args));
6219
+ return;
6220
+ }
5748
6221
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
5749
6222
  return;
5750
6223
  }
@@ -5780,15 +6253,16 @@ function startGraphMcpServer() {
5780
6253
  process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
5781
6254
  `);
5782
6255
  }
5783
- 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;
5784
6257
  var init_graph_mcp = __esm({
5785
6258
  "src/server/graph-mcp.ts"() {
5786
6259
  "use strict";
5787
- import_node_fs19 = require("node:fs");
5788
- import_node_path21 = require("node:path");
6260
+ import_node_fs20 = require("node:fs");
6261
+ import_node_path22 = require("node:path");
5789
6262
  import_node_child_process2 = require("node:child_process");
5790
6263
  import_node_os2 = require("node:os");
5791
6264
  init_graph();
6265
+ init_blast_radius_builder();
5792
6266
  init_lockfile();
5793
6267
  init_config();
5794
6268
  init_parser_registry();
@@ -6070,6 +6544,116 @@ Use this when the user asks "is the chart running", "show me the project graph U
6070
6544
  },
6071
6545
  required: ["layer"]
6072
6546
  }
6547
+ },
6548
+ {
6549
+ name: "blast_points",
6550
+ description: `Calculate the blast radius for a node \u2014 what depends on it across all project layers. Returns reverse dependencies aggregated with hop distance and summary stats.
6551
+
6552
+ USE THIS when assessing the impact of changing a file, table, or endpoint. Replaces multiple read_graph calls with a single query that:
6553
+ - Traverses REVERSE edges (who imports/depends on this node)
6554
+ - Searches across ALL layers if layer is omitted
6555
+ - Returns affected nodes with hop distance, type, layer, and module
6556
+ - Provides a summary with counts by layer, by hop, and risk assessment
6557
+
6558
+ Example: blast_points(node_id: "server/auth/middleware.ts", hops: 2) \u2192 returns all files that import middleware.ts, and all files that import THOSE files.`,
6559
+ inputSchema: {
6560
+ type: "object",
6561
+ properties: {
6562
+ node_id: {
6563
+ type: "string",
6564
+ description: "The node to analyze (file path, table name, etc.)"
6565
+ },
6566
+ layer: {
6567
+ type: "string",
6568
+ description: "Layer the node lives in (e.g. 'ui', 'api', 'db'). Omit to auto-detect by searching all layers."
6569
+ },
6570
+ hops: {
6571
+ type: "number",
6572
+ description: "Max hops to traverse outward. Default 2."
6573
+ },
6574
+ direction: {
6575
+ type: "string",
6576
+ enum: ["reverse", "both"],
6577
+ description: "'reverse' (default) = only what depends on this node. 'both' = full neighborhood."
6578
+ }
6579
+ },
6580
+ required: ["node_id"]
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
+ }
6073
6657
  }
6074
6658
  ];
6075
6659
  COMPACT_SCHEMA = {
@@ -6136,10 +6720,10 @@ Use this when the user asks "is the chart running", "show me the project graph U
6136
6720
 
6137
6721
  // src/server/graph-mcp-entry.ts
6138
6722
  var import_node_child_process3 = require("node:child_process");
6139
- var import_node_fs20 = require("node:fs");
6140
- var import_node_path22 = __toESM(require("node:path"));
6141
- var import_node_os3 = require("node:os");
6142
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");
6143
6727
  init_lockfile();
6144
6728
  function logStderr(msg) {
6145
6729
  process.stderr.write(`[launch-chart] ${msg}
@@ -6155,11 +6739,11 @@ function maybeAutoServe() {
6155
6739
  return;
6156
6740
  }
6157
6741
  try {
6158
- const logDir = import_node_path22.default.join((0, import_node_os3.homedir)(), ".launchsecure");
6159
- (0, import_node_fs21.mkdirSync)(logDir, { recursive: true });
6160
- const logPath = import_node_path22.default.join(logDir, "launch-chart.log");
6161
- const out = (0, import_node_fs20.openSync)(logPath, "a");
6162
- 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");
6163
6747
  const entryPath = process.argv[1];
6164
6748
  const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
6165
6749
  detached: true,