dsh-codebase-chat 0.23.0 → 0.24.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/dist/index.d.ts CHANGED
@@ -79,6 +79,7 @@ declare function getIndex(projectPath: string, progress?: (message: string) => v
79
79
 
80
80
  declare function resolveProjectPath(projectPath?: string): string;
81
81
  declare function findProjectRoot(absProject: string): Promise<string>;
82
+ declare function getCacheDir(): string;
82
83
 
83
84
  /**
84
85
  * Count the exact number of tokens for a given text using the CL100K base
@@ -171,13 +172,52 @@ interface HealthReport {
171
172
  grade: 'A' | 'B' | 'C' | 'D' | 'E';
172
173
  }
173
174
  interface AnalyzeOptions {
174
- /** Restrict analysis to these project-relative paths (e.g. a diff scope). */
175
+ /** Restrict *reported findings* to these project-relative paths (e.g. a diff
176
+ * scope). The import graph and usage checks still run on the whole project
177
+ * so cycles/uses crossing the scope boundary are detected. */
175
178
  files?: Set<string>;
176
179
  }
177
180
  declare function analyzeProject(projectPath: string, opts?: AnalyzeOptions): Promise<HealthReport>;
178
181
  declare function formatHealthReport(r: HealthReport, lang?: 'fr' | 'en'): string;
179
182
  declare function formatHealthReportMd(r: HealthReport, lang?: 'fr' | 'en'): string;
180
183
 
184
+ interface ImpactDependent {
185
+ file: string;
186
+ depth: number;
187
+ }
188
+ interface ImpactReport {
189
+ projectPath: string;
190
+ query: string;
191
+ target: string;
192
+ exportedSymbols: {
193
+ name: string;
194
+ line: number;
195
+ }[];
196
+ dependents: ImpactDependent[];
197
+ directCount: number;
198
+ totalFiles: number;
199
+ /** Share of the project's code files that transitively depend on the target. */
200
+ percent: number;
201
+ inCycle: boolean;
202
+ isEntry: boolean;
203
+ risk: 'low' | 'medium' | 'high';
204
+ }
205
+ type ImpactResult = {
206
+ ok: true;
207
+ report: ImpactReport;
208
+ } | {
209
+ ok: false;
210
+ query: string;
211
+ candidates: string[];
212
+ };
213
+ /**
214
+ * Blast-radius analysis: which files break if `query` changes. Reverse BFS on
215
+ * the local import graph — deterministic, no LLM.
216
+ */
217
+ declare function analyzeImpact(projectPath: string, query: string): Promise<ImpactResult>;
218
+ declare function formatImpactReport(r: ImpactReport, lang?: 'fr' | 'en'): string;
219
+ declare function formatImpactReportMd(r: ImpactReport, lang?: 'fr' | 'en'): string;
220
+
181
221
  type ConfigLang = 'fr' | 'en';
182
222
  /**
183
223
  * Per-project settings read from `.codebase-chat.json` at the project root.
@@ -221,4 +261,4 @@ interface DiffScope {
221
261
  */
222
262
  declare function getChangedFiles(absProject: string, base: string): Promise<DiffScope>;
223
263
 
