dsh-codebase-chat 0.22.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.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
  }
@@ -1168,6 +1175,33 @@ function selectChunks(scored, maxTokens, maxChunkTokens = Infinity) {
1168
1175
  return { chunks: result, tokens: used };
1169
1176
  }
1170
1177
 
1178
+ // src/diff.ts
1179
+ import { execFile } from "child_process";
1180
+ import { sep as sep3 } from "path";
1181
+ import { promisify } from "util";
1182
+ var run = promisify(execFile);
1183
+ async function git(absProject, args) {
1184
+ const { stdout } = await run("git", ["-C", absProject, ...args], { maxBuffer: 16 * 1024 * 1024 });
1185
+ return stdout;
1186
+ }
1187
+ function toRelList(output) {
1188
+ return output.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => l.split(sep3).join("/"));
1189
+ }
1190
+ async function getChangedFiles(absProject, base) {
1191
+ try {
1192
+ const [tracked, untracked] = await Promise.all([
1193
+ // Working tree vs base — covers committed, staged and unstaged edits.
1194
+ git(absProject, ["diff", "--name-only", base, "--"]),
1195
+ git(absProject, ["ls-files", "--others", "--exclude-standard"])
1196
+ ]);
1197
+ const files = /* @__PURE__ */ new Set([...toRelList(tracked), ...toRelList(untracked)]);
1198
+ return { files, ok: true };
1199
+ } catch (err) {
1200
+ const msg = err instanceof Error ? err.message : String(err);
1201
+ return { files: /* @__PURE__ */ new Set(), ok: false, error: msg.split("\n")[0] };
1202
+ }
1203
+ }
1204
+
1171
1205
  // src/context.ts
1172
1206
  var DEFAULT_MAX_TOKENS = 6e4;
1173
1207
  var HEAD_BUDGET_TOKENS = 800;
@@ -1178,23 +1212,38 @@ function getLabels(lang) {
1178
1212
  tree: "File tree",
1179
1213
  noConstraints: "No explicit constraints documented.",
1180
1214
  constraints: "IDENTIFIED PRODUCT CONSTRAINTS",
1181
- answerIn: "Answer in English."
1215
+ answerIn: "Answer in English.",
1216
+ diffScope: (base, n) => `Scope: ${n} file(s) changed vs ${base}`,
1217
+ diffUnavailable: (base) => `Scope: diff vs ${base} unavailable (not a git repo?) \u2014 full project`
1182
1218
  } : {
1183
1219
  project: "Projet",
1184
1220
  focus: "Focus",
1185
1221
  tree: "Arborescence",
1186
1222
  noConstraints: "Aucune contrainte explicite document\xE9e.",
1187
1223
  constraints: "CONTRAINTES PRODUIT IDENTIFI\xC9ES",
1188
- answerIn: "R\xE9ponds obligatoirement en fran\xE7ais."
1224
+ answerIn: "R\xE9ponds obligatoirement en fran\xE7ais.",
1225
+ diffScope: (base, n) => `P\xE9rim\xE8tre : ${n} fichier(s) modifi\xE9(s) vs ${base}`,
1226
+ diffUnavailable: (base) => `P\xE9rim\xE8tre : diff vs ${base} indisponible (pas un repo git ?) \u2014 projet complet`
1189
1227
  };
1190
1228
  }
1191
1229
  async function extractProductConstraints(absProject) {
1192
- 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
+ ];
1193
1242
  const constraints = [];
