import { predictImpact } from './impact-predictor.js';
import { sha256, stableJson } from '../util/hash.js';
import { clamp, estimateTokens, metadataString, normalizeLabel, sameSignals, truncate, uniqueStrings } from '../util/text.js';
import { isRfc4122Uuid } from '../util/uuid.js';
import { candidateText } from './candidate-helpers.js';
import { KnowledgeSafetyService } from '../security/knowledge-safety.js';
import { assembleContextPack, DEFAULT_DEEP_CONTEXT_BUDGET, DEFAULT_USEFULNESS_CAPS, normalizeDeepContextBudget, UNCAPPED_USEFULNESS_CAPS, } from './context-pack.js';
import { classifyQuery } from './classifier.js';
import { RetrievalDebugBuilder, stripDebugTrace, timed } from './debug.js';
import { ContextFitEvaluator } from './context-fit.js';
import { fuseCandidates } from './fusion.js';
import { preprocessLongPrompt } from './preprocessor.js';
import { freshnessWindowFor, getRetrievalPolicy, getRetrievalPolicyFingerprint, TIER_RANK_MULTIPLIERS } from './policy.js';
import { evaluateTierTransition } from '../atoms/tier.js';
import { WorktreeProvider } from './worktree.js';
import { resolveLayeredConflicts } from '../user-style/conflict-resolver.js';
import { namespaceMatchesFilter, readNamespaceFromMetadata, } from '../storage/knowledge-namespace.js';
const DEFAULT_TOKEN_BUDGET = 4000;
const SEARCH_LIMIT = 18;
const RERANK_LIMIT = 24;
const REVIEW_TARGET_LIMIT = 12;
// Only the top ~12 review targets (REVIEW_TARGET_LIMIT) are ever surfaced; 8 per
// status (×6 statuses = 48) is plenty for the top-N selection while cutting the over-fetch.
const REVIEW_QUEUE_STATUS_LIMIT = 8;
const CONTINUATION_SESSION_LIMIT = 6;
const CONTINUATION_FILE_LIMIT = 8;
const CONTINUATION_SYMBOL_LIMIT = 8;
const CONTINUATION_ERROR_LIMIT = 6;
// Concern C2 — task types for which we run impact prediction. Exploration /
// planning / review / testing read code without proposing edits, so blast
// radius would just be noise.
const IMPACT_PREDICTION_TASK_TYPES = new Set([
    'implementation',
    'refactor',
    'debugging',
]);
const RETRY_FEEDBACK_TYPES = new Set(['rejected', 'irrelevant', 'stale']);
const MISSING_CONTEXT_FEEDBACK_TYPES = new Set([
    'missing_context',
    'missing_orientation',
    'missing_current_handoff',
    'missing_verification_commands',
]);
const PROPOSAL_FEEDBACK_TYPES = new Set([
    'rejected',
    'irrelevant',
    'stale',
    'too_much_adjacent_context',
]);
const ATOM_REUSE_FEEDBACK_TYPES = new Set([
    'selected',
    'selected_but_noisy',
]);
export class RetrievalService {
    store;
    cache;
    models;
    config;
    safety;
    fitEvaluator;
    worktreeProvider;
    constructor(store, cache, models, config, safety = new KnowledgeSafetyService(), fitEvaluator = new ContextFitEvaluator(), worktreeProvider) {
        this.store = store;
        this.cache = cache;
        this.models = models;
        this.config = config;
        this.safety = safety;
        this.fitEvaluator = fitEvaluator;
        this.worktreeProvider = worktreeProvider
            ?? new WorktreeProvider({
                enabled: config.worktree.enabled,
                maxFiles: config.worktree.maxFiles,
                maxMtimeAgeHours: config.worktree.maxMtimeAgeHours,
                maxIngestContentBytes: config.ingest.maxContentBytes,
            }, safety);
    }
    async searchContext(input) {
        // Plan A — preprocess after redact, before normalize. Long prompts get
        // structural sweep + intent extraction (or anchor-window fallback) and skip
        // the continuation walker entirely. Multi-task prompts also skip the walker
        // because cross-task continuation noise hurts retrieval.
        const redacted = redactSearchInput(input, this.safety);
        const preprocessed = await preprocessLongPrompt(redacted, this.models, this.cache);
        const normalized = await this.addContinuationProvenanceMaybe(normalizeSearchInput(preprocessed, this.config));
        const totalStartedAt = Date.now();
        const classificationStartedAt = Date.now();
        const classified = classifyQuery(normalized);
        const classificationElapsedMs = Date.now() - classificationStartedAt;
        // Phase 7 — gated query rewrite. Run a fast lexical+vector probe pass to
        // gauge baseline retrieval confidence; if the strongest rawScore across
        // lexical/vector top-K clears the policy threshold, skip
        // `models.rewriteQuery` entirely. The 2026 Dell production paper showed
        // unconditional rewrite costs latency for near-zero gain post-reranker.
        // When rewrite does fire, request the diverse-angle variant
        // (task-perspective rewrites populate exactTerms for OR-style FTS expansion).
        //
        // Performance: the embedding computed for the probe vector pass is reused
        // by `findCandidates` so gated searches still embed only once. This keeps
        // probe overhead bounded to one extra lexical + vector store lookup with a
        // small `probeSearchLimit` cap.
        // Task 7.1 — resolve the retrieval policy once per searchContext call and
        // thread it down to the helpers that would otherwise re-read the (cached)
        // getRetrievalPolicy() singleton. Clarity-only: it is the same cached object.
        const policy = getRetrievalPolicy();
        const rewriteConfig = policy.queryRewrite;
        const probeProject = normalized.project ?? classified.project;
        let probeConfidence;
        let probeElapsedMs = 0;
        let rewriteSkippedReason;
        let probeEmbedding;
        if (rewriteConfig.gated) {
            const probeStartedAt = Date.now();
            const probeResult = await this.computeRewriteProbeConfidence(normalized, classified, probeProject, rewriteConfig);
            probeConfidence = probeResult.confidence;
            probeEmbedding = probeResult.embedding;
            probeElapsedMs = Date.now() - probeStartedAt;
            if (probeConfidence >= rewriteConfig.probeConfidenceThreshold) {
                rewriteSkippedReason = 'probe_confident';
            }
        }
        const rewriteStartedAt = Date.now();
        const rewrite = rewriteSkippedReason
            ? undefined
            : await this.models.rewriteQuery({
                prompt: normalized.prompt,
                classified,
                mode: rewriteConfig.gated ? 'diverse_angle' : 'paraphrase',
            });
        const rewriteElapsedMs = Date.now() - rewriteStartedAt;
        const rewriteResult = applyQueryRewrite(classified, rewrite, this.safety);
        const fingerprint = fingerprintSearch(normalized, rewriteResult.classified, this.config, rewrite);
        const cacheKey = `context:${fingerprint}`;
        const debug = normalized.debug
            ? new RetrievalDebugBuilder({
                fingerprint,
                cacheKey,
                cacheHit: false,
                cacheBypassed: true,
                searchLimit: SEARCH_LIMIT,
                rerankLimit: RERANK_LIMIT,
                tokenBudget: normalized.tokenBudget,
                rejectedKnowledgeIds: normalized.rejectedKnowledgeIds,
            })
            : undefined;
        debug?.recordElapsed('classification', classificationElapsedMs);
        if (probeElapsedMs > 0) {
            debug?.recordElapsed('rewriteProbe', probeElapsedMs);
        }
        debug?.recordElapsed('rewrite', rewriteElapsedMs);
        debug?.recordQueryRewrite({
            originalLexicalQuery: classified.lexicalQuery,
            rewrite: rewriteResult.rewrite,
            addedExactTerms: rewriteResult.addedExactTerms,
            gated: rewriteConfig.gated,
            probeConfidence,
            probeThreshold: rewriteConfig.gated ? rewriteConfig.probeConfidenceThreshold : undefined,
            skipped: rewriteSkippedReason,
        });
        const shouldResolveReviewTargets = shouldResolveReviewTargetsFor(rewriteResult.classified);
        const cached = shouldResolveReviewTargets
            ? undefined
            : await this.getCachedContextPack(cacheKey, normalized);
        if (cached) {
            return this.safety.sanitizeContextPack(cached);
        }
        const project = normalized.project ?? rewriteResult.classified.project;
        const queryId = await timed('contextQuery', this.createContextQuery(normalized, rewriteResult.classified, fingerprint, project), debug);
        const { candidates, worktree: worktreeResult } = await this.findCandidates(normalized, rewriteResult.classified, project, debug, 
        // Phase 7 — Reuse the probe's embedding to avoid a second embed call when
        // the rewrite didn't change the lexicalQuery. If applyQueryRewrite added
        // new exactTerms, the lexicalQuery is different and the embedding must be
        // recomputed; otherwise the probe embedding is identical.
        rewriteResult.addedExactTerms.length === 0 ? probeEmbedding : undefined, policy);
        const rankingResult = await this.rankCandidates(normalized.prompt, candidates, rewriteResult.classified, project, debug, normalized.disabledSources, policy);
        const rankedCandidates = rankingResult.candidates;
        const fitStartedAt = Date.now();
        const fitEvaluation = this.fitEvaluator.evaluate({
            project,
            classified: rewriteResult.classified,
            candidates: rankedCandidates,
            rejectedKnowledgeIds: normalized.rejectedKnowledgeIds,
            signal: { ...rankingResult.signal, worktreeMatchScore: worktreeResult.matchScore },
        });
        const contextFit = applyNoiseTolerance(fitEvaluation.contextFit, rewriteResult.classified, fitEvaluation.candidates, normalized.noiseTolerance);
        debug?.recordTiming('fit', fitStartedAt);
        debug?.recordStage('fit', fitEvaluation.candidates);
        debug?.recordFitScores(fitEvaluation.candidates);
        const reviewTargetResolution = shouldResolveReviewTargets
            ? await this.resolveReviewTargets(normalized, rewriteResult.classified, project)
            : { reviewTargets: [], omittedReviewTargetCount: 0 };
        const assemblyStartedAt = Date.now();
        const impactPrediction = await this.computeImpactPrediction(rewriteResult.classified, project, policy);
        // Concern F — resolve user-style ↔ project-convention contradictions before
        // pack assembly. Suppressed knowledgeIds drop out of the pack entirely;
        // instructionLines are appended to pack.instruction below.
        const styleConflict = resolveLayeredConflicts(fitEvaluation.candidates);
        const suppressedByStyleConflict = new Set(styleConflict.suppressedCandidateIds);
        const packCandidates = suppressedByStyleConflict.size > 0
            ? fitEvaluation.candidates.filter((c) => !suppressedByStyleConflict.has(c.knowledgeId))
            : fitEvaluation.candidates;
        const pack = this.buildContextPack({
            queryId,
            project,
            classified: rewriteResult.classified,
            candidates: packCandidates,
            contextFit,
            input: normalized,
            reviewTargets: reviewTargetResolution.reviewTargets,
            omittedReviewTargetCount: reviewTargetResolution.omittedReviewTargetCount,
            impactPrediction,
            extraInstructionLines: styleConflict.instructionLines,
        });
        if (normalized.contextMode === 'layered') {
            pack.deepContext = await this.buildDeepContext(pack, normalized.deepContextBudget);
        }
        debug?.recordTiming('assembly', assemblyStartedAt);
        await this.saveContextPack(cacheKey, pack, debug);
        debug?.recordTiming('total', totalStartedAt);
        if (debug) {
            pack.debug = debug.buildTrace(pack);
        }
        return pack;
    }
    async getContextPack(id) {
        const pack = await this.store.getContextPack(id);
        return pack ? this.safety.sanitizeContextPack(pack) : undefined;
    }
    async recordFeedback(input) {
        const feedback = await this.store.recordFeedback(input);
        const pack = input.contextPackId ? await this.store.getContextPack(input.contextPackId) : undefined;
        // The FeedbackEvent insert above is the anchor the dependent learning writes
        // reference (feedback.id); it stays outside the transaction. Only the
        // dependent fan-out (gaps/proposals/atom reuse) is wrapped so a mid-fan-out
        // failure rolls back all of its writes atomically.
        await this.store.withTransaction(async (tx) => {
            await this.recordFeedbackLearning(tx, input, feedback, pack);
            await this.recordAtomReuse(tx, input, pack);
        });
        if (!shouldRetry(input.feedbackType) || !input.contextPackId || !pack) {
            return { feedback };
        }
        const retry = await this.searchContext(buildRetryInput(pack, input));
        return { feedback, retry };
    }
    async recordFeedbackLearning(tx, input, feedback, pack) {
        const project = input.project ?? pack?.project;
        const sourceSessionId = metadataUuidString(input.metadata, 'agentSessionId');
        if (MISSING_CONTEXT_FEEDBACK_TYPES.has(input.feedbackType)) {
            await tx.createKnowledgeGap({
                project,
                sourceFeedbackId: feedback.id,
                sourceSessionId,
                contextPackId: input.contextPackId,
                prompt: pack?.prompt ?? metadataString(input.metadata, 'prompt') ?? input.reason ?? `${input.feedbackType} feedback`,
                classified: pack?.classified,
                missingSignals: feedbackMissingSignals(input, pack),
                reason: input.reason,
                metadata: {
                    source: 'feedback',
                    feedbackType: input.feedbackType,
                },
            });
            return;
        }
        if (!PROPOSAL_FEEDBACK_TYPES.has(input.feedbackType)) {
            return;
        }
        const affectedKnowledgeIds = feedbackAffectedKnowledgeIds(input, pack);
        for (const affectedKnowledgeId of affectedKnowledgeIds) {
            await tx.createLearningProposal({
                project,
                proposalType: proposalTypeForFeedback(input.feedbackType),
                sourceFeedbackId: feedback.id,
                sourceSessionId,
                contextPackId: input.contextPackId,
                affectedKnowledgeId,
                reason: proposalReason(input.feedbackType, input.reason),
                evidence: proposalEvidence(input, pack, affectedKnowledgeId),
                metadata: {
                    source: 'feedback',
                    feedbackType: input.feedbackType,
                    suggestedAction: suggestedActionForFeedback(input.feedbackType),
                },
            });
            if (input.feedbackType === 'too_much_adjacent_context') {
                continue;
            }
            const knowledge = await tx.getKnowledge(affectedKnowledgeId);
            if (knowledge?.metadata.source === 'agent_session_finish' || knowledge?.metadata.learningMode === 'auto') {
                await tx.createLearningProposal({
                    project: project ?? knowledge.project,
                    proposalType: 'auto_memory_cleanup',
                    sourceFeedbackId: feedback.id,
                    sourceSessionId,
                    contextPackId: input.contextPackId,
                    affectedKnowledgeId,
                    reason: `Auto-approved memory received ${input.feedbackType} feedback and needs review.`,
                    evidence: proposalEvidence(input, pack, affectedKnowledgeId),
                    metadata: {
                        source: 'feedback',
                        feedbackType: input.feedbackType,
                        suggestedAction: 'review auto memory status, archive it, or mark it superseded',
                    },
                });
            }
        }
    }
    /**
     * On positive feedback (`selected` / `selected_but_noisy`), every atom contained in the
     * pack was demonstrably useful: bump its reuseCount and last-reused timestamp, then re-evaluate
     * its tier and persist the new tier if it crossed a transition threshold.
     */
    async recordAtomReuse(tx, input, pack) {
        if (!pack || !ATOM_REUSE_FEEDBACK_TYPES.has(input.feedbackType)) {
            return;
        }
        const atomIds = new Set();
        for (const section of pack.sections) {
            for (const item of section.items) {
                if (item.metadata?.atomTier !== undefined) {
                    atomIds.add(item.knowledgeId);
                }
            }
        }
        if (atomIds.size === 0) {
            return;
        }
        const nowIso = new Date().toISOString();
        for (const atomId of atomIds) {
            const updated = await tx.incrementAtomReuse(atomId, nowIso);
            if (!updated) {
                continue;
            }
            const nextTier = evaluateTierTransition(updated, new Date(nowIso));
            if (nextTier !== updated.tier) {
                await tx.updateAtom(atomId, { tier: nextTier });
            }
        }
    }
    async getCachedContextPack(cacheKey, input) {
        if (input.bypassCache || input.debug)
            return undefined;
        try {
            return await this.cache.getJson(cacheKey);
        }
        catch (error) {
            console.error('[retrieval] context-pack cache read failed; continuing uncached.', error);
            return undefined;
        }
    }
    async createContextQuery(input, classified, fingerprint, project) {
        return this.store.createContextQuery({
            project,
            prompt: input.prompt,
            fingerprint,
            classified,
            tokenBudget: input.tokenBudget,
        });
    }
    /**
     * Phase 7 — Gated rewrite probe. Runs a fast lexical+vector top-K pass and
     * fuses just those two sources to estimate baseline retrieval confidence.
     * Top1 fused score is returned to the gate decision in `searchContext`.
     *
     * Deliberately scoped: no graph, memory, worktree, or rerank — we want a
     * cheap signal that approximates "do we already have a strong direct hit
     * lexically or semantically?" If yes, the costly `models.rewriteQuery`
     * call is skipped.
     */
    async computeRewriteProbeConfidence(input, classified, project, config) {
        const limit = Math.max(1, Math.floor(config.probeSearchLimit));
        const options = {
            project,
            limit,
            rejectedKnowledgeIds: input.rejectedKnowledgeIds,
        };
        let lexical = [];
        let vector = [];
        let embedding;
        try {
            // Embed once; reuse the vector for `searchVector` here AND in
            // `findCandidates` (returned to the caller). This keeps the gated path
            // at one embed call per search rather than two.
            embedding = await this.models.embed(`${classified.lexicalQuery}\n\n${input.prompt}`);
            const [lexicalResult, vectorResult] = await Promise.all([
                this.store.searchLexical(classified, options),
                this.store.searchVector(embedding, options).catch(() => []),
            ]);
            lexical = lexicalResult;
            vector = vectorResult;
        }
        catch {
            // Probe is best-effort: if either lookup throws, treat the probe as
            // unconfident and let the rewrite path run as before.
            return { confidence: 0, embedding };
        }
        const safeLexical = this.safety.sanitizeSearchCandidates(lexical);
        const safeVector = this.safety.sanitizeSearchCandidates(vector);
        if (safeLexical.length === 0 && safeVector.length === 0) {
            return { confidence: 0, embedding };
        }
        // Phase 7 deviation from the literal spec ("top1.fusedScore"):
        // `fuseCandidates` normalizes the top-ranked candidate to 1.0 (relative
        // ranking), so reading the post-fusion top1 score always returns 1.0 when
        // any candidate exists — that would gate every search. Instead, read the
        // strongest raw match across the lexical + vector top-K, which is what the
        // 0.65 threshold actually wants to capture: "is there a direct hit in
        // either source whose rawScore (FTS rank-decay or cosine) clears the bar?"
        let best = 0;
        for (const candidate of safeLexical) {
            if (candidate.rawScore > best)
                best = candidate.rawScore;
        }
        for (const candidate of safeVector) {
            if (candidate.rawScore > best)
                best = candidate.rawScore;
        }
        return { confidence: best, embedding };
    }
    async findCandidates(input, classified, project, debug, precomputedEmbedding, resolvedPolicy = getRetrievalPolicy()) {
        const options = {
            project,
            limit: SEARCH_LIMIT,
            rejectedKnowledgeIds: input.rejectedKnowledgeIds,
        };
        // Phase 7 — when the gated rewrite probe pre-computed an embedding for the
        // same lexicalQuery, reuse it rather than embedding twice per search.
        const embedding = precomputedEmbedding
            ? Promise.resolve(precomputedEmbedding)
            : timed('embedding', this.models.embed(`${classified.lexicalQuery}\n\n${input.prompt}`), debug);
        const vectorResults = embedding
            .then((embedding) => timed('vector', this.store.searchVector(embedding, options), debug));
        const worktreeResults = timed('worktree', this.worktreeProvider.search({
            cwd: input.cwd,
            prompt: input.prompt,
            classified,
            taskType: classified.taskType,
            project,
            limit: SEARCH_LIMIT,
            rejectedKnowledgeIds: input.rejectedKnowledgeIds,
        }), debug);
        const [metadata, lexical, memory, vector, worktree, atoms, userStyle, convention] = await Promise.all([
            timed('metadata', this.store.searchMetadata(classified, options), debug),
            timed('lexical', this.store.searchLexical(classified, options), debug),
            timed('memory', this.store.searchMemories(classified, options), debug),
            vectorResults,
            worktreeResults,
            timed('atoms', this.searchAtomCandidates(classified, options, project), debug),
            timed('userStyle', this.searchUserStyleCandidates(classified, options), debug),
            timed('convention', this.searchConventionCandidates(classified, options, project), debug),
        ]);
        const namespaceFilter = input.namespace;
        const safeResults = {
            metadata: applyNamespaceFilter(this.safety.sanitizeSearchCandidates(metadata), namespaceFilter),
            lexical: applyNamespaceFilter(this.safety.sanitizeSearchCandidates(lexical), namespaceFilter),
            memory: applyNamespaceFilter(this.safety.sanitizeSearchCandidates(memory), namespaceFilter),
            vector: applyNamespaceFilter(this.safety.sanitizeSearchCandidates(vector), namespaceFilter),
            graph: [],
            // WorktreeProvider already sanitizes through KnowledgeSafetyService; namespace filter
            // does not apply to worktree (live evidence has no persisted namespace by design).
            worktree: this.safety.sanitizeSearchCandidates(worktree.candidates),
            // Atoms are synthesized SearchCandidates from atom claims; sanitize and namespace-filter
            // them like the persisted sources. Tier weighting is applied later in fusion + adjustments.
            atoms: applyNamespaceFilter(this.safety.sanitizeSearchCandidates(atoms), namespaceFilter),
            // Concern F — user-style atoms intentionally bypass the namespace filter:
            // the layer is cross-project and namespace filters target project scope.
            userStyle: this.safety.sanitizeSearchCandidates(userStyle),
            // Phase 2 — convention atoms intentionally bypass the namespace filter:
            // team conventions are cross-project and namespace filters target project scope.
            convention: this.safety.sanitizeSearchCandidates(convention),
        };
        const seedKnowledgeIds = uniqueStrings([
            ...safeResults.metadata,
            ...safeResults.lexical,
            ...safeResults.memory,
            ...safeResults.vector,
            ...safeResults.worktree,
        ].map((candidate) => candidate.knowledgeId));
        const graph = await timed('graph', this.store.searchGraphRelations(classified, { ...options, seedKnowledgeIds }), debug);
        safeResults.graph = enrichGraphCandidates(applyNamespaceFilter(this.safety.sanitizeSearchCandidates(graph), namespaceFilter), classified, [
            ...safeResults.metadata,
            ...safeResults.lexical,
            ...safeResults.memory,
            ...safeResults.vector,
        ]);
        // Concern C2 — seed an atom-graph walk from atoms that already surfaced
        // through any source (atoms / metadata / lexical / memory / vector / worktree)
        // and merge depth-2 atom hits into the graph candidate group. Bounded by the
        // policy's walkDepth, edgeWeights, and decayPerHop.
        const atomGraphHits = await this.expandAtomGraphFromSeeds(safeResults, classified, project, resolvedPolicy);
        if (atomGraphHits.length > 0) {
            safeResults.graph = [
                ...safeResults.graph,
                ...applyNamespaceFilter(this.safety.sanitizeSearchCandidates(atomGraphHits), namespaceFilter),
            ];
        }
        debug?.recordStage('metadata', safeResults.metadata);
        debug?.recordStage('lexical', safeResults.lexical);
        debug?.recordStage('memory', safeResults.memory);
        debug?.recordStage('vector', safeResults.vector);
        debug?.recordStage('graph', safeResults.graph);
        debug?.recordStage('worktree', safeResults.worktree);
        debug?.recordStage('atoms', safeResults.atoms);
        debug?.recordStage('userStyle', safeResults.userStyle);
        debug?.recordStage('convention', safeResults.convention);
        return { candidates: safeResults, worktree };
    }
    /**
     * Concern F — fetch the configured user's cross-project style atoms whose
     * trigger overlaps the classified query. Returns [] when `TUBEROSA_USER_ID`
     * is unset or the layer is disabled.
     */
    async searchUserStyleCandidates(classified, options) {
        if (!this.config.userStyle.userId || this.config.userStyle.enabled === false)
            return [];
        const trigger = {
            errors: classified.errors,
            files: classified.files,
            symbols: classified.symbols,
            taskTypes: classified.taskType && classified.taskType !== 'unknown' ? [classified.taskType] : undefined,
        };
        if ((trigger.errors?.length ?? 0) === 0
            && (trigger.files?.length ?? 0) === 0
            && (trigger.symbols?.length ?? 0) === 0
            && (trigger.taskTypes?.length ?? 0) === 0) {
            return [];
        }
        const atoms = await this.store.searchAtomsByTrigger(trigger, {
            project: undefined,
            scope: 'user',
            userId: this.config.userStyle.userId,
            limit: options.limit,
        });
        const rejected = new Set(options.rejectedKnowledgeIds ?? []);
        return atoms
            .filter((atom) => atom.status === 'active' && !rejected.has(atom.id))
            .map((atom, index) => userStyleAtomToCandidate(atom, index));
    }
    /**
     * Phase 2 — convention lane. Fetches team conventions (scope 'team',
     * cross-project) and project conventions (scope 'project' for the current
     * project) whose trigger overlaps the classified query, keeps only active,
     * non-rejected `type: 'convention'` atoms, and synthesizes convention-source
     * candidates. Bypasses the namespace filter (mirrors the user-style lane).
     */
    async searchConventionCandidates(classified, options, project) {
        if (this.config.userStyle.conventionsEnabled === false)
            return [];
        const trigger = {
            errors: classified.errors,
            files: classified.files,
            symbols: classified.symbols,
            taskTypes: classified.taskType && classified.taskType !== 'unknown' ? [classified.taskType] : undefined,
        };
        const hasTrigger = (trigger.errors?.length ?? 0) > 0
            || (trigger.files?.length ?? 0) > 0
            || (trigger.symbols?.length ?? 0) > 0
            || (trigger.taskTypes?.length ?? 0) > 0;
        if (!hasTrigger)
            return [];
        const [teamAtoms, projectAtoms] = await Promise.all([
            this.store.searchAtomsByTrigger(trigger, {
                project: undefined,
                scope: 'team',
                teamId: this.config.userStyle.teamId,
                limit: options.limit,
            }),
            this.store.searchAtomsByTrigger(trigger, {
                project: project ?? classified.project,
                scope: 'project',
                limit: options.limit,
            }),
        ]);
        const rejected = new Set(options.rejectedKnowledgeIds ?? []);
        return [...teamAtoms, ...projectAtoms]
            .filter((atom) => atom.type === 'convention' && atom.status === 'active' && !rejected.has(atom.id))
            .map((atom, index) => conventionAtomToCandidate(atom, index));
    }
    /**
     * Atoms as a retrieval candidate source. Matches knowledge atoms by their
     * trigger signals (errors/files/symbols/taskType) and synthesizes
     * SearchCandidates seeded with the tier multiplier as rawScore so verified /
     * canonical atoms enter fusion ahead of drafts for the same trigger.
     */
    async searchAtomCandidates(classified, options, project) {
        const trigger = {
            errors: classified.errors,
            files: classified.files,
            symbols: classified.symbols,
            taskTypes: classified.taskType && classified.taskType !== 'unknown' ? [classified.taskType] : undefined,
        };
        if ((trigger.errors?.length ?? 0) === 0
            && (trigger.files?.length ?? 0) === 0
            && (trigger.symbols?.length ?? 0) === 0
            && (trigger.taskTypes?.length ?? 0) === 0) {
            return [];
        }
        const atoms = await this.store.searchAtomsByTrigger(trigger, {
            project: project ?? classified.project,
            limit: options.limit,
        });
        const rejected = new Set(options.rejectedKnowledgeIds ?? []);
        return atoms
            .filter((atom) => atom.status === 'active' && !rejected.has(atom.id))
            .map((atom, index) => atomToCandidate(atom, index));
    }
    async rankCandidates(prompt, candidates, classified, project, debug, disabledSources, resolvedPolicy = getRetrievalPolicy()) {
        const fusionStartedAt = Date.now();
        const disabled = new Set(disabledSources ?? []);
        const candidateGroups = [
            disabled.has('metadata') ? [] : candidates.metadata,
            disabled.has('lexical') ? [] : candidates.lexical,
            disabled.has('memory') ? [] : candidates.memory,
            disabled.has('atoms') ? [] : candidates.atoms,
            disabled.has('vector') ? [] : candidates.vector,
            disabled.has('graph') ? [] : candidates.graph,
            disabled.has('worktree') ? [] : candidates.worktree,
            disabled.has('userStyle') ? [] : candidates.userStyle,
            disabled.has('convention') ? [] : candidates.convention,
        ];
        // Phase 2 — fetch feedback summaries BEFORE fusion so the per-candidate
        // multiplicative penalty is applied to the fused score (and therefore visible to
        // rerank). Bound the set to all candidates fusion will see — duplicates across
        // groups are deduped by the Map+Set below.
        const candidateIdsForFeedback = new Set();
        for (const group of candidateGroups) {
            for (const candidate of group) {
                candidateIdsForFeedback.add(candidate.knowledgeId);
            }
        }
        const feedbackSummaries = candidateIdsForFeedback.size > 0
            ? await this.store.getFeedbackSummaries([...candidateIdsForFeedback], { project })
            : new Map();
        let fused;
        if (debug) {
            const fuseResult = fuseCandidates(candidateGroups, classified, {
                collectBreakdown: true,
                feedbackSummaries,
            });
            fused = fuseResult.ranked.slice(0, RERANK_LIMIT);
            if (fuseResult.breakdown) {
                debug.recordFusionBreakdown(fuseResult.breakdown);
            }
        }
        else {
            const fuseResult = fuseCandidates(candidateGroups, classified, {
                feedbackSummaries,
                collectBreakdown: true,
            });
            fused = fuseResult.ranked.slice(0, RERANK_LIMIT);
        }
        debug?.recordTiming('fusion', fusionStartedAt);
        debug?.recordStage('fusion', fused);
        // Phase 3 — wrap rerank in try/catch. On failure, fall back to the fused ordering
        // (already RRF-normalized + feedback-multiplied) and propagate a structured signal
        // up to the context-fit evaluator so it can downgrade fitStatus rather than letting
        // trust silently collapse to 0 when the reranker throws.
        let reranked;
        let rerankSignal = { rerankerAvailable: true };
        try {
            const rerankResult = await timed('rerank', this.models.rerank({ prompt, classified, candidates: fused }), debug);
            debug?.recordProviderRerank({
                model: rerankResult.model,
                inputKnowledgeIds: fused.map((candidate) => candidate.knowledgeId),
                decisions: rerankResult.decisions,
            });
            if (debug) {
                debug.recordRerankScores(rerankResult.candidates);
            }
            reranked = this.safety.sanitizeSearchCandidates(rerankResult.candidates, {
                onFilterEvent: debug ? (event) => debug.recordFilterEvent(event) : undefined,
            });
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            rerankSignal = { rerankerAvailable: false, rerankerError: message };
            const fallbackCandidates = fused.map((candidate) => ({
                ...candidate,
                rerankScore: candidate.fusedScore,
                finalScore: candidate.fusedScore,
            }));
            reranked = this.safety.sanitizeSearchCandidates(fallbackCandidates, {
                onFilterEvent: debug ? (event) => debug.recordFilterEvent(event) : undefined,
            });
            debug?.recordProviderRerank({
                model: 'fallback:fused-order',
                inputKnowledgeIds: fused.map((candidate) => candidate.knowledgeId),
                decisions: [],
            });
        }
        const adjusted = await this.applyRankingAdjustments(reranked, classified, project ?? classified.project, debug, resolvedPolicy);
        debug?.recordStage('rerank', adjusted);
        return { candidates: adjusted, signal: rerankSignal };
    }
    async applyRankingAdjustments(candidates, classified, project, debug, resolvedPolicy = getRetrievalPolicy()) {
        const summaries = await this.store.getFeedbackSummaries([...new Set(candidates.map((candidate) => candidate.knowledgeId))], { project });
        const supersededBy = await this.supersededByRelations(candidates, project);
        const onSuppression = debug ? (event) => debug.recordSuppressionEvent(event) : undefined;
        return candidates
            .filter((candidate) => readLegacyStatus(candidate) !== 'legacy_archived')
            .map((candidate) => applyLegacyReplacedDownweight(candidate))
            .map((candidate) => applyFeedbackSummary(candidate, summaries.get(candidate.knowledgeId), onSuppression))
            .map((candidate) => applyIntentSuppression(candidate, classified, supersededBy.get(candidate.knowledgeId) ?? [], onSuppression, resolvedPolicy))
            .map((candidate) => applyAtomTierMultiplier(candidate))
            .map((candidate) => applyUserStyleMultiplier(candidate, resolvedPolicy))
            .sort((left, right) => right.finalScore - left.finalScore || left.rank - right.rank)
            .map((candidate, index) => ({ ...candidate, rank: index + 1 }));
    }
    async supersededByRelations(candidates, project) {
        const knowledgeIds = uniqueStrings(candidates.map((candidate) => candidate.knowledgeId));
        const entries = await Promise.all(knowledgeIds.map(async (knowledgeId) => {
            const relations = await this.store.listKnowledgeRelations({
                project,
                targetKnowledgeId: knowledgeId,
                relationType: 'supersedes',
                limit: 8,
            });
            return [knowledgeId, relations.filter((relation) => relation.fromKnowledgeId !== knowledgeId && relation.confidence >= 0.5)];
        }));
        return new Map(entries.filter(([, relations]) => relations.length > 0));
    }
    async resolveReviewTargets(_input, classified, project) {
        const explicitIds = uniqueStrings(classified.intent.objectHints ?? []);
        const explicitTargets = await Promise.all(explicitIds.map((id) => this.resolveExplicitReviewTarget(id)));
        const queueEntries = shouldSurfaceReviewQueues(classified)
            ? await this.listReviewQueueTargets(project)
            : [];
        const explicitTargetIds = new Set(explicitTargets.map((target) => target.id));
        const queuedTargets = queueEntries
            .filter((entry) => !explicitTargetIds.has(entry.target.id))
            .sort(compareReviewQueueEntries)
            .map((entry) => entry.target);
        const availableQueueSlots = Math.max(0, REVIEW_TARGET_LIMIT - explicitTargets.length);
        const selectedQueueTargets = queuedTargets.slice(0, availableQueueSlots);
        return {
            reviewTargets: [...explicitTargets, ...selectedQueueTargets],
            omittedReviewTargetCount: Math.max(0, queuedTargets.length - selectedQueueTargets.length),
        };
    }
    async resolveExplicitReviewTarget(id) {
        const [draft, gap, proposal, pack, session, knowledge] = await Promise.all([
            this.store.getReflectionDraft(id),
            this.store.getKnowledgeGap(id),
            this.store.getLearningProposal(id),
            this.store.getContextPack(id),
            this.store.getAgentSession(id),
            this.store.getKnowledge(id),
        ]);
        if (draft) {
            return reflectionDraftTarget(draft, 'Prompt named this reflection draft id.');
        }
        if (gap) {
            return knowledgeGapTarget(gap, 'Prompt named this knowledge gap id.');
        }
        if (proposal) {
            return learningProposalTarget(proposal, 'Prompt named this learning proposal id.');
        }
        if (pack) {
            return contextPackTarget(pack, 'Prompt named this context pack id.');
        }
        if (session) {
            return agentSessionTarget(session, 'Prompt named this agent session id.');
        }
        if (knowledge) {
            return knowledgeTarget(knowledge, 'Prompt named this knowledge item id.');
        }
        return {
            kind: 'unknown',
            id,
            status: 'not_found',
            title: `Unresolved object ${id}`,
            recommendedAction: 'Verify the id or create the missing review record before proceeding.',
            reason: 'Prompt named this UUID, but it did not match a known review object.',
        };
    }
    async listReviewQueueTargets(project) {
        const [pendingDrafts, needsChangesDrafts, openGaps, needsChangesGaps, openProposals, needsChangesProposals,] = await Promise.all([
            this.store.listReflectionDrafts({ project, status: 'pending', limit: REVIEW_QUEUE_STATUS_LIMIT }),
            this.store.listReflectionDrafts({ project, status: 'needs_changes', limit: REVIEW_QUEUE_STATUS_LIMIT }),
            this.store.listKnowledgeGaps({ project, status: 'open', limit: REVIEW_QUEUE_STATUS_LIMIT }),
            this.store.listKnowledgeGaps({ project, status: 'needs_changes', limit: REVIEW_QUEUE_STATUS_LIMIT }),
            this.store.listLearningProposals({ project, status: 'open', limit: REVIEW_QUEUE_STATUS_LIMIT }),
            this.store.listLearningProposals({ project, status: 'needs_changes', limit: REVIEW_QUEUE_STATUS_LIMIT }),
        ]);
        return [
            ...pendingDrafts.map((draft) => reviewQueueEntry(reflectionDraftTarget(draft, 'Pending reflection draft in the review queue.'), draft.createdAt)),
            ...needsChangesDrafts.map((draft) => reviewQueueEntry(reflectionDraftTarget(draft, 'Reflection draft needs changes before approval.'), draft.createdAt)),
            ...openGaps.map((gap) => reviewQueueEntry(knowledgeGapTarget(gap, 'Open knowledge gap in the review queue.'), gap.createdAt)),
            ...needsChangesGaps.map((gap) => reviewQueueEntry(knowledgeGapTarget(gap, 'Knowledge gap needs changes before it can be closed.'), gap.createdAt)),
            ...openProposals.map((proposal) => reviewQueueEntry(learningProposalTarget(proposal, 'Open learning proposal in the review queue.'), proposal.createdAt)),
            ...needsChangesProposals.map((proposal) => reviewQueueEntry(learningProposalTarget(proposal, 'Learning proposal needs changes before approval or dismissal.'), proposal.createdAt)),
        ];
    }
    buildContextPack(input) {
        const pack = assembleContextPack({
            queryId: input.queryId,
            project: input.project,
            prompt: input.input.prompt,
            classified: input.classified,
            candidates: input.candidates,
            tokenBudget: input.input.tokenBudget,
            rejectedKnowledgeIds: input.input.rejectedKnowledgeIds,
            contextFit: input.contextFit,
            usefulnessCaps: usefulnessCapsForRequest(input.input),
            reviewTargets: input.reviewTargets,
            omittedReviewTargetCount: input.omittedReviewTargetCount,
        });
        if (input.impactPrediction) {
            pack.impactPrediction = input.impactPrediction;
        }
        if (input.extraInstructionLines && input.extraInstructionLines.length > 0) {
            pack.instruction = input.extraInstructionLines.join('\n');
        }
        return pack;
    }
    /**
     * Concern C2 — seed `walkAtomGraph` from atom ids present in any retrieval
     * source and synthesize SearchCandidates for the depth-2 hits, tagged with a
     * `graphPath` metadata field so the agent can see *why* the item surfaced.
     */
    async expandAtomGraphFromSeeds(results, classified, project, resolvedPolicy = getRetrievalPolicy()) {
        if (!project)
            return [];
        const policy = resolvedPolicy.graph;
        if (policy.walkDepth < 1)
            return [];
        const seedIds = uniqueStrings([
            ...results.atoms,
            ...results.metadata,
            ...results.lexical,
            ...results.memory,
            ...results.vector,
            ...results.worktree,
        ]
            .filter((c) => c.itemType === 'memory')
            .map((c) => c.knowledgeId));
        if (seedIds.length === 0)
            return [];
        const hits = await this.store.walkAtomGraph({
            project,
            seedAtomIds: seedIds,
            depth: policy.walkDepth,
            edgeWeights: policy.edgeWeights,
            decayPerHop: policy.decayPerHop,
            limit: SEARCH_LIMIT,
        });
        if (hits.length === 0)
            return [];
        const seen = new Set(seedIds);
        const synthesized = [];
        for (const hit of hits) {
            if (seen.has(hit.atomId))
                continue;
            seen.add(hit.atomId);
            const atom = await this.store.getAtom(hit.atomId);
            if (!atom)
                continue;
            if (atom.status === 'archived' || atom.status === 'legacy_archived')
                continue;
            const candidate = atomToCandidate(atom, synthesized.length);
            const edgePath = hit.path.map((s) => s.edgeKind).join('→');
            synthesized.push({
                ...candidate,
                source: 'graph',
                rawScore: hit.pathScore,
                metadata: {
                    ...(candidate.metadata ?? {}),
                    atomGraphPath: hit.path,
                    atomGraphReason: `graph:${edgePath}`,
                },
            });
        }
        // Touch classified to keep the parameter useful when future scoring needs the
        // task type to weight kinds — keeps the signature ready for that follow-up.
        void classified;
        return synthesized;
    }
    /**
     * Concern C2 — run impact prediction for edit-like task types only. Empty
     * predictions are dropped so the field stays out of the pack when there's
     * nothing useful to show.
     */
    async computeImpactPrediction(classified, project, resolvedPolicy = getRetrievalPolicy()) {
        if (!IMPACT_PREDICTION_TASK_TYPES.has(classified.taskType))
            return undefined;
        if (!project)
            return undefined;
        const hasSeeds = classified.files.length > 0 || classified.symbols.length > 0;
        if (!hasSeeds)
            return undefined;
        const policy = resolvedPolicy.graph;
        const prediction = await predictImpact(this.store, {
            project,
            files: classified.files,
            symbols: classified.symbols,
            policy,
        });
        return prediction.predictedAffected.length > 0 ? prediction : undefined;
    }
    async buildDeepContext(pack, budget) {
        const selectedItems = pack.sections.flatMap((section) => section.items);
        const knowledgeIds = uniqueStrings(selectedItems.map((item) => item.knowledgeId));
        const chunksByKnowledgeId = groupChunks(await this.store.listKnowledgeChunks(knowledgeIds));
        let remaining = budget;
        const sections = [];
        for (const section of pack.sections) {
            const deepItems = [];
            for (const item of section.items) {
                if (remaining <= 0) {
                    break;
                }
                const chunks = chunksByKnowledgeId.get(item.knowledgeId) ?? [];
                const selectedChunks = takeChunksWithinBudget(chunks, remaining);
                const deepItem = buildDeepContextItem(item, selectedChunks.length ? selectedChunks : undefined, remaining);
                if (!deepItem) {
                    continue;
                }
                deepItems.push(deepItem);
                remaining -= deepItem.tokenEstimate;
            }
            sections.push({
                name: section.name,
                items: deepItems,
                tokenEstimate: deepItems.reduce((sum, item) => sum + item.tokenEstimate, 0),
            });
        }
        return {
            mode: 'layered',
            budget,
            tokenEstimate: sections.reduce((sum, section) => sum + section.tokenEstimate, 0),
            sections,
        };
    }
    async saveContextPack(cacheKey, pack, debug) {
        const compactPack = stripDebugTrace(pack);
        await timed('save', saveCompactContextPack(this.store, this.cache, cacheKey, compactPack, this.config.context.cacheTtlSeconds), debug);
    }
    async addContinuationProvenanceMaybe(input) {
        // Plan A — gate the continuation walker so it never runs on long prompts
        // (a long multi-task ask is a fresh start, not a continuation) or on
        // multi-subTask prompts (cross-task continuation noise).
        if (input.promptPreprocessing?.lengthClass === 'long')
            return input;
        if ((input.promptPreprocessing?.subTasks?.length ?? 0) > 1)
            return input;
        return this.addContinuationProvenance(input);
    }
    async addContinuationProvenance(input) {
        if (!isContinuationPrompt(input.prompt)) {
            return input;
        }
        const project = input.project ?? projectFromInput(input);
        const sessions = await this.store.listAgentSessions({ project, limit: CONTINUATION_SESSION_LIMIT });
        const recentSignals = emptyContinuationSignals();
        for (const session of sessions) {
            const decisions = await this.store.listAgentContextDecisions({ sessionId: session.id, limit: 20 });
            const selectedDecisions = decisions.filter((decision) => decision.decision === 'selected');
            if (selectedDecisions.length === 0) {
                continue;
            }
            mergeSignals(recentSignals, extractPromptSignals(`${session.prompt}\n${session.summary ?? ''}`));
            for (const decision of selectedDecisions) {
                mergeSignals(recentSignals, extractDecisionSignals(decision));
            }
            for (const packId of selectedContextPackIds(selectedDecisions)) {
                const pack = await this.store.getContextPack(packId);
                if (pack) {
                    mergeSignals(recentSignals, extractContextPackSignals(pack));
                }
            }
        }
        const enriched = {
            files: mergeExplicitAndInferred(input.files, recentSignals.files, CONTINUATION_FILE_LIMIT),
            symbols: mergeExplicitAndInferred(input.symbols, recentSignals.symbols, CONTINUATION_SYMBOL_LIMIT),
            errors: mergeExplicitAndInferred(input.errors, recentSignals.errors, CONTINUATION_ERROR_LIMIT),
        };
        if (sameSignals(input.files, enriched.files)
            && sameSignals(input.symbols, enriched.symbols)
            && sameSignals(input.errors, enriched.errors)) {
            return input;
        }
        return { ...input, ...enriched };
    }
}
function reviewQueueEntry(target, createdAt) {
    return { target, createdAt };
}
function shouldResolveReviewTargetsFor(classified) {
    return (classified.intent.objectHints ?? []).length > 0 || shouldSurfaceReviewQueues(classified);
}
function shouldSurfaceReviewQueues(classified) {
    const mode = taskBriefMode(classified);
    return mode === 'reflection_review'
        || mode === 'context_quality_review'
        || mode === 'handoff_cleanup'
        || mode === 'operations_review';
}
function taskBriefMode(classified) {
    return classified.intent.taskBriefMode ?? (classified.taskType === 'debugging'
        ? 'debugging'
        : classified.taskType === 'planning'
            ? 'planning'
            : classified.taskType === 'review'
                ? 'review'
                : classified.taskType === 'unknown'
                    ? 'unknown'
                    : 'implementation');
}
function compareReviewQueueEntries(left, right) {
    const urgencyDelta = reviewStatusUrgency(left.target.status) - reviewStatusUrgency(right.target.status);
    if (urgencyDelta !== 0) {
        return urgencyDelta;
    }
    return (right.createdAt ?? '').localeCompare(left.createdAt ?? '');
}
function reviewStatusUrgency(status) {
    switch (status) {
        case 'needs_changes':
            return 0;
        case 'pending':
        case 'open':
            return 1;
        default:
            return 2;
    }
}
function reflectionDraftTarget(draft, reason) {
    return {
        kind: 'reflection_draft',
        id: draft.id,
        status: draft.status,
        title: draft.title,
        recommendedAction: draft.status === 'needs_changes'
            ? 'Revise the draft or reject it before it can become searchable memory.'
            : 'Review accuracy, usefulness, scope, privacy, labels, references, and duplicate risk.',
        reason,
    };
}
function knowledgeGapTarget(gap, reason) {
    return {
        kind: 'knowledge_gap',
        id: gap.id,
        status: gap.status,
        title: truncate(gap.reason ?? gap.prompt, 96),
        recommendedAction: gap.status === 'needs_changes'
            ? 'Revise the gap review notes or dismiss it after the missing context is handled.'
            : 'Inspect missing signals and decide whether to add knowledge, labels, references, or dismiss the gap.',
        reason,
    };
}
function learningProposalTarget(proposal, reason) {
    return {
        kind: 'learning_proposal',
        id: proposal.id,
        status: proposal.status,
        title: `${proposal.proposalType}: ${truncate(proposal.reason, 82)}`,
        recommendedAction: proposal.status === 'needs_changes'
            ? 'Revise proposal metadata or dismiss it before approval.'
            : learningProposalAction(proposal),
        reason,
    };
}
function contextPackTarget(pack, reason) {
    return {
        kind: 'context_pack',
        id: pack.id,
        status: pack.status,
        title: `Context pack: ${truncate(pack.prompt, 82)}`,
        recommendedAction: 'Inspect the pack, confirm fit, and record a context decision when appropriate.',
        reason,
    };
}
function agentSessionTarget(session, reason) {
    return {
        kind: 'agent_session',
        id: session.id,
        status: session.status,
        title: `Agent session: ${truncate(session.prompt, 82)}`,
        recommendedAction: session.status === 'active'
            ? 'Inspect context decisions and finish or annotate the active session.'
            : 'Inspect session decisions, outcome, and reflection links.',
        reason,
    };
}
function knowledgeTarget(knowledge, reason) {
    return {
        kind: 'knowledge',
        id: knowledge.id,
        status: knowledge.status ?? 'unknown',
        title: knowledge.title,
        recommendedAction: knowledge.status === 'needs_review'
            ? 'Review labels, references, freshness, and safety before relying on this knowledge.'
            : 'Inspect this knowledge item and confirm it is relevant to the task.',
        reason,
    };
}
function learningProposalAction(proposal) {
    switch (proposal.proposalType) {
        case 'missing_label':
            return 'Review suggested labels and approve, request changes, or dismiss.';
        case 'missing_reference':
            return 'Review suggested references and approve, request changes, or dismiss.';
        case 'missing_relation':
            return 'Review whether labels or graph relations should be tightened.';
        case 'supersedes':
            return 'Review whether the candidate should supersede affected knowledge.';
        case 'auto_memory_cleanup':
            return 'Review the auto-memory cleanup action before changing memory status.';
        case 'user_style_candidate':
            return 'Review the clustered user-style candidate before promoting it to a user-style atom.';
    }
}
async function saveCompactContextPack(store, cache, cacheKey, pack, ttlSeconds) {
    await store.saveContextPack(pack);
    try {
        await cache.setJson(cacheKey, pack, ttlSeconds);
    }
    catch (error) {
        console.error('[retrieval] context-pack cache write failed; pack persisted to store.', error);
    }
}
function atomToCandidate(atom, index) {
    const references = atom.evidence
        .filter((evidence) => evidence.kind === 'file')
        .map((evidence) => ({
        type: 'file',
        uri: evidence.path,
        lineStart: evidence.lineStart,
        lineEnd: evidence.lineEnd,
        commitSha: evidence.commitSha,
    }));
    return {
        knowledgeId: atom.id,
        title: atom.claim,
        summary: atom.claim,
        content: atom.claim,
        contextualContent: atom.claim,
        itemType: 'memory',
        project: atom.project,
        labels: [],
        references,
        tokenEstimate: estimateTokens(atom.claim),
        trustLevel: 1,
        source: 'atoms',
        // Neutral rawScore (fusion ranks by `rank`, not rawScore — rawScore only acts
        // as a content-collision tiebreaker, and atoms have distinct knowledgeIds).
        // Tier weighting is applied in exactly one place: applyAtomTierMultiplier,
        // which scales finalScore by TIER_RANK_MULTIPLIERS[atomTier] post-rerank so
        // verified/canonical atoms outrank drafts for the same trigger.
        rawScore: 1,
        rank: index + 1,
        metadata: { atomTier: atom.tier, atomType: atom.type },
    };
}
function applyAtomTierMultiplier(candidate) {
    if (candidate.source !== 'atoms') {
        return candidate;
    }
    const tier = candidate.metadata?.atomTier;
    if (!tier || !(tier in TIER_RANK_MULTIPLIERS)) {
        return candidate;
    }
    return { ...candidate, finalScore: candidate.finalScore * TIER_RANK_MULTIPLIERS[tier] };
}
/**
 * Concern F — user-style atoms get a tier-aware multiplier plus a
 * personal_workflow boost. Project atoms use applyAtomTierMultiplier; the two
 * paths stay separate so tier multipliers can diverge (drafts of a personal
 * preference are weaker signals than verified project conventions).
 */
