@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.
- package/dist/chart-client/assets/{index-Dm6IBkiC.js → index-BpQPtTuo.js} +89 -89
- package/dist/chart-client/assets/index-CbZ13AXL.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-BCYw64M7.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +49 -49
- package/dist/server/cli.js +206 -59
- package/dist/server/graph-mcp-entry.js +223 -65
- package/package.json +1 -1
- package/dist/chart-client/assets/index-l-yyLDX5.css +0 -1
- package/dist/client/assets/index-rlw8dmPR.css +0 -32
- /package/dist/client/assets/{index-CyML1UiJ.js → index-3ENenBk-.js} +0 -0
|
@@ -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
|
|
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(
|
|
3130
|
-
|
|
3131
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
4520
|
+
overrides,
|
|
4521
|
+
isOverride: overrides.appDir
|
|
4522
4522
|
}));
|
|
4523
4523
|
return;
|
|
4524
4524
|
}
|
package/dist/server/cli.js
CHANGED
|
@@ -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
|
|
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(
|
|
9412
|
-
|
|
9413
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
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
|
}
|