/** * BRAIN-powered tiered duplicate-task detection for `cleo add`. * * Before a new task is inserted, this module queries active tasks and computes * similarity between the incoming title+description and each active task. * * Three-tier escalation (T1681): * * Tier 1 — BM25 / vector similarity (current: Jaccard trigrams or cosine): * score >= 0.92 → reject (clear match) * score < 0.50 → insert (clear different) * score in [0.50, 0.92) → escalate to Tier 2 * * Tier 2 — Jaccard on word-level n-grams (title+description+labels): * score >= 0.85 → reject * score < 0.40 → insert * score in [0.40, 0.85) → escalate to Tier 3 (LLM, paid) * * Tier 3 — LLM reasoning (max 1 call per cleo add): * are_duplicate=true → reject * are_duplicate=false → insert * LLM error / timeout → fall back to BM25-only decision (never block on error) * * Thresholds (original T1633 behavior preserved for BM25 clear-match path): * BM25 score >= 0.85 → warning emitted to stderr (non-blocking) * BM25 score >= 0.92 → rejected with E_DUPLICATE_TASK_LIKELY * * @epic T1627 * @task T1633 * @task T1681 */ import type { Task } from '@cleocode/contracts'; import { z } from 'zod'; import type { DataAccessor } from '../store/data-accessor.js'; /** BM25 score at which a non-blocking warning is emitted. */ export declare const DUPLICATE_WARN_THRESHOLD = 0.85; /** BM25 score at which task creation is rejected (clear match — no escalation). */ export declare const DUPLICATE_REJECT_THRESHOLD = 0.92; /** Maximum number of candidates to surface in the warning/rejection message. */ export declare const MAX_CANDIDATES = 3; /** A candidate active task that scored above the warning threshold. */ export interface DuplicateCandidate { /** The matching active task ID. */ id: string; /** The matching active task title. */ title: string; /** Similarity score in [0, 1]. */ score: number; } /** Result returned by {@link checkDuplicates}. */ export interface DuplicateCheckResult { /** * Maximum similarity score across all active tasks. * 0 when no active tasks are found or all scores are below warning threshold. */ maxScore: number; /** Top-N candidates sorted by score descending (above warning threshold only). */ candidates: DuplicateCandidate[]; /** Whether any candidate exceeds the reject threshold. */ shouldReject: boolean; /** Whether any candidate exceeds the warn threshold (but not the reject threshold). */ shouldWarn: boolean; /** * Which tier produced the final decision. * @task T1681 */ tier?: 'bm25' | 'jaccard' | 'llm'; } /** * Structured output schema for the LLM duplicate-reasoning call. * @task T1681 */ export declare const DuplicateReasoningSchema: z.ZodObject<{ are_duplicate: z.ZodBoolean; confidence: z.ZodNumber; distinction: z.ZodNullable; suggestion: z.ZodEnum<{ merge: "merge"; "keep-both": "keep-both"; "block-new": "block-new"; }>; }, z.core.$strip>; /** Inferred type for the LLM reasoning result. */ export type DuplicateReasoning = z.infer; /** * Compute lexical similarity between two (title, description) pairs. * * Uses Jaccard similarity over character trigrams of a weighted blob * (title 2×, description 1×). This is zero-dependency, deterministic, * and symmetrical. Used as the BM25 proxy in Tier 1. * * @param titleA - First title. * @param descA - First description. * @param titleB - Second title. * @param descB - Second description. * @returns Similarity score in [0, 1]. */ export declare function computeLexicalSimilarity(titleA: string, descA: string, titleB: string, descB: string): number; /** * Compute Jaccard similarity over word-level n-grams including labels. * Used as Tier-2 discriminator when BM25 is ambiguous. * * @param titleA - First title. * @param descA - First description. * @param labelsA - First task labels. * @param titleB - Second title. * @param descB - Second description. * @param labelsB - Second task labels. * @returns Jaccard score in [0, 1]. */ export declare function computeJaccardWordSimilarity(titleA: string, descA: string, labelsA: string[], titleB: string, descB: string, labelsB: string[]): number; /** * Call the daemon LLM to determine whether two tasks are semantic duplicates. * * Cost cap: max 1 call per `cleo add` invocation (enforced by the caller via * a `llmCallMade` flag). Never throws — returns null on error/timeout so the * caller can fall back to BM25-only decision. * * @param incomingTitle - Title of the task being added. * @param incomingDescription - Description of the task being added. * @param candidate - Best-scoring candidate from Tier 1/2. * @param cwd - Project root for credential + config resolution. * @returns Structured reasoning result, or null when the call fails/times out. * @task T1681 */ export declare function callLlmDuplicateReasoning(incomingTitle: string, incomingDescription: string, candidate: DuplicateCandidate & { description?: string; }, cwd?: string): Promise; /** * Check whether the incoming task's title and description are similar to any * active task in the database using three-tier escalation (T1681). * * Algorithm: * * Tier 1 — BM25 / vector similarity (per candidate): * For each active task, compute vector cosine similarity (if embedding is available) * or Jaccard character-trigram similarity as a proxy. This produces a BM25-like score. * - score >= DUPLICATE_REJECT_THRESHOLD (0.92): clear match → reject immediately. * - score < BM25_ESCALATE_LOW (0.50): clear different → skip Tier 2 + 3 for this candidate. * - score in [0.50, 0.92): ambiguous → collect for Tier-2 Jaccard evaluation. * * Tier 2 — Jaccard word n-grams (title+description+labels): * For candidates that were ambiguous in Tier 1, compute Jaccard over word-level n-grams. * Labels are included to surface tag-level similarity that character trigrams miss. * - score >= JACCARD_REJECT_THRESHOLD (0.85): reject. * - score < JACCARD_ESCALATE_LOW (0.40): clear different → skip LLM. * - score in [0.40, 0.85): ambiguous → escalate to LLM (cost cap: 1 call per invocation). * * Tier 3 — LLM reasoning (max 1 call per `cleo add`): * Call the daemon provider with both task descriptions and the structured-output schema. * On error/timeout: fall back to Tier-1 BM25 decision for the candidate (never block). * * @param title - Title of the task being added. * @param description - Description of the task being added (empty string if not provided). * @param accessor - DataAccessor instance to load active tasks from. * @param labels - Labels of the task being added (empty array if not provided). * @param cwd - Project root for LLM credential resolution (Tier 3). * @returns Duplicate check result with tier provenance. * @task T1633 * @task T1681 */ export declare function checkDuplicates(title: string, description: string, accessor: DataAccessor, labels?: string[], cwd?: string): Promise; /** * Format a human-readable candidate list for warning/rejection messages. * * @param candidates - Array of duplicate candidates. * @returns Formatted multi-line string listing candidates. */ export declare function formatCandidateList(candidates: DuplicateCandidate[]): string; /** * Build a warning message for candidates above the warn threshold. * * @param candidates - Candidates to include in the message. * @returns Warning string (no newline at end). */ export declare function buildWarnMessage(candidates: DuplicateCandidate[]): string; /** * Build a rejection message for candidates above the reject threshold. * * @param candidates - Candidates to include in the message. * @returns Rejection string (no newline at end). */ export declare function buildRejectMessage(candidates: DuplicateCandidate[]): string; /** * Load active tasks from the data accessor, restricted to non-terminal statuses. * Exported for testing purposes. * * @param accessor - DataAccessor to query. * @returns Array of non-terminal tasks (pending, active, blocked). */ export declare function loadActiveTasks(accessor: DataAccessor): Promise; //# sourceMappingURL=duplicate-detector.d.ts.map