@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/llm/card-lock.ts"],"sourcesContent":["import { link, mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * One model writer per repository. Setup (`codebuddy`) starts a detached\n * `index --cards`, and every `codebuddy serve` schedules its own pass: without\n * this each loads its own model copy, they oversubscribe the CPU until every\n * file times out, and each rewrites `llm-summaries.json` from its own copy.\n * The lock file holds the writer's PID; a dead PID is a stale lock.\n */\nexport function cardLockPath(root: string): string {\n return join(root, \".codebuddy\", \"cache\", \"cards.lock\");\n}\n\nexport type CardLockResult<T> = { ok: true; value: T } | { ok: false; holder: number | null };\n\n/** Run `fn` holding the lock, or return the live holder's PID without running it. */\nexport async function withCardLock<T>(\n root: string,\n fn: () => Promise<T>,\n): Promise<CardLockResult<T>> {\n const path = cardLockPath(root);\n await mkdir(dirname(path), { recursive: true });\n // Written whole, then linked: a reader never sees an empty lock file.\n const temporary = `${path}.${process.pid}`;\n await writeFile(temporary, `${process.pid}\\n`, { mode: 0o600 });\n try {\n for (let attempt = 0; ; attempt++) {\n try {\n await link(temporary, path);\n break;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n const holder = await cardLockHolder(root);\n if (holder !== null || attempt > 0) return { ok: false, holder };\n // ponytail: two processes clearing the same stale lock at once can both\n // win; needs a rename-and-recheck if that window ever matters.\n await rm(path, { force: true });\n }\n }\n } finally {\n await rm(temporary, { force: true });\n }\n try {\n return { ok: true, value: await fn() };\n } finally {\n if ((await readPid(path)) === process.pid) await rm(path, { force: true });\n }\n}\n\n/** The live process writing cards, or null (no lock, or its process is gone). */\nexport async function cardLockHolder(root: string): Promise<number | null> {\n const pid = await readPid(cardLockPath(root));\n return pid !== null && isAlive(pid) ? pid : null;\n}\n\nfunction isAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\nasync function readPid(path: string): Promise<number | null> {\n const pid = Number.parseInt(await readFile(path, \"utf8\").catch(() => \"\"), 10);\n return Number.isInteger(pid) && pid > 0 ? pid : null;\n}\n"],"mappings":";AAAA,SAAS,MAAM,OAAO,UAAU,IAAI,iBAAiB;AACrD,SAAS,SAAS,YAAY;AASvB,SAAS,aAAa,MAAsB;AACjD,SAAO,KAAK,MAAM,cAAc,SAAS,YAAY;AACvD;AAKA,eAAsB,aACpB,MACA,IAC4B;AAC5B,QAAM,OAAO,aAAa,IAAI;AAC9B,QAAM,MAAM,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAE9C,QAAM,YAAY,GAAG,IAAI,IAAI,QAAQ,GAAG;AACxC,QAAM,UAAU,WAAW,GAAG,QAAQ,GAAG;AAAA,GAAM,EAAE,MAAM,IAAM,CAAC;AAC9D,MAAI;AACF,aAAS,UAAU,KAAK,WAAW;AACjC,UAAI;AACF,cAAM,KAAK,WAAW,IAAI;AAC1B;AAAA,MACF,SAAS,OAAO;AACd,YAAK,MAAgC,SAAS,SAAU,OAAM;AAC9D,cAAM,SAAS,MAAM,eAAe,IAAI;AACxC,YAAI,WAAW,QAAQ,UAAU,EAAG,QAAO,EAAE,IAAI,OAAO,OAAO;AAG/D,cAAM,GAAG,MAAM,EAAE,OAAO,KAAK,CAAC;AAAA,MAChC;AAAA,IACF;AAAA,EACF,UAAE;AACA,UAAM,GAAG,WAAW,EAAE,OAAO,KAAK,CAAC;AAAA,EACrC;AACA,MAAI;AACF,WAAO,EAAE,IAAI,MAAM,OAAO,MAAM,GAAG,EAAE;AAAA,EACvC,UAAE;AACA,QAAK,MAAM,QAAQ,IAAI,MAAO,QAAQ,IAAK,OAAM,GAAG,MAAM,EAAE,OAAO,KAAK,CAAC;AAAA,EAC3E;AACF;AAGA,eAAsB,eAAe,MAAsC;AACzE,QAAM,MAAM,MAAM,QAAQ,aAAa,IAAI,CAAC;AAC5C,SAAO,QAAQ,QAAQ,QAAQ,GAAG,IAAI,MAAM;AAC9C;AAEA,SAAS,QAAQ,KAAsB;AACrC,MAAI;AACF,YAAQ,KAAK,KAAK,CAAC;AACnB,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAQ,MAAgC,SAAS;AAAA,EACnD;AACF;AAEA,eAAe,QAAQ,MAAsC;AAC3D,QAAM,MAAM,OAAO,SAAS,MAAM,SAAS,MAAM,MAAM,EAAE,MAAM,MAAM,EAAE,GAAG,EAAE;AAC5E,SAAO,OAAO,UAAU,GAAG,KAAK,MAAM,IAAI,MAAM;AAClD;","names":[]}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
countTokens
|
|
2
|
+
countTokens
|
|
3
|
+
} from "./chunk-57642NGU.js";
|
|
4
|
+
import {
|
|
3
5
|
generateEntityId
|
|
4
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-EMWGLKPR.js";
|
|
5
7
|
import {
|
|
6
8
|
writeAtomic
|
|
7
9
|
} from "./chunk-CG5B3LYG.js";
|
|
@@ -331,4 +333,4 @@ export {
|
|
|
331
333
|
coveringTestCandidates,
|
|
332
334
|
detectTestCommand
|
|
333
335
|
};
|
|
334
|
-
//# sourceMappingURL=chunk-
|
|
336
|
+
//# sourceMappingURL=chunk-7IPSDDO5.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/savings/tokens.ts","../src/context/session-ledger.ts","../src/context/tests.ts"],"sourcesContent":["import { countTokens } from \"../planner/budget.js\";\n\n/**\n * The one place token counts are computed. Strings use the same deterministic\n * cl100k tokenizer as planner budgets; objects are measured by their JSON\n * serialization, which matches how they travel over MCP.\n */\n\nexport function estimateTokens(value: unknown): number {\n if (value === undefined || value === null) return 0;\n const text = typeof value === \"string\" ? value : JSON.stringify(value);\n if (!text) return 0;\n return countTokens(text);\n}\n","import { createHash } from \"node:crypto\";\nimport { mkdir, readdir, readFile, rm, stat } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { generateEntityId } from \"../core/ids.js\";\nimport { writeAtomic } from \"../core/markdown-store-fs.js\";\nimport { estimateTokens } from \"../savings/tokens.js\";\n\nconst VERSION = 2;\nconst MAX_CAPSULES = 128;\nconst MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;\nconst MAX_PENDING = 8;\nconst PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1000;\nconst STALE_LOCK_MS = 60_000;\n/** Acknowledged versions kept per section, so switching back to a task reuses them. */\nconst MAX_ACKNOWLEDGED_PER_KEY = 8;\n\ntype LedgerRecord = { hash: string; lastSentAt: string };\n/** A delivery sent to a client that has not yet confirmed receipt. */\ntype PendingDelivery = { createdAt: string; records: Record<string, string> };\n/** One acknowledged version of an `exchange` section: content hash and when it was sent. */\ntype AcknowledgedVersion = { hash: string; sentAt: string };\ntype LedgerFile = {\n version: 2;\n updatedAt: string;\n /** `deliver`/`markSent` capsules (hooks): one current hash per key. */\n records: Record<string, LedgerRecord>;\n pending?: Record<string, PendingDelivery>;\n /** `exchange` sections (MCP): every recently acknowledged version, newest first. */\n acknowledged?: Record<string, AcknowledgedVersion[]>;\n};\n\nexport type ContextCapsule = { key: string; text: string };\nexport type CapsuleDelivery = {\n /** Full text of new or changed capsules, in input order. */\n lines: string[];\n /** Keys of capsules already sent this session and not resent. */\n unchanged: string[];\n full: number;\n referenced: number;\n sentTokens: number;\n avoidedTokens: number;\n};\n\nexport type SectionHash = { key: string; hash: string };\nexport type AcknowledgedExchange = {\n deliveryId: string;\n /** Keys whose content the client confirmed receiving and that have not changed. */\n unchanged: string[];\n ack: \"none\" | \"accepted\" | \"unknown\";\n};\n\nexport class SessionCapsuleLedger {\n private readonly directory: string;\n\n constructor(repositoryRoot = process.cwd()) {\n this.directory = join(resolve(repositoryRoot), \".codebuddy\", \"cache\", \"session\");\n }\n\n async deliver(\n sessionId: string,\n capsules: ContextCapsule[],\n now = new Date(),\n ): Promise<CapsuleDelivery> {\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n const path = this.path(sessionId);\n const ledger = await this.read(path, now);\n const lines: string[] = [];\n const unchanged: string[] = [];\n let full = 0;\n let referenced = 0;\n let sentTokens = 0;\n let avoidedTokens = 0;\n\n for (const capsule of capsules) {\n const hash = digest(capsule.text);\n const existing = ledger.records[capsule.key];\n if (existing?.hash === hash) {\n unchanged.push(capsule.key);\n referenced++;\n avoidedTokens += estimateTokens(capsule.text);\n } else {\n lines.push(capsule.text);\n full++;\n sentTokens += estimateTokens(capsule.text);\n }\n ledger.records[capsule.key] = { hash, lastSentAt: now.toISOString() };\n }\n\n await this.write(path, ledger, now);\n return { lines, unchanged, full, referenced, sentTokens, avoidedTokens };\n } finally {\n await release();\n }\n }\n\n /** Keys of capsules whose current text was already recorded as sent this session. */\n async seen(\n sessionId: string,\n capsules: ContextCapsule[],\n now = new Date(),\n ): Promise<Set<string>> {\n const ledger = await this.read(this.path(sessionId), now);\n return new Set(\n capsules\n .filter((capsule) => ledger.records[capsule.key]?.hash === digest(capsule.text))\n .map((capsule) => capsule.key),\n );\n }\n\n /** Whether anything was delivered in this session yet (a later prompt is a follow-up). */\n async delivered(sessionId: string, now = new Date()): Promise<boolean> {\n const ledger = await this.read(this.path(sessionId), now);\n return Object.keys(ledger.records).length > 0;\n }\n\n /**\n * Record capsules as delivered. Call only after their text actually reached\n * the client, so capsules that were clipped or failed to write stay unseen\n * and are delivered by a later prompt.\n */\n async markSent(sessionId: string, capsules: ContextCapsule[], now = new Date()): Promise<void> {\n if (capsules.length === 0) return;\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n const path = this.path(sessionId);\n const ledger = await this.read(path, now);\n for (const capsule of capsules) {\n ledger.records[capsule.key] = { hash: digest(capsule.text), lastSentAt: now.toISOString() };\n }\n await this.write(path, ledger, now);\n } finally {\n await release();\n }\n }\n\n /**\n * Acknowledged delivery for clients that can lose context (MCP). Unlike\n * `deliver`, sending marks nothing as seen: the sent hashes are held as a\n * pending delivery until the client echoes its `deliveryId` back as `ack` on\n * a later call. Only acknowledged, unchanged sections sent within `ttlMs` are\n * reported unchanged, so a dropped response or a compacted client context\n * fails safe to a full resend.\n *\n * A section keeps its last few acknowledged versions (by content hash), not\n * just the latest, so returning to an earlier task in the same session does\n * not resend what the client already holds. `full` means the client lost its\n * context: every acknowledged version and unconfirmed delivery is forgotten.\n */\n async exchange(\n sessionId: string,\n sections: SectionHash[],\n options: {\n ack?: string;\n ttlMs: number;\n /** The client lost its context: nothing acknowledged before this call counts. */\n full?: boolean;\n now?: Date;\n /** Finalize the response before recording hashes of the actual sent sections. */\n prepare?: (exchange: AcknowledgedExchange) => SectionHash[];\n },\n ): Promise<AcknowledgedExchange> {\n const now = options.now ?? new Date();\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n const path = this.path(sessionId);\n const ledger = await this.read(path, now);\n let pending = ledger.pending ?? {};\n let acknowledged = ledger.acknowledged ?? {};\n\n let ack: AcknowledgedExchange[\"ack\"] = \"none\";\n if (options.ack !== undefined) {\n const confirmed = pending[options.ack];\n if (confirmed) {\n for (const [key, hash] of Object.entries(confirmed.records)) {\n acknowledged[key] = [\n { hash, sentAt: confirmed.createdAt },\n ...(acknowledged[key] ?? []).filter((version) => version.hash !== hash),\n ]\n .sort((left, right) => right.sentAt.localeCompare(left.sentAt))\n .slice(0, MAX_ACKNOWLEDGED_PER_KEY);\n }\n delete pending[options.ack];\n ack = \"accepted\";\n } else {\n ack = \"unknown\";\n }\n }\n if (options.full) {\n acknowledged = {};\n pending = {};\n }\n\n const unchanged: string[] = [];\n const sent: Record<string, string> = {};\n for (const section of sections) {\n const held = acknowledged[section.key]?.some((version) => {\n const age = now.getTime() - Date.parse(version.sentAt);\n return version.hash === section.hash && age >= 0 && age <= options.ttlMs;\n });\n if (held) {\n unchanged.push(section.key);\n } else {\n sent[section.key] = section.hash;\n }\n }\n\n const deliveryId = generateEntityId(\"dlv\", now.getTime());\n const exchange = { deliveryId, unchanged, ack };\n const delivered = options.prepare?.(exchange);\n pending[deliveryId] = {\n createdAt: now.toISOString(),\n records: delivered\n ? Object.fromEntries(delivered.map(({ key, hash }) => [key, hash]))\n : sent,\n };\n ledger.pending = Object.fromEntries(\n Object.entries(pending)\n .filter(([, item]) => now.getTime() - Date.parse(item.createdAt) <= PENDING_MAX_AGE_MS)\n .sort((left, right) => right[1].createdAt.localeCompare(left[1].createdAt))\n .slice(0, MAX_PENDING),\n );\n ledger.acknowledged = Object.fromEntries(\n Object.entries(acknowledged)\n .map(([key, versions]): [string, AcknowledgedVersion[]] => [\n key,\n versions.filter((version) => now.getTime() - Date.parse(version.sentAt) <= MAX_AGE_MS),\n ])\n .filter(([, versions]) => versions.length > 0)\n .slice(0, MAX_CAPSULES),\n );\n await this.write(path, ledger, now);\n return { deliveryId, unchanged, ack };\n } finally {\n await release();\n }\n }\n\n /** Forget everything sent in a session, e.g. after the client compacted its context. */\n async reset(sessionId: string): Promise<void> {\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n await rm(this.path(sessionId), { force: true });\n } finally {\n await release();\n }\n }\n\n /** Remove expired disposable state; locked sessions are left for the next pass. */\n async pruneExpired(now = new Date()): Promise<number> {\n let removed = 0;\n for (const entry of await readdir(this.directory).catch(() => [])) {\n if (!entry.endsWith(\".json\")) continue;\n const path = join(this.directory, entry);\n try {\n await stat(`${path}.lock`);\n continue;\n } catch {\n // No active lock.\n }\n try {\n const metadata = await stat(path);\n if (now.getTime() - metadata.mtimeMs > MAX_AGE_MS) {\n await rm(path, { force: true });\n removed++;\n }\n } catch {\n // Another cleanup may have removed it already.\n }\n }\n return removed;\n }\n\n private async write(path: string, ledger: LedgerFile, now: Date): Promise<void> {\n ledger.updatedAt = now.toISOString();\n ledger.records = Object.fromEntries(\n Object.entries(ledger.records)\n .sort((left, right) => right[1].lastSentAt.localeCompare(left[1].lastSentAt))\n .slice(0, MAX_CAPSULES),\n );\n await writeAtomic(path, `${JSON.stringify(ledger, null, 2)}\\n`);\n }\n\n private async read(path: string, now: Date): Promise<LedgerFile> {\n try {\n const parsed = JSON.parse(await readFile(path, \"utf8\")) as LedgerFile;\n if (parsed.version !== VERSION || typeof parsed.records !== \"object\") return empty(now);\n const records = Object.fromEntries(\n Object.entries(parsed.records).filter(([, record]) => {\n const age = now.getTime() - Date.parse(record.lastSentAt);\n return Number.isFinite(age) && age <= MAX_AGE_MS;\n }),\n );\n const pending =\n parsed.pending && typeof parsed.pending === \"object\" ? parsed.pending : undefined;\n const acknowledged =\n parsed.acknowledged && typeof parsed.acknowledged === \"object\"\n ? Object.fromEntries(\n Object.entries(parsed.acknowledged).filter(([, versions]) => Array.isArray(versions)),\n )\n : undefined;\n return {\n version: VERSION,\n updatedAt: parsed.updatedAt,\n records,\n ...(pending ? { pending } : {}),\n ...(acknowledged ? { acknowledged } : {}),\n };\n } catch {\n return empty(now);\n }\n }\n\n private path(sessionId: string): string {\n // The full identity participates in the key. Legacy sanitized filenames\n // are intentionally ignored, so a fresh session cannot inherit collisions.\n return join(this.directory, `${digest(sessionId)}.json`);\n }\n\n private async acquire(sessionId: string): Promise<() => Promise<void>> {\n const lock = `${this.path(sessionId)}.lock`;\n for (let attempt = 0; ; attempt++) {\n try {\n await mkdir(lock);\n return async () => rm(lock, { recursive: true, force: true });\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\" || attempt >= 200) throw error;\n try {\n const age = Date.now() - (await stat(lock)).mtimeMs;\n if (age > STALE_LOCK_MS) {\n await rm(lock, { recursive: true, force: true });\n continue;\n }\n } catch {\n // The owner may have released the lock between checks.\n }\n await new Promise((resolveWait) => setTimeout(resolveWait, 10));\n }\n }\n }\n}\n\nfunction empty(now: Date): LedgerFile {\n return { version: VERSION, updatedAt: now.toISOString(), records: {} };\n}\n\nfunction digest(text: string): string {\n return createHash(\"sha256\").update(text).digest(\"hex\");\n}\n","import { access, readFile } from \"node:fs/promises\";\nimport { basename, dirname, extname, join } from \"node:path\";\n\n/**\n * Which test covers a file and how this repo runs its tests, from naming\n * conventions and project markers only. Agents otherwise spend turns finding\n * both; a wrong guess costs one failed command, so first match wins.\n */\nexport function findCoveringTest(path: string, paths: ReadonlySet<string>): string | null {\n return coveringTestCandidates(path).find((candidate) => paths.has(candidate)) ?? null;\n}\n\n/** Conventional covering-test paths for `path`, most specific first. */\nexport function coveringTestCandidates(path: string): string[] {\n const ext = extname(path);\n const stem = basename(path, ext);\n const dir = dirname(path);\n const join2 = (...parts: string[]) => parts.filter((part) => part && part !== \".\").join(\"/\");\n // `src/a/x.ts` mirrors to `test/a/x.test.ts`: drop a leading source root.\n const mirrored = dir.replace(/^(src|lib|app)(\\/|$)/, \"\");\n return ext === \".py\"\n ? [\n join2(dir, `test_${stem}.py`),\n join2(dir, `${stem}_test.py`),\n join2(dir, \"tests\", `test_${stem}.py`),\n // `src/pkg/sub/x.py` → `tests/pkg/sub/test_x.py`, `tests/sub/test_x.py`, `tests/test_x.py`.\n join2(\"tests\", mirrored, `test_${stem}.py`),\n join2(\"tests\", mirrored.replace(/^[^/]+\\/?/, \"\"), `test_${stem}.py`),\n join2(\"tests\", `test_${stem}.py`),\n ]\n : ext === \".go\"\n ? [join2(dir, `${stem}_test.go`)]\n : [\n join2(dir, `${stem}.test${ext}`),\n join2(dir, `${stem}.spec${ext}`),\n join2(dir, \"__tests__\", `${stem}.test${ext}`),\n join2(dir, \"__tests__\", `${stem}${ext}`),\n join2(\"test\", mirrored, `${stem}.test${ext}`),\n join2(\"tests\", mirrored, `${stem}.test${ext}`),\n ];\n}\n\nconst NPM_PLACEHOLDER = /no test specified/;\n\nexport async function detectTestCommand(repositoryRoot: string): Promise<string | null> {\n const exists = (file: string) =>\n access(join(repositoryRoot, file)).then(\n () => true,\n () => false,\n );\n try {\n const pkg = JSON.parse(await readFile(join(repositoryRoot, \"package.json\"), \"utf8\")) as {\n scripts?: Record<string, unknown>;\n packageManager?: unknown;\n };\n const script = pkg.scripts?.test;\n if (typeof script === \"string\" && script.trim() && !NPM_PLACEHOLDER.test(script)) {\n const declared =\n typeof pkg.packageManager === \"string\" ? pkg.packageManager.split(/[\\s@]/, 1)[0] : \"\";\n // `bun test` is Bun's own runner, not the package's test script.\n if (declared === \"bun\") return \"bun run test\";\n if (declared && [\"npm\", \"pnpm\", \"yarn\"].includes(declared)) return `${declared} test`;\n if (await exists(\"pnpm-lock.yaml\")) return \"pnpm test\";\n if (await exists(\"yarn.lock\")) return \"yarn test\";\n if ((await exists(\"bun.lockb\")) || (await exists(\"bun.lock\"))) return \"bun run test\";\n return \"npm test\";\n }\n } catch {\n // No or unreadable package.json: try other ecosystems.\n }\n for (const marker of [\"pyproject.toml\", \"pytest.ini\", \"setup.cfg\"]) {\n if (await exists(marker)) return \"pytest\";\n }\n if (await exists(\"go.mod\")) return \"go test ./...\";\n if (await exists(\"Cargo.toml\")) return \"cargo test\";\n return null;\n}\n"],"mappings":"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","names":["readFile","join","join2"]}
|
|
1
|
+
{"version":3,"sources":["../src/savings/tokens.ts","../src/context/session-ledger.ts","../src/context/tests.ts"],"sourcesContent":["import { countTokens } from \"../planner/budget.js\";\n\n/**\n * The one place token counts are computed. Strings use the same deterministic\n * cl100k tokenizer as planner budgets; objects are measured by their JSON\n * serialization, which matches how they travel over MCP.\n */\n\nexport function estimateTokens(value: unknown): number {\n if (value === undefined || value === null) return 0;\n const text = typeof value === \"string\" ? value : JSON.stringify(value);\n if (!text) return 0;\n return countTokens(text);\n}\n","import { createHash } from \"node:crypto\";\nimport { mkdir, readdir, readFile, rm, stat } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { generateEntityId } from \"../core/ids.js\";\nimport { writeAtomic } from \"../core/markdown-store-fs.js\";\nimport { estimateTokens } from \"../savings/tokens.js\";\n\nconst VERSION = 2;\nconst MAX_CAPSULES = 128;\nconst MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;\nconst MAX_PENDING = 8;\nconst PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1000;\nconst STALE_LOCK_MS = 60_000;\n/** Acknowledged versions kept per section, so switching back to a task reuses them. */\nconst MAX_ACKNOWLEDGED_PER_KEY = 8;\n\ntype LedgerRecord = { hash: string; lastSentAt: string };\n/** A delivery sent to a client that has not yet confirmed receipt. */\ntype PendingDelivery = { createdAt: string; records: Record<string, string> };\n/** One acknowledged version of an `exchange` section: content hash and when it was sent. */\ntype AcknowledgedVersion = { hash: string; sentAt: string };\ntype LedgerFile = {\n version: 2;\n updatedAt: string;\n /** `deliver`/`markSent` capsules (hooks): one current hash per key. */\n records: Record<string, LedgerRecord>;\n pending?: Record<string, PendingDelivery>;\n /** `exchange` sections (MCP): every recently acknowledged version, newest first. */\n acknowledged?: Record<string, AcknowledgedVersion[]>;\n};\n\nexport type ContextCapsule = { key: string; text: string };\nexport type CapsuleDelivery = {\n /** Full text of new or changed capsules, in input order. */\n lines: string[];\n /** Keys of capsules already sent this session and not resent. */\n unchanged: string[];\n full: number;\n referenced: number;\n sentTokens: number;\n avoidedTokens: number;\n};\n\nexport type SectionHash = { key: string; hash: string };\nexport type AcknowledgedExchange = {\n deliveryId: string;\n /** Keys whose content the client confirmed receiving and that have not changed. */\n unchanged: string[];\n ack: \"none\" | \"accepted\" | \"unknown\";\n};\n\nexport class SessionCapsuleLedger {\n private readonly directory: string;\n\n constructor(repositoryRoot = process.cwd()) {\n this.directory = join(resolve(repositoryRoot), \".codebuddy\", \"cache\", \"session\");\n }\n\n async deliver(\n sessionId: string,\n capsules: ContextCapsule[],\n now = new Date(),\n ): Promise<CapsuleDelivery> {\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n const path = this.path(sessionId);\n const ledger = await this.read(path, now);\n const lines: string[] = [];\n const unchanged: string[] = [];\n let full = 0;\n let referenced = 0;\n let sentTokens = 0;\n let avoidedTokens = 0;\n\n for (const capsule of capsules) {\n const hash = digest(capsule.text);\n const existing = ledger.records[capsule.key];\n if (existing?.hash === hash) {\n unchanged.push(capsule.key);\n referenced++;\n avoidedTokens += estimateTokens(capsule.text);\n } else {\n lines.push(capsule.text);\n full++;\n sentTokens += estimateTokens(capsule.text);\n }\n ledger.records[capsule.key] = { hash, lastSentAt: now.toISOString() };\n }\n\n await this.write(path, ledger, now);\n return { lines, unchanged, full, referenced, sentTokens, avoidedTokens };\n } finally {\n await release();\n }\n }\n\n /** Keys of capsules whose current text was already recorded as sent this session. */\n async seen(\n sessionId: string,\n capsules: ContextCapsule[],\n now = new Date(),\n ): Promise<Set<string>> {\n const ledger = await this.read(this.path(sessionId), now);\n return new Set(\n capsules\n .filter((capsule) => ledger.records[capsule.key]?.hash === digest(capsule.text))\n .map((capsule) => capsule.key),\n );\n }\n\n /** Whether anything was delivered in this session yet (a later prompt is a follow-up). */\n async delivered(sessionId: string, now = new Date()): Promise<boolean> {\n const ledger = await this.read(this.path(sessionId), now);\n return Object.keys(ledger.records).length > 0;\n }\n\n /**\n * Record capsules as delivered. Call only after their text actually reached\n * the client, so capsules that were clipped or failed to write stay unseen\n * and are delivered by a later prompt.\n */\n async markSent(sessionId: string, capsules: ContextCapsule[], now = new Date()): Promise<void> {\n if (capsules.length === 0) return;\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n const path = this.path(sessionId);\n const ledger = await this.read(path, now);\n for (const capsule of capsules) {\n ledger.records[capsule.key] = { hash: digest(capsule.text), lastSentAt: now.toISOString() };\n }\n await this.write(path, ledger, now);\n } finally {\n await release();\n }\n }\n\n /**\n * Acknowledged delivery for clients that can lose context (MCP). Unlike\n * `deliver`, sending marks nothing as seen: the sent hashes are held as a\n * pending delivery until the client echoes its `deliveryId` back as `ack` on\n * a later call. Only acknowledged, unchanged sections sent within `ttlMs` are\n * reported unchanged, so a dropped response or a compacted client context\n * fails safe to a full resend.\n *\n * A section keeps its last few acknowledged versions (by content hash), not\n * just the latest, so returning to an earlier task in the same session does\n * not resend what the client already holds. `full` means the client lost its\n * context: every acknowledged version and unconfirmed delivery is forgotten.\n */\n async exchange(\n sessionId: string,\n sections: SectionHash[],\n options: {\n ack?: string;\n ttlMs: number;\n /** The client lost its context: nothing acknowledged before this call counts. */\n full?: boolean;\n now?: Date;\n /** Finalize the response before recording hashes of the actual sent sections. */\n prepare?: (exchange: AcknowledgedExchange) => SectionHash[];\n },\n ): Promise<AcknowledgedExchange> {\n const now = options.now ?? new Date();\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n const path = this.path(sessionId);\n const ledger = await this.read(path, now);\n let pending = ledger.pending ?? {};\n let acknowledged = ledger.acknowledged ?? {};\n\n let ack: AcknowledgedExchange[\"ack\"] = \"none\";\n if (options.ack !== undefined) {\n const confirmed = pending[options.ack];\n if (confirmed) {\n for (const [key, hash] of Object.entries(confirmed.records)) {\n acknowledged[key] = [\n { hash, sentAt: confirmed.createdAt },\n ...(acknowledged[key] ?? []).filter((version) => version.hash !== hash),\n ]\n .sort((left, right) => right.sentAt.localeCompare(left.sentAt))\n .slice(0, MAX_ACKNOWLEDGED_PER_KEY);\n }\n delete pending[options.ack];\n ack = \"accepted\";\n } else {\n ack = \"unknown\";\n }\n }\n if (options.full) {\n acknowledged = {};\n pending = {};\n }\n\n const unchanged: string[] = [];\n const sent: Record<string, string> = {};\n for (const section of sections) {\n const held = acknowledged[section.key]?.some((version) => {\n const age = now.getTime() - Date.parse(version.sentAt);\n return version.hash === section.hash && age >= 0 && age <= options.ttlMs;\n });\n if (held) {\n unchanged.push(section.key);\n } else {\n sent[section.key] = section.hash;\n }\n }\n\n const deliveryId = generateEntityId(\"dlv\", now.getTime());\n const exchange = { deliveryId, unchanged, ack };\n const delivered = options.prepare?.(exchange);\n pending[deliveryId] = {\n createdAt: now.toISOString(),\n records: delivered\n ? Object.fromEntries(delivered.map(({ key, hash }) => [key, hash]))\n : sent,\n };\n ledger.pending = Object.fromEntries(\n Object.entries(pending)\n .filter(([, item]) => now.getTime() - Date.parse(item.createdAt) <= PENDING_MAX_AGE_MS)\n .sort((left, right) => right[1].createdAt.localeCompare(left[1].createdAt))\n .slice(0, MAX_PENDING),\n );\n ledger.acknowledged = Object.fromEntries(\n Object.entries(acknowledged)\n .map(([key, versions]): [string, AcknowledgedVersion[]] => [\n key,\n versions.filter((version) => now.getTime() - Date.parse(version.sentAt) <= MAX_AGE_MS),\n ])\n .filter(([, versions]) => versions.length > 0)\n .slice(0, MAX_CAPSULES),\n );\n await this.write(path, ledger, now);\n return { deliveryId, unchanged, ack };\n } finally {\n await release();\n }\n }\n\n /** Forget everything sent in a session, e.g. after the client compacted its context. */\n async reset(sessionId: string): Promise<void> {\n await mkdir(this.directory, { recursive: true, mode: 0o700 });\n const release = await this.acquire(sessionId);\n try {\n await rm(this.path(sessionId), { force: true });\n } finally {\n await release();\n }\n }\n\n /** Remove expired disposable state; locked sessions are left for the next pass. */\n async pruneExpired(now = new Date()): Promise<number> {\n let removed = 0;\n for (const entry of await readdir(this.directory).catch(() => [])) {\n if (!entry.endsWith(\".json\")) continue;\n const path = join(this.directory, entry);\n try {\n await stat(`${path}.lock`);\n continue;\n } catch {\n // No active lock.\n }\n try {\n const metadata = await stat(path);\n if (now.getTime() - metadata.mtimeMs > MAX_AGE_MS) {\n await rm(path, { force: true });\n removed++;\n }\n } catch {\n // Another cleanup may have removed it already.\n }\n }\n return removed;\n }\n\n private async write(path: string, ledger: LedgerFile, now: Date): Promise<void> {\n ledger.updatedAt = now.toISOString();\n ledger.records = Object.fromEntries(\n Object.entries(ledger.records)\n .sort((left, right) => right[1].lastSentAt.localeCompare(left[1].lastSentAt))\n .slice(0, MAX_CAPSULES),\n );\n await writeAtomic(path, `${JSON.stringify(ledger, null, 2)}\\n`);\n }\n\n private async read(path: string, now: Date): Promise<LedgerFile> {\n try {\n const parsed = JSON.parse(await readFile(path, \"utf8\")) as LedgerFile;\n if (parsed.version !== VERSION || typeof parsed.records !== \"object\") return empty(now);\n const records = Object.fromEntries(\n Object.entries(parsed.records).filter(([, record]) => {\n const age = now.getTime() - Date.parse(record.lastSentAt);\n return Number.isFinite(age) && age <= MAX_AGE_MS;\n }),\n );\n const pending =\n parsed.pending && typeof parsed.pending === \"object\" ? parsed.pending : undefined;\n const acknowledged =\n parsed.acknowledged && typeof parsed.acknowledged === \"object\"\n ? Object.fromEntries(\n Object.entries(parsed.acknowledged).filter(([, versions]) => Array.isArray(versions)),\n )\n : undefined;\n return {\n version: VERSION,\n updatedAt: parsed.updatedAt,\n records,\n ...(pending ? { pending } : {}),\n ...(acknowledged ? { acknowledged } : {}),\n };\n } catch {\n return empty(now);\n }\n }\n\n private path(sessionId: string): string {\n // The full identity participates in the key. Legacy sanitized filenames\n // are intentionally ignored, so a fresh session cannot inherit collisions.\n return join(this.directory, `${digest(sessionId)}.json`);\n }\n\n private async acquire(sessionId: string): Promise<() => Promise<void>> {\n const lock = `${this.path(sessionId)}.lock`;\n for (let attempt = 0; ; attempt++) {\n try {\n await mkdir(lock);\n return async () => rm(lock, { recursive: true, force: true });\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\" || attempt >= 200) throw error;\n try {\n const age = Date.now() - (await stat(lock)).mtimeMs;\n if (age > STALE_LOCK_MS) {\n await rm(lock, { recursive: true, force: true });\n continue;\n }\n } catch {\n // The owner may have released the lock between checks.\n }\n await new Promise((resolveWait) => setTimeout(resolveWait, 10));\n }\n }\n }\n}\n\nfunction empty(now: Date): LedgerFile {\n return { version: VERSION, updatedAt: now.toISOString(), records: {} };\n}\n\nfunction digest(text: string): string {\n return createHash(\"sha256\").update(text).digest(\"hex\");\n}\n","import { access, readFile } from \"node:fs/promises\";\nimport { basename, dirname, extname, join } from \"node:path\";\n\n/**\n * Which test covers a file and how this repo runs its tests, from naming\n * conventions and project markers only. Agents otherwise spend turns finding\n * both; a wrong guess costs one failed command, so first match wins.\n */\nexport function findCoveringTest(path: string, paths: ReadonlySet<string>): string | null {\n return coveringTestCandidates(path).find((candidate) => paths.has(candidate)) ?? null;\n}\n\n/** Conventional covering-test paths for `path`, most specific first. */\nexport function coveringTestCandidates(path: string): string[] {\n const ext = extname(path);\n const stem = basename(path, ext);\n const dir = dirname(path);\n const join2 = (...parts: string[]) => parts.filter((part) => part && part !== \".\").join(\"/\");\n // `src/a/x.ts` mirrors to `test/a/x.test.ts`: drop a leading source root.\n const mirrored = dir.replace(/^(src|lib|app)(\\/|$)/, \"\");\n return ext === \".py\"\n ? [\n join2(dir, `test_${stem}.py`),\n join2(dir, `${stem}_test.py`),\n join2(dir, \"tests\", `test_${stem}.py`),\n // `src/pkg/sub/x.py` → `tests/pkg/sub/test_x.py`, `tests/sub/test_x.py`, `tests/test_x.py`.\n join2(\"tests\", mirrored, `test_${stem}.py`),\n join2(\"tests\", mirrored.replace(/^[^/]+\\/?/, \"\"), `test_${stem}.py`),\n join2(\"tests\", `test_${stem}.py`),\n ]\n : ext === \".go\"\n ? [join2(dir, `${stem}_test.go`)]\n : [\n join2(dir, `${stem}.test${ext}`),\n join2(dir, `${stem}.spec${ext}`),\n join2(dir, \"__tests__\", `${stem}.test${ext}`),\n join2(dir, \"__tests__\", `${stem}${ext}`),\n join2(\"test\", mirrored, `${stem}.test${ext}`),\n join2(\"tests\", mirrored, `${stem}.test${ext}`),\n ];\n}\n\nconst NPM_PLACEHOLDER = /no test specified/;\n\nexport async function detectTestCommand(repositoryRoot: string): Promise<string | null> {\n const exists = (file: string) =>\n access(join(repositoryRoot, file)).then(\n () => true,\n () => false,\n );\n try {\n const pkg = JSON.parse(await readFile(join(repositoryRoot, \"package.json\"), \"utf8\")) as {\n scripts?: Record<string, unknown>;\n packageManager?: unknown;\n };\n const script = pkg.scripts?.test;\n if (typeof script === \"string\" && script.trim() && !NPM_PLACEHOLDER.test(script)) {\n const declared =\n typeof pkg.packageManager === \"string\" ? pkg.packageManager.split(/[\\s@]/, 1)[0] : \"\";\n // `bun test` is Bun's own runner, not the package's test script.\n if (declared === \"bun\") return \"bun run test\";\n if (declared && [\"npm\", \"pnpm\", \"yarn\"].includes(declared)) return `${declared} test`;\n if (await exists(\"pnpm-lock.yaml\")) return \"pnpm test\";\n if (await exists(\"yarn.lock\")) return \"yarn test\";\n if ((await exists(\"bun.lockb\")) || (await exists(\"bun.lock\"))) return \"bun run test\";\n return \"npm test\";\n }\n } catch {\n // No or unreadable package.json: try other ecosystems.\n }\n for (const marker of [\"pyproject.toml\", \"pytest.ini\", \"setup.cfg\"]) {\n if (await exists(marker)) return \"pytest\";\n }\n if (await exists(\"go.mod\")) return \"go test ./...\";\n if (await exists(\"Cargo.toml\")) return \"cargo test\";\n return null;\n}\n"],"mappings":"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","names":["readFile","join","join2"]}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chatJson
|
|
3
|
+
} from "./chunk-MNSMW4MZ.js";
|
|
4
|
+
import {
|
|
5
|
+
cardLockHolder,
|
|
6
|
+
withCardLock
|
|
7
|
+
} from "./chunk-6OQKL35B.js";
|
|
8
|
+
import {
|
|
9
|
+
CARDS_DIR,
|
|
10
|
+
refreshZvecCardIndex
|
|
11
|
+
} from "./chunk-RKF4HBBQ.js";
|
|
12
|
+
import {
|
|
13
|
+
CHANGELOG_PATH,
|
|
14
|
+
NON_SOURCE_PATH,
|
|
15
|
+
TEST_PATH,
|
|
16
|
+
loadFreshIndex,
|
|
17
|
+
loadProjectArchitectureMap,
|
|
18
|
+
readBoundedSource
|
|
19
|
+
} from "./chunk-4DTO2E4Y.js";
|
|
20
|
+
import {
|
|
21
|
+
redactSensitive,
|
|
22
|
+
scanSensitive
|
|
23
|
+
} from "./chunk-AL6VBTNA.js";
|
|
24
|
+
import {
|
|
25
|
+
resolveLlmConfig
|
|
26
|
+
} from "./chunk-RGIVHKBH.js";
|
|
27
|
+
import {
|
|
28
|
+
writeAtomic
|
|
29
|
+
} from "./chunk-CG5B3LYG.js";
|
|
30
|
+
import {
|
|
31
|
+
waitForForeground,
|
|
32
|
+
warmLocalModel
|
|
33
|
+
} from "./chunk-NNJSAYHY.js";
|
|
34
|
+
|
|
35
|
+
// src/llm/summaries.ts
|
|
36
|
+
import { mkdir, readdir, readFile, rm } from "fs/promises";
|
|
37
|
+
import { dirname, join } from "path";
|
|
38
|
+
import { z } from "zod";
|
|
39
|
+
var SUMMARY_PROMPT_VERSION = 2;
|
|
40
|
+
var LIMITS = {
|
|
41
|
+
fileChars: 200,
|
|
42
|
+
symbolChars: 100,
|
|
43
|
+
symptomChars: 80,
|
|
44
|
+
termChars: 40,
|
|
45
|
+
symptoms: 6,
|
|
46
|
+
terms: 10,
|
|
47
|
+
symbols: 8,
|
|
48
|
+
signatures: 12,
|
|
49
|
+
neighbours: 5,
|
|
50
|
+
/** ~600 tokens per file: long-line (minified) files are cut, never sent whole. */
|
|
51
|
+
headChars: 1600,
|
|
52
|
+
signatureChars: 160,
|
|
53
|
+
batchTokens: 5e3,
|
|
54
|
+
batchFiles: 8,
|
|
55
|
+
outputTokensPerFile: 320,
|
|
56
|
+
/** The local grammar bounds every field, so one card always fits. */
|
|
57
|
+
localOutputTokens: 700,
|
|
58
|
+
tokensPerMinute: 6e3,
|
|
59
|
+
timeoutMs: 2e4,
|
|
60
|
+
/** One file per call for the local model: a 0.6B model loses track of batches. */
|
|
61
|
+
localTimeoutMs: 3e4,
|
|
62
|
+
/** This many local timeouts in a row means the machine is overloaded: stop the pass. */
|
|
63
|
+
localTimeoutRun: 5
|
|
64
|
+
};
|
|
65
|
+
function cardEligible(path) {
|
|
66
|
+
return !TEST_PATH.test(path) && !NON_SOURCE_PATH.test(path) && !CHANGELOG_PATH.test(path) && !GENERATED_PATH.test(path);
|
|
67
|
+
}
|
|
68
|
+
var GENERATED_PATH = /(^|\/)(vendor|vendored|third[_-]?party|node_modules|dist|build|generated|__generated__)\/|\.(min|bundle|generated|pb)\.[a-z]+$|_pb2\.py$|(^|\/)[^/]*lock[^/]*$/i;
|
|
69
|
+
function cardText(card) {
|
|
70
|
+
return [
|
|
71
|
+
card.summary,
|
|
72
|
+
...card.symptoms ?? [],
|
|
73
|
+
...Object.entries(card.symbols).map(([name, text]) => `${name}: ${text}`)
|
|
74
|
+
].filter(Boolean).join("\n");
|
|
75
|
+
}
|
|
76
|
+
function cardWords(card) {
|
|
77
|
+
return [...card.terms ?? [], ...card.symptoms ?? []].join(" ");
|
|
78
|
+
}
|
|
79
|
+
var storePath = (root) => join(root, ".codebuddy", "cache", "llm-summaries.json");
|
|
80
|
+
async function readStore(root) {
|
|
81
|
+
try {
|
|
82
|
+
return JSON.parse(await readFile(storePath(root), "utf8"));
|
|
83
|
+
} catch {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function writeStore(root, store) {
|
|
88
|
+
await mkdir(join(root, ".codebuddy", "cache"), { recursive: true });
|
|
89
|
+
await writeAtomic(storePath(root), `${JSON.stringify(store)}
|
|
90
|
+
`);
|
|
91
|
+
}
|
|
92
|
+
async function loadLlmSummaries(root, manifest) {
|
|
93
|
+
const store = await readStore(root);
|
|
94
|
+
return new Map(
|
|
95
|
+
Object.entries(store).filter(([path, entry]) => manifest.entries[path]?.hash === entry.hash)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
var outputSchema = z.object({
|
|
99
|
+
files: z.array(
|
|
100
|
+
z.object({
|
|
101
|
+
path: z.string(),
|
|
102
|
+
summary: z.string(),
|
|
103
|
+
symptoms: z.array(z.string()).default([]),
|
|
104
|
+
terms: z.array(z.string()).default([]),
|
|
105
|
+
symbols: z.array(z.object({ name: z.string(), summary: z.string() })).default([])
|
|
106
|
+
})
|
|
107
|
+
)
|
|
108
|
+
});
|
|
109
|
+
var boundedString = (maxLength) => ({ type: "string", maxLength });
|
|
110
|
+
var CARD_GRAMMAR = {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
files: {
|
|
114
|
+
type: "array",
|
|
115
|
+
minItems: 1,
|
|
116
|
+
maxItems: 1,
|
|
117
|
+
items: {
|
|
118
|
+
type: "object",
|
|
119
|
+
properties: {
|
|
120
|
+
path: boundedString(300),
|
|
121
|
+
summary: boundedString(LIMITS.fileChars),
|
|
122
|
+
symptoms: {
|
|
123
|
+
type: "array",
|
|
124
|
+
items: boundedString(LIMITS.symptomChars),
|
|
125
|
+
maxItems: LIMITS.symptoms
|
|
126
|
+
},
|
|
127
|
+
terms: { type: "array", items: boundedString(LIMITS.termChars), maxItems: LIMITS.terms },
|
|
128
|
+
symbols: {
|
|
129
|
+
type: "array",
|
|
130
|
+
maxItems: LIMITS.symbols,
|
|
131
|
+
items: {
|
|
132
|
+
type: "object",
|
|
133
|
+
properties: { name: boundedString(80), summary: boundedString(LIMITS.symbolChars) }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
var SYSTEM = [
|
|
142
|
+
"You write search cards for source files so that a plain-English bug report or task finds the right file.",
|
|
143
|
+
'Answer with JSON only: {"files":[{"path":"...","summary":"...","symptoms":["..."],"terms":["..."],"symbols":[{"name":"...","summary":"..."}]}]}.',
|
|
144
|
+
"For each input file:",
|
|
145
|
+
`summary: one plain-English sentence (max ${LIMITS.fileChars} chars) saying what the file does;`,
|
|
146
|
+
`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;`,
|
|
147
|
+
`terms: 5 to ${LIMITS.terms} code identifiers and domain words from the file that a search should match;`,
|
|
148
|
+
`symbols: up to ${LIMITS.symbols} main symbols (only names listed in "signatures"), each with one short phrase.`,
|
|
149
|
+
"Symptoms describe what goes wrong for a user of this specific file (a wrong value, an error, nothing happening), never generic phrases.",
|
|
150
|
+
"Never invent paths or symbol names."
|
|
151
|
+
].join(" ");
|
|
152
|
+
var passPath = (root) => join(root, ".codebuddy", "cache", "cards-pass.json");
|
|
153
|
+
async function summarizeRepository(root, options = {}) {
|
|
154
|
+
const resolved = options.config ?? await resolveLlmConfig(root);
|
|
155
|
+
if (!resolved.enabled || !resolved.features.summaries) {
|
|
156
|
+
return { summarized: 0, skipped: 0, timedOut: 0, remaining: 0, stoppedBy: "disabled" };
|
|
157
|
+
}
|
|
158
|
+
const config = resolved;
|
|
159
|
+
const locked = await withCardLock(root, async () => {
|
|
160
|
+
const result = await summarizeLocked(root, config, options);
|
|
161
|
+
const record = { ...result, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
162
|
+
await writeAtomic(passPath(root), `${JSON.stringify(record)}
|
|
163
|
+
`).catch(() => {
|
|
164
|
+
});
|
|
165
|
+
return result;
|
|
166
|
+
});
|
|
167
|
+
if (locked.ok) return locked.value;
|
|
168
|
+
console.error(`[cards] already being written by pid ${locked.holder ?? "unknown"}`);
|
|
169
|
+
return {
|
|
170
|
+
summarized: 0,
|
|
171
|
+
skipped: 0,
|
|
172
|
+
timedOut: 0,
|
|
173
|
+
remaining: 0,
|
|
174
|
+
stoppedBy: "locked",
|
|
175
|
+
lockedBy: locked.holder
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async function summarizeLocked(root, config, options) {
|
|
179
|
+
const { manifest } = await loadFreshIndex(root);
|
|
180
|
+
const store = await readStore(root);
|
|
181
|
+
for (const path of Object.keys(store)) if (!manifest.entries[path]) delete store[path];
|
|
182
|
+
const map = await loadProjectArchitectureMap(root, manifest).catch(() => null);
|
|
183
|
+
const pending = orderByImportance(
|
|
184
|
+
map,
|
|
185
|
+
Object.values(manifest.entries).filter((entry) => {
|
|
186
|
+
if (!cardEligible(entry.path)) return false;
|
|
187
|
+
const done = store[entry.path];
|
|
188
|
+
return !(done?.hash === entry.hash && done.model === config.model && done.version === SUMMARY_PROMPT_VERSION);
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
const local = config.provider === "local";
|
|
192
|
+
const batchFiles = local ? 1 : options.batchFiles ?? LIMITS.batchFiles;
|
|
193
|
+
const limit = options.limit ?? Number.POSITIVE_INFINITY;
|
|
194
|
+
const localTimeoutMs = options.localTimeoutMs ?? LIMITS.localTimeoutMs;
|
|
195
|
+
let summarized = 0;
|
|
196
|
+
let skipped = 0;
|
|
197
|
+
let timedOut = 0;
|
|
198
|
+
let timeoutRun = 0;
|
|
199
|
+
let index = 0;
|
|
200
|
+
let stoppedBy = "done";
|
|
201
|
+
if (pending.length > 0) options.onProgress?.(0, pending.length);
|
|
202
|
+
if (local && pending.length > 0 && config.local && !await warmLocalModel(config.local)) {
|
|
203
|
+
console.error("[cards] local model failed to load");
|
|
204
|
+
index = pending.length;
|
|
205
|
+
stoppedBy = "error";
|
|
206
|
+
}
|
|
207
|
+
while (index < pending.length) {
|
|
208
|
+
if (options.signal?.aborted) {
|
|
209
|
+
stoppedBy = "aborted";
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
if (summarized >= limit) {
|
|
213
|
+
stoppedBy = "limit";
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
const batch = [];
|
|
217
|
+
let tokens = 0;
|
|
218
|
+
while (index < pending.length && batch.length < batchFiles) {
|
|
219
|
+
if (summarized + batch.length >= limit) break;
|
|
220
|
+
const entry = pending[index];
|
|
221
|
+
const input = await fileInput(root, entry, map);
|
|
222
|
+
if (input === null) {
|
|
223
|
+
skipped++;
|
|
224
|
+
index++;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const cost = Math.ceil(JSON.stringify(input).length / 4);
|
|
228
|
+
if (batch.length > 0 && tokens + cost > LIMITS.batchTokens - 600) break;
|
|
229
|
+
batch.push({ entry, input });
|
|
230
|
+
tokens += cost;
|
|
231
|
+
index++;
|
|
232
|
+
}
|
|
233
|
+
if (batch.length === 0) continue;
|
|
234
|
+
if (local) await waitForForeground(root, options.signal);
|
|
235
|
+
const started = Date.now();
|
|
236
|
+
const ask = () => chatJson(config, {
|
|
237
|
+
system: SYSTEM,
|
|
238
|
+
user: JSON.stringify({ files: batch.map(({ input }) => input) }),
|
|
239
|
+
schema: outputSchema,
|
|
240
|
+
jsonSchema: CARD_GRAMMAR,
|
|
241
|
+
maxTokens: local ? LIMITS.localOutputTokens : LIMITS.outputTokensPerFile * batch.length,
|
|
242
|
+
deadlineMs: local ? localTimeoutMs : LIMITS.timeoutMs,
|
|
243
|
+
purpose: "background",
|
|
244
|
+
repositoryRoot: root
|
|
245
|
+
});
|
|
246
|
+
let result = await ask();
|
|
247
|
+
if (!result.ok && result.reason === "invalid") result = await ask();
|
|
248
|
+
if (!result.ok && local && result.reason === "timeout") {
|
|
249
|
+
timedOut += batch.length;
|
|
250
|
+
timeoutRun++;
|
|
251
|
+
for (const { entry } of batch)
|
|
252
|
+
console.error(
|
|
253
|
+
`[cards] ${entry.path} timed out after ${Math.round(localTimeoutMs / 1e3)} s`
|
|
254
|
+
);
|
|
255
|
+
options.onProgress?.(index, pending.length);
|
|
256
|
+
if (timeoutRun >= LIMITS.localTimeoutRun) {
|
|
257
|
+
stoppedBy = "timeout";
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (!result.ok) {
|
|
263
|
+
if (result.reason === "invalid" || result.reason === "http" && batch.length === 1) {
|
|
264
|
+
skipped += batch.length;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
index -= batch.length;
|
|
268
|
+
stoppedBy = result.reason === "budget" ? "budget" : result.reason === "cooldown" || result.reason === "rate-limited" ? "cooldown" : "error";
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
timeoutRun = 0;
|
|
272
|
+
for (const { entry } of batch) {
|
|
273
|
+
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);
|
|
274
|
+
if (!answer?.summary.trim()) {
|
|
275
|
+
skipped++;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
store[entry.path] = toCard(entry, config.model, answer);
|
|
279
|
+
summarized++;
|
|
280
|
+
}
|
|
281
|
+
await writeStore(root, store);
|
|
282
|
+
await writeCardFiles(
|
|
283
|
+
root,
|
|
284
|
+
store,
|
|
285
|
+
batch.map(({ entry }) => entry.path)
|
|
286
|
+
);
|
|
287
|
+
options.onProgress?.(index, pending.length);
|
|
288
|
+
if (options.pace !== false) {
|
|
289
|
+
const wait = result.tokens / LIMITS.tokensPerMinute * 6e4 - (Date.now() - started);
|
|
290
|
+
if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
await writeStore(root, store);
|
|
294
|
+
await pruneCardFiles(root, store, manifest);
|
|
295
|
+
if (summarized > 0)
|
|
296
|
+
await refreshZvecCardIndex(root, options.signal ? { signal: options.signal } : {});
|
|
297
|
+
return { summarized, skipped, timedOut, remaining: pending.length - index, stoppedBy };
|
|
298
|
+
}
|
|
299
|
+
function toCard(entry, model, answer) {
|
|
300
|
+
const known = new Set((entry.symbolTable ?? []).map((symbol) => symbol.name));
|
|
301
|
+
const clean = (items, count, chars) => [
|
|
302
|
+
...new Set(items.map((item) => redactSensitive(item.trim()).slice(0, chars)).filter(Boolean))
|
|
303
|
+
].slice(0, count);
|
|
304
|
+
return {
|
|
305
|
+
hash: entry.hash,
|
|
306
|
+
model,
|
|
307
|
+
version: SUMMARY_PROMPT_VERSION,
|
|
308
|
+
summary: redactSensitive(answer.summary.trim()).slice(0, LIMITS.fileChars),
|
|
309
|
+
symptoms: clean(answer.symptoms, LIMITS.symptoms, LIMITS.symptomChars),
|
|
310
|
+
terms: clean(answer.terms, LIMITS.terms, LIMITS.termChars),
|
|
311
|
+
symbols: Object.fromEntries(
|
|
312
|
+
answer.symbols.filter((symbol) => known.has(symbol.name)).slice(0, LIMITS.symbols).map((symbol) => [symbol.name, symbol.summary.trim().slice(0, LIMITS.symbolChars)])
|
|
313
|
+
)
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
async function writeCardFiles(root, store, paths) {
|
|
317
|
+
for (const path of paths) {
|
|
318
|
+
const card = store[path];
|
|
319
|
+
if (!card) continue;
|
|
320
|
+
const target = join(root, CARDS_DIR, `${path}.md`);
|
|
321
|
+
await mkdir(dirname(target), { recursive: true });
|
|
322
|
+
await writeAtomic(
|
|
323
|
+
target,
|
|
324
|
+
[
|
|
325
|
+
"---",
|
|
326
|
+
`source: ${JSON.stringify(path)}`,
|
|
327
|
+
`hash: ${card.hash}`,
|
|
328
|
+
"---",
|
|
329
|
+
"",
|
|
330
|
+
`# ${path}`,
|
|
331
|
+
"",
|
|
332
|
+
card.summary,
|
|
333
|
+
"",
|
|
334
|
+
...card.symptoms?.length ? ["Symptoms:", ...card.symptoms.map((s) => `- ${s}`), ""] : [],
|
|
335
|
+
...card.terms?.length ? [`Terms: ${card.terms.join(", ")}`, ""] : [],
|
|
336
|
+
...Object.entries(card.symbols).map(([name, text]) => `- ${name}: ${text}`),
|
|
337
|
+
""
|
|
338
|
+
].join("\n")
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async function pruneCardFiles(root, store, manifest) {
|
|
343
|
+
const directory = join(root, CARDS_DIR);
|
|
344
|
+
const current = new Set(
|
|
345
|
+
Object.entries(store).filter(([path, card]) => manifest.entries[path]?.hash === card.hash && card.symptoms).map(([path]) => `${path}.md`)
|
|
346
|
+
);
|
|
347
|
+
const existing = await readdir(directory, { recursive: true }).catch(() => []);
|
|
348
|
+
const present = new Set(existing.map((name) => name.split("\\").join("/")));
|
|
349
|
+
for (const name of present) {
|
|
350
|
+
if (name.endsWith(".md") && !name.startsWith(".zvec-grep") && !current.has(name)) {
|
|
351
|
+
await rm(join(directory, name), { force: true });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
await writeCardFiles(
|
|
355
|
+
root,
|
|
356
|
+
store,
|
|
357
|
+
[...current].filter((name) => !present.has(name)).map((name) => name.slice(0, -3))
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
async function cardProgress(root) {
|
|
361
|
+
const { manifest } = await loadFreshIndex(root);
|
|
362
|
+
const store = await readStore(root);
|
|
363
|
+
const eligible = Object.keys(manifest.entries).filter(cardEligible);
|
|
364
|
+
const lastPass = await readFile(passPath(root), "utf8").then((text) => JSON.parse(text)).catch(() => null);
|
|
365
|
+
return {
|
|
366
|
+
writer: await cardLockHolder(root),
|
|
367
|
+
lastPass,
|
|
368
|
+
carded: eligible.filter(
|
|
369
|
+
(path) => store[path]?.hash === manifest.entries[path]?.hash && store[path]?.version === SUMMARY_PROMPT_VERSION
|
|
370
|
+
).length,
|
|
371
|
+
eligible: eligible.length
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
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*$/;
|
|
375
|
+
async function fileInput(root, entry, map) {
|
|
376
|
+
const source = await readBoundedSource(root, entry.path);
|
|
377
|
+
if (source === null || scanSensitive(source).sensitive) return null;
|
|
378
|
+
const edges = map?.edges ?? [];
|
|
379
|
+
return {
|
|
380
|
+
path: entry.path,
|
|
381
|
+
language: entry.language,
|
|
382
|
+
// Not the deterministic summary: small models copy its "ts • 80 lines" form.
|
|
383
|
+
signatures: (entry.symbolTable ?? []).filter((symbol) => symbol.kind !== "heading").slice(0, LIMITS.signatures).map((symbol) => redactSensitive(symbol.signature.slice(0, LIMITS.signatureChars))),
|
|
384
|
+
neighbours: [
|
|
385
|
+
.../* @__PURE__ */ new Set([
|
|
386
|
+
...edges.filter((edge) => edge.from === entry.path).map((edge) => edge.to),
|
|
387
|
+
...edges.filter((edge) => edge.to === entry.path).map((edge) => edge.from)
|
|
388
|
+
])
|
|
389
|
+
].slice(0, LIMITS.neighbours),
|
|
390
|
+
// The module docstring and first definitions, never the whole file.
|
|
391
|
+
head: redactSensitive(
|
|
392
|
+
source.split(/\r?\n/).filter((line) => !IMPORT_LINE.test(line)).join("\n").slice(0, LIMITS.headChars)
|
|
393
|
+
)
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function orderByImportance(map, entries) {
|
|
397
|
+
const fanIn = /* @__PURE__ */ new Map();
|
|
398
|
+
for (const edge of map?.edges ?? []) fanIn.set(edge.to, (fanIn.get(edge.to) ?? 0) + 1);
|
|
399
|
+
return [...entries].sort(
|
|
400
|
+
(left, right) => (fanIn.get(right.path) ?? 0) - (fanIn.get(left.path) ?? 0) || left.path.localeCompare(right.path)
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export {
|
|
405
|
+
SUMMARY_PROMPT_VERSION,
|
|
406
|
+
cardEligible,
|
|
407
|
+
cardText,
|
|
408
|
+
cardWords,
|
|
409
|
+
loadLlmSummaries,
|
|
410
|
+
summarizeRepository,
|
|
411
|
+
cardProgress
|
|
412
|
+
};
|
|
413
|
+
//# sourceMappingURL=chunk-7JPUMXFE.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 { cardLockHolder, withCardLock } from \"./card-lock.js\";\nimport { chatJson } from \"./client.js\";\nimport { type EnabledLlmConfig, resolveLlmConfig } from \"./config.js\";\nimport { waitForForeground, warmLocalModel } 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 /** This many local timeouts in a row means the machine is overloaded: stop the pass. */\n localTimeoutRun: 5,\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 /** Local files that ran past the deadline (counted apart from `skipped`). */\n timedOut: number;\n remaining: number;\n stoppedBy:\n | \"done\"\n | \"limit\"\n | \"budget\"\n | \"cooldown\"\n | \"disabled\"\n | \"aborted\"\n | \"error\"\n | \"timeout\"\n | \"locked\";\n /** Set with `stoppedBy: \"locked\"`: the PID already writing cards (null if unknown). */\n lockedBy?: number | null;\n};\n\n/** The last finished pass, for `codebuddy models status`. */\nexport type CardPassRecord = Omit<SummarizeResult, \"lockedBy\"> & { at: string };\nconst passPath = (root: string) => join(root, \".codebuddy\", \"cache\", \"cards-pass.json\");\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 /** Per-file deadline for the local model (tests). */\n localTimeoutMs?: 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, timedOut: 0, remaining: 0, stoppedBy: \"disabled\" };\n }\n const config = resolved;\n const locked = await withCardLock(root, async () => {\n const result = await summarizeLocked(root, config, options);\n const record: CardPassRecord = { ...result, at: new Date().toISOString() };\n await writeAtomic(passPath(root), `${JSON.stringify(record)}\\n`).catch(() => {});\n return result;\n });\n if (locked.ok) return locked.value;\n console.error(`[cards] already being written by pid ${locked.holder ?? \"unknown\"}`);\n return {\n summarized: 0,\n skipped: 0,\n timedOut: 0,\n remaining: 0,\n stoppedBy: \"locked\",\n lockedBy: locked.holder,\n };\n}\n\nasync function summarizeLocked(\n root: string,\n config: EnabledLlmConfig,\n options: NonNullable<Parameters<typeof summarizeRepository>[1]>,\n): Promise<SummarizeResult> {\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 const localTimeoutMs = options.localTimeoutMs ?? LIMITS.localTimeoutMs;\n let summarized = 0;\n let skipped = 0;\n let timedOut = 0;\n let timeoutRun = 0;\n let index = 0;\n let stoppedBy: SummarizeResult[\"stoppedBy\"] = \"done\";\n if (pending.length > 0) options.onProgress?.(0, pending.length);\n // Load once, outside the per-file deadline: a cold load under memory\n // pressure must not time out (and skip) the first files.\n if (local && pending.length > 0 && config.local && !(await warmLocalModel(config.local))) {\n console.error(\"[cards] local model failed to load\");\n index = pending.length;\n stoppedBy = \"error\";\n }\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 ? 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 && local && result.reason === \"timeout\") {\n // One slow file is not an outage, but a run of them is an overloaded\n // machine: say so and stop instead of burning hours writing nothing.\n timedOut += batch.length;\n timeoutRun++;\n for (const { entry } of batch)\n console.error(\n `[cards] ${entry.path} timed out after ${Math.round(localTimeoutMs / 1000)} s`,\n );\n options.onProgress?.(index, pending.length);\n if (timeoutRun >= LIMITS.localTimeoutRun) {\n stoppedBy = \"timeout\";\n break;\n }\n continue;\n }\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 if (result.reason === \"invalid\" || (result.reason === \"http\" && batch.length === 1)) {\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 timeoutRun = 0;\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, timedOut, 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/**\n * Card coverage for `stats`/`storage`/`models`: current cards vs eligible\n * source files, the live writer's PID and the last finished pass.\n */\nexport async function cardProgress(root: string): Promise<{\n carded: number;\n eligible: number;\n writer: number | null;\n lastPass: CardPassRecord | null;\n}> {\n const { manifest } = await loadFreshIndex(root);\n const store = await readStore(root);\n const eligible = Object.keys(manifest.entries).filter(cardEligible);\n const lastPass = await readFile(passPath(root), \"utf8\")\n .then((text) => JSON.parse(text) as CardPassRecord)\n .catch(() => null);\n return {\n writer: await cardLockHolder(root),\n lastPass,\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":[]}
|