/** * DB-substrate survey primitives for `cleo doctor db-substrate`. * * Walks every entry in the `DB_INVENTORY` SSoT (`@cleocode/contracts`), * resolves each entry's `filePathTemplate` to an on-disk path, and * reports integrity, recent-write timestamp, file size, and up to 3 * representative row counts per role. * * Two modes: * * - `surveyProjectDbSubstrate(projectRoot)` — survey one project plus * the global tier of databases. * - `surveyFleetDbSubstrate(fleetRoot)` — walk every immediate * subdirectory of `fleetRoot` that contains a `.cleo/` and survey * each as a project. Surfaces orphan-project-root + * nested-nexus-duplicate warnings (T9550 regression class) that the * fleet survey detects en route. * * Path resolution honours the inventory's `` and * `$XDG_DATA_HOME` tokens via `@cleocode/paths` (`getCleoHome`). All * DB opens flow through `openCleoDbSnapshot` (in `packages/core/src/store/`, * therefore inside the `db-open-guard` allowlist). * * @task T10307 * @task T10311 — per-DB Drizzle migration coverage cross-check * @epic T10282 * @saga T10281 * @see ADR-068 — CLEO Database Charter */ import { type DbCrossDbOrphanReport, type DbInventoryEntry, type DbSubstrateAuditResult, type DbSubstrateEntry, type DbSubstrateMigrationCoverage, type DbSubstrateProjectSurvey, type DbSubstrateSummary, type DbSubstrateSurveyOptions, type DbSubstrateWarning, type PragmaDriftItem } from '@cleocode/contracts'; import { openCleoDbSnapshot } from '../store/open-cleo-db.js'; /** * Compute the stable project-id used to identify a project in * cross-project surveys. Matches the convention used by * `cleo nexus`: `base64url(path).slice(0, 32)`. * * @param projectRoot - Absolute path to the project root. * @returns A 32-character base64url-encoded slice of the path. */ export declare function computeSubstrateProjectId(projectRoot: string): string; /** * Substitute `` / `$XDG_DATA_HOME` tokens in an inventory * entry's `filePathTemplate` to an absolute on-disk path. * * Project-tier and derived-tier entries are anchored at `projectRoot`; * global-tier entries are anchored at `getCleoHome()`. * * @param entry - One row from `DB_INVENTORY`. * @param projectRoot - Absolute path to the project root (used only for * `` substitution). * @returns The resolved absolute path. */ export declare function resolveInventoryFilePath(entry: DbInventoryEntry, projectRoot: string): string; /** * Walk the canonical drift-pragma list against an open snapshot handle * and report every pragma whose actual value diverges from the SSoT. * * @remarks * Reads each pragma name from {@link PragmaSsot.driftPragmas} (sourced * from `specs/sqlite-pragmas.json#driftPragmas`), runs `PRAGMA ` * against `snapshotDb`, normalises the result via * {@link normalisePragmaValue}, and compares against the SSoT-declared * expected value (case-insensitively). * * Pragmas whose query throws are surfaced with `actual: null` so the * envelope reader can distinguish "differs" from "could not measure". * * **Important**: this function expects the snapshot to have been opened * WITHOUT `applyPragmas` (i.e. `applyPragmas: false`). The drift report * measures what the DB actually carries on disk + the connection's * defaults, NOT what `applyPerfPragmas` would set after open. This * captures the case where a discovery tool or legacy opener queries the * DB without going through the SSoT — exactly the regression class * Saga T10281 / Epic T10283 was filed to surface. * * @param snapshotDb - Open read-only snapshot handle. * @returns Drift items; empty array when every queried pragma matches * the canonical expectation. * * @task T10310 * @epic T10283 * @saga T10281 */ export declare function walkPragmaDrift(snapshotDb: ReturnType['db']): PragmaDriftItem[]; /** * Resolve the absolute path to the migrations folder for an inventory * entry whose `migrationsDir` is non-null. * * @remarks * The inventory stores `migrationsDir` as a repo-relative path * (e.g. `packages/core/migrations/drizzle-tasks/`). At runtime the * @cleocode/core package ships those migrations under its own root, so * we delegate to {@link resolveCorePackageMigrationsFolder} which works * across workspace-dev, bundled, and global-install layouts. The set * name is the trailing path segment of `migrationsDir` (with the * trailing slash stripped). * * @param migrationsDir - Repo-relative path from `DbInventoryEntry.migrationsDir`. * @returns Absolute path to the migrations folder. * * @task T10311 */ export declare function resolveInventoryMigrationsFolder(migrationsDir: string): string; /** * Cross-reference `__drizzle_migrations` rows against migration files on * disk and produce a {@link DbSubstrateMigrationCoverage} diff. * * @remarks * The cross-reference uses Drizzle's canonical SHA-256(migration.sql) * hash via `readMigrationFiles` — the same algorithm used by * `migrate()` and `reconcileJournal`. Hashes are the authoritative * identifier; folder names are surfaced for human readability only. * * Failure modes that return `null` (not an error): * - The DB has no `__drizzle_migrations` table (reserved opener, * fresh DB before bootstrap). * - `readMigrationFiles` throws (e.g. migrationsDir does not exist * in the running install — should never happen in production but * can during test fixtures that point at a temp path). * * @param snapshotDb - Open read-only snapshot of the target DB. * @param migrationsFolderPath - Absolute path to the migrations folder. * @returns A populated {@link DbSubstrateMigrationCoverage}, or `null` * when the cross-reference cannot be performed. * * @task T10311 */ export declare function computeMigrationCoverage(snapshotDb: ReturnType['db'], migrationsFolderPath: string): DbSubstrateMigrationCoverage | null; /** * Inspect one resolved DB file and produce the per-role substrate entry. * * @remarks * Performs: * 1. `fs.statSync` for size + mtime + existence check. * 2. `openCleoDbSnapshot` (read-only) for integrity_check + row counts. * The integrity_check call is bounded by * {@link DbSubstrateSurveyOptions.integrityCheckTimeoutMs} (default 60 s) * — wall-clock measured; SQLite work capped via * `PRAGMA integrity_check({@link INTEGRITY_CHECK_ERROR_CAP})`. * 3. On failure (integrity_check returned non-`'ok'`, open threw, OR * elapsed exceeded the timeout) AND `autoQuarantine !== false`, * moves the corrupt DB plus `-wal`/`-shm` sidecars into * `/.cleo/quarantine/-malformed-/` (T10312). * 4. Returns a fully-populated {@link DbSubstrateEntry} — all `null` * fields are explicit rather than absent. * * Snapshot handle is always closed before returning, even on error. * * @param entry - The `DB_INVENTORY` row being inspected. * @param filePath - The resolved absolute path to the DB file. * @param options - Tuning knobs for timeout + quarantine behaviour. * Defaults: `integrityCheckTimeoutMs=60000`, `autoQuarantine=true`. * @returns A populated {@link DbSubstrateEntry}. */ export declare function inspectDbFile(entry: DbInventoryEntry, filePath: string, options?: DbSubstrateSurveyOptions): DbSubstrateEntry; /** * I1 invariant: `brain_memory_links.task_id` (brain.db) must reference an * existing `tasks.id` row in tasks.db. * * @remarks * Strategy: read every distinct `task_id` from brain.db's * `brain_memory_links` (LIMIT 100), then for each candidate ask tasks.db * whether the row exists. The walker scans at most 100 candidate IDs, * regardless of how many orphan links the brain has — operators see * "≥ 100" only when the population truly exceeds the cap. * * @param tasksSnap - Open snapshot of tasks.db, or `null` when missing. * @param brainSnap - Open snapshot of brain.db, or `null` when missing. * @returns A {@link DbCrossDbOrphanReport} for invariant I1. */ export declare function checkInvariantI1(tasksSnap: ReturnType | null, brainSnap: ReturnType | null): DbCrossDbOrphanReport; /** * I2 invariant: `blob_attachments.doc_slug` (manifest.db) used as a * `T####`-shaped task identifier must reference an existing tasks.id row. * * @remarks * CLEO uses `taskId` as the `docSlug` for blob attachments * (see `packages/core/src/store/llmtxt-blob-adapter.ts`). Doc slugs that * match the canonical `^T\d+$` shape MUST point at a live task; any * other doc-slug shape (changesets, ADRs, research notes…) is out of * scope for this invariant. * * @param tasksSnap - Open snapshot of tasks.db, or `null` when missing. * @param manifestSnap - Open snapshot of manifest.db, or `null` when missing. * @returns A {@link DbCrossDbOrphanReport} for invariant I2. */ export declare function checkInvariantI2(tasksSnap: ReturnType | null, manifestSnap: ReturnType | null): DbCrossDbOrphanReport; /** * I3 invariant: nexus.db's `project_registry.project_path` row for the * current project must match `projectRoot` (or, when no row exists, the * project simply has not been registered with the nexus yet — that is * skipped, not flagged). * * @remarks * `.cleo/project-context.json` does NOT carry a `projectId` field; the * canonical identifier is derived from `base64url(path).slice(0, 32)`. * The invariant therefore asserts that the nexus registry's recorded * `project_path` MATCHES the live project root for the computed ID. * * Detected drift: a single nexus row whose `project_path` no longer * matches `projectRoot` (e.g. project was moved on disk; the * registry still points at the old location). * * @param projectRoot - Absolute path to the project root being audited. * @param nexusSnap - Open snapshot of nexus.db, or `null` when missing. * @returns A {@link DbCrossDbOrphanReport} for invariant I3. */ export declare function checkInvariantI3(projectRoot: string, nexusSnap: ReturnType | null): DbCrossDbOrphanReport; /** * I4 invariant: llmtxt.db's `session_id` (if the schema declares one) * must reference an existing `sessions.id` row in tasks.db. * * @remarks * Schema-aware: the live llmtxt schema (`llmtxt@2026.4.x`) does NOT yet * declare a `session_id` column on any table. The walker probes * `sqlite_master` for tables carrying a `session_id` column and runs * the check only when one exists. The check stays compatible with future * llmtxt schemas that introduce session linkage without code changes. * * When a candidate column is found, the bounded query gathers distinct * `session_id` values (LIMIT 100) and cross-references each against * tasks.db's `sessions` table. * * @param tasksSnap - Open snapshot of tasks.db, or `null` when missing. * @param llmtxtSnap - Open snapshot of llmtxt.db, or `null` when missing. * @returns A {@link DbCrossDbOrphanReport} for invariant I4. */ export declare function checkInvariantI4(tasksSnap: ReturnType | null, llmtxtSnap: ReturnType | null): DbCrossDbOrphanReport; /** * I5 invariant: `dead_letters.job_id` (conduit.db) must reference either * an existing tasks.id row OR a brain anchor (page node, observation, …). * * @remarks * Conduit jobs anchor against the broader CLEO substrate — a job's * `job_id` may be a task ID (`T####`) or a brain entity (e.g. * `observation:O-…`). The walker resolves each candidate against both * targets and only flags rows that match neither. * * @param tasksSnap - Open snapshot of tasks.db, or `null` when missing. * @param brainSnap - Open snapshot of brain.db, or `null` when missing. * @param conduitSnap - Open snapshot of conduit.db, or `null` when missing. * @returns A {@link DbCrossDbOrphanReport} for invariant I5. */ export declare function checkInvariantI5(tasksSnap: ReturnType | null, brainSnap: ReturnType | null, conduitSnap: ReturnType | null): DbCrossDbOrphanReport; /** * Run every cross-DB invariant in the T10320 catalogue (I1–I5) and * return per-invariant orphan reports. * * @remarks * Resolves every DB path through the inventory SSoT, opens each as a * read-only snapshot, runs the bounded invariant queries, and closes * the snapshots before returning. Every failure mode — missing file, * malformed DB, missing column, missing table — is captured on the * corresponding report's `skipped: true` branch; the walker never * throws. * * Each invariant has its own dedicated checker (exported for unit * tests) so the integration suite can verify the per-invariant * semantics in isolation. * * @param projectRoot - Absolute path to the project root. * @returns Five {@link DbCrossDbOrphanReport} entries — one per invariant, * always in the canonical order I1, I2, I3, I4, I5. * * @task T10323 * @epic T10285 * @saga T10281 */ export declare function walkCrossDbInvariants(projectRoot: string): DbCrossDbOrphanReport[]; /** * Walk the inventory and survey every project-tier + global-tier * database visible from one project root. * * @remarks * `derived` tier entries (e.g. `manifest.db`) are surveyed the same as * project-tier ones — they too need integrity verification even though * they can be rebuilt. The caller decides whether to treat their * absence as healthy. * * @param projectRoot - Absolute path to the project root to survey. * @param options - Tuning knobs forwarded to {@link inspectDbFile}. * @returns One {@link DbSubstrateProjectSurvey} keyed by canonical role. */ export declare function surveyProjectDbSubstrate(projectRoot: string, options?: DbSubstrateSurveyOptions): DbSubstrateProjectSurvey; /** * Decide whether the `.cleo/` directory at `cleoDirPath` belongs to a * legitimate CLEO project root. * * @remarks * The legitimacy heuristic (T10308 AC2): * * "If `/.cleo/` exists AND `/.cleo/project-info.json` AND * `/.cleo/tasks.db` BOTH exist, the directory IS a legitimate * project root. Otherwise it's an orphan." * * Both markers must be present for legitimacy — `project-info.json` is * written by `cleo init`, and `tasks.db` is the canonical SQLite store. * A `.cleo/` directory missing EITHER one is treated as an orphan * (regression class T9550: stray `.cleo/.context-state.json` writes * from sibling workspaces). * * @param cleoDirPath - Absolute path to a `.cleo/` directory (i.e. the * directory under suspicion, NOT its parent). * @returns `true` when both legitimacy markers exist; `false` otherwise. * * @task T10308 */ export declare function isLegitimateCleoProjectRoot(cleoDirPath: string): boolean; /** * Best-effort read of the `workspace` field from * `/.context-state.json`. * * @remarks * The 2026-05-23 audit found that `/mnt/projects/.cleo/.context-state.json` * carried `{ workspace: '/mnt/projects/awesome-skills' }` — irrefutable * evidence that the orphan was being written by the `awesome-skills` * workspace via mis-resolved cwd. This helper surfaces that attribution * so operators can identify the offending workspace and patch it. * * Any failure mode (missing file, unparseable JSON, missing field, wrong * type) returns `null` — never throws. * * @param cleoDirPath - Absolute path to the `.cleo/` directory under * investigation. * @returns The `workspace` field when present and a string; `null` * otherwise. * * @task T10308 */ export declare function readParentWorkspace(cleoDirPath: string): string | null; /** * Detect orphan project-root `.cleo/` directories at the PARENT of a * known project root. * * @remarks * The T9550 regression class wrote `.cleo/` at a project's parent path * (e.g. `/mnt/projects/.cleo/`) when `cwd` resolution miscascaded. This * helper checks the parent for a `.cleo/` directory and, if found AND * the parent is NOT itself a legitimate CLEO project root (per * {@link isLegitimateCleoProjectRoot}), surfaces it as a warning. * * T10308 strengthens the original T10307 implementation with the * legitimacy check + `.context-state.json` attribution. * * @param projectRoot - Absolute path to one project root. * @returns A warning if `/.cleo/` exists AND is not itself a * legitimate project root; otherwise `null`. */ export declare function detectOrphanProjectRootWarning(projectRoot: string): DbSubstrateWarning | null; /** * Detect nested-nexus structural duplicates at * `/nexus/{nexus,signaldock}.db`. * * @remarks * The canonical XDG layout writes these as flat files under `cleoHome` * (e.g. `~/.local/share/cleo/nexus.db`). Older code paths sometimes * wrote them into a nested `nexus/` subdirectory. Either pattern is * harmless individually, but co-existence is a structural duplicate * and a sign of bit-rot. * * @returns Zero, one, or two warning entries (one per duplicate file). */ export declare function detectNestedNexusDuplicates(): DbSubstrateWarning[]; /** * Roll up per-DB substrate entries into the aggregate counters surfaced * in `envelope.data.summary`. * * @param surveys - All per-project surveys. * @returns A {@link DbSubstrateSummary}. */ export declare function summarizeSubstrateSurveys(surveys: readonly DbSubstrateProjectSurvey[]): DbSubstrateSummary; /** * Single-project substrate survey — covers the current project root + * the global tier of databases. * * @param projectRoot - Absolute path to the project root. * @param options - Tuning knobs forwarded to {@link inspectDbFile}. * @returns The full {@link DbSubstrateAuditResult} with `scope='project'`. */ export declare function surveyDbSubstrate(projectRoot: string, options?: DbSubstrateSurveyOptions): DbSubstrateAuditResult; /** * Multi-project (fleet) substrate survey. * * Walks every immediate subdirectory of `fleetRoot` that contains a * `.cleo/` directory, and surveys each as a project root. The global * tier is collapsed into the FIRST project's entries — running global * DB integrity checks once per fleet is enough; running them per * project would just multiply the same `integrity_check` calls. * * @param fleetRoot - Absolute path whose immediate children are * candidate project roots (e.g. `/mnt/projects/`). * @param options - Tuning knobs forwarded to {@link inspectDbFile}. * @returns A {@link DbSubstrateAuditResult} with `scope='fleet'`. */ export declare function surveyFleetDbSubstrate(fleetRoot: string, options?: DbSubstrateSurveyOptions): DbSubstrateAuditResult; //# sourceMappingURL=db-substrate.d.ts.map