/** * Worktree-build schema-write guard (T12687). * * A CLI built inside a linked git worktree carries that branch's UNRELEASED * migrations and schema code. Path resolution maps a worktree to its parent * project's store (T12460), so running that build — `cleo check arch`, an * unsandboxed CLI test, a reviewer trying a PR build — changed the schema of * the LIVE main-checkout store. Incident 2026-09-29: cleo-nexus's slice-2 * build wrote `__drizzle_migrations` row 114 into the live cleocode store; the * released CLI then deleted the unknown row as an orphan and the next slice-2 * open re-ran `ADD COLUMN` and failed. * * ## One decision per store, enforced on the handle * * {@link schemaWriteRefusal} decides, once per store path, whether this build * may change that store's schema. When it may not, * {@link installSchemaWriteGuard} installs a SQLite authorizer on the handle * that DENIES every schema change — `ALTER`, `DROP`, and any `CREATE` of an * object that does not already exist (a `CREATE … IF NOT EXISTS` of an * existing object is a no-op and stays allowed). That covers every DDL site * at once — drizzle migrations, `ensureColumns`, raw `ALTER`s, table * rebuilds, twin collapse, exodus — including sites added later. Migration * sites additionally fail fast with {@link WorktreeBuildSchemaError} when the * build has pending migrations: the store would not match what this build * reads, so it refuses rather than failing later with `no such column`. * * ## Who is a dev build * * The build stamp `dist/build-provenance.json` (written at build time) names * the linked worktree the build was made in, or null for a main checkout / CI * clone — how released packages are built. The stamp travels with the build, * so an `npm pack`ed worktree build installed globally, or a `dist` copied * outside any checkout, is still recognised. Without a stamp (running from * source), the running file's own location decides, and a path under * `node_modules` counts as installed. * * Allowed without an opt-in: stores inside the build's own worktree, and the * test harness's fixtures (under `VITEST`, a store below `os.tmpdir()` — the * fork sandbox; or a store below a {@link TEST_SANDBOX_MARKER} for CLI * children a test spawns). `VITEST` alone is NOT an exemption: a test that * reaches a real checkout's store is refused (T12737). Outside the harness a * temp directory alone is not one either — a project cloned under `/tmp` is a * real store. Everything else needs `CLEO_ALLOW_WORKTREE_BUILD_MIGRATIONS=1`. * * @task T12687 */ import type { DatabaseSync } from 'node:sqlite'; import type { MigrationMeta } from 'drizzle-orm/migrator'; import { CleoError } from '../errors.js'; /** Opt-in that lets a worktree build change the schema of a store outside its worktree. */ export declare const ALLOW_WORKTREE_BUILD_MIGRATIONS_ENV = "CLEO_ALLOW_WORKTREE_BUILD_MIGRATIONS"; /** * File the test harness (`vitest.setup.ts`) writes at the root of each fork's * sandbox. A store below it is a fixture — including one opened by a CLI child * process the test spawned: such a child carries `VITEST` only when the test * passes its own environment through, and many build an explicit `env`. * A marker inside a git checkout is ignored: the sandbox is never in one, so a * marker there was committed or planted in a real project. */ export declare const TEST_SANDBOX_MARKER = ".cleo-test-sandbox"; /** Error code of {@link WorktreeBuildSchemaError}. */ export declare const E_WORKTREE_BUILD_SCHEMA = "E_WORKTREE_BUILD_SCHEMA"; /** Build stamp written by `scripts/write-build-provenance.mjs`. */ export interface BuildProvenance { /** * Linked worktree the build was made in, or null (main checkout / CI clone). * Absent when git could not tell at build time — the guard then decides from * the build's path instead of trusting the stamp. */ linkedWorktree: string | null; } /** Inputs of {@link schemaWriteRefusal}; defaults describe this process. */ export interface WorktreeBuildGuardOptions { /** A file of the running build. Defaults to this module. */ codePath: string; /** The build stamp; `undefined` reads it from beside the build, `null` means none. */ provenance: BuildProvenance | null | undefined; /** Environment consulted for the opt-in and the test harness. */ env: NodeJS.ProcessEnv; /** Honour {@link TEST_SANDBOX_MARKER} (default true; guard tests turn it off). */ honourTestSandbox: boolean; /** * Directories whose stores the test harness (`VITEST`) may migrate from a * worktree build. Defaults to `[os.tmpdir()]` — inside vitest, the fork * sandbox's temp directory. A store anywhere else (a real checkout's * `.cleo/cleo.db`) is refused even under `VITEST` (T12737). */ fixtureRoots: readonly string[]; } /** * Replace the guard's view of the running build (tests only: a test process * runs from source inside the harness, so it cannot be a dev build itself). * * @param next - Partial options, or null to restore the defaults. * @internal */ export declare function setWorktreeBuildGuardForTests(next: Partial | null): void; /** * Root of the linked git worktree containing `path`, or null when the nearest * enclosing checkout is a main checkout (or there is none). * * @param path - Any path. * @returns The linked worktree root, or null. */ export declare function linkedWorktreeRootOf(path: string): string | null; /** * The build stamp beside the running build: the nearest `build-provenance.json` * walking up from `codePath`, stopping at the package root. * * @param codePath - A file of the running build. * @returns The stamp, or null when there is none (running from source). */ export declare function readBuildProvenance(codePath: string): BuildProvenance | null; /** * The linked worktree this build came from, or null for a released build. * * @param options - Injection points; defaults describe this process. * @returns The worktree root, or null. */ export declare function devBuildWorktree(options?: Partial): string | null; /** * Why this build must not change the schema of `dbPath`, or null when it may. * * @param dbPath - Store path. * @param options - Injection points; defaults describe this process. * @returns The refusal naming the build's worktree, the store and the opt-in. * @task T12687 */ export declare function schemaWriteRefusal(dbPath: string, options?: Partial): string | null; /** Backwards-compatible alias of {@link schemaWriteRefusal}. */ export declare const worktreeBuildMigrationRefusal: typeof schemaWriteRefusal; /** * Whether this build may change the schema of the store behind `nativeDb`. * * @param nativeDb - Store handle (`location()` is its path; in-memory is always allowed). * @returns `true` when schema writes are allowed. */ export declare function schemaWritesAllowed(nativeDb: DatabaseSync): boolean; /** Refusal raised when a dev build would change a foreign store's schema (T12687). */ export declare class WorktreeBuildSchemaError extends CleoError { constructor(message: string); } /** Whether `error` is (or wraps) a {@link WorktreeBuildSchemaError}. */ export declare function isWorktreeBuildSchemaError(error: unknown): boolean; /** * Install the schema-write guard on a freshly opened writable handle. Call it * right after opening, before any schema code runs. * * Only a dev build (outside the test harness) installs anything; released * builds pay nothing. The authorizer then applies, PER SCHEMA, the decision * for that schema's file: on a refused schema it denies `ALTER`, `DROP` and * any `CREATE` of an object that does not already exist. That covers the main * schema and any store ATTACHed later — an exempt handle (in-memory or inside * the build's own worktree) that attaches a foreign store is guarded on the * attached schema too. * * @param nativeDb - Freshly opened writable handle. * @returns `true` when the main schema itself is refused. * @task T12687 */ export declare function installSchemaWriteGuard(nativeDb: DatabaseSync): boolean; /** * Re-read the schema map after an `ATTACH`, so the attached schema gets its own * decision and no-op `CREATE … IF NOT EXISTS` statements on it stay allowed. * * @param nativeDb - A handle passed to {@link installSchemaWriteGuard}. */ export declare function refreshSchemaWriteGuard(nativeDb: DatabaseSync): void; /** * Turn SQLite's bare `not authorized` from a guarded handle into the refusal * that says why and what to do. Other errors are returned unchanged. * * @param nativeDb - The handle the error came from. * @param error - The caught error. * @returns The error to rethrow. */ export declare function explainSchemaWriteDenial(nativeDb: DatabaseSync, error: unknown): unknown; /** * Migrations of one lineage that `nativeDb`'s journal does not yet record. * * @param nativeDb - Store handle. * @param migrations - The lineage's local migrations. * @returns Names (or hashes) of the pending migrations; all of them when the * journal table does not exist yet. */ export declare function pendingMigrations(nativeDb: DatabaseSync, migrations: readonly MigrationMeta[]): string[]; /** * The guard at a migration site: when this build may not change the store's * schema and the lineage has pending migrations, FAIL FAST — the store does * not match what this build reads, so continuing would only fail later with * `no such column`. * * @param nativeDb - Store handle. * @param migrations - The lineage's local migrations. * @throws {WorktreeBuildSchemaError} naming the build worktree, the store, * the pending migrations and the opt-in. * @task T12687 */ export declare function assertNoPendingMigrationsForWorktreeBuild(nativeDb: DatabaseSync, migrations: readonly MigrationMeta[]): void; //# sourceMappingURL=worktree-build-guard.d.ts.map