@jefuriiij/synthra 0.10.0 → 0.11.0
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/CHANGELOG.md +18 -0
- package/dist/cli/index.js +116 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/index.js +1 -1
- package/dist/dashboard/index.js.map +1 -1
- package/dist/server/index.js +115 -4
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -3106,11 +3106,14 @@ var TOOLS = [
|
|
|
3106
3106
|
},
|
|
3107
3107
|
{
|
|
3108
3108
|
name: "blast_radius",
|
|
3109
|
-
description: "
|
|
3109
|
+
description: "See what could break before an edit. A bare file target returns all files that depend on it transitively via imports, tests, and call edges. A 'file::symbol' target returns the exact caller SYMBOLS that transitively call it (name \u2192 file:line) plus the test files guarding the impact \u2014 the precise rename-safety view. Call edges are name-resolved (precise within a file, unique-name across files).",
|
|
3110
3110
|
inputSchema: {
|
|
3111
3111
|
type: "object",
|
|
3112
3112
|
properties: {
|
|
3113
|
-
target: {
|
|
3113
|
+
target: {
|
|
3114
|
+
type: "string",
|
|
3115
|
+
description: "File path (file-level dependents) or 'file::symbol' (caller symbols)."
|
|
3116
|
+
},
|
|
3114
3117
|
depth: { type: "number", description: "Max hops to traverse. Default 3." }
|
|
3115
3118
|
},
|
|
3116
3119
|
required: ["target"]
|
|
@@ -3161,7 +3164,8 @@ function blastRadius(args, ctx) {
|
|
|
3161
3164
|
const targetRaw = typeof args?.target === "string" ? args.target.trim() : "";
|
|
3162
3165
|
const maxDepth = typeof args?.depth === "number" && args.depth > 0 ? Math.floor(args.depth) : 3;
|
|
3163
3166
|
if (!targetRaw) return errorContent("blast_radius: 'target' (string) is required");
|
|
3164
|
-
|
|
3167
|
+
if (targetRaw.includes("::")) return blastRadiusSymbol(targetRaw, maxDepth, ctx);
|
|
3168
|
+
const filePath = targetRaw;
|
|
3165
3169
|
const root = ctx.graph.nodes.find((n) => n.kind === "file" && n.path === filePath);
|
|
3166
3170
|
if (!root) return errorContent(`blast_radius: file not in graph: ${filePath}`);
|
|
3167
3171
|
const fileIdBySymbol = /* @__PURE__ */ new Map();
|
|
@@ -3216,6 +3220,92 @@ _(no dependents \u2014 file is isolated)_`);
|
|
|
3216
3220
|
}
|
|
3217
3221
|
return textContent(lines.join("\n"));
|
|
3218
3222
|
}
|
|
3223
|
+
function blastRadiusSymbol(targetRaw, maxDepth, ctx) {
|
|
3224
|
+
const [rawFile, rawSym] = targetRaw.split("::", 2);
|
|
3225
|
+
const filePath = (rawFile ?? "").trim();
|
|
3226
|
+
const symName = (rawSym ?? "").trim();
|
|
3227
|
+
if (!symName) return errorContent("blast_radius: 'file::symbol' target needs a symbol name");
|
|
3228
|
+
const resolved = resolveFileTarget(ctx.graph, filePath);
|
|
3229
|
+
if ("ambiguous" in resolved) {
|
|
3230
|
+
const shown = resolved.ambiguous.slice(0, 5).join(", ");
|
|
3231
|
+
return errorContent(
|
|
3232
|
+
`blast_radius: '${filePath}' matches multiple files (${shown}). Pass a longer path.`
|
|
3233
|
+
);
|
|
3234
|
+
}
|
|
3235
|
+
if ("none" in resolved) return errorContent(`blast_radius: file not in graph: ${filePath}`);
|
|
3236
|
+
const fileNode = resolved.node;
|
|
3237
|
+
const symbol = ctx.graph.nodes.find(
|
|
3238
|
+
(n) => n.kind === "symbol" && n.file === fileNode.path && n.name === symName
|
|
3239
|
+
);
|
|
3240
|
+
if (!symbol)
|
|
3241
|
+
return errorContent(`blast_radius: symbol '${symName}' not found in ${fileNode.path}`);
|
|
3242
|
+
const callersBySym = /* @__PURE__ */ new Map();
|
|
3243
|
+
for (const e of ctx.graph.edges) {
|
|
3244
|
+
if (e.kind !== "calls" || e.from === e.to) continue;
|
|
3245
|
+
const list = callersBySym.get(e.to) ?? [];
|
|
3246
|
+
list.push(e.from);
|
|
3247
|
+
callersBySym.set(e.to, list);
|
|
3248
|
+
}
|
|
3249
|
+
const symById = /* @__PURE__ */ new Map();
|
|
3250
|
+
for (const n of ctx.graph.nodes) if (n.kind === "symbol") symById.set(n.id, n);
|
|
3251
|
+
const visited = /* @__PURE__ */ new Set([symbol.id]);
|
|
3252
|
+
const hits = [];
|
|
3253
|
+
let frontier = [symbol.id];
|
|
3254
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
3255
|
+
const next = [];
|
|
3256
|
+
for (const cur of frontier) {
|
|
3257
|
+
for (const fromId of callersBySym.get(cur) ?? []) {
|
|
3258
|
+
if (visited.has(fromId)) continue;
|
|
3259
|
+
visited.add(fromId);
|
|
3260
|
+
next.push(fromId);
|
|
3261
|
+
const s = symById.get(fromId);
|
|
3262
|
+
if (s) hits.push({ name: s.name, file: s.file, line: s.start_line, depth: d });
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
frontier = next;
|
|
3266
|
+
if (next.length === 0) break;
|
|
3267
|
+
}
|
|
3268
|
+
const header = `# Blast radius for ${fileNode.path}::${symbol.name} (callers, depth \u2264 ${maxDepth})`;
|
|
3269
|
+
if (hits.length === 0) {
|
|
3270
|
+
const tline2 = testsCoveringLine(ctx.graph, [fileNode.path]);
|
|
3271
|
+
return textContent(
|
|
3272
|
+
`${header}
|
|
3273
|
+
|
|
3274
|
+
_(no callers \u2014 safe to rename)_${tline2 ? `
|
|
3275
|
+
|
|
3276
|
+
${tline2}` : ""}`
|
|
3277
|
+
);
|
|
3278
|
+
}
|
|
3279
|
+
hits.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file) || a.line - b.line);
|
|
3280
|
+
const lines = [header, "", `${hits.length} caller symbol(s):`];
|
|
3281
|
+
for (const h of hits) lines.push(`- **depth ${h.depth}** \`${h.name}\` \u2192 ${h.file}:${h.line}`);
|
|
3282
|
+
const tline = testsCoveringLine(ctx.graph, [fileNode.path, ...hits.map((h) => h.file)]);
|
|
3283
|
+
if (tline) {
|
|
3284
|
+
lines.push("");
|
|
3285
|
+
lines.push(tline);
|
|
3286
|
+
}
|
|
3287
|
+
return textContent(lines.join("\n"));
|
|
3288
|
+
}
|
|
3289
|
+
function testsCoveringLine(graph, filePaths) {
|
|
3290
|
+
const fileByPath = /* @__PURE__ */ new Map();
|
|
3291
|
+
for (const n of graph.nodes) if (n.kind === "file") fileByPath.set(n.path, n);
|
|
3292
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3293
|
+
const tests = [];
|
|
3294
|
+
for (const p of new Set(filePaths)) {
|
|
3295
|
+
const fn = fileByPath.get(p);
|
|
3296
|
+
if (!fn) continue;
|
|
3297
|
+
for (const t of findTestsForFile(graph, fn)) {
|
|
3298
|
+
if (!seen.has(t.path)) {
|
|
3299
|
+
seen.add(t.path);
|
|
3300
|
+
tests.push(t.path);
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
if (tests.length === 0) return "";
|
|
3305
|
+
const shown = tests.slice(0, TESTS_MAX_FILES);
|
|
3306
|
+
const omitted = tests.length - shown.length;
|
|
3307
|
+
return `Tests covering the impact: ${shown.join(" \xB7 ")}${omitted > 0 ? ` \u2026+${omitted} more` : ""}`;
|
|
3308
|
+
}
|
|
3219
3309
|
var LIKELY_ENTRY_PATTERNS = [
|
|
3220
3310
|
/(?:^|\/)main\.[a-z0-9_]+$/i,
|
|
3221
3311
|
/(?:^|\/)index\.[a-z0-9_]+$/i,
|
|
@@ -3293,6 +3383,7 @@ function resolveFileTarget(graph, filePath) {
|
|
|
3293
3383
|
var DEPS_SIG_MAX = 140;
|
|
3294
3384
|
var DEPS_MAX_CALLEES = 10;
|
|
3295
3385
|
var DEPS_MAX_CALLERS = 12;
|
|
3386
|
+
var TESTS_MAX_FILES = 6;
|
|
3296
3387
|
function buildDepsFooter(symbol, graph, maxChars = loadConfig().readDepsMaxChars) {
|
|
3297
3388
|
const symById = /* @__PURE__ */ new Map();
|
|
3298
3389
|
for (const n of graph.nodes) if (n.kind === "symbol") symById.set(n.id, n);
|
|
@@ -3356,6 +3447,21 @@ function buildDepsFooter(symbol, graph, maxChars = loadConfig().readDepsMaxChars
|
|
|
3356
3447
|
}
|
|
3357
3448
|
return lines.join("\n");
|
|
3358
3449
|
}
|
|
3450
|
+
function buildTestsFooter(symbol, graph) {
|
|
3451
|
+
const fileNode = graph.nodes.find(
|
|
3452
|
+
(n) => n.kind === "file" && n.path === symbol.file
|
|
3453
|
+
);
|
|
3454
|
+
if (!fileNode) return "";
|
|
3455
|
+
const tests = findTestsForFile(graph, fileNode);
|
|
3456
|
+
if (tests.length > 0) {
|
|
3457
|
+
const shown = tests.slice(0, TESTS_MAX_FILES).map((t) => t.path);
|
|
3458
|
+
const omitted = tests.length - shown.length;
|
|
3459
|
+
const more = omitted > 0 ? ` \u2026+${omitted} more` : "";
|
|
3460
|
+
return `Tests (file-level): ${shown.join(" \xB7 ")}${more} \u2014 run after editing`;
|
|
3461
|
+
}
|
|
3462
|
+
if (isLikelyEntry(symbol.file)) return "";
|
|
3463
|
+
return "Tests: none linked to this file.";
|
|
3464
|
+
}
|
|
3359
3465
|
async function graphRead(args, ctx) {
|
|
3360
3466
|
const target = typeof args?.target === "string" ? args.target : "";
|
|
3361
3467
|
if (!target) return errorContent("graph_read: 'target' (string) is required");
|
|
@@ -3399,10 +3505,15 @@ ${fileNode.content}`);
|
|
|
3399
3505
|
|
|
3400
3506
|
---
|
|
3401
3507
|
${deps}` : "";
|
|
3508
|
+
const tests = buildTestsFooter(symbol, ctx.graph);
|
|
3509
|
+
const testsBlock = tests ? `
|
|
3510
|
+
|
|
3511
|
+
---
|
|
3512
|
+
${tests}` : "";
|
|
3402
3513
|
return textContent(
|
|
3403
3514
|
`# ${fileNode.path}::${symbol.name} (L${symbol.start_line}-${symbol.end_line})
|
|
3404
3515
|
|
|
3405
|
-
${body}${depsBlock}${editHint}`
|
|
3516
|
+
${body}${depsBlock}${testsBlock}${editHint}`
|
|
3406
3517
|
);
|
|
3407
3518
|
}
|
|
3408
3519
|
var editedFiles = /* @__PURE__ */ new Set();
|