@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.
Files changed (117) hide show
  1. package/ARCHITECTURE.md +98 -0
  2. package/CLAUDE_CODEX.md +33 -0
  3. package/README.md +114 -9
  4. package/dist/{adapters-S4HHTWJW.js → adapters-OJRSVFV3.js} +2 -2
  5. package/dist/{chunk-GXOML4UZ.js → chunk-4BSO3HST.js} +126 -18
  6. package/dist/chunk-4BSO3HST.js.map +1 -0
  7. package/dist/{chunk-CAEPHLHC.js → chunk-57642NGU.js} +2 -44
  8. package/dist/chunk-57642NGU.js.map +1 -0
  9. package/dist/{chunk-ESZXCZIN.js → chunk-6YI2D5F5.js} +33 -5
  10. package/dist/chunk-6YI2D5F5.js.map +1 -0
  11. package/dist/{chunk-GKNYMNFM.js → chunk-7IPSDDO5.js} +10 -3
  12. package/dist/chunk-7IPSDDO5.js.map +1 -0
  13. package/dist/chunk-BTTQWSDS.js +372 -0
  14. package/dist/chunk-BTTQWSDS.js.map +1 -0
  15. package/dist/{chunk-2WH2Q4TA.js → chunk-D5E7SGI5.js} +304 -77
  16. package/dist/chunk-D5E7SGI5.js.map +1 -0
  17. package/dist/chunk-EMWGLKPR.js +45 -0
  18. package/dist/chunk-EMWGLKPR.js.map +1 -0
  19. package/dist/chunk-GQKWUHMA.js +20 -0
  20. package/dist/chunk-GQKWUHMA.js.map +1 -0
  21. package/dist/chunk-ITXE27SZ.js +358 -0
  22. package/dist/chunk-ITXE27SZ.js.map +1 -0
  23. package/dist/{chunk-UK6XTS2Y.js → chunk-JJQDVGF7.js} +23 -23
  24. package/dist/chunk-JJQDVGF7.js.map +1 -0
  25. package/dist/{chunk-F4BJ3DXW.js → chunk-KQCP3ZYH.js} +1273 -331
  26. package/dist/chunk-KQCP3ZYH.js.map +1 -0
  27. package/dist/{chunk-QN76UJP7.js → chunk-KYRM3HPP.js} +89 -21
  28. package/dist/chunk-KYRM3HPP.js.map +1 -0
  29. package/dist/{chunk-KNR2P2UY.js → chunk-M75SN4D4.js} +19 -4
  30. package/dist/chunk-M75SN4D4.js.map +1 -0
  31. package/dist/{chunk-J4T6HHI4.js → chunk-RZ266NHE.js} +538 -317
  32. package/dist/chunk-RZ266NHE.js.map +1 -0
  33. package/dist/chunk-U7KDM62P.js +43 -0
  34. package/dist/chunk-U7KDM62P.js.map +1 -0
  35. package/dist/chunk-UATJCD2T.js +368 -0
  36. package/dist/chunk-UATJCD2T.js.map +1 -0
  37. package/dist/{chunk-ENKDFLOO.js → chunk-UOTKHSQ5.js} +25 -8
  38. package/dist/chunk-UOTKHSQ5.js.map +1 -0
  39. package/dist/{chunk-TLWGZVPC.js → chunk-UR3DF74I.js} +1007 -132
  40. package/dist/chunk-UR3DF74I.js.map +1 -0
  41. package/dist/chunk-UTPC66MV.js +128 -0
  42. package/dist/chunk-UTPC66MV.js.map +1 -0
  43. package/dist/{chunk-W7GAXL6C.js → chunk-V7PT6ER5.js} +6 -5
  44. package/dist/chunk-V7PT6ER5.js.map +1 -0
  45. package/dist/chunk-WHKXUXHE.js +156 -0
  46. package/dist/chunk-WHKXUXHE.js.map +1 -0
  47. package/dist/{chunk-SRA4MDVG.js → chunk-YMXFSHXT.js} +978 -950
  48. package/dist/chunk-YMXFSHXT.js.map +1 -0
  49. package/dist/cli/index.js +23 -9
  50. package/dist/cli/index.js.map +1 -1
  51. package/dist/{codebuddy-DO7ZhoWn.d.ts → codebuddy-CHqMptZB.d.ts} +35 -0
  52. package/dist/config-TXQJCCVI.js +15 -0
  53. package/dist/expand-KWYGNCKZ.js +98 -0
  54. package/dist/expand-KWYGNCKZ.js.map +1 -0
  55. package/dist/file-vectors-MKXTSGGH.js +19 -0
  56. package/dist/index-facts-4MTEHKMN.js +38 -0
  57. package/dist/index-pass-TBDUEZCB.js +14 -0
  58. package/dist/index.d.ts +6 -4
  59. package/dist/index.js +25 -23
  60. package/dist/install-E3JWINTN.js +31 -0
  61. package/dist/langgraph/index.d.ts +1 -1
  62. package/dist/lean-MLTAFS5L.js +29 -0
  63. package/dist/local-75DJECVR.js +48 -0
  64. package/dist/local-llm-MECVJTVU.js +19 -0
  65. package/dist/{main-YJKSYFPT.js → main-RLMKQZW4.js} +999 -328
  66. package/dist/main-RLMKQZW4.js.map +1 -0
  67. package/dist/{memory-TGHVTBME.js → memory-PFFVMTCH.js} +9 -6
  68. package/dist/{memory-TGHVTBME.js.map → memory-PFFVMTCH.js.map} +1 -1
  69. package/dist/{operations-OG4ISWIM.js → operations-STR7S5UO.js} +8 -7
  70. package/dist/operations-STR7S5UO.js.map +1 -0
  71. package/dist/{pretool-command-PRFOIHMW.js → pretool-command-B5GY5T6E.js} +2 -2
  72. package/dist/pretool-command-B5GY5T6E.js.map +1 -0
  73. package/dist/prompt-gate-TDTFFQW2.js +8 -0
  74. package/dist/prompt-gate-TDTFFQW2.js.map +1 -0
  75. package/dist/{rerank-IQAEWLQR.js → rerank-XXKWDI5Q.js} +7 -4
  76. package/dist/{rerank-IQAEWLQR.js.map → rerank-XXKWDI5Q.js.map} +1 -1
  77. package/dist/{runtime-GCSEIAVD.js → runtime-SWFWGTQ4.js} +18 -9
  78. package/dist/runtime-SWFWGTQ4.js.map +1 -0
  79. package/dist/summaries-QXWFFDUZ.js +31 -0
  80. package/dist/summaries-QXWFFDUZ.js.map +1 -0
  81. package/dist/{zvec-XDZNZ2OL.js → zvec-F73IBWO5.js} +13 -6
  82. package/dist/zvec-F73IBWO5.js.map +1 -0
  83. package/package.json +13 -3
  84. package/dist/chunk-2WH2Q4TA.js.map +0 -1
  85. package/dist/chunk-6NZEZJYD.js +0 -206
  86. package/dist/chunk-6NZEZJYD.js.map +0 -1
  87. package/dist/chunk-CAEPHLHC.js.map +0 -1
  88. package/dist/chunk-ENKDFLOO.js.map +0 -1
  89. package/dist/chunk-ESZXCZIN.js.map +0 -1
  90. package/dist/chunk-F4BJ3DXW.js.map +0 -1
  91. package/dist/chunk-GKNYMNFM.js.map +0 -1
  92. package/dist/chunk-GXOML4UZ.js.map +0 -1
  93. package/dist/chunk-ITQO632W.js +0 -673
  94. package/dist/chunk-ITQO632W.js.map +0 -1
  95. package/dist/chunk-J4T6HHI4.js.map +0 -1
  96. package/dist/chunk-KNR2P2UY.js.map +0 -1
  97. package/dist/chunk-QN76UJP7.js.map +0 -1
  98. package/dist/chunk-SFMIMLXR.js +0 -63
  99. package/dist/chunk-SFMIMLXR.js.map +0 -1
  100. package/dist/chunk-SRA4MDVG.js.map +0 -1
  101. package/dist/chunk-TLWGZVPC.js.map +0 -1
  102. package/dist/chunk-UK6XTS2Y.js.map +0 -1
  103. package/dist/chunk-W7GAXL6C.js.map +0 -1
  104. package/dist/config-GEZFLXK6.js +0 -12
  105. package/dist/install-BXM56QCD.js +0 -19
  106. package/dist/lean-command-7DFW32MA.js +0 -19
  107. package/dist/main-YJKSYFPT.js.map +0 -1
  108. package/dist/summaries-ML53R7EL.js +0 -21
  109. /package/dist/{adapters-S4HHTWJW.js.map → adapters-OJRSVFV3.js.map} +0 -0
  110. /package/dist/{config-GEZFLXK6.js.map → config-TXQJCCVI.js.map} +0 -0
  111. /package/dist/{install-BXM56QCD.js.map → file-vectors-MKXTSGGH.js.map} +0 -0
  112. /package/dist/{lean-command-7DFW32MA.js.map → index-facts-4MTEHKMN.js.map} +0 -0
  113. /package/dist/{operations-OG4ISWIM.js.map → index-pass-TBDUEZCB.js.map} +0 -0
  114. /package/dist/{pretool-command-PRFOIHMW.js.map → install-E3JWINTN.js.map} +0 -0
  115. /package/dist/{runtime-GCSEIAVD.js.map → lean-MLTAFS5L.js.map} +0 -0
  116. /package/dist/{summaries-ML53R7EL.js.map → local-75DJECVR.js.map} +0 -0
  117. /package/dist/{zvec-XDZNZ2OL.js.map → local-llm-MECVJTVU.js.map} +0 -0