224
- export { CONFIG_FILE, type CloneGroup, type CodeChunk, type CodeIndex, type ConfigLang, type ContextOptions, type ContextResult, type Cycle, type DiffScope, type HealthReport, type Hotspot, type IndexedFile, type InvertedIndex, type ProjectConfig, type UnusedExport, analyzeProject, buildContext, buildIndex, chunkByTokens, clearConfigCache, cosineSimilarity, countTokens, disposeTreeSitter, embedIndex, ensureTreeSitterForExt, extractChunks, findProjectRoot, formatHealthReport, formatHealthReportMd, getChangedFiles, getEmbedding, getEmbeddings, getExtractor, getIndex, globToRegExp, initTreeSitter, loadIndex, loadProjectConfig, matchesAnyGlob, resolveProjectPath, saveIndex, scoreChunks, selectChunks, treeSitterReady, truncateToTokens };
264
+ export { CONFIG_FILE, type CloneGroup, type CodeChunk, type CodeIndex, type ConfigLang, type ContextOptions, type ContextResult, type Cycle, type DiffScope, type HealthReport, type Hotspot, type ImpactDependent, type ImpactReport, type ImpactResult, type IndexedFile, type InvertedIndex, type ProjectConfig, type UnusedExport, analyzeImpact, analyzeProject, buildContext, buildIndex, chunkByTokens, clearConfigCache, cosineSimilarity, countTokens, disposeTreeSitter, embedIndex, ensureTreeSitterForExt, extractChunks, findProjectRoot, formatHealthReport, formatHealthReportMd, formatImpactReport, formatImpactReportMd, getCacheDir, getChangedFiles, getEmbedding, getEmbeddings, getExtractor, getIndex, globToRegExp, initTreeSitter, loadIndex, loadProjectConfig, matchesAnyGlob, resolveProjectPath, saveIndex, scoreChunks, selectChunks, treeSitterReady, truncateToTokens };
package/dist/index.js CHANGED
@@ -647,7 +647,12 @@ var DEFAULT_SKIP_DIRS = /* @__PURE__ */ new Set([
647
647
  ".vscode",
648
648
  "__pycache__",
649
649
  ".dsh-tmp",
650
- ".dsh-vision-router"
650
+ ".dsh-vision-router",
651
+ ".agents",
652
+ ".claude",
653
+ ".devin",
654
+ ".playwright-mcp",
655
+ ".windsurf"
651
656
  ]);
652
657
  var DEFAULT_SKIP_FILES = /* @__PURE__ */ new Set([]);
