/** * Containment-aware scope reconciliation across a saga or epic. * * ## Why this exists * * `duplicate-detector.ts` runs at INSERT time over a FLAT candidate set — it * queries `{status: [pending, active, blocked], limit}` with no parent or saga * scoping — and its verdict is binary: reject, or insert. Two consequences, * both reported from the field on large projects: * * 1. **Partial overlap is invisible.** Two tasks that share part of their * scope are neither duplicates nor unrelated, and the insert-time check * has no vocabulary for that. They both get created and quietly compete. * 2. **Nothing is ever re-examined.** Overlap mostly emerges AFTER filing, as * scope drifts during execution and several agents extend adjacent tasks * toward the same middle. The insert-time check has long since run. * * `cleo reconcile` already existed but only covers release tags. The `relates` * vocabulary (`duplicates`, `absorbs`, `supersedes`) already existed too, and * nothing produced or consumed it — it was declarable by hand and never by a * workflow. * * This module supplies the missing sweep: retro-active, scoped to a container, * and classifying each overlapping pair into a concrete ACTION rather than a * duplicate yes/no. * * ## Deliberately deterministic * * Detection reuses `computeLexicalSimilarity` and `computeJaccardWordSimilarity` * from the duplicate detector. No LLM call, no embedding provider. A sweep an * agent is expected to run repeatedly must be cheap, offline, and give the same * answer twice — an LLM-scored sweep that reshuffles its own findings between * runs cannot be acted on incrementally. * * @task T12299 */ import { type EngineResult } from '../engine-result.js'; import type { DataAccessor } from '../store/data-accessor.js'; /** * Pairs are O(n²). Above this many live nodes the sweep reports a bounded scan * rather than silently comparing a subset — a truncated sweep that reads as * complete is worse than one that says what it skipped. */ export declare const MAX_SWEEP_NODES = 400; /** At or above this combined score two tasks describe the same deliverable. */ export declare const MERGE_THRESHOLD = 0.9; /** * At or above this two tasks share enough scope to need a decision. * * Calibrated against real data, not chosen: swept saga T9977 in this repo (144 * live tasks, 10,160 pairs) and counted findings by threshold — * * ``` * >= 0.55: 427 >= 0.70: 7 * >= 0.60: 128 >= 0.75: 5 * >= 0.65: 32 >= 0.85: 2 * ``` * * 0.55 returns 427 findings, which is not a report anyone acts on. 0.65 returns * 32 from 144 tasks, which is reviewable. * * The residual false positives are instructive and worth knowing before * lowering this again: on a codebase with naming conventions, titles share * vocabulary without sharing scope. `Route createAgentWorktree through core SDK` * vs `Route destroyAgentWorktree through core SDK` scores 0.852 and is two * genuinely different pieces of work. That is why high-confidence actions * (`merge`/`absorb`) sit at {@link MERGE_THRESHOLD} and additionally require a * tier/parent match — on that same real sweep they fired ZERO times, while * firing correctly on a synthetic control with true duplicates. */ export declare const OVERLAP_THRESHOLD = 0.65; /** What to do about an overlapping pair. */ export type ReconcileAction = 'merge' | 'absorb' | 'split' | 'link'; /** One overlapping pair and the action proposed for it. */ export interface ScopeOverlap { /** The earlier-created task of the pair; the survivor for `merge`/`absorb`. */ readonly keepId: string; readonly keepTitle: string; /** The later-created task; the one absorbed or superseded. */ readonly otherId: string; readonly otherTitle: string; /** Proposed action. */ readonly action: ReconcileAction; /** Combined similarity in [0,1], max of the lexical and word-level scores. */ readonly score: number; /** `relates` edge `--apply` would write for this pair. */ readonly relation: 'duplicates' | 'absorbs' | 'related'; /** Why this action, in one sentence, for a human or an agent to act on. */ readonly rationale: string; /** True when the two sit under the same immediate parent. */ readonly sameParent: boolean; } /** Result of a scope reconciliation sweep. */ export interface ReconcileScopeResult { /** Container the sweep was scoped to. */ readonly rootId: string; /** Live tasks compared. */ readonly scanned: number; /** Pairs compared. */ readonly pairs: number; /** Overlapping pairs, worst first. */ readonly overlaps: ScopeOverlap[]; /** Relation edges written. Zero unless `apply` was set. */ readonly applied: number; /** Set when the node count exceeded {@link MAX_SWEEP_NODES}. */ readonly truncated?: { limit: number; liveNodes: number; }; } /** Options for {@link reconcileScope}. */ export interface ReconcileScopeOptions { /** Saga, epic or any container to sweep. Its whole subtree is considered. */ rootId: string; /** Write the proposed `relates` edges. Read-only when false or omitted. */ apply?: boolean; /** Report pairs at or above this score. Defaults to {@link OVERLAP_THRESHOLD}. */ threshold?: number; } /** * Sweep a container for tasks whose scope overlaps, and propose what to do. * * Read-only unless `apply` is set. Even with `apply`, the ONLY mutation is * writing `relates` edges — nothing is merged, retitled, reparented or deleted. * A sweep that restructured the graph on its own reading of a similarity score * would be acting on exactly the judgement it is least qualified to make; the * edges make the overlap visible and leave the decision with the caller. * * Idempotent: `addRelation` upserts, so a second run over an unchanged graph * writes the same edges and changes nothing. * * @param options - Sweep inputs; see {@link ReconcileScopeOptions}. * @param cwd - Project root. Defaults to the resolved CLEO project. * @param accessor - Optional pre-bound data accessor. * @returns The overlaps found, worst first, and how many edges were written. * @throws {@link CleoError} `E_NOT_FOUND` when the root does not exist. * * @example * ```ts * const r = await reconcileScope({ rootId: 'T12243' }); * for (const o of r.overlaps) console.log(o.action, o.keepId, o.otherId, o.rationale); * ``` */ export declare function reconcileScope(options: ReconcileScopeOptions, cwd?: string, accessor?: DataAccessor): Promise; /** * Dispatch-layer wrapper for {@link reconcileScope}. * * Mirrors `taskDecompose`: converts thrown {@link CleoError}s into an * {@link EngineResult} so the gateway renders a LAFS envelope, preserving the * original LAFS code rather than blanket-labelling every failure. * * @param projectRoot - Absolute path to the CLEO project root. * @param options - Sweep inputs; see {@link ReconcileScopeOptions}. * @returns Engine result carrying the overlaps and applied-edge count. */ export declare function taskReconcileScope(projectRoot: string, options: ReconcileScopeOptions): Promise>; //# sourceMappingURL=reconcile-scope.d.ts.map