/** * Recover legacy tasks whose id a DIFFERENT live task now holds (T13172). * * ## Why this exists * * A deferred exodus-on-open (#1826) left an empty consolidated `cleo.db` while * the legacy `tasks.db` still held the project. The user kept working: the * empty store allocated `T001` again (strict-spine makes it a saga) while the * legacy store also holds a `T001`. The reconcile copies with * `INSERT OR IGNORE` and assesses by key, so the legacy `T001` was skipped, * counted as present, and its children (`parent_id = 'T001'`) attached to the * new saga — while the receipt said every legacy row was present. * * The fix renumbers before copying, never after: the legacy file is copied into * scratch, every colliding legacy task gets a fresh id there, and everything * that refers to it in that copy follows, exactly as a display-id rename does * in `cleo.db`: * * - every column that holds a task id ({@link taskReferenceColumns}: declared * foreign keys plus the ROW_IDENTITY refs, key refs, owners and JSON-array * refs such as a session's `tasks_completed_json`), mapped to the legacy * table names; * - the acceptance criteria whose ids are derived from the task id * (`buildAcRowId`) are re-derived, with their evidence bindings and history * ({@link rederiveAcIdsNative}). Without that the recovered task's * "tests pass" criterion has the same id as the live `T001`'s and is * skipped by `INSERT OR IGNORE`. * * The reconcile then assesses and copies from the copy, so the copy engine, * its verification and its revert work unchanged, and live rows are never * touched. What a collision is, and when a legacy task was already recovered * by an earlier run, is decided by {@link TASK_ID_COLLISIONS_SQL}. A collision * whose creation times cannot be compared is reported, never renumbered. * * Not rewritten: task ids inside free text (titles, descriptions, notes). * * @module * @task T13172 */ import type { SupersededStoreConflict, SupersededStoreIdRemap } from '@cleocode/contracts'; import type { LegacyDbDescriptor } from './types.js'; /** The note every receipt with remaps carries about what is not rewritten. */ export declare const FREE_TEXT_IDS_NOTE = "task ids written inside free text (titles, descriptions, notes) are not rewritten"; /** A remap with the legacy facts the post-copy check and later runs compare. */ export interface PlannedRemap extends SupersededStoreIdRemap { /** The legacy row's creation time, compared with the copied row after the copy. */ legacyCreatedAt: string; } /** Result of {@link remapCollidingTaskIds}. */ export interface TaskIdRemapResult { /** The sources to assess and copy: the legacy tasks source replaced by its renumbered copy. */ readonly sources: LegacyDbDescriptor[]; /** One entry per renumbered legacy task; empty when nothing collides. */ readonly remaps: PlannedRemap[]; /** Collisions the run cannot decide (unparseable creation time), as one conflict, or `null`. */ readonly undecided: SupersededStoreConflict | null; /** Path of the renumbered copy, or `null` when no copy was needed. */ readonly remappedPath: string | null; } /** * Renumber legacy tasks whose id a different live task holds, in a scratch * copy of the legacy `tasks.db`, and return the sources to reconcile from. * * @param liveStorePath - The live project `cleo.db` (read only). * @param sources - The project's legacy sources. * @param scratch - Directory the renumbered copy is written to. * @param cleoDir - The project's resolved `.cleo`, where earlier receipts live. * @returns The sources (unchanged when nothing needs renumbering), the * remaps, and any collision the run cannot decide. * @example * ```ts * const { sources, remaps } = remapCollidingTaskIds(livePath, fileSources, scratch, cleoDir); * for (const r of remaps) console.log(`${r.legacyId} -> ${r.newId}`); * ``` */ export declare function remapCollidingTaskIds(liveStorePath: string, sources: readonly LegacyDbDescriptor[], scratch: string, cleoDir: string): TaskIdRemapResult; /** * Remaps whose new id the live store does not hold as the recovered legacy * task after the copy: a concurrent write took the id first, so the copy's * `INSERT OR IGNORE` skipped the recovered task. Empty when every remap landed. * * @param liveStorePath - The live project `cleo.db`. * @param remaps - The run's remaps. * @returns `legacyId -> newId` for each remap that did not land. * @example * ```ts * if (unlandedRemaps(live, remaps).length > 0) revert(); * ``` */ export declare function unlandedRemaps(liveStorePath: string, remaps: readonly PlannedRemap[]): string[]; /** * One line naming every remap, for a receipt's reason. * * @param remaps - The remaps of a run. * @returns E.g. `1 legacy task(s) … recovered under new ids: legacy T001 ("old") -> T004, …`. * @example * ```ts * describeRemaps(remaps); * ``` */ export declare function describeRemaps(remaps: readonly SupersededStoreIdRemap[]): string; /** * One line naming the collisions a run could not decide, for a receipt's reason. * * @param conflict - The undecided conflict. * @returns E.g. `2 legacy task(s) left uncopied (T001, T007): …`. * @example * ```ts * describeUndecided(result.undecided); * ``` */ export declare function describeUndecided(conflict: SupersededStoreConflict): string; //# sourceMappingURL=task-id-remap.d.ts.map