dsh-codebase-chat 0.22.0 → 0.23.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
@@ -57,12 +57,16 @@ interface ContextOptions {
57
57
  instruction?: string;
58
58
  /** Enable local semantic embeddings for retrieval */
59
59
  embed?: boolean;
60
+ /** Git ref (branch/tag/SHA) — scope retrieval to files changed vs this ref */
61
+ diff?: string;
60
62
  }
61
63
  interface ContextResult {
62
64
  absProject: string;
63
65
  context: string;
64
66
  chunks: CodeChunk[];
65
67
  tokenCount: number;
68
+ /** Project-relative files in scope when `diff` was requested */
69
+ diffFiles?: string[];
66
70
  }
67
71
 
68
72
  declare function buildContext(options: ContextOptions): Promise<ContextResult>;
@@ -166,7 +170,11 @@ interface HealthReport {
166
170
  score: number;
167
171
  grade: 'A' | 'B' | 'C' | 'D' | 'E';
168
172
  }
169
- declare function analyzeProject(projectPath: string): Promise<HealthReport>;
173
+ interface AnalyzeOptions {
174
+ /** Restrict analysis to these project-relative paths (e.g. a diff scope). */
175
+ files?: Set<string>;
176
+ }
177
+ declare function analyzeProject(projectPath: string, opts?: AnalyzeOptions): Promise<HealthReport>;
170
178
  declare function formatHealthReport(r: HealthReport, lang?: 'fr' | 'en'): string;
171
179
  declare function formatHealthReportMd(r: HealthReport, lang?: 'fr' | 'en'): string;
172
180
 
@@ -199,4 +207,18 @@ declare function globToRegExp(glob: string): RegExp;
199
207
  /** Match a project-relative path (posix separators) against a list of globs. */
200
208
  declare function matchesAnyGlob(relPath: string, globs: string[] | undefined): boolean;
201
209
 
202
- export { CONFIG_FILE, type CloneGroup, type CodeChunk, type CodeIndex, type ConfigLang, type ContextOptions, type ContextResult, type Cycle, 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, getEmbedding, getEmbeddings, getExtractor, getIndex, globToRegExp, initTreeSitter, loadIndex, loadProjectConfig, matchesAnyGlob, resolveProjectPath, saveIndex, scoreChunks, selectChunks, treeSitterReady, truncateToTokens };
210
+ interface DiffScope {
211
+ /** Project-relative paths (forward slashes) changed vs the base ref. */
212
+ files: Set<string>;
213
+ /** False when the project is not a git repo or git is unavailable. */
214
+ ok: boolean;
215
+ /** Why the scope could not be computed (when ok === false). */
216
+ error?: string;
217
+ }
218
+ /**
219
+ * Files changed vs a git ref — committed diffs, staged/unstaged edits and
220
+ * untracked files. Used to scope retrieval and audits to "what changed".
221
+ */
222
+ declare function getChangedFiles(absProject: string, base: string): Promise<DiffScope>;
223
+
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 };
package/dist/index.js CHANGED
@@ -1168,6 +1168,33 @@ function selectChunks(scored, maxTokens, maxChunkTokens = Infinity) {
1168
1168
  return { chunks: result, tokens: used };
1169
1169
  }
1170
1170
 
1171
+ // src/diff.ts
1172
+ import { execFile } from "child_process";
1173
+ import { sep as sep3 } from "path";
1174
+ import { promisify } from "util";
1175
+ var run = promisify(execFile);
1176
+ async function git(absProject, args) {
1177
+ const { stdout } = await run("git", ["-C", absProject, ...args], { maxBuffer: 16 * 1024 * 1024 });
1178
+ return stdout;
1179
+ }
1180
+ function toRelList(output) {
1181
+ return output.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => l.split(sep3).join("/"));
1182
+ }
1183
+ async function getChangedFiles(absProject, base) {
1184
+ try {
1185
+ const [tracked, untracked] = await Promise.all([
1186
+ // Working tree vs base — covers committed, staged and unstaged edits.
1187
+ git(absProject, ["diff", "--name-only", base, "--"]),
1188
+ git(absProject, ["ls-files", "--others", "--exclude-standard"])
1189
+ ]);
1190
+ const files = /* @__PURE__ */ new Set([...toRelList(tracked), ...toRelList(untracked)]);
1191
+ return { files, ok: true };
1192
+ } catch (err) {
1193
+ const msg = err instanceof Error ? err.message : String(err);
1194
+ return { files: /* @__PURE__ */ new Set(), ok: false, error: msg.split("\n")[0] };
1195
+ }
1196
+ }
1197
+
1171
1198
  // src/context.ts
