/** * Dependency-cycle guard support for `tasks_task_dependencies` (T12886). * * The guard itself is the pair of BEFORE triggers created by the * `drizzle-cleo-project/20260930160000_t12886-dependency-cycle-guard` * migration: every writer, raw SQL included, is refused with * {@link DEPENDENCY_CYCLE_CODE}. A trigger message must be a literal, so it * cannot say WHICH cycle. This module supplies that part: * * - {@link rethrowDependencyCycle} turns the trigger abort into a * {@link CleoError} that names the cycle (`T1 → T2 → T3 → T1`) and says * how to fix it. The write chokepoints call it. * - {@link detectDependencyCycles} finds cycles already stored (edges written * before the guard existed) and proposes the edges to remove, for * `cleo doctor dep-cycles`. It never writes. * * An edge `{ taskId, dependsOn }` means "taskId depends on dependsOn"; a cycle * is printed in that direction, each arrow reading "depends on". * * @module * @task T12886 */ import type { NodeSQLiteDatabase } from 'drizzle-orm/node-sqlite'; import { CleoError } from '../errors.js'; /** Stable error code raised by the dependency-cycle triggers. */ export declare const DEPENDENCY_CYCLE_CODE = "E_TASK_DEPENDENCY_CYCLE"; /** One dependency edge: `taskId` depends on `dependsOn`. */ export interface DependencyEdge { /** The dependent task. */ readonly taskId: string; /** The task it waits for. */ readonly dependsOn: string; } /** A proposed repair: remove this edge. */ export interface DependencyCycleRepair { /** The edge to remove. */ readonly edge: DependencyEdge; /** The command that removes it. */ readonly command: string; /** The cycle this removal breaks, in "depends on" order, first id repeated last. */ readonly breaks: readonly string[]; } /** One strongly connected component of the dependency graph that contains a cycle. */ export interface DependencyCycleComponent { /** Every task in the component, sorted. */ readonly tasks: readonly string[]; /** One cycle through the component, first id repeated last. */ readonly cycle: readonly string[]; } /** Result of {@link detectDependencyCycles}. */ export interface DependencyCycleReport { /** Edges examined. */ readonly edgeCount: number; /** Components that contain at least one cycle (self-dependencies included). */ readonly components: readonly DependencyCycleComponent[]; /** * Edges whose removal leaves the graph acyclic: the back edges of one * depth-first traversal. Not guaranteed minimal (that problem is NP-hard); * review before applying. */ readonly repairPlan: readonly DependencyCycleRepair[]; } /** Render a cycle as `T1 → T2 → T1`. */ export declare function formatDependencyCycle(cycle: readonly string[]): string; /** * The cycle that `edge` would close over `existing`, or `null` when it closes none. * * @param existing - Edges already stored. * @param edge - The edge being added. * @returns The cycle in "depends on" order, starting and ending at `edge.taskId`. * * @example * ```ts * findClosedCycle([{ taskId: 'T2', dependsOn: 'T1' }], { taskId: 'T1', dependsOn: 'T2' }); * // => ['T1', 'T2', 'T1'] * ``` */ export declare function findClosedCycle(existing: Iterable, edge: DependencyEdge): string[] | null; /** * Build the error for an edge that closes `cycle`. * * @param edge - The refused edge. * @param cycle - The cycle it closes, from {@link findClosedCycle}. * @returns A {@link CleoError} naming the cycle, with a fix hint. */ export declare function dependencyCycleError(edge: DependencyEdge, cycle: readonly string[]): CleoError; /** * True when `err` is the dependency-cycle trigger abort. Drizzle wraps the * SQLite error in a `DrizzleQueryError` whose own message is the failed SQL, * so the `cause` chain is searched too. */ export declare function isDependencyCycleAbort(err: unknown): boolean; /** * Read every stored dependency edge. * * @param db - Drizzle handle on the project store. * @returns All rows of `tasks_task_dependencies`. */ export declare function readDependencyEdges(db: NodeSQLiteDatabase): DependencyEdge[]; /** * Rethrow a write error, naming the cycle when it is the cycle-guard abort. * * Call it from the catch of a statement that inserted `attempted`. SQLite * rolls back the aborted statement, so the stored edges are the state before * it; the attempted edges are replayed over them in order to find the one * that closes a cycle. * * @param db - Drizzle handle the statement ran on. * @param err - The caught error. * @param attempted - The edges the statement tried to insert, in order. * @returns Never; always throws. */ export declare function rethrowDependencyCycle(db: NodeSQLiteDatabase, err: unknown, attempted: readonly DependencyEdge[]): never; /** A dependency edge left out because it would close a cycle. */ export interface SkippedDependencyEdge { /** The edge that was not written. */ readonly edge: DependencyEdge; /** The cycle it would have closed, first id repeated last. */ readonly cycle: readonly string[]; /** One-line explanation naming the cycle. */ readonly message: string; } /** * Insert one edge (ignoring an exact duplicate); when the cycle guard refuses * it, leave it out and say why instead of failing. * * For bulk importers that must keep a record and its other edges when one * edge is refused (legacy JSON import). Any other error propagates. * * @param db - Drizzle handle on the project store. * @param edge - The edge to insert. * @returns `null` when written (or already present), else what was skipped. */ export declare function insertDependencyEdgeOrSkipCycle(db: NodeSQLiteDatabase, edge: DependencyEdge): SkippedDependencyEdge | null; /** * Split planned edges into the ones that can be written and the ones that * would close a cycle, replaying them in order over `existing`. Deterministic: * of two edges that close a cycle together, the LATER one is dropped. Pure. * * @param existing - Edges already stored in the target. * @param planned - Edges to add, in plan order. * @returns The dropped edges with their cycles; every other planned edge is safe. */ export declare function dropCycleClosingEdges(existing: readonly DependencyEdge[], planned: readonly DependencyEdge[]): SkippedDependencyEdge[]; /** * Find every stored dependency cycle and a repair plan. Pure: never writes. * * Tarjan's algorithm (iterative, so a long chain cannot overflow the stack) * finds the strongly connected components; any component with more than one * task, or a task that depends on itself, holds a cycle. The repair plan is * the back edges of a depth-first traversal, whose removal leaves the graph * acyclic. * * @param edges - The stored edges. * @returns Cyclic components and the edges to remove. * * @example * ```ts * const report = detectDependencyCycles(readDependencyEdges(db)); * for (const r of report.repairPlan) console.log(r.command); * ``` */ export declare function detectDependencyCycles(edges: readonly DependencyEdge[]): DependencyCycleReport; //# sourceMappingURL=dependency-cycles.d.ts.map