@orquentra/codebuddy 2.9.0 → 2.15.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/ARCHITECTURE.md +98 -0
- package/CLAUDE_CODEX.md +33 -0
- package/README.md +114 -9
- package/dist/{adapters-S4HHTWJW.js → adapters-OJRSVFV3.js} +2 -2
- package/dist/{chunk-GXOML4UZ.js → chunk-4BSO3HST.js} +126 -18
- package/dist/chunk-4BSO3HST.js.map +1 -0
- package/dist/{chunk-CAEPHLHC.js → chunk-57642NGU.js} +2 -44
- package/dist/chunk-57642NGU.js.map +1 -0
- package/dist/{chunk-ESZXCZIN.js → chunk-6YI2D5F5.js} +33 -5
- package/dist/chunk-6YI2D5F5.js.map +1 -0
- package/dist/{chunk-GKNYMNFM.js → chunk-7IPSDDO5.js} +10 -3
- package/dist/chunk-7IPSDDO5.js.map +1 -0
- package/dist/chunk-BTTQWSDS.js +372 -0
- package/dist/chunk-BTTQWSDS.js.map +1 -0
- package/dist/{chunk-2WH2Q4TA.js → chunk-D5E7SGI5.js} +304 -77
- package/dist/chunk-D5E7SGI5.js.map +1 -0
- package/dist/chunk-EMWGLKPR.js +45 -0
- package/dist/chunk-EMWGLKPR.js.map +1 -0
- package/dist/chunk-GQKWUHMA.js +20 -0
- package/dist/chunk-GQKWUHMA.js.map +1 -0
- package/dist/chunk-ITXE27SZ.js +358 -0
- package/dist/chunk-ITXE27SZ.js.map +1 -0
- package/dist/{chunk-UK6XTS2Y.js → chunk-JJQDVGF7.js} +23 -23
- package/dist/chunk-JJQDVGF7.js.map +1 -0
- package/dist/{chunk-F4BJ3DXW.js → chunk-KQCP3ZYH.js} +1273 -331
- package/dist/chunk-KQCP3ZYH.js.map +1 -0
- package/dist/{chunk-QN76UJP7.js → chunk-KYRM3HPP.js} +89 -21
- package/dist/chunk-KYRM3HPP.js.map +1 -0
- package/dist/{chunk-KNR2P2UY.js → chunk-M75SN4D4.js} +19 -4
- package/dist/chunk-M75SN4D4.js.map +1 -0
- package/dist/{chunk-J4T6HHI4.js → chunk-RZ266NHE.js} +538 -317
- package/dist/chunk-RZ266NHE.js.map +1 -0
- package/dist/chunk-U7KDM62P.js +43 -0
- package/dist/chunk-U7KDM62P.js.map +1 -0
- package/dist/chunk-UATJCD2T.js +368 -0
- package/dist/chunk-UATJCD2T.js.map +1 -0
- package/dist/{chunk-ENKDFLOO.js → chunk-UOTKHSQ5.js} +25 -8
- package/dist/chunk-UOTKHSQ5.js.map +1 -0
- package/dist/{chunk-TLWGZVPC.js → chunk-UR3DF74I.js} +1007 -132
- package/dist/chunk-UR3DF74I.js.map +1 -0
- package/dist/chunk-UTPC66MV.js +128 -0
- package/dist/chunk-UTPC66MV.js.map +1 -0
- package/dist/{chunk-W7GAXL6C.js → chunk-V7PT6ER5.js} +6 -5
- package/dist/chunk-V7PT6ER5.js.map +1 -0
- package/dist/chunk-WHKXUXHE.js +156 -0
- package/dist/chunk-WHKXUXHE.js.map +1 -0
- package/dist/{chunk-SRA4MDVG.js → chunk-YMXFSHXT.js} +978 -950
- package/dist/chunk-YMXFSHXT.js.map +1 -0
- package/dist/cli/index.js +23 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/{codebuddy-DO7ZhoWn.d.ts → codebuddy-CHqMptZB.d.ts} +35 -0
- package/dist/config-TXQJCCVI.js +15 -0
- package/dist/expand-KWYGNCKZ.js +98 -0
- package/dist/expand-KWYGNCKZ.js.map +1 -0
- package/dist/file-vectors-MKXTSGGH.js +19 -0
- package/dist/index-facts-4MTEHKMN.js +38 -0
- package/dist/index-pass-TBDUEZCB.js +14 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +25 -23
- package/dist/install-E3JWINTN.js +31 -0
- package/dist/langgraph/index.d.ts +1 -1
- package/dist/lean-MLTAFS5L.js +29 -0
- package/dist/local-75DJECVR.js +48 -0
- package/dist/local-llm-MECVJTVU.js +19 -0
- package/dist/{main-YJKSYFPT.js → main-RLMKQZW4.js} +999 -328
- package/dist/main-RLMKQZW4.js.map +1 -0
- package/dist/{memory-TGHVTBME.js → memory-PFFVMTCH.js} +9 -6
- package/dist/{memory-TGHVTBME.js.map → memory-PFFVMTCH.js.map} +1 -1
- package/dist/{operations-OG4ISWIM.js → operations-STR7S5UO.js} +8 -7
- package/dist/operations-STR7S5UO.js.map +1 -0
- package/dist/{pretool-command-PRFOIHMW.js → pretool-command-B5GY5T6E.js} +2 -2
- package/dist/pretool-command-B5GY5T6E.js.map +1 -0
- package/dist/prompt-gate-TDTFFQW2.js +8 -0
- package/dist/prompt-gate-TDTFFQW2.js.map +1 -0
- package/dist/{rerank-IQAEWLQR.js → rerank-XXKWDI5Q.js} +7 -4
- package/dist/{rerank-IQAEWLQR.js.map → rerank-XXKWDI5Q.js.map} +1 -1
- package/dist/{runtime-GCSEIAVD.js → runtime-SWFWGTQ4.js} +18 -9
- package/dist/runtime-SWFWGTQ4.js.map +1 -0
- package/dist/summaries-QXWFFDUZ.js +31 -0
- package/dist/summaries-QXWFFDUZ.js.map +1 -0
- package/dist/{zvec-XDZNZ2OL.js → zvec-F73IBWO5.js} +13 -6
- package/dist/zvec-F73IBWO5.js.map +1 -0
- package/package.json +13 -3
- package/dist/chunk-2WH2Q4TA.js.map +0 -1
- package/dist/chunk-6NZEZJYD.js +0 -206
- package/dist/chunk-6NZEZJYD.js.map +0 -1
- package/dist/chunk-CAEPHLHC.js.map +0 -1
- package/dist/chunk-ENKDFLOO.js.map +0 -1
- package/dist/chunk-ESZXCZIN.js.map +0 -1
- package/dist/chunk-F4BJ3DXW.js.map +0 -1
- package/dist/chunk-GKNYMNFM.js.map +0 -1
- package/dist/chunk-GXOML4UZ.js.map +0 -1
- package/dist/chunk-ITQO632W.js +0 -673
- package/dist/chunk-ITQO632W.js.map +0 -1
- package/dist/chunk-J4T6HHI4.js.map +0 -1
- package/dist/chunk-KNR2P2UY.js.map +0 -1
- package/dist/chunk-QN76UJP7.js.map +0 -1
- package/dist/chunk-SFMIMLXR.js +0 -63
- package/dist/chunk-SFMIMLXR.js.map +0 -1
- package/dist/chunk-SRA4MDVG.js.map +0 -1
- package/dist/chunk-TLWGZVPC.js.map +0 -1
- package/dist/chunk-UK6XTS2Y.js.map +0 -1
- package/dist/chunk-W7GAXL6C.js.map +0 -1
- package/dist/config-GEZFLXK6.js +0 -12
- package/dist/install-BXM56QCD.js +0 -19
- package/dist/lean-command-7DFW32MA.js +0 -19
- package/dist/main-YJKSYFPT.js.map +0 -1
- package/dist/summaries-ML53R7EL.js +0 -21
- /package/dist/{adapters-S4HHTWJW.js.map → adapters-OJRSVFV3.js.map} +0 -0
- /package/dist/{config-GEZFLXK6.js.map → config-TXQJCCVI.js.map} +0 -0
- /package/dist/{install-BXM56QCD.js.map → file-vectors-MKXTSGGH.js.map} +0 -0
- /package/dist/{lean-command-7DFW32MA.js.map → index-facts-4MTEHKMN.js.map} +0 -0
- /package/dist/{operations-OG4ISWIM.js.map → index-pass-TBDUEZCB.js.map} +0 -0
- /package/dist/{pretool-command-PRFOIHMW.js.map → install-E3JWINTN.js.map} +0 -0
- /package/dist/{runtime-GCSEIAVD.js.map → lean-MLTAFS5L.js.map} +0 -0
- /package/dist/{summaries-ML53R7EL.js.map → local-75DJECVR.js.map} +0 -0
- /package/dist/{zvec-XDZNZ2OL.js.map → local-llm-MECVJTVU.js.map} +0 -0
|
@@ -1,37 +1,702 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
extractPythonImports,
|
|
3
|
+
extractSymbolTable,
|
|
4
|
+
supportsSymbols
|
|
5
|
+
} from "./chunk-Y7D7PUBE.js";
|
|
5
6
|
import {
|
|
6
7
|
isCredentialPath
|
|
7
8
|
} from "./chunk-AL6VBTNA.js";
|
|
8
|
-
import {
|
|
9
|
-
LOCAL_EMBEDDING_MODEL
|
|
10
|
-
} from "./chunk-HPOR6YXA.js";
|
|
11
9
|
import {
|
|
12
10
|
readConfigFile
|
|
13
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-M75SN4D4.js";
|
|
14
12
|
import {
|
|
13
|
+
assertNoSymlinkBetween,
|
|
15
14
|
writeAtomic
|
|
16
15
|
} from "./chunk-CG5B3LYG.js";
|
|
16
|
+
import {
|
|
17
|
+
LOCAL_EMBEDDING_MODEL
|
|
18
|
+
} from "./chunk-HPOR6YXA.js";
|
|
19
|
+
|
|
20
|
+
// src/context/file-vectors.ts
|
|
21
|
+
import { createHash as createHash3 } from "crypto";
|
|
22
|
+
import { mkdir as mkdir5, readFile as readFile10 } from "fs/promises";
|
|
23
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
24
|
+
|
|
25
|
+
// src/indexer/engine.ts
|
|
26
|
+
import { createHash } from "crypto";
|
|
27
|
+
import { readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
28
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
29
|
+
|
|
30
|
+
// src/map/cache.ts
|
|
31
|
+
import { mkdir as mkdir2, readFile as readFile3 } from "fs/promises";
|
|
32
|
+
import { dirname as dirname3, extname as extname2, join as join3, resolve as resolve3 } from "path";
|
|
33
|
+
|
|
34
|
+
// src/indexer/store.ts
|
|
35
|
+
import { mkdir, readFile } from "fs/promises";
|
|
36
|
+
import { dirname, join, resolve } from "path";
|
|
37
|
+
import { z } from "zod";
|
|
38
|
+
|
|
39
|
+
// src/indexer/types.ts
|
|
40
|
+
var INDEX_MANIFEST_VERSION = 3;
|
|
41
|
+
|
|
42
|
+
// src/indexer/store.ts
|
|
43
|
+
var symbolSpanSchema = z.object({
|
|
44
|
+
name: z.string(),
|
|
45
|
+
kind: z.enum(["function", "method", "class", "interface", "type", "enum", "const", "heading"]),
|
|
46
|
+
startLine: z.number().int().nonnegative(),
|
|
47
|
+
endLine: z.number().int().nonnegative(),
|
|
48
|
+
signature: z.string(),
|
|
49
|
+
exported: z.boolean()
|
|
50
|
+
});
|
|
51
|
+
var entrySchema = z.object({
|
|
52
|
+
path: z.string(),
|
|
53
|
+
hash: z.string(),
|
|
54
|
+
size: z.number().int().nonnegative(),
|
|
55
|
+
mtimeMs: z.number().optional(),
|
|
56
|
+
ctimeMs: z.number().optional(),
|
|
57
|
+
language: z.string(),
|
|
58
|
+
lines: z.number().int().nonnegative(),
|
|
59
|
+
symbols: z.array(z.string()),
|
|
60
|
+
symbolTable: z.array(symbolSpanSchema).optional(),
|
|
61
|
+
summary: z.string(),
|
|
62
|
+
indexedAt: z.string()
|
|
63
|
+
});
|
|
64
|
+
var manifestSchema = z.object({
|
|
65
|
+
version: z.literal(INDEX_MANIFEST_VERSION),
|
|
66
|
+
generatedAt: z.string(),
|
|
67
|
+
entries: z.record(z.string(), entrySchema)
|
|
68
|
+
});
|
|
69
|
+
var IndexStore = class {
|
|
70
|
+
repositoryRoot;
|
|
71
|
+
cacheDirectory;
|
|
72
|
+
manifestPath;
|
|
73
|
+
constructor(repositoryRoot = process.cwd()) {
|
|
74
|
+
this.repositoryRoot = resolve(repositoryRoot);
|
|
75
|
+
this.cacheDirectory = join(this.repositoryRoot, ".codebuddy", "cache");
|
|
76
|
+
this.manifestPath = join(this.cacheDirectory, "index.json");
|
|
77
|
+
}
|
|
78
|
+
/** Read the manifest, or an empty one if absent/corrupt/version-mismatched. */
|
|
79
|
+
async read() {
|
|
80
|
+
try {
|
|
81
|
+
const raw = await readFile(this.manifestPath, "utf8");
|
|
82
|
+
const parsed = manifestSchema.safeParse(JSON.parse(raw));
|
|
83
|
+
if (!parsed.success) return emptyManifest();
|
|
84
|
+
return parsed.data;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error.code === "ENOENT") return emptyManifest();
|
|
87
|
+
return emptyManifest();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async write(manifest) {
|
|
91
|
+
await assertNoSymlinkBetween(this.repositoryRoot, dirname(this.manifestPath));
|
|
92
|
+
await mkdir(this.cacheDirectory, { recursive: true, mode: 448 });
|
|
93
|
+
await writeAtomic(this.manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
94
|
+
`);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
function emptyManifest() {
|
|
98
|
+
return { version: INDEX_MANIFEST_VERSION, generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(), entries: {} };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/map/indexer.ts
|
|
102
|
+
import { readdir, readFile as readFile2, stat } from "fs/promises";
|
|
103
|
+
import { dirname as dirname2, extname, join as join2, relative, resolve as resolve2 } from "path";
|
|
104
|
+
var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py"]);
|
|
105
|
+
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
106
|
+
".git",
|
|
107
|
+
"node_modules",
|
|
108
|
+
"dist",
|
|
109
|
+
"build",
|
|
110
|
+
"coverage",
|
|
111
|
+
".codebuddy",
|
|
112
|
+
// Python virtualenvs and caches would otherwise map site-packages.
|
|
113
|
+
".venv",
|
|
114
|
+
"venv",
|
|
115
|
+
".tox",
|
|
116
|
+
"__pycache__"
|
|
117
|
+
]);
|
|
118
|
+
async function buildArchitectureMap(repositoryRoot = process.cwd()) {
|
|
119
|
+
const root = resolve2(repositoryRoot);
|
|
120
|
+
const files = await listSourceFiles(root);
|
|
121
|
+
const fileSet = new Set(files.map((file) => normalize(relative(root, file))));
|
|
122
|
+
const modules = [];
|
|
123
|
+
const edges = [];
|
|
124
|
+
for (const absolutePath of files) {
|
|
125
|
+
const source = await readFile2(absolutePath, "utf8");
|
|
126
|
+
const from = normalize(relative(root, absolutePath));
|
|
127
|
+
const resolvedImports = moduleImports(root, from, source, fileSet);
|
|
128
|
+
edges.push(...resolvedImports.map(({ to, kind }) => ({ from, to, kind })));
|
|
129
|
+
modules.push({
|
|
130
|
+
path: from,
|
|
131
|
+
language: moduleLanguage(from),
|
|
132
|
+
imports: [...new Set(resolvedImports.map(({ to }) => to))].sort()
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
modules: modules.sort((left, right) => left.path.localeCompare(right.path)),
|
|
137
|
+
edges: edges.sort(
|
|
138
|
+
(left, right) => `${left.from}:${left.to}`.localeCompare(`${right.from}:${right.to}`)
|
|
139
|
+
)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function updateArchitectureMap(repositoryRoot, sourcePaths, previous, changedPaths) {
|
|
143
|
+
const root = resolve2(repositoryRoot);
|
|
144
|
+
const normalizedPaths = sourcePaths.map(normalize).sort();
|
|
145
|
+
const fileSet = new Set(normalizedPaths);
|
|
146
|
+
const changed = new Set(changedPaths.map(normalize));
|
|
147
|
+
const retainedModules = (previous?.modules ?? []).filter(
|
|
148
|
+
(module) => fileSet.has(module.path) && !changed.has(module.path)
|
|
149
|
+
);
|
|
150
|
+
const retainedEdges = (previous?.edges ?? []).filter(
|
|
151
|
+
(edge) => fileSet.has(edge.from) && fileSet.has(edge.to) && !changed.has(edge.from)
|
|
152
|
+
);
|
|
153
|
+
const modules = [...retainedModules];
|
|
154
|
+
const edges = [...retainedEdges];
|
|
155
|
+
for (const path of normalizedPaths) {
|
|
156
|
+
if (!changed.has(path)) continue;
|
|
157
|
+
const source = await readFile2(join2(root, path), "utf8");
|
|
158
|
+
const resolvedImports = moduleImports(root, path, source, fileSet);
|
|
159
|
+
edges.push(...resolvedImports.map(({ to, kind }) => ({ from: path, to, kind })));
|
|
160
|
+
modules.push({
|
|
161
|
+
path,
|
|
162
|
+
language: moduleLanguage(path),
|
|
163
|
+
imports: [...new Set(resolvedImports.map(({ to }) => to))].sort()
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
map: {
|
|
168
|
+
modules: modules.sort((left, right) => left.path.localeCompare(right.path)),
|
|
169
|
+
edges: edges.sort(
|
|
170
|
+
(left, right) => `${left.from}:${left.to}`.localeCompare(`${right.from}:${right.to}`)
|
|
171
|
+
)
|
|
172
|
+
},
|
|
173
|
+
stats: {
|
|
174
|
+
readFiles: changed.size,
|
|
175
|
+
reusedFiles: retainedModules.length,
|
|
176
|
+
rebuilt: previous === null || changed.size === normalizedPaths.length
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function queryMap(map, query) {
|
|
181
|
+
const limit = query.limit ?? 50;
|
|
182
|
+
return map.edges.filter((edge) => query.from ? edge.from === normalize(query.from) : true).filter((edge) => query.to ? edge.to === normalize(query.to) : true).slice(0, limit);
|
|
183
|
+
}
|
|
184
|
+
function neighbours(map, modulePath, direction = "both") {
|
|
185
|
+
const target = normalize(modulePath);
|
|
186
|
+
const edges = map.edges.filter((edge) => {
|
|
187
|
+
if (direction === "in") return edge.to === target;
|
|
188
|
+
if (direction === "out") return edge.from === target;
|
|
189
|
+
return edge.from === target || edge.to === target;
|
|
190
|
+
});
|
|
191
|
+
const paths = /* @__PURE__ */ new Set([target]);
|
|
192
|
+
for (const edge of edges) {
|
|
193
|
+
paths.add(edge.from);
|
|
194
|
+
paths.add(edge.to);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
modules: map.modules.filter((module) => paths.has(module.path)),
|
|
198
|
+
edges
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function listSourceFiles(root) {
|
|
202
|
+
const files = [];
|
|
203
|
+
await walk(root, files);
|
|
204
|
+
return files.sort();
|
|
205
|
+
}
|
|
206
|
+
async function walk(directory, files) {
|
|
207
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
const absolute = join2(directory, entry.name);
|
|
210
|
+
if (entry.isDirectory()) {
|
|
211
|
+
if (IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
212
|
+
await walk(absolute, files);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (!entry.isFile()) continue;
|
|
216
|
+
if (!SUPPORTED_EXTENSIONS.has(extname(entry.name))) continue;
|
|
217
|
+
const info = await stat(absolute);
|
|
218
|
+
if (info.size > 5e5) continue;
|
|
219
|
+
files.push(absolute);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function moduleLanguage(path) {
|
|
223
|
+
const extension = extname(path);
|
|
224
|
+
if (extension === ".py") return "py";
|
|
225
|
+
return extension.includes("ts") ? "ts" : "js";
|
|
226
|
+
}
|
|
227
|
+
function moduleImports(root, from, source, fileSet) {
|
|
228
|
+
if (extname(from) === ".py") {
|
|
229
|
+
return pythonImports(from, source, fileSet).map((to) => ({ to, kind: "import" }));
|
|
230
|
+
}
|
|
231
|
+
return extractImports(source).flatMap((specifier) => {
|
|
232
|
+
const to = resolveImport(root, from, specifier.value, fileSet);
|
|
233
|
+
return to ? [{ to, kind: specifier.kind }] : [];
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
var PYTHON_ROOTS = ["", "src/"];
|
|
237
|
+
function pythonImports(from, source, fileSet) {
|
|
238
|
+
const pick = (...candidates) => candidates.find((candidate) => fileSet.has(candidate)) ?? null;
|
|
239
|
+
const moduleFile = (base) => pick(`${base}.py`, `${base}/__init__.py`);
|
|
240
|
+
const packageDir = from.split("/").slice(0, -1);
|
|
241
|
+
const targets = [];
|
|
242
|
+
for (const statement of extractPythonImports(source)) {
|
|
243
|
+
const parts = statement.module ? statement.module.split(".") : [];
|
|
244
|
+
let bases;
|
|
245
|
+
if (statement.level > 0) {
|
|
246
|
+
const up = statement.level - 1;
|
|
247
|
+
if (up > packageDir.length) continue;
|
|
248
|
+
bases = [[...packageDir.slice(0, packageDir.length - up), ...parts].join("/")];
|
|
249
|
+
} else {
|
|
250
|
+
bases = PYTHON_ROOTS.map((prefix) => `${prefix}${parts.join("/")}`);
|
|
251
|
+
}
|
|
252
|
+
for (const base of bases) {
|
|
253
|
+
const prefix = base ? `${base}/` : "";
|
|
254
|
+
const module = parts.length > 0 ? moduleFile(base) : pick(`${prefix}__init__.py`);
|
|
255
|
+
const submodules = statement.names.flatMap((name) => moduleFile(`${prefix}${name}`) ?? []);
|
|
256
|
+
if (!module && submodules.length === 0) continue;
|
|
257
|
+
targets.push(...submodules);
|
|
258
|
+
if (module && submodules.length < Math.max(1, statement.names.length)) targets.push(module);
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return [...new Set(targets)].filter((target) => target !== from);
|
|
263
|
+
}
|
|
264
|
+
function extractImports(source) {
|
|
265
|
+
const imports = [];
|
|
266
|
+
const stripped = source.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:])\/\/[^\n]*/g, "$1 ");
|
|
267
|
+
const staticImport = /\bimport\s+(?:type\s+)?(?:[^'"()+]+?\s+from\s+)?["']([^"']+)["']/g;
|
|
268
|
+
const exportFrom = /\bexport\s+[^'"()+]+?\s+from\s+["']([^"']+)["']/g;
|
|
269
|
+
const dynamicImport = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
270
|
+
for (const match of stripped.matchAll(staticImport)) {
|
|
271
|
+
if (match[1]) imports.push({ value: match[1], kind: "import" });
|
|
272
|
+
}
|
|
273
|
+
for (const match of stripped.matchAll(exportFrom)) {
|
|
274
|
+
if (match[1]) imports.push({ value: match[1], kind: "import" });
|
|
275
|
+
}
|
|
276
|
+
for (const match of stripped.matchAll(dynamicImport)) {
|
|
277
|
+
if (match[1]) imports.push({ value: match[1], kind: "dynamic_import" });
|
|
278
|
+
}
|
|
279
|
+
return imports;
|
|
280
|
+
}
|
|
281
|
+
function resolveImport(root, from, specifier, fileSet) {
|
|
282
|
+
if (!specifier.startsWith(".")) return null;
|
|
283
|
+
const base = normalize(relative(root, resolve2(root, dirname2(from), specifier)));
|
|
284
|
+
const stem = base.replace(/\.(js|jsx|mjs|cjs)$/, "");
|
|
285
|
+
const bases = stem === base ? [base] : [base, stem];
|
|
286
|
+
const candidates = bases.flatMap((b) => [
|
|
287
|
+
b,
|
|
288
|
+
`${b}.ts`,
|
|
289
|
+
`${b}.tsx`,
|
|
290
|
+
`${b}.js`,
|
|
291
|
+
`${b}.jsx`,
|
|
292
|
+
`${b}.mjs`,
|
|
293
|
+
`${b}.cjs`,
|
|
294
|
+
`${b}/index.ts`,
|
|
295
|
+
`${b}/index.tsx`,
|
|
296
|
+
`${b}/index.js`,
|
|
297
|
+
`${b}/index.jsx`
|
|
298
|
+
]);
|
|
299
|
+
return candidates.find((candidate) => fileSet.has(candidate)) ?? null;
|
|
300
|
+
}
|
|
301
|
+
function normalize(path) {
|
|
302
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/map/cache.ts
|
|
306
|
+
var CACHE_VERSION = 1;
|
|
307
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py"]);
|
|
308
|
+
async function loadArchitectureMap(repositoryRoot = process.cwd(), suppliedManifest) {
|
|
309
|
+
const root = resolve3(repositoryRoot);
|
|
310
|
+
const manifest = suppliedManifest ?? await new IndexStore(root).read();
|
|
311
|
+
if (Object.keys(manifest.entries).length === 0) {
|
|
312
|
+
const map = await buildArchitectureMap(root);
|
|
313
|
+
return {
|
|
314
|
+
map,
|
|
315
|
+
stats: { readFiles: map.modules.length, reusedFiles: 0, rebuilt: true },
|
|
316
|
+
source: "fallback"
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return refreshArchitectureCache(root, manifest);
|
|
320
|
+
}
|
|
321
|
+
async function refreshArchitectureCache(repositoryRoot, manifest) {
|
|
322
|
+
const root = resolve3(repositoryRoot);
|
|
323
|
+
const path = join3(root, ".codebuddy", "cache", "architecture.json");
|
|
324
|
+
const currentHashes = sourceHashes(manifest);
|
|
325
|
+
const currentPaths = Object.keys(currentHashes).sort();
|
|
326
|
+
const cached = await readCache(path);
|
|
327
|
+
const cachedPaths = cached ? Object.keys(cached.sourceHashes).sort() : [];
|
|
328
|
+
const fileSetChanged = cachedPaths.join("\n") !== currentPaths.join("\n");
|
|
329
|
+
const changedPaths = !cached || fileSetChanged ? currentPaths : currentPaths.filter(
|
|
330
|
+
(sourcePath) => cached.sourceHashes[sourcePath] !== currentHashes[sourcePath]
|
|
331
|
+
);
|
|
332
|
+
if (cached && changedPaths.length === 0) {
|
|
333
|
+
return {
|
|
334
|
+
map: cached.map,
|
|
335
|
+
stats: { readFiles: 0, reusedFiles: cached.map.modules.length, rebuilt: false },
|
|
336
|
+
source: "cache"
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const updated = await updateArchitectureMap(
|
|
340
|
+
root,
|
|
341
|
+
currentPaths,
|
|
342
|
+
fileSetChanged ? null : cached?.map ?? null,
|
|
343
|
+
changedPaths
|
|
344
|
+
);
|
|
345
|
+
const next = {
|
|
346
|
+
version: CACHE_VERSION,
|
|
347
|
+
generatedAt: manifest.generatedAt,
|
|
348
|
+
sourceHashes: currentHashes,
|
|
349
|
+
map: updated.map
|
|
350
|
+
};
|
|
351
|
+
await assertNoSymlinkBetween(root, dirname3(path));
|
|
352
|
+
await mkdir2(dirname3(path), { recursive: true, mode: 448 });
|
|
353
|
+
await writeAtomic(path, `${JSON.stringify(next, null, 2)}
|
|
354
|
+
`);
|
|
355
|
+
return { ...updated, source: fileSetChanged || !cached ? "rebuild" : "incremental" };
|
|
356
|
+
}
|
|
357
|
+
async function readCache(path) {
|
|
358
|
+
try {
|
|
359
|
+
const parsed = JSON.parse(await readFile3(path, "utf8"));
|
|
360
|
+
if (parsed.version !== CACHE_VERSION || !parsed.map || !parsed.sourceHashes) return null;
|
|
361
|
+
if (!Array.isArray(parsed.map.modules) || !Array.isArray(parsed.map.edges)) return null;
|
|
362
|
+
if (!parsed.map.modules.every(
|
|
363
|
+
(module) => module && typeof module.path === "string" && typeof module.language === "string" && Array.isArray(module.imports)
|
|
364
|
+
) || !parsed.map.edges.every(
|
|
365
|
+
(edge) => edge && typeof edge.from === "string" && typeof edge.to === "string"
|
|
366
|
+
)) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
return parsed;
|
|
370
|
+
} catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function sourceHashes(manifest) {
|
|
375
|
+
return Object.fromEntries(
|
|
376
|
+
Object.values(manifest.entries).filter((entry) => SOURCE_EXTENSIONS.has(extname2(entry.path).toLowerCase())).map((entry) => [entry.path, entry.hash])
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/indexer/scan.ts
|
|
381
|
+
import { spawn } from "child_process";
|
|
382
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
383
|
+
import { extname as extname4, join as join4, relative as relative2, sep } from "path";
|
|
384
|
+
|
|
385
|
+
// src/indexer/summary.ts
|
|
386
|
+
import { extname as extname3 } from "path";
|
|
387
|
+
var LANGUAGE_BY_EXTENSION = {
|
|
388
|
+
".ts": "ts",
|
|
389
|
+
".tsx": "tsx",
|
|
390
|
+
".js": "js",
|
|
391
|
+
".jsx": "jsx",
|
|
392
|
+
".mjs": "js",
|
|
393
|
+
".cjs": "js",
|
|
394
|
+
".json": "json",
|
|
395
|
+
".md": "md",
|
|
396
|
+
".mdx": "md",
|
|
397
|
+
".rst": "rst",
|
|
398
|
+
".yaml": "yaml",
|
|
399
|
+
".yml": "yaml",
|
|
400
|
+
".toml": "toml",
|
|
401
|
+
".css": "css",
|
|
402
|
+
".scss": "scss",
|
|
403
|
+
".html": "html",
|
|
404
|
+
".py": "py",
|
|
405
|
+
".go": "go",
|
|
406
|
+
".rs": "rust",
|
|
407
|
+
".java": "java",
|
|
408
|
+
".rb": "ruby",
|
|
409
|
+
".php": "php",
|
|
410
|
+
".sh": "shell",
|
|
411
|
+
".sql": "sql",
|
|
412
|
+
".txt": "text"
|
|
413
|
+
};
|
|
414
|
+
var SUPPORTED_EXTENSIONS2 = new Set(Object.keys(LANGUAGE_BY_EXTENSION));
|
|
415
|
+
var CODE_LANGUAGES = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx"]);
|
|
416
|
+
var SYMBOL_LIMIT = 8;
|
|
417
|
+
var SUMMARY_COMMENT_MAX = 80;
|
|
418
|
+
function languageFor(path) {
|
|
419
|
+
return LANGUAGE_BY_EXTENSION[extname3(path).toLowerCase()] ?? "text";
|
|
420
|
+
}
|
|
421
|
+
function summarizeSource(path, content) {
|
|
422
|
+
const language = languageFor(path);
|
|
423
|
+
const lines = countLines(content);
|
|
424
|
+
const symbols = CODE_LANGUAGES.has(language) ? extractSymbols(content) : language === "py" ? pythonPublicNames(content) : [];
|
|
425
|
+
const summary = composeSummary(language, lines, symbols, content);
|
|
426
|
+
return { language, lines, symbols, summary };
|
|
427
|
+
}
|
|
428
|
+
function countLines(content) {
|
|
429
|
+
if (content.length === 0) return 0;
|
|
430
|
+
return content.replace(/\r/g, "").replace(/\n$/, "").split("\n").length;
|
|
431
|
+
}
|
|
432
|
+
function extractSymbols(content) {
|
|
433
|
+
const names = [];
|
|
434
|
+
const push = (name) => {
|
|
435
|
+
const trimmed = name?.trim();
|
|
436
|
+
if (trimmed && /^[A-Za-z_$][\w$]*$/.test(trimmed) && !names.includes(trimmed)) {
|
|
437
|
+
names.push(trimmed);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
const declaration = /^export\s+(?:default\s+)?(?:async\s+)?(?:abstract\s+)?(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][\w$]*)/gm;
|
|
441
|
+
for (const match of content.matchAll(declaration)) push(match[1]);
|
|
442
|
+
const namedExports = /^export\s*\{([^}]*)\}/gm;
|
|
443
|
+
for (const match of content.matchAll(namedExports)) {
|
|
444
|
+
for (const part of (match[1] ?? "").split(",")) {
|
|
445
|
+
const alias = part.includes(" as ") ? part.split(" as ").pop() : part;
|
|
446
|
+
push(alias);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return names.slice(0, SYMBOL_LIMIT);
|
|
450
|
+
}
|
|
451
|
+
function pythonPublicNames(content) {
|
|
452
|
+
return extractSymbolTable(content, "x.py").filter((symbol) => symbol.exported && symbol.kind !== "method").map((symbol) => symbol.name).slice(0, SYMBOL_LIMIT);
|
|
453
|
+
}
|
|
454
|
+
function composeSummary(language, lines, symbols, content) {
|
|
455
|
+
const head = `${language} \u2022 ${lines} line${lines === 1 ? "" : "s"}`;
|
|
456
|
+
if (symbols.length > 0) return `${head} \u2022 exports ${symbols.join(", ")}`;
|
|
457
|
+
const comment = leadingComment(content);
|
|
458
|
+
if (comment) return `${head} \u2022 ${comment}`;
|
|
459
|
+
return head;
|
|
460
|
+
}
|
|
461
|
+
function leadingComment(content) {
|
|
462
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
463
|
+
const line = raw.trim();
|
|
464
|
+
if (line === "") continue;
|
|
465
|
+
const stripped = line.replace(/^\/\/+/, "").replace(/^\/\*+/, "").replace(/\*+\/$/, "").replace(/^[#*]+/, "").trim();
|
|
466
|
+
if (line.startsWith("//") || line.startsWith("/*") || line.startsWith("#") || line.startsWith("*")) {
|
|
467
|
+
if (stripped) return truncate(stripped, SUMMARY_COMMENT_MAX);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
function truncate(value, max) {
|
|
475
|
+
return value.length <= max ? value : `${value.slice(0, max - 1)}\u2026`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/indexer/scan.ts
|
|
479
|
+
var FALLBACK_IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
480
|
+
".git",
|
|
481
|
+
"node_modules",
|
|
482
|
+
"dist",
|
|
483
|
+
"build",
|
|
484
|
+
"coverage",
|
|
485
|
+
".codebuddy",
|
|
486
|
+
"graphify-out",
|
|
487
|
+
".next",
|
|
488
|
+
".turbo",
|
|
489
|
+
"out",
|
|
490
|
+
".venv",
|
|
491
|
+
"venv",
|
|
492
|
+
"__pycache__"
|
|
493
|
+
]);
|
|
494
|
+
async function listIndexableFiles(repositoryRoot) {
|
|
495
|
+
const tracked = await gitListFiles(repositoryRoot);
|
|
496
|
+
const paths = tracked ?? await walkFiles(repositoryRoot);
|
|
497
|
+
return paths.map(normalize2).filter((path) => !path.startsWith(".codebuddy/") && !path.startsWith("graphify-out/")).filter((path) => SUPPORTED_EXTENSIONS2.has(extname4(path).toLowerCase())).sort();
|
|
498
|
+
}
|
|
499
|
+
async function gitListFiles(repositoryRoot) {
|
|
500
|
+
const result = await runGit(repositoryRoot, [
|
|
501
|
+
"ls-files",
|
|
502
|
+
"-z",
|
|
503
|
+
"--cached",
|
|
504
|
+
"--others",
|
|
505
|
+
"--exclude-standard"
|
|
506
|
+
]);
|
|
507
|
+
if (result.code !== 0) return null;
|
|
508
|
+
return result.stdout.split("\0").filter(Boolean);
|
|
509
|
+
}
|
|
510
|
+
async function walkFiles(root) {
|
|
511
|
+
const files = [];
|
|
512
|
+
const walk2 = async (directory) => {
|
|
513
|
+
const entries = await readdir2(directory, { withFileTypes: true });
|
|
514
|
+
for (const entry of entries) {
|
|
515
|
+
const absolute = join4(directory, entry.name);
|
|
516
|
+
if (entry.isDirectory()) {
|
|
517
|
+
if (FALLBACK_IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
518
|
+
await walk2(absolute);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (entry.isFile()) files.push(relative2(root, absolute));
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
await walk2(root);
|
|
525
|
+
return files;
|
|
526
|
+
}
|
|
527
|
+
function runGit(cwd, args) {
|
|
528
|
+
return new Promise((resolveResult) => {
|
|
529
|
+
const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
530
|
+
let stdout = "";
|
|
531
|
+
let stderr = "";
|
|
532
|
+
child.stdout.setEncoding("utf8");
|
|
533
|
+
child.stdout.on("data", (chunk2) => {
|
|
534
|
+
stdout += String(chunk2);
|
|
535
|
+
});
|
|
536
|
+
child.stderr.on("data", (chunk2) => {
|
|
537
|
+
stderr += String(chunk2);
|
|
538
|
+
});
|
|
539
|
+
child.on("error", () => resolveResult({ code: -1, stdout: "", stderr: "git unavailable" }));
|
|
540
|
+
child.on("close", (code) => resolveResult({ code: code ?? 0, stdout, stderr }));
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function normalize2(path) {
|
|
544
|
+
return (sep === "\\" ? path.replace(/\\/g, "/") : path).replace(/^\.\//, "");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/indexer/engine.ts
|
|
548
|
+
var MAX_FILE_BYTES = 512e3;
|
|
549
|
+
var RACY_WINDOW_MS = 3e3;
|
|
550
|
+
async function indexRepository(options = {}) {
|
|
551
|
+
return (await refreshIndex(options)).result;
|
|
552
|
+
}
|
|
553
|
+
async function loadFreshIndex(repositoryRoot) {
|
|
554
|
+
try {
|
|
555
|
+
const { result, manifest } = await refreshIndex({ repositoryRoot });
|
|
556
|
+
const delta = result.added + result.changed + result.removed;
|
|
557
|
+
const failures = result.errors.length > 0 ? `; ${result.errors.length} file(s) could not be validated and were omitted` : "";
|
|
558
|
+
return {
|
|
559
|
+
manifest,
|
|
560
|
+
explain: delta === 0 ? `index: ${result.totalIndexed} indexed file(s) match current source${failures}` : `index: refreshed against current source (${result.changed} changed, ${result.added} added, ${result.removed} removed; ${result.totalIndexed} indexed)${failures}`
|
|
561
|
+
};
|
|
562
|
+
} catch (error) {
|
|
563
|
+
return {
|
|
564
|
+
manifest: emptyManifest(),
|
|
565
|
+
explain: `index: freshness check failed (${error.message}); indexed evidence omitted`
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
var passes = /* @__PURE__ */ new Map();
|
|
570
|
+
function refreshIndex(options) {
|
|
571
|
+
const key = resolve4(options.repositoryRoot ?? process.cwd());
|
|
572
|
+
const next = (passes.get(key) ?? Promise.resolve()).catch(() => void 0).then(() => runIndexPass(options));
|
|
573
|
+
passes.set(key, next);
|
|
574
|
+
void next.finally(() => {
|
|
575
|
+
if (passes.get(key) === next) passes.delete(key);
|
|
576
|
+
}).catch(() => void 0);
|
|
577
|
+
return next;
|
|
578
|
+
}
|
|
579
|
+
async function runIndexPass(options) {
|
|
580
|
+
const start = Date.now();
|
|
581
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
582
|
+
const repositoryRoot = resolve4(options.repositoryRoot ?? process.cwd());
|
|
583
|
+
const store = new IndexStore(repositoryRoot);
|
|
584
|
+
const [files, previous] = await Promise.all([listIndexableFiles(repositoryRoot), store.read()]);
|
|
585
|
+
const entries = {};
|
|
586
|
+
const errors = [];
|
|
587
|
+
let added = 0;
|
|
588
|
+
let changed = 0;
|
|
589
|
+
let unchanged = 0;
|
|
590
|
+
let skipped = 0;
|
|
591
|
+
let dirty = previous.generatedAt === emptyManifest().generatedAt;
|
|
592
|
+
for (const path of files) {
|
|
593
|
+
try {
|
|
594
|
+
const info = await stat2(join5(repositoryRoot, path));
|
|
595
|
+
if (!info.isFile()) continue;
|
|
596
|
+
if (info.size > MAX_FILE_BYTES) {
|
|
597
|
+
skipped++;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const existing = previous.entries[path];
|
|
601
|
+
if (!options.full && existing && statUnchanged(existing, info)) {
|
|
602
|
+
entries[path] = existing;
|
|
603
|
+
unchanged++;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
const content = await readFile4(join5(repositoryRoot, path), "utf8");
|
|
607
|
+
const hash = hashContent(content);
|
|
608
|
+
const metadata = trustedMetadata(info);
|
|
609
|
+
dirty = true;
|
|
610
|
+
if (!options.full && existing && existing.hash === hash) {
|
|
611
|
+
const { mtimeMs: _mtime, ctimeMs: _ctime, ...rest } = existing;
|
|
612
|
+
entries[path] = { ...rest, size: info.size, ...metadata };
|
|
613
|
+
unchanged++;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
const { language, lines, symbols, summary } = summarizeSource(path, content);
|
|
617
|
+
const symbolTable = supportsSymbols(path) ? extractSymbolTable(content, path) : [];
|
|
618
|
+
entries[path] = {
|
|
619
|
+
path,
|
|
620
|
+
hash,
|
|
621
|
+
size: info.size,
|
|
622
|
+
...metadata,
|
|
623
|
+
language,
|
|
624
|
+
lines,
|
|
625
|
+
symbols,
|
|
626
|
+
...symbolTable.length > 0 ? { symbolTable } : {},
|
|
627
|
+
summary,
|
|
628
|
+
indexedAt: now().toISOString()
|
|
629
|
+
};
|
|
630
|
+
if (existing) changed++;
|
|
631
|
+
else added++;
|
|
632
|
+
} catch (error) {
|
|
633
|
+
errors.push({ path, error: error.message });
|
|
634
|
+
skipped++;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const removed = Object.keys(previous.entries).filter((path) => !(path in entries)).length;
|
|
638
|
+
const manifest = {
|
|
639
|
+
version: INDEX_MANIFEST_VERSION,
|
|
640
|
+
generatedAt: now().toISOString(),
|
|
641
|
+
entries: sortEntries(entries)
|
|
642
|
+
};
|
|
643
|
+
if (dirty || removed > 0) await store.write(manifest);
|
|
644
|
+
const architecture = await refreshArchitectureCache(repositoryRoot, manifest);
|
|
645
|
+
const result = {
|
|
646
|
+
scanned: files.length,
|
|
647
|
+
added,
|
|
648
|
+
changed,
|
|
649
|
+
unchanged,
|
|
650
|
+
removed,
|
|
651
|
+
skipped,
|
|
652
|
+
errors,
|
|
653
|
+
durationMs: Date.now() - start,
|
|
654
|
+
totalIndexed: Object.keys(entries).length,
|
|
655
|
+
architecture: {
|
|
656
|
+
source: architecture.source,
|
|
657
|
+
readFiles: architecture.stats.readFiles,
|
|
658
|
+
reusedFiles: architecture.stats.reusedFiles
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
return { result, manifest };
|
|
662
|
+
}
|
|
663
|
+
function statUnchanged(entry, info) {
|
|
664
|
+
return entry.mtimeMs !== void 0 && entry.size === info.size && entry.mtimeMs === info.mtimeMs && entry.ctimeMs === info.ctimeMs;
|
|
665
|
+
}
|
|
666
|
+
function trustedMetadata(info) {
|
|
667
|
+
if (Date.now() - Math.max(info.mtimeMs, info.ctimeMs) < RACY_WINDOW_MS) return {};
|
|
668
|
+
return { mtimeMs: info.mtimeMs, ctimeMs: info.ctimeMs };
|
|
669
|
+
}
|
|
670
|
+
function hashContent(content) {
|
|
671
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
672
|
+
}
|
|
673
|
+
function sortEntries(entries) {
|
|
674
|
+
return Object.fromEntries(
|
|
675
|
+
Object.keys(entries).sort().map((key) => [key, entries[key]])
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/context/discovery.ts
|
|
680
|
+
import { readFile as readFile9, realpath, stat as stat4 } from "fs/promises";
|
|
681
|
+
import { isAbsolute as isAbsolute3, join as join8, relative as relative6 } from "path";
|
|
17
682
|
|
|
18
683
|
// src/integrations/graphify.ts
|
|
19
684
|
import { execFile } from "child_process";
|
|
20
|
-
import { mkdir, mkdtemp, readFile as
|
|
685
|
+
import { mkdir as mkdir3, mkdtemp, readFile as readFile6, rm, stat as stat3, utimes } from "fs/promises";
|
|
21
686
|
import { tmpdir } from "os";
|
|
22
|
-
import { dirname, join, relative, resolve, sep } from "path";
|
|
687
|
+
import { dirname as dirname4, join as join6, relative as relative3, resolve as resolve5, sep as sep2 } from "path";
|
|
23
688
|
import { promisify } from "util";
|
|
24
689
|
|
|
25
690
|
// src/map/graph-freshness.ts
|
|
26
|
-
import { createHash } from "crypto";
|
|
27
|
-
import { readFile } from "fs/promises";
|
|
28
|
-
var digest = (value) =>
|
|
691
|
+
import { createHash as createHash2 } from "crypto";
|
|
692
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
693
|
+
var digest = (value) => createHash2("sha256").update(value).digest("hex");
|
|
29
694
|
async function writeGraphSourceSnapshot(path, manifest) {
|
|
30
695
|
await writeAtomic(
|
|
31
696
|
`${path}.sources.json`,
|
|
32
697
|
JSON.stringify({
|
|
33
698
|
version: 1,
|
|
34
|
-
graphHash: digest(await
|
|
699
|
+
graphHash: digest(await readFile5(path, "utf8")),
|
|
35
700
|
sources: Object.fromEntries(
|
|
36
701
|
Object.values(manifest.entries).map((entry) => [entry.path, entry.hash])
|
|
37
702
|
)
|
|
@@ -41,13 +706,13 @@ async function writeGraphSourceSnapshot(path, manifest) {
|
|
|
41
706
|
async function readGraphSourceSnapshot(path) {
|
|
42
707
|
let raw;
|
|
43
708
|
try {
|
|
44
|
-
raw = await
|
|
709
|
+
raw = await readFile5(`${path}.sources.json`, "utf8");
|
|
45
710
|
} catch (error) {
|
|
46
711
|
return error.code === "ENOENT" ? null : {};
|
|
47
712
|
}
|
|
48
713
|
try {
|
|
49
714
|
const snapshot = JSON.parse(raw);
|
|
50
|
-
if (snapshot.version !== 1 || snapshot.graphHash !== digest(await
|
|
715
|
+
if (snapshot.version !== 1 || snapshot.graphHash !== digest(await readFile5(path, "utf8")) || !snapshot.sources || typeof snapshot.sources !== "object" || Array.isArray(snapshot.sources))
|
|
51
716
|
return {};
|
|
52
717
|
return Object.fromEntries(
|
|
53
718
|
Object.entries(snapshot.sources).filter(
|
|
@@ -134,17 +799,17 @@ async function graphifyGraphExists(repositoryRoot, graphPath = DEFAULT_GRAPH_PAT
|
|
|
134
799
|
}
|
|
135
800
|
async function generateGraphifyGraph(options) {
|
|
136
801
|
const run = options.run ?? runCommand;
|
|
137
|
-
const repositoryRoot =
|
|
802
|
+
const repositoryRoot = resolve5(options.repositoryRoot);
|
|
138
803
|
const graphPath = options.graphPath ?? DEFAULT_GRAPH_PATH;
|
|
139
804
|
const destination = resolveGraphPath(repositoryRoot, graphPath);
|
|
140
|
-
await
|
|
141
|
-
await ignoreGeneratedGraphDirectory(
|
|
805
|
+
await mkdir3(dirname4(destination), { recursive: true });
|
|
806
|
+
await ignoreGeneratedGraphDirectory(dirname4(destination));
|
|
142
807
|
const { manifest: sourceSnapshot } = await loadFreshIndex(repositoryRoot);
|
|
143
808
|
const current = await readGraphFileIfPresent(destination);
|
|
144
809
|
const isDefaultPath = graphPath === DEFAULT_GRAPH_PATH;
|
|
145
810
|
if (current && isDefaultPath) {
|
|
146
|
-
const backup = await
|
|
147
|
-
const backupStat = await
|
|
811
|
+
const backup = await readFile6(destination, "utf8");
|
|
812
|
+
const backupStat = await stat3(destination);
|
|
148
813
|
try {
|
|
149
814
|
await run("graphify", ["update", repositoryRoot, "--force", "--no-cluster"], repositoryRoot);
|
|
150
815
|
const updated = await readGraphFile(destination);
|
|
@@ -158,16 +823,16 @@ async function generateGraphifyGraph(options) {
|
|
|
158
823
|
);
|
|
159
824
|
}
|
|
160
825
|
}
|
|
161
|
-
const stagingRoot = await mkdtemp(
|
|
826
|
+
const stagingRoot = await mkdtemp(join6(tmpdir(), "codebuddy-graphify-"));
|
|
162
827
|
try {
|
|
163
828
|
await run(
|
|
164
829
|
"graphify",
|
|
165
830
|
["extract", repositoryRoot, "--out", stagingRoot, "--code-only", "--no-cluster"],
|
|
166
831
|
repositoryRoot
|
|
167
832
|
);
|
|
168
|
-
const stagedPath =
|
|
833
|
+
const stagedPath = join6(stagingRoot, DEFAULT_GRAPH_PATH);
|
|
169
834
|
const staged = await readGraphFile(stagedPath);
|
|
170
|
-
await writeAtomic(destination, await
|
|
835
|
+
await writeAtomic(destination, await readFile6(stagedPath, "utf8"));
|
|
171
836
|
await writeGraphSourceSnapshot(destination, sourceSnapshot);
|
|
172
837
|
return { graphPath, generated: true, ...staged };
|
|
173
838
|
} finally {
|
|
@@ -175,17 +840,17 @@ async function generateGraphifyGraph(options) {
|
|
|
175
840
|
}
|
|
176
841
|
}
|
|
177
842
|
async function ignoreGeneratedGraphDirectory(directory) {
|
|
178
|
-
const path =
|
|
843
|
+
const path = join6(directory, ".gitignore");
|
|
179
844
|
try {
|
|
180
|
-
await
|
|
845
|
+
await stat3(path);
|
|
181
846
|
} catch {
|
|
182
847
|
await writeAtomic(path, "# CodeBuddy-generated Graphify output (derived, rebuildable)\n*\n");
|
|
183
848
|
}
|
|
184
849
|
}
|
|
185
850
|
function resolveGraphPath(repositoryRoot, graphPath) {
|
|
186
|
-
const absolute =
|
|
187
|
-
const relativePath =
|
|
188
|
-
if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${
|
|
851
|
+
const absolute = resolve5(repositoryRoot, graphPath);
|
|
852
|
+
const relativePath = relative3(repositoryRoot, absolute);
|
|
853
|
+
if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep2}`)) {
|
|
189
854
|
throw new Error(`Graph path must be inside the project root: ${graphPath}`);
|
|
190
855
|
}
|
|
191
856
|
return absolute;
|
|
@@ -198,10 +863,10 @@ async function readGraphFileIfPresent(path) {
|
|
|
198
863
|
}
|
|
199
864
|
}
|
|
200
865
|
async function readGraphFile(path) {
|
|
201
|
-
const info = await
|
|
866
|
+
const info = await stat3(path);
|
|
202
867
|
if (!info.isFile() || info.size === 0)
|
|
203
868
|
throw new Error(`Graphify graph is missing or empty: ${path}`);
|
|
204
|
-
const raw = JSON.parse(await
|
|
869
|
+
const raw = JSON.parse(await readFile6(path, "utf8"));
|
|
205
870
|
const nodes = arrayLength(raw.nodes ?? raw.entities);
|
|
206
871
|
const edges = arrayLength(raw.edges ?? raw.links ?? raw.relationships);
|
|
207
872
|
if (nodes === 0) throw new Error(`Graphify graph contains no nodes: ${path}`);
|
|
@@ -211,17 +876,9 @@ function arrayLength(value) {
|
|
|
211
876
|
return Array.isArray(value) ? value.length : 0;
|
|
212
877
|
}
|
|
213
878
|
|
|
214
|
-
// src/context/file-vectors.ts
|
|
215
|
-
import { mkdir as mkdir3, readFile as readFile6 } from "fs/promises";
|
|
216
|
-
import { dirname as dirname3, join as join4 } from "path";
|
|
217
|
-
|
|
218
|
-
// src/context/discovery.ts
|
|
219
|
-
import { readFile as readFile5, realpath, stat as stat2 } from "fs/promises";
|
|
220
|
-
import { isAbsolute as isAbsolute3, join as join3, relative as relative4 } from "path";
|
|
221
|
-
|
|
222
879
|
// src/map/graphify.ts
|
|
223
|
-
import { readFile as
|
|
224
|
-
import { isAbsolute, relative as
|
|
880
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
881
|
+
import { isAbsolute, relative as relative4, resolve as resolve6 } from "path";
|
|
225
882
|
var NON_DEPENDENCY_RELATIONS = /* @__PURE__ */ new Set([
|
|
226
883
|
"contains",
|
|
227
884
|
"part_of",
|
|
@@ -255,7 +912,7 @@ var DIRECT_DEPENDENCY_RELATIONS = /* @__PURE__ */ new Set([
|
|
|
255
912
|
]);
|
|
256
913
|
var MAX_EDGE_SYMBOLS = 8;
|
|
257
914
|
async function loadGraphifyMap(graphPath, repositoryRoot = process.cwd()) {
|
|
258
|
-
const raw = JSON.parse(await
|
|
915
|
+
const raw = JSON.parse(await readFile7(graphPath, "utf8"));
|
|
259
916
|
const nodes = arrayRecord(raw.nodes ?? raw.entities);
|
|
260
917
|
const edges = arrayRecord(raw.edges ?? raw.links ?? raw.relationships);
|
|
261
918
|
const nodePaths = /* @__PURE__ */ new Map();
|
|
@@ -268,7 +925,7 @@ async function loadGraphifyMap(graphPath, repositoryRoot = process.cwd()) {
|
|
|
268
925
|
nodePaths.set(id, path);
|
|
269
926
|
const label = stringValue(node.label ?? node.name);
|
|
270
927
|
if (label && label !== path && !path.endsWith(`/${label}`)) nodeLabels.set(id, label);
|
|
271
|
-
modules.push({ path, language:
|
|
928
|
+
modules.push({ path, language: languageFor2(path), imports: [] });
|
|
272
929
|
}
|
|
273
930
|
const modulePaths = new Set(modules.map((module) => module.path));
|
|
274
931
|
const importedEdges = [];
|
|
@@ -348,21 +1005,21 @@ function graphifyPath(node, repositoryRoot) {
|
|
|
348
1005
|
return candidate ? normalizeGraphPath(candidate, repositoryRoot) : null;
|
|
349
1006
|
}
|
|
350
1007
|
function normalizeGraphPath(path, repositoryRoot) {
|
|
351
|
-
const absolute = isAbsolute(path) ?
|
|
352
|
-
const relativePath =
|
|
1008
|
+
const absolute = isAbsolute(path) ? resolve6(path) : resolve6(repositoryRoot, path);
|
|
1009
|
+
const relativePath = relative4(resolve6(repositoryRoot), absolute).replace(/\\/g, "/");
|
|
353
1010
|
if (relativePath === "" || relativePath === ".." || relativePath.startsWith("../")) return null;
|
|
354
1011
|
return relativePath.replace(/^\.\//, "");
|
|
355
1012
|
}
|
|
356
1013
|
function stringValue(value) {
|
|
357
1014
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
358
1015
|
}
|
|
359
|
-
function
|
|
1016
|
+
function languageFor2(path) {
|
|
360
1017
|
if (/\.py$/.test(path)) return "py";
|
|
361
1018
|
return /\.tsx?$/.test(path) ? "ts" : "js";
|
|
362
1019
|
}
|
|
363
1020
|
|
|
364
1021
|
// src/context/anchors.ts
|
|
365
|
-
import { isAbsolute as isAbsolute2, relative as
|
|
1022
|
+
import { isAbsolute as isAbsolute2, relative as relative5, resolve as resolve7, sep as sep3 } from "path";
|
|
366
1023
|
var MAX_ANCHORS = 3;
|
|
367
1024
|
var MAX_ERROR_SEARCH_FILES = 2e3;
|
|
368
1025
|
var JS_FRAME = /\bat\s+(?:[^\s()]+\s+\()?([^\s()]+?):(\d+):\d+\)?/g;
|
|
@@ -370,7 +1027,7 @@ var PY_FRAME = /\bFile\s+"([^"]+)",\s+line\s+(\d+)/g;
|
|
|
370
1027
|
var PATH_TOKEN = /(?:^|[\s(`'"[])((?:\.{0,2}\/)?[\w@.-]+(?:\/[\w@.-]+)*\.[A-Za-z0-9]+)(?::(\d+))?/g;
|
|
371
1028
|
var QUOTED = /"([^"\n]{8,200})"|'([^'\n]{8,200})'|`([^`\n]{8,200})`/g;
|
|
372
1029
|
async function extractTaskAnchors(repositoryRoot, task, paths) {
|
|
373
|
-
const root =
|
|
1030
|
+
const root = resolve7(repositoryRoot);
|
|
374
1031
|
const found = [];
|
|
375
1032
|
const add = (raw, line, kind, at) => {
|
|
376
1033
|
const path = toIndexedPath(root, raw, paths);
|
|
@@ -398,9 +1055,9 @@ async function extractTaskAnchors(repositoryRoot, task, paths) {
|
|
|
398
1055
|
return anchors.slice(0, MAX_ANCHORS);
|
|
399
1056
|
}
|
|
400
1057
|
function toIndexedPath(root, raw, paths) {
|
|
401
|
-
const absolute = isAbsolute2(raw) ? raw :
|
|
402
|
-
const rel =
|
|
403
|
-
if (rel === "" || rel.startsWith(`..${
|
|
1058
|
+
const absolute = isAbsolute2(raw) ? raw : resolve7(root, raw);
|
|
1059
|
+
const rel = relative5(root, absolute);
|
|
1060
|
+
if (rel === "" || rel.startsWith(`..${sep3}`) || rel === ".." || isAbsolute2(rel)) return null;
|
|
404
1061
|
const normalized = rel.replace(/\\/g, "/");
|
|
405
1062
|
return paths.has(normalized) ? normalized : null;
|
|
406
1063
|
}
|
|
@@ -426,15 +1083,16 @@ async function errorAnchors(root, task, paths) {
|
|
|
426
1083
|
}
|
|
427
1084
|
|
|
428
1085
|
// src/context/content-index.ts
|
|
429
|
-
import { mkdir as
|
|
430
|
-
import { dirname as
|
|
1086
|
+
import { mkdir as mkdir4, readFile as readFile8 } from "fs/promises";
|
|
1087
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
431
1088
|
var PASSAGE_LINES = 40;
|
|
432
1089
|
var K1 = 1.2;
|
|
433
1090
|
var B = 0.75;
|
|
434
1091
|
var MIN_TERMS = 2;
|
|
435
1092
|
var MIN_SHARE = 0.5;
|
|
436
1093
|
var MIN_IDF_COVERAGE = 0.3;
|
|
437
|
-
|
|
1094
|
+
var CARD_WEIGHT = 0.5;
|
|
1095
|
+
async function rankContent(repositoryRoot, manifest, terms, wantsTests, wantsDocs = false, wantsChangelog = false, cards = /* @__PURE__ */ new Map()) {
|
|
438
1096
|
const none = { matches: [], idf: /* @__PURE__ */ new Map() };
|
|
439
1097
|
if (terms.length === 0) return none;
|
|
440
1098
|
const paths = Object.keys(manifest.entries).filter(
|
|
@@ -478,29 +1136,38 @@ async function rankContent(repositoryRoot, manifest, terms, wantsTests, wantsDoc
|
|
|
478
1136
|
}
|
|
479
1137
|
const documents = Object.entries(files);
|
|
480
1138
|
if (documents.length === 0) return none;
|
|
1139
|
+
const cardWords = new Map(
|
|
1140
|
+
documents.flatMap(([path]) => {
|
|
1141
|
+
const text = cards.get(path);
|
|
1142
|
+
return text ? [[path, new Set(splitWords(text))]] : [];
|
|
1143
|
+
})
|
|
1144
|
+
);
|
|
1145
|
+
const has = (path, file, term) => (file.counts[term] ?? 0) > 0 || cardWords.get(path)?.has(term) === true;
|
|
481
1146
|
const averageLength = documents.reduce((sum, [, file]) => sum + file.length, 0) / documents.length;
|
|
482
1147
|
const idf = new Map(
|
|
483
1148
|
terms.map((term) => {
|
|
484
|
-
const df = documents.filter(([, file]) => (file
|
|
1149
|
+
const df = documents.filter(([path, file]) => has(path, file, term)).length;
|
|
485
1150
|
return [term, Math.log(1 + (documents.length - df + 0.5) / (df + 0.5))];
|
|
486
1151
|
})
|
|
487
1152
|
);
|
|
488
1153
|
const totalIdf = [...idf.values()].reduce((sum, value) => sum + value, 0);
|
|
489
1154
|
const scored = documents.map(([path, file]) => {
|
|
490
1155
|
const fileName = new Set(splitWords(path.slice(path.lastIndexOf("/") + 1)));
|
|
1156
|
+
const card = cardWords.get(path);
|
|
491
1157
|
let score = 0;
|
|
492
1158
|
let covered = 0;
|
|
493
1159
|
for (const term of terms) {
|
|
494
|
-
const
|
|
1160
|
+
const inSource = file.counts[term] ?? 0;
|
|
1161
|
+
const tf = inSource || (card?.has(term) ? CARD_WEIGHT : 0);
|
|
495
1162
|
if (tf === 0) continue;
|
|
496
|
-
covered += idf.get(term) ?? 0;
|
|
1163
|
+
covered += (idf.get(term) ?? 0) * (inSource > 0 ? 1 : CARD_WEIGHT);
|
|
497
1164
|
const saturation = tf * (K1 + 1) / (tf + K1 * (1 - B + B * file.length / averageLength));
|
|
498
1165
|
score += (idf.get(term) ?? 0) * saturation * (fileName.has(term) ? 1.5 : 1);
|
|
499
1166
|
}
|
|
500
1167
|
const named = terms.filter((term) => fileName.has(term));
|
|
501
1168
|
const matched = Math.max(
|
|
502
1169
|
named.length,
|
|
503
|
-
...file.passages.map(
|
|
1170
|
+
...[...file.passages, ...card ? [[...card]] : []].map(
|
|
504
1171
|
(words) => (/* @__PURE__ */ new Set([...named, ...terms.filter((term) => words.includes(term))])).size
|
|
505
1172
|
)
|
|
506
1173
|
);
|
|
@@ -520,11 +1187,11 @@ function idfCoverage(text, terms, idf) {
|
|
|
520
1187
|
return terms.reduce((sum, term) => sum + (words.has(term) ? idf.get(term) ?? 0 : 0), 0) / total;
|
|
521
1188
|
}
|
|
522
1189
|
function cachePath(repositoryRoot) {
|
|
523
|
-
return
|
|
1190
|
+
return join7(repositoryRoot, ".codebuddy", "cache", "content-terms.json");
|
|
524
1191
|
}
|
|
525
1192
|
async function loadCache(repositoryRoot) {
|
|
526
1193
|
try {
|
|
527
|
-
const parsed = JSON.parse(await
|
|
1194
|
+
const parsed = JSON.parse(await readFile8(cachePath(repositoryRoot), "utf8"));
|
|
528
1195
|
if (parsed.version === 2 && parsed.files && typeof parsed.files === "object") return parsed;
|
|
529
1196
|
} catch {
|
|
530
1197
|
}
|
|
@@ -533,7 +1200,7 @@ async function loadCache(repositoryRoot) {
|
|
|
533
1200
|
async function saveCache(repositoryRoot, cache) {
|
|
534
1201
|
try {
|
|
535
1202
|
const path = cachePath(repositoryRoot);
|
|
536
|
-
await
|
|
1203
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
537
1204
|
await writeAtomic(path, JSON.stringify(cache));
|
|
538
1205
|
} catch {
|
|
539
1206
|
}
|
|
@@ -547,22 +1214,25 @@ var MAX_EVIDENCE_BYTES = 512e3;
|
|
|
547
1214
|
async function readBoundedSource(repositoryRoot, path) {
|
|
548
1215
|
if (isCredentialPath(path)) return null;
|
|
549
1216
|
try {
|
|
550
|
-
const absolute =
|
|
551
|
-
if ((await
|
|
552
|
-
const source = await
|
|
1217
|
+
const absolute = join8(repositoryRoot, path);
|
|
1218
|
+
if ((await stat4(absolute)).size > MAX_EVIDENCE_BYTES) return null;
|
|
1219
|
+
const source = await readFile9(absolute, "utf8");
|
|
553
1220
|
return source.includes("\0") ? null : source;
|
|
554
1221
|
} catch {
|
|
555
1222
|
return null;
|
|
556
1223
|
}
|
|
557
1224
|
}
|
|
558
1225
|
async function discoverTaskPaths(repositoryRoot, task, manifest, mapSource, options = {}) {
|
|
1226
|
+
task = stripIssueTemplate(task);
|
|
559
1227
|
const anchors = await extractTaskAnchors(
|
|
560
1228
|
repositoryRoot,
|
|
561
1229
|
task,
|
|
562
1230
|
new Set(Object.keys(manifest.entries))
|
|
563
1231
|
);
|
|
564
|
-
if (anchors.length === 0)
|
|
565
|
-
|
|
1232
|
+
if (anchors.length === 0) {
|
|
1233
|
+
const ranked2 = await rankTaskFiles(repositoryRoot, task, manifest, mapSource, options);
|
|
1234
|
+
return await withExpansion(repositoryRoot, task, ranked2, manifest, mapSource, options) ?? ranked2;
|
|
1235
|
+
}
|
|
566
1236
|
const anchored = anchors.map((anchor) => anchor.path);
|
|
567
1237
|
const hits = anchors.flatMap(
|
|
568
1238
|
(anchor) => anchor.line === null ? [] : [
|
|
@@ -579,7 +1249,9 @@ async function discoverTaskPaths(repositoryRoot, task, manifest, mapSource, opti
|
|
|
579
1249
|
const explain = `retrieval: anchors (${anchors.map(
|
|
580
1250
|
(anchor) => `${anchor.kind} ${anchor.path}${anchor.line === null ? "" : `:${anchor.line}`}`
|
|
581
1251
|
).join(", ")})`;
|
|
582
|
-
if (anchored.length >= TASK_FILES)
|
|
1252
|
+
if (anchored.length >= TASK_FILES) {
|
|
1253
|
+
return { paths: anchored, hits, explain: [explain], confidence: "high" };
|
|
1254
|
+
}
|
|
583
1255
|
const ranked = await rankTaskFiles(repositoryRoot, task, manifest, mapSource, options);
|
|
584
1256
|
return {
|
|
585
1257
|
...ranked.symbols ? { symbols: ranked.symbols } : {},
|
|
@@ -588,35 +1260,85 @@ async function discoverTaskPaths(repositoryRoot, task, manifest, mapSource, opti
|
|
|
588
1260
|
TASK_FILES
|
|
589
1261
|
),
|
|
590
1262
|
hits: [...hits, ...ranked.hits.filter((hit) => !anchored.includes(hit.path))],
|
|
591
|
-
explain: [explain, ...ranked.explain]
|
|
1263
|
+
explain: [explain, ...ranked.explain],
|
|
1264
|
+
confidence: "high"
|
|
592
1265
|
};
|
|
593
1266
|
}
|
|
594
1267
|
async function withLlmSummaries(repositoryRoot, manifest) {
|
|
595
1268
|
const entries = Object.values(manifest.entries);
|
|
596
1269
|
try {
|
|
597
|
-
|
|
598
|
-
const
|
|
599
|
-
if (!config.enabled || !config.features.summaries) return entries;
|
|
600
|
-
const { loadLlmSummaries } = await import("./summaries-ML53R7EL.js");
|
|
1270
|
+
if ((await readConfigFile(repositoryRoot)).llm?.enabled === false) return entries;
|
|
1271
|
+
const { loadLlmSummaries, cardWords } = await import("./summaries-QXWFFDUZ.js");
|
|
601
1272
|
const summaries = await loadLlmSummaries(repositoryRoot, manifest);
|
|
602
1273
|
if (summaries.size === 0) return entries;
|
|
603
1274
|
return entries.map((entry) => {
|
|
604
1275
|
const summary = summaries.get(entry.path);
|
|
605
|
-
return summary ? {
|
|
1276
|
+
return summary ? {
|
|
1277
|
+
...entry,
|
|
1278
|
+
llmText: `${summary.summary} ${Object.values(summary.symbols).join(" ")}`,
|
|
1279
|
+
...summary.symptoms ? { cardWords: cardWords(summary) } : {}
|
|
1280
|
+
} : entry;
|
|
606
1281
|
});
|
|
607
1282
|
} catch {
|
|
608
1283
|
return entries;
|
|
609
1284
|
}
|
|
610
1285
|
}
|
|
1286
|
+
var EXPANSION_MS = { hook: 300, pack: 800 };
|
|
1287
|
+
async function withExpansion(repositoryRoot, task, ranked, manifest, mapSource, options) {
|
|
1288
|
+
if (ranked.paths.length > 0 && ranked.confidence !== "low") return null;
|
|
1289
|
+
if (identifierTokens(task).some((token) => CODE_IDENTIFIER.test(token))) return null;
|
|
1290
|
+
const deadlineAt = options.expansionDeadlineAt ?? Date.now() + EXPANSION_MS.pack;
|
|
1291
|
+
const { expandTask } = await import("./expand-KWYGNCKZ.js");
|
|
1292
|
+
const expansion = await expandTask(repositoryRoot, task, deadlineAt - Date.now());
|
|
1293
|
+
if (!expansion) return null;
|
|
1294
|
+
const expanded = await rankTaskFiles(
|
|
1295
|
+
repositoryRoot,
|
|
1296
|
+
`${task} ${expansion.terms.join(" ")}`,
|
|
1297
|
+
manifest,
|
|
1298
|
+
mapSource,
|
|
1299
|
+
options
|
|
1300
|
+
);
|
|
1301
|
+
const note = `expansion: task expanded with ${expansion.terms.length} code term(s) (${expansion.terms.join(", ")}; ${expansion.cached ? "cached" : "local model"})`;
|
|
1302
|
+
if (expanded.paths.length === 0)
|
|
1303
|
+
return { ...ranked, explain: [...ranked.explain, `${note}; still no match`] };
|
|
1304
|
+
return { ...expanded, explain: [...ranked.explain, note, ...expanded.explain] };
|
|
1305
|
+
}
|
|
1306
|
+
var AGREEMENT = { topK: 5, chunkFloor: 0.8, cardFloor: 0.75 };
|
|
1307
|
+
function agreementVotes(path, signals, config = AGREEMENT) {
|
|
1308
|
+
const names = [];
|
|
1309
|
+
let scored = 0;
|
|
1310
|
+
if (signals.zg.slice(0, config.topK).includes(path)) names.push("zg");
|
|
1311
|
+
const within = (ranking, floor) => (ranking ?? []).slice(0, config.topK).some((match) => match.path === path && match.score >= floor);
|
|
1312
|
+
if (within(signals.chunks, config.chunkFloor)) {
|
|
1313
|
+
names.push("chunk");
|
|
1314
|
+
scored++;
|
|
1315
|
+
}
|
|
1316
|
+
if (within(signals.cards, config.cardFloor)) {
|
|
1317
|
+
names.push("card");
|
|
1318
|
+
scored++;
|
|
1319
|
+
}
|
|
1320
|
+
if (signals.term) {
|
|
1321
|
+
names.push("terms");
|
|
1322
|
+
scored++;
|
|
1323
|
+
}
|
|
1324
|
+
return { votes: names.length, scored, names };
|
|
1325
|
+
}
|
|
1326
|
+
function agrees(vote) {
|
|
1327
|
+
return vote.votes >= 2 && vote.scored >= 1;
|
|
1328
|
+
}
|
|
611
1329
|
async function rankTaskFiles(repositoryRoot, task, manifest, mapSource, options) {
|
|
612
1330
|
const wantsChangelog = asksForChangelog(task);
|
|
613
1331
|
const entries = (await withLlmSummaries(repositoryRoot, manifest)).filter(
|
|
614
1332
|
(entry) => wantsChangelog || !CHANGELOG_PATH.test(entry.path)
|
|
615
1333
|
);
|
|
616
1334
|
const wantsTests = /\b(tests?|spec)\b/i.test(task);
|
|
617
|
-
const identifiers = identifierTokens(task).filter(
|
|
1335
|
+
const identifiers = identifierTokens(task).filter(
|
|
1336
|
+
(token) => CODE_IDENTIFIER.test(token) && !ABBREVIATION.test(token)
|
|
1337
|
+
);
|
|
618
1338
|
if (identifiers.length > 0) {
|
|
619
|
-
const exact = entries.filter(
|
|
1339
|
+
const exact = entries.filter(
|
|
1340
|
+
(entry) => (wantsTests || !TEST_PATH.test(entry.path)) && countsAsSource(entry.path, task) && !EXCEPTION_MODULE.test(entry.path)
|
|
1341
|
+
).map((entry) => ({
|
|
620
1342
|
path: entry.path,
|
|
621
1343
|
named: new Set(
|
|
622
1344
|
(entry.symbolTable ?? []).flatMap(
|
|
@@ -637,6 +1359,7 @@ async function rankTaskFiles(repositoryRoot, task, manifest, mapSource, options)
|
|
|
637
1359
|
return {
|
|
638
1360
|
paths: expansion2.paths.slice(0, TASK_FILES),
|
|
639
1361
|
hits: [],
|
|
1362
|
+
confidence: "high",
|
|
640
1363
|
explain: [
|
|
641
1364
|
`retrieval: exact-symbol routing (${identifiers.slice(0, 5).join(", ")})`,
|
|
642
1365
|
...expansion2.explain ? [expansion2.explain] : []
|
|
@@ -653,7 +1376,8 @@ async function rankTaskFiles(repositoryRoot, task, manifest, mapSource, options)
|
|
|
653
1376
|
taskTerms(task),
|
|
654
1377
|
wantsTests,
|
|
655
1378
|
DOCS_TASK.test(task),
|
|
656
|
-
wantsChangelog
|
|
1379
|
+
wantsChangelog,
|
|
1380
|
+
new Map(entries.flatMap((entry) => entry.cardWords ? [[entry.path, entry.cardWords]] : []))
|
|
657
1381
|
);
|
|
658
1382
|
const keyword = content.length > 0 ? fuseRankings([summaryKeyword, content.map((match) => match.path)]) : summaryKeyword;
|
|
659
1383
|
const weakKeyword = scored.length - keyword.length;
|
|
@@ -664,7 +1388,7 @@ async function rankTaskFiles(repositoryRoot, task, manifest, mapSource, options)
|
|
|
664
1388
|
reason: "semantic retrieval off",
|
|
665
1389
|
stale: 0,
|
|
666
1390
|
changed: 0
|
|
667
|
-
} : await import("./zvec-
|
|
1391
|
+
} : await import("./zvec-F73IBWO5.js").then(
|
|
668
1392
|
({ zvecCandidates }) => zvecCandidates(
|
|
669
1393
|
repositoryRoot,
|
|
670
1394
|
task,
|
|
@@ -686,51 +1410,64 @@ async function rankTaskFiles(repositoryRoot, task, manifest, mapSource, options)
|
|
|
686
1410
|
let ranked = keyword;
|
|
687
1411
|
let hits = [];
|
|
688
1412
|
const explain = [];
|
|
1413
|
+
const agreement = await agreementConfig(repositoryRoot);
|
|
1414
|
+
const cardsOn = entries.some((entry) => entry.cardWords);
|
|
1415
|
+
const dense = options.semantic === false && options.vectorDeadlineAt === void 0 ? null : await raceDeadline(
|
|
1416
|
+
rankFileVectors(repositoryRoot, manifest, task, options.embed),
|
|
1417
|
+
options.semantic === false ? options.vectorDeadlineAt : void 0
|
|
1418
|
+
);
|
|
1419
|
+
const chunks = dense?.chunks ?? null;
|
|
1420
|
+
const cards = cardsOn ? dense?.cards ?? null : null;
|
|
1421
|
+
const usable = (path) => (wantsTests || !TEST_PATH.test(path)) && (wantsChangelog || !CHANGELOG_PATH.test(path));
|
|
1422
|
+
const zg = fuseRankings([semantic.paths, semantic.cardPaths ?? []]).filter(usable);
|
|
1423
|
+
const votes = /* @__PURE__ */ new Map();
|
|
1424
|
+
const denseAvailable = zg.length > 0 || chunks !== null || cards !== null;
|
|
689
1425
|
if (semantic.status !== "disabled" && semantic.paths.length === 0) {
|
|
690
1426
|
explain.push(`retrieval: keyword ranking only (${semantic.reason})`);
|
|
691
|
-
}
|
|
1427
|
+
}
|
|
1428
|
+
if (denseAvailable) {
|
|
692
1429
|
const terms = taskTerms(task);
|
|
693
1430
|
const vector = [];
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
)) {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1431
|
+
let rejected = 0;
|
|
1432
|
+
const top = (ranking) => (ranking ?? []).slice(0, agreement.topK).map((match) => match.path);
|
|
1433
|
+
const candidates = [
|
|
1434
|
+
.../* @__PURE__ */ new Set([
|
|
1435
|
+
...semantic.paths,
|
|
1436
|
+
...zg.slice(0, agreement.topK),
|
|
1437
|
+
...top(chunks),
|
|
1438
|
+
...top(cards)
|
|
1439
|
+
])
|
|
1440
|
+
].filter(usable);
|
|
1441
|
+
for (const path of candidates) {
|
|
1442
|
+
const termVote = await termEvidence(repositoryRoot, path, semantic, terms, idf, manifest);
|
|
1443
|
+
const vote = agreementVotes(path, { zg, chunks, cards, term: termVote }, agreement);
|
|
1444
|
+
votes.set(path, vote);
|
|
1445
|
+
if (keyword.includes(path)) {
|
|
1446
|
+
vector.push(path);
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
if (!countsAsSource(path, task) || !agrees(vote)) {
|
|
1450
|
+
rejected++;
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
vector.push(path);
|
|
1454
|
+
if (!evidence.has(path)) evidence.set(path, 0);
|
|
718
1455
|
}
|
|
719
1456
|
ranked = rankEvidenceCandidates(
|
|
720
1457
|
scored.map((entry) => entry.path),
|
|
721
1458
|
keyword,
|
|
722
1459
|
vector,
|
|
723
|
-
|
|
1460
|
+
[]
|
|
724
1461
|
);
|
|
725
|
-
hits = semantic.hits.filter((hit) => vector.includes(hit.path)
|
|
1462
|
+
hits = semantic.hits.filter((hit) => vector.includes(hit.path));
|
|
1463
|
+
const sources = [
|
|
1464
|
+
...zg.length > 0 ? [semantic.reason] : [],
|
|
1465
|
+
...chunks ? ["file vectors"] : [],
|
|
1466
|
+
...cards ? ["intent-card vectors"] : []
|
|
1467
|
+
];
|
|
726
1468
|
explain.push(
|
|
727
|
-
`retrieval: keyword ranking fused with ${
|
|
1469
|
+
`retrieval: keyword ranking fused with ${sources.join(", ")}; agreement rule (2 of zg/chunk/card/terms in top ${agreement.topK})` + (rejected > 0 ? `; ${rejected} dense candidate(s) without agreement dropped` : "")
|
|
728
1470
|
);
|
|
729
|
-
if (belowFloor > 0) {
|
|
730
|
-
explain.push(
|
|
731
|
-
`abstention: ${belowFloor} vector-only file(s) below zvec.minScore ${minScore} (task IDF coverage of current source) dropped`
|
|
732
|
-
);
|
|
733
|
-
}
|
|
734
1471
|
}
|
|
735
1472
|
let symbols;
|
|
736
1473
|
const reranked = await llmRerank({
|
|
@@ -752,21 +1489,21 @@ async function rankTaskFiles(repositoryRoot, task, manifest, mapSource, options)
|
|
|
752
1489
|
const wantsDocs = DOCS_TASK.test(task);
|
|
753
1490
|
if (!ranked.some((path) => countsAsSource(path, task))) {
|
|
754
1491
|
explain.push(
|
|
755
|
-
"abstention: no source file reached the evidence floor (" + (ranked.length > 0 ? "only docs/config did" : `best keyword score ${scored[0]?.score ?? 0} < ${EVIDENCE_FLOOR.keywordScore}`) + (commands.length > 0 ? "); only the named CLI command returned" : "); no task targets returned")
|
|
1492
|
+
"abstention: no source file reached the evidence floor or the agreement rule (" + (ranked.length > 0 ? "only docs/config did" : `best keyword score ${scored[0]?.score ?? 0} < ${EVIDENCE_FLOOR.keywordScore}`) + (commands.length > 0 ? "); only the named CLI command returned" : "); no task targets returned")
|
|
756
1493
|
);
|
|
757
|
-
return { paths: commands, hits: [], explain };
|
|
1494
|
+
return { paths: commands, hits: [], explain, confidence: commands.length > 0 ? "high" : "low" };
|
|
758
1495
|
}
|
|
759
1496
|
if (weakKeyword > 0) {
|
|
760
1497
|
explain.push(
|
|
761
1498
|
`abstention: ${weakKeyword} weak keyword candidate(s) below the evidence floor (score < ${EVIDENCE_FLOOR.keywordScore}) dropped`
|
|
762
1499
|
);
|
|
763
1500
|
}
|
|
764
|
-
const vectors =
|
|
1501
|
+
const vectors = reranked || !chunks ? null : cards ? fuseRankings([chunks, cards].map((ranking) => ranking.map((match) => match.path))) : chunks.map((match) => match.path);
|
|
765
1502
|
if (vectors && vectors.length > 0) {
|
|
766
1503
|
const doc = wantsDocs ? ranked.filter((path) => DOC_PATH.test(path)).slice(0, 1) : [];
|
|
767
|
-
ranked = [...doc, ...vectors
|
|
1504
|
+
ranked = [...doc, ...vectors].slice(0, TASK_FILES);
|
|
768
1505
|
explain.push(
|
|
769
|
-
`retrieval: file vectors ordered the files (best ${
|
|
1506
|
+
`retrieval: ${cards ? "file and card" : "file"} vectors ordered the files (best chunk ${chunks?.[0]?.score.toFixed(2)})`
|
|
770
1507
|
);
|
|
771
1508
|
}
|
|
772
1509
|
const expansion = expandAlongImports(ranked, await mapSource, evidence, task);
|
|
@@ -780,18 +1517,82 @@ async function rankTaskFiles(repositoryRoot, task, manifest, mapSource, options)
|
|
|
780
1517
|
`abstention: ${paths.length - TASK_FILES} lower-ranked candidate(s) beyond the top ${TASK_FILES} dropped`
|
|
781
1518
|
);
|
|
782
1519
|
}
|
|
1520
|
+
const returned = paths.slice(0, TASK_FILES);
|
|
1521
|
+
const confidence = confidenceOf(returned, {
|
|
1522
|
+
keyword,
|
|
1523
|
+
strongKeyword: scored.filter((entry) => entry.score >= 2 * EVIDENCE_FLOOR.keywordScore).map((entry) => entry.path),
|
|
1524
|
+
commands,
|
|
1525
|
+
votes,
|
|
1526
|
+
denseAvailable
|
|
1527
|
+
});
|
|
1528
|
+
if (confidence === "low") {
|
|
1529
|
+
explain.push(
|
|
1530
|
+
"abstention: low confidence (no returned file had two agreeing signals); no task targets returned"
|
|
1531
|
+
);
|
|
1532
|
+
return { paths: [], hits: [], explain, confidence };
|
|
1533
|
+
}
|
|
783
1534
|
return {
|
|
784
|
-
paths:
|
|
1535
|
+
paths: returned,
|
|
785
1536
|
hits,
|
|
786
1537
|
explain,
|
|
1538
|
+
confidence,
|
|
787
1539
|
...symbols && Object.keys(symbols).length > 0 ? { symbols } : {}
|
|
788
1540
|
};
|
|
789
1541
|
}
|
|
1542
|
+
function confidenceOf(paths, input) {
|
|
1543
|
+
if (paths.some((path) => input.commands.includes(path))) return "high";
|
|
1544
|
+
if (!input.denseAvailable) return "medium";
|
|
1545
|
+
let best = 0;
|
|
1546
|
+
for (const path of paths) {
|
|
1547
|
+
const dense = input.votes.get(path)?.votes ?? 0;
|
|
1548
|
+
const lexical = input.strongKeyword.includes(path) ? 2 : input.keyword.includes(path) ? 1 : 0;
|
|
1549
|
+
best = Math.max(best, dense + lexical);
|
|
1550
|
+
}
|
|
1551
|
+
return best >= 3 ? "high" : best >= 2 ? "medium" : "low";
|
|
1552
|
+
}
|
|
1553
|
+
async function termEvidence(repositoryRoot, path, semantic, terms, idf, manifest) {
|
|
1554
|
+
if (!await hasMatchedPassageEvidence(
|
|
1555
|
+
repositoryRoot,
|
|
1556
|
+
path,
|
|
1557
|
+
semantic.hits,
|
|
1558
|
+
terms,
|
|
1559
|
+
manifest.entries[path]?.symbolTable ?? []
|
|
1560
|
+
)) {
|
|
1561
|
+
return false;
|
|
1562
|
+
}
|
|
1563
|
+
const source = await readBoundedSource(repositoryRoot, path) ?? "";
|
|
1564
|
+
return idfCoverage(`${path} ${source}`, terms, idf) >= (semantic.minScore ?? DEFAULT_MIN_SCORE);
|
|
1565
|
+
}
|
|
1566
|
+
var DEFAULT_MIN_SCORE = 0.3;
|
|
1567
|
+
async function agreementConfig(repositoryRoot) {
|
|
1568
|
+
try {
|
|
1569
|
+
const configured = (await readConfigFile(repositoryRoot)).discovery?.agreement ?? {};
|
|
1570
|
+
return {
|
|
1571
|
+
topK: configured.topK ?? AGREEMENT.topK,
|
|
1572
|
+
chunkFloor: configured.chunkFloor ?? AGREEMENT.chunkFloor,
|
|
1573
|
+
cardFloor: configured.cardFloor ?? AGREEMENT.cardFloor
|
|
1574
|
+
};
|
|
1575
|
+
} catch {
|
|
1576
|
+
return AGREEMENT;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
async function raceDeadline(promise, deadlineAt) {
|
|
1580
|
+
if (deadlineAt === void 0) return promise;
|
|
1581
|
+
const ms = deadlineAt - Date.now();
|
|
1582
|
+
if (ms <= 0) return null;
|
|
1583
|
+
let timer;
|
|
1584
|
+
return Promise.race([
|
|
1585
|
+
promise,
|
|
1586
|
+
new Promise((resolve8) => {
|
|
1587
|
+
timer = setTimeout(() => resolve8(null), ms);
|
|
1588
|
+
})
|
|
1589
|
+
]).finally(() => clearTimeout(timer));
|
|
1590
|
+
}
|
|
790
1591
|
var MIN_RERANK_MS = 200;
|
|
791
1592
|
async function llmRerank(input) {
|
|
792
1593
|
let config;
|
|
793
1594
|
try {
|
|
794
|
-
config = await (await import("./config-
|
|
1595
|
+
config = await (await import("./config-TXQJCCVI.js")).resolveLlmConfig(input.repositoryRoot);
|
|
795
1596
|
} catch {
|
|
796
1597
|
return null;
|
|
797
1598
|
}
|
|
@@ -816,7 +1617,7 @@ async function llmRerank(input) {
|
|
|
816
1617
|
input.deadlineAt === void 0 ? config.timeoutMs : input.deadlineAt - Date.now()
|
|
817
1618
|
);
|
|
818
1619
|
if (deadlineMs < MIN_RERANK_MS) return skipped("no time left in the hook budget");
|
|
819
|
-
const { rerankCandidates } = await import("./rerank-
|
|
1620
|
+
const { rerankCandidates } = await import("./rerank-XXKWDI5Q.js");
|
|
820
1621
|
const result = await rerankCandidates(
|
|
821
1622
|
input.repositoryRoot,
|
|
822
1623
|
config,
|
|
@@ -888,7 +1689,7 @@ async function commandFiles(repositoryRoot, task, entries) {
|
|
|
888
1689
|
var COMMAND_DIR = /(^|\/)(cli|cmd|commands?)\//;
|
|
889
1690
|
async function packageBins(repositoryRoot) {
|
|
890
1691
|
try {
|
|
891
|
-
const manifest = JSON.parse(await
|
|
1692
|
+
const manifest = JSON.parse(await readFile9(join8(repositoryRoot, "package.json"), "utf8"));
|
|
892
1693
|
if (typeof manifest.bin === "string" && typeof manifest.name === "string") {
|
|
893
1694
|
return [manifest.name.slice(manifest.name.lastIndexOf("/") + 1)];
|
|
894
1695
|
}
|
|
@@ -1058,6 +1859,17 @@ function foldPlural(word) {
|
|
|
1058
1859
|
if (/(ss|sh|ch|x|z)es$/.test(word)) return word.slice(0, -2);
|
|
1059
1860
|
return word.slice(0, -1);
|
|
1060
1861
|
}
|
|
1862
|
+
var ISSUE_TEMPLATE = [
|
|
1863
|
+
/_No response_/gi,
|
|
1864
|
+
/\(Please specify[^)]*\)/gi,
|
|
1865
|
+
/-\s*\[[ xX]\]\s*[\w.+-]+/g,
|
|
1866
|
+
/\d+\.\s*(?:Go to|Click on|Scroll down to)\s*'\.+'|\bSee error\b/gi,
|
|
1867
|
+
/#{1,6}\s*[\w-]+ Version\b(?:\s*v?\d+(?:\.\d+)+[\w.-]*,?)*/gi,
|
|
1868
|
+
/#{1,6}\s*(?:Description|Summary|URL to code causing the issue|MCVE|Minimal reproducible example|Steps to reproduce|Expected behaviou?r|Actual behaviou?r|Screenshots|Logs|Environment|Platform|Additional context)\b/gi
|
|
1869
|
+
];
|
|
1870
|
+
function stripIssueTemplate(task) {
|
|
1871
|
+
return ISSUE_TEMPLATE.reduce((text, pattern) => text.replace(pattern, " "), task);
|
|
1872
|
+
}
|
|
1061
1873
|
function taskTerms(task) {
|
|
1062
1874
|
return [
|
|
1063
1875
|
...new Set(
|
|
@@ -1070,6 +1882,8 @@ function countTerms(text, terms) {
|
|
|
1070
1882
|
const words = new Set(splitWords(text));
|
|
1071
1883
|
return terms.filter((term) => words.has(term)).length;
|
|
1072
1884
|
}
|
|
1885
|
+
var ABBREVIATION = /^[a-z](?:\.[a-z])+$/i;
|
|
1886
|
+
var EXCEPTION_MODULE = /(^|\/)(exceptions?|errors?)(\/|\.[a-z]+$)/i;
|
|
1073
1887
|
var CODE_IDENTIFIER = /[a-z0-9][A-Z]|[_$.]/;
|
|
1074
1888
|
function identifierTokens(task) {
|
|
1075
1889
|
const tokens = task.match(/[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*/g) ?? [];
|
|
@@ -1083,24 +1897,24 @@ async function loadProjectArchitectureMap(repositoryRoot, manifestSource) {
|
|
|
1083
1897
|
const builtIn = (await loadArchitectureMap(repositoryRoot, manifest)).map;
|
|
1084
1898
|
if (config.graphify?.enabled && await graphifyGraphExists(repositoryRoot, config.graphify.graphPath)) {
|
|
1085
1899
|
try {
|
|
1086
|
-
const graphPath =
|
|
1900
|
+
const graphPath = join8(
|
|
1087
1901
|
repositoryRoot,
|
|
1088
1902
|
config.graphify.graphPath ?? "graphify-out/graph.json"
|
|
1089
1903
|
);
|
|
1090
|
-
const [graph, graphFile,
|
|
1904
|
+
const [graph, graphFile, sourceHashes2] = await Promise.all([
|
|
1091
1905
|
loadGraphifyMap(graphPath, repositoryRoot),
|
|
1092
|
-
|
|
1906
|
+
stat4(graphPath),
|
|
1093
1907
|
readGraphSourceSnapshot(graphPath)
|
|
1094
1908
|
]);
|
|
1095
1909
|
const livePaths = new Set(
|
|
1096
1910
|
(await Promise.all(
|
|
1097
1911
|
graph.modules.map(async (module) => {
|
|
1098
1912
|
try {
|
|
1099
|
-
const absolute = await realpath(
|
|
1100
|
-
const inside =
|
|
1913
|
+
const absolute = await realpath(join8(repositoryRoot, module.path));
|
|
1914
|
+
const inside = relative6(await realpath(repositoryRoot), absolute);
|
|
1101
1915
|
if (inside.startsWith("..") || isAbsolute3(inside)) return null;
|
|
1102
|
-
const source = await
|
|
1103
|
-
const fresh =
|
|
1916
|
+
const source = await stat4(absolute);
|
|
1917
|
+
const fresh = sourceHashes2 === null ? source.mtimeMs <= graphFile.mtimeMs : sourceHashes2[module.path] !== void 0 && sourceHashes2[module.path] === manifest.entries[module.path]?.hash;
|
|
1104
1918
|
return source.isFile() && fresh ? module.path : null;
|
|
1105
1919
|
} catch {
|
|
1106
1920
|
return null;
|
|
@@ -1165,7 +1979,9 @@ async function refreshFileVectors(repositoryRoot, manifest, deadlineAt, embed =
|
|
|
1165
1979
|
if (cached && cached.hash === manifest.entries[path]?.hash) files[path] = cached;
|
|
1166
1980
|
else todo.push(path);
|
|
1167
1981
|
}
|
|
1982
|
+
const cards = await currentCardTexts(repositoryRoot, manifest);
|
|
1168
1983
|
let embedded = 0;
|
|
1984
|
+
let embeddedCards = 0;
|
|
1169
1985
|
let lastSave = Date.now();
|
|
1170
1986
|
for (const path of todo) {
|
|
1171
1987
|
if (Date.now() >= deadlineAt) break;
|
|
@@ -1185,13 +2001,52 @@ async function refreshFileVectors(repositoryRoot, manifest, deadlineAt, embed =
|
|
|
1185
2001
|
lastSave = Date.now();
|
|
1186
2002
|
}
|
|
1187
2003
|
}
|
|
2004
|
+
const stale = Object.keys(files).filter((path) => {
|
|
2005
|
+
const text = cards.get(path);
|
|
2006
|
+
return text !== void 0 && files[path]?.cardHash !== textHash(text);
|
|
2007
|
+
});
|
|
2008
|
+
for (let start = 0; start < stale.length && Date.now() < deadlineAt; start += BATCH) {
|
|
2009
|
+
const batch = stale.slice(start, start + BATCH);
|
|
2010
|
+
const vectors = await embed(batch.map((path) => cards.get(path)));
|
|
2011
|
+
batch.forEach((path, index) => {
|
|
2012
|
+
const file = files[path];
|
|
2013
|
+
const vector = vectors[index];
|
|
2014
|
+
if (!file || !vector) return;
|
|
2015
|
+
files[path] = {
|
|
2016
|
+
...file,
|
|
2017
|
+
card: encode(vector),
|
|
2018
|
+
cardHash: textHash(cards.get(path))
|
|
2019
|
+
};
|
|
2020
|
+
embeddedCards += 1;
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
for (const [path, file] of Object.entries(files)) {
|
|
2024
|
+
if (file.card && !cards.has(path)) files[path] = { hash: file.hash, vectors: file.vectors };
|
|
2025
|
+
}
|
|
1188
2026
|
await saveStore(repositoryRoot, { version: 1, model: MODEL, files });
|
|
1189
2027
|
return {
|
|
1190
2028
|
embedded,
|
|
1191
2029
|
files: Object.keys(files).length,
|
|
1192
|
-
pending: eligible.length - Object.keys(files).length
|
|
2030
|
+
pending: eligible.length - Object.keys(files).length,
|
|
2031
|
+
...embeddedCards > 0 ? { cards: embeddedCards } : {}
|
|
1193
2032
|
};
|
|
1194
2033
|
}
|
|
2034
|
+
async function currentCardTexts(repositoryRoot, manifest) {
|
|
2035
|
+
try {
|
|
2036
|
+
const { cardText, loadLlmSummaries } = await import("./summaries-QXWFFDUZ.js");
|
|
2037
|
+
const texts = /* @__PURE__ */ new Map();
|
|
2038
|
+
for (const [path, card] of await loadLlmSummaries(repositoryRoot, manifest)) {
|
|
2039
|
+
if (card.symptoms) texts.set(path, `${path}
|
|
2040
|
+
${cardText(card)}`);
|
|
2041
|
+
}
|
|
2042
|
+
return texts;
|
|
2043
|
+
} catch {
|
|
2044
|
+
return /* @__PURE__ */ new Map();
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
function textHash(text) {
|
|
2048
|
+
return createHash3("sha256").update(text).digest("hex").slice(0, 16);
|
|
2049
|
+
}
|
|
1195
2050
|
async function syncFileVectors(repositoryRoot, budgetMs) {
|
|
1196
2051
|
const started = Date.now();
|
|
1197
2052
|
try {
|
|
@@ -1202,6 +2057,9 @@ async function syncFileVectors(repositoryRoot, budgetMs) {
|
|
|
1202
2057
|
}
|
|
1203
2058
|
}
|
|
1204
2059
|
async function rankFilesByVector(repositoryRoot, manifest, task, embed = localEmbed) {
|
|
2060
|
+
return (await rankFileVectors(repositoryRoot, manifest, task, embed))?.chunks ?? null;
|
|
2061
|
+
}
|
|
2062
|
+
async function rankFileVectors(repositoryRoot, manifest, task, embed = localEmbed) {
|
|
1205
2063
|
if (process.env.CODEBUDDY_SEMANTIC === "0") return null;
|
|
1206
2064
|
const store = await loadStore(repositoryRoot);
|
|
1207
2065
|
const current = eligiblePaths(manifest).filter(
|
|
@@ -1216,10 +2074,16 @@ async function rankFilesByVector(repositoryRoot, manifest, task, embed = localEm
|
|
|
1216
2074
|
} catch {
|
|
1217
2075
|
return null;
|
|
1218
2076
|
}
|
|
1219
|
-
|
|
2077
|
+
const order = (left, right) => right.score - left.score || left.path.localeCompare(right.path);
|
|
2078
|
+
const chunks = current.map((path) => ({
|
|
1220
2079
|
path,
|
|
1221
2080
|
score: Math.max(...(store.files[path]?.vectors ?? []).map((v) => dot(decode(v), query)))
|
|
1222
|
-
})).sort(
|
|
2081
|
+
})).sort(order);
|
|
2082
|
+
const cards = current.flatMap((path) => {
|
|
2083
|
+
const card = store.files[path]?.card;
|
|
2084
|
+
return card ? [{ path, score: dot(decode(card), query) }] : [];
|
|
2085
|
+
}).sort(order);
|
|
2086
|
+
return { chunks, cards: cards.length > 0 ? cards : null };
|
|
1223
2087
|
}
|
|
1224
2088
|
function eligiblePaths(manifest) {
|
|
1225
2089
|
return Object.keys(manifest.entries).filter(
|
|
@@ -1251,11 +2115,11 @@ function dot(left, right) {
|
|
|
1251
2115
|
return sum;
|
|
1252
2116
|
}
|
|
1253
2117
|
function storePath(repositoryRoot) {
|
|
1254
|
-
return
|
|
2118
|
+
return join9(repositoryRoot, ".codebuddy", "cache", "file-vectors.json");
|
|
1255
2119
|
}
|
|
1256
2120
|
async function loadStore(repositoryRoot) {
|
|
1257
2121
|
try {
|
|
1258
|
-
const parsed = JSON.parse(await
|
|
2122
|
+
const parsed = JSON.parse(await readFile10(storePath(repositoryRoot), "utf8"));
|
|
1259
2123
|
if (parsed.version === 1 && parsed.model === MODEL && parsed.files) return parsed;
|
|
1260
2124
|
} catch {
|
|
1261
2125
|
}
|
|
@@ -1263,19 +2127,30 @@ async function loadStore(repositoryRoot) {
|
|
|
1263
2127
|
}
|
|
1264
2128
|
async function saveStore(repositoryRoot, store) {
|
|
1265
2129
|
const path = storePath(repositoryRoot);
|
|
1266
|
-
await
|
|
2130
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
1267
2131
|
await writeAtomic(path, JSON.stringify(store));
|
|
1268
2132
|
}
|
|
1269
2133
|
|
|
1270
2134
|
export {
|
|
2135
|
+
buildArchitectureMap,
|
|
2136
|
+
queryMap,
|
|
2137
|
+
neighbours,
|
|
2138
|
+
indexRepository,
|
|
2139
|
+
loadFreshIndex,
|
|
2140
|
+
hashContent,
|
|
1271
2141
|
setupGraphify,
|
|
1272
2142
|
generateGraphifyGraph,
|
|
1273
2143
|
loadGraphifyMap,
|
|
2144
|
+
refreshFileVectors,
|
|
1274
2145
|
syncFileVectors,
|
|
2146
|
+
rankFilesByVector,
|
|
2147
|
+
rankFileVectors,
|
|
1275
2148
|
TASK_PATH_LIMIT,
|
|
1276
2149
|
readBoundedSource,
|
|
1277
2150
|
discoverTaskPaths,
|
|
2151
|
+
EXPANSION_MS,
|
|
1278
2152
|
TEST_PATH,
|
|
2153
|
+
CHANGELOG_PATH,
|
|
1279
2154
|
asksForChangelog,
|
|
1280
2155
|
NON_SOURCE_PATH,
|
|
1281
2156
|
taskTerms,
|
|
@@ -1284,4 +2159,4 @@ export {
|
|
|
1284
2159
|
namedSegments,
|
|
1285
2160
|
loadProjectArchitectureMap
|
|
1286
2161
|
};
|
|
1287
|
-
//# sourceMappingURL=chunk-
|
|
2162
|
+
//# sourceMappingURL=chunk-UR3DF74I.js.map
|