1172
1199
  var DEFAULT_MAX_TOKENS = 6e4;
1173
1200
  var HEAD_BUDGET_TOKENS = 800;
@@ -1178,14 +1205,18 @@ function getLabels(lang) {
1178
1205
  tree: "File tree",
1179
1206
  noConstraints: "No explicit constraints documented.",
1180
1207
  constraints: "IDENTIFIED PRODUCT CONSTRAINTS",
1181
- answerIn: "Answer in English."
1208
+ answerIn: "Answer in English.",
1209
+ diffScope: (base, n) => `Scope: ${n} file(s) changed vs ${base}`,
1210
+ diffUnavailable: (base) => `Scope: diff vs ${base} unavailable (not a git repo?) \u2014 full project`
1182
1211
  } : {
1183
1212
  project: "Projet",
1184
1213
  focus: "Focus",
1185
1214
  tree: "Arborescence",
1186
1215
  noConstraints: "Aucune contrainte explicite document\xE9e.",
1187
1216
  constraints: "CONTRAINTES PRODUIT IDENTIFI\xC9ES",
1188
- answerIn: "R\xE9ponds obligatoirement en fran\xE7ais."
1217
+ answerIn: "R\xE9ponds obligatoirement en fran\xE7ais.",
1218
+ diffScope: (base, n) => `P\xE9rim\xE8tre : ${n} fichier(s) modifi\xE9(s) vs ${base}`,
1219
+ diffUnavailable: (base) => `P\xE9rim\xE8tre : diff vs ${base} indisponible (pas un repo git ?) \u2014 projet complet`
1189
1220
  };
1190
1221
  }
1191
1222
  async function extractProductConstraints(absProject) {
@@ -1212,13 +1243,30 @@ function formatChunk(chunk) {
1212
1243
  ${chunk.content}`;
1213
1244
  }
1214
1245
  async function buildContext(options) {
1215
- const { project, query, filePath, searchQuery, instruction, embed = false } = options;
1246
+ const { project, query, filePath, searchQuery, instruction, embed = false, diff } = options;
1216
1247
  const absProject = await findProjectRoot(resolveProjectPath(project));
1217
1248
  const cfg = await loadProjectConfig(absProject);
1218
1249
  const maxTokens = options.maxTokens ?? cfg.maxTokens ?? DEFAULT_MAX_TOKENS;
1219
1250
  const lang = options.lang ?? cfg.lang ?? "fr";
1220
1251
  const labels = getLabels(lang);
1221
1252
  const index = await getIndex(absProject);
1253
+ let scopedIndex = index;
1254
+ let diffFiles;
1255
+ let scopeLine = "";
1256
+ if (diff && !filePath) {
1257
+ const scope = await getChangedFiles(absProject, diff);
1258
+ if (scope.ok) {
1259
+ diffFiles = [...scope.files].filter((f) => index.files[f]);
1260
+ const files = {};
1261
+ for (const f of diffFiles) files[f] = index.files[f];
1262
+ scopedIndex = { ...index, files };
1263
+ scopeLine = `${labels.diffScope(diff, diffFiles.length)}
1264
+ `;
1265
+ } else {
1266
+ scopeLine = `${labels.diffUnavailable(diff)}
1267
+ `;
1268
+ }
1269
+ }
1222
1270
  if (embed) {
1223
1271
  try {
1224
1272
  await embedIndex(index);
@@ -1240,17 +1288,17 @@ async function buildContext(options) {
1240
1288
  );
1241
1289
  selectedChunks = chunks;
1242
1290
  } else {
1243
- const scored = await scoreChunks(index, filePath, embed);
1291
+ const scored = await scoreChunks(scopedIndex, filePath, embed);
1244
1292
  const { chunks } = selectChunks(scored, bodyLimit, maxChunkTokens);
1245
1293
  if (chunks.length === 0) throw new Error(`File not found: ${filePath}`);
1246
1294
  selectedChunks = chunks;
1247
1295
  }
1248
1296
  } else if (searchQuery) {
1249
- const scored = await scoreChunks(index, searchQuery, embed);
1297
+ const scored = await scoreChunks(scopedIndex, searchQuery, embed);
1250
1298
  const { chunks } = selectChunks(scored, bodyLimit, maxChunkTokens);
1251
1299
  selectedChunks = chunks;
1252
1300
  } else {
1253
- const scored = await scoreChunks(index, query, embed);
1301
+ const scored = await scoreChunks(scopedIndex, query, embed);
1254
1302
  const { chunks } = selectChunks(scored, bodyLimit, maxChunkTokens);
1255
1303
  selectedChunks = chunks;
1256
1304
  }
@@ -1260,7 +1308,7 @@ ${constraints.map((c) => `- ${c}`).join("\n")}` : `== ${labels.constraints} ==
1260
1308
  ${labels.noConstraints}`;
1261
1309
  const head = `${labels.project} : ${absProject}
1262
1310
  ${labels.focus} : ${focus}
1263
- ${constraintsText}
1311
+ ${scopeLine}${constraintsText}
1264
1312
 
1265
1313
  == ${labels.tree} ==
1266
1314
  ${index.tree}
@@ -1284,12 +1332,13 @@ ${finalInstruction}`;
1284
1332
  absProject,
1285
1333
  context: prompt,
1286
1334
  chunks: selectedChunks,
1287
- tokenCount
1335
+ tokenCount,
1336
+ diffFiles
1288
1337
  };
1289
1338
  }
