/** * Evidence ↔ AC binding pruning — the single write path that removes * `tasks_evidence_ac_bindings` rows (T12790). * * ## Why this exists * * The consolidated project schema (`drizzle-cleo-project/20260531000001`) * declares NO foreign key from `tasks_evidence_ac_bindings.ac_id` to * `tasks_task_acceptance_criteria.id`. The legacy table's `ON DELETE CASCADE` * did not survive consolidation, so every AC row that left the store — * through an AC edit ({@link import('../tasks/ac-table.js').applyAcPlan}) or a * hard task delete (the AC rows cascade from `tasks_tasks`) — left its * bindings dangling. Adding the FK back would need a table rebuild, so the * accessor deletes the bindings explicitly instead, in the same transaction * that removes the AC rows. * * ## History is kept in the audit log, not in the bindings table * * One reader depends on historical bindings: alias-drift detection in * `satisfies-validator.ts` (ADR-079-r2 §3) compares an alias atom's current * AC against the AC a previous `satisfies` binding recorded. A binding for a * removed AC is meaningless to the coverage gate, but it is exactly the * record that says "alias AC2 used to mean X". So before a binding is * deleted, its full row is written to `tasks_audit_log` under * {@link AC_BINDINGS_PRUNED_ACTION}, keyed by the task that owned the AC, and * the drift detector reads those rows as well as the live table. * * Every function takes the drizzle handle of an ALREADY-OPEN accessor * transaction; none of them opens or commits one. * * @task T12790 * @adr ADR-079-r2 */ import type { AcBindingRow } from '@cleocode/contracts'; import type { getDb } from './sqlite.js'; /** Drizzle handle for the project tasks store. */ type TasksDb = Awaited>; /** `tasks_audit_log.action` recorded for every batch of pruned bindings. */ export declare const AC_BINDINGS_PRUNED_ACTION = "ac.bindings.pruned"; /** * Why a batch of bindings was pruned. * * - `ac-removed` — the AC rows left the task's set in an AC edit. * - `task-removed` — the owning task was hard-deleted (its AC rows cascade). * - `orphan-repair` — `cleo doctor ac-bindings --fix` removed rows whose AC * was already gone before T12790. */ export type AcBindingPruneReason = 'ac-removed' | 'task-removed' | 'orphan-repair'; /** `details` payload of a {@link AC_BINDINGS_PRUNED_ACTION} audit row. */ export interface AcBindingsPrunedDetails { /** Why the bindings were removed. */ readonly reason: AcBindingPruneReason; /** The AC ids whose bindings were removed. */ readonly acIds: readonly string[]; /** The removed binding rows, verbatim. */ readonly bindings: readonly AcBindingRow[]; } /** Owner key used when an orphan's owning task cannot be inferred. */ export declare const UNKNOWN_BINDING_OWNER = "unknown"; /** * Generate a task audit log row id in the `log--` shape. * * Shared by the task accessor's `appendLog` and the binding prune below, so * both write ids of one shape. * * @task T4837 · T12790 */ export declare function generateAuditLogId(): string; /** * Delete every binding whose `ac_id` is in `acIds`, recording them first in * the audit log under `ownerTaskId` (the task that owned those ACs). * * Call it in the same transaction that removes the AC rows, BEFORE or AFTER * the AC delete — the bindings table has no FK, so order does not matter. * * @param db - drizzle handle of the open accessor transaction * @param ownerTaskId - task that owned the removed ACs * @param acIds - AC ids leaving the store * @param reason - why they are leaving * @returns number of bindings removed * * @task T12790 */ export declare function pruneAcBindingsForAcIds(db: TasksDb, ownerTaskId: string, acIds: readonly string[], reason: AcBindingPruneReason, keepBindingsForUids?: readonly string[]): Promise; /** * Delete the bindings of every AC `taskId` owns — the hard task-delete path * (the AC rows go by `ON DELETE CASCADE` from `tasks_tasks`) and the * delete-every-AC-row path. * * @param db - drizzle handle of the open accessor transaction * @param taskId - task whose AC rows are all leaving * @param reason - `task-removed` (default) or `ac-removed` * @returns number of bindings removed * * @task T12790 */ export declare function pruneAcBindingsForTask(db: TasksDb, taskId: string, reason?: AcBindingPruneReason): Promise; /** * Read every binding whose `ac_id` names no row of * `tasks_task_acceptance_criteria`. Read-only. * * @param db - drizzle handle for the project tasks store * @returns the dangling bindings, oldest first * * @task T12790 */ export declare function selectOrphanAcBindings(db: TasksDb): Promise; /** * Infer the task that owned an orphan binding's AC. * * Only `satisfies:->#` atom ids name it (the target owns * the AC). Every other atom shape yields {@link UNKNOWN_BINDING_OWNER}. * * @param binding - an orphan binding * @returns owning task id, or `'unknown'` * * @task T12790 */ export declare function inferBindingOwnerTask(binding: AcBindingRow): string; /** * Remove every orphan binding, one audit row per inferred owning task. * * @param db - drizzle handle of the open accessor transaction * @returns the removed bindings * * @task T12790 */ export declare function pruneOrphanAcBindings(db: TasksDb): Promise; /** * Read the bindings that were pruned from ACs owned by `ownerTaskId`, from * their {@link AC_BINDINGS_PRUNED_ACTION} audit rows. Read-only. * * This is where binding history lives once the rows leave * `tasks_evidence_ac_bindings`; alias-drift detection reads it. * * @param db - drizzle handle for the project tasks store * @param ownerTaskId - task that owned the pruned ACs * @returns the pruned binding rows (malformed audit payloads are skipped) * * @task T12790 */ export declare function selectPrunedAcBindings(db: TasksDb, ownerTaskId: string): Promise; export {}; //# sourceMappingURL=ac-binding-prune.d.ts.map