1194
1243
  for (const name of candidates) {
1195
1244
  const text = await safeReadText(join5(absProject, name));
1196
1245
  if (!text) continue;
1197
- 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;
1198
1247
  let m;
1199
1248
  while ((m = regex.exec(text)) !== null) {
1200
1249
  const line = m[0].replace(/\s+/g, " ").trim();
@@ -1212,13 +1261,30 @@ function formatChunk(chunk) {
1212
1261
  ${chunk.content}`;
1213
1262
  }
1214
1263
  async function buildContext(options) {
1215
- const { project, query, filePath, searchQuery, instruction, embed = false } = options;
1264
+ const { project, query, filePath, searchQuery, instruction, embed = false, diff } = options;
1216
1265
  const absProject = await findProjectRoot(resolveProjectPath(project));
1217
1266
  const cfg = await loadProjectConfig(absProject);
1218
1267
  const maxTokens = options.maxTokens ?? cfg.maxTokens ?? DEFAULT_MAX_TOKENS;
1219
1268
  const lang = options.lang ?? cfg.lang ?? "fr";
1220
1269
  const labels = getLabels(lang);
1221
1270
  const index = await getIndex(absProject);
1271
+ let scopedIndex = index;
1272
+ let diffFiles;
1273
+ let scopeLine = "";
1274
+ if (diff && !filePath) {
1275
+ const scope = await getChangedFiles(absProject, diff);
1276
+ if (scope.ok) {
1277
+ diffFiles = [...scope.files].filter((f) => index.files[f]);
1278
+ const files = {};
1279
+ for (const f of diffFiles) files[f] = index.files[f];
1280
+ scopedIndex = { ...index, files };
1281
+ scopeLine = `${labels.diffScope(diff, diffFiles.length)}
1282
+ `;
1283
+ } else {
1284
+ scopeLine = `${labels.diffUnavailable(diff)}
1285
+ `;
1286
+ }
1287
+ }
1222
1288
  if (embed) {
1223
1289
  try {
1224
1290
  await embedIndex(index);
@@ -1240,17 +1306,17 @@ async function buildContext(options) {
1240
1306
  );
1241
1307
  selectedChunks = chunks;
1242
1308
  } else {
1243
- const scored = await scoreChunks(index, filePath, embed);
1309
+ const scored = await scoreChunks(scopedIndex, filePath, embed);
1244
1310
  const { chunks } = selectChunks(scored, bodyLimit, maxChunkTokens);
1245
1311
  if (chunks.length === 0) throw new Error(`File not found: ${filePath}`);
1246
1312
  selectedChunks = chunks;
1247
1313
  }
1248
1314
  } else if (searchQuery) {
1249
- const scored = await scoreChunks(index, searchQuery, embed);
1315
+ const scored = await scoreChunks(scopedIndex, searchQuery, embed);
1250
1316
  const { chunks } = selectChunks(scored, bodyLimit, maxChunkTokens);
1251
1317
  selectedChunks = chunks;
1252
1318
  } else {
1253
- const scored = await scoreChunks(index, query, embed);
1319
+ const scored = await scoreChunks(scopedIndex, query, embed);
1254
1320
  const { chunks } = selectChunks(scored, bodyLimit, maxChunkTokens);
1255
1321
  selectedChunks = chunks;
1256
1322
  }
@@ -1260,7 +1326,7 @@ ${constraints.map((c) => `- ${c}`).join("\n")}` : `== ${labels.constraints} ==
1260
1326
  ${labels.noConstraints}`;
1261
1327
  const head = `${labels.project} : ${absProject}
1262
1328
  ${labels.focus} : ${focus}
1263
- ${constraintsText}
1329
+ ${scopeLine}${constraintsText}
1264
1330
 
1265
1331
  == ${labels.tree} ==
1266
1332
  ${index.tree}
@@ -1284,12 +1350,13 @@ ${finalInstruction}`;
1284
1350
  absProject,
1285
1351
  context: prompt,
1286
1352
  chunks: selectedChunks,
1287
- tokenCount
1353
+ tokenCount,
1354
+ diffFiles
1288
1355
  };
1289
1356
  }
1290
1357
 
1291
1358
  // src/analysis.ts
1292
- import { basename, extname as extname2, join as join6, relative as relative3, sep as sep3, posix as posixPath } from "path";
1359
+ import { basename, extname as extname2, join as join6, relative as relative3, sep as sep4, posix as posixPath } from "path";
1293
1360
  import { readFile as readFile4 } from "fs/promises";
1294
1361
  var CODE_EXTS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
1295
1362
  var ENTRY_BASENAMES = /* @__PURE__ */ new Set(["index", "main", "app", "cli", "server", "bin", "mod"]);
@@ -1332,36 +1399,58 @@ function parseExports(text) {
1332
1399
  }
1333
1400
  function findCycles(edges) {
1334
1401
  const adj = /* @__PURE__ */ new Map();
1402
+ const nodes = /* @__PURE__ */ new Set();
1335
1403
  for (const e of edges) {
1404
+ nodes.add(e.from);
1405
+ nodes.add(e.to);
1336
1406
  if (!adj.has(e.from)) adj.set(e.from, []);
1337
1407
  adj.get(e.from).push(e.to);
1338
1408
  }
1339
- const cycles = [];
1340
- const seen = /* @__PURE__ */ new Set();
1341
- const stack = [];
1409
+ const index = /* @__PURE__ */ new Map();
1410
+ const low = /* @__PURE__ */ new Map();
1342
1411
  const onStack = /* @__PURE__ */ new Set();
1343
- function dfs(node) {
1344
- stack.push(node);
1345
- onStack.add(node);
1346
- for (const next of adj.get(node) ?? []) {
1347
- if (onStack.has(next)) {
1348
- const cycle = stack.slice(stack.indexOf(next)).concat(next);
1349
- const body = cycle.slice(0, -1);
1350
- const minIdx = body.indexOf(body.reduce((a, b) => a < b ? a : b));
1351
- const key = body.slice(minIdx).concat(body.slice(0, minIdx)).join(">");
1352
- if (!seen.has(key)) {
1353
- seen.add(key);
1354
- 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);
1355
1449
  }
1356
- } else if (!stack.includes(next)) {
1357
- dfs(next);
1358
1450
  }
1359
1451
  }
1360
- stack.pop();
1361
- onStack.delete(node);
1362
1452
  }
1363
- for (const n of adj.keys()) dfs(n);
1364
- return cycles;
1453
+ return sccs.map((m) => ({ path: m.sort() })).sort((a, b) => b.path.length - a.path.length);
1365
1454
  }
