/** * Opportunity enricher: when creating an opportunity, find overlapping existing * opportunities (by non-introducer actor userId), check semantic relatedness, and * optionally merge into a single enriched opportunity and expire the old one(s). */ import type { CreateOpportunityData, Opportunity, OpportunityStatus } from '../../shared/interfaces/database.interface.js'; import type { Embedder } from '../../shared/interfaces/embedder.interface.js'; /** * Statuses excluded from the merge-candidate pool by default. * * - 'accepted': the pair already connected — do NOT fold a new discovery into * the historical opp. IND-237 surfaces the existing conversation separately. * - 'negotiating': a negotiation is in-flight for this pair; rolling a new * candidate into it would blur the outcome of the active turn. Wait for the * negotiation to finalize (→ draft/pending/rejected/stalled) first, then * enrichment can pick it up on the next pass. * - 'expired': already superseded by a later enriched opportunity. Without this * exclusion, each enrichment cycle re-merges previously-expired enriched * opportunities, compounding their actor arrays into 100+ entries. * * Exported for callers that want to extend rather than replace the default. */ export declare const DEFAULT_ENRICHER_EXCLUDE_STATUSES: OpportunityStatus[]; export type EnricherDatabase = { findOpportunitiesByActors(actorIds: string[], options?: { includeIntroducers?: boolean; statuses?: OpportunityStatus[]; excludeStatuses?: OpportunityStatus[]; }): Promise; }; export type EnrichmentResult = { enriched: false; data: CreateOpportunityData; } | { enriched: true; data: CreateOpportunityData; expiredIds: string[]; resolvedStatus: OpportunityStatus; }; export type EnrichOrCreateOptions = { similarityThreshold?: number; /** * Statuses to exclude from the merge-candidate pool. Defaults to * {@link DEFAULT_ENRICHER_EXCLUDE_STATUSES} (`['accepted', 'negotiating']`). * Pass an empty array `[]` to consider all statuses. */ excludeStatuses?: OpportunityStatus[]; /** Restrict merge/expiration candidates to the authoritative owned intent. */ ownedIntentScope?: { triggerIntentId: string; ownerUserId: string; }; }; /** * Enrich or create: find overlapping opportunities, filter by semantic relatedness * using a two-phase approach, merge actors and interpretation into a single * CreateOpportunityData, and return the data plus IDs to expire. If no related * overlap, return original data unchanged. * * Phase 1 — Intent check (free, no API call): * Shared intent IDs mean the same declared user goal drove both opportunities. * This is rare because the IntentReconciler already deduplicates intents per-user * upstream, but when it fires it is definitive. * * Phase 2 — Batched embedding similarity (one API call for all remaining): * For opportunities without shared intents, embed all reasoning texts in a single * batch call and compare cosine similarity. Cross-user intent comparison (e.g. * Alice's "find ML co-founder" vs Bob's "join ML startup") is implicitly handled * here since reasoning text synthesizes both users' intents. */ export declare function enrichOrCreate(database: EnricherDatabase, embedder: Embedder, newData: CreateOpportunityData, options?: EnrichOrCreateOptions): Promise;