@launchsecure/launch-kit 0.0.14 → 0.0.15

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.
@@ -624,19 +624,26 @@ init_config();
624
624
  // src/server/graph/core/resolve-paths.ts
625
625
  var import_node_fs2 = require("node:fs");
626
626
  var import_node_path2 = require("node:path");
627
+ function detectDbDir(rootDir, config) {
628
+ if (config.paths?.dbDir) return (0, import_node_path2.join)(rootDir, config.paths.dbDir);
629
+ const prismaDir = (0, import_node_path2.join)(rootDir, "prisma");
630
+ if ((0, import_node_fs2.existsSync)(prismaDir)) return prismaDir;
631
+ return null;
632
+ }
627
633
  function resolveProjectPaths(rootDir, config) {
634
+ const dbDir = detectDbDir(rootDir, config);
628
635
  if (config.paths?.appDir) {
629
636
  const appDir = (0, import_node_path2.join)(rootDir, config.paths.appDir);
630
637
  const srcDir = config.paths.srcDir ? (0, import_node_path2.join)(rootDir, config.paths.srcDir) : (0, import_node_path2.dirname)(appDir);
631
- return { srcDir, appDir, apiDir: (0, import_node_path2.join)(appDir, "api") };
638
+ return { srcDir, appDir, apiDir: (0, import_node_path2.join)(appDir, "api"), dbDir };
632
639
  }
633
640
  const srcApp = (0, import_node_path2.join)(rootDir, "src", "app");
634
641
  if ((0, import_node_fs2.existsSync)(srcApp)) {
635
- return { srcDir: (0, import_node_path2.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path2.join)(srcApp, "api") };
642
+ return { srcDir: (0, import_node_path2.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path2.join)(srcApp, "api"), dbDir };
636
643
  }
637
644
  const rootApp = (0, import_node_path2.join)(rootDir, "app");
638
645
  if ((0, import_node_fs2.existsSync)(rootApp)) {
639
- return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path2.join)(rootApp, "api") };
646
+ return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path2.join)(rootApp, "api"), dbDir };
640
647
  }
641
648
  return null;
642
649
  }
