import type { DocKind } from '@cleocode/contracts'; /** * A fully-resolved document record surfaced by the read model. * * Unifies fields from tasks.db attachments, manifest.db blobs, and the * publications ledger into a single envelope. Content is NOT populated by * default — callers that need the raw text must call {@link DocsReadModel.fetchContent} * separately. This keeps list/status operations fast by avoiding unnecessary blob reads. */ export interface ResolvedDoc { /** Attachment UUID (tasks.db) or SHA-256 hex (blob store). Always set. */ readonly id: string; /** SHA-256 content hash. The canonical content address. */ readonly sha256: string; /** Document taxonomy kind (e.g. 'adr', 'spec', 'research'). Null for raw blobs. */ readonly kind: DocKind | null; /** Display title. Falls back to slug, then blob name. */ readonly title: string | null; /** Stable human-readable slug from attachments.slug. Null for slug-less blobs. */ readonly slug: string | null; /** * The DISPLAY number to render for this doc (e.g. ADR "051"), decoupled from * the slug (T11875). Resolved via {@link resolveDisplayNumber}: the stored * `attachments.display_alias` wins when set, else the slug-derived number, else * `null` for non-numbered / slug-less docs. */ readonly displayNumber: number | null; /** Owner entity ID (task/session/observation ID). */ readonly ownerId: string; /** Owner entity type. */ readonly ownerType: string; /** Attachment or blob name (e.g. "spec.md"). */ readonly blobName: string; /** Byte size of the stored content. */ readonly sizeBytes: number; /** * Number of owners referencing this doc, sourced from `attachments.ref_count` * in tasks.db. Zero for manifest-only blobs (which have no ref-count concept) * and for freshly-added docs not yet attached to an owner. Surfacing the real * value keeps `docs list` consistent with the `docs add` response (T11572). */ readonly refCount: number; /** IANA MIME type, when known. */ readonly mimeType: string | null; /** Human-readable summary from attachments.summary. */ readonly summary: string | null; /** Lifecycle status: 'active', 'archived', 'draft', etc. */ readonly lifecycleStatus: string; /** ISO 8601 creation timestamp from the primary store. */ readonly createdAt: string; /** Project-root-relative published file path, or null if unpublished. */ readonly publishedPath: string | null; /** ISO 8601 timestamp of latest publish, or null if unpublished. */ readonly publishedAt: string | null; /** SHA of the blob at the time of publish, or null if unpublished. */ readonly lastPublishedBlobSha: string | null; /** Drift classification between published file and backing blob. */ readonly publicationDrift: 'in-sync' | 'modified' | 'deleted' | 'unpublished'; /** Which backing store this doc was resolved from. */ readonly source: 'tasks-db' | 'manifest-db' | 'merged'; } /** * Discriminated result of {@link DocsReadModel.fetchDecoded}. * * The success arm carries both the resolved doc record and its decoded * UTF-8 body; the failure arms distinguish a missing reference from a * resolvable doc whose blob could not be read. */ export type DecodedDocResult = { readonly ok: true; readonly doc: ResolvedDoc; readonly content: string; } | { readonly ok: false; readonly reason: 'not-found'; } | { readonly ok: false; readonly reason: 'no-content'; readonly doc: ResolvedDoc; }; /** * Filters for {@link DocsReadModel.listProjectDocs}. */ export interface ListProjectDocsOpts { /** Restrict to a single document kind. */ kind?: string; /** Maximum number of results. Default: 200. */ limit?: number; /** Whether to only return docs with a slug. Default: false. */ sluggedOnly?: boolean; /** Whether to include unpublished docs. Default: true. */ includeUnpublished?: boolean; } /** * Options for constructing a {@link DocsReadModel}. */ export interface DocsReadModelOptions { /** * Absolute path to the CLEO project root (the directory containing `.cleo/`). * Defaults to {@link getProjectRoot}() when omitted. */ projectRoot?: string; } /** * Unified read-side query model for the CLEO docs system. * * Hides the three-backend reality (tasks.db attachments, manifest.db blobs, * docs-publications.json ledger) behind a single typed query surface. * CLI command handlers should depend on this class rather than calling * `AttachmentStore`, `blobList`, or `readPublicationsLedger` directly. * * Construction is cheap — the underlying stores are opened lazily on * first use and closed via {@link close}. * * @example * ```ts * const model = createDocsReadModel({ projectRoot: '/path/to/project' }); * * // Resolve the latest version of an ADR by slug * const doc = await model.resolveBySlug('adr-068-db-charter'); * * // List all docs owned by a task * const docs = await model.resolveByOwner('T11049'); * * // Check publication status * const pubs = await model.listProjectDocs({ sluggedOnly: true }); * ``` */ export declare class DocsReadModel { private readonly projectRoot; private publicationCache; constructor(options?: DocsReadModelOptions); /** * Resolve a single document by its stable slug. * * Queries the tasks.db attachments table for a row whose `slug` column * matches. When found, enriches the result with blob-store metadata and * publication status. * * @param slug - The exact slug to look up (case-sensitive). * @returns A resolved doc, or `null` if no attachment carries this slug. */ resolveBySlug(slug: string): Promise; /** * Resolve all documents owned by a given entity. * * Merges results from both tasks.db (via `listByOwner`) and manifest.db * (via `blobList`). Tasks.db attachments take precedence when both stores * have entries for the same owner + blob name combination. * * @param ownerId - Task, session, or observation ID (e.g. "T11049"). * @param opts.ownerType - Owner type. Defaults to 'task'. * @param opts.kind - Optional kind filter. * @returns Array of resolved docs (may be empty). */ resolveByOwner(ownerId: string, opts?: { ownerType?: string; kind?: string; }): Promise; /** * Resolve a single document by attachment ID or SHA-256 content hash. * * Searches both tasks.db (by UUID attachment ID) and the blob store * (by SHA-256 content address). Tasks.db is tried first. * * @param id - Attachment UUID or 64-char hex SHA-256. * @returns A resolved doc, or `null` if no match is found. */ resolveByAttachmentId(id: string): Promise; /** * Resolve a document by blob name within an owner's scope. * * Typical use: looking up the latest version of a published doc where * you know the owner and the blob name but not the slug. * * @param ownerId - Owner entity ID. * @param blobName - The blob/attachment name. * @returns A resolved doc, or `null` if not found. */ resolveByBlobName(ownerId: string, blobName: string): Promise; /** * Resolve the latest version of a document by slug. * * Alias for {@link resolveBySlug} with version semantic baked into the * name so callers (status/fetch/publish/list commands) know this is the * canonical "latest" resolution path. * * @param slug - Stable slug. * @returns The latest resolved doc, or `null`. */ resolveLatest(slug: string): Promise; /** * List all documents in the project, merged from all backing stores. * * Deduplicates by SHA-256 content hash + slug, preferring tasks.db entries * over raw blob store entries when both exist for the same slug. * * @param opts - Optional filters and pagination. * @returns Array of resolved docs sorted by creation date (newest first). */ listProjectDocs(opts?: ListProjectDocsOpts): Promise; /** * Fetch the raw content bytes for a resolved document. * * The read model's resolution methods do NOT load content by default * (keeps list/status operations fast). Call this when you need the * actual text/bytes. * * @param doc - A resolved doc (from any resolution method). * @returns The raw content as a UTF-8 string, or `null` if the blob * cannot be read. */ fetchContent(doc: ResolvedDoc): Promise; /** * Resolve a reference (slug, attachment ID, or SHA-256) and return its * decoded UTF-8 body in a single call. * * Convenience wrapper around the resolve → {@link fetchContent} two-step * used by agent-facing surfaces (e.g. `cleo docs fetch --content`) that * want the raw document text without first hand-decoding the base64 * `bytesBase64` field from the LAFS fetch envelope (T10970). * * Resolution order mirrors the canonical fetch path: slug first (the * common agent ergonomic), then attachment ID / SHA-256 content address. * * @param ref - Slug, attachment UUID, or 64-char hex SHA-256. * @returns A discriminated result: * - `{ ok: true, doc, content }` when the ref resolved AND content was * retrievable. * - `{ ok: false, reason: 'not-found' }` when no doc carries the ref. * - `{ ok: false, reason: 'no-content', doc }` when the doc metadata * exists but its blob could not be read (e.g. purged or unpublished). */ fetchDecoded(ref: string): Promise; /** * Fetch content for multiple docs in parallel. * * @param docs - Resolved docs array. * @returns Map of doc id → content string. Docs whose content could * not be fetched are omitted. */ fetchContentBatch(docs: ResolvedDoc[]): Promise>; /** * Release any cached resources. The read model is stateless aside from * the publication cache; calling this is optional but recommended for * long-running processes to free memory. */ close(): void; /** * Check publication status for all published docs. * * Compares the blob SHA in the store against the file SHA on disk * for every entry in the docs-publications ledger. * Returns the same shape as `statusDocs()` for backward compatibility. */ status(projectRootOverride?: string): Promise<{ readonly items: readonly { readonly ownerId: string; readonly blobName: string; readonly publishedPath: string; readonly blobSha: string; readonly fileSha: string | null; readonly drift: 'in-sync' | 'added' | 'modified' | 'deleted'; }[]; readonly allInSync: boolean; }>; /** * Resolve docs from tasks.db attachments for a given owner. */ private resolveFromTasksDbByOwner; /** * Resolve ALL docs from tasks.db. */ private resolveAllFromTasksDb; /** * Resolve docs from manifest.db blobs for a given owner. */ private resolveFromManifestByOwner; /** * Resolve ALL docs from manifest.db across all known owners. * * Since manifest.db does not support "list all owners" natively, this * is best-effort — it queries all owner IDs we know about from tasks.db * and the publications ledger. */ private resolveAllFromManifest; /** * Resolve a doc from the blob store by SHA-256 content hash. * * This requires scanning owners since manifest.db indexes by (owner, name). * For large projects this is expensive — prefer attachment-ID lookup. */ private resolveFromBlobStoreBySha; /** * Build a ResolvedDoc from a manifest.db blob entry. */ private buildResolvedDocFromBlob; /** * Build a ResolvedDoc from a blob entry. */ private buildFromBlobEntry; /** * Enrich a tasks.db attachment with blob-store and publication context. */ private enrichFromTasksDb; /** * Load publications from the JSON ledger, cached in memory. */ private loadPublications; } /** * Create a DocsReadModel for the given project root. * * CLI command handlers should call this factory instead of constructing * `DocsReadModel` directly (follows ADR-069 Coordination Layers boundary). * * @param projectRoot - Absolute path to the CLEO project root. * @returns A fully initialized DocsReadModel. */ export declare function createDocsReadModel(projectRoot?: string): DocsReadModel; //# sourceMappingURL=docs-read-model.d.ts.map