function userStyleAtomToCandidate(atom, index) {
    return {
        knowledgeId: atom.id,
        title: atom.claim,
        summary: atom.claim,
        content: atom.claim,
        contextualContent: atom.claim,
        itemType: 'memory',
        project: atom.project,
        labels: [],
        references: [],
        tokenEstimate: estimateTokens(atom.claim),
        trustLevel: 1,
        source: 'userStyle',
        rawScore: 1,
        rank: index + 1,
        metadata: {
            userStyleAtomId: atom.id,
            userStyleTier: atom.tier,
            userStylePriority: atom.priority,
            userId: atom.userId,
        },
    };
}
/**
 * Phase 2 — convention atoms (scope 'team' cross-project + scope 'project')
 * are synthesized into candidates carrying their scope/tier so fusion and the
 * essential-pin step can reason about them. Kept separate from
 * userStyleAtomToCandidate so the two layers can diverge.
 */
function conventionAtomToCandidate(atom, index) {
    return {
        knowledgeId: atom.id,
        title: atom.claim,
        summary: atom.claim,
        content: atom.claim,
        contextualContent: atom.claim,
        itemType: 'rule',
        project: atom.project,
        labels: [],
        references: [],
        tokenEstimate: Math.max(1, Math.ceil(atom.claim.length / 4)),
        trustLevel: 1,
        source: 'convention',
        // A project-scope convention atom is also picked up by the generic `atoms`
        // lane (same project + trigger). Fusion dedups by knowledgeId and keeps the
        // chunk whose rawScore wins the `existing.rawScore >= candidate.rawScore`
        // tiebreaker (rawScore is a pure collision tiebreaker — it does not affect
        // the fused score). A marginally higher rawScore than the atoms lane's `1`
        // ensures the authoritative `convention` source label survives the merge.
        rawScore: 1.1,
        rank: index + 1,
        metadata: {
            conventionAtomId: atom.id,
            conventionScope: atom.scope,
            conventionTier: atom.tier,
            conventionSteps: atom.metadata?.steps,
            conventionCategory: atom.metadata?.category,
        },
    };
}
function applyUserStyleMultiplier(candidate, policy = getRetrievalPolicy()) {
    if (candidate.source !== 'userStyle')
        return candidate;
    const meta = candidate.metadata;
    const tier = meta?.userStyleTier;
    if (!tier)
        return candidate;
    let multiplier = policy.userStyle.tierMultipliers[tier];
    if (typeof multiplier !== 'number')
        return candidate;
    if (meta?.userStylePriority === 'personal_workflow') {
        multiplier *= policy.userStyle.personalWorkflowBoost;
    }
    const matchReason = `userStyle:${meta?.userStylePriority ?? 'coding_preference'}:`;
    const matchReasons = candidate.matchReasons?.includes(matchReason)
        ? candidate.matchReasons
        : [...(candidate.matchReasons ?? []), matchReason];
    return { ...candidate, finalScore: candidate.finalScore * multiplier, matchReasons };
}
const LEGACY_REPLACED_GRACE_MULTIPLIER = 0.2;
function readLegacyStatus(candidate) {
    return candidate.metadata?.legacyStatus;
}
function applyLegacyReplacedDownweight(candidate) {
    if (readLegacyStatus(candidate) !== 'legacy_replaced') {
        return candidate;
    }
    return { ...candidate, finalScore: candidate.finalScore * LEGACY_REPLACED_GRACE_MULTIPLIER };
}
function redactSearchInput(input, safety) {
    return {
        ...input,
        prompt: safety.redactSecrets(input.prompt),
        errors: input.errors?.map((error) => safety.redactSecrets(error)),
    };
}
function selectedContextPackIds(decisions) {
    return uniqueStrings([
        ...decisions.flatMap((decision) => [decision.contextPackId, decision.retryContextPackId]),
    ].filter((value) => Boolean(value)));
}
function emptyContinuationSignals() {
    return { files: [], symbols: [], errors: [] };
}
function mergeSignals(target, source) {
    target.files.push(...source.files);
    target.symbols.push(...source.symbols);
    target.errors.push(...source.errors);
}
function extractContextPackSignals(pack) {
    return {
        files: uniqueStrings([
            ...pack.classified.files,
            ...pack.sections.flatMap((section) => section.items.flatMap((item) => [
                ...item.labels.filter((label) => label.type === 'file').map((label) => label.value),
                ...item.references.filter((reference) => reference.type === 'file').map((reference) => reference.uri),
            ])),
        ].filter(isLikelyProjectFile)),
        symbols: uniqueStrings([
            ...pack.classified.symbols,
            ...pack.sections.flatMap((section) => section.items.flatMap((item) => (item.labels.filter((label) => label.type === 'symbol').map((label) => label.value)))),
        ].filter(isUsefulContinuationSymbol)),
        errors: uniqueStrings([
            ...pack.classified.errors,
            ...pack.sections.flatMap((section) => section.items.flatMap((item) => (item.labels.filter((label) => label.type === 'error').map((label) => label.value)))),
        ].filter(isUsefulContinuationError)),
    };
}
function extractPromptSignals(prompt) {
    return {
        files: prompt.match(/(?:[\w.-]+\/)+[\w.-]+\.[a-zA-Z0-9]+|[\w.-]+\.[jt]sx?|[\w.-]+\.py|[\w.-]+\.go|[\w.-]+\.rs|[\w.-]+\.md/g)?.filter(isLikelyProjectFile) ?? [],
        symbols: uniqueStrings([
            ...(prompt.match(/\b[A-Z][A-Za-z0-9_]*(?:Service|Controller|Repository|Provider|Handler|Store|Model|Schema|Config|Client)\b/g) ?? []),
            ...(prompt.match(/\b[A-Z][A-Za-z0-9_]{2,}\b/g) ?? []),
        ].filter(isUsefulContinuationSymbol)),
        errors: uniqueStrings([
            ...(prompt.match(/\b[A-Z][A-Z0-9_]*(?:Error|Exception|Failure)\b/g) ?? []),
            ...(prompt.match(/\b[A-Z]{2,}[-_][A-Z0-9_-]+\b/g) ?? []),
            ...(prompt.match(/\b(?:TS|ERR|E)[-_]?\d{3,6}\b/g) ?? []),
        ].filter(isUsefulContinuationError)),
    };
}
function extractDecisionSignals(decision) {
    return {
        files: metadataStringArray(decision.metadata.files).filter(isLikelyProjectFile),
        symbols: metadataStringArray(decision.metadata.symbols).filter(isUsefulContinuationSymbol),
        errors: metadataStringArray(decision.metadata.errors).filter(isUsefulContinuationError),
    };
}
function metadataStringArray(value) {
    return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
}
function isLikelyProjectFile(value) {
    return /^[\w./-]+\.[a-zA-Z0-9]+$/.test(value) && !value.includes('://');
}
function isUsefulContinuationSymbol(value) {
    return /^[A-Za-z_$][\w$.:#-]{2,}$/.test(value)
        && !CONTINUATION_SYMBOL_STOP_WORDS.has(value.toLowerCase())
        && !isLikelyDocumentIdentifier(value);
}
function isUsefulContinuationError(value) {
    return /^[A-Za-z0-9_-]{3,}$/.test(value)
        && (/\d/.test(value) || /(?:Error|Exception|Failure)$/.test(value))
        && !isLikelyDocumentIdentifier(value);
}
function mergeExplicitAndInferred(explicit, inferred, inferredLimit) {
    const explicitValues = explicit ?? [];
    const availableInferred = Math.max(0, inferredLimit - explicitValues.length);
    const merged = uniqueStrings([
        ...explicitValues,
        ...uniqueStrings(inferred).slice(0, availableInferred),
    ]);
    return merged.length ? merged : undefined;
}
function projectFromInput(input) {
    const hint = input.repoHint ?? input.cwd;
    return hint ? normalizeLabel(hint.split('/').filter(Boolean).at(-1) ?? hint) : undefined;
}
/** @internal Exposed for unit tests; matches the phrases that trigger continuation-provenance expansion. */
export function isContinuationPrompt(prompt) {
    const lower = prompt.toLowerCase();
    // Phrase-anchored: requires an explicit continuation construct. Avoids false positives like
    // "current rate limit policy", "continue using strict mode", "tests continue to pass" that
    // would otherwise pay the session × decision × pack expansion.
    return /\bcontinue\s+(?:the|our|my|this|where|from|previous|prior|last)\b|\bresume (?:the |my |our )?(?:work|session|task)\b|\bhand[ -]?off\b|\bpick up where\b|\bwhere we left off\b|\bpicking up where\b|\bpick up the (?:work|task)\b/.test(lower);
}
const CONTINUATION_SYMBOL_STOP_WORDS = new Set([
    'continue',
    'continuation',
    'current',
    'phase',
    'roadmap',
    'the',
    'for',
    'keep',
    'strip',
    'before',
    'after',
    'added',
    'updated',
    'verified',
    'loaded',
    'implemented',
    'improve',
    'focus',
    'next',
    'agent',
    'context',
    'usefulness',
    'hardening',
    'tuberosa',
    'mcp',
    'implement',
    'everything',
    'tried',
    'failed',
    'needed',
    'correction',
    'things',
    'status',
    'summary',
    'notes',
    'draft',
    'drafts',
    'gap',
    'gaps',
    'proposal',
    'proposals',
    'operation',
    'operations',
    'uuid',
    'uuids',
    'http',
    'api',
    'json',
]);
function isLikelyDocumentIdentifier(value) {
    return /^[A-Z][A-Z0-9_]+$/.test(value) && value.includes('_') && !/\d/.test(value);
}
function usefulnessCapsForRequest(input) {
    if (input.debug) {
        return UNCAPPED_USEFULNESS_CAPS;
    }
    if (input.noiseTolerance === 'strict') {
        return {
            priorLessons: 3,
            adjacentContext: 1,
        };
    }
    if (input.deepContextBudget > DEFAULT_DEEP_CONTEXT_BUDGET) {
        const expansion = Math.min(2, input.deepContextBudget / DEFAULT_DEEP_CONTEXT_BUDGET);
        return {
            priorLessons: Math.ceil(DEFAULT_USEFULNESS_CAPS.priorLessons * expansion),
            adjacentContext: Math.ceil(DEFAULT_USEFULNESS_CAPS.adjacentContext * expansion),
        };
    }
    return DEFAULT_USEFULNESS_CAPS;
}
function normalizeSearchInput(input, config) {
    return {
        ...input,
        tokenBudget: input.tokenBudget ?? DEFAULT_TOKEN_BUDGET,
        contextMode: input.contextMode ?? config.context.mode ?? 'layered',
        noiseTolerance: input.noiseTolerance ?? 'balanced',
        deepContextBudget: normalizeDeepContextBudget(input.deepContextBudget ?? config.context.deepContextBudget),
        rejectedKnowledgeIds: input.rejectedKnowledgeIds ?? [],
        debug: input.debug ?? false,
    };
}
function groupChunks(chunks) {
    const grouped = new Map();
    for (const chunk of chunks) {
        const list = grouped.get(chunk.knowledgeId) ?? [];
        list.push(chunk);
        grouped.set(chunk.knowledgeId, list);
    }
    for (const list of grouped.values()) {
        list.sort((left, right) => left.chunkIndex - right.chunkIndex);
    }
    return grouped;
}
function takeChunksWithinBudget(chunks, budget) {
    const selected = [];
    let tokens = 0;
    for (const chunk of chunks) {
        if (selected.length > 0 && tokens + chunk.tokenEstimate > budget) {
            break;
        }
        if (chunk.tokenEstimate > budget && selected.length === 0) {
            break;
        }
        selected.push(chunk);
        tokens += chunk.tokenEstimate;
    }
    return selected;
}
function buildDeepContextItem(candidate, chunks, budget) {
    const chunkIds = chunks?.map((chunk) => chunk.id) ?? (candidate.chunkId ? [candidate.chunkId] : []);
    const content = chunks?.map((chunk) => chunk.content).join('\n\n') ?? candidate.content;
    const contextualContent = chunks?.map((chunk) => chunk.contextualContent).join('\n\n---\n\n') ?? candidate.contextualContent;
    const tokenEstimate = chunks?.reduce((sum, chunk) => sum + chunk.tokenEstimate, 0) ?? candidate.tokenEstimate;
    if (tokenEstimate > budget) {
        return undefined;
    }
    return {
        knowledgeId: candidate.knowledgeId,
        title: candidate.title,
        summary: candidate.summary,
        itemType: candidate.itemType,
        project: candidate.project,
        labels: candidate.labels,
        references: candidate.references,
        source: candidate.source,
        rank: candidate.rank,
        finalScore: candidate.finalScore,
        matchReasons: candidate.matchReasons,
        evidenceCategory: candidate.evidenceCategory,
        evidenceStrength: candidate.evidenceStrength,
        usefulnessReason: candidate.usefulnessReason,
        actionableMissingSignals: candidate.actionableMissingSignals,
        chunkIds,
        content,
        contextualContent,
        tokenEstimate,
    };
}
/**
 * Phase 6a — drop candidates whose persisted namespace mismatches the supplied filter.
 * Read-side filter; the candidate's `metadata.namespace` is set on upsert by both
 * stores. Backwards-compatible: a missing filter (the common case) returns the input
 * untouched.
 */
function applyNamespaceFilter(candidates, filter) {
    if (!filter || (!filter.kind && !filter.agent && !filter.project))
        return candidates;
    return candidates.filter((candidate) => {
        const stored = readNamespaceFromMetadata(candidate.metadata);
        return namespaceMatchesFilter(stored, filter);
    });
}
function enrichGraphCandidates(graphCandidates, classified, seedCandidates) {
    if (graphCandidates.length === 0) {
        return [];
    }
    const connectedSignals = compactRecord({
        files: signalsCoveredBySeeds(classified.files, seedCandidates),
        symbols: signalsCoveredBySeeds(classified.symbols, seedCandidates),
        errors: signalsCoveredBySeeds(classified.errors, seedCandidates),
    });
    if (Object.keys(connectedSignals).length === 0) {
        return graphCandidates;
    }
    return graphCandidates.map((candidate) => ({
        ...candidate,
        metadata: {
            ...(candidate.metadata ?? {}),
            graphContextFit: connectedSignals,
        },
    }));
}
function signalsCoveredBySeeds(signals, seedCandidates) {
    return uniqueStrings(signals.filter((signal) => seedCandidates.some((candidate) => candidateContainsSignal(candidate, signal))));
}
function candidateContainsSignal(candidate, signal) {
    const text = [
        candidate.title,
        candidate.summary,
        candidate.content,
        candidate.contextualContent,
        candidate.labels.map((label) => `${label.type}:${label.value}`).join(' '),
        candidate.references.map((reference) => reference.uri).join(' '),
        JSON.stringify(candidate.metadata ?? {}),
    ].join(' ').toLowerCase();
    return text.includes(signal.toLowerCase());
}
function compactRecord(record) {
    return Object.fromEntries(Object.entries(record).filter(([, values]) => values.length > 0));
}
function fingerprintSearch(input, classified, config, rewrite) {
    return sha256(stableJson({
        prompt: input.prompt,
        project: input.project,
        repoHint: input.repoHint,
        cwd: input.cwd,
        taskType: input.taskType,
        files: input.files ?? [],
        symbols: input.symbols ?? [],
        errors: input.errors ?? [],
        tokenBudget: input.tokenBudget,
        contextMode: input.contextMode,
        noiseTolerance: input.noiseTolerance,
        deepContextBudget: input.deepContextBudget,
        rejectedKnowledgeIds: input.rejectedKnowledgeIds,
        lexicalQuery: classified.lexicalQuery,
        exactTerms: classified.exactTerms,
        queryRewriteModel: rewrite?.model,
        rerankModel: config.model.openAiRerankModel,
        policyFingerprint: getRetrievalPolicyFingerprint(),
        namespace: input.namespace ?? null,
    }));
}
function applyNoiseTolerance(contextFit, classified, candidates, noiseTolerance) {
    if (noiseTolerance !== 'strict') {
        return contextFit;
    }
    const hasDirectEvidence = candidates.slice(0, 3).some((candidate) => hasHardSignalEvidence(candidate, classified));
    if (hasDirectEvidence) {
        return contextFit;
    }
    if (contextFit.fitStatus === 'needs_confirmation') {
        return {
            ...contextFit,
            fitStatus: 'insufficient',
            fitScore: Math.min(contextFit.fitScore, 0.44),
            fitReasons: uniqueStrings([
                ...contextFit.fitReasons,
                'strict noise tolerance rejected weak semantic match',
            ]),
            missingSignals: uniqueStrings([
                ...contextFit.missingSignals,
                'strict_no_hard_signal',
            ]),
        };
    }
    if (contextFit.fitStatus !== 'ready') {
        return contextFit;
    }
    return {
        ...contextFit,
        fitStatus: 'needs_confirmation',
        fitScore: Math.min(contextFit.fitScore, 0.71),
        fitReasons: uniqueStrings([
            ...contextFit.fitReasons,
            'strict noise tolerance downgraded weak semantic match',
        ]),
        missingSignals: uniqueStrings([
            ...contextFit.missingSignals,
            'strict noise tolerance requires direct file, symbol, or error evidence before proceeding',
        ]),
    };
}
function applyQueryRewrite(classified, rewrite, safety) {
    if (!rewrite?.lexicalQuery.trim()) {
        return { classified, addedExactTerms: [] };
    }
    const sanitizedRewriteQuery = safety.redactSecrets(rewrite.lexicalQuery);
    const sanitizedRewriteTerms = uniqueStrings((rewrite.exactTerms ?? [])
        .map((term) => safety.redactSecrets(term))
        .filter((term) => term.length <= 120));
    const exactTerms = uniqueStrings([...classified.exactTerms, ...sanitizedRewriteTerms]).slice(0, 48);
    const lexicalQuery = uniqueStrings([
        ...exactTerms,
        ...(sanitizedRewriteQuery.match(/[a-zA-Z0-9_./:-]{3,}/g) ?? []),
        ...(classified.lexicalQuery.match(/[a-zA-Z0-9_./:-]{3,}/g) ?? []),
    ]).slice(0, 64).join(' ');
    const addedExactTerms = exactTerms.filter((term) => !classified.exactTerms.includes(term));
    const sanitizedRewrite = {
        lexicalQuery,
        exactTerms: sanitizedRewriteTerms,
        reasons: (rewrite.reasons ?? []).map((reason) => safety.redactSecrets(reason)),
        model: rewrite.model,
    };
    return {
        classified: {
            ...classified,
            exactTerms,
            lexicalQuery,
            confidence: Math.min(0.98, classified.confidence + (addedExactTerms.length > 0 ? 0.06 : 0.03)),
        },
        addedExactTerms,
        rewrite: sanitizedRewrite,
    };
}
function shouldRetry(feedbackType) {
    return RETRY_FEEDBACK_TYPES.has(feedbackType);
}
function buildRetryInput(pack, feedback) {
    return {
        prompt: pack.prompt,
        project: feedback.project ?? pack.project,
        tokenBudget: contextPackTokenBudget(pack),
        rejectedKnowledgeIds: rejectedKnowledgeIds(pack, feedback),
        bypassCache: true,
    };
}
function contextPackTokenBudget(pack) {
    return pack.sections.reduce((sum, section) => sum + section.tokenEstimate, 0) || DEFAULT_TOKEN_BUDGET;
}
function rejectedKnowledgeIds(pack, feedback) {
    return [
        ...new Set([
            ...pack.sections.flatMap((section) => section.items.map((item) => item.knowledgeId)),
            ...(feedback.rejectedKnowledgeIds ?? []),
        ]),
    ];
}
function feedbackMissingSignals(input, pack) {
    const metadataSignals = metadataStringArray(input.metadata?.missingSignals);
    const signals = [
        ...(pack?.contextFit?.missingSignals ?? []),
        ...metadataSignals,
        ...(input.reason ? [input.reason] : []),
    ];
    return uniqueStrings(signals).slice(0, 16);
}
function feedbackAffectedKnowledgeIds(input, pack) {
    if (input.rejectedKnowledgeIds?.length) {
        return uniqueStrings(input.rejectedKnowledgeIds);
    }
    return uniqueStrings(pack?.sections.flatMap((section) => section.items.map((item) => item.knowledgeId)) ?? []);
}
function proposalTypeForFeedback(feedbackType) {
    if (feedbackType === 'stale') {
        return 'supersedes';
    }
    return 'missing_relation';
}
function proposalReason(feedbackType, reason) {
    if (reason) {
        return reason;
    }
    if (feedbackType === 'stale') {
        return 'Stale context should be reviewed for a supersedes relation or archival.';
    }
    if (feedbackType === 'too_much_adjacent_context') {
        return 'Adjacent context dominated the pack; review labels, relations, or freshness to lower its rank for similar prompts.';
    }
    return 'Rejected or irrelevant context should be reviewed for missing labels, references, or relations.';
}
function suggestedActionForFeedback(feedbackType) {
    if (feedbackType === 'stale') {
        return 'review for a supersedes relation, freshness update, or archive';
    }
    if (feedbackType === 'too_much_adjacent_context') {
        return 'review adjacent context noise: tighten labels, add relations, or lower freshness/trust for similar prompts';
    }
    return 'review labels, references, and graph relations before changing ranking';
}
function proposalEvidence(input, pack, affectedKnowledgeId) {
    const item = pack?.sections
        .flatMap((section) => section.items)
        .find((candidate) => candidate.knowledgeId === affectedKnowledgeId);
    const evidence = [
        `feedback:${input.feedbackType}`,
        input.contextPackId ? `contextPack:${input.contextPackId}` : undefined,
        input.reason ? `reason:${input.reason}` : undefined,
        ...(item?.matchReasons ?? []).slice(0, 6).map((reason) => `match:${reason}`),
        ...(item?.fitMissingSignals ?? []).slice(0, 6).map((signal) => `missing:${signal}`),
    ].filter((value) => Boolean(value));
    return uniqueStrings(evidence).slice(0, 16);
}
function applyFeedbackSummary(candidate, summary, onSuppression) {
    if (!summary) {
        return candidate;
    }
    const adjustment = feedbackScoreAdjustment(summary);
    const status = feedbackStatus(summary);
    const feedbackMetadata = {
        status,
        selectedCount: summary.selectedCount,
        selectedNoisyCount: summary.selectedNoisyCount,
        rejectedCount: summary.rejectedCount,
        irrelevantCount: summary.irrelevantCount,
        staleCount: summary.staleCount,
        latestFeedbackType: summary.latestFeedbackType,
        latestFeedbackAt: summary.latestFeedbackAt,
        scoreAdjustment: adjustment,
    };
    if (onSuppression && adjustment < 0) {
        const reason = feedbackSuppressionReason(status);
        if (reason) {
            onSuppression({
                knowledgeId: candidate.knowledgeId,
                reason,
                deltaScore: adjustment,
                confidence: feedbackSuppressionConfidence(summary, status),
                evidence: `feedback status=${status}, latest=${summary.latestFeedbackType ?? 'unknown'}`,
            });
        }
    }
    return {
        ...candidate,
        finalScore: clampScore(candidate.finalScore + adjustment),
        matchReasons: [
            ...candidate.matchReasons,
            ...feedbackMatchReasons(summary, adjustment),
        ],
        metadata: {
            ...(candidate.metadata ?? {}),
            feedback: feedbackMetadata,
        },
    };
}
function feedbackSuppressionReason(status) {
    switch (status) {
        case 'stale':
            return 'feedback_stale';
        case 'rejected':
            return 'feedback_rejected';
        case 'irrelevant':
            return 'feedback_irrelevant';
        default:
            return undefined;
    }
}
function feedbackSuppressionConfidence(summary, status) {
    const negativeCount = summary.staleCount + summary.rejectedCount + summary.irrelevantCount;
    if (negativeCount === 0)
        return 0.5;
    const baseline = Math.min(1, negativeCount / 3);
    const isLatestMatching = summary.latestFeedbackType === status;
    return clamp(isLatestMatching ? Math.max(baseline, 0.7) : baseline, 0, 1);
}
/**
 * Hard floor for cumulative suppression damping. A positive-scoring candidate
 * never drops below this floor purely from intent suppressions. Independent
 * positive boosts (e.g., domain_match) can still raise it above.
 */
const SUPPRESSION_FLOOR = 0.1;
function applyIntentSuppression(candidate, classified, supersededBy, onSuppression, policy = getRetrievalPolicy()) {
    const adjustment = intentSuppressionAdjustment(candidate, classified, supersededBy, policy);
    if (adjustment.factor === 1 && adjustment.boost === 0 && adjustment.events.length === 0) {
        return candidate;
    }
    if (onSuppression) {
        for (const event of adjustment.events) {
            if (event.deltaScore < 0) {
                onSuppression({ ...event, knowledgeId: candidate.knowledgeId });
            }
        }
    }
    // Phase 2 — multiplicative damping with hard floor. Penalties never push a
    // positive score below the floor; positive boosts (e.g., domain match) add on
    // top, then the result is clamped to [0, 1].
    const base = candidate.finalScore;
    const dampedBase = base > SUPPRESSION_FLOOR
        ? Math.max(base * adjustment.factor, SUPPRESSION_FLOOR)
        : base * adjustment.factor;
    const nextScore = clampScore(dampedBase + adjustment.boost);
    const effectiveDelta = nextScore - base;
    return {
        ...candidate,
        finalScore: nextScore,
        matchReasons: [
            ...candidate.matchReasons,
            ...adjustment.reasons,
        ],
        metadata: {
            ...(candidate.metadata ?? {}),
            retrievalSuppression: {
                scoreAdjustment: roundFeedbackAdjustment(effectiveDelta),
                suppressionFactor: roundFeedbackAdjustment(adjustment.factor),
                boost: roundFeedbackAdjustment(adjustment.boost),
                reasons: adjustment.reasons,
                supersededBy: supersededBy.map((relation) => relation.fromKnowledgeId),
            },
        },
    };
}
/**
 * Convert an additive-style "delta" (legacy: -0.14 etc.) into a multiplicative
 * factor in (0, 1]. The mapping preserves intent ordering (bigger penalty →
 * smaller factor) but bounds the per-source contribution so cumulative product
 * stays well-behaved.
 */
function penaltyDeltaToFactor(delta) {
    if (delta >= 0)
        return 1;
    // |delta|=0.28 → ~0.62; |delta|=0.14 → ~0.79; |delta|=0.10 → ~0.85; |delta|=0.08 → ~0.88
    return clamp(Math.exp(2.2 * delta), 0.4, 1);
}
function intentSuppressionAdjustment(candidate, classified, supersededBy, policy = getRetrievalPolicy()) {
    const reasons = [];
    const events = [];
    let factor = 1;
    let boost = 0;
    const hasHardEvidence = hasHardSignalEvidence(candidate, classified);
    if (policy.suppressionEnabled.superseded && supersededBy.length > 0) {
        const strongest = Math.max(...supersededBy.map((relation) => relation.confidence));
        const delta = -Math.min(0.28, 0.18 + strongest * 0.08);
        factor *= penaltyDeltaToFactor(delta);
        reasons.push(`suppression:superseded:${supersededBy[0].fromKnowledgeId}`);
        events.push({
            reason: 'superseded',
            deltaScore: roundFeedbackAdjustment(delta),
            confidence: clamp(strongest, 0, 1),
            evidence: `superseded by ${supersededBy[0].fromKnowledgeId} (confidence=${strongest.toFixed(2)})`,
        });
    }
    if (policy.suppressionEnabled.stale && isStaleCandidate(candidate, policy) && !hasHardEvidence) {
        const delta = -0.14;
        factor *= penaltyDeltaToFactor(delta);
        reasons.push('suppression:freshness:stale');
        events.push({
            reason: 'stale_freshness',
            deltaScore: delta,
            confidence: staleFreshnessConfidence(candidate, policy),
            evidence: `itemType=${candidate.itemType} freshnessAt=${candidate.freshnessAt ?? metadataString(candidate.metadata, 'freshnessAt') ?? 'unknown'}`,
        });
    }
    if (policy.suppressionEnabled.feedback) {
        const feedback = feedbackStatusFromCandidate(candidate);
        if ((feedback === 'stale' || feedback === 'rejected' || feedback === 'irrelevant') && !hasHardEvidence) {
            const delta = feedback === 'stale' ? -0.1 : -0.08;
            factor *= penaltyDeltaToFactor(delta);
            reasons.push(`suppression:prior feedback:${feedback}`);
            const suppressionReason = feedbackSuppressionReason(feedback);
            if (suppressionReason) {
                events.push({
                    reason: suppressionReason,
                    deltaScore: delta,
                    confidence: 0.7,
                    evidence: `candidate-side feedback status=${feedback}`,
                });
            }
        }
    }
    if (policy.suppressionEnabled.evidenceMismatch
        && !hasHardEvidence
        && !requiredEvidenceMatches(candidate, classified.intent.requiredEvidenceTypes)) {
        const delta = -0.1;
        factor *= penaltyDeltaToFactor(delta);
        reasons.push('suppression:evidence_mismatch');
        events.push({
            reason: 'evidence_mismatch',
            deltaScore: delta,
            confidence: 0.6,
            evidence: `required evidence types=${classified.intent.requiredEvidenceTypes.join(',') || 'none'}`,
        });
    }
    // Phase 5 — worktree candidates that match a prompt-named file represent live truth;
    // give them a strong positive boost so they outrank conflicting durable memory for
    // continuation / self-edit tasks. RRF fusion otherwise compounds across multiple
    // memory/graph/lexical contributions and the single-source worktree weight (1.30)
    // alone cannot beat a memory hit by 3 sources. The boost is sized so that a
    // prompt-named worktree candidate at a mid-tier fused score reliably outranks a
    // memory candidate sweeping memory+graph+lexical at the top of the fused list.
    if (candidate.source === 'worktree') {
        const worktreeMeta = candidate.metadata?.worktree;
        if (worktreeMeta?.promptMatch || worktreeMeta?.reason === 'prompt_named') {
            // The boost must overcome (a) the single-source-fusion penalty vs a memory
            // candidate hit by 3 sources, and (b) the domain_match bonus durable memories
            // typically already carry. Sized empirically against the Phase 5 regression
            // fixture; smaller values let multi-source memories win.
            const delta = 0.6;
            boost += delta;
            reasons.push('boost:worktree_live_evidence:prompt_named');
        }
        else if (worktreeMeta?.reason === 'git_changed') {
            const delta = 0.22;
            boost += delta;
            reasons.push('boost:worktree_live_evidence:git_changed');
        }
    }
    if (policy.suppressionEnabled.domainMismatch && classified.domain) {
        const domainLabels = candidate.labels.filter((label) => label.type === 'domain');
        if (domainLabels.length > 0) {
            const target = classified.domain.toLowerCase();
            const matches = domainLabels.some((label) => label.value.toLowerCase() === target);
            if (matches) {
                // Permissive boost: a matching domain (classifier-inferred is fine) lifts the score.
                const delta = policy.domainMismatch.matchBoost;
                boost += delta;
                reasons.push(`boost:domain_match:${classified.domain}`);
            }
            else {
                // Strict penalty: only fire when at least one EXPLICIT (user-supplied / reviewed) domain
                // label exists and none match. Classifier-inferred-only candidates are heuristic and
                // would create false-positive suppression for any candidate that simply lives under a
                // different `src/X/` directory than the query.
                const explicitDomainLabels = domainLabels.filter(isExplicitDomainCandidateLabel);
                if (explicitDomainLabels.length > 0) {
                    const delta = policy.domainMismatch.mismatchPenalty;
                    factor *= penaltyDeltaToFactor(delta);
                    reasons.push(`suppression:domain_mismatch:${classified.domain}`);
                    events.push({
                        reason: 'domain_mismatch',
                        deltaScore: roundFeedbackAdjustment(delta),
                        confidence: 0.9,
                        evidence: `candidate domain labels=[${explicitDomainLabels.map((label) => label.value).join(', ')}] expected=${classified.domain}`,
                    });
                }
            }
        }
    }
    return { factor, boost, reasons, events };
}
function staleFreshnessConfidence(candidate, policy = getRetrievalPolicy()) {
    const freshnessAt = candidate.freshnessAt ?? metadataString(candidate.metadata, 'freshnessAt');
    if (!freshnessAt)
        return 0.5;
    const timestamp = Date.parse(freshnessAt);
    if (Number.isNaN(timestamp))
        return 0.5;
    const window = freshnessWindowFor(policy, candidate.itemType);
    const ageDays = Math.max(0, Math.floor((Date.now() - timestamp) / 86_400_000));
    if (ageDays <= window.staleDays)
        return 0.5;
    const overshootRatio = (ageDays - window.staleDays) / Math.max(1, window.staleDays);
    return clamp(0.6 + overshootRatio * 0.4, 0.6, 1);
}
function hasHardSignalEvidence(candidate, classified) {
    return [
        ...classified.files,
        ...classified.symbols,
        ...classified.errors,
    ].some((signal) => candidateText(candidate).includes(signal.toLowerCase()));
}
function isStaleCandidate(candidate, policy = getRetrievalPolicy()) {
    const metadataStale = candidate.metadata?.stale === true || feedbackStatusFromCandidate(candidate) === 'stale';
    if (metadataStale) {
        return true;
    }
    const freshnessAt = candidate.freshnessAt ?? metadataString(candidate.metadata, 'freshnessAt');
    if (!freshnessAt) {
        return false;
    }
    const timestamp = Date.parse(freshnessAt);
    if (Number.isNaN(timestamp)) {
        return false;
    }
    const window = freshnessWindowFor(policy, candidate.itemType);
    return Date.now() - timestamp > window.staleDays * 86_400_000;
}
function requiredEvidenceMatches(candidate, evidenceTypes) {
    if (evidenceTypes.length === 0) {
        return true;
    }
    return evidenceTypes.some((type) => candidateSupportsEvidence(candidate, type));
}
function candidateSupportsEvidence(candidate, evidenceType) {
    switch (evidenceType) {
        case 'spec':
            return candidate.itemType === 'spec' || hasMetadataTaxonomy(candidate, 'domain_rule');
        case 'workflow':
            return candidate.itemType === 'workflow' || candidate.itemType === 'rule' || hasMetadataTaxonomy(candidate, 'workflow');
        case 'code_reference':
            return candidate.itemType === 'code_ref'
                || candidate.references.some((reference) => reference.type === 'file')
                || candidate.labels.some((label) => label.type === 'file' || label.type === 'symbol');
        case 'bugfix':
            return candidate.itemType === 'bugfix'
                || candidate.labels.some((label) => label.type === 'error' || (label.type === 'task_type' && label.value === 'debugging'));
        case 'incident_lesson':
            return hasMetadataTaxonomy(candidate, 'incident_lesson')
                || metadataString(candidate.metadata, 'triggerType') === 'error_recovery'
                || candidate.references.some((reference) => reference.uri.startsWith('tuberosa://error-logs/'));
        case 'reflection_memory':
            return candidate.itemType === 'memory' || Boolean(metadataString(candidate.metadata, 'triggerType'));
        case 'session_history':
            return Boolean(metadataString(candidate.metadata, 'agentSessionId'))
                || candidate.references.some((reference) => reference.type === 'conversation');
        case 'handoff':
            return candidate.references.some((reference) => reference.uri === 'handoff.md')
                || candidate.labels.some((label) => label.type === 'file' && label.value === 'handoff.md');
        case 'docs':
            return candidate.itemType === 'wiki'
                || candidate.references.some((reference) => reference.type === 'file' && reference.uri.endsWith('.md'));
        case 'tests':
            return candidate.references.some((reference) => /(^|\/|-)test(s)?[/.]|\.test\./.test(reference.uri))
                || candidate.labels.some((label) => label.type === 'file' && /(^|\/|-)test(s)?[/.]|\.test\./.test(label.value));
    }
}
function hasMetadataTaxonomy(candidate, taxonomy) {
    return metadataString(candidate.metadata, 'taxonomy') === taxonomy;
}
function metadataUuidString(metadata, key) {
    const value = metadataString(metadata, key);
    return value && isRfc4122Uuid(value) ? value : undefined;
}
function feedbackStatusFromCandidate(candidate) {
    const direct = metadataString(candidate.metadata, 'feedbackStatus') ?? metadataString(candidate.metadata, 'priorFeedback');
    if (direct) {
        return direct;
    }
    const feedback = candidate.metadata?.feedback;
    if (!feedback || typeof feedback !== 'object') {
        return undefined;
    }
    const status = feedback.status;
    return typeof status === 'string' ? status : undefined;
}
function feedbackScoreAdjustment(summary) {
    const selectedBoost = Math.min(0.1, summary.selectedCount * 0.04);
    const noisyPenalty = Math.min(0.08, summary.selectedNoisyCount * 0.03);
    const stalePenalty = Math.min(0.24, summary.staleCount * 0.2);
    const rejectionPenalty = Math.min(0.18, (summary.rejectedCount + summary.irrelevantCount) * 0.09);
    return roundFeedbackAdjustment(selectedBoost - noisyPenalty - stalePenalty - rejectionPenalty);
}
function feedbackStatus(summary) {
    if (summary.latestFeedbackType && summary.latestFeedbackType !== 'missing_context') {
        return summary.latestFeedbackType;
    }
    if (summary.staleCount > 0) {
        return 'stale';
    }
    if (summary.rejectedCount > 0) {
        return 'rejected';
    }
    if (summary.irrelevantCount > 0) {
        return 'irrelevant';
    }
    return 'selected';
}
function feedbackMatchReasons(summary, adjustment) {
    const reasons = [];
    if (summary.selectedCount > 0) {
        reasons.push(`feedback:selected:${summary.selectedCount}`);
    }
    if (summary.selectedNoisyCount > 0) {
        reasons.push(`feedback:selected_but_noisy:${summary.selectedNoisyCount}`);
    }
    if (summary.rejectedCount > 0) {
        reasons.push(`feedback:rejected:${summary.rejectedCount}`);
    }
    if (summary.irrelevantCount > 0) {
        reasons.push(`feedback:irrelevant:${summary.irrelevantCount}`);
    }
    if (summary.staleCount > 0) {
        reasons.push(`feedback:stale:${summary.staleCount}`);
    }
    if (adjustment !== 0) {
        reasons.push(`feedback adjustment:${adjustment > 0 ? '+' : ''}${adjustment.toFixed(3)}`);
    }
    return reasons;
}
function clampScore(value) {
    return Math.max(0, Math.min(1, value));
}
function roundFeedbackAdjustment(value) {
    return Math.round(value * 1000) / 1000;
}
function isExplicitDomainCandidateLabel(label) {
    const provenance = label.provenance?.source;
    if (!provenance)
        return true; // user-supplied input with no provenance attached
    return provenance !== 'classifier';
}