1290
1339
 
1291
1340
  // src/analysis.ts
1292
- import { basename, extname as extname2, join as join6, relative as relative3, sep as sep3, posix as posixPath } from "path";
1341
+ import { basename, extname as extname2, join as join6, relative as relative3, sep as sep4, posix as posixPath } from "path";
1293
1342
  import { readFile as readFile4 } from "fs/promises";
1294
1343
  var CODE_EXTS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
1295
1344
  var ENTRY_BASENAMES = /* @__PURE__ */ new Set(["index", "main", "app", "cli", "server", "bin", "mod"]);
@@ -1404,15 +1453,16 @@ function findDuplicates(fileTexts) {
1404
1453
  }
1405
1454
  return [...groups.values()].sort((a, b) => b.lines - a.lines).slice(0, 15);
1406
1455
  }
1407
- async function analyzeProject(projectPath) {
1456
+ async function analyzeProject(projectPath, opts = {}) {
1408
1457
  const abs = await findProjectRoot(resolveProjectPath(projectPath));
1409
1458
  const fileTexts = /* @__PURE__ */ new Map();
1410
1459
  const codeFiles = [];
1411
1460
  const walk = await getWalkOptions(abs);
1412
1461
  for await (const full of walkFiles(abs, walk.skipDirs, walk.skipFiles, walk.ignoreGlobs)) {
1413
- const rel = relative3(abs, full).split(sep3).join("/");
1462
+ const rel = relative3(abs, full).split(sep4).join("/");
1414
1463
  const ext = extname2(rel).toLowerCase();
1415
1464
  if (!CODE_EXTS.has(ext) || SKIP_EXTS.has(ext) || rel.includes(".min.")) continue;
1465
+ if (opts.files && !opts.files.has(rel)) continue;
1416
1466
  const text = await safeReadText(full);
1417
1467
  if (!text) continue;
1418
1468
  codeFiles.push(rel);
@@ -1624,6 +1674,7 @@ export {
1624
1674
  findProjectRoot,
1625
1675
  formatHealthReport,
1626
1676
  formatHealthReportMd,
1677
+ getChangedFiles,
1627
1678
  getEmbedding,
1628
1679
  getEmbeddings,
1629
1680
  getExtractor,