diffdoc 0.6.2 → 0.6.4

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.
@@ -1,17 +1,19 @@
1
- {
2
- "baseDir": "./.diffdoc",
3
- "aiProvider": "local",
4
- "localLlmEndpoint": "http://localhost:11434/v1",
5
- "localEmbedEndpoint": "http://localhost:11434/v1/embeddings",
6
- "localChatModel": "qwen2.5-coder:7b",
7
- "localEmbedModel": "nomic-embed-code",
8
- "cloudLlmEndpoint": "https://api.openai.com/v1",
9
- "cloudChatModel": "gpt-4o-mini",
10
- "cloudEmbedModel": "text-embedding-3-small",
11
- "embedBatchSize": 25,
12
- "summarizeConcurrency": 2,
13
- "openaiApiKey": "",
14
- "includeGlobs": [],
15
- "excludeGlobs": [],
16
- "ignoreFile": ".diffdocignore"
17
- }
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/sullyTheDev/diffdoc/main/schemas/v2/diffdocrc.schema.json",
3
+ "baseDir": "./.diffdoc",
4
+ "repoPath": ".",
5
+ "aiProvider": "local",
6
+ "localLlmEndpoint": "http://localhost:11434/v1",
7
+ "localEmbedEndpoint": "http://localhost:11434/v1/embeddings",
8
+ "localChatModel": "qwen2.5-coder:7b",
9
+ "localEmbedModel": "nomic-embed-code",
10
+ "cloudLlmEndpoint": "https://api.openai.com/v1",
11
+ "cloudChatModel": "gpt-4o-mini",
12
+ "cloudEmbedModel": "text-embedding-3-small",
13
+ "embedBatchSize": 25,
14
+ "summarizeConcurrency": 2,
15
+ "openaiApiKey": "",
16
+ "includeGlobs": [],
17
+ "excludeGlobs": [],
18
+ "ignoreFile": ".diffdocignore"
19
+ }
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runPrune = runPrune;
7
+ const promises_1 = __importDefault(require("node:fs/promises"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const promises_2 = require("node:readline/promises");
10
+ const node_process_1 = require("node:process");
11
+ const artifacts_1 = require("../types/artifacts");
12
+ const paths_1 = require("../utils/paths");
13
+ const scan_1 = require("../utils/scan");
14
+ function getSummaryDir(manifestPath) {
15
+ return node_path_1.default.resolve(node_path_1.default.dirname(manifestPath), "summaries");
16
+ }
17
+ function getSummaryPath(summaryDir, hash) {
18
+ return node_path_1.default.resolve(summaryDir, `${hash}.json`);
19
+ }
20
+ async function readManifest(manifestPath) {
21
+ const raw = JSON.parse(await promises_1.default.readFile(manifestPath, "utf8"));
22
+ const result = artifacts_1.RepoManifestSchema.safeParse(raw);
23
+ if (!result.success) {
24
+ const issues = result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n");
25
+ throw new Error(`Invalid manifest in ${manifestPath}:\n${issues}`);
26
+ }
27
+ return result.data;
28
+ }
29
+ async function writeManifest(manifestPath, manifest) {
30
+ await promises_1.default.mkdir(node_path_1.default.dirname(manifestPath), { recursive: true });
31
+ await promises_1.default.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
32
+ }
33
+ function countHashRefs(files) {
34
+ const refs = new Map();
35
+ for (const hash of Object.values(files)) {
36
+ refs.set(hash, (refs.get(hash) || 0) + 1);
37
+ }
38
+ return refs;
39
+ }
40
+ async function fileExists(filePath) {
41
+ try {
42
+ await promises_1.default.access(filePath);
43
+ return true;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ async function promptConfirm(message) {
50
+ const rl = (0, promises_2.createInterface)({ input: node_process_1.stdin, output: node_process_1.stdout });
51
+ try {
52
+ const answer = await rl.question(`${message} (y/N): `);
53
+ return answer.trim().toLowerCase() === "y";
54
+ }
55
+ finally {
56
+ rl.close();
57
+ }
58
+ }
59
+ async function runPrune(options, config) {
60
+ const commandCwd = process.cwd();
61
+ const repoPath = node_path_1.default.resolve(commandCwd, options.path || config.repoPath);
62
+ const manifestPath = (0, paths_1.resolveDiffdocArtifactPath)(options.manifest, config.baseDir);
63
+ const summaryDir = getSummaryDir(manifestPath);
64
+ const manifest = await readManifest(manifestPath);
65
+ const manifestFilePaths = Object.keys(manifest.files);
66
+ // Scan eligible files
67
+ const includePatterns = (0, scan_1.compileGlobs)(config.summarize.includeGlobs.map(scan_1.normalizeGlobPattern));
68
+ const excludePatterns = (0, scan_1.compileGlobs)(config.summarize.excludeGlobs.map(scan_1.normalizeGlobPattern));
69
+ const ignoreMatcher = await (0, scan_1.readIgnoreMatcher)(repoPath, config.summarize.ignoreFile);
70
+ const scannedFiles = await (0, scan_1.walkCodeFiles)(repoPath, includePatterns, excludePatterns, ignoreMatcher);
71
+ const scannedFileSet = new Set(scannedFiles);
72
+ // Identify entries to prune
73
+ const toPrune = [];
74
+ for (const filePath of manifestFilePaths) {
75
+ if (scannedFileSet.has(filePath)) {
76
+ continue;
77
+ }
78
+ // Check if the file is deleted from disk or just excluded by rules
79
+ const absolutePath = node_path_1.default.resolve(repoPath, filePath);
80
+ const exists = await fileExists(absolutePath);
81
+ toPrune.push({
82
+ filePath,
83
+ reason: exists ? "excluded" : "deleted"
84
+ });
85
+ }
86
+ if (toPrune.length === 0) {
87
+ if (options.json) {
88
+ const report = {
89
+ scannedFileCount: scannedFiles.length,
90
+ manifestFileCount: manifestFilePaths.length,
91
+ pruned: [],
92
+ manifestEntriesRemoved: 0,
93
+ summaryAssetsDeleted: 0
94
+ };
95
+ console.log(JSON.stringify(report, null, 2));
96
+ }
97
+ else {
98
+ console.log(`Scanned files: ${scannedFiles.length}`);
99
+ console.log(`Manifest files: ${manifestFilePaths.length}`);
100
+ console.log("Nothing to prune.");
101
+ }
102
+ return;
103
+ }
104
+ // Dry run: report and exit
105
+ if (options.dryRun) {
106
+ if (options.json) {
107
+ const report = {
108
+ scannedFileCount: scannedFiles.length,
109
+ manifestFileCount: manifestFilePaths.length,
110
+ pruned: toPrune,
111
+ manifestEntriesRemoved: toPrune.length,
112
+ summaryAssetsDeleted: 0 // unknown until actual execution
113
+ };
114
+ console.log(JSON.stringify(report, null, 2));
115
+ }
116
+ else {
117
+ console.log(`Scanned files: ${scannedFiles.length}`);
118
+ console.log(`Manifest files: ${manifestFilePaths.length}`);
119
+ console.log(`Files to prune: ${toPrune.length}`);
120
+ for (const entry of toPrune) {
121
+ console.log(` - ${entry.filePath} (${entry.reason})`);
122
+ }
123
+ console.log("");
124
+ console.log("Dry run complete. Use without --dry-run to execute.");
125
+ }
126
+ return;
127
+ }
128
+ // Confirmation prompt
129
+ if (!options.yes) {
130
+ if (!options.json) {
131
+ console.log(`Scanned files: ${scannedFiles.length}`);
132
+ console.log(`Manifest files: ${manifestFilePaths.length}`);
133
+ console.log(`Files to prune: ${toPrune.length}`);
134
+ for (const entry of toPrune) {
135
+ console.log(` - ${entry.filePath} (${entry.reason})`);
136
+ }
137
+ console.log("");
138
+ }
139
+ const confirmed = await promptConfirm(`Remove ${toPrune.length} entries from manifest?`);
140
+ if (!confirmed) {
141
+ console.log("Aborted.");
142
+ return;
143
+ }
144
+ }
145
+ // Execute pruning
146
+ const refs = countHashRefs(manifest.files);
147
+ let summaryAssetsDeleted = 0;
148
+ for (const entry of toPrune) {
149
+ const hash = manifest.files[entry.filePath];
150
+ if (!hash) {
151
+ continue;
152
+ }
153
+ delete manifest.files[entry.filePath];
154
+ const newRefCount = Math.max((refs.get(hash) || 1) - 1, 0);
155
+ refs.set(hash, newRefCount);
156
+ // Delete summary asset if no longer referenced
157
+ if (newRefCount === 0) {
158
+ const summaryPath = getSummaryPath(summaryDir, hash);
159
+ try {
160
+ await promises_1.default.unlink(summaryPath);
161
+ summaryAssetsDeleted += 1;
162
+ }
163
+ catch (error) {
164
+ const nodeError = error;
165
+ if (nodeError.code !== "ENOENT") {
166
+ throw error;
167
+ }
168
+ }
169
+ }
170
+ }
171
+ await writeManifest(manifestPath, manifest);
172
+ const report = {
173
+ scannedFileCount: scannedFiles.length,
174
+ manifestFileCount: manifestFilePaths.length,
175
+ pruned: toPrune,
176
+ manifestEntriesRemoved: toPrune.length,
177
+ summaryAssetsDeleted
178
+ };
179
+ if (options.json) {
180
+ console.log(JSON.stringify(report, null, 2));
181
+ }
182
+ else {
183
+ console.log(`Removed ${toPrune.length} manifest entries.`);
184
+ console.log(`Deleted ${summaryAssetsDeleted} orphaned summary assets.`);
185
+ }
186
+ }
@@ -11,6 +11,7 @@ const embed_1 = require("./embed");
11
11
  const artifacts_1 = require("../types/artifacts");
12
12
  const paths_1 = require("../utils/paths");
13
13
  const llm_1 = require("../utils/llm");
14
+ const scan_1 = require("../utils/scan");
14
15
  function getSummaryDir(manifestPath) {
15
16
  return node_path_1.default.resolve(node_path_1.default.dirname(manifestPath), "summaries");
16
17
  }
@@ -140,6 +141,12 @@ async function getIndexFreshness(manifest, config) {
140
141
  extra
141
142
  };
142
143
  }
144
+ async function scanEligibleFiles(repoPath, config) {
145
+ const includePatterns = (0, scan_1.compileGlobs)(config.summarize.includeGlobs.map(scan_1.normalizeGlobPattern));
146
+ const excludePatterns = (0, scan_1.compileGlobs)(config.summarize.excludeGlobs.map(scan_1.normalizeGlobPattern));
147
+ const ignoreMatcher = await (0, scan_1.readIgnoreMatcher)(repoPath, config.summarize.ignoreFile);
148
+ return (0, scan_1.walkCodeFiles)(repoPath, includePatterns, excludePatterns, ignoreMatcher);
149
+ }
143
150
  function formatSummaryFreshness(stats) {
144
151
  if (stats.missingFromManifestCount === 0 && stats.staleCount === 0) {
145
152
  return "fresh";
@@ -147,6 +154,10 @@ function formatSummaryFreshness(stats) {
147
154
  return `stale (missing: ${stats.missingFromManifestCount}, stale: ${stats.staleCount})`;
148
155
  }
149
156
  function buildSummarizeCommand(manifestOption) {
157
+ const command = "diffdoc summarize --mode delta";
158
+ return manifestOption === "manifest.json" ? command : `${command} --out ${manifestOption}`;
159
+ }
160
+ function buildSummarizeRefreshCommand(manifestOption) {
150
161
  const command = "diffdoc summarize --mode all --refresh";
151
162
  return manifestOption === "manifest.json" ? command : `${command} --out ${manifestOption}`;
152
163
  }
@@ -154,10 +165,22 @@ function buildEmbedCommand(manifestOption) {
154
165
  const command = "diffdoc embed";
155
166
  return manifestOption === "manifest.json" ? command : `${command} --manifest ${manifestOption}`;
156
167
  }
157
- function getNextCommand(manifestOption, summaryStats, indexFreshness) {
158
- if (summaryStats.missingFromManifestCount > 0 || summaryStats.staleCount > 0) {
168
+ function getNextCommand(manifestOption, summaryStats, indexFreshness, untrackedCount, newlyExcludedCount) {
169
+ if (newlyExcludedCount > 0) {
170
+ return {
171
+ command: "diffdoc prune",
172
+ reason: `${newlyExcludedCount} file(s) in manifest are no longer eligible`
173
+ };
174
+ }
175
+ if (untrackedCount > 0) {
159
176
  return {
160
177
  command: buildSummarizeCommand(manifestOption),
178
+ reason: `${untrackedCount} eligible file(s) not yet summarized`
179
+ };
180
+ }
181
+ if (summaryStats.missingFromManifestCount > 0 || summaryStats.staleCount > 0) {
182
+ return {
183
+ command: buildSummarizeRefreshCommand(manifestOption),
161
184
  reason: "summary artifacts are missing or stale"
162
185
  };
163
186
  }
@@ -178,11 +201,18 @@ function getNextCommand(manifestOption, summaryStats, indexFreshness) {
178
201
  reason: "summaries and index are fresh"
179
202
  };
180
203
  }
181
- function buildStatusReport(manifest, summaryStats, indexFreshness, manifestOption) {
182
- const nextCommand = getNextCommand(manifestOption, summaryStats, indexFreshness);
204
+ function buildStatusReport(manifest, scannedFiles, summaryStats, indexFreshness, manifestOption) {
205
+ const manifestFilePaths = new Set(Object.keys(manifest.files));
206
+ const scannedFileSet = new Set(scannedFiles);
207
+ const untrackedCount = scannedFiles.filter((f) => !manifestFilePaths.has(f)).length;
208
+ const newlyExcludedCount = [...manifestFilePaths].filter((f) => !scannedFileSet.has(f)).length;
209
+ const nextCommand = getNextCommand(manifestOption, summaryStats, indexFreshness, untrackedCount, newlyExcludedCount);
183
210
  return {
184
211
  manifestSchema: manifest.schemaVersion,
185
- trackedFileCount: Object.keys(manifest.files).length,
212
+ scannedFileCount: scannedFiles.length,
213
+ trackedFileCount: manifestFilePaths.size,
214
+ untrackedCount,
215
+ newlyExcludedCount,
186
216
  summaryFileCount: summaryStats.summaryFileCount,
187
217
  orphanCount: summaryStats.orphanCount,
188
218
  summaryFreshness: {
@@ -207,18 +237,23 @@ function formatIndexFreshness(freshness) {
207
237
  async function runStatus(options, config) {
208
238
  const manifestPath = (0, paths_1.resolveDiffdocArtifactPath)(options.manifest, config.baseDir);
209
239
  const manifest = await readManifest(manifestPath);
240
+ const commandCwd = process.cwd();
241
+ const repoPath = node_path_1.default.resolve(commandCwd, options.path || config.repoPath);
242
+ const scannedFiles = await scanEligibleFiles(repoPath, config);
210
243
  const summaryStats = await getSummaryStats(manifestPath, manifest);
211
244
  const indexFreshness = await getIndexFreshness(manifest, config);
212
- const report = buildStatusReport(manifest, summaryStats, indexFreshness, options.manifest);
245
+ const report = buildStatusReport(manifest, scannedFiles, summaryStats, indexFreshness, options.manifest);
213
246
  if (options.json) {
214
247
  console.log(JSON.stringify(report, null, 2));
215
248
  return;
216
249
  }
217
250
  console.log(`manifest schema: ${report.manifestSchema}`);
218
- console.log(`tracked files: ${report.trackedFileCount}`);
251
+ console.log(`scanned files: ${report.scannedFileCount}`);
252
+ console.log(`manifest files: ${report.trackedFileCount}`);
253
+ console.log(`untracked: ${report.untrackedCount}`);
254
+ console.log(`newly excluded: ${report.newlyExcludedCount}`);
219
255
  console.log(`summary files: ${report.summaryFileCount}`);
220
256
  console.log(`orphans: ${report.orphanCount}`);
221
- console.log(`stale summaries: ${report.summaryFreshness.stale}`);
222
257
  console.log(`summary freshness: ${formatSummaryFreshness(summaryStats)}`);
223
258
  console.log(`index freshness: ${formatIndexFreshness(indexFreshness)}`);
224
259
  console.log("");
@@ -6,74 +6,21 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runSummarize = runSummarize;
7
7
  const promises_1 = __importDefault(require("node:fs/promises"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
- const ignore_1 = __importDefault(require("ignore"));
10
9
  const artifacts_1 = require("../types/artifacts");
11
10
  const schemas_1 = require("../schemas");
12
11
  const git_1 = require("../utils/git");
13
12
  const hashing_1 = require("../utils/hashing");
14
13
  const llm_1 = require("../utils/llm");
15
14
  const paths_1 = require("../utils/paths");
15
+ const scan_1 = require("../utils/scan");
16
16
  const MANIFEST_SCHEMA_URL = `${schemas_1.SCHEMA_BASE_URL}/manifest.schema.json`;
17
17
  const SUMMARY_ASSET_SCHEMA_URL = `${schemas_1.SCHEMA_BASE_URL}/summary-asset.schema.json`;
18
- function normalizeRelativePath(filePath) {
19
- return filePath.split(node_path_1.default.sep).join("/");
20
- }
21
18
  function getSummaryDir(manifestPath) {
22
19
  return node_path_1.default.resolve(node_path_1.default.dirname(manifestPath), "summaries");
23
20
  }
24
21
  function getSummaryPath(summaryDir, hash) {
25
22
  return node_path_1.default.resolve(summaryDir, `${hash}.json`);
26
23
  }
27
- function normalizeGlobPattern(pattern) {
28
- return pattern.split(node_path_1.default.sep).join("/");
29
- }
30
- function escapeRegex(value) {
31
- return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
32
- }
33
- function globToRegExp(pattern) {
34
- const normalized = normalizeGlobPattern(pattern);
35
- let regexBody = "";
36
- for (let i = 0; i < normalized.length; i += 1) {
37
- const char = normalized[i];
38
- const next = normalized[i + 1];
39
- if (char === "*" && next === "*") {
40
- regexBody += ".*";
41
- i += 1;
42
- continue;
43
- }
44
- if (char === "*") {
45
- regexBody += "[^/]*";
46
- continue;
47
- }
48
- if (char === "?") {
49
- regexBody += "[^/]";
50
- continue;
51
- }
52
- regexBody += escapeRegex(char);
53
- }
54
- return new RegExp(`^${regexBody}$`);
55
- }
56
- function compileGlobs(patterns) {
57
- return patterns.filter(Boolean).map(globToRegExp);
58
- }
59
- function matchesAny(filePath, patterns) {
60
- return patterns.some((pattern) => pattern.test(filePath));
61
- }
62
- function shouldIncludeFile(filePath, includeGlobs, excludeGlobs, ignoreMatcher) {
63
- if (ignoreMatcher.ignores(filePath)) {
64
- return false;
65
- }
66
- if (excludeGlobs.length > 0 && matchesAny(filePath, excludeGlobs)) {
67
- return false;
68
- }
69
- if (includeGlobs.length > 0 && !matchesAny(filePath, includeGlobs)) {
70
- return false;
71
- }
72
- return true;
73
- }
74
- function isIgnoredDirectory(dirPath, ignoreMatcher) {
75
- return ignoreMatcher.ignores(dirPath) || ignoreMatcher.ignores(`${dirPath}/`);
76
- }
77
24
  async function atomicWriteUtf8(targetPath, content) {
78
25
  await promises_1.default.mkdir(node_path_1.default.dirname(targetPath), { recursive: true });
79
26
  const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`;
@@ -182,44 +129,6 @@ async function readManifest(manifestPath) {
182
129
  throw error;
183
130
  }
184
131
  }
185
- async function readIgnoreMatcher(repoPath, ignoreFilePath) {
186
- const matcher = (0, ignore_1.default)();
187
- const absolutePath = node_path_1.default.isAbsolute(ignoreFilePath)
188
- ? ignoreFilePath
189
- : node_path_1.default.resolve(repoPath, ignoreFilePath);
190
- try {
191
- const raw = await promises_1.default.readFile(absolutePath, "utf8");
192
- return matcher.add(raw);
193
- }
194
- catch (error) {
195
- const nodeError = error;
196
- if (nodeError.code === "ENOENT") {
197
- return matcher;
198
- }
199
- throw error;
200
- }
201
- }
202
- async function walkCodeFiles(rootPath, includeGlobs, excludeGlobs, ignoreMatcher, currentPath = rootPath) {
203
- const entries = await promises_1.default.readdir(currentPath, { withFileTypes: true });
204
- const files = [];
205
- for (const entry of entries) {
206
- const entryPath = node_path_1.default.join(currentPath, entry.name);
207
- if (entry.isDirectory()) {
208
- const relativePath = normalizeRelativePath(node_path_1.default.relative(rootPath, entryPath));
209
- if (!isIgnoredDirectory(relativePath, ignoreMatcher)) {
210
- files.push(...await walkCodeFiles(rootPath, includeGlobs, excludeGlobs, ignoreMatcher, entryPath));
211
- }
212
- continue;
213
- }
214
- if (entry.isFile()) {
215
- const relativePath = normalizeRelativePath(node_path_1.default.relative(rootPath, entryPath));
216
- if (shouldIncludeFile(relativePath, includeGlobs, excludeGlobs, ignoreMatcher)) {
217
- files.push(relativePath);
218
- }
219
- }
220
- }
221
- return files.sort();
222
- }
223
132
  function countHashRefs(files) {
224
133
  const refs = new Map();
225
134
  for (const hash of Object.values(files)) {
@@ -330,19 +239,19 @@ async function runSummarize(options, config) {
330
239
  }
331
240
  const startedAt = new Date();
332
241
  const commandCwd = process.cwd();
333
- const repoPath = node_path_1.default.resolve(commandCwd, options.path);
242
+ const repoPath = node_path_1.default.resolve(commandCwd, options.path || config.repoPath);
334
243
  const manifestPath = (0, paths_1.resolveDiffdocArtifactPath)(options.out, config.baseDir);
335
244
  const summaryDir = getSummaryDir(manifestPath);
336
245
  const manifest = await readManifest(manifestPath);
337
246
  const refs = countHashRefs(manifest.files);
338
- const includePatterns = compileGlobs((options.includeGlobs && options.includeGlobs.length > 0)
339
- ? options.includeGlobs.map(normalizeGlobPattern)
340
- : config.summarize.includeGlobs.map(normalizeGlobPattern));
341
- const excludePatterns = compileGlobs((options.excludeGlobs && options.excludeGlobs.length > 0)
342
- ? options.excludeGlobs.map(normalizeGlobPattern)
343
- : config.summarize.excludeGlobs.map(normalizeGlobPattern));
247
+ const includePatterns = (0, scan_1.compileGlobs)((options.includeGlobs && options.includeGlobs.length > 0)
248
+ ? options.includeGlobs.map(scan_1.normalizeGlobPattern)
249
+ : config.summarize.includeGlobs.map(scan_1.normalizeGlobPattern));
250
+ const excludePatterns = (0, scan_1.compileGlobs)((options.excludeGlobs && options.excludeGlobs.length > 0)
251
+ ? options.excludeGlobs.map(scan_1.normalizeGlobPattern)
252
+ : config.summarize.excludeGlobs.map(scan_1.normalizeGlobPattern));
344
253
  const ignoreFile = options.ignoreFile || config.summarize.ignoreFile;
345
- const ignoreMatcher = await readIgnoreMatcher(repoPath, ignoreFile);
254
+ const ignoreMatcher = await (0, scan_1.readIgnoreMatcher)(repoPath, ignoreFile);
346
255
  const customPromptHash = getPromptHash(config);
347
256
  const customPromptSource = customPromptHash ? config.summarize.summaryPromptSource : undefined;
348
257
  const summaryFreshnessExpected = (hash) => ({
@@ -400,7 +309,7 @@ async function runSummarize(options, config) {
400
309
  manifest.files = {};
401
310
  refs.clear();
402
311
  await writeManifest(manifestPath, manifest);
403
- const files = await walkCodeFiles(repoPath, includePatterns, excludePatterns, ignoreMatcher);
312
+ const files = await (0, scan_1.walkCodeFiles)(repoPath, includePatterns, excludePatterns, ignoreMatcher);
404
313
  const totalFiles = files.length;
405
314
  let completedFiles = 0;
406
315
  if (!isJson) {
@@ -462,7 +371,7 @@ async function runSummarize(options, config) {
462
371
  totals.scanned += 1;
463
372
  });
464
373
  try {
465
- if (!shouldIncludeFile(filePath, includePatterns, excludePatterns, ignoreMatcher)) {
374
+ if (!(0, scan_1.shouldIncludeFile)(filePath, includePatterns, excludePatterns, ignoreMatcher)) {
466
375
  await withManifestLock(async () => {
467
376
  const removed = await removeManifestPath(filePath, manifest, manifestPath, summaryDir, refs);
468
377
  if (removed) {
@@ -7,6 +7,7 @@ exports.runValidate = runValidate;
7
7
  const promises_1 = __importDefault(require("node:fs/promises"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const artifacts_1 = require("../types/artifacts");
10
+ const schemas_1 = require("../schemas");
10
11
  const paths_1 = require("../utils/paths");
11
12
  function getSummaryDir(manifestPath) {
12
13
  return node_path_1.default.resolve(node_path_1.default.dirname(manifestPath), "summaries");
@@ -80,6 +81,8 @@ async function runValidate(options, config) {
80
81
  }
81
82
  }
82
83
  const report = {
84
+ schemaVersion: schemas_1.SCHEMA_DIR_VERSION,
85
+ schemaBaseUri: schemas_1.SCHEMA_BASE_URL,
83
86
  valid: manifestValid && issues.length === 0,
84
87
  manifestPath,
85
88
  manifestValid,
@@ -91,6 +94,8 @@ async function runValidate(options, config) {
91
94
  console.log(JSON.stringify(report, null, 2));
92
95
  }
93
96
  else {
97
+ console.log(`Schema version: v${schemas_1.SCHEMA_DIR_VERSION}`);
98
+ console.log(`Schema URI: ${schemas_1.SCHEMA_BASE_URL}`);
94
99
  console.log(`Manifest: ${manifestPath}`);
95
100
  console.log(`Manifest valid: ${manifestValid ? "yes" : "NO"}`);
96
101
  console.log(`Summary assets checked: ${summaryAssetsChecked}`);
package/dist/config.js CHANGED
@@ -123,6 +123,7 @@ function buildRuntimeConfig(options, needs = { chat: true, embeddings: true }) {
123
123
  }
124
124
  return {
125
125
  baseDir: readOption(mergedOptions.baseDir, "DIFFDOC_BASE_DIR", "./.diffdoc"),
126
+ repoPath: readOption(mergedOptions.repoPath, "DIFFDOC_REPO_PATH", "."),
126
127
  provider,
127
128
  chat: {
128
129
  apiKey,
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ const commander_1 = require("commander");
5
5
  const config_1 = require("./config");
6
6
  const embed_1 = require("./commands/embed");
7
7
  const init_1 = require("./commands/init");
8
+ const prune_1 = require("./commands/prune");
8
9
  const query_1 = require("./commands/query");
9
10
  const status_1 = require("./commands/status");
10
11
  const summarize_1 = require("./commands/summarize");
@@ -43,7 +44,7 @@ function addCloudEndpointAndKeyOptions(command) {
43
44
  program
44
45
  .name("diffdoc")
45
46
  .description("Translate repository code shifts into plain-English business context")
46
- .version("0.6.0");
47
+ .version("0.6.4");
47
48
  program
48
49
  .command("init")
49
50
  .description("Initialize DiffDoc configuration for this repository")
@@ -146,12 +147,31 @@ addCloudEndpointAndKeyOptions(addEmbeddingOptions(addBaseOptions(program
146
147
  addBaseOptions(program
147
148
  .command("status"))
148
149
  .description("Show manifest and index sync status")
150
+ .option("--path <path>", "repository or code path to scan")
149
151
  .option("--manifest <path>", "manifest input path under --base-dir", "manifest.json")
150
152
  .option("--json", "print status as JSON for CI", false)
151
153
  .action(async (options) => {
152
154
  try {
153
155
  const config = (0, config_1.buildRuntimeConfig)(options, { embeddings: false, chat: false });
154
- await (0, status_1.runStatus)({ manifest: options.manifest, json: options.json }, config);
156
+ await (0, status_1.runStatus)({ path: options.path, manifest: options.manifest, json: options.json }, config);
157
+ }
158
+ catch (error) {
159
+ console.error(error instanceof Error ? error.message : error);
160
+ process.exit(1);
161
+ }
162
+ });
163
+ addBaseOptions(program
164
+ .command("prune"))
165
+ .description("Remove manifest entries for deleted or excluded files")
166
+ .option("--path <path>", "repository or code path to scan")
167
+ .option("--manifest <path>", "manifest input path under --base-dir", "manifest.json")
168
+ .option("--dry-run", "show what would be pruned without executing", false)
169
+ .option("--yes", "skip confirmation prompt", false)
170
+ .option("--json", "print prune report as JSON for CI", false)
171
+ .action(async (options) => {
172
+ try {
173
+ const config = (0, config_1.buildRuntimeConfig)(options, { embeddings: false, chat: false });
174
+ await (0, prune_1.runPrune)({ path: options.path, manifest: options.manifest, dryRun: options.dryRun, yes: options.yes, json: options.json }, config);
155
175
  }
156
176
  catch (error) {
157
177
  console.error(error instanceof Error ? error.message : error);
package/dist/schemas.js CHANGED
@@ -15,6 +15,7 @@ exports.SCHEMA_BASE_URL = `https://raw.githubusercontent.com/sullyTheDev/diffdoc
15
15
  exports.DiffdocConfigSchema = zod_1.z.object({
16
16
  $schema: zod_1.z.string().optional(),
17
17
  baseDir: zod_1.z.string().optional(),
18
+ repoPath: zod_1.z.string().optional(),
18
19
  aiProvider: zod_1.z.enum(["local", "cloud"]).optional(),
19
20
  localLlmEndpoint: zod_1.z.string().optional(),
20
21
  localEmbedEndpoint: zod_1.z.string().optional(),
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.normalizeRelativePath = normalizeRelativePath;
7
+ exports.normalizeGlobPattern = normalizeGlobPattern;
8
+ exports.compileGlobs = compileGlobs;
9
+ exports.shouldIncludeFile = shouldIncludeFile;
10
+ exports.readIgnoreMatcher = readIgnoreMatcher;
11
+ exports.walkCodeFiles = walkCodeFiles;
12
+ const promises_1 = __importDefault(require("node:fs/promises"));
13
+ const node_path_1 = __importDefault(require("node:path"));
14
+ const ignore_1 = __importDefault(require("ignore"));
15
+ function normalizeRelativePath(filePath) {
16
+ return filePath.split(node_path_1.default.sep).join("/");
17
+ }
18
+ function normalizeGlobPattern(pattern) {
19
+ return pattern.split(node_path_1.default.sep).join("/");
20
+ }
21
+ function escapeRegex(value) {
22
+ return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
23
+ }
24
+ function globToRegExp(pattern) {
25
+ const normalized = normalizeGlobPattern(pattern);
26
+ let regexBody = "";
27
+ for (let i = 0; i < normalized.length; i += 1) {
28
+ const char = normalized[i];
29
+ const next = normalized[i + 1];
30
+ if (char === "*" && next === "*") {
31
+ regexBody += ".*";
32
+ i += 1;
33
+ continue;
34
+ }
35
+ if (char === "*") {
36
+ regexBody += "[^/]*";
37
+ continue;
38
+ }
39
+ if (char === "?") {
40
+ regexBody += "[^/]";
41
+ continue;
42
+ }
43
+ regexBody += escapeRegex(char);
44
+ }
45
+ return new RegExp(`^${regexBody}$`);
46
+ }
47
+ function compileGlobs(patterns) {
48
+ return patterns.filter(Boolean).map(globToRegExp);
49
+ }
50
+ function matchesAny(filePath, patterns) {
51
+ return patterns.some((pattern) => pattern.test(filePath));
52
+ }
53
+ function shouldIncludeFile(filePath, includeGlobs, excludeGlobs, ignoreMatcher) {
54
+ if (ignoreMatcher.ignores(filePath)) {
55
+ return false;
56
+ }
57
+ if (excludeGlobs.length > 0 && matchesAny(filePath, excludeGlobs)) {
58
+ return false;
59
+ }
60
+ if (includeGlobs.length > 0 && !matchesAny(filePath, includeGlobs)) {
61
+ return false;
62
+ }
63
+ return true;
64
+ }
65
+ function isIgnoredDirectory(dirPath, ignoreMatcher) {
66
+ return ignoreMatcher.ignores(dirPath) || ignoreMatcher.ignores(`${dirPath}/`);
67
+ }
68
+ async function readIgnoreMatcher(repoPath, ignoreFilePath) {
69
+ const matcher = (0, ignore_1.default)();
70
+ const absolutePath = node_path_1.default.isAbsolute(ignoreFilePath)
71
+ ? ignoreFilePath
72
+ : node_path_1.default.resolve(repoPath, ignoreFilePath);
73
+ try {
74
+ const raw = await promises_1.default.readFile(absolutePath, "utf8");
75
+ return matcher.add(raw);
76
+ }
77
+ catch (error) {
78
+ const nodeError = error;
79
+ if (nodeError.code === "ENOENT") {
80
+ return matcher;
81
+ }
82
+ throw error;
83
+ }
84
+ }
85
+ async function walkCodeFiles(rootPath, includeGlobs, excludeGlobs, ignoreMatcher, currentPath = rootPath) {
86
+ const entries = await promises_1.default.readdir(currentPath, { withFileTypes: true });
87
+ const files = [];
88
+ for (const entry of entries) {
89
+ const entryPath = node_path_1.default.join(currentPath, entry.name);
90
+ if (entry.isDirectory()) {
91
+ const relativePath = normalizeRelativePath(node_path_1.default.relative(rootPath, entryPath));
92
+ if (!isIgnoredDirectory(relativePath, ignoreMatcher)) {
93
+ files.push(...await walkCodeFiles(rootPath, includeGlobs, excludeGlobs, ignoreMatcher, entryPath));
94
+ }
95
+ continue;
96
+ }
97
+ if (entry.isFile()) {
98
+ const relativePath = normalizeRelativePath(node_path_1.default.relative(rootPath, entryPath));
99
+ if (shouldIncludeFile(relativePath, includeGlobs, excludeGlobs, ignoreMatcher)) {
100
+ files.push(relativePath);
101
+ }
102
+ }
103
+ }
104
+ return files.sort();
105
+ }
package/package.json CHANGED
@@ -1,52 +1,52 @@
1
- {
2
- "name": "diffdoc",
3
- "version": "0.6.2",
4
- "description": "Translate repository code shifts into plain-English business context",
5
- "license": "MIT",
6
- "author": "Christopher Sullivan",
7
- "homepage": "https://github.com/sullyTheDev/diffdoc#readme",
8
- "bugs": {
9
- "url": "https://github.com/sullyTheDev/diffdoc/issues"
10
- },
11
- "repository": {
12
- "type": "git",
13
- "url": "git+https://github.com/sullyTheDev/diffdoc.git"
14
- },
15
- "type": "commonjs",
16
- "main": "dist/index.js",
17
- "bin": {
18
- "diffdoc": "dist/index.js",
19
- "diffdoc-mcp": "dist/mcp.js"
20
- },
21
- "files": [
22
- "dist",
23
- "schemas",
24
- "README.md",
25
- "LICENSE",
26
- ".diffdocrc.example"
27
- ],
28
- "engines": {
29
- "node": ">=22"
30
- },
31
- "scripts": {
32
- "build": "tsc && node dist/scripts/generate-schemas.js",
33
- "generate:schemas": "node dist/scripts/generate-schemas.js",
34
- "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
35
- "start": "tsc && node ./dist/index.js",
36
- "prepare": "npm run build"
37
- },
38
- "dependencies": {
39
- "@modelcontextprotocol/sdk": "^1.29.0",
40
- "commander": "^12.0.0",
41
- "ignore": "^7.0.5",
42
- "openai": "^4.28.0",
43
- "simple-git": "^3.24.0",
44
- "vectra": "^0.14.0",
45
- "zod": "^3.25.76"
46
- },
47
- "devDependencies": {
48
- "@types/node": "^20.19.41",
49
- "typescript": "^5.3.3",
50
- "zod-to-json-schema": "^3.25.2"
51
- }
52
- }
1
+ {
2
+ "name": "diffdoc",
3
+ "version": "0.6.4",
4
+ "description": "Translate repository code shifts into plain-English business context",
5
+ "license": "MIT",
6
+ "author": "Christopher Sullivan",
7
+ "homepage": "https://github.com/sullyTheDev/diffdoc#readme",
8
+ "bugs": {
9
+ "url": "https://github.com/sullyTheDev/diffdoc/issues"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/sullyTheDev/diffdoc.git"
14
+ },
15
+ "type": "commonjs",
16
+ "main": "dist/index.js",
17
+ "bin": {
18
+ "diffdoc": "dist/index.js",
19
+ "diffdoc-mcp": "dist/mcp.js"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "schemas",
24
+ "README.md",
25
+ "LICENSE",
26
+ ".diffdocrc.example"
27
+ ],
28
+ "engines": {
29
+ "node": ">=22"
30
+ },
31
+ "scripts": {
32
+ "build": "tsc && node dist/scripts/generate-schemas.js",
33
+ "generate:schemas": "node dist/scripts/generate-schemas.js",
34
+ "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
35
+ "start": "tsc && node ./dist/index.js",
36
+ "prepare": "npm run build"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.29.0",
40
+ "commander": "^12.0.0",
41
+ "ignore": "^7.0.5",
42
+ "openai": "^4.28.0",
43
+ "simple-git": "^3.24.0",
44
+ "vectra": "^0.14.0",
45
+ "zod": "^3.25.76"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^20.19.41",
49
+ "typescript": "^5.3.3",
50
+ "zod-to-json-schema": "^3.25.2"
51
+ }
52
+ }
@@ -10,6 +10,9 @@
10
10
  "baseDir": {
11
11
  "type": "string"
12
12
  },
13
+ "repoPath": {
14
+ "type": "string"
15
+ },
13
16
  "aiProvider": {
14
17
  "type": "string",
15
18
  "enum": [