@orquentra/codebuddy 2.14.0 → 2.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +98 -0
- package/README.md +87 -6
- package/dist/{chunk-YNSTIMCF.js → chunk-4DTO2E4Y.js} +352 -157
- package/dist/chunk-4DTO2E4Y.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-6OQKL35B.js +55 -0
- package/dist/chunk-6OQKL35B.js.map +1 -0
- package/dist/{chunk-4DKYKDHI.js → chunk-7IPSDDO5.js} +5 -3
- package/dist/{chunk-4DKYKDHI.js.map → chunk-7IPSDDO5.js.map} +1 -1
- package/dist/chunk-7JPUMXFE.js +413 -0
- package/dist/chunk-7JPUMXFE.js.map +1 -0
- package/dist/{chunk-LRQNPYCK.js → chunk-AFXWCSQ4.js} +13 -7
- package/dist/chunk-AFXWCSQ4.js.map +1 -0
- package/dist/chunk-BNZDEU5K.js +368 -0
- package/dist/chunk-BNZDEU5K.js.map +1 -0
- package/dist/chunk-EMWGLKPR.js +45 -0
- package/dist/chunk-EMWGLKPR.js.map +1 -0
- package/dist/{chunk-YJPCS2GP.js → chunk-GNV4X67F.js} +59 -20
- package/dist/chunk-GNV4X67F.js.map +1 -0
- package/dist/{chunk-YWDH6DQ6.js → chunk-IM5RFEW4.js} +21 -19
- package/dist/{chunk-YWDH6DQ6.js.map → chunk-IM5RFEW4.js.map} +1 -1
- package/dist/chunk-K4XNTYV7.js +136 -0
- package/dist/chunk-K4XNTYV7.js.map +1 -0
- package/dist/{chunk-ESZXCZIN.js → chunk-MNSMW4MZ.js} +33 -5
- package/dist/chunk-MNSMW4MZ.js.map +1 -0
- package/dist/chunk-NNJSAYHY.js +372 -0
- package/dist/chunk-NNJSAYHY.js.map +1 -0
- package/dist/{chunk-XSNW6Y3X.js → chunk-OO7Q7XZF.js} +28 -24
- package/dist/chunk-OO7Q7XZF.js.map +1 -0
- package/dist/{chunk-L6BRYZ6G.js → chunk-PZ4EBFIA.js} +7 -7
- package/dist/{chunk-L6BRYZ6G.js.map → chunk-PZ4EBFIA.js.map} +1 -1
- package/dist/chunk-RGIVHKBH.js +156 -0
- package/dist/chunk-RGIVHKBH.js.map +1 -0
- package/dist/{chunk-LW75SKZR.js → chunk-RKF4HBBQ.js} +126 -18
- package/dist/chunk-RKF4HBBQ.js.map +1 -0
- package/dist/{chunk-7QMHIJBA.js → chunk-VBDM7HC5.js} +969 -968
- package/dist/chunk-VBDM7HC5.js.map +1 -0
- package/dist/chunk-VF7DKRIU.js +50 -0
- package/dist/chunk-VF7DKRIU.js.map +1 -0
- package/dist/{chunk-KNR2P2UY.js → chunk-Y4QQML24.js} +21 -6
- package/dist/chunk-Y4QQML24.js.map +1 -0
- package/dist/cli/index.js +4 -4
- package/dist/config-S2BDMT4B.js +15 -0
- package/dist/expand-TONFVT5D.js +98 -0
- package/dist/expand-TONFVT5D.js.map +1 -0
- package/dist/file-vectors-VREL75MA.js +19 -0
- package/dist/index-facts-MBJKBYA3.js +38 -0
- package/dist/index-pass-RP23G2CR.js +15 -0
- package/dist/index.js +24 -22
- package/dist/{install-EU35ERK5.js → install-MCJIJMFF.js} +11 -10
- package/dist/{lean-43ADYTBW.js → lean-CBHVW6JM.js} +8 -7
- package/dist/local-TH5XCLVJ.js +48 -0
- package/dist/local-llm-GZREB5DF.js +19 -0
- package/dist/local-llm-GZREB5DF.js.map +1 -0
- package/dist/{main-SFC5LV4E.js → main-QF6Z552J.js} +346 -140
- package/dist/main-QF6Z552J.js.map +1 -0
- package/dist/{memory-TGHVTBME.js → memory-LSZKSF53.js} +9 -6
- package/dist/{memory-TGHVTBME.js.map → memory-LSZKSF53.js.map} +1 -1
- package/dist/{operations-6Z6ZDVBP.js → operations-BPI633VN.js} +8 -7
- package/dist/operations-BPI633VN.js.map +1 -0
- package/dist/{rerank-IQAEWLQR.js → rerank-EPCTSEVP.js} +7 -4
- package/dist/{rerank-IQAEWLQR.js.map → rerank-EPCTSEVP.js.map} +1 -1
- package/dist/{runtime-W5AW32SD.js → runtime-W5ACTDFS.js} +9 -8
- package/dist/runtime-W5ACTDFS.js.map +1 -0
- package/dist/summaries-W2J5FCLG.js +32 -0
- package/dist/summaries-W2J5FCLG.js.map +1 -0
- package/dist/{zvec-24HLAFGN.js → zvec-LSA2IRAU.js} +13 -5
- package/dist/zvec-LSA2IRAU.js.map +1 -0
- package/package.json +3 -2
- package/dist/chunk-7QMHIJBA.js.map +0 -1
- package/dist/chunk-CAEPHLHC.js.map +0 -1
- package/dist/chunk-ESC4Y3L7.js +0 -204
- package/dist/chunk-ESC4Y3L7.js.map +0 -1
- package/dist/chunk-ESZXCZIN.js.map +0 -1
- package/dist/chunk-KNR2P2UY.js.map +0 -1
- package/dist/chunk-LRQNPYCK.js.map +0 -1
- package/dist/chunk-LW75SKZR.js.map +0 -1
- package/dist/chunk-SFMIMLXR.js +0 -63
- package/dist/chunk-SFMIMLXR.js.map +0 -1
- package/dist/chunk-XSNW6Y3X.js.map +0 -1
- package/dist/chunk-YJPCS2GP.js.map +0 -1
- package/dist/chunk-YNSTIMCF.js.map +0 -1
- package/dist/config-GEZFLXK6.js +0 -12
- package/dist/main-SFC5LV4E.js.map +0 -1
- package/dist/summaries-32PHOB6L.js +0 -20
- /package/dist/{config-GEZFLXK6.js.map → config-S2BDMT4B.js.map} +0 -0
- /package/dist/{install-EU35ERK5.js.map → file-vectors-VREL75MA.js.map} +0 -0
- /package/dist/{lean-43ADYTBW.js.map → index-facts-MBJKBYA3.js.map} +0 -0
- /package/dist/{operations-6Z6ZDVBP.js.map → index-pass-RP23G2CR.js.map} +0 -0
- /package/dist/{runtime-W5AW32SD.js.map → install-MCJIJMFF.js.map} +0 -0
- /package/dist/{summaries-32PHOB6L.js.map → lean-CBHVW6JM.js.map} +0 -0
- /package/dist/{zvec-24HLAFGN.js.map → local-TH5XCLVJ.js.map} +0 -0
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
detectTestCommand,
|
|
4
4
|
estimateTokens,
|
|
5
5
|
findCoveringTest
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-7IPSDDO5.js";
|
|
7
7
|
import {
|
|
8
8
|
hookRoot
|
|
9
9
|
} from "./chunk-TGFTKWOR.js";
|
|
@@ -11,13 +11,14 @@ import {
|
|
|
11
11
|
skipPromptRetrieval
|
|
12
12
|
} from "./chunk-GQKWUHMA.js";
|
|
13
13
|
import {
|
|
14
|
+
EXPANSION_MS,
|
|
14
15
|
NON_SOURCE_PATH,
|
|
15
16
|
discoverTaskPaths,
|
|
16
17
|
loadFreshIndex,
|
|
17
18
|
loadProjectArchitectureMap,
|
|
18
19
|
readBoundedSource,
|
|
19
20
|
taskTerms
|
|
20
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-4DTO2E4Y.js";
|
|
21
22
|
import {
|
|
22
23
|
loadKnownSecretValues,
|
|
23
24
|
redactSourceText
|
|
@@ -44,18 +45,23 @@ async function runLeanHook(input, emit, budgetMs = LEAN_HOOK_BUDGET_MS) {
|
|
|
44
45
|
if (prompt === "" || prompt.startsWith("/")) return "";
|
|
45
46
|
const sessionId = input.session_id ?? "default";
|
|
46
47
|
try {
|
|
48
|
+
void import("./local-TH5XCLVJ.js").then(({ markForeground }) => markForeground(repositoryRoot));
|
|
47
49
|
const { manifest } = await loadFreshIndex(repositoryRoot);
|
|
50
|
+
const renderBy = started + budgetMs - RENDER_RESERVE_MS;
|
|
48
51
|
const discovery = await discoverTaskPaths(
|
|
49
52
|
repositoryRoot,
|
|
50
53
|
prompt,
|
|
51
54
|
manifest,
|
|
52
55
|
loadProjectArchitectureMap(repositoryRoot, manifest),
|
|
53
|
-
// The rerank,
|
|
54
|
-
//
|
|
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.
|
|
55
59
|
{
|
|
56
60
|
semantic: false,
|
|
57
|
-
llmDeadlineAt:
|
|
58
|
-
zvecDeadlineAt:
|
|
61
|
+
llmDeadlineAt: renderBy,
|
|
62
|
+
zvecDeadlineAt: renderBy,
|
|
63
|
+
vectorDeadlineAt: renderBy,
|
|
64
|
+
expansionDeadlineAt: Math.min(renderBy, Date.now() + EXPANSION_MS.hook)
|
|
59
65
|
}
|
|
60
66
|
);
|
|
61
67
|
const terms = taskTerms(prompt);
|
|
@@ -181,4 +187,4 @@ export {
|
|
|
181
187
|
renderLeanBlock,
|
|
182
188
|
isReadOnlyPrompt
|
|
183
189
|
};
|
|
184
|
-
//# sourceMappingURL=chunk-
|
|
190
|
+
//# sourceMappingURL=chunk-AFXWCSQ4.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":[]}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chatJson
|
|
3
|
+
} from "./chunk-MNSMW4MZ.js";
|
|
4
|
+
import {
|
|
5
|
+
MemoryFileStore,
|
|
6
|
+
termOverlap
|
|
7
|
+
} from "./chunk-VBDM7HC5.js";
|
|
8
|
+
import {
|
|
9
|
+
generateEntityId
|
|
10
|
+
} from "./chunk-EMWGLKPR.js";
|
|
11
|
+
import {
|
|
12
|
+
redactSensitive,
|
|
13
|
+
scanSensitive
|
|
14
|
+
} from "./chunk-AL6VBTNA.js";
|
|
15
|
+
import {
|
|
16
|
+
resolveLlmConfig
|
|
17
|
+
} from "./chunk-RGIVHKBH.js";
|
|
18
|
+
import {
|
|
19
|
+
resolveNamespaceFrom
|
|
20
|
+
} from "./chunk-Y4QQML24.js";
|
|
21
|
+
import {
|
|
22
|
+
writeAtomic
|
|
23
|
+
} from "./chunk-CG5B3LYG.js";
|
|
24
|
+
import {
|
|
25
|
+
waitForForeground
|
|
26
|
+
} from "./chunk-NNJSAYHY.js";
|
|
27
|
+
|
|
28
|
+
// src/llm/index-facts.ts
|
|
29
|
+
import { createHash } from "crypto";
|
|
30
|
+
import { mkdir, readdir, readFile } from "fs/promises";
|
|
31
|
+
import { dirname, join } from "path";
|
|
32
|
+
import { parse as parseYaml } from "yaml";
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
var INDEX_FACT_AGENT = "codebuddy-index";
|
|
35
|
+
var MAX_FACTS = 40;
|
|
36
|
+
var PER_SOURCE = 8;
|
|
37
|
+
var SECTION_CHARS = 1600;
|
|
38
|
+
var DUPLICATE_OVERLAP = 0.8;
|
|
39
|
+
var TIMEOUT_MS = { local: 6e4, hosted: 2e4 };
|
|
40
|
+
var PROSE_SOURCES = /^(README|CONTRIBUTING|AGENTS|CLAUDE)(\.[a-z]+)?$/i;
|
|
41
|
+
var POLICY_DOC = /(deprecat|release[-_ ]?(policy|process)|versioning|support[-_ ]?policy|compatib)/i;
|
|
42
|
+
async function factSources(root) {
|
|
43
|
+
const top = await readdir(root).catch(() => []);
|
|
44
|
+
const docs = await readdir(join(root, "docs"), { recursive: true }).catch(() => []);
|
|
45
|
+
const workflows = await readdir(join(root, ".github", "workflows")).catch(() => []);
|
|
46
|
+
return [
|
|
47
|
+
...["package.json", "pyproject.toml"].filter((name) => top.includes(name)),
|
|
48
|
+
...top.filter((name) => PROSE_SOURCES.test(name)).sort(),
|
|
49
|
+
...workflows.filter((name) => /\.ya?ml$/.test(name)).sort().map((name) => `.github/workflows/${name}`),
|
|
50
|
+
...docs.map((name) => name.split("\\").join("/")).filter((name) => /\.(md|rst|txt)$/i.test(name) && POLICY_DOC.test(name)).sort().slice(0, 4).map((name) => `docs/${name}`)
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
async function extractIndexFacts(root, options = {}) {
|
|
54
|
+
const resolved = options.config ?? await resolveLlmConfig(root);
|
|
55
|
+
if (!resolved.enabled || resolved.features.facts === false) {
|
|
56
|
+
return { written: 0, removed: 0, skippedSources: 0, stoppedBy: "disabled" };
|
|
57
|
+
}
|
|
58
|
+
const config = resolved;
|
|
59
|
+
const namespace = options.namespace ?? await resolveNamespaceFrom(root);
|
|
60
|
+
const store = new MemoryFileStore(root);
|
|
61
|
+
const ledger = await readLedger(root);
|
|
62
|
+
const sources = await factSources(root);
|
|
63
|
+
let facts = await store.listFacts();
|
|
64
|
+
let removed = 0;
|
|
65
|
+
const removeFrom = async (path) => {
|
|
66
|
+
for (const fact of facts) {
|
|
67
|
+
if (fact.createdByAgent === INDEX_FACT_AGENT && fact.paths[0] === path) {
|
|
68
|
+
await store.deleteFact(fact.id);
|
|
69
|
+
removed++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
facts = await store.listFacts();
|
|
73
|
+
};
|
|
74
|
+
for (const path of Object.keys(ledger.sources)) {
|
|
75
|
+
if (!sources.includes(path)) {
|
|
76
|
+
await removeFrom(path);
|
|
77
|
+
delete ledger.sources[path];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let written = 0;
|
|
81
|
+
let skippedSources = 0;
|
|
82
|
+
let stoppedBy = "done";
|
|
83
|
+
const indexFacts = () => facts.filter((fact) => fact.createdByAgent === INDEX_FACT_AGENT).length;
|
|
84
|
+
for (const path of sources) {
|
|
85
|
+
if (options.signal?.aborted) {
|
|
86
|
+
stoppedBy = "aborted";
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
const text = await readFile(join(root, path), "utf8").catch(() => null);
|
|
90
|
+
if (text === null) continue;
|
|
91
|
+
const hash = sha(text);
|
|
92
|
+
if (ledger.sources[path] === hash) {
|
|
93
|
+
skippedSources++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
await removeFrom(path);
|
|
97
|
+
if (indexFacts() >= MAX_FACTS) {
|
|
98
|
+
stoppedBy = "cap";
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
const candidates = await candidatesFor(root, path, text, config, options.signal);
|
|
102
|
+
let fromSource = 0;
|
|
103
|
+
for (const candidate of candidates) {
|
|
104
|
+
if (fromSource >= PER_SOURCE || indexFacts() >= MAX_FACTS) break;
|
|
105
|
+
if (scanSensitive(candidate.text).sensitive) continue;
|
|
106
|
+
if (facts.some(
|
|
107
|
+
(fact) => Math.max(
|
|
108
|
+
termOverlap(candidate.text, fact.object),
|
|
109
|
+
termOverlap(candidate.text, fact.content)
|
|
110
|
+
) >= DUPLICATE_OVERLAP
|
|
111
|
+
)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const id = generateEntityId("fact");
|
|
115
|
+
const content = `${candidate.text} (source: ${path}:L${candidate.line})`;
|
|
116
|
+
await store.writeFact({
|
|
117
|
+
id,
|
|
118
|
+
namespace,
|
|
119
|
+
subject: path,
|
|
120
|
+
predicate: "project-fact",
|
|
121
|
+
object: candidate.text,
|
|
122
|
+
confidence: 0.8,
|
|
123
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
124
|
+
createdByAgent: INDEX_FACT_AGENT,
|
|
125
|
+
sourceInteractionId: null,
|
|
126
|
+
sourceDeleted: false,
|
|
127
|
+
content,
|
|
128
|
+
paths: [path],
|
|
129
|
+
verification: [`${path}:L${candidate.line}`],
|
|
130
|
+
idempotencyKey: `index:${path}:${sha(candidate.text).slice(0, 16)}`.slice(0, 200),
|
|
131
|
+
fingerprint: sha(`${namespace}
|
|
132
|
+
${content}`)
|
|
133
|
+
});
|
|
134
|
+
facts = await store.listFacts();
|
|
135
|
+
written++;
|
|
136
|
+
fromSource++;
|
|
137
|
+
}
|
|
138
|
+
ledger.sources[path] = hash;
|
|
139
|
+
await writeLedger(root, ledger);
|
|
140
|
+
}
|
|
141
|
+
await writeLedger(root, ledger);
|
|
142
|
+
return { written, removed, skippedSources, stoppedBy };
|
|
143
|
+
}
|
|
144
|
+
async function listIndexFacts(root) {
|
|
145
|
+
return (await new MemoryFileStore(root).listFacts()).filter(
|
|
146
|
+
(fact) => fact.createdByAgent === INDEX_FACT_AGENT
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
async function pruneIndexFacts(root) {
|
|
150
|
+
const store = new MemoryFileStore(root);
|
|
151
|
+
const facts = await listIndexFacts(root);
|
|
152
|
+
for (const fact of facts) await store.deleteFact(fact.id);
|
|
153
|
+
await writeLedger(root, { version: 1, sources: {} });
|
|
154
|
+
return facts.length;
|
|
155
|
+
}
|
|
156
|
+
async function candidatesFor(root, path, text, config, signal) {
|
|
157
|
+
if (path === "package.json") return packageFacts(path, text);
|
|
158
|
+
if (path === "pyproject.toml") return pyprojectFacts(path, text);
|
|
159
|
+
if (path.startsWith(".github/workflows/")) return workflowFacts(path, text);
|
|
160
|
+
return proseFacts(root, path, stripManagedBlocks(text), config, signal);
|
|
161
|
+
}
|
|
162
|
+
function packageFacts(path, text) {
|
|
163
|
+
let manifest;
|
|
164
|
+
try {
|
|
165
|
+
manifest = JSON.parse(text);
|
|
166
|
+
} catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
const out = [];
|
|
170
|
+
const node = manifest.engines?.node;
|
|
171
|
+
if (node)
|
|
172
|
+
out.push({ path, line: lineOf(text, '"node"'), text: `The project requires Node.js ${node}.` });
|
|
173
|
+
for (const name of ["test", "lint", "typecheck", "build", "format"]) {
|
|
174
|
+
const script = manifest.scripts?.[name];
|
|
175
|
+
if (script) {
|
|
176
|
+
out.push({
|
|
177
|
+
path,
|
|
178
|
+
line: lineOf(text, `"${name}"`),
|
|
179
|
+
text: `Run \`npm run ${name}\` to ${VERB[name]} (it runs \`${script.slice(0, 120)}\`).`
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
var VERB = {
|
|
186
|
+
test: "run the tests",
|
|
187
|
+
lint: "lint the code",
|
|
188
|
+
typecheck: "type-check the code",
|
|
189
|
+
build: "build the project",
|
|
190
|
+
format: "format the code"
|
|
191
|
+
};
|
|
192
|
+
function pyprojectFacts(path, text) {
|
|
193
|
+
const out = [];
|
|
194
|
+
const python = text.match(/^\s*requires-python\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
195
|
+
if (python) {
|
|
196
|
+
out.push({
|
|
197
|
+
path,
|
|
198
|
+
line: lineOf(text, "requires-python"),
|
|
199
|
+
text: `The project supports Python ${python}.`
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
for (const [tool, what] of [
|
|
203
|
+
["ruff", "Ruff is the linter/formatter"],
|
|
204
|
+
["mypy", "mypy type-checks the code"],
|
|
205
|
+
["pyright", "Pyright type-checks the code"],
|
|
206
|
+
["pytest", "Tests run with pytest"]
|
|
207
|
+
]) {
|
|
208
|
+
const header = `[tool.${tool}`;
|
|
209
|
+
if (text.includes(header)) {
|
|
210
|
+
out.push({
|
|
211
|
+
path,
|
|
212
|
+
line: lineOf(text, header),
|
|
213
|
+
text: `${what} (configured in pyproject.toml).`
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
var CHECK_COMMAND = /\b(test|tests|pytest|lint|ruff|mypy|pyright|tsc|typecheck|vitest|jest|biome|eslint|go vet|cargo (test|clippy)|coverage)\b/i;
|
|
220
|
+
function workflowFacts(path, text) {
|
|
221
|
+
let workflow;
|
|
222
|
+
try {
|
|
223
|
+
workflow = parseYaml(text) ?? {};
|
|
224
|
+
} catch {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
const commands = /* @__PURE__ */ new Set();
|
|
228
|
+
for (const job of Object.values(workflow.jobs ?? {})) {
|
|
229
|
+
for (const step of job?.steps ?? []) {
|
|
230
|
+
const run = typeof step?.run === "string" ? step.run.split("\n")[0]?.trim() : void 0;
|
|
231
|
+
if (run && CHECK_COMMAND.test(run) && run.length <= 120) commands.add(run);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const triggers = JSON.stringify(workflow.on ?? workflow.true ?? "");
|
|
235
|
+
const gates = triggers.includes("pull_request");
|
|
236
|
+
return [...commands].slice(0, 4).map((command) => ({
|
|
237
|
+
path,
|
|
238
|
+
line: lineOf(text, command),
|
|
239
|
+
text: `CI (${workflow.name ?? path}) runs \`${command}\`${gates ? " on pull requests; it must pass before merging" : ""}.`
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
var proseSchema = z.object({
|
|
243
|
+
facts: z.array(z.object({ line: z.number().int(), fact: z.string() })).default([])
|
|
244
|
+
});
|
|
245
|
+
var FACT_GRAMMAR = {
|
|
246
|
+
type: "object",
|
|
247
|
+
properties: {
|
|
248
|
+
facts: {
|
|
249
|
+
type: "array",
|
|
250
|
+
maxItems: 6,
|
|
251
|
+
items: {
|
|
252
|
+
type: "object",
|
|
253
|
+
properties: { line: { type: "integer" }, fact: { type: "string", maxLength: 240 } }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
var SYSTEM = [
|
|
259
|
+
"You extract durable project facts from a repository document for a team memory.",
|
|
260
|
+
'Answer with JSON only: {"facts":[{"line":12,"fact":"..."}]}.',
|
|
261
|
+
"Keep only lasting rules and facts a developer must know: conventions, required checks, supported versions, deprecation and release policy, how to run or test the project.",
|
|
262
|
+
"Each fact is one short sentence in the document's own words; line is the number of the line it comes from.",
|
|
263
|
+
"Skip marketing, installation steps for end users, links, badges and anything temporary. Give at most 6 facts; none is fine."
|
|
264
|
+
].join(" ");
|
|
265
|
+
async function proseFacts(root, path, text, config, signal) {
|
|
266
|
+
const lines = text.split(/\r?\n/);
|
|
267
|
+
const out = [];
|
|
268
|
+
for (const section of sections(lines)) {
|
|
269
|
+
if (signal?.aborted || out.length >= PER_SOURCE) break;
|
|
270
|
+
if (config.local) await waitForForeground(root, signal);
|
|
271
|
+
const numbered = section.map(({ line, text: text2 }) => `${line}: ${redactSensitive(text2)}`).join("\n");
|
|
272
|
+
const result = await chatJson(config, {
|
|
273
|
+
system: SYSTEM,
|
|
274
|
+
user: `File: ${path}
|
|
275
|
+
|
|
276
|
+
${numbered}`,
|
|
277
|
+
schema: proseSchema,
|
|
278
|
+
jsonSchema: FACT_GRAMMAR,
|
|
279
|
+
maxTokens: 500,
|
|
280
|
+
deadlineMs: config.local ? TIMEOUT_MS.local : TIMEOUT_MS.hosted,
|
|
281
|
+
purpose: "background",
|
|
282
|
+
repositoryRoot: root
|
|
283
|
+
});
|
|
284
|
+
if (!result.ok) continue;
|
|
285
|
+
for (const fact of result.value.facts) {
|
|
286
|
+
const sentence = fact.fact.trim().replace(/\s+/g, " ").slice(0, 240);
|
|
287
|
+
if (isStatement(sentence) && hasLineEvidence(lines, fact.line, sentence)) {
|
|
288
|
+
out.push({ path, line: fact.line, text: sentence });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
function isStatement(text) {
|
|
295
|
+
if (/```|^\$\s|^(cd|npm|npx|pip|git|make|curl)\s/i.test(text)) return false;
|
|
296
|
+
return (text.match(new RegExp("\\p{L}{2,}", "gu")) ?? []).length >= 5;
|
|
297
|
+
}
|
|
298
|
+
function hasLineEvidence(lines, line, fact) {
|
|
299
|
+
if (line < 1 || line > lines.length) return false;
|
|
300
|
+
const nearby = new Set(
|
|
301
|
+
lines.slice(Math.max(0, line - 3), line + 2).join(" ").toLowerCase().match(/[\p{L}\p{N}_]{4,}/gu) ?? []
|
|
302
|
+
);
|
|
303
|
+
const words = [...new Set(fact.toLowerCase().match(/[\p{L}\p{N}_]{4,}/gu) ?? [])];
|
|
304
|
+
const shared = words.filter((word) => nearby.has(word)).length;
|
|
305
|
+
return shared >= Math.min(2, words.length) && shared > 0;
|
|
306
|
+
}
|
|
307
|
+
function sections(lines) {
|
|
308
|
+
const out = [];
|
|
309
|
+
let current = [];
|
|
310
|
+
let size = 0;
|
|
311
|
+
lines.forEach((text, index) => {
|
|
312
|
+
const heading = /^#{1,3}\s/.test(text);
|
|
313
|
+
if (current.length > 0 && (heading || size + text.length > SECTION_CHARS)) {
|
|
314
|
+
out.push(current);
|
|
315
|
+
current = [];
|
|
316
|
+
size = 0;
|
|
317
|
+
}
|
|
318
|
+
if (text.trim()) {
|
|
319
|
+
current.push({ line: index + 1, text: text.slice(0, 400) });
|
|
320
|
+
size += Math.min(text.length, 400) + 6;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
if (current.length > 0) out.push(current);
|
|
324
|
+
return out.slice(0, 12);
|
|
325
|
+
}
|
|
326
|
+
function stripManagedBlocks(text) {
|
|
327
|
+
return text.replace(
|
|
328
|
+
/<!-- codebuddy:[\w-]+:start -->[\s\S]*?<!-- codebuddy:[\w-]+:end -->/g,
|
|
329
|
+
(block) => block.replace(/[^\n]/g, "")
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
function lineOf(text, needle) {
|
|
333
|
+
const index = text.indexOf(needle);
|
|
334
|
+
return index < 0 ? 1 : text.slice(0, index).split("\n").length;
|
|
335
|
+
}
|
|
336
|
+
function sha(text) {
|
|
337
|
+
return createHash("sha256").update(text).digest("hex");
|
|
338
|
+
}
|
|
339
|
+
function ledgerPath(root) {
|
|
340
|
+
return join(root, ".codebuddy", "cache", "index-facts.json");
|
|
341
|
+
}
|
|
342
|
+
async function readLedger(root) {
|
|
343
|
+
try {
|
|
344
|
+
const parsed = JSON.parse(await readFile(ledgerPath(root), "utf8"));
|
|
345
|
+
if (parsed.version === 1 && parsed.sources) return parsed;
|
|
346
|
+
} catch {
|
|
347
|
+
}
|
|
348
|
+
return { version: 1, sources: {} };
|
|
349
|
+
}
|
|
350
|
+
async function writeLedger(root, ledger) {
|
|
351
|
+
await mkdir(dirname(ledgerPath(root)), { recursive: true });
|
|
352
|
+
await writeAtomic(ledgerPath(root), JSON.stringify(ledger));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export {
|
|
356
|
+
INDEX_FACT_AGENT,
|
|
357
|
+
MAX_FACTS,
|
|
358
|
+
factSources,
|
|
359
|
+
extractIndexFacts,
|
|
360
|
+
listIndexFacts,
|
|
361
|
+
pruneIndexFacts,
|
|
362
|
+
packageFacts,
|
|
363
|
+
pyprojectFacts,
|
|
364
|
+
workflowFacts,
|
|
365
|
+
isStatement,
|
|
366
|
+
hasLineEvidence
|
|
367
|
+
};
|
|
368
|
+
//# sourceMappingURL=chunk-BNZDEU5K.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/llm/index-facts.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdir, readdir, readFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport { z } from \"zod\";\nimport { resolveNamespaceFrom } from \"../core/config-file.js\";\nimport { generateEntityId } from \"../core/ids.js\";\nimport { writeAtomic } from \"../core/markdown-store-fs.js\";\nimport { MemoryFileStore } from \"../core/memory-file-store.js\";\nimport { termOverlap } from \"../core/memory-search.js\";\nimport { redactSensitive, scanSensitive } from \"../core/sensitive.js\";\nimport { chatJson } from \"./client.js\";\nimport { type EnabledLlmConfig, resolveLlmConfig } from \"./config.js\";\nimport { waitForForeground } from \"./local.js\";\n\n/**\n * Project facts drawn from the repository at index time, so a fresh project\n * does not start with empty memory: supported versions and the test/lint\n * commands from manifests (read deterministically), required CI checks from\n * workflows, and rules from README / CONTRIBUTING / AGENTS.md / CLAUDE.md and\n * deprecation or release policies (read by the small model, local or BYOK).\n *\n * Every fact keeps its source: `paths: [file]`, `verification: [\"file:Lline\"]`,\n * and a line the fact's words must actually appear near. They are ordinary\n * Markdown facts (the durable store `remember` writes too), marked\n * `createdByAgent: \"codebuddy-index\"`; re-extraction only ever replaces facts\n * with that origin whose source file changed, never a human or agent fact.\n * Sensitive text is dropped, not redacted. At most `MAX_FACTS` per repository.\n */\nexport const INDEX_FACT_AGENT = \"codebuddy-index\";\nexport const MAX_FACTS = 40;\nconst PER_SOURCE = 8;\nconst SECTION_CHARS = 1_600;\n/** A fact this close to an existing one (Jaccard of content words) is a duplicate. */\nconst DUPLICATE_OVERLAP = 0.8;\nconst TIMEOUT_MS = { local: 60_000, hosted: 20_000 } as const;\n\nexport type IndexFactsResult = {\n written: number;\n removed: number;\n skippedSources: number;\n stoppedBy: \"done\" | \"disabled\" | \"cap\" | \"aborted\";\n};\n\ntype Candidate = { path: string; line: number; text: string };\ntype Ledger = { version: 1; sources: Record<string, string> };\n\nconst PROSE_SOURCES = /^(README|CONTRIBUTING|AGENTS|CLAUDE)(\\.[a-z]+)?$/i;\nconst POLICY_DOC =\n /(deprecat|release[-_ ]?(policy|process)|versioning|support[-_ ]?policy|compatib)/i;\n\n/** Files facts are read from, relative to the root, in priority order. */\nexport async function factSources(root: string): Promise<string[]> {\n const top = await readdir(root).catch(() => [] as string[]);\n const docs = await readdir(join(root, \"docs\"), { recursive: true }).catch(() => [] as string[]);\n const workflows = await readdir(join(root, \".github\", \"workflows\")).catch(() => [] as string[]);\n return [\n ...[\"package.json\", \"pyproject.toml\"].filter((name) => top.includes(name)),\n ...top.filter((name) => PROSE_SOURCES.test(name)).sort(),\n ...workflows\n .filter((name) => /\\.ya?ml$/.test(name))\n .sort()\n .map((name) => `.github/workflows/${name}`),\n ...docs\n .map((name) => name.split(\"\\\\\").join(\"/\"))\n .filter((name) => /\\.(md|rst|txt)$/i.test(name) && POLICY_DOC.test(name))\n .sort()\n .slice(0, 4)\n .map((name) => `docs/${name}`),\n ];\n}\n\n/**\n * Extract (or refresh) index facts. Sources whose content hash is unchanged\n * are skipped; a changed or deleted source has its old index facts removed\n * first. Without an enabled model layer nothing happens (2.14 behaviour).\n */\nexport async function extractIndexFacts(\n root: string,\n options: { config?: EnabledLlmConfig; signal?: AbortSignal; namespace?: string } = {},\n): Promise<IndexFactsResult> {\n const resolved = options.config ?? (await resolveLlmConfig(root));\n if (!resolved.enabled || resolved.features.facts === false) {\n return { written: 0, removed: 0, skippedSources: 0, stoppedBy: \"disabled\" };\n }\n const config = resolved;\n const namespace = options.namespace ?? (await resolveNamespaceFrom(root));\n const store = new MemoryFileStore(root);\n const ledger = await readLedger(root);\n const sources = await factSources(root);\n let facts = await store.listFacts();\n let removed = 0;\n const removeFrom = async (path: string) => {\n for (const fact of facts) {\n if (fact.createdByAgent === INDEX_FACT_AGENT && fact.paths[0] === path) {\n await store.deleteFact(fact.id);\n removed++;\n }\n }\n facts = await store.listFacts();\n };\n for (const path of Object.keys(ledger.sources)) {\n if (!sources.includes(path)) {\n await removeFrom(path);\n delete ledger.sources[path];\n }\n }\n\n let written = 0;\n let skippedSources = 0;\n let stoppedBy: IndexFactsResult[\"stoppedBy\"] = \"done\";\n const indexFacts = () => facts.filter((fact) => fact.createdByAgent === INDEX_FACT_AGENT).length;\n for (const path of sources) {\n if (options.signal?.aborted) {\n stoppedBy = \"aborted\";\n break;\n }\n const text = await readFile(join(root, path), \"utf8\").catch(() => null);\n if (text === null) continue;\n const hash = sha(text);\n if (ledger.sources[path] === hash) {\n skippedSources++;\n continue;\n }\n await removeFrom(path);\n if (indexFacts() >= MAX_FACTS) {\n stoppedBy = \"cap\";\n break;\n }\n const candidates = await candidatesFor(root, path, text, config, options.signal);\n let fromSource = 0;\n for (const candidate of candidates) {\n if (fromSource >= PER_SOURCE || indexFacts() >= MAX_FACTS) break;\n if (scanSensitive(candidate.text).sensitive) continue;\n // Never a second copy of what a human, agent or earlier source already said.\n if (\n facts.some(\n (fact) =>\n Math.max(\n termOverlap(candidate.text, fact.object),\n termOverlap(candidate.text, fact.content),\n ) >= DUPLICATE_OVERLAP,\n )\n ) {\n continue;\n }\n const id = generateEntityId(\"fact\");\n const content = `${candidate.text} (source: ${path}:L${candidate.line})`;\n await store.writeFact({\n id,\n namespace,\n subject: path,\n predicate: \"project-fact\",\n object: candidate.text,\n confidence: 0.8,\n createdAt: new Date().toISOString(),\n createdByAgent: INDEX_FACT_AGENT,\n sourceInteractionId: null,\n sourceDeleted: false,\n content,\n paths: [path],\n verification: [`${path}:L${candidate.line}`],\n idempotencyKey: `index:${path}:${sha(candidate.text).slice(0, 16)}`.slice(0, 200),\n fingerprint: sha(`${namespace}\\n${content}`),\n });\n facts = await store.listFacts();\n written++;\n fromSource++;\n }\n ledger.sources[path] = hash;\n await writeLedger(root, ledger);\n }\n await writeLedger(root, ledger);\n return { written, removed, skippedSources, stoppedBy };\n}\n\n/** Index facts currently stored (for `memory list|prune --origin index`). */\nexport async function listIndexFacts(root: string) {\n return (await new MemoryFileStore(root).listFacts()).filter(\n (fact) => fact.createdByAgent === INDEX_FACT_AGENT,\n );\n}\n\n/** Delete every index fact and forget the source hashes, so the next pass re-extracts. */\nexport async function pruneIndexFacts(root: string): Promise<number> {\n const store = new MemoryFileStore(root);\n const facts = await listIndexFacts(root);\n for (const fact of facts) await store.deleteFact(fact.id);\n await writeLedger(root, { version: 1, sources: {} });\n return facts.length;\n}\n\nasync function candidatesFor(\n root: string,\n path: string,\n text: string,\n config: EnabledLlmConfig,\n signal: AbortSignal | undefined,\n): Promise<Candidate[]> {\n if (path === \"package.json\") return packageFacts(path, text);\n if (path === \"pyproject.toml\") return pyprojectFacts(path, text);\n if (path.startsWith(\".github/workflows/\")) return workflowFacts(path, text);\n return proseFacts(root, path, stripManagedBlocks(text), config, signal);\n}\n\n/** Supported Node version and the project's own check commands. */\nexport function packageFacts(path: string, text: string): Candidate[] {\n let manifest: { engines?: Record<string, string>; scripts?: Record<string, string> };\n try {\n manifest = JSON.parse(text);\n } catch {\n return [];\n }\n const out: Candidate[] = [];\n const node = manifest.engines?.node;\n if (node)\n out.push({ path, line: lineOf(text, '\"node\"'), text: `The project requires Node.js ${node}.` });\n for (const name of [\"test\", \"lint\", \"typecheck\", \"build\", \"format\"]) {\n const script = manifest.scripts?.[name];\n if (script) {\n out.push({\n path,\n line: lineOf(text, `\"${name}\"`),\n text: `Run \\`npm run ${name}\\` to ${VERB[name]} (it runs \\`${script.slice(0, 120)}\\`).`,\n });\n }\n }\n return out;\n}\nconst VERB: Record<string, string> = {\n test: \"run the tests\",\n lint: \"lint the code\",\n typecheck: \"type-check the code\",\n build: \"build the project\",\n format: \"format the code\",\n};\n\n/** Supported Python versions and the configured tools. */\nexport function pyprojectFacts(path: string, text: string): Candidate[] {\n const out: Candidate[] = [];\n const python = text.match(/^\\s*requires-python\\s*=\\s*[\"']([^\"']+)[\"']/m)?.[1];\n if (python) {\n out.push({\n path,\n line: lineOf(text, \"requires-python\"),\n text: `The project supports Python ${python}.`,\n });\n }\n for (const [tool, what] of [\n [\"ruff\", \"Ruff is the linter/formatter\"],\n [\"mypy\", \"mypy type-checks the code\"],\n [\"pyright\", \"Pyright type-checks the code\"],\n [\"pytest\", \"Tests run with pytest\"],\n ] as const) {\n const header = `[tool.${tool}`;\n if (text.includes(header)) {\n out.push({\n path,\n line: lineOf(text, header),\n text: `${what} (configured in pyproject.toml).`,\n });\n }\n }\n return out;\n}\n\nconst CHECK_COMMAND =\n /\\b(test|tests|pytest|lint|ruff|mypy|pyright|tsc|typecheck|vitest|jest|biome|eslint|go vet|cargo (test|clippy)|coverage)\\b/i;\n\n/** CI's required checks: the run steps that test, lint or type-check. */\nexport function workflowFacts(path: string, text: string): Candidate[] {\n let workflow: {\n name?: string;\n on?: unknown;\n jobs?: Record<string, { steps?: Array<{ run?: string }> }>;\n };\n try {\n workflow = parseYaml(text) ?? {};\n } catch {\n return [];\n }\n const commands = new Set<string>();\n for (const job of Object.values(workflow.jobs ?? {})) {\n for (const step of job?.steps ?? []) {\n const run = typeof step?.run === \"string\" ? step.run.split(\"\\n\")[0]?.trim() : undefined;\n if (run && CHECK_COMMAND.test(run) && run.length <= 120) commands.add(run);\n }\n }\n // Only a workflow that runs on pull requests gates merging (a release job does not).\n // YAML 1.1 readers may parse the `on:` key as boolean true.\n const triggers = JSON.stringify(workflow.on ?? (workflow as Record<string, unknown>).true ?? \"\");\n const gates = triggers.includes(\"pull_request\");\n return [...commands].slice(0, 4).map((command) => ({\n path,\n line: lineOf(text, command),\n text: `CI (${workflow.name ?? path}) runs \\`${command}\\`${gates ? \" on pull requests; it must pass before merging\" : \"\"}.`,\n }));\n}\n\nconst proseSchema = z.object({\n facts: z.array(z.object({ line: z.number().int(), fact: z.string() })).default([]),\n});\n\n/** Grammar bounds for the local model (see summaries.ts CARD_GRAMMAR). */\nconst FACT_GRAMMAR = {\n type: \"object\",\n properties: {\n facts: {\n type: \"array\",\n maxItems: 6,\n items: {\n type: \"object\",\n properties: { line: { type: \"integer\" }, fact: { type: \"string\", maxLength: 240 } },\n },\n },\n },\n};\n\nconst SYSTEM = [\n \"You extract durable project facts from a repository document for a team memory.\",\n 'Answer with JSON only: {\"facts\":[{\"line\":12,\"fact\":\"...\"}]}.',\n \"Keep only lasting rules and facts a developer must know: conventions, required checks, supported versions, deprecation and release policy, how to run or test the project.\",\n \"Each fact is one short sentence in the document's own words; line is the number of the line it comes from.\",\n \"Skip marketing, installation steps for end users, links, badges and anything temporary. Give at most 6 facts; none is fine.\",\n].join(\" \");\n\nasync function proseFacts(\n root: string,\n path: string,\n text: string,\n config: EnabledLlmConfig,\n signal: AbortSignal | undefined,\n): Promise<Candidate[]> {\n const lines = text.split(/\\r?\\n/);\n const out: Candidate[] = [];\n for (const section of sections(lines)) {\n if (signal?.aborted || out.length >= PER_SOURCE) break;\n if (config.local) await waitForForeground(root, signal);\n const numbered = section\n .map(({ line, text }) => `${line}: ${redactSensitive(text)}`)\n .join(\"\\n\");\n const result = await chatJson(config, {\n system: SYSTEM,\n user: `File: ${path}\\n\\n${numbered}`,\n schema: proseSchema,\n jsonSchema: FACT_GRAMMAR,\n maxTokens: 500,\n deadlineMs: config.local ? TIMEOUT_MS.local : TIMEOUT_MS.hosted,\n purpose: \"background\",\n repositoryRoot: root,\n });\n if (!result.ok) continue;\n for (const fact of result.value.facts) {\n const sentence = fact.fact.trim().replace(/\\s+/g, \" \").slice(0, 240);\n if (isStatement(sentence) && hasLineEvidence(lines, fact.line, sentence)) {\n out.push({ path, line: fact.line, text: sentence });\n }\n }\n }\n return out;\n}\n\n/**\n * A fact is a statement: at least five words of prose, not a shell line, a\n * code fence or a bare heading the small model copied out of a README.\n */\nexport function isStatement(text: string): boolean {\n if (/```|^\\$\\s|^(cd|npm|npx|pip|git|make|curl)\\s/i.test(text)) return false;\n return (text.match(/\\p{L}{2,}/gu) ?? []).length >= 5;\n}\n\n/**\n * The model's fact must be supported by its cited line (± 2): at least two of\n * its content words (≥ 4 letters) appear there, or all of them for a short fact.\n */\nexport function hasLineEvidence(lines: string[], line: number, fact: string): boolean {\n if (line < 1 || line > lines.length) return false;\n const nearby = new Set(\n lines\n .slice(Math.max(0, line - 3), line + 2)\n .join(\" \")\n .toLowerCase()\n .match(/[\\p{L}\\p{N}_]{4,}/gu) ?? [],\n );\n const words = [...new Set(fact.toLowerCase().match(/[\\p{L}\\p{N}_]{4,}/gu) ?? [])];\n const shared = words.filter((word) => nearby.has(word)).length;\n return shared >= Math.min(2, words.length) && shared > 0;\n}\n\n/** Heading-delimited chunks of at most `SECTION_CHARS`, with 1-based line numbers. */\nfunction sections(lines: string[]): Array<Array<{ line: number; text: string }>> {\n const out: Array<Array<{ line: number; text: string }>> = [];\n let current: Array<{ line: number; text: string }> = [];\n let size = 0;\n lines.forEach((text, index) => {\n const heading = /^#{1,3}\\s/.test(text);\n if (current.length > 0 && (heading || size + text.length > SECTION_CHARS)) {\n out.push(current);\n current = [];\n size = 0;\n }\n if (text.trim()) {\n current.push({ line: index + 1, text: text.slice(0, 400) });\n size += Math.min(text.length, 400) + 6;\n }\n });\n if (current.length > 0) out.push(current);\n // A long README: the first sections carry the rules that matter.\n return out.slice(0, 12);\n}\n\n/** CodeBuddy's own managed blocks in CLAUDE.md / AGENTS.md are not project facts. */\nfunction stripManagedBlocks(text: string): string {\n // Blank the block (keeping its line breaks) so cited line numbers stay true.\n return text.replace(\n /<!-- codebuddy:[\\w-]+:start -->[\\s\\S]*?<!-- codebuddy:[\\w-]+:end -->/g,\n (block) => block.replace(/[^\\n]/g, \"\"),\n );\n}\n\nfunction lineOf(text: string, needle: string): number {\n const index = text.indexOf(needle);\n return index < 0 ? 1 : text.slice(0, index).split(\"\\n\").length;\n}\n\nfunction sha(text: string): string {\n return createHash(\"sha256\").update(text).digest(\"hex\");\n}\n\nfunction ledgerPath(root: string): string {\n return join(root, \".codebuddy\", \"cache\", \"index-facts.json\");\n}\n\nasync function readLedger(root: string): Promise<Ledger> {\n try {\n const parsed = JSON.parse(await readFile(ledgerPath(root), \"utf8\")) as Ledger;\n if (parsed.version === 1 && parsed.sources) return parsed;\n } catch {\n // Missing: every source is new.\n }\n return { version: 1, sources: {} };\n}\n\nasync function writeLedger(root: string, ledger: Ledger): Promise<void> {\n await mkdir(dirname(ledgerPath(root)), { recursive: true });\n await writeAtomic(ledgerPath(root), JSON.stringify(ledger));\n}\n"],"mappings":"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","names":["text"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/core/ids.ts
|
|
2
|
+
import { createHash, randomBytes } from "crypto";
|
|
3
|
+
var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
4
|
+
function generateUlid(now = Date.now()) {
|
|
5
|
+
let timePart = "";
|
|
6
|
+
let ts = now;
|
|
7
|
+
for (let i = 0; i < 10; i++) {
|
|
8
|
+
timePart = (CROCKFORD[ts % 32] ?? "0") + timePart;
|
|
9
|
+
ts = Math.floor(ts / 32);
|
|
10
|
+
}
|
|
11
|
+
const bytes = randomBytes(10);
|
|
12
|
+
let randPart = "";
|
|
13
|
+
for (let i = 0; i < 16; i++) {
|
|
14
|
+
const bitOffset = i * 5;
|
|
15
|
+
const byteIndex = Math.floor(bitOffset / 8);
|
|
16
|
+
const bitInByte = bitOffset % 8;
|
|
17
|
+
const hi = bytes[byteIndex] ?? 0;
|
|
18
|
+
const lo = bytes[byteIndex + 1] ?? 0;
|
|
19
|
+
const combined = (hi << 8 | lo) >>> 0;
|
|
20
|
+
const shift = 16 - bitInByte - 5;
|
|
21
|
+
const value = combined >>> shift & 31;
|
|
22
|
+
randPart += CROCKFORD[value] ?? "0";
|
|
23
|
+
}
|
|
24
|
+
return timePart + randPart;
|
|
25
|
+
}
|
|
26
|
+
function generateSessionId(now) {
|
|
27
|
+
return `sess_${generateUlid(now).toLowerCase()}`;
|
|
28
|
+
}
|
|
29
|
+
function generateEntityId(prefix, now) {
|
|
30
|
+
return `${prefix}_${generateUlid(now).toLowerCase()}`;
|
|
31
|
+
}
|
|
32
|
+
function computeContentHash(namespace, sessionId, content) {
|
|
33
|
+
return createHash("sha256").update(`${namespace}${sessionId}${content}`).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
function computeFactHash(namespace, content) {
|
|
36
|
+
return createHash("sha256").update(`${namespace}fact${content}`).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
generateSessionId,
|
|
41
|
+
generateEntityId,
|
|
42
|
+
computeContentHash,
|
|
43
|
+
computeFactHash
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=chunk-EMWGLKPR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/ids.ts"],"sourcesContent":["import { createHash, randomBytes } from \"node:crypto\";\n\nconst CROCKFORD = \"0123456789ABCDEFGHJKMNPQRSTVWXYZ\";\n\nexport function generateUlid(now: number = Date.now()): string {\n let timePart = \"\";\n let ts = now;\n for (let i = 0; i < 10; i++) {\n timePart = (CROCKFORD[ts % 32] ?? \"0\") + timePart;\n ts = Math.floor(ts / 32);\n }\n\n const bytes = randomBytes(10);\n let randPart = \"\";\n for (let i = 0; i < 16; i++) {\n const bitOffset = i * 5;\n const byteIndex = Math.floor(bitOffset / 8);\n const bitInByte = bitOffset % 8;\n const hi = bytes[byteIndex] ?? 0;\n const lo = bytes[byteIndex + 1] ?? 0;\n const combined = ((hi << 8) | lo) >>> 0;\n const shift = 16 - bitInByte - 5;\n const value = (combined >>> shift) & 0x1f;\n randPart += CROCKFORD[value] ?? \"0\";\n }\n\n return timePart + randPart;\n}\n\nexport function generateSessionId(now?: number): string {\n return `sess_${generateUlid(now).toLowerCase()}`;\n}\n\nexport function generateEntityId(prefix: string, now?: number): string {\n return `${prefix}_${generateUlid(now).toLowerCase()}`;\n}\n\nexport function computeContentHash(namespace: string, sessionId: string, content: string): string {\n return createHash(\"sha256\").update(`${namespace}\\x1f${sessionId}\\x1f${content}`).digest(\"hex\");\n}\n\nexport function computeFactHash(namespace: string, content: string): string {\n return createHash(\"sha256\").update(`${namespace}\\x1ffact\\x1f${content}`).digest(\"hex\");\n}\n"],"mappings":";AAAA,SAAS,YAAY,mBAAmB;AAExC,IAAM,YAAY;AAEX,SAAS,aAAa,MAAc,KAAK,IAAI,GAAW;AAC7D,MAAI,WAAW;AACf,MAAI,KAAK;AACT,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,gBAAY,UAAU,KAAK,EAAE,KAAK,OAAO;AACzC,SAAK,KAAK,MAAM,KAAK,EAAE;AAAA,EACzB;AAEA,QAAM,QAAQ,YAAY,EAAE;AAC5B,MAAI,WAAW;AACf,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK;AAC3B,UAAM,YAAY,IAAI;AACtB,UAAM,YAAY,KAAK,MAAM,YAAY,CAAC;AAC1C,UAAM,YAAY,YAAY;AAC9B,UAAM,KAAK,MAAM,SAAS,KAAK;AAC/B,UAAM,KAAK,MAAM,YAAY,CAAC,KAAK;AACnC,UAAM,YAAa,MAAM,IAAK,QAAQ;AACtC,UAAM,QAAQ,KAAK,YAAY;AAC/B,UAAM,QAAS,aAAa,QAAS;AACrC,gBAAY,UAAU,KAAK,KAAK;AAAA,EAClC;AAEA,SAAO,WAAW;AACpB;AAEO,SAAS,kBAAkB,KAAsB;AACtD,SAAO,QAAQ,aAAa,GAAG,EAAE,YAAY,CAAC;AAChD;AAEO,SAAS,iBAAiB,QAAgB,KAAsB;AACrE,SAAO,GAAG,MAAM,IAAI,aAAa,GAAG,EAAE,YAAY,CAAC;AACrD;AAEO,SAAS,mBAAmB,WAAmB,WAAmB,SAAyB;AAChG,SAAO,WAAW,QAAQ,EAAE,OAAO,GAAG,SAAS,IAAO,SAAS,IAAO,OAAO,EAAE,EAAE,OAAO,KAAK;AAC/F;AAEO,SAAS,gBAAgB,WAAmB,SAAyB;AAC1E,SAAO,WAAW,QAAQ,EAAE,OAAO,GAAG,SAAS,SAAe,OAAO,EAAE,EAAE,OAAO,KAAK;AACvF;","names":[]}
|