@@ -0,0 +1,358 @@
1
+ import {
2
+ chatJson
3
+ } from "./chunk-6YI2D5F5.js";
4
+ import {
5
+ CARDS_DIR,
6
+ refreshZvecCardIndex
7
+ } from "./chunk-4BSO3HST.js";
8
+ import {
9
+ CHANGELOG_PATH,
10
+ NON_SOURCE_PATH,
11
+ TEST_PATH,
12
+ loadFreshIndex,
13
+ loadProjectArchitectureMap,
14
+ readBoundedSource
15
+ } from "./chunk-UR3DF74I.js";
16
+ import {
17
+ redactSensitive,
18
+ scanSensitive
19
+ } from "./chunk-AL6VBTNA.js";
20
+ import {
21
+ resolveLlmConfig
22
+ } from "./chunk-WHKXUXHE.js";
23
+ import {
24
+ writeAtomic
25
+ } from "./chunk-CG5B3LYG.js";
26
+ import {
27
+ waitForForeground
28
+ } from "./chunk-BTTQWSDS.js";
29
+
30
+ // src/llm/summaries.ts
31
+ import { mkdir, readdir, readFile, rm } from "fs/promises";
32
+ import { dirname, join } from "path";
33
+ import { z } from "zod";
34
+ var SUMMARY_PROMPT_VERSION = 2;
35
+ var LIMITS = {
36
+ fileChars: 200,
37
+ symbolChars: 100,
38
+ symptomChars: 80,
39
+ termChars: 40,
40
+ symptoms: 6,
41
+ terms: 10,
42
+ symbols: 8,
43
+ signatures: 12,
44
+ neighbours: 5,
45
+ /** ~600 tokens per file: long-line (minified) files are cut, never sent whole. */
46
+ headChars: 1600,
47
+ signatureChars: 160,
48
+ batchTokens: 5e3,
49
+ batchFiles: 8,
50
+ outputTokensPerFile: 320,
51
+ /** The local grammar bounds every field, so one card always fits. */
52
+ localOutputTokens: 700,
53
+ tokensPerMinute: 6e3,
54
+ timeoutMs: 2e4,
55
+ /** One file per call for the local model: a 0.6B model loses track of batches. */
56
+ localTimeoutMs: 3e4
57
+ };
58
+ function cardEligible(path) {
59
+ return !TEST_PATH.test(path) && !NON_SOURCE_PATH.test(path) && !CHANGELOG_PATH.test(path) && !GENERATED_PATH.test(path);
60
+ }
61
+ var GENERATED_PATH = /(^|\/)(vendor|vendored|third[_-]?party|node_modules|dist|build|generated|__generated__)\/|\.(min|bundle|generated|pb)\.[a-z]+$|_pb2\.py$|(^|\/)[^/]*lock[^/]*$/i;
62
+ function cardText(card) {
63
+ return [
64
+ card.summary,
65
+ ...card.symptoms ?? [],
66
+ ...Object.entries(card.symbols).map(([name, text]) => `${name}: ${text}`)
67
+ ].filter(Boolean).join("\n");
68
+ }
69
+ function cardWords(card) {
70
+ return [...card.terms ?? [], ...card.symptoms ?? []].join(" ");
71
+ }
72
+ var storePath = (root) => join(root, ".codebuddy", "cache", "llm-summaries.json");
73
+ async function readStore(root) {
74
+ try {
75
+ return JSON.parse(await readFile(storePath(root), "utf8"));
76
+ } catch {
77
+ return {};
78
+ }
79
+ }
80
+ async function writeStore(root, store) {
81
+ await mkdir(join(root, ".codebuddy", "cache"), { recursive: true });
82
+ await writeAtomic(storePath(root), `${JSON.stringify(store)}
83
+ `);
84
+ }
85
+ async function loadLlmSummaries(root, manifest) {
86
+ const store = await readStore(root);
87
+ return new Map(
88
+ Object.entries(store).filter(([path, entry]) => manifest.entries[path]?.hash === entry.hash)
89
+ );
90
+ }
91
+ var outputSchema = z.object({
92
+ files: z.array(
93
+ z.object({
94
+ path: z.string(),
95
+ summary: z.string(),
96
+ symptoms: z.array(z.string()).default([]),
97
+ terms: z.array(z.string()).default([]),
98
+ symbols: z.array(z.object({ name: z.string(), summary: z.string() })).default([])
99
+ })
100
+ )
101
+ });
102
+ var boundedString = (maxLength) => ({ type: "string", maxLength });
103
+ var CARD_GRAMMAR = {
104
+ type: "object",
105
+ properties: {
106
+ files: {
107
+ type: "array",
108
+ minItems: 1,
109
+ maxItems: 1,
110
+ items: {
111
+ type: "object",
112
+ properties: {
113
+ path: boundedString(300),
114
+ summary: boundedString(LIMITS.fileChars),
115
+ symptoms: {
116
+ type: "array",
117
+ items: boundedString(LIMITS.symptomChars),
118
+ maxItems: LIMITS.symptoms
119
+ },
120
+ terms: { type: "array", items: boundedString(LIMITS.termChars), maxItems: LIMITS.terms },
121
+ symbols: {
122
+ type: "array",
123
+ maxItems: LIMITS.symbols,
124
+ items: {
125
+ type: "object",
126
+ properties: { name: boundedString(80), summary: boundedString(LIMITS.symbolChars) }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }
133
+ };
134
+ var SYSTEM = [
135
+ "You write search cards for source files so that a plain-English bug report or task finds the right file.",
136
+ 'Answer with JSON only: {"files":[{"path":"...","summary":"...","symptoms":["..."],"terms":["..."],"symbols":[{"name":"...","summary":"..."}]}]}.',
137
+ "For each input file:",
138
+ `summary: one plain-English sentence (max ${LIMITS.fileChars} chars) saying what the file does;`,
139
+ `symptoms: 3 to ${LIMITS.symptoms} short phrases a user or bug report would use to describe a problem caused by this file, in everyday words without code identifiers;`,
140
+ `terms: 5 to ${LIMITS.terms} code identifiers and domain words from the file that a search should match;`,
141
+ `symbols: up to ${LIMITS.symbols} main symbols (only names listed in "signatures"), each with one short phrase.`,
142
+ "Symptoms describe what goes wrong for a user of this specific file (a wrong value, an error, nothing happening), never generic phrases.",
143
+ "Never invent paths or symbol names."
144
+ ].join(" ");
145
+ async function summarizeRepository(root, options = {}) {
146
+ const resolved = options.config ?? await resolveLlmConfig(root);
147
+ if (!resolved.enabled || !resolved.features.summaries) {
148
+ return { summarized: 0, skipped: 0, remaining: 0, stoppedBy: "disabled" };
149
+ }
150
+ const config = resolved;
151
+ const { manifest } = await loadFreshIndex(root);
152
+ const store = await readStore(root);
153
+ for (const path of Object.keys(store)) if (!manifest.entries[path]) delete store[path];
154
+ const map = await loadProjectArchitectureMap(root, manifest).catch(() => null);
155
+ const pending = orderByImportance(
156
+ map,
157
+ Object.values(manifest.entries).filter((entry) => {
158
+ if (!cardEligible(entry.path)) return false;
159
+ const done = store[entry.path];
160
+ return !(done?.hash === entry.hash && done.model === config.model && done.version === SUMMARY_PROMPT_VERSION);
161
+ })
162
+ );
163
+ const local = config.provider === "local";
164
+ const batchFiles = local ? 1 : options.batchFiles ?? LIMITS.batchFiles;
165
+ const limit = options.limit ?? Number.POSITIVE_INFINITY;
166
+ let summarized = 0;
167
+ let skipped = 0;
168
+ let index = 0;
169
+ let stoppedBy = "done";
170
+ while (index < pending.length) {
171
+ if (options.signal?.aborted) {
172
+ stoppedBy = "aborted";
173
+ break;
174
+ }
175
+ if (summarized >= limit) {
176
+ stoppedBy = "limit";
177
+ break;
178
+ }
179
+ const batch = [];
180
+ let tokens = 0;
181
+ while (index < pending.length && batch.length < batchFiles) {
182
+ if (summarized + batch.length >= limit) break;
183
+ const entry = pending[index];
184
+ const input = await fileInput(root, entry, map);
185
+ if (input === null) {
186
+ skipped++;
187
+ index++;
188
+ continue;
189
+ }
190
+ const cost = Math.ceil(JSON.stringify(input).length / 4);
191
+ if (batch.length > 0 && tokens + cost > LIMITS.batchTokens - 600) break;
192
+ batch.push({ entry, input });
193
+ tokens += cost;
194
+ index++;
195
+ }
196
+ if (batch.length === 0) continue;
197
+ if (local) await waitForForeground(root, options.signal);
198
+ const started = Date.now();
199
+ const ask = () => chatJson(config, {
200
+ system: SYSTEM,
201
+ user: JSON.stringify({ files: batch.map(({ input }) => input) }),
202
+ schema: outputSchema,
203
+ jsonSchema: CARD_GRAMMAR,
204
+ maxTokens: local ? LIMITS.localOutputTokens : LIMITS.outputTokensPerFile * batch.length,
205
+ deadlineMs: local ? LIMITS.localTimeoutMs : LIMITS.timeoutMs,
206
+ purpose: "background",
207
+ repositoryRoot: root
208
+ });
209
+ let result = await ask();
210
+ if (!result.ok && result.reason === "invalid") result = await ask();
211
+ if (!result.ok) {
212
+ if (result.reason === "invalid" || result.reason === "http" && batch.length === 1 || local && result.reason === "timeout") {
213
+ skipped += batch.length;
214
+ continue;
215
+ }
216
+ index -= batch.length;
217
+ stoppedBy = result.reason === "budget" ? "budget" : result.reason === "cooldown" || result.reason === "rate-limited" ? "cooldown" : "error";
218
+ break;
219
+ }
220
+ for (const { entry } of batch) {
221
+ const answer = result.value.files.find((file) => file.path === entry.path) ?? (batch.length === 1 && result.value.files.length === 1 ? result.value.files[0] : void 0);
222
+ if (!answer?.summary.trim()) {
223
+ skipped++;
224
+ continue;
225
+ }
226
+ store[entry.path] = toCard(entry, config.model, answer);
227
+ summarized++;
228
+ }
229
+ await writeStore(root, store);
230
+ await writeCardFiles(
231
+ root,
232
+ store,
233
+ batch.map(({ entry }) => entry.path)
234
+ );
235
+ options.onProgress?.(index, pending.length);
236
+ if (options.pace !== false) {
237
+ const wait = result.tokens / LIMITS.tokensPerMinute * 6e4 - (Date.now() - started);
238
+ if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
239
+ }
240
+ }
241
+ await writeStore(root, store);
242
+ await pruneCardFiles(root, store, manifest);
243
+ if (summarized > 0)
244
+ await refreshZvecCardIndex(root, options.signal ? { signal: options.signal } : {});
245
+ return { summarized, skipped, remaining: pending.length - index, stoppedBy };
246
+ }
247
+ function toCard(entry, model, answer) {
248
+ const known = new Set((entry.symbolTable ?? []).map((symbol) => symbol.name));
249
+ const clean = (items, count, chars) => [
250
+ ...new Set(items.map((item) => redactSensitive(item.trim()).slice(0, chars)).filter(Boolean))
251
+ ].slice(0, count);
252
+ return {
253
+ hash: entry.hash,
254
+ model,
255
+ version: SUMMARY_PROMPT_VERSION,
256
+ summary: redactSensitive(answer.summary.trim()).slice(0, LIMITS.fileChars),
257
+ symptoms: clean(answer.symptoms, LIMITS.symptoms, LIMITS.symptomChars),
258
+ terms: clean(answer.terms, LIMITS.terms, LIMITS.termChars),
259
+ symbols: Object.fromEntries(
260
+ answer.symbols.filter((symbol) => known.has(symbol.name)).slice(0, LIMITS.symbols).map((symbol) => [symbol.name, symbol.summary.trim().slice(0, LIMITS.symbolChars)])
261
+ )
262
+ };
263
+ }
264
+ async function writeCardFiles(root, store, paths) {
265
+ for (const path of paths) {
266
+ const card = store[path];
267
+ if (!card) continue;
268
+ const target = join(root, CARDS_DIR, `${path}.md`);
269
+ await mkdir(dirname(target), { recursive: true });
270
+ await writeAtomic(
271
+ target,
272
+ [
273
+ "---",
274
+ `source: ${JSON.stringify(path)}`,
275
+ `hash: ${card.hash}`,
276
+ "---",
277
+ "",
278
+ `# ${path}`,
279
+ "",
280
+ card.summary,
281
+ "",
282
+ ...card.symptoms?.length ? ["Symptoms:", ...card.symptoms.map((s) => `- ${s}`), ""] : [],
283
+ ...card.terms?.length ? [`Terms: ${card.terms.join(", ")}`, ""] : [],
284
+ ...Object.entries(card.symbols).map(([name, text]) => `- ${name}: ${text}`),
285
+ ""
286
+ ].join("\n")
287
+ );
288
+ }
289
+ }
290
+ async function pruneCardFiles(root, store, manifest) {
291
+ const directory = join(root, CARDS_DIR);
292
+ const current = new Set(
293
+ Object.entries(store).filter(([path, card]) => manifest.entries[path]?.hash === card.hash && card.symptoms).map(([path]) => `${path}.md`)
294
+ );
295
+ const existing = await readdir(directory, { recursive: true }).catch(() => []);
296
+ const present = new Set(existing.map((name) => name.split("\\").join("/")));
297
+ for (const name of present) {
298
+ if (name.endsWith(".md") && !name.startsWith(".zvec-grep") && !current.has(name)) {
299
+ await rm(join(directory, name), { force: true });
300
+ }
301
+ }
302
+ await writeCardFiles(
303
+ root,
304
+ store,
305
+ [...current].filter((name) => !present.has(name)).map((name) => name.slice(0, -3))
306
+ );
307
+ }
308
+ async function cardProgress(root) {
309
+ const { manifest } = await loadFreshIndex(root);
310
+ const store = await readStore(root);
311
+ const eligible = Object.keys(manifest.entries).filter(cardEligible);
312
+ return {
313
+ carded: eligible.filter(
314
+ (path) => store[path]?.hash === manifest.entries[path]?.hash && store[path]?.version === SUMMARY_PROMPT_VERSION
315
+ ).length,
316
+ eligible: eligible.length
317
+ };
318
+ }
319
+ var IMPORT_LINE = /^\s*(import\b|from\s+\S+\s+import\b|export\s+(\*|\{[^}]*\})\s+from\b|(const|let|var)\s+\S+\s*=\s*require\()|^\s*$/;
320
+ async function fileInput(root, entry, map) {
321
+ const source = await readBoundedSource(root, entry.path);
322
+ if (source === null || scanSensitive(source).sensitive) return null;
323
+ const edges = map?.edges ?? [];
324
+ return {
325
+ path: entry.path,
326
+ language: entry.language,
327
+ // Not the deterministic summary: small models copy its "ts • 80 lines" form.
328
+ signatures: (entry.symbolTable ?? []).filter((symbol) => symbol.kind !== "heading").slice(0, LIMITS.signatures).map((symbol) => redactSensitive(symbol.signature.slice(0, LIMITS.signatureChars))),
329
+ neighbours: [
330
+ .../* @__PURE__ */ new Set([
331
+ ...edges.filter((edge) => edge.from === entry.path).map((edge) => edge.to),
332
+ ...edges.filter((edge) => edge.to === entry.path).map((edge) => edge.from)
333
+ ])
334
+ ].slice(0, LIMITS.neighbours),
335
+ // The module docstring and first definitions, never the whole file.
336
+ head: redactSensitive(
337
+ source.split(/\r?\n/).filter((line) => !IMPORT_LINE.test(line)).join("\n").slice(0, LIMITS.headChars)
338
+ )
339
+ };
340
+ }
341
+ function orderByImportance(map, entries) {
342
+ const fanIn = /* @__PURE__ */ new Map();
343
+ for (const edge of map?.edges ?? []) fanIn.set(edge.to, (fanIn.get(edge.to) ?? 0) + 1);
344
+ return [...entries].sort(
345
+ (left, right) => (fanIn.get(right.path) ?? 0) - (fanIn.get(left.path) ?? 0) || left.path.localeCompare(right.path)
346
+ );
347
+ }
348
+
349
+ export {
350
+ SUMMARY_PROMPT_VERSION,
351
+ cardEligible,
352
+ cardText,
353
+ cardWords,
354
+ loadLlmSummaries,
355
+ summarizeRepository,
356
+ cardProgress
357
+ };
358
+ //# sourceMappingURL=chunk-ITXE27SZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/llm/summaries.ts"],"sourcesContent":["import { mkdir, readdir, readFile, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { z } from \"zod\";\nimport {\n CHANGELOG_PATH,\n loadProjectArchitectureMap,\n NON_SOURCE_PATH,\n readBoundedSource,\n TEST_PATH,\n} from \"../context/discovery.js\";\nimport { writeAtomic } from \"../core/markdown-store-fs.js\";\nimport { redactSensitive, scanSensitive } from \"../core/sensitive.js\";\nimport { loadFreshIndex } from \"../indexer/engine.js\";\nimport type { IndexEntry, IndexManifest } from \"../indexer/types.js\";\nimport { CARDS_DIR, refreshZvecCardIndex } from \"../retrieval/zvec.js\";\nimport { chatJson } from \"./client.js\";\nimport { type EnabledLlmConfig, resolveLlmConfig } from \"./config.js\";\nimport { waitForForeground } from \"./local.js\";\n\n/**\n * Intent cards: for each source file, a plain-English summary, the symptoms a\n * bug report would use, the code terms a search should hit, and one-line\n * descriptions of its main symbols. Written off the prompt path by the local\n * model (`provider: \"local\"`) or the BYOK model (`codebuddy index --cards`,\n * `codebuddy serve`, `watch`, `codebuddy llm summarize`) and cached by content\n * hash. Retrieval uses them three ways: a card vector per file\n * (`file-vectors.ts`), a card workspace in zvec-grep (`CARDS_DIR`), and\n * down-weighted card words in the content index. A card alone never admits a\n * file: card evidence counts when another dense signal agrees (see the\n * agreement rule in `context/discovery.ts`).\n */\nexport const SUMMARY_PROMPT_VERSION = 2;\nconst LIMITS = {\n fileChars: 200,\n symbolChars: 100,\n symptomChars: 80,\n termChars: 40,\n symptoms: 6,\n terms: 10,\n symbols: 8,\n signatures: 12,\n neighbours: 5,\n /** ~600 tokens per file: long-line (minified) files are cut, never sent whole. */\n headChars: 1_600,\n signatureChars: 160,\n batchTokens: 5_000,\n batchFiles: 8,\n outputTokensPerFile: 320,\n /** The local grammar bounds every field, so one card always fits. */\n localOutputTokens: 700,\n tokensPerMinute: 6_000,\n timeoutMs: 20_000,\n /** One file per call for the local model: a 0.6B model loses track of batches. */\n localTimeoutMs: 30_000,\n} as const;\n\nexport type LlmSummary = {\n hash: string;\n model: string;\n version: number;\n summary: string;\n symbols: Record<string, string>;\n /** Card fields (prompt version ≥ 2). */\n symptoms?: string[];\n terms?: string[];\n};\n\n/**\n * Source files that get a card: no tests, docs, config, changelogs, vendored,\n * generated, minified or lock files (secret-bearing ones are skipped when read).\n */\nexport function cardEligible(path: string): boolean {\n return (\n !TEST_PATH.test(path) &&\n !NON_SOURCE_PATH.test(path) &&\n !CHANGELOG_PATH.test(path) &&\n !GENERATED_PATH.test(path)\n );\n}\nconst GENERATED_PATH =\n /(^|\\/)(vendor|vendored|third[_-]?party|node_modules|dist|build|generated|__generated__)\\/|\\.(min|bundle|generated|pb)\\.[a-z]+$|_pb2\\.py$|(^|\\/)[^/]*lock[^/]*$/i;\n\n/** The text embedded as a file's card vector (empty for a summary without card fields). */\nexport function cardText(card: LlmSummary): string {\n return [\n card.summary,\n ...(card.symptoms ?? []),\n ...Object.entries(card.symbols).map(([name, text]) => `${name}: ${text}`),\n ]\n .filter(Boolean)\n .join(\"\\n\");\n}\n\n/** Words the content index adds (down-weighted) for a card: terms and symptoms. */\nexport function cardWords(card: LlmSummary): string {\n return [...(card.terms ?? []), ...(card.symptoms ?? [])].join(\" \");\n}\n\ntype Store = Record<string, LlmSummary>;\nconst storePath = (root: string) => join(root, \".codebuddy\", \"cache\", \"llm-summaries.json\");\n\nasync function readStore(root: string): Promise<Store> {\n try {\n return JSON.parse(await readFile(storePath(root), \"utf8\")) as Store;\n } catch {\n return {};\n }\n}\n\nasync function writeStore(root: string, store: Store): Promise<void> {\n await mkdir(join(root, \".codebuddy\", \"cache\"), { recursive: true });\n await writeAtomic(storePath(root), `${JSON.stringify(store)}\\n`);\n}\n\n/** Summaries whose content hash still matches the current index. */\nexport async function loadLlmSummaries(\n root: string,\n manifest: IndexManifest,\n): Promise<Map<string, LlmSummary>> {\n const store = await readStore(root);\n return new Map(\n Object.entries(store).filter(([path, entry]) => manifest.entries[path]?.hash === entry.hash),\n );\n}\n\nconst outputSchema = z.object({\n files: z.array(\n z.object({\n path: z.string(),\n summary: z.string(),\n symptoms: z.array(z.string()).default([]),\n terms: z.array(z.string()).default([]),\n symbols: z.array(z.object({ name: z.string(), summary: z.string() })).default([]),\n }),\n ),\n});\n\n/**\n * Grammar bounds for the local model: without them a 0.6B model writes\n * paragraph summaries and endless arrays until it hits the token cap and the\n * JSON is cut off. Zod still validates (and trims) every answer.\n */\nconst boundedString = (maxLength: number) => ({ type: \"string\", maxLength });\nconst CARD_GRAMMAR = {\n type: \"object\",\n properties: {\n files: {\n type: \"array\",\n minItems: 1,\n maxItems: 1,\n items: {\n type: \"object\",\n properties: {\n path: boundedString(300),\n summary: boundedString(LIMITS.fileChars),\n symptoms: {\n type: \"array\",\n items: boundedString(LIMITS.symptomChars),\n maxItems: LIMITS.symptoms,\n },\n terms: { type: \"array\", items: boundedString(LIMITS.termChars), maxItems: LIMITS.terms },\n symbols: {\n type: \"array\",\n maxItems: LIMITS.symbols,\n items: {\n type: \"object\",\n properties: { name: boundedString(80), summary: boundedString(LIMITS.symbolChars) },\n },\n },\n },\n },\n },\n },\n};\n\nconst SYSTEM = [\n \"You write search cards for source files so that a plain-English bug report or task finds the right file.\",\n 'Answer with JSON only: {\"files\":[{\"path\":\"...\",\"summary\":\"...\",\"symptoms\":[\"...\"],\"terms\":[\"...\"],\"symbols\":[{\"name\":\"...\",\"summary\":\"...\"}]}]}.',\n \"For each input file:\",\n `summary: one plain-English sentence (max ${LIMITS.fileChars} chars) saying what the file does;`,\n `symptoms: 3 to ${LIMITS.symptoms} short phrases a user or bug report would use to describe a problem caused by this file, in everyday words without code identifiers;`,\n `terms: 5 to ${LIMITS.terms} code identifiers and domain words from the file that a search should match;`,\n `symbols: up to ${LIMITS.symbols} main symbols (only names listed in \"signatures\"), each with one short phrase.`,\n \"Symptoms describe what goes wrong for a user of this specific file (a wrong value, an error, nothing happening), never generic phrases.\",\n \"Never invent paths or symbol names.\",\n].join(\" \");\n\nexport type SummarizeResult = {\n summarized: number;\n skipped: number;\n remaining: number;\n stoppedBy: \"done\" | \"limit\" | \"budget\" | \"cooldown\" | \"disabled\" | \"aborted\" | \"error\";\n};\n\nexport async function summarizeRepository(\n root: string,\n options: {\n limit?: number;\n onProgress?: (done: number, total: number) => void;\n signal?: AbortSignal;\n /** Injected config (tests, CLI); defaults to `resolveLlmConfig`. */\n config?: EnabledLlmConfig;\n /** Wait between batches to stay under the free tier's tokens/minute. */\n pace?: boolean;\n batchFiles?: number;\n } = {},\n): Promise<SummarizeResult> {\n const resolved = options.config ?? (await resolveLlmConfig(root));\n if (!resolved.enabled || !resolved.features.summaries) {\n return { summarized: 0, skipped: 0, remaining: 0, stoppedBy: \"disabled\" };\n }\n const config = resolved;\n const { manifest } = await loadFreshIndex(root);\n const store = await readStore(root);\n for (const path of Object.keys(store)) if (!manifest.entries[path]) delete store[path];\n\n const map = await loadProjectArchitectureMap(root, manifest).catch(() => null);\n const pending = orderByImportance(\n map,\n Object.values(manifest.entries).filter((entry) => {\n if (!cardEligible(entry.path)) return false;\n const done = store[entry.path];\n return !(\n done?.hash === entry.hash &&\n done.model === config.model &&\n done.version === SUMMARY_PROMPT_VERSION\n );\n }),\n );\n const local = config.provider === \"local\";\n const batchFiles = local ? 1 : (options.batchFiles ?? LIMITS.batchFiles);\n const limit = options.limit ?? Number.POSITIVE_INFINITY;\n let summarized = 0;\n let skipped = 0;\n let index = 0;\n let stoppedBy: SummarizeResult[\"stoppedBy\"] = \"done\";\n\n while (index < pending.length) {\n if (options.signal?.aborted) {\n stoppedBy = \"aborted\";\n break;\n }\n if (summarized >= limit) {\n stoppedBy = \"limit\";\n break;\n }\n const batch: Array<{ entry: IndexEntry; input: FileInput }> = [];\n let tokens = 0;\n while (index < pending.length && batch.length < batchFiles) {\n if (summarized + batch.length >= limit) break;\n const entry = pending[index] as IndexEntry;\n const input = await fileInput(root, entry, map);\n if (input === null) {\n skipped++;\n index++;\n continue;\n }\n const cost = Math.ceil(JSON.stringify(input).length / 4);\n if (batch.length > 0 && tokens + cost > LIMITS.batchTokens - 600) break;\n batch.push({ entry, input });\n tokens += cost;\n index++;\n }\n if (batch.length === 0) continue;\n\n // A prompt hook or tool call is running: let it have the machine first.\n if (local) await waitForForeground(root, options.signal);\n const started = Date.now();\n const ask = () =>\n chatJson(config, {\n system: SYSTEM,\n user: JSON.stringify({ files: batch.map(({ input }) => input) }),\n schema: outputSchema,\n jsonSchema: CARD_GRAMMAR,\n maxTokens: local ? LIMITS.localOutputTokens : LIMITS.outputTokensPerFile * batch.length,\n deadlineMs: local ? LIMITS.localTimeoutMs : LIMITS.timeoutMs,\n purpose: \"background\",\n repositoryRoot: root,\n });\n let result = await ask();\n // Invalid output gets one retry, then the file is skipped.\n if (!result.ok && result.reason === \"invalid\") result = await ask();\n if (!result.ok) {\n // A batch the provider rejects outright (bad output, or e.g. 413 for one\n // file) is skipped, not retried forever; limits and outages stop the run.\n // A local timeout is one slow file, not an outage.\n if (\n result.reason === \"invalid\" ||\n (result.reason === \"http\" && batch.length === 1) ||\n (local && result.reason === \"timeout\")\n ) {\n skipped += batch.length;\n continue;\n }\n index -= batch.length;\n stoppedBy =\n result.reason === \"budget\"\n ? \"budget\"\n : result.reason === \"cooldown\" || result.reason === \"rate-limited\"\n ? \"cooldown\"\n : \"error\";\n break;\n }\n for (const { entry } of batch) {\n // A one-file batch (the local model) may echo the path imperfectly.\n const answer =\n result.value.files.find((file) => file.path === entry.path) ??\n (batch.length === 1 && result.value.files.length === 1 ? result.value.files[0] : undefined);\n if (!answer?.summary.trim()) {\n skipped++;\n continue;\n }\n store[entry.path] = toCard(entry, config.model, answer);\n summarized++;\n }\n await writeStore(root, store);\n await writeCardFiles(\n root,\n store,\n batch.map(({ entry }) => entry.path),\n );\n options.onProgress?.(index, pending.length);\n if (options.pace !== false) {\n const wait = (result.tokens / LIMITS.tokensPerMinute) * 60_000 - (Date.now() - started);\n if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));\n }\n }\n await writeStore(root, store);\n await pruneCardFiles(root, store, manifest);\n if (summarized > 0)\n await refreshZvecCardIndex(root, options.signal ? { signal: options.signal } : {});\n return { summarized, skipped, remaining: pending.length - index, stoppedBy };\n}\n\ntype CardAnswer = z.infer<typeof outputSchema>[\"files\"][number];\n\n/** Bounded, de-duplicated card fields; symbols only from the file's symbol table. */\nfunction toCard(entry: IndexEntry, model: string, answer: CardAnswer): LlmSummary {\n const known = new Set((entry.symbolTable ?? []).map((symbol) => symbol.name));\n const clean = (items: string[], count: number, chars: number) =>\n [\n ...new Set(items.map((item) => redactSensitive(item.trim()).slice(0, chars)).filter(Boolean)),\n ].slice(0, count);\n return {\n hash: entry.hash,\n model,\n version: SUMMARY_PROMPT_VERSION,\n summary: redactSensitive(answer.summary.trim()).slice(0, LIMITS.fileChars),\n symptoms: clean(answer.symptoms, LIMITS.symptoms, LIMITS.symptomChars),\n terms: clean(answer.terms, LIMITS.terms, LIMITS.termChars),\n symbols: Object.fromEntries(\n answer.symbols\n .filter((symbol) => known.has(symbol.name))\n .slice(0, LIMITS.symbols)\n .map((symbol) => [symbol.name, symbol.summary.trim().slice(0, LIMITS.symbolChars)]),\n ),\n };\n}\n\n/** One Markdown card per source file for the zvec-grep card workspace. */\nasync function writeCardFiles(root: string, store: Store, paths: string[]): Promise<void> {\n for (const path of paths) {\n const card = store[path];\n if (!card) continue;\n const target = join(root, CARDS_DIR, `${path}.md`);\n await mkdir(dirname(target), { recursive: true });\n await writeAtomic(\n target,\n [\n \"---\",\n `source: ${JSON.stringify(path)}`,\n `hash: ${card.hash}`,\n \"---\",\n \"\",\n `# ${path}`,\n \"\",\n card.summary,\n \"\",\n ...(card.symptoms?.length ? [\"Symptoms:\", ...card.symptoms.map((s) => `- ${s}`), \"\"] : []),\n ...(card.terms?.length ? [`Terms: ${card.terms.join(\", \")}`, \"\"] : []),\n ...Object.entries(card.symbols).map(([name, text]) => `- ${name}: ${text}`),\n \"\",\n ].join(\"\\n\"),\n );\n }\n}\n\n/** Write missing cards and delete cards whose source is gone or no longer current. */\nasync function pruneCardFiles(root: string, store: Store, manifest: IndexManifest): Promise<void> {\n const directory = join(root, CARDS_DIR);\n const current = new Set(\n Object.entries(store)\n .filter(([path, card]) => manifest.entries[path]?.hash === card.hash && card.symptoms)\n .map(([path]) => `${path}.md`),\n );\n const existing = await readdir(directory, { recursive: true }).catch(() => [] as string[]);\n const present = new Set(existing.map((name) => name.split(\"\\\\\").join(\"/\")));\n for (const name of present) {\n if (name.endsWith(\".md\") && !name.startsWith(\".zvec-grep\") && !current.has(name)) {\n await rm(join(directory, name), { force: true });\n }\n }\n await writeCardFiles(\n root,\n store,\n [...current].filter((name) => !present.has(name)).map((name) => name.slice(0, -3)),\n );\n}\n\n/** Card coverage for `stats`/`storage`/`models`: current cards vs eligible source files. */\nexport async function cardProgress(root: string): Promise<{ carded: number; eligible: number }> {\n const { manifest } = await loadFreshIndex(root);\n const store = await readStore(root);\n const eligible = Object.keys(manifest.entries).filter(cardEligible);\n return {\n carded: eligible.filter(\n (path) =>\n store[path]?.hash === manifest.entries[path]?.hash &&\n store[path]?.version === SUMMARY_PROMPT_VERSION,\n ).length,\n eligible: eligible.length,\n };\n}\n\ntype FileInput = {\n path: string;\n language: string;\n signatures: string[];\n /** Files this one imports and files that import it (paths only). */\n neighbours: string[];\n head: string;\n};\n\ntype ImportMap = Awaited<ReturnType<typeof loadProjectArchitectureMap>> | null;\n\n/** Import lines say little about what a file does; the card reads past them. */\nconst IMPORT_LINE =\n /^\\s*(import\\b|from\\s+\\S+\\s+import\\b|export\\s+(\\*|\\{[^}]*\\})\\s+from\\b|(const|let|var)\\s+\\S+\\s*=\\s*require\\()|^\\s*$/;\n\n/** What is sent for one file; null for unreadable or secret-bearing files. */\nasync function fileInput(\n root: string,\n entry: IndexEntry,\n map: ImportMap,\n): Promise<FileInput | null> {\n const source = await readBoundedSource(root, entry.path);\n if (source === null || scanSensitive(source).sensitive) return null;\n const edges = map?.edges ?? [];\n return {\n path: entry.path,\n language: entry.language,\n // Not the deterministic summary: small models copy its \"ts • 80 lines\" form.\n signatures: (entry.symbolTable ?? [])\n .filter((symbol) => symbol.kind !== \"heading\")\n .slice(0, LIMITS.signatures)\n .map((symbol) => redactSensitive(symbol.signature.slice(0, LIMITS.signatureChars))),\n neighbours: [\n ...new Set([\n ...edges.filter((edge) => edge.from === entry.path).map((edge) => edge.to),\n ...edges.filter((edge) => edge.to === entry.path).map((edge) => edge.from),\n ]),\n ].slice(0, LIMITS.neighbours),\n // The module docstring and first definitions, never the whole file.\n head: redactSensitive(\n source\n .split(/\\r?\\n/)\n .filter((line) => !IMPORT_LINE.test(line))\n .join(\"\\n\")\n .slice(0, LIMITS.headChars),\n ),\n };\n}\n\n/** Most-imported source first. */\nfunction orderByImportance(map: ImportMap, entries: IndexEntry[]): IndexEntry[] {\n const fanIn = new Map<string, number>();\n for (const edge of map?.edges ?? []) fanIn.set(edge.to, (fanIn.get(edge.to) ?? 0) + 1);\n return [...entries].sort(\n (left, right) =>\n (fanIn.get(right.path) ?? 0) - (fanIn.get(left.path) ?? 0) ||\n left.path.localeCompare(right.path),\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,27 +1,24 @@
1
- import {
2
- failSoft,
3
- readHookInput,
4
- writeStdout
5
- } from "./chunk-FJOTP632.js";
6
1
  import {
7
2
  SessionCapsuleLedger,
8
3
  detectTestCommand,
9
4
  estimateTokens,
10
5
  findCoveringTest
11
- } from "./chunk-GKNYMNFM.js";
6
+ } from "./chunk-7IPSDDO5.js";
12
7
  import {
13
8
  hookRoot
14
9
  } from "./chunk-TGFTKWOR.js";
15
10
  import {
11
+ skipPromptRetrieval
12
+ } from "./chunk-GQKWUHMA.js";
13
+ import {
14
+ EXPANSION_MS,
16
15
  NON_SOURCE_PATH,
17
16
  discoverTaskPaths,
17
+ loadFreshIndex,
18
18
  loadProjectArchitectureMap,
19
19
  readBoundedSource,
20
20
  taskTerms
21
- } from "./chunk-TLWGZVPC.js";
22
- import {
23
- loadFreshIndex
24
- } from "./chunk-ITQO632W.js";
21
+ } from "./chunk-UR3DF74I.js";
25
22
  import {
26
23
  loadKnownSecretValues,
27
24
  redactSourceText
@@ -41,24 +38,30 @@ var LEAN_LIMITS = {
41
38
  var LEAN_HOOK_BUDGET_MS = 1500;
42
39
  var RENDER_RESERVE_MS = 300;
43
40
  async function runLeanHook(input, emit, budgetMs = LEAN_HOOK_BUDGET_MS) {
41
+ if (skipPromptRetrieval(input.prompt)) return "";
44
42
  const started = Date.now();
45
43
  const repositoryRoot = hookRoot(input.cwd);
46
44
  const prompt = input.prompt?.trim() ?? "";
47
45
  if (prompt === "" || prompt.startsWith("/")) return "";
48
46
  const sessionId = input.session_id ?? "default";
49
47
  try {
48
+ void import("./local-75DJECVR.js").then(({ markForeground }) => markForeground(repositoryRoot));
50
49
  const { manifest } = await loadFreshIndex(repositoryRoot);
50
+ const renderBy = started + budgetMs - RENDER_RESERVE_MS;
51
51
  const discovery = await discoverTaskPaths(
52
52
  repositoryRoot,
53
53
  prompt,
54
54
  manifest,
55
55
  loadProjectArchitectureMap(repositoryRoot, manifest),
56
- // The rerank, and zvec-grep (opt-in, only with a running `zg server`),
57
- // must answer while rendering still fits in the hook budget.
56
+ // The rerank, zvec-grep (only with a running `zg server`, which `serve`
57
+ // starts), the local file/card vectors and the expansion cache must all
58
+ // answer while rendering still fits in the hook budget.
58
59
  {
59
60
  semantic: false,
60
- llmDeadlineAt: started + budgetMs - RENDER_RESERVE_MS,
61
- zvecDeadlineAt: started + budgetMs - RENDER_RESERVE_MS
61
+ llmDeadlineAt: renderBy,
62
+ zvecDeadlineAt: renderBy,
63
+ vectorDeadlineAt: renderBy,
64
+ expansionDeadlineAt: Math.min(renderBy, Date.now() + EXPANSION_MS.hook)
62
65
  }
63
66
  );
64
67
  const terms = taskTerms(prompt);
@@ -176,15 +179,12 @@ function isReadOnlyPrompt(prompt) {
176
179
  return asks && !EDIT_VERB.test(text);
177
180
  }
178
181
 
179
- // src/hooks/lean-command.ts
180
- async function runLeanHookCommand() {
181
- await failSoft(async () => {
182
- await runLeanHook(await readHookInput(), writeStdout);
183
- });
184
- }
185
-
186
182
  export {
183
+ LEAN_POINTER_PREFIX,
184
+ LEAN_LIMITS,
185
+ LEAN_HOOK_BUDGET_MS,
186
+ runLeanHook,
187
187
  renderLeanBlock,
188
- runLeanHookCommand
188
+ isReadOnlyPrompt
189
189
  };
190
- //# sourceMappingURL=chunk-UK6XTS2Y.js.map
190
+ //# sourceMappingURL=chunk-JJQDVGF7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/lean.ts"],"sourcesContent":["/**\n * Lean mode: the prompt hook's only job is to cut the agent's token spend. It\n * injects ranked file pointers (and code spans for read-only questions) and\n * nothing else — no MCP tools, rules block, or memory capture.\n */\nimport { createHash } from \"node:crypto\";\nimport { appendFile, mkdir } from \"node:fs/promises\";\nimport { extname, join } from \"node:path\";\nimport {\n discoverTaskPaths,\n EXPANSION_MS,\n loadProjectArchitectureMap,\n NON_SOURCE_PATH,\n readBoundedSource,\n taskTerms,\n} from \"../context/discovery.js\";\nimport { type ContextCapsule, SessionCapsuleLedger } from \"../context/session-ledger.js\";\nimport { detectTestCommand, findCoveringTest } from \"../context/tests.js\";\nimport { loadKnownSecretValues, redactSourceText } from \"../core/sensitive.js\";\nimport { loadFreshIndex } from \"../indexer/engine.js\";\nimport { estimateTokens } from \"../savings/tokens.js\";\nimport { skipPromptRetrieval } from \"./prompt-gate.js\";\nimport { hookRoot } from \"./root.js\";\nimport type { HookInput } from \"./runtime.js\";\n\nexport const LEAN_POINTER_PREFIX = \"CodeBuddy likely files: \";\nexport const LEAN_LIMITS = {\n paths: 6,\n symbolsPerFile: 3,\n wholeFileLines: 80,\n spanTokens: 1200,\n} as const;\n\nexport type LeanPointer = {\n path: string;\n symbols: Array<{ name: string; startLine: number; endLine: number }>;\n /** The test file that covers this path, when one follows a known convention. */\n test?: string;\n};\n\ntype LoadedPointer = LeanPointer & { lines: string[] };\n\n/** A lean block finished later than this is dropped: the host may already have moved on. */\nexport const LEAN_HOOK_BUDGET_MS = 1500;\n/** Rendering after the BYOK rerank needs this much of the budget. */\nconst RENDER_RESERVE_MS = 300;\n\n/**\n * UserPromptSubmit in lean mode → ranked file pointers, or nothing. Never\n * throws: an error is appended to `.codebuddy/cache/hooks/errors.log` and the\n * turn proceeds as if CodeBuddy were not installed.\n */\nexport async function runLeanHook(\n input: HookInput,\n emit?: (block: string) => Promise<void>,\n budgetMs = LEAN_HOOK_BUDGET_MS,\n): Promise<string> {\n if (skipPromptRetrieval(input.prompt)) return \"\";\n const started = Date.now();\n const repositoryRoot = hookRoot(input.cwd);\n const prompt = input.prompt?.trim() ?? \"\";\n if (prompt === \"\" || prompt.startsWith(\"/\")) return \"\";\n const sessionId = input.session_id ?? \"default\";\n try {\n // Background carding in `serve` yields while a hook runs.\n void import(\"../llm/local.js\").then(({ markForeground }) => markForeground(repositoryRoot));\n const { manifest } = await loadFreshIndex(repositoryRoot);\n const renderBy = started + budgetMs - RENDER_RESERVE_MS;\n const discovery = await discoverTaskPaths(\n repositoryRoot,\n prompt,\n manifest,\n loadProjectArchitectureMap(repositoryRoot, manifest),\n // The rerank, zvec-grep (only with a running `zg server`, which `serve`\n // starts), the local file/card vectors and the expansion cache must all\n // answer while rendering still fits in the hook budget.\n {\n semantic: false,\n llmDeadlineAt: renderBy,\n zvecDeadlineAt: renderBy,\n vectorDeadlineAt: renderBy,\n expansionDeadlineAt: Math.min(renderBy, Date.now() + EXPANSION_MS.hook),\n },\n );\n const terms = taskTerms(prompt);\n const indexed = new Set(Object.keys(manifest.entries));\n const pointers: LeanPointer[] = discovery.paths\n .filter((path) => !NON_SOURCE_PATH.test(path))\n .map((path) => {\n const test = findCoveringTest(path, indexed);\n const table = manifest.entries[path]?.symbolTable ?? [];\n // Symbols the BYOK rerank named come first, then term matches.\n const picked = discovery.symbols?.[path] ?? [];\n const ordered = [\n ...picked.flatMap((name) => table.filter((symbol) => symbol.name === name)),\n ...table.filter(\n (symbol) =>\n !picked.includes(symbol.name) &&\n terms.some((term) => symbol.name.toLowerCase().includes(term)),\n ),\n ];\n return {\n path,\n symbols: ordered.map(({ name, startLine, endLine }) => ({ name, startLine, endLine })),\n ...(test ? { test } : {}),\n };\n });\n const ledger = new SessionCapsuleLedger(repositoryRoot);\n // The host keeps the block for this turn only, so nothing counts as already sent.\n if (input.ephemeral_context) await ledger.reset(sessionId);\n const { text, emitted } = await renderLeanBlock({\n repositoryRoot,\n prompt,\n pointers,\n ledger,\n sessionId,\n testCommand: pointers.length > 0 ? await detectTestCommand(repositoryRoot) : null,\n });\n if (Date.now() - started >= budgetMs) return \"\";\n if (text) await emit?.(text);\n await ledger.markSent(sessionId, emitted);\n return text;\n } catch (error) {\n await logHookError(repositoryRoot, sessionId, error).catch(() => {});\n return \"\";\n }\n}\n\nasync function logHookError(root: string, sessionId: string, error: unknown): Promise<void> {\n const directory = join(root, \".codebuddy\", \"cache\", \"hooks\");\n await mkdir(directory, { recursive: true });\n const message = (error as Error)?.message ?? String(error);\n await appendFile(\n join(directory, \"errors.log\"),\n `${new Date().toISOString()} ${message.replace(/\\s+/g, \" \")} session=${sessionId}\\n`,\n );\n}\n\n/**\n * The lean block for one prompt: a single pointer line, plus current-source\n * spans when the prompt is read-only. Capsules already delivered this session\n * are left out; the caller marks `emitted` as sent once the block is written.\n */\nexport async function renderLeanBlock(input: {\n repositoryRoot: string;\n prompt: string;\n pointers: LeanPointer[];\n ledger: SessionCapsuleLedger;\n sessionId: string;\n /** Sent once per session, after the pointers. */\n testCommand?: string | null;\n}): Promise<{ text: string; emitted: ContextCapsule[] }> {\n const loaded: LoadedPointer[] = [];\n for (const pointer of input.pointers) {\n if (loaded.length === LEAN_LIMITS.paths) break;\n const source = await readBoundedSource(input.repositoryRoot, pointer.path);\n if (source === null) continue; // deleted, oversized or binary since indexing\n loaded.push({\n path: pointer.path,\n symbols: pointer.symbols.slice(0, LEAN_LIMITS.symbolsPerFile),\n ...(pointer.test ? { test: pointer.test } : {}),\n lines: source.split(/\\r?\\n/),\n });\n }\n if (loaded.length === 0) return { text: \"\", emitted: [] };\n const knownSecrets = await loadKnownSecretValues(input.repositoryRoot);\n\n const capsules: ContextCapsule[] = [\n {\n key: \"lean:pointers\",\n text: LEAN_POINTER_PREFIX + loaded.map(describePointer).join(\", \"),\n },\n ...(input.testCommand\n ? [{ key: \"lean:test-command\", text: `CodeBuddy test command: ${input.testCommand}` }]\n : []),\n ];\n if (isReadOnlyPrompt(input.prompt)) {\n let tokens = 0;\n for (const span of loaded.flatMap((pointer) => spansOf(pointer, knownSecrets))) {\n const cost = estimateTokens(span.text);\n if (tokens + cost > LEAN_LIMITS.spanTokens) break;\n tokens += cost;\n capsules.push(span);\n }\n }\n const seen = await input.ledger.seen(input.sessionId, capsules);\n const emitted = capsules.filter((capsule) => !seen.has(capsule.key));\n return { text: emitted.map((capsule) => capsule.text).join(\"\\n\"), emitted };\n}\n\nfunction describePointer(pointer: LoadedPointer): string {\n const ranges = pointer.symbols.map((s) => `${s.name} L${s.startLine}-${s.endLine}`).join(\", \");\n const test = pointer.test ? `test: ${pointer.test}` : \"\";\n const detail = [ranges, test].filter(Boolean).join(\"; \");\n return detail ? `${pointer.path} (${detail})` : pointer.path;\n}\n\n/** Whole small files; otherwise the matched symbol spans. */\nfunction spansOf(pointer: LoadedPointer, knownSecrets: readonly string[]): ContextCapsule[] {\n const ranges =\n pointer.lines.length <= LEAN_LIMITS.wholeFileLines\n ? [{ start: 1, end: pointer.lines.length }]\n : pointer.symbols.map((s) => ({ start: s.startLine, end: s.endLine }));\n const language = extname(pointer.path).slice(1);\n return ranges.map(({ start, end }) => {\n const body = redactSourceText(pointer.lines.slice(start - 1, end).join(\"\\n\"), knownSecrets);\n const hash = createHash(\"sha1\").update(body).digest(\"hex\").slice(0, 12);\n return {\n key: `lean:span:${pointer.path}:${start}-${end}:${hash}`,\n text: `${pointer.path}:L${start}-${end}\\n\\`\\`\\`${language}\\n${body}\\n\\`\\`\\``,\n };\n });\n}\n\nconst FORCED_READ_ONLY =\n /\\b(without editing|without changing|don't change|do not change|don't edit|do not edit)\\b/i;\nconst QUESTION_START = /^(how|what|why|where|which|who|explain|describe|summarize)\\b/i;\nconst EDIT_VERB =\n /\\b(add|change|fix|implement|update|remove|delete|rename|refactor|write|create|make|edit|replace|move|migrate|support|build)\\b/i;\n\n/**\n * Whether a prompt asks a question without asking for edits. Conservative: a\n * false positive costs up to the span budget, a false negative costs nothing.\n */\nexport function isReadOnlyPrompt(prompt: string): boolean {\n const text = prompt.trim();\n if (text === \"\" || text.startsWith(\"/\")) return false;\n if (FORCED_READ_ONLY.test(text)) return true;\n const asks = QUESTION_START.test(text) || text.endsWith(\"?\");\n return asks && !EDIT_VERB.test(text);\n}\n"],"mappings":"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","names":[]}