@@ -2000,6 +2007,7 @@ function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
2000
2007
  var fetchResolverParser = {
2001
2008
  id: "fetch-resolver",
2002
2009
  layer: "crosslayer",
2010
+ concern: "api-binding",
2003
2011
  detect(_rootDir) {
2004
2012
  return true;
2005
2013
  },
@@ -2113,6 +2121,7 @@ function toNodeId2(srcDir, absPath) {
2113
2121
  var apiAnnotationsParser = {
2114
2122
  id: "api-annotations",
2115
2123
  layer: "crosslayer",
2124
+ concern: "api-binding",
2116
2125
  detect(rootDir) {
2117
2126
  return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
2118
2127
  },
@@ -2199,6 +2208,7 @@ function toNodeId3(srcDir, absPath) {
2199
2208
  var urlLiteralScannerParser = {
2200
2209
  id: "url-literal-scanner",
2201
2210
  layer: "crosslayer",
2211
+ concern: "api-binding",
2202
2212
  detect(rootDir) {
2203
2213
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2204
2214
  return paths !== null;
@@ -2810,6 +2820,7 @@ function collectStaticRefsRegex(content, valueLookup, allValues) {
2810
2820
  var staticRefScannerParser = {
2811
2821
  id: "static-ref-scanner",
2812
2822
  layer: "crosslayer",
2823
+ concern: "static-ref",
2813
2824
  detect(rootDir) {
2814
2825
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2815
2826
  return paths !== null;
@@ -2994,6 +3005,9 @@ function loadCustomParsers(registry, config, rootDir, disabled) {
2994
3005
  `
2995
3006
  );
2996
3007
  }
3008
+ if (parser.layer === "crosslayer" && entry.concern && !("concern" in parser && parser.concern)) {
3009
+ parser.concern = entry.concern;
3010
+ }
2997
3011
  registry.register(parser);
2998
3012
  } catch (err) {
2999
3013
  process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err}
@@ -3091,44 +3105,21 @@ function dedupCrossRefs(refs) {
3091
3105
  }
3092
3106
  return result;
3093
3107
  }
3094
- function applyCrossLayerResults(uiOutput, results, primaryId) {
3095
- const allCrossRefs = [...uiOutput.cross_refs];
3096
- const allFlagged = [...uiOutput.flagged_edges];
3097
- const allWarnings = [...uiOutput.warnings];
3098
- const primaryResult = results.find((r) => r.parserId === primaryId);
3099
- const secondaryResults = results.filter((r) => r.parserId !== primaryId);
3100
- if (primaryResult) {
3101
- allCrossRefs.push(...primaryResult.output.cross_refs);
3102
- allFlagged.push(...primaryResult.output.flagged_edges);
3103
- allWarnings.push(...primaryResult.output.warnings);
3104
- }
3105
- const primarySet = new Set(
3106
- (primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
3107
- );
3108
- for (const sec of secondaryResults) {
3109
- for (const ref of sec.output.cross_refs) {
3110
- const key = `${ref.source}|${ref.target}|${ref.type}`;
3111
- if (primarySet.has(key)) {
3112
- allCrossRefs.push(ref);
3113
- } else {
3114
- allFlagged.push({
3115
- source: ref.source,
3116
- target: ref.target,
3117
- type: "out_of_pattern",
3118
- label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
3119
- confidence: "medium"
3120
- });
3121
- allCrossRefs.push(ref);
3122
- }
3123
- }
3124
- allFlagged.push(...sec.output.flagged_edges);
3125
- allWarnings.push(...sec.output.warnings);
3126
- }
3108
+ function applyCrossLayerResults(uiOutput, results) {
3127
3109
  return {
3128
3110
  ...uiOutput,
3129
- cross_refs: dedupCrossRefs(allCrossRefs),
3130
- flagged_edges: allFlagged,
3131
- warnings: allWarnings
3111
+ cross_refs: dedupCrossRefs([
3112
+ ...uiOutput.cross_refs,
3113
+ ...results.flatMap((r) => r.output.cross_refs)
3114
+ ]),
3115
+ flagged_edges: [
3116
+ ...uiOutput.flagged_edges,
3117
+ ...results.flatMap((r) => r.output.flagged_edges)
3118
+ ],
3119
+ warnings: [
3120
+ ...uiOutput.warnings,
3121
+ ...results.flatMap((r) => r.output.warnings)
3122
+ ]
3132
3123
  };
3133
3124
  }
3134
3125
 
@@ -3167,10 +3158,9 @@ function generateLayer(rootDir, layer) {
3167
3158
  if (existing) layerOutputs.set(otherLayer, existing);
3168
3159
  }
3169
3160
  const crossParsers = registry.getCrossLayerParsers();
3170
- const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
3171
3161
  const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
3172
3162
  if (crossResults.length > 0) {
3173
- merged = applyCrossLayerResults(merged, crossResults, primaryId);
3163
+ merged = applyCrossLayerResults(merged, crossResults);
3174
3164
  }
3175
3165
  }
3176
3166
  return {
@@ -3217,11 +3207,10 @@ function generateAll(rootDir) {
3217
3207
  });
3218
3208
  }
3219
3209
  const crossParsers = registry.getCrossLayerParsers();
3220
- const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
3221
3210
  const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
3222
3211
  if (crossResults.length > 0 && layerOutputs.has("ui")) {
3223
3212
  const uiOutput = layerOutputs.get("ui");
3224
- const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
3213
+ const merged = applyCrossLayerResults(uiOutput, crossResults);
3225
3214
  layerOutputs.set("ui", merged);
3226
3215
  const uiResult = results.find((r) => r.layer === "ui");
3227
3216
  if (uiResult) {
@@ -4386,17 +4375,23 @@ async function startChartServer(opts = {}) {
4386
4375
  if (req.method === "GET" && url2.pathname === "/api/parser-config") {
4387
4376
  const config2 = loadConfig(reqRoot);
4388
4377
  const registry = createRegistry(config2, reqRoot);
4378
+ const toLabel = (id) => id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
4389
4379
  const detection = [];
4390
4380
  for (const parser of registry.getAll()) {
4391
4381
  if ("layers" in parser && Array.isArray(parser.layers)) {
4392
4382
  const mp = parser;
4393
- detection.push({ id: mp.id, layers: mp.layers, detected: mp.detect(reqRoot) });
4383
+ detection.push({ id: mp.id, layers: mp.layers, label: toLabel(mp.id), detected: mp.detect(reqRoot) });
4394
4384
  } else if ("layer" in parser && parser.layer !== "crosslayer") {
4395
4385
  const sp = parser;
4396
- detection.push({ id: sp.id, layers: [sp.layer], detected: sp.detect(reqRoot) });
4386
+ detection.push({ id: sp.id, layers: [sp.layer], label: toLabel(sp.id), detected: sp.detect(reqRoot) });
4397
4387
  }
4398
4388
  }
4399
- const crosslayerParsers = registry.getCrossLayerParsers().map((p) => ({ id: p.id }));
4389
+ const crosslayerParsers = {};
4390
+ for (const p of registry.getCrossLayerParsers()) {
4391
+ const concern = p.concern ?? "api-binding";
4392
+ if (!crosslayerParsers[concern]) crosslayerParsers[concern] = [];
4393
+ crosslayerParsers[concern].push({ id: p.id, label: toLabel(p.id) });
4394
+ }
4400
4395
  res.writeHead(200, { "Content-Type": "application/json" });
4401
4396
  res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
4402
4397
  return;
@@ -4509,16 +4504,21 @@ async function startChartServer(opts = {}) {
4509
4504
  if (req.method === "GET" && url2.pathname === "/api/detected-paths") {
4510
4505
  const config2 = loadConfig(reqRoot);
4511
4506
  const paths = resolveProjectPaths(reqRoot, config2);
4512
- const isOverride = !!config2.paths?.appDir;
4507
+ const overrides = {
4508
+ appDir: !!config2.paths?.appDir,
4509
+ dbDir: !!config2.paths?.dbDir
4510
+ };
4513
4511
  res.writeHead(200, { "Content-Type": "application/json" });
4514
4512
  res.end(JSON.stringify({
4515
4513
  projectRoot: reqRoot,
4516
4514
  detected: paths ? {
4517
4515
  srcDir: import_node_path19.default.relative(reqRoot, paths.srcDir) || ".",
4518
4516
  appDir: import_node_path19.default.relative(reqRoot, paths.appDir),
4519
- apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir)
4517
+ apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir),
4518
+ dbDir: paths.dbDir ? import_node_path19.default.relative(reqRoot, paths.dbDir) : null
4520
4519
  } : null,
4521
- isOverride
4520
+ overrides,
4521
+ isOverride: overrides.appDir
4522
4522
  }));
4523
4523
  return;
4524
4524
  }
@@ -6906,19 +6906,26 @@ init_config();
6906
6906
  // src/server/graph/core/resolve-paths.ts
6907
6907
  var import_node_fs2 = require("node:fs");
6908
6908
  var import_node_path2 = require("node:path");
6909
+ function detectDbDir(rootDir, config) {
6910
+ if (config.paths?.dbDir) return (0, import_node_path2.join)(rootDir, config.paths.dbDir);
6911
+ const prismaDir = (0, import_node_path2.join)(rootDir, "prisma");
6912
+ if ((0, import_node_fs2.existsSync)(prismaDir)) return prismaDir;
6913
+ return null;
6914
+ }
6909
6915
  function resolveProjectPaths(rootDir, config) {
6916
+ const dbDir = detectDbDir(rootDir, config);
6910
6917
  if (config.paths?.appDir) {
6911
6918
  const appDir = (0, import_node_path2.join)(rootDir, config.paths.appDir);
6912
6919
  const srcDir = config.paths.srcDir ? (0, import_node_path2.join)(rootDir, config.paths.srcDir) : (0, import_node_path2.dirname)(appDir);
6913
- return { srcDir, appDir, apiDir: (0, import_node_path2.join)(appDir, "api") };
6920
+ return { srcDir, appDir, apiDir: (0, import_node_path2.join)(appDir, "api"), dbDir };
6914
6921
  }
6915
6922
  const srcApp = (0, import_node_path2.join)(rootDir, "src", "app");
6916
6923
  if ((0, import_node_fs2.existsSync)(srcApp)) {
6917
- return { srcDir: (0, import_node_path2.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path2.join)(srcApp, "api") };
6924
+ return { srcDir: (0, import_node_path2.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path2.join)(srcApp, "api"), dbDir };
6918
6925
  }
6919
6926
  const rootApp = (0, import_node_path2.join)(rootDir, "app");
6920
6927
  if ((0, import_node_fs2.existsSync)(rootApp)) {
6921
- return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path2.join)(rootApp, "api") };
6928
+ return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path2.join)(rootApp, "api"), dbDir };
6922
6929
  }
6923
6930
  return null;
6924
6931
  }
@@ -8282,6 +8289,7 @@ function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
8282
8289
  var fetchResolverParser = {
8283
8290
  id: "fetch-resolver",
8284
8291
  layer: "crosslayer",
8292
+ concern: "api-binding",
8285
8293
  detect(_rootDir) {
8286
8294
  return true;
8287
8295
  },
@@ -8395,6 +8403,7 @@ function toNodeId2(srcDir, absPath) {
8395
8403
  var apiAnnotationsParser = {
8396
8404
  id: "api-annotations",
8397
8405
  layer: "crosslayer",
8406
+ concern: "api-binding",
8398
8407
  detect(rootDir) {
8399
8408
  return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
8400
8409
  },
@@ -8481,6 +8490,7 @@ function toNodeId3(srcDir, absPath) {
8481
8490
  var urlLiteralScannerParser = {
8482
8491
  id: "url-literal-scanner",
8483
8492
  layer: "crosslayer",
8493
+ concern: "api-binding",
8484
8494
  detect(rootDir) {
8485
8495
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
8486
8496
  return paths !== null;
@@ -9092,6 +9102,7 @@ function collectStaticRefsRegex(content, valueLookup, allValues) {
9092
9102
  var staticRefScannerParser = {
9093
9103
  id: "static-ref-scanner",
9094
9104
  layer: "crosslayer",
9105
+ concern: "static-ref",
9095
9106
  detect(rootDir) {
9096
9107
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
9097
9108
  return paths !== null;
@@ -9276,6 +9287,9 @@ function loadCustomParsers(registry, config, rootDir, disabled) {
9276
9287
  `
9277
9288
  );
9278
9289
  }
9290
+ if (parser.layer === "crosslayer" && entry.concern && !("concern" in parser && parser.concern)) {
9291
+ parser.concern = entry.concern;
9292
+ }
9279
9293
  registry.register(parser);
9280
9294
  } catch (err2) {
9281
9295
  process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
@@ -9373,44 +9387,21 @@ function dedupCrossRefs(refs) {
9373
9387
  }
9374
9388
  return result;
9375
9389
  }
9376
- function applyCrossLayerResults(uiOutput, results, primaryId) {
9377
- const allCrossRefs = [...uiOutput.cross_refs];
9378
- const allFlagged = [...uiOutput.flagged_edges];
9379
- const allWarnings = [...uiOutput.warnings];
9380
- const primaryResult = results.find((r) => r.parserId === primaryId);
9381
- const secondaryResults = results.filter((r) => r.parserId !== primaryId);
9382
- if (primaryResult) {
9383
- allCrossRefs.push(...primaryResult.output.cross_refs);
9384
- allFlagged.push(...primaryResult.output.flagged_edges);
9385
- allWarnings.push(...primaryResult.output.warnings);
9386
- }
9387
- const primarySet = new Set(
9388
- (primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
9389
- );
9390
- for (const sec of secondaryResults) {
9391
- for (const ref of sec.output.cross_refs) {
9392
- const key = `${ref.source}|${ref.target}|${ref.type}`;
9393
- if (primarySet.has(key)) {
9394
- allCrossRefs.push(ref);
9395
- } else {
9396
- allFlagged.push({
9397
- source: ref.source,
9398
- target: ref.target,
9399
- type: "out_of_pattern",
9400
- label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
9401
- confidence: "medium"
9402
- });
9403
- allCrossRefs.push(ref);
9404
- }
9405
- }
9406
- allFlagged.push(...sec.output.flagged_edges);
9407
- allWarnings.push(...sec.output.warnings);
9408
- }
9390
+ function applyCrossLayerResults(uiOutput, results) {
9409
9391
  return {
9410
9392
  ...uiOutput,
9411
- cross_refs: dedupCrossRefs(allCrossRefs),
9412
- flagged_edges: allFlagged,
9413
- warnings: allWarnings
9393
+ cross_refs: dedupCrossRefs([
9394
+ ...uiOutput.cross_refs,
9395
+ ...results.flatMap((r) => r.output.cross_refs)
9396
+ ]),
9397
+ flagged_edges: [
9398
+ ...uiOutput.flagged_edges,
9399
+ ...results.flatMap((r) => r.output.flagged_edges)
9400
+ ],
9401
+ warnings: [
9402
+ ...uiOutput.warnings,
9403
+ ...results.flatMap((r) => r.output.warnings)
9404
+ ]
9414
9405
  };
9415
9406
  }
9416
9407
 
@@ -9449,10 +9440,9 @@ function generateLayer(rootDir, layer) {
9449
9440
  if (existing) layerOutputs.set(otherLayer, existing);
9450
9441
  }
9451
9442
  const crossParsers = registry.getCrossLayerParsers();
9452
- const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
9453
9443
  const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
9454
9444
  if (crossResults.length > 0) {
9455
- merged = applyCrossLayerResults(merged, crossResults, primaryId);
9445
+ merged = applyCrossLayerResults(merged, crossResults);
9456
9446
  }
9457
9447
  }
9458
9448
  return {
@@ -9499,11 +9489,10 @@ function generateAll(rootDir) {
9499
9489
  });
9500
9490
  }
9501
9491
  const crossParsers = registry.getCrossLayerParsers();
9502
- const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
9503
9492
  const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
9504
9493
  if (crossResults.length > 0 && layerOutputs.has("ui")) {
9505
9494
  const uiOutput = layerOutputs.get("ui");
9506
- const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
9495
+ const merged = applyCrossLayerResults(uiOutput, crossResults);
9507
9496
  layerOutputs.set("ui", merged);
9508
9497
  const uiResult = results.find((r) => r.layer === "ui");
9509
9498
  if (uiResult) {
@@ -10901,6 +10890,41 @@ Use this when the user asks "is the chart running", "show me the project graph U
10901
10890
  },
10902
10891
  required: ["layer"]
10903
10892
  }
10893
+ },
10894
+ {
10895
+ name: "blast_points",
10896
+ 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.
10897
+
10898
+ USE THIS when assessing the impact of changing a file, table, or endpoint. Replaces multiple read_graph calls with a single query that:
10899
+ - Traverses REVERSE edges (who imports/depends on this node)
10900
+ - Searches across ALL layers if layer is omitted
10901
+ - Returns affected nodes with hop distance, type, layer, and module
10902
+ - Provides a summary with counts by layer, by hop, and risk assessment
10903
+
10904
+ 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.`,
10905
+ inputSchema: {
10906
+ type: "object",
10907
+ properties: {
10908
+ node_id: {
10909
+ type: "string",
10910
+ description: "The node to analyze (file path, table name, etc.)"
10911
+ },
10912
+ layer: {
10913
+ type: "string",
10914
+ description: "Layer the node lives in (e.g. 'ui', 'api', 'db'). Omit to auto-detect by searching all layers."
10915
+ },
10916
+ hops: {
10917
+ type: "number",
10918
+ description: "Max hops to traverse outward. Default 2."
10919
+ },
10920
+ direction: {
10921
+ type: "string",
10922
+ enum: ["reverse", "both"],
10923
+ description: "'reverse' (default) = only what depends on this node. 'both' = full neighborhood."
10924
+ }
10925
+ },
10926
+ required: ["node_id"]
10927
+ }
10904
10928
  }
10905
10929
  ];
10906
10930
  function matchesSearch(node, query) {
@@ -11058,6 +11082,125 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
11058
11082
  const edges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
11059
11083
  return { nodes, edges, budgetExceeded, stoppedAtHop };
11060
11084
  }
11085
+ function reverseNeighborhood(graph, centerId, hops, direction) {
11086
+ const center = graph.nodes.find((n) => n.id === centerId);
11087
+ if (!center) return { nodes: /* @__PURE__ */ new Map(), edges: [] };
11088
+ const visited = /* @__PURE__ */ new Map();
11089
+ visited.set(centerId, { node: center, hop: 0 });
11090
+ let frontier = /* @__PURE__ */ new Set([centerId]);
11091
+ for (let h = 0; h < hops; h++) {
11092
+ const next = /* @__PURE__ */ new Set();
11093
+ for (const edge of graph.edges) {
11094
+ if (direction === "reverse") {
11095
+ if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
11096
+ } else {
11097
+ if (frontier.has(edge.source) && !visited.has(edge.target)) next.add(edge.target);
11098
+ if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
11099
+ }
11100
+ }
11101
+ for (const id of next) {
11102
+ const node = graph.nodes.find((n) => n.id === id);
11103
+ if (node) visited.set(id, { node, hop: h + 1 });
11104
+ }
11105
+ frontier = next;
11106
+ if (frontier.size === 0) break;
11107
+ }
11108
+ const nodeIds = new Set(visited.keys());
11109
+ const edges = graph.edges.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target));
11110
+ return { nodes: visited, edges };
11111
+ }
11112
+ function handleBlastPoints(args) {
11113
+ const rootDir = process.cwd();
11114
+ const nodeId = args.node_id;
11115
+ const requestedLayer = args.layer;
11116
+ const hops = args.hops ?? 2;
11117
+ const direction = args.direction ?? "reverse";
11118
+ let targetLayer = requestedLayer;
11119
+ if (!targetLayer) {
11120
+ const graphs = readAllGraphs(rootDir);
11121
+ for (const [layer, graph2] of Object.entries(graphs)) {
11122
+ if (graph2 && graph2.nodes.some((n) => n.id === nodeId)) {
11123
+ targetLayer = layer;
11124
+ break;
11125
+ }
11126
+ }
11127
+ if (!targetLayer) {
11128
+ return err(`Node "${nodeId}" not found in any layer. Available layers: ${getAvailableLayers(rootDir).join(", ")}`);
11129
+ }
11130
+ }
11131
+ const graph = readGraph(rootDir, targetLayer);
11132
+ if (!graph) {
11133
+ return err(`No graph for layer "${targetLayer}". Run generate_graph first.`);
11134
+ }
11135
+ const center = graph.nodes.find((n) => n.id === nodeId);
11136
+ if (!center) {
11137
+ return err(`Node "${nodeId}" not found in ${targetLayer} layer.`);
11138
+ }
11139
+ const result = reverseNeighborhood(graph, nodeId, hops, direction);
11140
+ const affected = [];
11141
+ for (const [id, { node, hop }] of result.nodes) {
11142
+ if (id === nodeId) continue;
11143
+ const tags = node.tags;
11144
+ affected.push({
11145
+ id: node.id,
11146
+ name: node.name,
11147
+ type: node.type,
11148
+ layer: targetLayer,
11149
+ hop,
11150
+ module: tags?.module
11151
+ });
11152
+ }
11153
+ const otherLayers = getAvailableLayers(rootDir).filter((l) => l !== targetLayer && l !== "static");
11154
+ for (const otherLayer of otherLayers) {
11155
+ const otherGraph = readGraph(rootDir, otherLayer);
11156
+ if (!otherGraph) continue;
11157
+ for (const edge of otherGraph.edges) {
11158
+ if (edge.target === nodeId || edge.source === nodeId) {
11159
+ const dependentId = edge.target === nodeId ? edge.source : edge.target;
11160
+ if (affected.some((a) => a.id === dependentId)) continue;
11161
+ const depNode = otherGraph.nodes.find((n) => n.id === dependentId);
11162
+ if (depNode) {
11163
+ const tags = depNode.tags;
11164
+ affected.push({
11165
+ id: depNode.id,
11166
+ name: depNode.name,
11167
+ type: depNode.type,
11168
+ layer: otherLayer,
11169
+ hop: 1,
11170
+ module: tags?.module
11171
+ });
11172
+ }
11173
+ }
11174
+ }
11175
+ }
11176
+ const byLayer = {};
11177
+ const byHop = {};
11178
+ const modulesSet = /* @__PURE__ */ new Set();
11179
+ for (const a of affected) {
11180
+ byLayer[a.layer] = (byLayer[a.layer] ?? 0) + 1;
11181
+ byHop[String(a.hop)] = (byHop[String(a.hop)] ?? 0) + 1;
11182
+ if (a.module) modulesSet.add(a.module);
11183
+ }
11184
+ const crossesLayers = Object.keys(byLayer).length > 1;
11185
+ const centerTags = center.tags;
11186
+ return okJson({
11187
+ center: {
11188
+ id: center.id,
11189
+ name: center.name,
11190
+ type: center.type,
11191
+ layer: targetLayer,
11192
+ module: centerTags?.module
11193
+ },
11194
+ affected,
11195
+ summary: {
11196
+ total: affected.length,
11197
+ by_layer: byLayer,
11198
+ by_hop: byHop,
11199
+ modules_touched: Array.from(modulesSet).sort(),
11200
+ crosses_layers: crossesLayers
11201
+ }
11202
+ });
11203
+ }
11061
11204
  function layerSummary(graph) {
11062
11205
  const typeCounts = {};
11063
11206
  const moduleCounts = {};
@@ -11619,16 +11762,13 @@ function handleDetectProjectStack() {
11619
11762
  for (const l of p.layers) availableLayers.add(l);
11620
11763
  }
11621
11764
  }
11622
- let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
11765
+ const stats = { calls_api: 0, references_api: 0, annotations: 0 };
11623
11766
  const uiGraph = readGraph(rootDir, "ui");
11624
11767
  if (uiGraph) {
11625
11768
  for (const ref of uiGraph.cross_refs ?? []) {
11626
11769
  if (ref.type === "calls_api") stats.calls_api++;
11627
11770
  if (ref.type === "references_api") stats.references_api++;
11628
11771
  }
11629
- for (const f of uiGraph.flagged_edges ?? []) {
11630
- if (f.type === "out_of_pattern") stats.out_of_pattern++;
11631
- }
11632
11772
  }
11633
11773
  const srcDir = (0, import_node_path20.join)(rootDir, "src");
11634
11774
  if ((0, import_node_fs18.existsSync)(srcDir)) {
@@ -11652,12 +11792,6 @@ function handleDetectProjectStack() {
11652
11792
  };
11653
11793
  scanDir(srcDir);
11654
11794
  }
11655
- let recommendedPrimary = "fetch-resolver";
11656
- if (stats.annotations > 0 && stats.annotations >= stats.calls_api) {
11657
- recommendedPrimary = "api-annotations";
11658
- } else if (stats.calls_api === 0 && stats.references_api > 0) {
11659
- recommendedPrimary = "url-literal-scanner";
11660
- }
11661
11795
  const supportedLanguages = /* @__PURE__ */ new Map();
11662
11796
  supportedLanguages.set("typescript", parserResults.filter((p) => p.detected && p.layers.some((l) => l === "ui" || l === "api")).map((p) => p.id));
11663
11797
  supportedLanguages.set("prisma", parserResults.filter((p) => p.detected && p.layers.includes("db")).map((p) => p.id));
@@ -11668,13 +11802,22 @@ function handleDetectProjectStack() {
11668
11802
  languages,
11669
11803
  parsers: parserResults,
11670
11804
  available_layers: [...availableLayers],
11671
- crosslayer_parsers: [
11672
- { id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
11673
- { id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
11674
- { id: "url-literal-scanner", description: "Finds /api/... string literals as fallback detection" }
11675
- ],
11805
+ crosslayer_parsers: (() => {
11806
+ const descriptions = {
11807
+ "fetch-resolver": "Detects direct fetch()/api.get() calls with inline URLs",
11808
+ "api-annotations": "Scans for @api METHOD /path annotations in JSDoc/comments",
11809
+ "url-literal-scanner": "Finds /api/... string literals as fallback detection",
11810
+ "static-ref-scanner": "Finds references to static values (enums, permissions, roles)"
11811
+ };
11812
+ const grouped = {};
11813
+ for (const p of registry.getCrossLayerParsers()) {
11814
+ const concern = p.concern ?? "api-binding";
11815
+ if (!grouped[concern]) grouped[concern] = [];
11816
+ grouped[concern].push({ id: p.id, description: descriptions[p.id] ?? p.id });
11817
+ }
11818
+ return grouped;
11819
+ })(),
11676
11820
  stats,
11677
- recommended_primary: recommendedPrimary,
11678
11821
  ...unsupportedHint ? { unsupported_hint: unsupportedHint } : {},
11679
11822
  current_config: Object.keys(config).length > 0 ? config : null,
11680
11823
  config_path: ".launchchart.json"
@@ -11755,6 +11898,10 @@ async function handleMessage(msg) {
11755
11898
  respond(id ?? null, handleAuditLayer(args));
11756
11899
  return;
11757
11900
  }
11901
+ if (toolName === "blast_points") {
11902
+ respond(id ?? null, handleBlastPoints(args));
11903
+ return;
11904
+ }
11758
11905
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
11759
11906
  return;
11760
11907
  }