import * as fs from 'node:fs'; import type { CortexStore } from '../db/store.js'; /** * The flat digest index (AD-3): a derived, regenerable, line-oriented * projection of `content_digests` that the hot path can search with `grep` * alone — no SQLite, no JSON parser, no Node process. * * **It lives in the project root, and that is forced, not chosen.** Story 2.5 * moved the store to `$CORTEX_HOME`, but recorded an architectural floor: the * hook scripts resolve their files as `"$CWD/.cortex.*"` in pure bash, and * finding a relocated file would mean hashing a path inside `PostToolUse` — * which needs either sha256 in bash or a Node process, and N-4 forbids a * process per tool call. The spool, `.cortex.state` and `.cortex.agent-used` * stay in the project root for exactly this reason, and so does this file. * * **Derived, never authoritative.** Deleting it loses nothing: it is rebuilt in * full from the table on the next cold-path run. That is why it is written as a * complete projection rather than appended per batch — an appended index would * satisfy "the flush writes it" while quietly failing "a deleted index is fully * regenerated". * * **Cold path is the sole writer** (AD-2). Nothing under `hooks/claude/` writes * this file; a test asserts that. */ export declare const DIGEST_INDEX_FILENAME = ".cortex.index"; /** Written when a record has no `agent_id`, so the column count is fixed. */ export declare const INDEX_ABSENT = "-"; /** * Temp-file suffix for the atomic write. Exported because `IGNORE_ENTRIES` * must cover it: a rename that fails and whose cleanup unlink also fails leaves * this file in the user's project root, breaking `cortex install`'s promise * that a checkout stays clean — the same reason `.cortex.spool.jsonl.processing` * is on that list beside `.cortex.spool.jsonl`. */ export declare const INDEX_TEMP_SUFFIX = ".tmp-"; export declare function deriveDigestIndexPath(projectRoot: string): string; /** * Make one field safe for a tab-separated, newline-delimited record. * * Not defensive dressing: `path` comes from whatever the agent read, and * `scope_key` embeds a branch ref, which git permits to contain a startling * range of bytes. A raw tab forges a column and a raw newline forges an entire * record — the same class of hazard `inspect-memory` and `renderedAlternatives` * already guard, and here it would let one file's line claim another file's * digest. Percent-escaped rather than dropped, so the value stays reversible * and `grep` still matches a literal path. */ export declare function escapeIndexField(value: string): string; export declare function unescapeIndexField(value: string): string; export interface DigestIndexRecord { scopeKey: string; path: string; sha256: string | null; byteSize: number; sessionId: string; agentId: string | null; } /** One record, as the single line the hot path greps. */ export declare function formatIndexLine(record: DigestIndexRecord): string; export declare function parseIndexLine(line: string): DigestIndexRecord | null; export declare function collectIndexRecords(store: CortexStore, projectRoot: string): DigestIndexRecord[]; export declare function renderDigestIndex(records: DigestIndexRecord[]): string; /** * Rebuild the index from the table. * * Written temp-file-plus-rename because the hot path reads it concurrently: * a partial in-place write does not fail a `grep`, it answers it *wrongly*, * which is the one outcome AD-6 forbids. LF explicitly, never the platform's * line ending — there is no `.gitattributes`, and a CRLF file breaks bash * everywhere except Windows (Story 2.4's finding). * * Returns the number of records written, or `null` when nothing was written * because the write failed. Failure is not thrown: this runs on ambient paths * and AD-12 binds them to silence. The index is derived, so a failed write * costs a rebuild, never data. */ export interface IndexWriteDeps { writeFileSync: typeof fs.writeFileSync; renameSync: typeof fs.renameSync; } export declare function writeDigestIndex(store: CortexStore, projectRoot: string, /** * Injected only so the write can be shown to be temp-file-plus-rename rather * than an in-place overwrite. Atomicity has no observable difference in the * success case — a direct write produces the same final bytes — so without a * seam a mutation removing the rename survives every behavioural test, which * is exactly what happened. Production always takes the default. */ deps?: IndexWriteDeps): number | null; /** * Whether a *usable* index is present, so a caller can rebuild it (AC #3). * * A zero-byte file counts as absent. `isFile()` alone is true for one, so an * index that was written empty — which is exactly what a scope-matching failure * produces — satisfied the regeneration guard forever and never self-healed. * Measured: zero-byte index, one row in the table, an idle flush, still zero * bytes. Treating empty as absent costs one cheap rewrite on a genuinely empty * project and buys unconditional recovery everywhere else. */ export declare function digestIndexExists(projectRoot: string): boolean; /** * The exact literal a consumer must search for to find one file's record. * * Exported because the lookup is **not** "grep the path you have", and every * way of getting it wrong fails silently as a false "unread" rather than as an * error. Three transformations stand between a caller's path and the stored * key, all measured: * * - **Case is folded** on win32 and darwin, so grepping a mixed-case path * returns nothing. * - **The key is scope-root-relative**, so grepping an absolute path returns * nothing. * - **The field is percent-escaped**, so a path containing `%` is stored as * `%25` and the raw path returns nothing. * * The delimiters matter too: without the surrounding tabs, `store.ts` also * matches `store.tsx`, and a path containing `.` or `[` matches the wrong * record — or none — unless the consumer passes `-F`. **`grep -F` is required, * not advisory.** */ export declare function indexLookupNeedle(filePath: string, scopeRoot: string | null | undefined): string; //# sourceMappingURL=digest-index.d.ts.map