/** * Blob keep-links: attachment blobs outlive a stale-reading older build * (T12535, mixed-version defence). * * ## Why * * After the twin collapse this build reads and writes `docs_attachments`, * while the 2026.9.20 build still reads and writes the bare `attachments` * table. Both keep blob bytes at the same content-addressed path * (`.cleo/attachments/sha256//`). The older build deletes a blob * when ITS last bare ref goes (and its janitor deletes blobs no BARE row * names), unaware that this build's twin may still reference the content. * * ## How * * Every blob this build stores, references or carries in the collapse gets a * hard link under `.cleo/attachments/keep//` (a copy where hard * links are not possible). The older build only ever unlinks the primary path, * so the bytes survive in the keep-link, and this build restores the primary * path from it on read, on every merge and in the repair. * * This build never deletes a blob straight away: its last deref leaves a * tombstone (`.tomb`, holding the time). The repair (`cleo doctor`, * the janitor) removes the primary file and the keep-link only after the grace * period, and only when neither table names the content. * * @module * @task T12535 */ /** * Resolve a file extension from a MIME type. * * @param mime - IANA MIME type string * @returns Extension string including the leading dot (e.g., `".md"`) */ export declare function extFromMime(mime: string): string; /** * The primary on-disk path of a blob. * * @param cleoDir - The project's `.cleo` directory. * @param sha256 - The content hash. * @param mime - The blob's MIME type (gives the extension). * @returns `.cleo/attachments/sha256//`. */ export declare function blobFilePath(cleoDir: string, sha256: string, mime: string): string; /** * The primary path of the blob an attachment row stores bytes for, or `null` * for kinds kept by reference (url, llms-txt, llmtxt-doc) or malformed JSON. * * @param cleoDir - The project's `.cleo` directory. * @param sha256 - The row's content hash. * @param attachmentJson - The row's `attachment_json`. * @returns The primary blob path, or `null`. */ export declare function blobFileForRow(cleoDir: string, sha256: string, attachmentJson: string): string | null; /** * Read-only probe: whether a blob's bytes are retrievable right now. * * True when the primary file exists or a live (non-tombstoned) keep-link * holds the bytes. Unlike {@link restoreBlob} this never writes — the primary * file is NOT re-created from the keep-link — so callers running in a * dry-run/planning pass can use it without side effects. * * @param cleoDir - The project's `.cleo` directory. * @param sha256 - The content hash. * @param mime - The blob's MIME type (gives the primary file's extension). * @returns Whether the content is on disk (primary or keep-link). * @task T13562 */ export declare function blobContentExists(cleoDir: string, sha256: string, mime: string): boolean; /** * Hard-link (or copy) a blob's primary file to its keep-link. Idempotent and * best effort: a missing primary file or an unwritable keep directory is not * an error (the blob is then simply not protected). * * @param cleoDir - The project's `.cleo` directory. * @param sha256 - The content hash. * @param primary - The blob's primary path. */ export declare function pinBlob(cleoDir: string, sha256: string, primary: string): void; /** * Put a blob's primary file back from its keep-link when the primary is * missing (an older build unlinked it). Clears a pending tombstone: a restore * means something still needs the bytes. * * @param cleoDir - The project's `.cleo` directory. * @param sha256 - The content hash. * @param primary - The blob's primary path. * @returns Whether the primary file exists afterwards. */ export declare function restoreBlob(cleoDir: string, sha256: string, primary: string): boolean; /** * Mark a blob this build no longer references: the repair deletes it after the * grace period if neither table names the content then. The primary file and * the keep-link stay until then. * * @param cleoDir - The project's `.cleo` directory. * @param sha256 - The content hash. * @param primary - The blob's primary path (pinned first, so the tombstone has * a keep-link to guard). * @param now - The tombstone time (ms since the epoch). */ export declare function tombstoneBlob(cleoDir: string, sha256: string, primary: string, now: number): void; /** What {@link collectBlobGarbage} did. */ export interface BlobGarbageResult { /** Content hashes whose keep-link (and primary file) were deleted. */ readonly deleted: string[]; /** Tombstones or unreferenced keep-links still inside the grace period. */ readonly waiting: number; /** Tombstones dropped because a table names the content again. */ readonly revived: number; } /** * Collect keep-links nobody needs. For each keep-link: * * - referenced by a row in either table: kept; a tombstone on it is dropped * and its primary file restored if it is gone; * - not referenced, tombstoned (this build's last deref): the primary file * and the keep-link are deleted once the tombstone is older than the grace * period; * - not referenced, no tombstone (an older build deleted the row): the * keep-link (and any primary file) is deleted once the file is older than * the grace period. * * @param cleoDir - The project's `.cleo` directory. * @param isReferenced - Whether a row in either table names the content. * @param graceMs - How long a tombstone (or an unreferenced keep-link) must age. * @param now - The current time (ms since the epoch). * @param dryRun - Report only. * @returns What was deleted, kept waiting, or revived. */ export declare function collectBlobGarbage(cleoDir: string, isReferenced: (sha256: string) => boolean, graceMs: number, now: number, dryRun: boolean): BlobGarbageResult; /** * Whether a blob has a pending tombstone (its deletion waits for the grace * period, so other sweeps leave it alone). * * @param cleoDir - The project's `.cleo` directory. * @param sha256 - The content hash. * @returns `true` when tombstoned. */ export declare function isTombstoned(cleoDir: string, sha256: string): boolean; /** * Delete a blob's keep-link (the primary file was deleted by a sweep that * checked both tables). * * @param cleoDir - The project's `.cleo` directory. * @param sha256 - The content hash. */ export declare function unpinBlob(cleoDir: string, sha256: string): void; /** * True when `target` exists AND its bytes re-hash to `sha256` (T13657). * * Existence alone is never proof that a content-addressed file holds the * bytes its name claims: a truncated leftover from an interrupted * pre-atomic write must be replaced from verified bytes, not trusted. Any * read failure (absent, unreadable) returns false. * * @param target - Absolute path of the content-addressed file. * @param sha256 - Lowercase hex SHA-256 the bytes must match. */ export declare function fileBytesMatchSha256(target: string, sha256: string): boolean; /** * Locate the on-disk source of a sha's legacy bytes for the blob-store * backfill (T13653). Tries, in order: * * 1. the primary legacy path (`.cleo/attachments/sha256/<2>/<62>`, * extension from the row's mime); * 2. ANY file in the sha's prefix directory whose stem equals the sha's * remaining 62 chars (the recorded mime may disagree with the extension * a historical writer used); * 3. a live keep-link (`.cleo/attachments/keep/<2>/<62>` — not tombstoned). * * Returns the first existing candidate, or null when the bytes are gone. * Read-only — verification happens in {@link backfillBlobStoreBytes}. * * @param cleoDir - Absolute `.cleo` directory. * @param sha256 - Recorded content hash. * @param mime - Recorded mime (picks the primary path's extension). * @returns The first existing candidate path, or null. * @internal */ export declare function resolveLegacyByteSource(cleoDir: string, sha256: string, mime: string): string | null; /** * Backfill outcome for one sha (T13653). * * - `written` — bytes verified and mirrored into the blob store (also * returned when a corrupt/truncated existing target was * replaced from the verified source, T13654). * - `present` — blob-store file already existed AND re-hashes to the * recorded sha (idempotent re-run; left untouched). * - `unverified`— the source bytes' SHA-256 does not match the recorded hash; * nothing was written (writing unverified bytes would poison * the content-addressed store). * - `unreadable`— the source could not be read; nothing was written. */ export type BlobBackfillOutcome = 'written' | 'present' | 'unverified' | 'unreadable'; /** * Mirror one sha's legacy bytes into the blob store (`.cleo/blobs/blobs/`). * * Hash-verified: the source bytes are SHA-256-hashed before any write and * must equal the recorded sha256. Crash-safe (T13654): bytes are written to * a tmp file and renamed into place — an interrupted write never leaves a * truncated file at the final content address (which later runs would count * as present and `blobReadBySha` would then reject as corrupt with no heal * path). An existing target is RE-HASHED rather than assumed good: when its * bytes mismatch the recorded hash (truncation/corruption), it is replaced * from the verified source; a matching target is left untouched. * * @internal */ export declare function backfillBlobStoreBytes(cleoDir: string, sha256: string, sourcePath: string): Promise; /** * Self-heal one blob-store miss from hash-verified legacy bytes (T13705). * * Docs written before the blob store held every body keep their bytes only in * the legacy attachment tree, which the read paths no longer consult (#2210). * When those bytes exist and hash to the recorded sha, they are mirrored into * the blob store exactly as `cleo docs doctor --repair` would ({@link * backfillBlobStoreBytes}: atomic write, only ever adds an absent content-addressed * file), so the first read heals the doc. Missing or mismatched bytes write * nothing. * * The read never fails on the write (T13706): on a read-only `.cleo`, a * sandbox or a full disk the verified bytes are still returned, a warning is * emitted, and the blob-store file is left for `cleo docs doctor --repair`. * * @param cleoDir - Absolute `.cleo` directory the blob store lives under. * @param sha256 - The content hash the doc row records. * @returns The hash-verified bytes, or null when no legacy bytes verify. * @task T13705 */ export declare function healBlobFromLegacy(cleoDir: string, sha256: string): Promise; //# sourceMappingURL=blob-keep.d.ts.map