653
658
  async function getWalkOptions(absProject) {
@@ -662,11 +667,10 @@ function projectHash(absProject) {
662
667
  return createHash("sha256").update(absProject.toLowerCase()).digest("hex").slice(0, 16);
663
668
  }
664
669
  function resolveProjectPath(projectPath) {
665
- const raw = (projectPath ?? "").trim().toLowerCase().replace(/['"]/g, "");
666
- if (raw === "dako") return "D:\\Nouveau dossier";
667
- if (!projectPath) return process.cwd();
668
- if (isAbsolute(projectPath)) return resolve(projectPath);
669
- return resolve(process.cwd(), projectPath);
670
+ const raw = (projectPath ?? "").trim().replace(/['"]/g, "");
671
+ if (!raw) return process.cwd();
672
+ if (isAbsolute(raw)) return resolve(raw);
673
+ return resolve(process.cwd(), raw);
670
674
  }
671
675
  async function findProjectRoot(absProject) {
672
676
  try {
@@ -1102,9 +1106,12 @@ function lexicalScore(index, query) {
1102
1106
  const terms = tokenizeQuery(query);
1103
1107
  const scores = /* @__PURE__ */ new Map();
1104
1108
  if (terms.length === 0) return scores;
1109
+ const totalFiles = Math.max(Object.keys(index.files).length, 1);
1105
1110
  for (const term of terms) {
1106
1111
  const posting = index.terms[term];
1107
1112
  if (!posting) continue;
1113
+ const df = Object.keys(posting).length;
1114
+ const idf = Math.log(1 + totalFiles / df);
1108
1115
  for (const [relPath, count] of Object.entries(posting)) {
1109
1116
  const file = index.files[relPath];
1110
1117
  if (!file) continue;
@@ -1115,7 +1122,7 @@ function lexicalScore(index, query) {
1115
1122
  if (!contentHit && !nameHit) continue;
1116
1123
  const bonus = (nameHit ? 4 : 0) + (chunk.kind === "function" || chunk.kind === "method" ? 1 : 0) + (contentHit ? 1 : 0);
1117
1124
  const prev = scores.get(key) ?? 0;
1118
- scores.set(key, prev + count + bonus);
1125
+ scores.set(key, prev + count * idf + bonus);
1119
1126
  }
1120
1127
  }
1121
1128
  }
@@ -1220,12 +1227,23 @@ function getLabels(lang) {
1220
1227
  };
1221
1228
  }
1222
1229
  async function extractProductConstraints(absProject) {
1223
- const candidates = ["README.md", "README.MD", "readme.md", "MEMORY.md", "CONTRIBUTING.md"];
1230
+ const candidates = [
1231
+ "README.md",
1232
+ "README.MD",
1233
+ "readme.md",
1234
+ "MEMORY.md",
1235
+ "CONTRIBUTING.md",
1236
+ "AGENTS.md",
1237
+ "CLAUDE.md",
1238
+ ".windsurfrules",
1239
+ ".cursorrules",
1240
+ ".cursorrules.md"
1241
+ ];
1224
1242
  const constraints = [];
1225
1243
  for (const name of candidates) {
1226
1244
  const text = await safeReadText(join5(absProject, name));
1227
1245
  if (!text) continue;
1228
- const regex = /(?:constraint|contrainte|must|doit|interdit|forbidden|rule|règle|limitation)[\s\S]{0,200}/gi;
1246
+ const regex = /(?:constraint|contrainte|must|doit|interdit|forbidden|rule|règle|limitation|never|always|jamais|toujours|zéro|zero)[\s\S]{0,200}/gi;
1229
1247
  let m;
1230
1248
  while ((m = regex.exec(text)) !== null) {
1231
1249
  const line = m[0].replace(/\s+/g, " ").trim();
@@ -1381,36 +1399,58 @@ function parseExports(text) {
1381
1399
  }
1382
1400
  function findCycles(edges) {
1383
1401
  const adj = /* @__PURE__ */ new Map();
1402
+ const nodes = /* @__PURE__ */ new Set();
1384
1403
  for (const e of edges) {
1404
+ nodes.add(e.from);
1405
+ nodes.add(e.to);
1385
1406
  if (!adj.has(e.from)) adj.set(e.from, []);
1386
1407
  adj.get(e.from).push(e.to);
1387
1408
  }
1388
- const cycles = [];
1389
- const seen = /* @__PURE__ */ new Set();
1390
- const stack = [];
1409
+ const index = /* @__PURE__ */ new Map();
1410
+ const low = /* @__PURE__ */ new Map();
1391
1411
  const onStack = /* @__PURE__ */ new Set();
1392
- function dfs(node) {
1393
- stack.push(node);
1394
- onStack.add(node);
1395
- for (const next of adj.get(node) ?? []) {
1396
- if (onStack.has(next)) {
1397
- const cycle = stack.slice(stack.indexOf(next)).concat(next);
1398
- const body = cycle.slice(0, -1);
1399
- const minIdx = body.indexOf(body.reduce((a, b) => a < b ? a : b));
1400
- const key = body.slice(minIdx).concat(body.slice(0, minIdx)).join(">");
1401
- if (!seen.has(key)) {
1402
- seen.add(key);
1403
- cycles.push({ path: cycle });
1412
+ const stack = [];
1413
+ const sccs = [];
1414
+ let counter = 0;
1415
+ for (const root of nodes) {
1416
+ if (index.has(root)) continue;
1417
+ const work = [[root, 0]];
1418
+ while (work.length) {
1419
+ const top = work[work.length - 1];
1420
+ const [v, ci] = top;
1421
+ if (ci === 0) {
1422
+ index.set(v, counter);
1423
+ low.set(v, counter);
1424
+ counter++;
1425
+ stack.push(v);
1426
+ onStack.add(v);
1427
+ }
1428
+ const children = adj.get(v) ?? [];
1429
+ if (ci < children.length) {
1430
+ top[1] = ci + 1;
1431
+ const w = children[ci];
1432
+ if (!index.has(w)) work.push([w, 0]);
1433
+ else if (onStack.has(w)) low.set(v, Math.min(low.get(v), index.get(w)));
1434
+ } else {
1435
+ work.pop();
1436
+ if (work.length) {
1437
+ const parent = work[work.length - 1][0];
1438
+ low.set(parent, Math.min(low.get(parent), low.get(v)));
1439
+ }
1440
+ if (low.get(v) === index.get(v)) {
1441
+ const scc = [];
1442
+ let w;
1443
+ do {
1444
+ w = stack.pop();
1445
+ onStack.delete(w);
1446
+ scc.push(w);
1447
+ } while (w !== v);
1448
+ if (scc.length > 1 || (adj.get(v) ?? []).includes(v)) sccs.push(scc);
1404
1449
  }
1405
- } else if (!stack.includes(next)) {
1406
- dfs(next);
1407
1450
  }
1408
1451
  }
1409
- stack.pop();
1410
- onStack.delete(node);
1411
1452
  }
1412
- for (const n of adj.keys()) dfs(n);
1413
- return cycles;
1453
+ return sccs.map((m) => ({ path: m.sort() })).sort((a, b) => b.path.length - a.path.length);
1414
1454
  }
1415
1455
  function looksLikeEntry(rel, pkg) {
1416
1456
  const base = basename(rel).toLowerCase().replace(extname2(rel), "");
@@ -1453,7 +1493,7 @@ function findDuplicates(fileTexts) {
1453
1493
  }
1454
1494
  return [...groups.values()].sort((a, b) => b.lines - a.lines).slice(0, 15);
1455
1495
  }
1456
- async function analyzeProject(projectPath, opts = {}) {
1496
+ async function collectImportGraph(projectPath) {
1457
1497
  const abs = await findProjectRoot(resolveProjectPath(projectPath));
1458
1498
  const fileTexts = /* @__PURE__ */ new Map();
1459
1499
  const codeFiles = [];
@@ -1462,17 +1502,11 @@ async function analyzeProject(projectPath, opts = {}) {
1462
1502
  const rel = relative3(abs, full).split(sep4).join("/");
1463
1503
  const ext = extname2(rel).toLowerCase();
1464
1504
  if (!CODE_EXTS.has(ext) || SKIP_EXTS.has(ext) || rel.includes(".min.")) continue;
1465
- if (opts.files && !opts.files.has(rel)) continue;
1466
1505
  const text = await safeReadText(full);
1467
1506
  if (!text) continue;
1468
1507
  codeFiles.push(rel);
1469
1508
  fileTexts.set(rel, text);
1470
1509
  }
1471
- let pkg = {};
1472
- try {
1473
- pkg = JSON.parse(await readFile4(join6(abs, "package.json"), "utf8"));
1474
- } catch {
1475
- }
1476
1510
  const known = new Set(codeFiles);
1477
1511
  const edges = [];
1478
1512
  const inDegree = /* @__PURE__ */ new Map();
@@ -1482,18 +1516,32 @@ async function analyzeProject(projectPath, opts = {}) {
1482
1516
  inDegree.set(to, (inDegree.get(to) ?? 0) + 1);
1483
1517
  }
1484
1518
  }
1485
- const cycles = findCycles(edges);
1486
- const unusedFiles = codeFiles.filter((rel) => !inDegree.has(rel) && !looksLikeEntry(rel, pkg)).sort();
1487
- const otherText = /* @__PURE__ */ new Map();
1488
- for (const [file, text] of fileTexts) otherText.set(file, text);
1519
+ return { abs, codeFiles, fileTexts, edges, inDegree };
1520
+ }
1521
+ async function analyzeProject(projectPath, opts = {}) {
1522
+ const { abs, codeFiles, fileTexts, edges, inDegree } = await collectImportGraph(projectPath);
1523
+ const scope = opts.files;
1524
+ const scopedFiles = scope ? codeFiles.filter((f) => scope.has(f)) : codeFiles;
1525
+ let pkg = {};
1526
+ try {
1527
+ pkg = JSON.parse(await readFile4(join6(abs, "package.json"), "utf8"));
1528
+ } catch {
1529
+ }
1530
+ const cycles = findCycles(edges).filter((c) => !scope || c.path.some((node) => scope.has(node)));
1531
+ const unusedFiles = scopedFiles.filter((rel) => !inDegree.has(rel) && !looksLikeEntry(rel, pkg)).sort();
1532
+ const IDENT_RE = /[A-Za-z_$][\w$]*/g;
1533
+ const identifiersByFile = /* @__PURE__ */ new Map();
1534
+ for (const [file, text] of fileTexts) {
1535
+ identifiersByFile.set(file, new Set(text.match(IDENT_RE) ?? []));
1536
+ }
1489
1537
  const unusedExports = [];
1490
- for (const rel of codeFiles) {
1538
+ for (const rel of scopedFiles) {
1491
1539
  for (const exp of parseExports(fileTexts.get(rel))) {
1492
1540
  if (exp.name === "default") continue;
1493
1541
  let used = false;
1494
- for (const [otherFile, text] of fileTexts) {
1542
+ for (const [otherFile, ids] of identifiersByFile) {
1495
1543
  if (otherFile === rel) continue;
1496
- if (new RegExp(`\\b${exp.name.replace(/[$_]/g, "\\$&")}\\b`).test(text)) {
1544
+ if (ids.has(exp.name)) {
1497
1545
  used = true;
1498
1546
  break;
1499
1547
  }
@@ -1501,21 +1549,22 @@ async function analyzeProject(projectPath, opts = {}) {
1501
1549
  if (!used) unusedExports.push({ file: rel, name: exp.name, line: exp.line });
1502
1550
  }
1503
1551
  }
1504
- const duplicates = findDuplicates(fileTexts);
1552
+ const duplicates = findDuplicates(fileTexts).filter((g) => !scope || g.files.some((f) => scope.has(f)));
1505
1553
  const hotspots = [];
1506
- for (const [rel, text] of fileTexts) {
1507
- const score2 = complexityOf(text);
1554
+ for (const rel of scopedFiles) {
1555
+ const score2 = complexityOf(fileTexts.get(rel));
1508
1556
  if (score2 >= 12) hotspots.push({ file: rel, startLine: 1, score: score2 });
1509
1557
  }
1510
1558
  hotspots.sort((a, b) => b.score - a.score);
1511
- const codeLines = [...fileTexts.values()].reduce((s, t2) => s + t2.split("\n").length, 0);
1559
+ const codeLines = scopedFiles.reduce((s, f) => s + fileTexts.get(f).split("\n").length, 0);
1512
1560
  const dupLines = duplicates.reduce((s, g) => s + g.lines, 0);
1513
- const penalties = cycles.length * 6 + unusedFiles.length * 2 + Math.min(unusedExports.length, 20) * 1 + Math.round(dupLines / Math.max(codeLines, 1) * 100) + Math.min(hotspots.length, 15) * 2;
1561
+ const cyclesPenalty = cycles.reduce((s, c) => s + c.path.length, 0) * 2;
1562
+ const penalties = cyclesPenalty + unusedFiles.length * 2 + Math.min(unusedExports.length, 20) * 1 + Math.round(dupLines / Math.max(codeLines, 1) * 100) + Math.min(hotspots.length, 15) * 2;
1514
1563
  const score = Math.max(0, Math.min(100, 100 - penalties));
1515
1564
  const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 65 ? "C" : score >= 50 ? "D" : "E";
1516
1565
  return {
1517
1566
  projectPath: abs,
1518
- analyzedFiles: codeFiles.length,
1567
+ analyzedFiles: scopedFiles.length,
1519
1568
  importEdges: edges.length,
1520
1569
  cycles,
1521
1570
  unusedFiles,
@@ -1557,7 +1606,7 @@ function formatHealthReport(r, lang = "fr") {
1557
1606
  out.push(`${t2.score}: ${r.score}/100 (${r.grade}) \xB7 ${r.analyzedFiles} ${t2.files} \xB7 ${r.importEdges} ${t2.edges}`);
1558
1607
  out.push("");
1559
1608
  out.push(`\u25CF ${t2.cycles} (${r.cycles.length})`);
1560
- for (const c of r.cycles.slice(0, 10)) out.push(` ${c.path.join(" \u2192 ")}`);
1609
+ for (const c of r.cycles.slice(0, 10)) out.push(` ${formatCycle(c)}`);
1561
1610
  if (r.cycles.length === 0) out.push(` ${t2.none}`);
1562
1611
  out.push("");
1563
1612
  out.push(`\u25CF ${t2.unusedFiles} (${r.unusedFiles.length}) \u2014 ${t2.noteUnused}`);
@@ -1573,6 +1622,12 @@ function formatHealthReport(r, lang = "fr") {
1573
1622
  for (const h of r.hotspots.slice(0, 10)) out.push(` ${h.file} \u2014 score ${h.score}`);
1574
1623
  return out.join("\n");
1575
1624
  }
1625
+ function formatCycle(c, md = false) {
1626
+ const q = (s) => md ? `\`${s}\`` : s;
1627
+ if (c.path.length <= 4) return c.path.map(q).join(" \u2192 ") + " \u2192 " + q(c.path[0]);
1628
+ const shown = c.path.slice(0, 4).map(q).join(", ");
1629
+ return `${c.path.length} files: ${shown}, \u2026`;
1630
+ }
1576
1631
  var GRADE_ICON = { A: "\u{1F7E2}", B: "\u{1F535}", C: "\u{1F7E1}", D: "\u{1F7E0}", E: "\u{1F534}" };
1577
1632
  function scoreBar(score) {
1578
1633
  const filled = Math.round(score / 10);
@@ -1623,7 +1678,7 @@ function formatHealthReportMd(r, lang = "fr") {
1623
1678
  out.push("");
1624
1679
  out.push(`### ${t2.cycles} \u2014 ${r.cycles.length}`);
1625
1680
  if (r.cycles.length === 0) out.push(t2.none);
1626
- else for (const c of r.cycles.slice(0, 10)) out.push(`- \`${c.path.join("` \u2192 `")}\``);
1681
+ else for (const c of r.cycles.slice(0, 10)) out.push(`- ${formatCycle(c, true)}`);
1627
1682
  if (r.cycles.length > 10) out.push(t2.more(r.cycles.length - 10));
1628
1683
  out.push("");
1629
1684
  out.push(`### ${t2.unusedFiles} \u2014 ${r.unusedFiles.length}`);
@@ -1658,8 +1713,202 @@ function formatHealthReportMd(r, lang = "fr") {
1658
1713
  } else out.push(t2.none);
1659
1714
  return out.join("\n");
1660
1715
  }
1716
+
1717
+ // src/impact.ts
1718
+ import { basename as basename2 } from "path";
1719
+ import { readFile as readFile5 } from "fs/promises";
1720
+ import { join as join7 } from "path";
1721
+ function matchRank(rel, q) {
1722
+ if (rel === q) return 0;
1723
+ if (rel.endsWith(`/${q}`)) return 1;
1724
+ if (basename2(rel) === q) return 2;
1725
+ if (rel.toLowerCase().includes(q.toLowerCase())) return 3;
1726
+ return -1;
1727
+ }
1728
+ function resolveTarget(codeFiles, query) {
1729
+ const q = query.trim().replace(/\\/g, "/").replace(/^\.\//, "");
1730
+ if (!q) return { candidates: [] };
1731
+ const ranked = codeFiles.map((f) => ({ f, r: matchRank(f, q) })).filter((m) => m.r >= 0).sort((a, b) => a.r - b.r || a.f.length - b.f.length);
1732
+ if (!ranked.length) return { candidates: [] };
1733
+ const best = ranked.filter((m) => m.r === ranked[0].r);
1734
+ if (best.length === 1) return { target: best[0].f, candidates: [] };
1735
+ return { candidates: best.slice(0, 12).map((m) => m.f) };
1736
+ }
1737
+ async function analyzeImpact(projectPath, query) {
1738
+ const g = await collectImportGraph(projectPath);
1739
+ const { target, candidates } = resolveTarget(g.codeFiles, query);
1740
+ if (!target) return { ok: false, query, candidates };
1741
+ const rev = /* @__PURE__ */ new Map();
1742
+ for (const e of g.edges) {
1743
+ if (!rev.has(e.to)) rev.set(e.to, []);
1744
+ rev.get(e.to).push(e.from);
1745
+ }
1746
+ let inCycle = false;
1747
+ const queue = [target];
1748
+ let head = 0;
1749
+ const dist = /* @__PURE__ */ new Map([[target, 0]]);
1750
+ while (head < queue.length) {
1751
+ const node = queue[head++];
1752
+ const d = dist.get(node);
1753
+ for (const up of rev.get(node) ?? []) {
1754
+ if (up === target) {
1755
+ inCycle = true;
1756
+ continue;
1757
+ }
1758
+ if (dist.has(up)) continue;
1759
+ dist.set(up, d + 1);
1760
+ queue.push(up);
1761
+ }
1762
+ }
1763
+ dist.delete(target);
1764
+ const dependents = [...dist.entries()].map(([file, d]) => ({ file, depth: d })).sort((a, b) => a.depth - b.depth || a.file.localeCompare(b.file));
1765
+ const directCount = dependents.filter((d) => d.depth === 1).length;
1766
+ let pkg = {};
1767
+ try {
1768
+ pkg = JSON.parse(await readFile5(join7(g.abs, "package.json"), "utf8"));
1769
+ } catch {
1770
+ }
1771
+ const isEntry = looksLikeEntry(target, pkg);
1772
+ const total = dependents.length;
1773
+ const percent = g.codeFiles.length ? total / g.codeFiles.length : 0;
1774
+ const risk = inCycle || percent >= 0.25 || total >= 25 ? "high" : percent >= 0.05 || total >= 5 ? "medium" : "low";
1775
+ return {
1776
+ ok: true,
1777
+ report: {
1778
+ projectPath: g.abs,
1779
+ query,
1780
+ target,
1781
+ exportedSymbols: parseExports(g.fileTexts.get(target) ?? ""),
1782
+ dependents,
1783
+ directCount,
1784
+ totalFiles: g.codeFiles.length,
1785
+ percent,
1786
+ inCycle,
1787
+ isEntry,
1788
+ risk
1789
+ }
1790
+ };
1791
+ }
1792
+ var RISK_ICON = { low: "\u{1F7E2}", medium: "\u{1F7E1}", high: "\u{1F534}" };
1793
+ function formatImpactReport(r, lang = "fr") {
1794
+ const t2 = lang === "en" ? {
1795
+ title: "IMPACT ANALYSIS",
1796
+ target: "Target",
1797
+ exports: "Exported symbols",
1798
+ direct: "Direct dependents",
1799
+ total: "Total blast radius",
1800
+ files: "code files",
1801
+ inCycle: "part of a dependency cycle",
1802
+ entry: "entry point",
1803
+ risk: "Risk",
1804
+ low: "LOW",
1805
+ medium: "MEDIUM",
1806
+ high: "HIGH",
1807
+ depth: "depth",
1808
+ none: "none \u2014 nothing imports this file",
1809
+ more: (n) => `\u2026and ${n} more`
1810
+ } : {
1811
+ title: "ANALYSE D\u2019IMPACT",
1812
+ target: "Cible",
1813
+ exports: "Symboles export\xE9s",
1814
+ direct: "D\xE9pendants directs",
1815
+ total: "Rayon d\u2019impact total",
1816
+ files: "fichiers de code",
1817
+ inCycle: "dans un cycle de d\xE9pendances",
1818
+ entry: "point d\u2019entr\xE9e",
1819
+ risk: "Risque",
1820
+ low: "FAIBLE",
1821
+ medium: "MOYEN",
1822
+ high: "\xC9LEV\xC9",
1823
+ depth: "profondeur",
1824
+ none: "aucun \u2014 rien n\u2019importe ce fichier",
1825
+ more: (n) => `\u2026et ${n} autres`
1826
+ };
1827
+ const tags = [r.isEntry ? t2.entry : "", r.inCycle ? t2.inCycle : ""].filter(Boolean).join(" \xB7 ");
1828
+ const out = [];
1829
+ out.push(`== ${t2.title} \u2014 ${basename2(r.projectPath)} ==`);
1830
+ out.push(`${t2.target}: ${r.target}${tags ? ` (${tags})` : ""}`);
1831
+ out.push(`${t2.exports}: ${r.exportedSymbols.length ? r.exportedSymbols.map((s) => s.name).join(", ") : "\u2014"}`);
1832
+ out.push(`${t2.direct}: ${r.directCount} \xB7 ${t2.total}: ${r.dependents.length} / ${r.totalFiles} ${t2.files} (${Math.round(r.percent * 100)}%)`);
1833
+ out.push(`${t2.risk}: ${t2[riskKey(r.risk)]}`);
1834
+ out.push("");
1835
+ if (!r.dependents.length) {
1836
+ out.push(` ${t2.none}`);
1837
+ return out.join("\n");
1838
+ }
1839
+ const byDepth = /* @__PURE__ */ new Map();
1840
+ for (const d of r.dependents) {
1841
+ if (!byDepth.has(d.depth)) byDepth.set(d.depth, []);
1842
+ byDepth.get(d.depth).push(d.file);
1843
+ }
1844
+ for (const [d, files] of [...byDepth.entries()].sort((a, b) => a[0] - b[0])) {
1845
+ out.push(` ${t2.depth} ${d} (${files.length}):`);
1846
+ for (const f of files.slice(0, 12)) out.push(` ${f}`);
1847
+ if (files.length > 12) out.push(` ${t2.more(files.length - 12)}`);
1848
+ }
1849
+ return out.join("\n");
1850
+ }
1851
+ function riskKey(r) {
1852
+ return r;
1853
+ }
1854
+ function formatImpactReportMd(r, lang = "fr") {
1855
+ const t2 = lang === "en" ? {
1856
+ title: "Impact analysis",
1857
+ target: "Target",
1858
+ exports: "Exported symbols",
1859
+ direct: "Direct dependents",
1860
+ total: "Total blast radius",
1861
+ files: "code files",
1862
+ inCycle: "part of a dependency cycle",
1863
+ entry: "entry point",
1864
+ risk: "Risk",
1865
+ low: "LOW",
1866
+ medium: "MEDIUM",
1867
+ high: "HIGH",
1868
+ colDepth: "Depth",
1869
+ colFile: "File",
1870
+ none: "_None \u2014 nothing imports this file._",
1871
+ more: (n) => `_\u2026and ${n} more_`
1872
+ } : {
1873
+ title: "Analyse d\u2019impact",
1874
+ target: "Cible",
1875
+ exports: "Symboles export\xE9s",
1876
+ direct: "D\xE9pendants directs",
1877
+ total: "Rayon d\u2019impact total",
1878
+ files: "fichiers de code",
1879
+ inCycle: "dans un cycle de d\xE9pendances",
1880
+ entry: "point d\u2019entr\xE9e",
1881
+ risk: "Risque",
1882
+ low: "FAIBLE",
1883
+ medium: "MOYEN",
1884
+ high: "\xC9LEV\xC9",
1885
+ colDepth: "Profondeur",
1886
+ colFile: "Fichier",
1887
+ none: "_Aucun \u2014 rien n\u2019importe ce fichier._",
1888
+ more: (n) => `_\u2026et ${n} autres_`
1889
+ };
1890
+ const tags = [r.isEntry ? t2.entry : "", r.inCycle ? t2.inCycle : ""].filter(Boolean).join(" \xB7 ");
1891
+ const out = [];
1892
+ out.push(`## ${RISK_ICON[r.risk]} ${t2.title} \u2014 \`${basename2(r.projectPath)}\``);
1893
+ out.push("");
1894
+ out.push(`**${t2.target} : \`${r.target}\`**${tags ? ` \u2014 _${tags}_` : ""}`);
1895
+ out.push("");
1896
+ out.push(`- **${t2.exports}** : ${r.exportedSymbols.length ? r.exportedSymbols.map((s) => `\`${s.name}\``).join(", ") : "\u2014"}`);
1897
+ out.push(`- **${t2.direct}** : ${r.directCount} \xB7 **${t2.total}** : **${r.dependents.length}** / ${r.totalFiles} ${t2.files} (**${Math.round(r.percent * 100)}%**)`);
1898
+ out.push(`- **${t2.risk}** : **${t2[riskKey(r.risk)]}**`);
1899
+ out.push("");
1900
+ if (!r.dependents.length) {
1901
+ out.push(t2.none);
1902
+ return out.join("\n");
1903
+ }
1904
+ out.push(`| ${t2.colDepth} | ${t2.colFile} |`, "|---|---|");
1905
+ for (const d of r.dependents.slice(0, 30)) out.push(`| ${d.depth} | \`${d.file}\` |`);
1906
+ if (r.dependents.length > 30) out.push(`| | ${t2.more(r.dependents.length - 30)} |`);
1907
+ return out.join("\n");
1908
+ }
1661
1909
  export {
1662
1910
  CONFIG_FILE,
1911
+ analyzeImpact,
1663
1912
  analyzeProject,
1664
1913
  buildContext,
1665
1914
  buildIndex,
@@ -1674,6 +1923,9 @@ export {
1674
1923
  findProjectRoot,
1675
1924
  formatHealthReport,
1676
1925
  formatHealthReportMd,
1926
+ formatImpactReport,
1927
+ formatImpactReportMd,
1928
+ getCacheDir,
1677
1929
  getChangedFiles,
1678
1930
  getEmbedding,
1679
1931
  getEmbeddings,