import { s as object, _ as _enum, u as string, v as number, w as boolean } from './paths-2M_UxD_H.js';
import 'node:fs';
import 'node:path';
import 'node:os';
import 'node:events';
import 'node:diagnostics_channel';
import 'fs';
import 'events';
import 'util';
import 'path';
import 'assert';
import 'worker_threads';
import 'module';
import 'node:url';
import 'url';
import 'buffer';
import 'node:module';
import 'node:child_process';
import 'node:crypto';
import 'node:process';
import 'node:async_hooks';

//#region ../core/dist/tasks/duplicate-detector.js
/**
* 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
*/
/** BM25 score at which a non-blocking warning is emitted. */
var DUPLICATE_WARN_THRESHOLD = .85;
/** BM25 score at which task creation is rejected (clear match — no escalation). */
var DUPLICATE_REJECT_THRESHOLD = .92;
/**
* BM25 lower bound for the ambiguous range.
* Scores below this are considered clear-different and skip Jaccard + LLM.
*/
var BM25_ESCALATE_LOW = .5;
/**
* Jaccard reject threshold.
* Scores >= this after Tier-2 escalation cause rejection without calling LLM.
*/
var JACCARD_REJECT_THRESHOLD = .85;
/**
* Jaccard lower bound for the ambiguous range.
* Scores below this after Tier-2 escalation are considered clear-different.
*/
var JACCARD_ESCALATE_LOW = .4;
/** Maximum active tasks to scan per invocation. Prevents runaway cost on huge task lists. */
var MAX_ACTIVE_TASKS_SCAN = 500;
/** LLM call timeout in milliseconds. */
var LLM_TIMEOUT_MS = 15e3;
/**
* Structured output schema for the LLM duplicate-reasoning call.
* @task T1681
*/
var DuplicateReasoningSchema = object({
	are_duplicate: boolean(),
	confidence: number().min(0).max(1),
	distinction: string().nullable(),
	suggestion: _enum([
		"merge",
		"keep-both",
		"block-new"
	])
});
/**
* Normalise text for comparison: lowercase, collapse whitespace,
* strip punctuation (except hyphens which carry semantic meaning).
*
* @param text - Raw text to normalise.
* @returns Normalised string.
*/
function normalise(text) {
	return text.toLowerCase().replace(/[^\w\s-]/g, " ").replace(/\s+/g, " ").trim();
}
/**
* Build a Set of character trigrams from normalised text.
* Trigrams capture local character patterns and are robust to
* word-order variation and minor phrasing differences.
*
* @param text - Normalised text.
* @returns Set of 3-character substrings.
*/
function trigrams(text) {
	const result = /* @__PURE__ */ new Set();
	if (text.length < 3) {
		if (text.length > 0) result.add(text.padEnd(3, " "));
		return result;
	}
	for (let i = 0; i <= text.length - 3; i++) result.add(text.slice(i, i + 3));
	return result;
}
/**
* Jaccard similarity between two Sets: |A ∩ B| / |A ∪ B|.
* Returns 0 when both sets are empty.
*
* @param a - First set.
* @param b - Second set.
* @returns Jaccard score in [0, 1].
*/
function jaccard(a, b) {
	if (a.size === 0 && b.size === 0) return 0;
	let intersection = 0;
	for (const v of a) if (b.has(v)) intersection++;
	const union = a.size + b.size - intersection;
	return union === 0 ? 0 : intersection / union;
}
/**
* Build a weighted search blob from title and description.
* Title is given 2× weight by repetition to reflect higher semantic importance.
*
* @param title - Task title.
* @param description - Task description (may be empty).
* @returns A single normalised string for trigram hashing.
*/
function buildSearchBlob(title, description) {
	return normalise(`${title} ${title} ${description}`);
}
/**
* 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].
*/
function computeLexicalSimilarity(titleA, descA, titleB, descB) {
	const blobA = buildSearchBlob(titleA, descA);
	const blobB = buildSearchBlob(titleB, descB);
	if (blobA === blobB) return 1;
	return jaccard(trigrams(blobA), trigrams(blobB));
}
/**
* Build word unigrams and bigrams from a token list.
* Includes labels as additional tokens to capture tag-level similarity.
*
* @param tokens - Array of normalised word tokens.
* @returns Set of word n-grams (unigrams + bigrams).
*/
function wordNgrams(tokens) {
	const result = /* @__PURE__ */ new Set();
	for (const tok of tokens) result.add(tok);
	for (let i = 0; i < tokens.length - 1; i++) result.add(`${tokens[i]} ${tokens[i + 1]}`);
	return result;
}
/**
* Tokenise a string into normalised word tokens.
*
* @param text - Raw text.
* @returns Array of lowercase non-empty word tokens.
*/
function tokenise(text) {
	return normalise(text).split(/\s+/).filter((t) => t.length > 0);
}
/**
* Build word-level n-gram set from title + description + labels.
* This differs from the BM25 blob (which uses character trigrams, no labels).
*
* @param title - Task title.
* @param description - Task description.
* @param labels - Task labels (empty array if none).
* @returns Set of word unigrams + bigrams.
*/
function buildWordNgramSet(title, description, labels) {
	const titleTokens = tokenise(title);
	const descTokens = tokenise(description);
	const labelTokens = labels.flatMap((l) => tokenise(l));
	return wordNgrams([
		...titleTokens,
		...titleTokens,
		...descTokens,
		...labelTokens
	]);
}
/**
* 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].
*/
function computeJaccardWordSimilarity(titleA, descA, labelsA, titleB, descB, labelsB) {
	return jaccard(buildWordNgramSet(titleA, descA, labelsA), buildWordNgramSet(titleB, descB, labelsB));
}
/**
* Attempt to compute vector-based similarity for a single active task.
*
* Embeds the incoming blob and the candidate task's blob, then computes
* cosine similarity from the dot product (assuming unit-norm vectors from
* standard embedding models).
*
* Returns `null` when embedding is unavailable (triggers lexical fallback).
*
* @param incomingBlob - Normalised search blob for the new task.
* @param candidateBlob - Normalised search blob for the candidate task.
* @returns Cosine similarity in [0, 1], or null when unavailable.
*/
async function tryVectorSimilarity(incomingBlob, candidateBlob) {
	try {
		const { isEmbeddingAvailable, embedText } = await import('./brain-embedding-Djw9ekXu.js');
		if (!isEmbeddingAvailable()) return null;
		const [vecA, vecB] = await Promise.all([embedText(incomingBlob), embedText(candidateBlob)]);
		if (!vecA || !vecB || vecA.length !== vecB.length) return null;
		let dot = 0;
		let normA = 0;
		let normB = 0;
		for (let i = 0; i < vecA.length; i++) {
			dot += (vecA[i] ?? 0) * (vecB[i] ?? 0);
			normA += (vecA[i] ?? 0) ** 2;
			normB += (vecB[i] ?? 0) ** 2;
		}
		const denom = Math.sqrt(normA) * Math.sqrt(normB);
		if (denom === 0) return null;
		return Math.max(0, Math.min(1, dot / denom));
	} catch {
		return null;
	}
}
/**
* Build the system prompt for the LLM duplicate-detection call.
*/
function buildDuplicateSystemPrompt() {
	return [
		"You are a task-management assistant determining whether two software tasks are semantic duplicates.",
		"Respond ONLY with valid JSON matching this schema:",
		"{ \"are_duplicate\": boolean, \"confidence\": number (0-1), \"distinction\": string|null, \"suggestion\": \"merge\"|\"keep-both\"|\"block-new\" }",
		"",
		"Rules:",
		"- are_duplicate=true when both tasks describe the SAME work deliverable (even if worded differently)",
		"- are_duplicate=false when they target different deliverables, different scopes, or different problem domains",
		"- distinction must be non-null when are_duplicate=false (explain what makes them different)",
		"- suggestion: \"merge\"=consolidate into one task, \"keep-both\"=distinct tasks worth tracking separately, \"block-new\"=new task is redundant",
		"- Do NOT add any explanation outside the JSON object."
	].join("\n");
}
/**
* Build the user prompt for the LLM duplicate-detection call.
*/
function buildDuplicateUserPrompt(incomingTitle, incomingDescription, candidateTitle, candidateDescription, candidateId) {
	return [
		`Task A (NEW — being added now):`,
		`  Title: ${incomingTitle}`,
		`  Description: ${incomingDescription || "(no description provided)"}`,
		"",
		`Task B (EXISTING — ${candidateId}):`,
		`  Title: ${candidateTitle}`,
		`  Description: ${candidateDescription || "(no description)"}`,
		"",
		"Are these the same task (semantic duplicate)? Respond with JSON only."
	].join("\n");
}
/**
* 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
*/
async function callLlmDuplicateReasoning(incomingTitle, incomingDescription, candidate, cwd) {
	try {
		const { authHeadersFromSealed } = await import('./credentials-DMWnF6hI.js');
		const { resolveLLMForRole } = await import('./role-resolver2-CsY9c7Bd.js');
		const llm = await resolveLLMForRole("consolidation", { projectRoot: cwd });
		if (!llm.sealedCredential || !llm.credential) return null;
		const systemPrompt = buildDuplicateSystemPrompt();
		const userPrompt = buildDuplicateUserPrompt(incomingTitle, incomingDescription, candidate.title, candidate.description ?? "", candidate.id);
		const modelConfig = {
			transport: llm.provider,
			model: llm.model,
			apiKey: (await llm.sealedCredential.fetch()).value,
			extraHeaders: llm.credential.authType === "oauth" ? await authHeadersFromSealed(llm.sealedCredential, llm.credential.authType) : void 0
		};
		const { cleoLlmCall } = await import('./api2-CEEWaywB.js');
		const callPromise = cleoLlmCall({
			modelConfig,
			prompt: userPrompt,
			maxTokens: 256,
			jsonMode: true,
			temperature: 0,
			enableRetry: false,
			messages: [{
				role: "system",
				content: systemPrompt
			}]
		});
		const timeoutPromise = new Promise((resolve) => {
			setTimeout(() => resolve(null), LLM_TIMEOUT_MS);
		});
		const result = await Promise.race([callPromise, timeoutPromise]);
		if (!result) return null;
		const responseContent = typeof result === "object" && result !== null && "content" in result ? result.content : null;
		if (!responseContent) return null;
		let parsed;
		if (typeof responseContent === "string") try {
			parsed = JSON.parse(responseContent);
		} catch {
			const jsonMatch = /\{[\s\S]*\}/.exec(responseContent);
			if (!jsonMatch) return null;
			try {
				parsed = JSON.parse(jsonMatch[0]);
			} catch {
				return null;
			}
		}
		else if (typeof responseContent === "object") parsed = responseContent;
		else return null;
		const validation = DuplicateReasoningSchema.safeParse(parsed);
		if (!validation.success) return null;
		return validation.data;
	} catch {
		return null;
	}
}
/**
* 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
*/
async function checkDuplicates(title, description, accessor, labels, cwd) {
	const incomingLabels = labels ?? [];
	const { tasks: activeTasks } = await accessor.queryTasks({
		status: [
			"pending",
			"active",
			"blocked"
		],
		limit: MAX_ACTIVE_TASKS_SCAN
	});
	if (activeTasks.length === 0) return {
		maxScore: 0,
		candidates: [],
		shouldReject: false,
		shouldWarn: false,
		tier: "bm25"
	};
	const incomingBlob = buildSearchBlob(title, description);
	const embeddingEnabled = await (async () => {
		try {
			const { isEmbeddingAvailable } = await import('./brain-embedding-Djw9ekXu.js');
			return isEmbeddingAvailable();
		} catch {
			return false;
		}
	})();
	const tier1Ambiguous = [];
	const clearMatchCandidates = [];
	for (const task of activeTasks) {
		if (task.title === title) {
			if ((task.description ?? "") === description) continue;
		}
		const candidateBlob = buildSearchBlob(task.title, task.description ?? "");
		let bm25Score;
		if (embeddingEnabled) bm25Score = await tryVectorSimilarity(incomingBlob, candidateBlob) ?? computeLexicalSimilarity(title, description, task.title, task.description ?? "");
		else bm25Score = computeLexicalSimilarity(title, description, task.title, task.description ?? "");
		if (bm25Score >= .92) clearMatchCandidates.push({
			id: task.id,
			title: task.title,
			score: bm25Score
		});
		else if (bm25Score >= BM25_ESCALATE_LOW) {
			tier1Ambiguous.push({
				task,
				bm25Score
			});
			if (bm25Score >= .85) clearMatchCandidates.push({
				id: task.id,
				title: task.title,
				score: bm25Score
			});
		}
	}
	if (clearMatchCandidates.some((c) => c.score >= .92)) {
		const sorted = clearMatchCandidates.filter((c) => c.score >= DUPLICATE_REJECT_THRESHOLD).sort((a, b) => b.score - a.score).slice(0, 3);
		return {
			maxScore: sorted[0]?.score ?? 0,
			candidates: sorted,
			shouldReject: true,
			shouldWarn: false,
			tier: "bm25"
		};
	}
	const tier2Ambiguous = [];
	const tier2Candidates = [];
	for (const { task, bm25Score: _bm25 } of tier1Ambiguous) {
		const jScore = computeJaccardWordSimilarity(title, description, incomingLabels, task.title, task.description ?? "", task.labels ?? []);
		if (jScore >= JACCARD_REJECT_THRESHOLD) tier2Candidates.push({
			id: task.id,
			title: task.title,
			score: jScore
		});
		else if (jScore >= JACCARD_ESCALATE_LOW) tier2Ambiguous.push({
			task,
			jaccardScore: jScore
		});
	}
	if (tier2Candidates.length > 0) {
		const sorted = tier2Candidates.sort((a, b) => b.score - a.score).slice(0, 3);
		return {
			maxScore: sorted[0]?.score ?? 0,
			candidates: sorted,
			shouldReject: true,
			shouldWarn: false,
			tier: "jaccard"
		};
	}
	if (tier2Ambiguous.length > 0) {
		tier2Ambiguous.sort((a, b) => b.jaccardScore - a.jaccardScore);
		const topCandidate = tier2Ambiguous[0];
		const reasoning = await callLlmDuplicateReasoning(title, description, {
			id: topCandidate.task.id,
			title: topCandidate.task.title,
			score: topCandidate.jaccardScore,
			description: topCandidate.task.description ?? ""
		}, cwd);
		if (reasoning !== null) {
			if (reasoning.are_duplicate) {
				const candidate = {
					id: topCandidate.task.id,
					title: topCandidate.task.title,
					score: reasoning.confidence
				};
				return {
					maxScore: reasoning.confidence,
					candidates: [candidate],
					shouldReject: true,
					shouldWarn: false,
					tier: "llm"
				};
			}
			return {
				maxScore: topCandidate.jaccardScore,
				candidates: [],
				shouldReject: false,
				shouldWarn: false,
				tier: "llm"
			};
		}
		const fallbackScore = tier1Ambiguous.find((e) => e.task.id === topCandidate.task.id)?.bm25Score ?? topCandidate.jaccardScore;
		if (fallbackScore >= .85) return {
			maxScore: fallbackScore,
			candidates: [{
				id: topCandidate.task.id,
				title: topCandidate.task.title,
				score: fallbackScore
			}],
			shouldReject: false,
			shouldWarn: true,
			tier: "bm25"
		};
	}
	const warnCandidates = clearMatchCandidates.filter((c) => c.score >= .85 && c.score < .92).sort((a, b) => b.score - a.score).slice(0, 3);
	if (warnCandidates.length > 0) return {
		maxScore: warnCandidates[0]?.score ?? 0,
		candidates: warnCandidates,
		shouldReject: false,
		shouldWarn: true,
		tier: "bm25"
	};
	return {
		maxScore: 0,
		candidates: [],
		shouldReject: false,
		shouldWarn: false,
		tier: "bm25"
	};
}
/**
* Format a human-readable candidate list for warning/rejection messages.
*
* @param candidates - Array of duplicate candidates.
* @returns Formatted multi-line string listing candidates.
*/
function formatCandidateList(candidates) {
	return candidates.map((c) => `  • ${c.id}: "${c.title}" (similarity: ${(c.score * 100).toFixed(0)}%)`).join("\n");
}
/**
* 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).
*/
function buildWarnMessage(candidates) {
	return `[BRAIN duplicate-check] Similar active tasks found (score >= ${Math.round(DUPLICATE_WARN_THRESHOLD * 100)}%):\n` + formatCandidateList(candidates) + `\nUse --force-duplicate to bypass this warning if the task is intentionally different.`;
}
/**
* 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).
*/
function buildRejectMessage(candidates) {
	return `[BRAIN duplicate-check] Task creation REJECTED — very similar active tasks found (score >= ${Math.round(DUPLICATE_REJECT_THRESHOLD * 100)}%):\n` + formatCandidateList(candidates) + `\nRun with --force-duplicate to bypass (audited to .cleo/audit/duplicate-bypass.jsonl).`;
}

export { buildRejectMessage, buildWarnMessage, checkDuplicates };
//# sourceMappingURL=duplicate-detector-BS8g-a8m.js.map