1366
1455
  function looksLikeEntry(rel, pkg) {
1367
1456
  const base = basename(rel).toLowerCase().replace(extname2(rel), "");
@@ -1404,13 +1493,13 @@ function findDuplicates(fileTexts) {
1404
1493
  }
1405
1494
  return [...groups.values()].sort((a, b) => b.lines - a.lines).slice(0, 15);
1406
1495
  }
1407
- async function analyzeProject(projectPath) {
1496
+ async function collectImportGraph(projectPath) {
1408
1497
  const abs = await findProjectRoot(resolveProjectPath(projectPath));
1409
1498
  const fileTexts = /* @__PURE__ */ new Map();
1410
1499
  const codeFiles = [];
1411
1500
  const walk = await getWalkOptions(abs);
1412
1501
  for await (const full of walkFiles(abs, walk.skipDirs, walk.skipFiles, walk.ignoreGlobs)) {
1413
- const rel = relative3(abs, full).split(sep3).join("/");
1502
+ const rel = relative3(abs, full).split(sep4).join("/");
1414
1503
  const ext = extname2(rel).toLowerCase();
1415
1504
  if (!CODE_EXTS.has(ext) || SKIP_EXTS.has(ext) || rel.includes(".min.")) continue;
1416
1505
  const text = await safeReadText(full);
@@ -1418,11 +1507,6 @@ async function analyzeProject(projectPath) {
1418
1507
  codeFiles.push(rel);
1419
1508
  fileTexts.set(rel, text);
1420
1509
  }
1421
- let pkg = {};
1422
- try {
1423
- pkg = JSON.parse(await readFile4(join6(abs, "package.json"), "utf8"));
1424
- } catch {
1425
- }
1426
1510
  const known = new Set(codeFiles);
1427
1511
  const edges = [];
1428
1512
  const inDegree = /* @__PURE__ */ new Map();
@@ -1432,18 +1516,32 @@ async function analyzeProject(projectPath) {
1432
1516
  inDegree.set(to, (inDegree.get(to) ?? 0) + 1);
1433
1517
  }
1434
1518
  }
1435
- const cycles = findCycles(edges);
1436
- const unusedFiles = codeFiles.filter((rel) => !inDegree.has(rel) && !looksLikeEntry(rel, pkg)).sort();
1437
- const otherText = /* @__PURE__ */ new Map();
1438
- 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
+ }
1439
1537
  const unusedExports = [];
1440
- for (const rel of codeFiles) {
1538
+ for (const rel of scopedFiles) {
1441
1539
  for (const exp of parseExports(fileTexts.get(rel))) {
1442
1540
  if (exp.name === "default") continue;
1443
1541
  let used = false;
1444
- for (const [otherFile, text] of fileTexts) {
1542
+ for (const [otherFile, ids] of identifiersByFile) {
1445
1543
  if (otherFile === rel) continue;
1446
- if (new RegExp(`\\b${exp.name.replace(/[$_]/g, "\\$&")}\\b`).test(text)) {
1544
+ if (ids.has(exp.name)) {
1447
1545
  used = true;
1448
1546
  break;
1449
1547
  }
@@ -1451,21 +1549,22 @@ async function analyzeProject(projectPath) {
1451
1549
  if (!used) unusedExports.push({ file: rel, name: exp.name, line: exp.line });
1452
1550
  }
1453
1551
  }
1454
- const duplicates = findDuplicates(fileTexts);
1552
+ const duplicates = findDuplicates(fileTexts).filter((g) => !scope || g.files.some((f) => scope.has(f)));
1455
1553
  const hotspots = [];
1456
- for (const [rel, text] of fileTexts) {
1457
- const score2 = complexityOf(text);
1554
+ for (const rel of scopedFiles) {
1555
+ const score2 = complexityOf(fileTexts.get(rel));
1458
1556
  if (score2 >= 12) hotspots.push({ file: rel, startLine: 1, score: score2 });
1459
1557
  }
1460
1558
  hotspots.sort((a, b) => b.score - a.score);
1461
- 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);
1462
1560
  const dupLines = duplicates.reduce((s, g) => s + g.lines, 0);
1463
- 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;
1464
1563
  const score = Math.max(0, Math.min(100, 100 - penalties));
1465
1564
  const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 65 ? "C" : score >= 50 ? "D" : "E";
1466
1565
  return {
1467
1566
  projectPath: abs,
1468
- analyzedFiles: codeFiles.length,
1567
+ analyzedFiles: scopedFiles.length,
1469
1568
  importEdges: edges.length,
1470
1569
  cycles,
1471
1570
  unusedFiles,
@@ -1507,7 +1606,7 @@ function formatHealthReport(r, lang = "fr") {
1507
1606
  out.push(`${t2.score}: ${r.score}/100 (${r.grade}) \xB7 ${r.analyzedFiles} ${t2.files} \xB7 ${r.importEdges} ${t2.edges}`);
1508
1607
  out.push("");
1509
1608
  out.push(`\u25CF ${t2.cycles} (${r.cycles.length})`);
1510
- 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)}`);
1511
1610
  if (r.cycles.length === 0) out.push(` ${t2.none}`);
1512
1611
  out.push("");
1513
1612
  out.push(`\u25CF ${t2.unusedFiles} (${r.unusedFiles.length}) \u2014 ${t2.noteUnused}`);
@@ -1523,6 +1622,12 @@ function formatHealthReport(r, lang = "fr") {
1523
1622
  for (const h of r.hotspots.slice(0, 10)) out.push(` ${h.file} \u2014 score ${h.score}`);
1524
1623
  return out.join("\n");
1525
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
+ }
1526
1631
  var GRADE_ICON = { A: "\u{1F7E2}", B: "\u{1F535}", C: "\u{1F7E1}", D: "\u{1F7E0}", E: "\u{1F534}" };
1527
1632
  function scoreBar(score) {
1528
1633
  const filled = Math.round(score / 10);
@@ -1573,7 +1678,7 @@ function formatHealthReportMd(r, lang = "fr") {
1573
1678
  out.push("");
1574
1679
  out.push(`### ${t2.cycles} \u2014 ${r.cycles.length}`);
1575
1680
  if (r.cycles.length === 0) out.push(t2.none);
1576
- 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)}`);
1577
1682
  if (r.cycles.length > 10) out.push(t2.more(r.cycles.length - 10));
1578
1683
  out.push("");
1579
1684
  out.push(`### ${t2.unusedFiles} \u2014 ${r.unusedFiles.length}`);
@@ -1608,8 +1713,202 @@ function formatHealthReportMd(r, lang = "fr") {
1608
1713
  } else out.push(t2.none);
1609
1714
  return out.join("\n");
1610
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
+ }
1611
1909
  export {
1612
1910
  CONFIG_FILE,
1911
+ analyzeImpact,
1613
1912
  analyzeProject,
1614
1913
  buildContext,
1615
1914
  buildIndex,
@@ -1624,6 +1923,10 @@ export {
1624
1923
  findProjectRoot,
1625
1924
  formatHealthReport,
1626
1925
  formatHealthReportMd,
1926
+ formatImpactReport,
1927
+ formatImpactReportMd,
1928
+ getCacheDir,
1929
+ getChangedFiles,
1627
1930
  getEmbedding,
1628
1931
  getEmbeddings,
1629
1932
  getExtractor,