@jefuriiij/synthra 0.10.0 → 0.12.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 +38 -0
- package/dist/cli/index.js +248 -7
- 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 +247 -6
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -1811,7 +1811,7 @@ import { basename as basename2 } from "path";
|
|
|
1811
1811
|
// src/hooks/claude-md.ts
|
|
1812
1812
|
import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
1813
1813
|
import { basename, dirname as dirname5 } from "path";
|
|
1814
|
-
var POLICY_VERSION =
|
|
1814
|
+
var POLICY_VERSION = 8;
|
|
1815
1815
|
var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
|
|
1816
1816
|
var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
|
|
1817
1817
|
var ANY_BLOCK_RE = /<!--\s*synthra-policy\s+v\d+\s+BEGIN\s*-->[\s\S]*?<!--\s*synthra-policy\s+v\d+\s+END\s*-->\s*/g;
|
|
@@ -1830,7 +1830,7 @@ function policyBlock() {
|
|
|
1830
1830
|
"> `mcp__synthra__graph_register_edit`. **Short names will NOT resolve**",
|
|
1831
1831
|
"> in ToolSearch or invocation \u2014 always use the full namespaced form.",
|
|
1832
1832
|
"> If the tools are deferred, load their schemas with ToolSearch:",
|
|
1833
|
-
"> `select:mcp__synthra__graph_continue,mcp__synthra__graph_read,mcp__synthra__graph_register_edit`.",
|
|
1833
|
+
"> `select:mcp__synthra__graph_continue,mcp__synthra__graph_read,mcp__synthra__graph_register_edit,mcp__synthra__find_symbol`.",
|
|
1834
1834
|
"> Below, short names (`graph_continue` etc.) appear in prose for",
|
|
1835
1835
|
"> readability only.",
|
|
1836
1836
|
"",
|
|
@@ -1844,6 +1844,10 @@ function policyBlock() {
|
|
|
1844
1844
|
" symbol is ~50 tokens, reading a whole file is thousands.",
|
|
1845
1845
|
"- **`graph_register_edit(files)`** \u2014 after you edit files, call this so",
|
|
1846
1846
|
" subsequent turns weight your changes and avoid stale snapshots.",
|
|
1847
|
+
"- **`find_symbol(name)`** \u2014 **reuse-first**: before writing a new helper,",
|
|
1848
|
+
" util, or function, call this to check whether one already exists. If it",
|
|
1849
|
+
" returns matches, reuse or extend them instead of re-implementing; only",
|
|
1850
|
+
' "no match \u2014 safe to create" means it is genuinely new.',
|
|
1847
1851
|
"",
|
|
1848
1852
|
"### When to call `graph_continue` \u2014 and when to skip",
|
|
1849
1853
|
"",
|
|
@@ -3106,11 +3110,14 @@ var TOOLS = [
|
|
|
3106
3110
|
},
|
|
3107
3111
|
{
|
|
3108
3112
|
name: "blast_radius",
|
|
3109
|
-
description: "
|
|
3113
|
+
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
3114
|
inputSchema: {
|
|
3111
3115
|
type: "object",
|
|
3112
3116
|
properties: {
|
|
3113
|
-
target: {
|
|
3117
|
+
target: {
|
|
3118
|
+
type: "string",
|
|
3119
|
+
description: "File path (file-level dependents) or 'file::symbol' (caller symbols)."
|
|
3120
|
+
},
|
|
3114
3121
|
depth: { type: "number", description: "Max hops to traverse. Default 3." }
|
|
3115
3122
|
},
|
|
3116
3123
|
required: ["target"]
|
|
@@ -3125,6 +3132,27 @@ var TOOLS = [
|
|
|
3125
3132
|
limit: { type: "number", description: "Cap on returned files. Default 50." }
|
|
3126
3133
|
}
|
|
3127
3134
|
}
|
|
3135
|
+
},
|
|
3136
|
+
{
|
|
3137
|
+
name: "find_symbol",
|
|
3138
|
+
description: "Find existing symbols by name BEFORE writing a new one \u2014 reuse beats re-implementing. Returns exact-name definitions (signatures + graph_read targets) or, if none, similarly-named symbols. 'No symbol matching \u2026 \u2014 safe to create' means it's genuinely new.",
|
|
3139
|
+
inputSchema: {
|
|
3140
|
+
type: "object",
|
|
3141
|
+
properties: {
|
|
3142
|
+
name: { type: "string", description: "Symbol name (or near-name) to look for." }
|
|
3143
|
+
},
|
|
3144
|
+
required: ["name"]
|
|
3145
|
+
}
|
|
3146
|
+
},
|
|
3147
|
+
{
|
|
3148
|
+
name: "duplicate_symbols",
|
|
3149
|
+
description: "List symbol names defined in more than one file (functions/classes/types; methods excluded) \u2014 consolidation candidates for review. Advisory: duplicates may be intentional.",
|
|
3150
|
+
inputSchema: {
|
|
3151
|
+
type: "object",
|
|
3152
|
+
properties: {
|
|
3153
|
+
limit: { type: "number", description: "Cap on returned names. Default 30." }
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3128
3156
|
}
|
|
3129
3157
|
];
|
|
3130
3158
|
async function callTool(name, args, ctx) {
|
|
@@ -3147,6 +3175,10 @@ async function callTool(name, args, ctx) {
|
|
|
3147
3175
|
return blastRadius(args, ctx);
|
|
3148
3176
|
case "dead_code":
|
|
3149
3177
|
return deadCode(args, ctx);
|
|
3178
|
+
case "find_symbol":
|
|
3179
|
+
return findSymbol(args, ctx);
|
|
3180
|
+
case "duplicate_symbols":
|
|
3181
|
+
return duplicateSymbols(args, ctx);
|
|
3150
3182
|
default:
|
|
3151
3183
|
return errorContent(`Unknown tool: ${name}`);
|
|
3152
3184
|
}
|
|
@@ -3161,7 +3193,8 @@ function blastRadius(args, ctx) {
|
|
|
3161
3193
|
const targetRaw = typeof args?.target === "string" ? args.target.trim() : "";
|
|
3162
3194
|
const maxDepth = typeof args?.depth === "number" && args.depth > 0 ? Math.floor(args.depth) : 3;
|
|
3163
3195
|
if (!targetRaw) return errorContent("blast_radius: 'target' (string) is required");
|
|
3164
|
-
|
|
3196
|
+
if (targetRaw.includes("::")) return blastRadiusSymbol(targetRaw, maxDepth, ctx);
|
|
3197
|
+
const filePath = targetRaw;
|
|
3165
3198
|
const root = ctx.graph.nodes.find((n) => n.kind === "file" && n.path === filePath);
|
|
3166
3199
|
if (!root) return errorContent(`blast_radius: file not in graph: ${filePath}`);
|
|
3167
3200
|
const fileIdBySymbol = /* @__PURE__ */ new Map();
|
|
@@ -3216,6 +3249,92 @@ _(no dependents \u2014 file is isolated)_`);
|
|
|
3216
3249
|
}
|
|
3217
3250
|
return textContent(lines.join("\n"));
|
|
3218
3251
|
}
|
|
3252
|
+
function blastRadiusSymbol(targetRaw, maxDepth, ctx) {
|
|
3253
|
+
const [rawFile, rawSym] = targetRaw.split("::", 2);
|
|
3254
|
+
const filePath = (rawFile ?? "").trim();
|
|
3255
|
+
const symName = (rawSym ?? "").trim();
|
|
3256
|
+
if (!symName) return errorContent("blast_radius: 'file::symbol' target needs a symbol name");
|
|
3257
|
+
const resolved = resolveFileTarget(ctx.graph, filePath);
|
|
3258
|
+
if ("ambiguous" in resolved) {
|
|
3259
|
+
const shown = resolved.ambiguous.slice(0, 5).join(", ");
|
|
3260
|
+
return errorContent(
|
|
3261
|
+
`blast_radius: '${filePath}' matches multiple files (${shown}). Pass a longer path.`
|
|
3262
|
+
);
|
|
3263
|
+
}
|
|
3264
|
+
if ("none" in resolved) return errorContent(`blast_radius: file not in graph: ${filePath}`);
|
|
3265
|
+
const fileNode = resolved.node;
|
|
3266
|
+
const symbol = ctx.graph.nodes.find(
|
|
3267
|
+
(n) => n.kind === "symbol" && n.file === fileNode.path && n.name === symName
|
|
3268
|
+
);
|
|
3269
|
+
if (!symbol)
|
|
3270
|
+
return errorContent(`blast_radius: symbol '${symName}' not found in ${fileNode.path}`);
|
|
3271
|
+
const callersBySym = /* @__PURE__ */ new Map();
|
|
3272
|
+
for (const e of ctx.graph.edges) {
|
|
3273
|
+
if (e.kind !== "calls" || e.from === e.to) continue;
|
|
3274
|
+
const list = callersBySym.get(e.to) ?? [];
|
|
3275
|
+
list.push(e.from);
|
|
3276
|
+
callersBySym.set(e.to, list);
|
|
3277
|
+
}
|
|
3278
|
+
const symById = /* @__PURE__ */ new Map();
|
|
3279
|
+
for (const n of ctx.graph.nodes) if (n.kind === "symbol") symById.set(n.id, n);
|
|
3280
|
+
const visited = /* @__PURE__ */ new Set([symbol.id]);
|
|
3281
|
+
const hits = [];
|
|
3282
|
+
let frontier = [symbol.id];
|
|
3283
|
+
for (let d = 1; d <= maxDepth; d++) {
|
|
3284
|
+
const next = [];
|
|
3285
|
+
for (const cur of frontier) {
|
|
3286
|
+
for (const fromId of callersBySym.get(cur) ?? []) {
|
|
3287
|
+
if (visited.has(fromId)) continue;
|
|
3288
|
+
visited.add(fromId);
|
|
3289
|
+
next.push(fromId);
|
|
3290
|
+
const s = symById.get(fromId);
|
|
3291
|
+
if (s) hits.push({ name: s.name, file: s.file, line: s.start_line, depth: d });
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
frontier = next;
|
|
3295
|
+
if (next.length === 0) break;
|
|
3296
|
+
}
|
|
3297
|
+
const header = `# Blast radius for ${fileNode.path}::${symbol.name} (callers, depth \u2264 ${maxDepth})`;
|
|
3298
|
+
if (hits.length === 0) {
|
|
3299
|
+
const tline2 = testsCoveringLine(ctx.graph, [fileNode.path]);
|
|
3300
|
+
return textContent(
|
|
3301
|
+
`${header}
|
|
3302
|
+
|
|
3303
|
+
_(no callers \u2014 safe to rename)_${tline2 ? `
|
|
3304
|
+
|
|
3305
|
+
${tline2}` : ""}`
|
|
3306
|
+
);
|
|
3307
|
+
}
|
|
3308
|
+
hits.sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file) || a.line - b.line);
|
|
3309
|
+
const lines = [header, "", `${hits.length} caller symbol(s):`];
|
|
3310
|
+
for (const h of hits) lines.push(`- **depth ${h.depth}** \`${h.name}\` \u2192 ${h.file}:${h.line}`);
|
|
3311
|
+
const tline = testsCoveringLine(ctx.graph, [fileNode.path, ...hits.map((h) => h.file)]);
|
|
3312
|
+
if (tline) {
|
|
3313
|
+
lines.push("");
|
|
3314
|
+
lines.push(tline);
|
|
3315
|
+
}
|
|
3316
|
+
return textContent(lines.join("\n"));
|
|
3317
|
+
}
|
|
3318
|
+
function testsCoveringLine(graph, filePaths) {
|
|
3319
|
+
const fileByPath = /* @__PURE__ */ new Map();
|
|
3320
|
+
for (const n of graph.nodes) if (n.kind === "file") fileByPath.set(n.path, n);
|
|
3321
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3322
|
+
const tests = [];
|
|
3323
|
+
for (const p of new Set(filePaths)) {
|
|
3324
|
+
const fn = fileByPath.get(p);
|
|
3325
|
+
if (!fn) continue;
|
|
3326
|
+
for (const t of findTestsForFile(graph, fn)) {
|
|
3327
|
+
if (!seen.has(t.path)) {
|
|
3328
|
+
seen.add(t.path);
|
|
3329
|
+
tests.push(t.path);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
if (tests.length === 0) return "";
|
|
3334
|
+
const shown = tests.slice(0, TESTS_MAX_FILES);
|
|
3335
|
+
const omitted = tests.length - shown.length;
|
|
3336
|
+
return `Tests covering the impact: ${shown.join(" \xB7 ")}${omitted > 0 ? ` \u2026+${omitted} more` : ""}`;
|
|
3337
|
+
}
|
|
3219
3338
|
var LIKELY_ENTRY_PATTERNS = [
|
|
3220
3339
|
/(?:^|\/)main\.[a-z0-9_]+$/i,
|
|
3221
3340
|
/(?:^|\/)index\.[a-z0-9_]+$/i,
|
|
@@ -3263,6 +3382,107 @@ _(no file is unreferenced \u2014 every file is either imported by another, has a
|
|
|
3263
3382
|
);
|
|
3264
3383
|
return textContent(lines.join("\n"));
|
|
3265
3384
|
}
|
|
3385
|
+
var FIND_MAX = 12;
|
|
3386
|
+
var FIND_SIG_MAX = 140;
|
|
3387
|
+
function symbolEntry(s) {
|
|
3388
|
+
const sig = s.signature.trim().slice(0, FIND_SIG_MAX);
|
|
3389
|
+
return `\u2022 ${sig} \u2192 mcp__synthra__graph_read("${s.file}::${s.name}") [${s.symbol_kind}, L${s.start_line}]`;
|
|
3390
|
+
}
|
|
3391
|
+
var byFileLine = (a, b) => a.file === b.file ? a.start_line - b.start_line : a.file < b.file ? -1 : 1;
|
|
3392
|
+
function findSymbol(args, ctx) {
|
|
3393
|
+
const name = typeof args?.name === "string" ? args.name.trim() : "";
|
|
3394
|
+
if (!name) return errorContent("find_symbol: 'name' (string) is required");
|
|
3395
|
+
const symbols = ctx.graph.nodes.filter((n) => n.kind === "symbol");
|
|
3396
|
+
const lower = name.toLowerCase();
|
|
3397
|
+
const exact = symbols.filter((s) => s.name === name);
|
|
3398
|
+
const exactHits = exact.length > 0 ? exact : symbols.filter((s) => s.name.toLowerCase() === lower);
|
|
3399
|
+
if (exactHits.length > 0) {
|
|
3400
|
+
const sorted = exactHits.slice().sort(byFileLine);
|
|
3401
|
+
const shown2 = sorted.slice(0, FIND_MAX);
|
|
3402
|
+
const omitted2 = sorted.length - shown2.length;
|
|
3403
|
+
const lines2 = [
|
|
3404
|
+
`# find_symbol: "${name}"`,
|
|
3405
|
+
"",
|
|
3406
|
+
`Exact matches (${sorted.length}) \u2014 reuse one of these instead of writing a new one:`,
|
|
3407
|
+
...shown2.map(symbolEntry)
|
|
3408
|
+
];
|
|
3409
|
+
if (omitted2 > 0) lines2.push(`\u2026+${omitted2} more`);
|
|
3410
|
+
return textContent(lines2.join("\n"));
|
|
3411
|
+
}
|
|
3412
|
+
const tokens = new Set(tokenizeQuery(name));
|
|
3413
|
+
const scored = symbols.map((s) => {
|
|
3414
|
+
const n = s.name.toLowerCase();
|
|
3415
|
+
let score2 = 0;
|
|
3416
|
+
if (n.includes(lower) || lower.includes(n)) score2 += 2;
|
|
3417
|
+
for (const t of tokens) if (n.includes(t)) score2 += 1;
|
|
3418
|
+
return { s, score: score2 };
|
|
3419
|
+
}).filter((x) => x.score > 0).sort((a, b) => b.score - a.score || byFileLine(a.s, b.s));
|
|
3420
|
+
if (scored.length === 0) {
|
|
3421
|
+
return textContent(
|
|
3422
|
+
`# find_symbol: "${name}"
|
|
3423
|
+
|
|
3424
|
+
No symbol matching "${name}" \u2014 safe to create.`
|
|
3425
|
+
);
|
|
3426
|
+
}
|
|
3427
|
+
const shown = scored.slice(0, FIND_MAX);
|
|
3428
|
+
const omitted = scored.length - shown.length;
|
|
3429
|
+
const lines = [
|
|
3430
|
+
`# find_symbol: "${name}"`,
|
|
3431
|
+
"",
|
|
3432
|
+
`No exact match. Similar names (${scored.length}) \u2014 reuse or extend one before writing new:`,
|
|
3433
|
+
...shown.map((x) => symbolEntry(x.s))
|
|
3434
|
+
];
|
|
3435
|
+
if (omitted > 0) lines.push(`\u2026+${omitted} more`);
|
|
3436
|
+
return textContent(lines.join("\n"));
|
|
3437
|
+
}
|
|
3438
|
+
var DUP_INCLUDE = /* @__PURE__ */ new Set([
|
|
3439
|
+
"function",
|
|
3440
|
+
"class",
|
|
3441
|
+
"interface",
|
|
3442
|
+
"type",
|
|
3443
|
+
"enum",
|
|
3444
|
+
"const",
|
|
3445
|
+
"component"
|
|
3446
|
+
]);
|
|
3447
|
+
function duplicateSymbols(args, ctx) {
|
|
3448
|
+
const limit = typeof args?.limit === "number" && args.limit > 0 ? Math.floor(args.limit) : 30;
|
|
3449
|
+
const defsByName = /* @__PURE__ */ new Map();
|
|
3450
|
+
const filesByName = /* @__PURE__ */ new Map();
|
|
3451
|
+
for (const n of ctx.graph.nodes) {
|
|
3452
|
+
if (n.kind !== "symbol" || !DUP_INCLUDE.has(n.symbol_kind)) continue;
|
|
3453
|
+
(defsByName.get(n.name) ?? defsByName.set(n.name, []).get(n.name)).push({
|
|
3454
|
+
file: n.file,
|
|
3455
|
+
line: n.start_line
|
|
3456
|
+
});
|
|
3457
|
+
(filesByName.get(n.name) ?? filesByName.set(n.name, /* @__PURE__ */ new Set()).get(n.name)).add(n.file);
|
|
3458
|
+
}
|
|
3459
|
+
const dups = [...defsByName.entries()].filter(([name]) => (filesByName.get(name)?.size ?? 0) >= 2).map(([name, defs]) => ({
|
|
3460
|
+
name,
|
|
3461
|
+
defs: defs.slice().sort((a, b) => a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1)
|
|
3462
|
+
})).sort((a, b) => b.defs.length - a.defs.length || a.name.localeCompare(b.name));
|
|
3463
|
+
if (dups.length === 0) {
|
|
3464
|
+
return textContent(
|
|
3465
|
+
"# Duplicate symbols\n\n_(no top-level symbol name is defined in more than one file)_"
|
|
3466
|
+
);
|
|
3467
|
+
}
|
|
3468
|
+
const shown = dups.slice(0, limit);
|
|
3469
|
+
const lines = [
|
|
3470
|
+
"# Duplicate symbols (consolidation candidates)",
|
|
3471
|
+
"",
|
|
3472
|
+
`${shown.length} of ${dups.length} name(s) defined in multiple files (functions/classes/types; methods excluded):`,
|
|
3473
|
+
""
|
|
3474
|
+
];
|
|
3475
|
+
for (const d of shown) {
|
|
3476
|
+
lines.push(
|
|
3477
|
+
`- \`${d.name}\` (${d.defs.length}): ${d.defs.map((x) => `${x.file}:${x.line}`).join(" \xB7 ")}`
|
|
3478
|
+
);
|
|
3479
|
+
}
|
|
3480
|
+
lines.push("");
|
|
3481
|
+
lines.push(
|
|
3482
|
+
"_advisory: the same name in multiple files may be intentional \u2014 verify before consolidating._"
|
|
3483
|
+
);
|
|
3484
|
+
return textContent(lines.join("\n"));
|
|
3485
|
+
}
|
|
3266
3486
|
async function graphContinue(args, ctx) {
|
|
3267
3487
|
const query = typeof args?.query === "string" ? args.query : "";
|
|
3268
3488
|
if (!query) return errorContent("graph_continue: 'query' (string) is required");
|
|
@@ -3293,6 +3513,7 @@ function resolveFileTarget(graph, filePath) {
|
|
|
3293
3513
|
var DEPS_SIG_MAX = 140;
|
|
3294
3514
|
var DEPS_MAX_CALLEES = 10;
|
|
3295
3515
|
var DEPS_MAX_CALLERS = 12;
|
|
3516
|
+
var TESTS_MAX_FILES = 6;
|
|
3296
3517
|
function buildDepsFooter(symbol, graph, maxChars = loadConfig().readDepsMaxChars) {
|
|
3297
3518
|
const symById = /* @__PURE__ */ new Map();
|
|
3298
3519
|
for (const n of graph.nodes) if (n.kind === "symbol") symById.set(n.id, n);
|
|
@@ -3356,6 +3577,21 @@ function buildDepsFooter(symbol, graph, maxChars = loadConfig().readDepsMaxChars
|
|
|
3356
3577
|
}
|
|
3357
3578
|
return lines.join("\n");
|
|
3358
3579
|
}
|
|
3580
|
+
function buildTestsFooter(symbol, graph) {
|
|
3581
|
+
const fileNode = graph.nodes.find(
|
|
3582
|
+
(n) => n.kind === "file" && n.path === symbol.file
|
|
3583
|
+
);
|
|
3584
|
+
if (!fileNode) return "";
|
|
3585
|
+
const tests = findTestsForFile(graph, fileNode);
|
|
3586
|
+
if (tests.length > 0) {
|
|
3587
|
+
const shown = tests.slice(0, TESTS_MAX_FILES).map((t) => t.path);
|
|
3588
|
+
const omitted = tests.length - shown.length;
|
|
3589
|
+
const more = omitted > 0 ? ` \u2026+${omitted} more` : "";
|
|
3590
|
+
return `Tests (file-level): ${shown.join(" \xB7 ")}${more} \u2014 run after editing`;
|
|
3591
|
+
}
|
|
3592
|
+
if (isLikelyEntry(symbol.file)) return "";
|
|
3593
|
+
return "Tests: none linked to this file.";
|
|
3594
|
+
}
|
|
3359
3595
|
async function graphRead(args, ctx) {
|
|
3360
3596
|
const target = typeof args?.target === "string" ? args.target : "";
|
|
3361
3597
|
if (!target) return errorContent("graph_read: 'target' (string) is required");
|
|
@@ -3399,10 +3635,15 @@ ${fileNode.content}`);
|
|
|
3399
3635
|
|
|
3400
3636
|
---
|
|
3401
3637
|
${deps}` : "";
|
|
3638
|
+
const tests = buildTestsFooter(symbol, ctx.graph);
|
|
3639
|
+
const testsBlock = tests ? `
|
|
3640
|
+
|
|
3641
|
+
---
|
|
3642
|
+
${tests}` : "";
|
|
3402
3643
|
return textContent(
|
|
3403
3644
|
`# ${fileNode.path}::${symbol.name} (L${symbol.start_line}-${symbol.end_line})
|
|
3404
3645
|
|
|
3405
|
-
${body}${depsBlock}${editHint}`
|
|
3646
|
+
${body}${depsBlock}${testsBlock}${editHint}`
|
|
3406
3647
|
);
|
|
3407
3648
|
}
|
|
3408
3649
|
var editedFiles = /* @__PURE__ */ new Set();
|