diffdoc 0.3.0 → 0.4.1
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/.diffdocrc.example +1 -0
- package/README.md +57 -7
- package/dist/commands/embed.js +23 -22
- package/dist/commands/init.js +221 -0
- package/dist/commands/status.js +166 -0
- package/dist/commands/summarize.js +114 -17
- package/dist/config.js +14 -1
- package/dist/index.js +39 -18
- package/dist/services/retrieval.js +1 -1
- package/dist/utils/llm.js +2 -2
- package/package.json +3 -3
package/.diffdocrc.example
CHANGED
package/README.md
CHANGED
|
@@ -34,7 +34,9 @@ Package scripts can call the installed binary:
|
|
|
34
34
|
```json
|
|
35
35
|
{
|
|
36
36
|
"scripts": {
|
|
37
|
+
"diffdoc:init": "diffdoc init",
|
|
37
38
|
"diffdoc:summarize": "diffdoc summarize",
|
|
39
|
+
"diffdoc:status": "diffdoc status",
|
|
38
40
|
"diffdoc:embed": "diffdoc embed",
|
|
39
41
|
"diffdoc:search": "diffdoc search",
|
|
40
42
|
"diffdoc:query": "diffdoc query",
|
|
@@ -114,6 +116,26 @@ DiffDoc includes `diffdoc embed` as a built-in convenience path for creating a l
|
|
|
114
116
|
|
|
115
117
|
## Commands
|
|
116
118
|
|
|
119
|
+
Initialize DiffDoc configuration for a repository:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
diffdoc init
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Use defaults without prompts:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
diffdoc init --yes
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Choose a provider and overwrite an existing config file:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
diffdoc init --provider cloud --force
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`init` creates or updates repo setup files, appends missing `.gitignore` entries, and prints next commands. It does not run `summarize` or `embed`.
|
|
138
|
+
|
|
117
139
|
Summarize a repository into `./.diffdoc/manifest.json`:
|
|
118
140
|
|
|
119
141
|
```bash
|
|
@@ -138,12 +160,42 @@ Add include/exclude filters at runtime:
|
|
|
138
160
|
diffdoc summarize --path . --mode all --include-glob "src/**/*.ts" --exclude-glob "**/*.test.ts"
|
|
139
161
|
```
|
|
140
162
|
|
|
163
|
+
Emit a CI-friendly JSON summarize report:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
diffdoc summarize --path . --mode delta --json
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Inspect manifest-relative artifact freshness:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
diffdoc status
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Use a custom manifest path under `--base-dir`:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
diffdoc status --manifest manifest.json
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Emit CI-friendly JSON output:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
diffdoc status --json
|
|
185
|
+
```
|
|
186
|
+
|
|
141
187
|
Embed the manifest into a local Vectra index at `./.diffdoc/vectra`:
|
|
142
188
|
|
|
143
189
|
```bash
|
|
144
190
|
diffdoc embed
|
|
145
191
|
```
|
|
146
192
|
|
|
193
|
+
Limit how many summary documents are sent per embeddings request:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
diffdoc embed --embed-batch-size 20
|
|
197
|
+
```
|
|
198
|
+
|
|
147
199
|
Force full index rebuild:
|
|
148
200
|
|
|
149
201
|
```bash
|
|
@@ -174,12 +226,6 @@ Include retrieved code snapshots after the generated answer:
|
|
|
174
226
|
diffdoc query "How does embedding work?" --top 3 --code
|
|
175
227
|
```
|
|
176
228
|
|
|
177
|
-
Prompt the configured chat model directly:
|
|
178
|
-
|
|
179
|
-
```bash
|
|
180
|
-
diffdoc prompt "Confirm the configured model is reachable."
|
|
181
|
-
```
|
|
182
|
-
|
|
183
229
|
Use a custom config file:
|
|
184
230
|
|
|
185
231
|
```bash
|
|
@@ -259,7 +305,11 @@ Run `diffdoc summarize` and `diffdoc embed` before using the MCP server, otherwi
|
|
|
259
305
|
- Manifest schema is currently `schemaVersion: 2`; older manifest shapes are not auto-migrated.
|
|
260
306
|
- Commit `.diffdoc/manifest.json` when using delta workflows. Delta summarization reads the previous manifest state to decide which changed files need fresh summaries.
|
|
261
307
|
- `summarize` requires a configured chat model.
|
|
262
|
-
- `
|
|
308
|
+
- `summarize` prints run progress and final totals (`scanned`, `skipped`, `updated`, `failed`, `pruned`).
|
|
309
|
+
- `summarize --json` prints a single machine-readable run report to stdout for CI parsing.
|
|
310
|
+
- `status` does not require a configured chat or embedding model.
|
|
311
|
+
- `status --json` prints a machine-readable report with summary and index freshness details.
|
|
312
|
+
- `embed` requires a configured embedding model. Use `embedBatchSize` in `.diffdocrc`, `DIFFDOC_EMBED_BATCH_SIZE`, or `--embed-batch-size` to tune how many summary documents are sent per embeddings request.
|
|
263
313
|
- `search` requires a configured embedding model and returns raw retrieval results without calling the chat model.
|
|
264
314
|
- `query` requires both a configured chat model and embedding model.
|
|
265
315
|
- For code-oriented embedding models such as `nomic-embed-code`, DiffDoc prefixes query embeddings with `Represent this query for searching relevant code:`.
|
package/dist/commands/embed.js
CHANGED
|
@@ -111,30 +111,31 @@ async function runEmbed(options, config) {
|
|
|
111
111
|
console.log(`Index is already up to date at ${indexPath}.`);
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
|
-
const embeddings = toUpsert.length > 0
|
|
115
|
-
? await (0, llm_1.generateEmbeddings)(toUpsert.map((item) => item.document), config.embeddings)
|
|
116
|
-
: [];
|
|
117
114
|
await index.beginUpdate();
|
|
118
115
|
try {
|
|
119
|
-
for (let
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
116
|
+
for (let start = 0; start < toUpsert.length; start += config.embeddings.batchSize) {
|
|
117
|
+
const batch = toUpsert.slice(start, start + config.embeddings.batchSize);
|
|
118
|
+
const embeddings = await (0, llm_1.generateEmbeddings)(batch.map((item) => item.document), config.embeddings);
|
|
119
|
+
for (let i = 0; i < batch.length; i += 1) {
|
|
120
|
+
const item = batch[i];
|
|
121
|
+
const metadata = item.rawCodeSnapshot
|
|
122
|
+
? {
|
|
123
|
+
filePath: item.filePath,
|
|
124
|
+
hash: item.hash,
|
|
125
|
+
summaryText: item.summaryText,
|
|
126
|
+
rawCodeSnapshot: item.rawCodeSnapshot
|
|
127
|
+
}
|
|
128
|
+
: {
|
|
129
|
+
filePath: item.filePath,
|
|
130
|
+
hash: item.hash,
|
|
131
|
+
summaryText: item.summaryText
|
|
132
|
+
};
|
|
133
|
+
await index.upsertItem({
|
|
134
|
+
id: item.filePath,
|
|
135
|
+
vector: embeddings[i],
|
|
136
|
+
metadata
|
|
137
|
+
});
|
|
138
|
+
}
|
|
138
139
|
}
|
|
139
140
|
for (const itemId of toDelete) {
|
|
140
141
|
await index.deleteItem(itemId);
|
|
@@ -0,0 +1,221 @@
|
|
|
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.runInit = runInit;
|
|
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 DEFAULT_CONFIG = {
|
|
12
|
+
baseDir: "./.diffdoc",
|
|
13
|
+
aiProvider: "local",
|
|
14
|
+
localLlmEndpoint: "http://localhost:11434/v1",
|
|
15
|
+
localEmbedEndpoint: "http://localhost:11434/v1/embeddings",
|
|
16
|
+
localChatModel: "qwen2.5-coder:7b",
|
|
17
|
+
localEmbedModel: "nomic-embed-code",
|
|
18
|
+
cloudLlmEndpoint: "https://api.openai.com/v1",
|
|
19
|
+
cloudChatModel: "gpt-4o-mini",
|
|
20
|
+
cloudEmbedModel: "text-embedding-3-small",
|
|
21
|
+
openaiApiKey: "",
|
|
22
|
+
includeGlobs: [],
|
|
23
|
+
excludeGlobs: [],
|
|
24
|
+
ignoreFile: ".diffdocignore"
|
|
25
|
+
};
|
|
26
|
+
function parseProvider(value, fallback) {
|
|
27
|
+
const provider = value || fallback;
|
|
28
|
+
if (provider !== "local" && provider !== "cloud") {
|
|
29
|
+
throw new Error('Invalid init provider. Expected "local" or "cloud".');
|
|
30
|
+
}
|
|
31
|
+
return provider;
|
|
32
|
+
}
|
|
33
|
+
function parseCsv(value) {
|
|
34
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
function toDisplayPath(filePath) {
|
|
37
|
+
return filePath.split(node_path_1.default.sep).join("/");
|
|
38
|
+
}
|
|
39
|
+
function resolveRepoPath(filePath) {
|
|
40
|
+
return node_path_1.default.resolve(process.cwd(), filePath);
|
|
41
|
+
}
|
|
42
|
+
function relativeToRepo(absolutePath) {
|
|
43
|
+
const relative = node_path_1.default.relative(process.cwd(), absolutePath) || ".";
|
|
44
|
+
return toDisplayPath(relative);
|
|
45
|
+
}
|
|
46
|
+
function normalizeRepoPattern(value) {
|
|
47
|
+
return toDisplayPath(value.trim())
|
|
48
|
+
.replace(/^\.\//, "")
|
|
49
|
+
.replace(/\/+/g, "/")
|
|
50
|
+
.replace(/\/$/, "");
|
|
51
|
+
}
|
|
52
|
+
async function fileExists(filePath) {
|
|
53
|
+
try {
|
|
54
|
+
await promises_1.default.access(filePath);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function readExistingConfig(configPath) {
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(await promises_1.default.readFile(configPath, "utf8"));
|
|
64
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
const nodeError = error;
|
|
68
|
+
if (nodeError.code === "ENOENT") {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function promptText(rl, question, fallback) {
|
|
75
|
+
const suffix = fallback ? ` (${fallback})` : "";
|
|
76
|
+
const answer = (await rl.question(`${question}${suffix}: `)).trim();
|
|
77
|
+
return answer || fallback;
|
|
78
|
+
}
|
|
79
|
+
async function promptBoolean(rl, question, fallback) {
|
|
80
|
+
const suffix = fallback ? "Y/n" : "y/N";
|
|
81
|
+
const answer = (await rl.question(`${question} (${suffix}): `)).trim().toLowerCase();
|
|
82
|
+
if (!answer)
|
|
83
|
+
return fallback;
|
|
84
|
+
return answer === "y" || answer === "yes";
|
|
85
|
+
}
|
|
86
|
+
async function buildInteractiveConfig(options, existing) {
|
|
87
|
+
const rl = (0, promises_2.createInterface)({ input: node_process_1.stdin, output: node_process_1.stdout });
|
|
88
|
+
try {
|
|
89
|
+
const fallbackProvider = parseProvider(existing.aiProvider, "local");
|
|
90
|
+
const provider = parseProvider(options.provider || await promptText(rl, "AI provider: local or cloud", fallbackProvider), fallbackProvider);
|
|
91
|
+
const baseDir = options.baseDir || await promptText(rl, "DiffDoc artifact directory", existing.baseDir || DEFAULT_CONFIG.baseDir);
|
|
92
|
+
const ignoreFile = await promptText(rl, "Ignore file path", existing.ignoreFile || DEFAULT_CONFIG.ignoreFile);
|
|
93
|
+
const includeGlobs = parseCsv(await promptText(rl, "Include globs, comma-separated", (existing.includeGlobs || []).join(",")));
|
|
94
|
+
const excludeGlobs = parseCsv(await promptText(rl, "Exclude globs, comma-separated", (existing.excludeGlobs || []).join(",")));
|
|
95
|
+
const config = {
|
|
96
|
+
...DEFAULT_CONFIG,
|
|
97
|
+
...existing,
|
|
98
|
+
baseDir,
|
|
99
|
+
aiProvider: provider,
|
|
100
|
+
ignoreFile,
|
|
101
|
+
includeGlobs,
|
|
102
|
+
excludeGlobs
|
|
103
|
+
};
|
|
104
|
+
if (provider === "local") {
|
|
105
|
+
config.localLlmEndpoint = await promptText(rl, "Local chat endpoint", config.localLlmEndpoint || DEFAULT_CONFIG.localLlmEndpoint);
|
|
106
|
+
config.localChatModel = await promptText(rl, "Local chat model", config.localChatModel || DEFAULT_CONFIG.localChatModel);
|
|
107
|
+
config.localEmbedEndpoint = await promptText(rl, "Local embedding endpoint", config.localEmbedEndpoint || DEFAULT_CONFIG.localEmbedEndpoint);
|
|
108
|
+
config.localEmbedModel = await promptText(rl, "Local embedding model", config.localEmbedModel || DEFAULT_CONFIG.localEmbedModel);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
config.cloudLlmEndpoint = await promptText(rl, "Cloud OpenAI-compatible endpoint", config.cloudLlmEndpoint || DEFAULT_CONFIG.cloudLlmEndpoint);
|
|
112
|
+
config.cloudChatModel = await promptText(rl, "Cloud chat model", config.cloudChatModel || DEFAULT_CONFIG.cloudChatModel);
|
|
113
|
+
config.cloudEmbedModel = await promptText(rl, "Cloud embedding model", config.cloudEmbedModel || DEFAULT_CONFIG.cloudEmbedModel);
|
|
114
|
+
if (await promptBoolean(rl, "Store OPENAI_API_KEY in config file", false)) {
|
|
115
|
+
config.openaiApiKey = await promptText(rl, "OpenAI-compatible API key", config.openaiApiKey || "");
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
config.openaiApiKey = "";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return config;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
rl.close();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function buildYesConfig(options, existing) {
|
|
128
|
+
return {
|
|
129
|
+
...DEFAULT_CONFIG,
|
|
130
|
+
...existing,
|
|
131
|
+
baseDir: options.baseDir || existing.baseDir || DEFAULT_CONFIG.baseDir,
|
|
132
|
+
aiProvider: parseProvider(options.provider || existing.aiProvider, "local"),
|
|
133
|
+
openaiApiKey: ""
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async function writeJsonFile(filePath, value, summary, force) {
|
|
137
|
+
const exists = await fileExists(filePath);
|
|
138
|
+
if (exists && !force) {
|
|
139
|
+
summary.skipped.push(relativeToRepo(filePath));
|
|
140
|
+
summary.warnings.push(`${relativeToRepo(filePath)} already exists; pass --force to overwrite.`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(filePath), { recursive: true });
|
|
144
|
+
await promises_1.default.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
145
|
+
(exists ? summary.updated : summary.created).push(relativeToRepo(filePath));
|
|
146
|
+
}
|
|
147
|
+
function buildStarterIgnore(baseDir) {
|
|
148
|
+
const normalizedBaseDir = normalizeRepoPattern(baseDir);
|
|
149
|
+
return [
|
|
150
|
+
"# DiffDoc ignore patterns",
|
|
151
|
+
"node_modules/**",
|
|
152
|
+
".git/**",
|
|
153
|
+
`${normalizedBaseDir}/**`,
|
|
154
|
+
"dist/**",
|
|
155
|
+
""
|
|
156
|
+
].join("\n");
|
|
157
|
+
}
|
|
158
|
+
async function createIgnoreFile(ignorePath, config, summary) {
|
|
159
|
+
if (await fileExists(ignorePath)) {
|
|
160
|
+
summary.skipped.push(relativeToRepo(ignorePath));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
await promises_1.default.writeFile(ignorePath, buildStarterIgnore(config.baseDir), "utf8");
|
|
164
|
+
summary.created.push(relativeToRepo(ignorePath));
|
|
165
|
+
}
|
|
166
|
+
function buildGitignoreEntries(configPath, config) {
|
|
167
|
+
const configRelative = normalizeRepoPattern(relativeToRepo(configPath));
|
|
168
|
+
const baseDir = normalizeRepoPattern(config.baseDir);
|
|
169
|
+
return [`${baseDir}/vectra/`, configRelative];
|
|
170
|
+
}
|
|
171
|
+
async function updateGitignore(configPath, config, summary) {
|
|
172
|
+
const gitignorePath = resolveRepoPath(".gitignore");
|
|
173
|
+
const entries = buildGitignoreEntries(configPath, config);
|
|
174
|
+
let existing = "";
|
|
175
|
+
let exists = false;
|
|
176
|
+
try {
|
|
177
|
+
existing = await promises_1.default.readFile(gitignorePath, "utf8");
|
|
178
|
+
exists = true;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
const nodeError = error;
|
|
182
|
+
if (nodeError.code !== "ENOENT")
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
const existingLines = new Set(existing.split(/\r?\n/).map(normalizeRepoPattern).filter(Boolean));
|
|
186
|
+
const missing = entries.filter((entry) => !existingLines.has(normalizeRepoPattern(entry)));
|
|
187
|
+
if (missing.length === 0) {
|
|
188
|
+
summary.skipped.push(".gitignore");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
|
|
192
|
+
await promises_1.default.writeFile(gitignorePath, `${existing}${prefix}${missing.join("\n")}\n`, "utf8");
|
|
193
|
+
(exists ? summary.updated : summary.created).push(".gitignore");
|
|
194
|
+
}
|
|
195
|
+
function printList(label, values) {
|
|
196
|
+
console.log(`${label}: ${values.length > 0 ? values.join(", ") : "none"}`);
|
|
197
|
+
}
|
|
198
|
+
async function runInit(options) {
|
|
199
|
+
const summary = { created: [], updated: [], skipped: [], warnings: [] };
|
|
200
|
+
const configPath = resolveRepoPath(options.config || ".diffdocrc");
|
|
201
|
+
const existingConfig = await readExistingConfig(configPath);
|
|
202
|
+
const config = options.yes
|
|
203
|
+
? buildYesConfig(options, existingConfig)
|
|
204
|
+
: await buildInteractiveConfig(options, existingConfig);
|
|
205
|
+
const ignorePath = resolveRepoPath(config.ignoreFile || ".diffdocignore");
|
|
206
|
+
await writeJsonFile(configPath, config, summary, options.force);
|
|
207
|
+
await createIgnoreFile(ignorePath, config, summary);
|
|
208
|
+
await updateGitignore(configPath, config, summary);
|
|
209
|
+
console.log("DiffDoc init complete.");
|
|
210
|
+
console.log("");
|
|
211
|
+
console.log("Init changes:");
|
|
212
|
+
printList("Created", summary.created);
|
|
213
|
+
printList("Updated", summary.updated);
|
|
214
|
+
printList("Skipped", summary.skipped);
|
|
215
|
+
printList("Warnings", summary.warnings);
|
|
216
|
+
console.log("");
|
|
217
|
+
console.log("Next commands:");
|
|
218
|
+
console.log("1. diffdoc summarize --path . --mode all");
|
|
219
|
+
console.log("2. diffdoc embed");
|
|
220
|
+
console.log("3. diffdoc status");
|
|
221
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
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.runStatus = runStatus;
|
|
7
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const vectra_1 = require("vectra");
|
|
10
|
+
const embed_1 = require("./embed");
|
|
11
|
+
const artifacts_1 = require("../types/artifacts");
|
|
12
|
+
const paths_1 = require("../utils/paths");
|
|
13
|
+
function getSummaryDir(manifestPath) {
|
|
14
|
+
return node_path_1.default.resolve(node_path_1.default.dirname(manifestPath), "summaries");
|
|
15
|
+
}
|
|
16
|
+
async function readManifest(manifestPath) {
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(await promises_1.default.readFile(manifestPath, "utf8"));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const nodeError = error;
|
|
23
|
+
if (nodeError.code === "ENOENT") {
|
|
24
|
+
throw new Error(`Manifest not found: ${manifestPath}. Run \"diffdoc summarize\" first.`);
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
29
|
+
throw new Error(`Invalid manifest JSON in ${manifestPath}. Expected an object.`);
|
|
30
|
+
}
|
|
31
|
+
const manifest = parsed;
|
|
32
|
+
if (manifest.schemaVersion !== artifacts_1.MANIFEST_SCHEMA_VERSION) {
|
|
33
|
+
throw new Error(`Unsupported manifest schema in ${manifestPath}. Expected schemaVersion ${artifacts_1.MANIFEST_SCHEMA_VERSION}.`);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
schemaVersion: artifacts_1.MANIFEST_SCHEMA_VERSION,
|
|
37
|
+
lastSyncedCommit: typeof manifest.lastSyncedCommit === "string" ? manifest.lastSyncedCommit : "",
|
|
38
|
+
files: manifest.files && typeof manifest.files === "object" ? manifest.files : {}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function getSummaryStats(manifestPath, manifest) {
|
|
42
|
+
const summaryDir = getSummaryDir(manifestPath);
|
|
43
|
+
let entries = [];
|
|
44
|
+
try {
|
|
45
|
+
entries = await promises_1.default.readdir(summaryDir);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const nodeError = error;
|
|
49
|
+
if (nodeError.code !== "ENOENT") {
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const summaryHashes = new Set(entries.filter((entry) => entry.endsWith(".json")).map((entry) => entry.slice(0, -5)));
|
|
54
|
+
const manifestHashes = new Set(Object.values(manifest.files));
|
|
55
|
+
let orphanCount = 0;
|
|
56
|
+
for (const hash of summaryHashes) {
|
|
57
|
+
if (!manifestHashes.has(hash)) {
|
|
58
|
+
orphanCount += 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
let missingFromManifestCount = 0;
|
|
62
|
+
for (const hash of manifestHashes) {
|
|
63
|
+
if (!summaryHashes.has(hash)) {
|
|
64
|
+
missingFromManifestCount += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
summaryFileCount: summaryHashes.size,
|
|
69
|
+
orphanCount,
|
|
70
|
+
missingFromManifestCount
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function getIndexFreshness(manifest, config) {
|
|
74
|
+
const indexPath = (0, embed_1.getVectraIndexPath)(config);
|
|
75
|
+
const index = new vectra_1.LocalIndex(indexPath);
|
|
76
|
+
const exists = await index.isIndexCreated();
|
|
77
|
+
if (!exists) {
|
|
78
|
+
return {
|
|
79
|
+
status: "missing",
|
|
80
|
+
missing: 0,
|
|
81
|
+
mismatched: 0,
|
|
82
|
+
extra: 0
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const items = await index.listItems();
|
|
86
|
+
const indexHashesByPath = new Map();
|
|
87
|
+
for (const item of items) {
|
|
88
|
+
if (!item.id || typeof item.id !== "string") {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const hash = item.metadata && typeof item.metadata.hash === "string"
|
|
92
|
+
? item.metadata.hash
|
|
93
|
+
: "";
|
|
94
|
+
indexHashesByPath.set(item.id, hash);
|
|
95
|
+
}
|
|
96
|
+
let missing = 0;
|
|
97
|
+
let mismatched = 0;
|
|
98
|
+
for (const [filePath, manifestHash] of Object.entries(manifest.files)) {
|
|
99
|
+
const indexedHash = indexHashesByPath.get(filePath);
|
|
100
|
+
if (indexedHash === undefined) {
|
|
101
|
+
missing += 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (indexedHash !== manifestHash) {
|
|
105
|
+
mismatched += 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const manifestPathSet = new Set(Object.keys(manifest.files));
|
|
109
|
+
let extra = 0;
|
|
110
|
+
for (const filePath of indexHashesByPath.keys()) {
|
|
111
|
+
if (!manifestPathSet.has(filePath)) {
|
|
112
|
+
extra += 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
status: missing === 0 && mismatched === 0 && extra === 0 ? "fresh" : "stale",
|
|
117
|
+
missing,
|
|
118
|
+
mismatched,
|
|
119
|
+
extra
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function formatSummaryFreshness(stats) {
|
|
123
|
+
if (stats.missingFromManifestCount === 0) {
|
|
124
|
+
return "fresh";
|
|
125
|
+
}
|
|
126
|
+
return `stale (missing: ${stats.missingFromManifestCount})`;
|
|
127
|
+
}
|
|
128
|
+
function buildStatusReport(manifest, summaryStats, indexFreshness) {
|
|
129
|
+
return {
|
|
130
|
+
manifestSchema: manifest.schemaVersion,
|
|
131
|
+
trackedFileCount: Object.keys(manifest.files).length,
|
|
132
|
+
summaryFileCount: summaryStats.summaryFileCount,
|
|
133
|
+
orphanCount: summaryStats.orphanCount,
|
|
134
|
+
summaryFreshness: {
|
|
135
|
+
status: summaryStats.missingFromManifestCount === 0 ? "fresh" : "stale",
|
|
136
|
+
missing: summaryStats.missingFromManifestCount
|
|
137
|
+
},
|
|
138
|
+
indexFreshness
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function formatIndexFreshness(freshness) {
|
|
142
|
+
if (freshness.status === "missing") {
|
|
143
|
+
return "missing";
|
|
144
|
+
}
|
|
145
|
+
if (freshness.status === "fresh") {
|
|
146
|
+
return "fresh";
|
|
147
|
+
}
|
|
148
|
+
return `stale (missing: ${freshness.missing}, mismatched: ${freshness.mismatched}, extra: ${freshness.extra})`;
|
|
149
|
+
}
|
|
150
|
+
async function runStatus(options, config) {
|
|
151
|
+
const manifestPath = (0, paths_1.resolveDiffdocArtifactPath)(options.manifest, config.baseDir);
|
|
152
|
+
const manifest = await readManifest(manifestPath);
|
|
153
|
+
const summaryStats = await getSummaryStats(manifestPath, manifest);
|
|
154
|
+
const indexFreshness = await getIndexFreshness(manifest, config);
|
|
155
|
+
const report = buildStatusReport(manifest, summaryStats, indexFreshness);
|
|
156
|
+
if (options.json) {
|
|
157
|
+
console.log(JSON.stringify(report, null, 2));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
console.log(`manifest schema: ${report.manifestSchema}`);
|
|
161
|
+
console.log(`tracked files: ${report.trackedFileCount}`);
|
|
162
|
+
console.log(`summary files: ${report.summaryFileCount}`);
|
|
163
|
+
console.log(`orphans: ${report.orphanCount}`);
|
|
164
|
+
console.log(`summary freshness: ${formatSummaryFreshness(summaryStats)}`);
|
|
165
|
+
console.log(`index freshness: ${formatIndexFreshness(indexFreshness)}`);
|
|
166
|
+
}
|
|
@@ -182,7 +182,7 @@ async function deleteSummaryIfUnreferenced(summaryDir, hash, refs) {
|
|
|
182
182
|
async function setManifestPathHash(filePath, newHash, manifest, manifestPath, summaryDir, refs) {
|
|
183
183
|
const previousHash = manifest.files[filePath];
|
|
184
184
|
if (previousHash === newHash) {
|
|
185
|
-
return;
|
|
185
|
+
return false;
|
|
186
186
|
}
|
|
187
187
|
if (previousHash) {
|
|
188
188
|
refs.set(previousHash, Math.max((refs.get(previousHash) || 1) - 1, 0));
|
|
@@ -193,16 +193,18 @@ async function setManifestPathHash(filePath, newHash, manifest, manifestPath, su
|
|
|
193
193
|
if (previousHash) {
|
|
194
194
|
await deleteSummaryIfUnreferenced(summaryDir, previousHash, refs);
|
|
195
195
|
}
|
|
196
|
+
return true;
|
|
196
197
|
}
|
|
197
198
|
async function removeManifestPath(filePath, manifest, manifestPath, summaryDir, refs) {
|
|
198
199
|
const previousHash = manifest.files[filePath];
|
|
199
200
|
if (!previousHash) {
|
|
200
|
-
return;
|
|
201
|
+
return false;
|
|
201
202
|
}
|
|
202
203
|
delete manifest.files[filePath];
|
|
203
204
|
refs.set(previousHash, Math.max((refs.get(previousHash) || 1) - 1, 0));
|
|
204
205
|
await writeManifest(manifestPath, manifest);
|
|
205
206
|
await deleteSummaryIfUnreferenced(summaryDir, previousHash, refs);
|
|
207
|
+
return true;
|
|
206
208
|
}
|
|
207
209
|
async function ensureSummaryAsset(summaryDir, hash, summaryText, rawCodeSnapshot, includeCodeSnapshot) {
|
|
208
210
|
const summaryPath = getSummaryPath(summaryDir, hash);
|
|
@@ -245,6 +247,7 @@ async function runSummarize(options, config) {
|
|
|
245
247
|
if (options.mode !== "all" && options.mode !== "delta") {
|
|
246
248
|
throw new Error('Invalid summarize mode. Expected "all" or "delta".');
|
|
247
249
|
}
|
|
250
|
+
const startedAt = new Date();
|
|
248
251
|
const commandCwd = process.cwd();
|
|
249
252
|
const repoPath = node_path_1.default.resolve(commandCwd, options.path);
|
|
250
253
|
const manifestPath = (0, paths_1.resolveDiffdocArtifactPath)(options.out, config.baseDir);
|
|
@@ -259,13 +262,29 @@ async function runSummarize(options, config) {
|
|
|
259
262
|
: config.summarize.excludeGlobs.map(normalizeGlobPattern));
|
|
260
263
|
const ignoreFile = options.ignoreFile || config.summarize.ignoreFile;
|
|
261
264
|
const ignorePatterns = compileGlobs(await readIgnorePatterns(repoPath, ignoreFile));
|
|
265
|
+
const totals = { scanned: 0, skipped: 0, updated: 0, failed: 0, pruned: 0 };
|
|
262
266
|
const failures = [];
|
|
267
|
+
const isJson = options.json;
|
|
268
|
+
if (!isJson) {
|
|
269
|
+
console.log(`Starting summarize run`);
|
|
270
|
+
console.log(`Mode: ${options.mode}`);
|
|
271
|
+
console.log(`Repo: ${repoPath}`);
|
|
272
|
+
console.log(`Manifest: ${manifestPath}`);
|
|
273
|
+
console.log(`Summaries: ${summaryDir}`);
|
|
274
|
+
console.log("---");
|
|
275
|
+
}
|
|
263
276
|
if (options.mode === "all") {
|
|
264
277
|
manifest.files = {};
|
|
265
278
|
refs.clear();
|
|
266
279
|
await writeManifest(manifestPath, manifest);
|
|
267
280
|
const files = await walkCodeFiles(repoPath, includePatterns, excludePatterns, ignorePatterns);
|
|
268
|
-
|
|
281
|
+
const totalFiles = files.length;
|
|
282
|
+
if (!isJson) {
|
|
283
|
+
console.log(`Candidates: ${totalFiles}`);
|
|
284
|
+
}
|
|
285
|
+
for (let i = 0; i < files.length; i += 1) {
|
|
286
|
+
const filePath = files[i];
|
|
287
|
+
totals.scanned += 1;
|
|
269
288
|
try {
|
|
270
289
|
const absolutePath = node_path_1.default.join(repoPath, filePath);
|
|
271
290
|
const rawCodeSnapshot = await promises_1.default.readFile(absolutePath, "utf8");
|
|
@@ -278,25 +297,51 @@ async function runSummarize(options, config) {
|
|
|
278
297
|
manifest.files[filePath] = hash;
|
|
279
298
|
refs.set(hash, (refs.get(hash) || 0) + 1);
|
|
280
299
|
await writeManifest(manifestPath, manifest);
|
|
281
|
-
|
|
300
|
+
totals.updated += 1;
|
|
301
|
+
if (!isJson) {
|
|
302
|
+
console.log(`[${i + 1}/${totalFiles}] summarized ${filePath}`);
|
|
303
|
+
}
|
|
282
304
|
}
|
|
283
305
|
catch (error) {
|
|
284
306
|
const message = error instanceof Error ? error.message : String(error);
|
|
285
307
|
failures.push({ filePath, message });
|
|
286
|
-
|
|
308
|
+
totals.failed += 1;
|
|
309
|
+
if (!isJson) {
|
|
310
|
+
console.error(`[${i + 1}/${totalFiles}] failed ${filePath}: ${message}`);
|
|
311
|
+
}
|
|
287
312
|
}
|
|
288
313
|
}
|
|
289
314
|
}
|
|
290
315
|
else {
|
|
291
316
|
const deltas = await (0, git_1.getGitDeltas)(repoPath, manifest.lastSyncedCommit);
|
|
317
|
+
const totalCandidates = deltas.modifiedOrAdded.length + deltas.deleted.length;
|
|
318
|
+
if (!isJson) {
|
|
319
|
+
console.log(`Candidates: ${totalCandidates} (${deltas.modifiedOrAdded.length} modified/added, ${deltas.deleted.length} deleted)`);
|
|
320
|
+
}
|
|
292
321
|
for (const deletedPath of deltas.deleted) {
|
|
293
|
-
await removeManifestPath(deletedPath, manifest, manifestPath, summaryDir, refs);
|
|
294
|
-
|
|
322
|
+
const removed = await removeManifestPath(deletedPath, manifest, manifestPath, summaryDir, refs);
|
|
323
|
+
if (removed) {
|
|
324
|
+
totals.pruned += 1;
|
|
325
|
+
}
|
|
326
|
+
if (!isJson) {
|
|
327
|
+
console.log(`pruned ${deletedPath}`);
|
|
328
|
+
}
|
|
295
329
|
}
|
|
296
|
-
for (
|
|
330
|
+
for (let i = 0; i < deltas.modifiedOrAdded.length; i += 1) {
|
|
331
|
+
const filePath = deltas.modifiedOrAdded[i];
|
|
332
|
+
totals.scanned += 1;
|
|
297
333
|
try {
|
|
298
334
|
if (!shouldIncludeFile(filePath, includePatterns, excludePatterns, ignorePatterns)) {
|
|
299
|
-
await removeManifestPath(filePath, manifest, manifestPath, summaryDir, refs);
|
|
335
|
+
const removed = await removeManifestPath(filePath, manifest, manifestPath, summaryDir, refs);
|
|
336
|
+
if (removed) {
|
|
337
|
+
totals.pruned += 1;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
totals.skipped += 1;
|
|
341
|
+
}
|
|
342
|
+
if (!isJson) {
|
|
343
|
+
console.log(`[${i + 1}/${deltas.modifiedOrAdded.length}] excluded ${filePath}`);
|
|
344
|
+
}
|
|
300
345
|
continue;
|
|
301
346
|
}
|
|
302
347
|
const previousHash = manifest.files[filePath];
|
|
@@ -304,6 +349,10 @@ async function runSummarize(options, config) {
|
|
|
304
349
|
const rawCodeSnapshot = await promises_1.default.readFile(absolutePath, "utf8");
|
|
305
350
|
const hash = (0, hashing_1.hashFileContent)(rawCodeSnapshot);
|
|
306
351
|
if (previousHash === hash) {
|
|
352
|
+
totals.skipped += 1;
|
|
353
|
+
if (!isJson) {
|
|
354
|
+
console.log(`[${i + 1}/${deltas.modifiedOrAdded.length}] unchanged ${filePath}`);
|
|
355
|
+
}
|
|
307
356
|
continue;
|
|
308
357
|
}
|
|
309
358
|
const summaryPath = getSummaryPath(summaryDir, hash);
|
|
@@ -311,29 +360,77 @@ async function runSummarize(options, config) {
|
|
|
311
360
|
const summaryText = await (0, llm_1.generateFunctionalSummary)(filePath, rawCodeSnapshot, config.chat);
|
|
312
361
|
await ensureSummaryAsset(summaryDir, hash, summaryText, rawCodeSnapshot, options.includeCodeSnapshot);
|
|
313
362
|
}
|
|
314
|
-
await setManifestPathHash(filePath, hash, manifest, manifestPath, summaryDir, refs);
|
|
315
|
-
|
|
363
|
+
const changed = await setManifestPathHash(filePath, hash, manifest, manifestPath, summaryDir, refs);
|
|
364
|
+
if (changed) {
|
|
365
|
+
totals.updated += 1;
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
totals.skipped += 1;
|
|
369
|
+
}
|
|
370
|
+
if (!isJson) {
|
|
371
|
+
console.log(`[${i + 1}/${deltas.modifiedOrAdded.length}] updated ${filePath}`);
|
|
372
|
+
}
|
|
316
373
|
}
|
|
317
374
|
catch (error) {
|
|
318
375
|
const nodeError = error;
|
|
319
376
|
if (nodeError.code === "ENOENT") {
|
|
320
|
-
await removeManifestPath(filePath, manifest, manifestPath, summaryDir, refs);
|
|
377
|
+
const removed = await removeManifestPath(filePath, manifest, manifestPath, summaryDir, refs);
|
|
378
|
+
if (removed) {
|
|
379
|
+
totals.pruned += 1;
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
totals.skipped += 1;
|
|
383
|
+
}
|
|
384
|
+
if (!isJson) {
|
|
385
|
+
console.log(`[${i + 1}/${deltas.modifiedOrAdded.length}] missing ${filePath}`);
|
|
386
|
+
}
|
|
321
387
|
continue;
|
|
322
388
|
}
|
|
323
389
|
const message = error instanceof Error ? error.message : String(error);
|
|
324
390
|
failures.push({ filePath, message });
|
|
325
|
-
|
|
391
|
+
totals.failed += 1;
|
|
392
|
+
if (!isJson) {
|
|
393
|
+
console.error(`[${i + 1}/${deltas.modifiedOrAdded.length}] failed ${filePath}: ${message}`);
|
|
394
|
+
}
|
|
326
395
|
}
|
|
327
396
|
}
|
|
328
397
|
}
|
|
329
398
|
manifest.lastSyncedCommit = await (0, git_1.getCurrentCommit)(repoPath);
|
|
330
399
|
await writeManifest(manifestPath, manifest);
|
|
331
400
|
await pruneOrphanedSummaries(summaryDir, manifest);
|
|
332
|
-
|
|
401
|
+
const finishedAt = new Date();
|
|
402
|
+
const durationMs = finishedAt.getTime() - startedAt.getTime();
|
|
403
|
+
const report = {
|
|
404
|
+
mode: options.mode,
|
|
405
|
+
repoPath,
|
|
406
|
+
manifestPath,
|
|
407
|
+
summaryDir,
|
|
408
|
+
startedAt: startedAt.toISOString(),
|
|
409
|
+
finishedAt: finishedAt.toISOString(),
|
|
410
|
+
durationMs,
|
|
411
|
+
totals,
|
|
412
|
+
failures
|
|
413
|
+
};
|
|
414
|
+
if (isJson) {
|
|
415
|
+
console.log(JSON.stringify(report, null, 2));
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
console.log("---");
|
|
419
|
+
console.log(`Summarize complete`);
|
|
420
|
+
console.log(`Scanned: ${totals.scanned}`);
|
|
421
|
+
console.log(`Updated: ${totals.updated}`);
|
|
422
|
+
console.log(`Skipped: ${totals.skipped}`);
|
|
423
|
+
console.log(`Pruned: ${totals.pruned}`);
|
|
424
|
+
console.log(`Failed: ${totals.failed}`);
|
|
425
|
+
console.log(`Duration: ${(durationMs / 1000).toFixed(2)}s`);
|
|
426
|
+
console.log(`Manifest: ${manifestPath}`);
|
|
427
|
+
}
|
|
333
428
|
if (failures.length > 0) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
429
|
+
if (!isJson) {
|
|
430
|
+
console.error(`\n${failures.length} file(s) failed during summarization:`);
|
|
431
|
+
for (const failure of failures) {
|
|
432
|
+
console.error(`- ${failure.filePath}: ${failure.message}`);
|
|
433
|
+
}
|
|
337
434
|
}
|
|
338
435
|
throw new Error("Summarization completed with failures.");
|
|
339
436
|
}
|
package/dist/config.js
CHANGED
|
@@ -25,6 +25,17 @@ function readListOption(value, envName, fallback = []) {
|
|
|
25
25
|
}
|
|
26
26
|
return fallback;
|
|
27
27
|
}
|
|
28
|
+
function readPositiveIntegerOption(value, envName, fallback) {
|
|
29
|
+
const rawValue = value ?? process.env[envName];
|
|
30
|
+
if (rawValue === undefined || rawValue === "") {
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
const parsed = typeof rawValue === "number" ? rawValue : Number.parseInt(rawValue, 10);
|
|
34
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
35
|
+
throw new Error(`Invalid ${envName}. Expected a positive integer.`);
|
|
36
|
+
}
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
28
39
|
function loadRcFile(configPath) {
|
|
29
40
|
const resolvedPath = node_path_1.default.resolve(process.cwd(), configPath || ".diffdocrc");
|
|
30
41
|
if (!node_fs_1.default.existsSync(resolvedPath)) {
|
|
@@ -57,6 +68,7 @@ function buildRuntimeConfig(options, needs = { chat: true, embeddings: true }) {
|
|
|
57
68
|
const mergedOptions = mergeConfigOptions(options);
|
|
58
69
|
const provider = readProvider(mergedOptions.aiProvider);
|
|
59
70
|
const apiKey = readOption(mergedOptions.openaiApiKey, "OPENAI_API_KEY", provider === "local" ? "local-key" : "");
|
|
71
|
+
const embedBatchSize = readPositiveIntegerOption(mergedOptions.embedBatchSize, "DIFFDOC_EMBED_BATCH_SIZE", 25);
|
|
60
72
|
const includeGlobs = readListOption(mergedOptions.includeGlobs, "DIFFDOC_INCLUDE_GLOBS");
|
|
61
73
|
const excludeGlobs = readListOption(mergedOptions.excludeGlobs, "DIFFDOC_EXCLUDE_GLOBS");
|
|
62
74
|
const ignoreFile = readOption(mergedOptions.ignoreFile, "DIFFDOC_IGNORE_FILE", ".diffdocignore");
|
|
@@ -98,7 +110,8 @@ function buildRuntimeConfig(options, needs = { chat: true, embeddings: true }) {
|
|
|
98
110
|
embeddings: {
|
|
99
111
|
apiKey,
|
|
100
112
|
baseURL: embedBaseURL,
|
|
101
|
-
model: embedModel
|
|
113
|
+
model: embedModel,
|
|
114
|
+
batchSize: embedBatchSize
|
|
102
115
|
},
|
|
103
116
|
summarize: {
|
|
104
117
|
includeGlobs,
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
const commander_1 = require("commander");
|
|
5
5
|
const config_1 = require("./config");
|
|
6
6
|
const embed_1 = require("./commands/embed");
|
|
7
|
+
const init_1 = require("./commands/init");
|
|
7
8
|
const query_1 = require("./commands/query");
|
|
9
|
+
const status_1 = require("./commands/status");
|
|
8
10
|
const summarize_1 = require("./commands/summarize");
|
|
9
|
-
const llm_1 = require("./utils/llm");
|
|
10
11
|
const program = new commander_1.Command();
|
|
11
12
|
function collectOption(value, previous) {
|
|
12
13
|
previous.push(value);
|
|
@@ -30,7 +31,8 @@ function addEmbeddingOptions(command) {
|
|
|
30
31
|
return command
|
|
31
32
|
.option("--local-embed-endpoint <url>", "local OpenAI-compatible embeddings endpoint")
|
|
32
33
|
.option("--local-embed-model <model>", "local embedding model name")
|
|
33
|
-
.option("--cloud-embed-model <model>", "cloud embedding model name")
|
|
34
|
+
.option("--cloud-embed-model <model>", "cloud embedding model name")
|
|
35
|
+
.option("--embed-batch-size <count>", "number of summary documents to send per embeddings request");
|
|
34
36
|
}
|
|
35
37
|
function addCloudEndpointAndKeyOptions(command) {
|
|
36
38
|
return command
|
|
@@ -40,7 +42,24 @@ function addCloudEndpointAndKeyOptions(command) {
|
|
|
40
42
|
program
|
|
41
43
|
.name("diffdoc")
|
|
42
44
|
.description("Translate repository code shifts into plain-English business context")
|
|
43
|
-
.version("0.1
|
|
45
|
+
.version("0.4.1");
|
|
46
|
+
program
|
|
47
|
+
.command("init")
|
|
48
|
+
.description("Initialize DiffDoc configuration for this repository")
|
|
49
|
+
.option("--yes", "use defaults without prompting", false)
|
|
50
|
+
.option("--provider <provider>", "AI provider: local or cloud")
|
|
51
|
+
.option("--config <path>", "path to .diffdocrc JSON config file")
|
|
52
|
+
.option("--base-dir <path>", "DiffDoc artifact directory")
|
|
53
|
+
.option("--force", "overwrite existing config file", false)
|
|
54
|
+
.action(async (options) => {
|
|
55
|
+
try {
|
|
56
|
+
await (0, init_1.runInit)(options);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error(error instanceof Error ? error.message : error);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
44
63
|
addChatOptions(addBaseOptions(program
|
|
45
64
|
.command("summarize")))
|
|
46
65
|
.description("Summarize repository code into a portable JSON manifest")
|
|
@@ -48,6 +67,7 @@ addChatOptions(addBaseOptions(program
|
|
|
48
67
|
.option("--out <path>", "manifest output path under --base-dir", "manifest.json")
|
|
49
68
|
.option("--mode <mode>", "summarization mode: all or delta", "all")
|
|
50
69
|
.option("--include-code-snapshot", "store raw code in summary assets", false)
|
|
70
|
+
.option("--json", "print summarize report as JSON for CI", false)
|
|
51
71
|
.option("--include-glob <pattern>", "include glob pattern (repeatable)", collectOption, [])
|
|
52
72
|
.option("--exclude-glob <pattern>", "exclude glob pattern (repeatable)", collectOption, [])
|
|
53
73
|
.option("--ignore-file <path>", "path to ignore pattern file relative to --path")
|
|
@@ -59,6 +79,7 @@ addChatOptions(addBaseOptions(program
|
|
|
59
79
|
out: options.out,
|
|
60
80
|
mode: options.mode,
|
|
61
81
|
includeCodeSnapshot: options.includeCodeSnapshot,
|
|
82
|
+
json: options.json,
|
|
62
83
|
includeGlobs: options.includeGlob,
|
|
63
84
|
excludeGlobs: options.excludeGlob,
|
|
64
85
|
ignoreFile: options.ignoreFile
|
|
@@ -69,21 +90,6 @@ addChatOptions(addBaseOptions(program
|
|
|
69
90
|
process.exit(1);
|
|
70
91
|
}
|
|
71
92
|
});
|
|
72
|
-
addChatOptions(addBaseOptions(program
|
|
73
|
-
.command("prompt")))
|
|
74
|
-
.description("Send a plain prompt to the configured LLM")
|
|
75
|
-
.argument("<message...>", "prompt text to send")
|
|
76
|
-
.action(async (messageParts, options) => {
|
|
77
|
-
try {
|
|
78
|
-
const config = (0, config_1.buildRuntimeConfig)(options, { chat: true });
|
|
79
|
-
const response = await (0, llm_1.promptLlm)(messageParts.join(" "), config.chat);
|
|
80
|
-
console.log(response);
|
|
81
|
-
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
console.error(error instanceof Error ? error.message : error);
|
|
84
|
-
process.exit(1);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
93
|
addEmbeddingOptions(addChatOptions(addBaseOptions(program
|
|
88
94
|
.command("query"))))
|
|
89
95
|
.description("Answer a question using retrieved local Vectra context")
|
|
@@ -131,6 +137,21 @@ addCloudEndpointAndKeyOptions(addEmbeddingOptions(addBaseOptions(program
|
|
|
131
137
|
process.exit(1);
|
|
132
138
|
}
|
|
133
139
|
});
|
|
140
|
+
addBaseOptions(program
|
|
141
|
+
.command("status"))
|
|
142
|
+
.description("Show manifest and index sync status")
|
|
143
|
+
.option("--manifest <path>", "manifest input path under --base-dir", "manifest.json")
|
|
144
|
+
.option("--json", "print status as JSON for CI", false)
|
|
145
|
+
.action(async (options) => {
|
|
146
|
+
try {
|
|
147
|
+
const config = (0, config_1.buildRuntimeConfig)(options, { embeddings: false, chat: false });
|
|
148
|
+
await (0, status_1.runStatus)({ manifest: options.manifest, json: options.json }, config);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
console.error(error instanceof Error ? error.message : error);
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
134
155
|
program.parseAsync(process.argv).catch((error) => {
|
|
135
156
|
console.error(error instanceof Error ? error.message : error);
|
|
136
157
|
process.exit(1);
|
|
@@ -67,7 +67,7 @@ async function answerFromIndex(question, topK, config) {
|
|
|
67
67
|
results: []
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
-
const answer = await (0, llm_1.
|
|
70
|
+
const answer = await (0, llm_1.generateAnswer)(buildAnswerPrompt(question, results), config.chat);
|
|
71
71
|
return {
|
|
72
72
|
answer,
|
|
73
73
|
sources: results.map((result) => ({ filePath: result.filePath, score: result.score })),
|
package/dist/utils/llm.js
CHANGED
|
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.generateFunctionalSummary = generateFunctionalSummary;
|
|
7
|
-
exports.
|
|
7
|
+
exports.generateAnswer = generateAnswer;
|
|
8
8
|
exports.generateEmbeddings = generateEmbeddings;
|
|
9
9
|
const openai_1 = __importDefault(require("openai"));
|
|
10
10
|
function createClient(config) {
|
|
@@ -31,7 +31,7 @@ async function generateFunctionalSummary(fileName, codeContent, config) {
|
|
|
31
31
|
});
|
|
32
32
|
return response.choices[0]?.message?.content?.trim() || "No business behavior summary was returned.";
|
|
33
33
|
}
|
|
34
|
-
async function
|
|
34
|
+
async function generateAnswer(prompt, config) {
|
|
35
35
|
const { client, model } = createClient(config);
|
|
36
36
|
const response = await client.chat.completions.create({
|
|
37
37
|
model,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "diffdoc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Translate repository code shifts into plain-English business context",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Christopher Sullivan",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"type": "commonjs",
|
|
16
16
|
"main": "dist/index.js",
|
|
17
17
|
"bin": {
|
|
18
|
-
"diffdoc": "
|
|
19
|
-
"diffdoc-mcp": "
|
|
18
|
+
"diffdoc": "dist/index.js",
|
|
19
|
+
"diffdoc-mcp": "dist/mcp.js"
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|