// ============================================================================
// lib/triage-core/types.ts — Shared Triage Data Model (Channel-Agnostic)
//
// Contract between all triage modules: classifier, signals, mental model,
// briefing, and learning. Works for both email and iMessage.
// ============================================================================

import type { CrossChannelContext } from './cross-channel-context.ts';
import type { SourceFactEnvelope, SourceFactCoverage } from './source-fact-envelope.ts';

// ---------------------------------------------------------------------------
// Platform & Channel
// ---------------------------------------------------------------------------

/** Supported triage platforms */
export type TriagePlatform = 'email' | 'imessage' | 'sms' | (string & {});

// ---------------------------------------------------------------------------
// Priority & Category Enums
// ---------------------------------------------------------------------------

/** Eisenhower matrix mapping: P0=Do Now, P1=Schedule, P2=Delegate, P3=Archive */
export type TriagePriority = 'P0' | 'P1' | 'P2' | 'P3';

/** Triage action category */
export type TriageCategory =
  | 'action_required'
  | 'review'
  | 'fyi'
  | 'delegate'
  | 'archive'
  | 'spam';

/** Suggested next action */
export type SuggestedAction =
  | 'reply_now'
  | 'schedule'
  | 'delegate'
  | 'archive'
  | 'snooze'
  | 'unsubscribe'
  | 'acknowledge'
  | 'follow_up';

/** Message direction */
export type MessageDirection = 'inbound' | 'outbound';

/** Message type classified by entity extractor */
export type MessageType =
  | 'question'
  | 'update'
  | 'request'
  | 'fyi'
  | 'greeting'
  | 'followup'
  | 'escalation';

/** Sentiment classification */
export type Sentiment = 'positive' | 'neutral' | 'negative';

// ---------------------------------------------------------------------------
// 29-Signal Classifier
// ---------------------------------------------------------------------------

/** All 29 signal sources for the shared classifier */
export type SignalSource =
  | 'graph_rank'              // 1: PageRank from Person node
  | 'channel_context'         // 2: Channel-specific (Gmail labels OR iMessage thread type)
  | 'urgency_content'         // 3: Regex urgency patterns in text
  | 'relationship'            // 4: Temporal decay relationship strength
  | 'contact_role'            // 5: Person.role lookup (c-level boost, etc.)
  | 'unknown_sender'          // 6: No Person node = cold penalty
  | 'open_questions'          // 7: Count of unanswered questions → priority boost
  | 'overdue_commitments'     // 8: Count of overdue items → priority boost
  | 'topic_continuity'        // 9: Active topic count with recent mentions
  | 'comms_style'             // 10: Response latency expectation mismatch
  | 'relationship_health'     // 11: Composite relationship health score
  | 'relationship_risk'       // 12: Relationship decay/risk detection
  | 'trajectory_signal'       // 13: Engagement trajectory direction
  | 'emotional_tone'          // 14: Sentiment/emotional tone analysis
  | 'favee_type'              // 15: Favee classification type signal
  | 'network_cohesion'        // 16: Network graph cohesion metric
  | 'trajectory_momentum'     // 17: Momentum of interaction trajectory
  | 'knowledge_freshness'     // 18: Freshness of knowledge about contact
  | 'user_tags'               // 19: User-assigned tag relevance
  | 'relationship_persistence'    // 20: Long-term relationship persistence
  | 'cross_channel_escalation'   // 21: Multi-channel contact within 48h
  | 'response_debt'              // 22: Days since last outbound reply
  | 'relationship_decay'            // 23: Silence relative to tier frequency
  | 'sender_legitimacy'             // 24: Unknown sender urgency claim validation
  | 'frequency_acceleration'        // 25: Message frequency escalation pattern
  | 'referral_chain'                // 26: Warm intro from trusted contact
  | 'sender_prestige'               // 27: Business prestige (domain, institutional, social proof)
  | 'personal_importance'           // 28: Personal importance (obligation, dormant, crisis)
  | 'goal_relevance'                // 29: HBO goal relevance (message relates to active business goal)
  | 'dunbar_boost'                  // 30: Dunbar layer composite boost (classifier post-processing)
  | 'opportunity_decay'             // 31: Time-decaying opportunity boost (aging unanswered messages)
  | 'session_decay'                 // 32: Post-scoring session-decay synthetic signal
  | 'season_mismatch'              // 33: Season mismatch penalty signal
  | 'initiator_boost'              // 34: Initiator relationship boost signal
  | 'crm_lead_stage'               // 35: CRM Lead pipeline stage (Mission 10a)
  | 'crm_outreach_gap'             // 36: Days since last CRM outreach / overdue follow-up (Mission 10a)
  | 'crm_open_opportunity'         // 37: Active/open CRM opportunity (Mission 10a)
  | 'crm_bounce_flag'              // 38: Recent bounced outreach to this contact (Mission 10a)
  | 'crm_deal_value'               // 39: Monetary value of linked CRM opportunity
  | 'crm_stage_velocity'           // 40: Pace of CRM stage movement
  | 'offline_floor';               // 41: Memgraph-down score floor for VIP/intimate/close (Mission 10b, synthetic)

/** Individual signal score from a scorer module */
export interface TriageSignal {
  source: SignalSource;
  /** Signal category for dashboard chip rendering */
  category: 'urgency' | 'relationship' | 'topic' | 'behavioral' | 'override';
  /** Raw score ∈ [0, 1] */
  score: number;
  /** Weight applied (from config or Thompson-learned) */
  weight: number;
  /** score * weight */
  weightedScore: number;
  /** Human-readable evidence string */
  evidence: string;
}

// ---------------------------------------------------------------------------
// Identity
// ---------------------------------------------------------------------------

/** Cross-platform identity linked to a Person */
export interface Identity {
  id: string;
  handle: string;
  platform: TriagePlatform | 'phone';
  displayName: string | null;
  createdAt: string;
  verified: boolean;
}

/** Identity resolution result */
export interface IdentityResolution {
  personId: string;
  identity: Identity;
  strategy: 'direct_match' | 'phone_normalized' | 'fuzzy_name' | 'shared_topic' | 'new_person';
  confidence: number;
  isNewPerson: boolean;
  isAutomated?: boolean;
}

// ---------------------------------------------------------------------------
// Mental Model — Entity Extraction
// ---------------------------------------------------------------------------

/** 12 question intent categories */
export type QuestionIntent =
  | 'deadline_inquiry'
  | 'status_request'
  | 'decision_needed'
  | 'clarification'
  | 'feedback_request'
  | 'availability'
  | 'pricing'
  | 'technical'
  | 'scheduling'
  | 'approval'
  | 'introduction'
  | 'general';

/** Extracted topic from a message */
export interface Topic {
  id: string;
  name: string;
  category: string;
  status: 'active' | 'dormant' | 'resolved';
  firstMentioned: string;
  lastMentioned: string;
  mentionCount: number;
  sentiment: Sentiment | null;
}

/** Extracted question from a message */
export interface Question {
  id: string;
  text: string;
  intent: QuestionIntent;
  status: 'open' | 'answered' | 'expired';
  urgency: number;
  askedAt: string;
  answeredAt: string | null;
}

/** Extracted commitment from a message */
export interface Commitment {
  id: string;
  text: string;
  owner: 'us' | 'them' | 'mutual';
  status: 'open' | 'completed' | 'overdue' | 'cancelled';
  dueDate: string | null;
  createdAt: string;
  completedAt: string | null;
}

/** Extracted entity (person, org, project, etc.) */
export interface ExtractedEntity {
  name: string;
  type: 'person' | 'organization' | 'project' | 'product' | 'location' | 'event';
  role: string | null;
}

export interface ExtractedLifeEvent {
  event: string;
  category: 'career' | 'education' | 'family' | 'health' | 'milestone' | 'other';
  date: string;
  sentiment: number;
}

/** Full extraction result from the LLM entity extractor */
export interface MessageExtraction {
  messageType: MessageType;
  urgency: number;
  sentiment: Sentiment;
  topics: Array<{ name: string; category: string; sentiment: Sentiment | null }>;
  questions: Array<{ text: string; intent: QuestionIntent; urgency: number }>;
  commitments: Array<{ text: string; owner: 'us' | 'them' | 'mutual'; dueDate: string | null; direction?: string; confidence?: string }>;
  entities: ExtractedEntity[];
  lifeEvents: ExtractedLifeEvent[];
  /** True if this message answers a previously open question */
  answersQuestion: boolean;
  /** True if this message references an open commitment */
  referencesCommitment: boolean;
  /** True when extraction fell back to defaults due to LLM failure */
  extractionFailed: boolean;
  /** LLM cost for this extraction */
  cost: number;
  /** Source method used for this extraction */
  extractionSource?: 'llm' | 'regex' | 'default';
  /** Deterministic source authority retained independently of model prose. */
  factEnvelope?: SourceFactEnvelope;
  /** Exact semantic coverage of the derived extraction against factEnvelope. */
  semanticCoverage?: SourceFactCoverage;
  /** Visible state when the source envelope could not be retained completely. */
  reviewRequired?: boolean;
  /** Successful provider/model identity, when an LLM was reached. */
  modelIdentity?: { provider: string; modelId: string; source: 'local-qwen' | 'converged-auth' | 'injected' };
}

// ---------------------------------------------------------------------------
// Mental Model — Person Model
// ---------------------------------------------------------------------------

/** Dunbar social layer classification */
export type DunbarLayer =
  | 'intimate'
  | 'close'
  | 'friend'
  | 'active'
  | 'recognized'
  | 'acquaintance'; // Legacy persisted alias retained until data migration.

/** Full mental model for a person — assembled from graph */
export interface PersonMentalModel {
  personId: string;
  canonicalName: string;
  identities: Identity[];
  summary: string | null;
  summaryUpdatedAt: string | null;
  communicationStyle: string | null;
  responseLatencyAvg: number | null;
  preferredChannel: TriagePlatform | null;
  timezone: string | null;
  totalInteractions: number;
  lastInteractionAt: string | null;
  isVip: boolean;
  pageRank: number;
  betweennessCentrality: number;
  degreeCentrality: number;
  communityId: number | null;

  /** Recent episodes (last 30 days, max 20) */
  recentEpisodes: Array<{
    id: string;
    text: string;
    platform: TriagePlatform;
    direction: MessageDirection;
    receivedAt: string;
    messageType: MessageType;
  }>;

  /** Active topics being discussed */
  activeTopics: Topic[];

  /** Open questions from this person */
  openQuestions: Question[];

  /** Open commitments involving this person */
  openCommitments: Commitment[];

  /** Relationship strength (0-1, computed by decay algorithm) */
  relationshipStrength: number;

  /** Connected people (same org, shared topics) */
  connectedPeople: Array<{ personId: string; name: string; relationship: string }>;

  /** Rich context brief for response drafting (Phase 1c) */
  contextBrief?: string;

  /** Dunbar social layer (Phase 2a) */
  dunbarLayer?: DunbarLayer;

  /** Relationship trajectory direction (Phase 3b) */
  trajectoryDirection?: string;
  trajectoryVelocity?: number;

  /** Katz centrality - considers all paths (Phase 2c) */
  katzCentrality?: number;

  /** Clustering coefficient (Phase 2c) */
  clusteringCoefficient?: number;

  /** Key facts last updated (Phase 1b) */
  keyFactsUpdatedAt?: string;

  /** Avg message length from comms style (Phase 1a) */
  avgMessageLength?: number;
  emojiRate?: number;
  formalityScore?: number;
  voiceFormalityScore?: number;
  initiationRatio?: number; // experimental — direction semantics unverified

  /** Professional profile fields from Person node */
  jobTitle?: string;
  company?: string;
  location?: string;

  /** V3 Quality Scorer — relationship health classification */
  healthStatus?: 'HEALTHY' | 'AT_RISK' | 'UNHEALTHY' | 'INSUFFICIENT_DATA' | 'SPARSE' | null;

  /** V3 Quality Scorer — full quality profile JSON */
  qualityProfile?: {
    version: number;
    scorerVersion: string;
    positiveAffect: number;
    emotionalNeg: number;
    emotionalRatio: number;
    emotionalClass: string;
    demandWithdraw: number;
    personalDebt: number;
    operationalNeg: number;
    opsRate: number;
    opsStrain: boolean;
    health: string;
    horsemen: { criticism: number; contempt: number; defensiveness: number; stonewalling: number };
    repair: number;
    weRatio: number;
    totalSignals: number;
    episodeCount: number;
    accountType: string;
    computedAt: string;
  } | null;

  /** V3 Account type — PERSON or SERVICE */
  accountType?: 'PERSON' | 'SERVICE' | null;

  /** V3 Resilience monitor — structural persistence prediction */
  resilienceLevel?: 'HIGH' | 'MODERATE' | 'LOW' | 'UNKNOWN' | null;
  resilienceScore?: number | null;

  /** Organization the person works at */
  organization?: {
    id: string;
    name: string;
    domain: string | null;
    role: string | null;
  } | null;

  /** Direct connections via KNOWS edges with roles and FAVEE */
  directConnections?: Array<{
    personId: string;
    name?: string;
    context?: string | null;
    strength?: number | null;
    source?: string | null;
    roles?: Array<{ type: string; label: string }>;
    favee?: Record<string, number>;
    hppCategory?: string;
    canonicalType?: string;
  }>;

  /** Tags associated with this person */
  tags?: string[];

  /** Active reminders for this person */
  activeReminders?: Array<{
    id: string;
    body: string;
    dueAt: string;
  }>;

  // ---------------------------------------------------------------------------
  // Phase 0.2: Veto Layer + Frequency/Life Stage Fields (Tournament-Validated)
  // ---------------------------------------------------------------------------

  /** Explicitly blocked by user (hard veto) */
  blocked?: boolean;
  /** Temporarily muted by user */
  muted?: boolean;
  /** Mute until this timestamp (temporal veto) */
  mutedUntil?: string | null;
  /** Rate of user dismissing this sender's messages (0-1, 90-day window) */
  dismissalRate?: number;
  /** When user last sent outbound TO this person (ISO timestamp) */
  lastOutboundTo?: string | null;

  /** Messages received from this person in last 7 days */
  recentInboundCount?: number;
  /** Rolling 90-day average of weekly inbound message count */
  avgWeeklyInbound?: number;

  /** Detected life stage (e.g., 'wedding', 'job_search', 'grieving', 'newborn') */
  currentLifeStage?: string | null;
  /** Confidence in life stage detection (0-1) */
  lifeStageConfidence?: number;
  /** Auto-expire after estimated duration */
  lifeStageExpiresAt?: string | null;

  // ---------------------------------------------------------------------------
  // Pre-fetched context bundle (D+S3 tournament winner, 2026-06-06)
  // classifyWithContext() populates these fields before calling classifyMessage()
  // so network-dependent signal scorers can skip rawRead calls.
  // When absent (undefined), scorers fall back to their normal rawRead paths.
  // ---------------------------------------------------------------------------

  /** Pre-fetched: number of trusted contacts that mentioned this sender recently (prestige-scorer) */
  _ctx_socialProof?: number;
  /** Pre-fetched: active goals from Memgraph for keyword matching (goal-relevance scorer) */
  _ctx_activeGoals?: Array<{ text: string; priority: string; keywords: string[] }>;
  /** Pre-fetched: whether the current user participated in this thread (broadcast suppression) */
  _ctx_userParticipated?: boolean;
  /** Pre-fetched: whether the user has sent outbound to this person recently (initiator override) */
  _ctx_isInitiator?: boolean;

  /** Active goal ID from HBO operating loop — used for goal-alignment boost in classifier */
  activeGoalId?: string;

  // ---------------------------------------------------------------------------
  // DM-017: Fields written to Person node but previously absent from interface
  // These are SET p.* by various triage-core modules and must be declared here
  // so TypeScript consumers can read them without casting to `any`.
  // ---------------------------------------------------------------------------

  /** Count of style override corrections applied by the user (style-learning.ts:95) */
  styleCorrections?: number;

  /** Most-used communication channel for this person (platform-awareness.ts:190) */
  primaryChannel?: string;

  /** Observability score [0-1] — how well we can observe this person's communication (platform-awareness.ts:337) */
  observabilityScore?: number;

  /** Aggregated emotion profile from emotion-aggregator.ts:95 */
  emotionProfile?: {
    dominantEmotion?: string;
    valence?: number;       // positive/negative [-1, 1]
    arousal?: number;       // calm/excited [0, 1]
    updatedAt?: string;     // ISO datetime of last aggregation
  };

  /** When this person's mental model was last read by the triage pipeline (model-assembler.ts:249) */
  lastTriageAccessAt?: string;

  /** When conversation facts were last extracted for this person (conversation-extractor.ts:505) */
  conversationFactsUpdatedAt?: string;

  /** Relationship-risk summary derived from the horsemen detector. */
  horsemenRisk?: 'low' | 'medium' | 'high' | null;

  /**
   * CRM enrichment (Mission 10a) — hydrated from the linked Lead (via IS_PERSON)
   * and its Contact's outreach history (via HAS_CONTACT) in model-assembler.ts.
   * Null when the person has no linked CRM Lead. Feeds the crm_* signal scorers.
   */
  _crmProfile?: {
    leadStatus?: string | null;        // Lead.status  (pipeline stage)
    lastContactedAt?: string | null;   // Lead.lastContactedAt  → outreach gap
    nextFollowUpAt?: string | null;    // Lead.nextFollowUpAt   → overdue follow-up
    source?: string | null;            // Lead.source
    stageChangedAt?: string | null;    // Lead.updatedAt (proxy for stage change time)
    leadCreatedAt?: string | null;     // Lead.createdAt
    bounced?: boolean;                 // any recent bounced outreach to the linked Contact
    dealValue?: number | null;         // linked Opportunity.value
  } | null;

  /** FAVEE trajectory time-series snapshots (favee-snapshots.ts:261) */
  faveeTrajectory?: {
    snapshots?: Array<{
      timestamp: string;
      formality: number;    // [0-1]
      activeness: number;   // [0-1]
      valence: number;      // [-1, 1]
      exchange: number;     // [0-1]
      equality: number;     // [0-1]
    }>;
  };
}

// ---------------------------------------------------------------------------
// Triage Item (Channel-Agnostic)
// ---------------------------------------------------------------------------

/** Channel-agnostic message representation for the classifier */
export interface TriageItem {
  /** Unique message ID */
  id: string;
  /** Platform-specific message ID (Gmail messageId, iMessage rowId, etc.) */
  rawId: string;
  /** Thread/conversation ID */
  threadId: string;
  /** Platform this message came from */
  platform: TriagePlatform;
  /** Sender handle (email address, phone number, iMessage ID) */
  senderHandle: string;
  /** Sender display name */
  senderName: string;
  /** Message subject (email) or first line (iMessage) */
  subject: string;
  /** Message body text */
  body: string;
  /** @deprecated Legacy alias for body — prefer body */
  text?: string;
  /** Short preview snippet */
  snippet: string;
  /** When received (ISO timestamp) */
  receivedAt: string;
  /** Direction */
  direction: MessageDirection;
  /** Is this a group conversation? */
  isGroup: boolean;
  /** How user received this message: direct, cc, or bcc (email-specific) */
  recipientType?: 'to' | 'cc' | 'bcc';
  /** Number of recipients (email-specific; >5 suggests broadcast) */
  recipientCount?: number;
  /** Display name of primary recipient (used for personalization checks) */
  recipientName?: string;
  /** Channel-specific message headers (email-specific: List-Unsubscribe, X-Mailer, etc.) */
  headers?: Record<string, string>;
  /**
   * Raw RFC 8601 `Authentication-Results` header stamped by the receiving
   * server (email-specific). Carries the SPF/DKIM/DMARC verdicts so
   * sender-legitimacy can score offline without any network call.
   */
  authResults?: string;

  /** Canonical backfill scope and immutable source provenance. */
  companyId?: string;
  providerAccountId?: string;
  providerObjectId?: string;
  sourceRecordId?: string;
  sourceCapturedAt?: string;
  sourceHash?: string;
  backfillRunId?: string;
  projectionEpoch?: number;

  /** Channel-specific labels (Gmail labels, Slack channel tags, etc.) */
  labels?: string[];

  // --- Classification results (filled by classifier) ---
  priority: TriagePriority;
  category: TriageCategory;
  suggestedAction: SuggestedAction;
  confidence: number;
  compositeScore: number;
  signals: TriageSignal[];

  // --- Mental model context (filled during pipeline) ---
  senderModel: PersonMentalModel | null;
  extraction: MessageExtraction | null;

  classifiedAt: string;
  classifierVersion: string;

  // --- Auto-draft (filled by draft-response pipeline for P0/P1) ---
  autoDraft?: string;
  autoDraftModel?: string;

  // --- Cross-channel context (filled by cross-channel-context pipeline for P0/P1) ---
  crossChannelContext?: CrossChannelContext;

  // --- Calendar context (filled by briefing pipeline for P0/P1) ---
  /** @planned not yet implemented — calendar integration */
  /** e.g. "📅 You have a meeting with [sender] at 3pm — reply before then" */
  calendarContext?: string;
  /** @planned not yet implemented — calendar integration */
  /** e.g. "Schedule meeting with [sender]" — set when body contains meeting-request patterns */
  suggestedCalendarAction?: string;
  /** @planned not yet implemented — calendar integration */
  situationContext?: string;

  // --- Secretary brief (filled by enrichBriefingWithSecretary / cacheInbox) ---
  /** Plain-text secretary-style brief for this item */
  secretaryBrief?: string;
  /** Structured action brief populated by cacheInbox */
  brief?: {
    action: string;
    action_type: string;
    timeframe: string | null;
    why: string;
    context: string | null;
    landmine: string | null;
  };

  // --- Open commitments count (denormalized from senderModel for API consumers) ---
  /** Count of open commitments involving this sender (from PersonMentalModel.openCommitments) */
  openCommitments?: number;
}

// ---------------------------------------------------------------------------
// Triage Batch & Stats
// ---------------------------------------------------------------------------

export interface TriageBatch {
  batchId: string;
  platform: TriagePlatform;
  startedAt: string;
  completedAt: string | null;
  totalMessages: number;
  classified: number;
  items: TriageItem[];
  stats: TriageStats;
}

export interface TriageStats {
  p0Count: number;
  p1Count: number;
  p2Count: number;
  p3Count: number;
  vipPending: number;
  avgResponseLag: number | null;
  estimatedTimeSaved: number;
  extractionCostTotal: number;
}

// ---------------------------------------------------------------------------
// Triage Briefing
// ---------------------------------------------------------------------------

export interface TriageBriefing {
  generatedAt: string;
  platform: TriagePlatform;
  sections: {
    actionRequired: TriageItem[];
    reviewToday: TriageItem[];
    fyi: TriageItem[];
    archived: number;
    spam: number;
  };
  stats: TriageStats;
  markdown: string;
  /** Per-person mental model context included in P0 entries */
  mentalModelContext: Record<string, string>;
}

// ---------------------------------------------------------------------------
// Triage Decision (for learning)
// ---------------------------------------------------------------------------

export interface TriageDecision {
  messageId: string;
  senderHandle: string;
  platform: TriagePlatform;
  suggestedPriority: TriagePriority;
  actualPriority: TriagePriority | null;
  suggestedAction: SuggestedAction;
  actualAction: string | null;
  agreedWithSuggestion: boolean | null;
  signals?: TriageSignal[];
  userOverride?: boolean;
  timestamp: string;
}

// ---------------------------------------------------------------------------
// Triage Config (29 signals)
// ---------------------------------------------------------------------------

export interface TriageConfig {
  signalWeights: Record<SignalSource, number>;
  thresholds: {
    p0: number;
    p1: number;
    p2: number;
  };
  vipEmails: string[];
  vipDomains: string[];
  vipPhones: string[];
  vipPageRankMin: number;
  vipBoost: number;
  batchSize: number;
  senderCacheTtlMs: number;
  extractionModel: string;
  summaryModel: string;
  /** Thresholds that control calibration and weekly-calibration behaviour. */
  calibrationThresholds: {
    /** Accept rate below this triggers a tighten recommendation. Default 0.30. */
    acceptRateLow: number;
    /** False negative rate above this triggers a loosen recommendation. Default 0.10. */
    falseNegativeRateHigh: number;
    /** Fast-dismiss ratio above this triggers a noise recommendation. Default 0.50. */
    fastDismissRatioHigh: number;
    /** Minimum dismiss count before fast-dismiss rule fires. Default 3. */
    fastDismissMinCount: number;
    /** Min accept rate for a type to avoid suppress_type recommendation. Default 0.15. */
    typeAcceptRateMin: number;
    /** Min accept rate for a type to trigger boost_type recommendation. Default 0.80. */
    typeAcceptRateHigh: number;
    /** Min samples for type-level recommendations to fire. Default 5. */
    typeSampleMin: number;
    /** Trend delta threshold (absolute pp): below this = stable. Default 0.05. */
    trendDeltaThreshold: number;
    /** Number of prior weeks to average for trend baseline. Default 4. */
    trendRollingWeeks: number;
    /** Min decisions before calibration tournament runs. Default 20. */
    minDecisionsForCalibration: number;
    /** Max calibration log entries to retain. Default 52. */
    maxCalibrationLogEntries: number;
  };
}

export const DEFAULT_TRIAGE_CONFIG: TriageConfig = {
  signalWeights: {
    // 34 signals: 29 scored signals (weights normalized to sum ≈ 1.0) + 5 synthetic post-processing signals (weight=0)
    // renormalized to sum = 1.000 (verified: sum of all 29 = 1.0 exactly after rounding).
    // Original proportions divided by 1.012 (the pre-normalization sum).
    // To recalibrate: edit proportions then renormalize via scripts/normalize-signal-weights.ts
    graph_rank:               0.056324,
    channel_context:          0.087945,
    urgency_content:          0.152174,
    relationship:             0.064229,
    contact_role:             0.048419,
    unknown_sender:           0.032609,
    open_questions:           0.056324,
    overdue_commitments:      0.040514,
    topic_continuity:         0.032609,
    comms_style:              0.023715,
    relationship_health:      0.023715,
    relationship_risk:        0.023715,
    trajectory_signal:        0.015810,
    emotional_tone:           0.023715,
    favee_type:               0.015810,
    network_cohesion:         0.015810,
    trajectory_momentum:      0.023715,
    knowledge_freshness:      0.015810,
    user_tags:                0.032609,
    relationship_persistence: 0.015810,
    cross_channel_escalation: 0.023715,
    response_debt:            0.023715,
    relationship_decay:       0.015810,
    sender_legitimacy:        0.023715,
    frequency_acceleration:   0.015810,
    referral_chain:           0.023715,
    sender_prestige:          0.023715,
    personal_importance:      0.032609,
    goal_relevance:           0.015810,
    // CRM signals (Mission 10a) — sourced from the linked Lead/Contact.
    //
    // RESERVED AT 0 (SIGNAL-RENORM-1, 2026-08-10). These previously carried "modest
    // NON-ZERO starting weights" (0.020/0.020/0.015/0.015/0.015/0.015 = 0.100) that
    // were never renormalized into the ≈1.0 sum above. Measured against the 1,436
    // scored InboxItems on the restored corpus, all six appear in ZERO items: no CRM
    // contact is linked, so no CRM scorer ever produces a signal.
    //
    // That combination was not inert — it was a DENOMINATOR LEAK. classifier.ts:324
    // normalizes by `Object.values(cfg.signalWeights)` (the sum of ALL configured
    // weights), while the numerator only ever contains signals that were actually
    // produced. learning.ts:getEffectiveWeights returns learned weights renormalized
    // to sum 1.0, and those learned weights only ever contain the 29 signals that DO
    // get produced — so the merge at classifier.ts:241 left these six at their
    // defaults and the denominator became 1.000 + 0.100 = 1.100 while the numerator
    // stayed at 1.000. Every compositeScore was deflated by exactly 1/1.100, and the
    // p0/p1/p2 thresholds below were hand-lowered to compensate.
    //
    // Weight 0 removes them from BOTH sides: scoringWeightsOnly() (learning.ts:400)
    // keeps only `v > 0`, and a 0 adds nothing to the classifier's denominator. The
    // union members, the scorers and the registry entries are all deliberately KEPT,
    // so wiring CRM contacts is a weight edit here — not a re-plumbing job.
    //
    // RE-ARMING IS A COUPLED CHANGE. Giving any of these a non-zero weight raises the
    // denominator again and deflates every composite; the thresholds below must be
    // re-derived in the SAME commit. See the receipt at
    // .wargaming/campaigns/email-backfill-unification/receipts/mission_SIGNAL-RENORM-1/
    // for the measured 0%-churn method, and the guard that fails on weighted-but-
    // never-produced signals so a re-arm without data cannot go unnoticed again.
    crm_lead_stage:           0,
    crm_outreach_gap:         0,
    crm_open_opportunity:     0,
    crm_bounce_flag:          0,
    crm_deal_value:           0,
    crm_stage_velocity:       0,
    // Post-processing boosts (weight=0 in scoring; injected as signals for audit trail)
    dunbar_boost:             0,
    opportunity_decay:        0,
    session_decay:            0,
    season_mismatch:          0,
    initiator_boost:          0,
    offline_floor:            0,
  },
  thresholds: {
    // Calibrated for realistic composite scores from first-time email senders.
    // Cold senders with strong urgency keywords score ~0.30-0.35; without keywords ~0.12-0.15.
    // Old thresholds (0.75/0.50/0.25) were too strict — everything came back P3.
    //
    // SCALED x1.100 (SIGNAL-RENORM-1, 2026-08-10) — the coupled half of zeroing the
    // phantom CRM weights above. Those weights sat only in the classifier's
    // normalization denominator, so removing them multiplies every compositeScore by
    // exactly 1.100; leaving these thresholds at 0.35/0.25/0.15 would silently
    // re-rank the inbox. THIS IS ONE CHANGE WITH THE BLOCK ABOVE — never edit either
    // half alone.
    //
    // Why exact preservation, and not a percentile refit: the scale is uniform, so
    // for any item with no additive boost, comparing (1.100 * composite) against
    // (1.100 * threshold) is algebraically IDENTICAL to the old comparison — the
    // multiplicative penalties downstream (season_mismatch, session_decay, the
    // category penalties) commute with it. Only additive boosts (VIP, Gmail-label,
    // dunbar, goal, health) fail to scale, and they can only move an item DOWN, by at
    // most 0.0909 * boost.
    //
    // MEASURED over the 1,436 scored InboxItems: 0/1436 = 0.000% priority churn, and
    // the P0/P1/P2/P3 histogram is unchanged. Negative control — weights zeroed but
    // these thresholds left alone — churns 276/1436 = 19.2%.
    p0: 0.385,  // was 0.35 (x1.100); originally 0.75
    p1: 0.275,  // was 0.25 (x1.100); originally 0.50
    p2: 0.165,  // was 0.15 (x1.100); originally 0.25
  },
  vipEmails: [],
  vipDomains: [],
  vipPhones: [],
  vipPageRankMin: 0.001,
  vipBoost: 0.20,
  batchSize: 50,
  senderCacheTtlMs: 24 * 60 * 60 * 1000,
  extractionModel: 'amazon-bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0',
  summaryModel: 'amazon-bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0',
  calibrationThresholds: {
    acceptRateLow: 0.30,
    falseNegativeRateHigh: 0.10,
    fastDismissRatioHigh: 0.50,
    fastDismissMinCount: 3,
    typeAcceptRateMin: 0.15,
    typeAcceptRateHigh: 0.80,
    typeSampleMin: 5,
    trendDeltaThreshold: 0.05,
    trendRollingWeeks: 4,
    minDecisionsForCalibration: 20,
    maxCalibrationLogEntries: 52,
  },
};

// ---------------------------------------------------------------------------
// Relationship Strength Params
// ---------------------------------------------------------------------------

export interface RelationshipStrengthParams {
  /** Exponential decay λ for frequency (default 0.03, half-life ~23 days) */
  frequencyDecayLambda: number;
  /** Max active topics to cap diversity score (default 5) */
  topicDiversityCap: number;
  /** Max open commitments to cap density score (default 3) */
  commitmentDensityCap: number;
  /** Exponential decay λ for recency (default 0.02, slower) */
  recencyDecayLambda: number;
  /** Factor weights — must sum to 1.0 */
  weights: {
    frequency: number;    // default 0.30
    topicDiversity: number; // default 0.15
    reciprocity: number;  // default 0.15
    commitmentDensity: number; // default 0.15
    recency: number;      // default 0.25
  };
  /** Cache TTL in ms (default 5 minutes) */
  cacheTtlMs: number;
}

export const DEFAULT_RELATIONSHIP_PARAMS: RelationshipStrengthParams = {
  frequencyDecayLambda: 0.03,
  topicDiversityCap: 5,
  commitmentDensityCap: 3,
  recencyDecayLambda: 0.02,
  weights: {
    frequency: 0.30,
    topicDiversity: 0.15,
    reciprocity: 0.15,
    commitmentDensity: 0.15,
    recency: 0.25,
  },
  cacheTtlMs: 5 * 60 * 1000,
};


// ---------------------------------------------------------------------------
// Infrastructure Ports (Dependency Inversion)
// Consumers inject implementations; core defines the contracts.
// ---------------------------------------------------------------------------

/**
 * Port for LLM calls. The shared module never calls Bedrock/OpenAI directly.
 * Consumers provide an adapter (e.g., BedrockAdapter, OpenAIAdapter).
 */
export interface LLMPort {
  /** Call an LLM with a system prompt and user message. Return structured JSON. */
  call(opts: {
    systemPrompt: string;
    userText: string;
    maxTokens?: number;
    tools?: unknown[];
    toolChoice?: unknown;
  }): Promise<{ content: unknown; inputTokens: number; outputTokens: number }>;
}

/**
 * Port for graph read operations.
 * Consumers provide a Memgraph/Neo4j/mock adapter.
 */
export interface GraphReadPort {
  read<T = Record<string, unknown>>(
    cypher: string,
    params?: Record<string, unknown>,
  ): Promise<T[]>;
}

/**
 * Port for graph write operations.
 */
export interface GraphWritePort {
  write(
    cypher: string,
    params?: Record<string, unknown>,
  ): Promise<void>;
}

/**
 * Combined graph port (most consumers need both).
 */
export interface GraphPort extends GraphReadPort, GraphWritePort {}

/**
 * Port for file-based persistence (learning decisions, weights).
 */
export interface StoragePort {
  read(key: string): string | null;
  write(key: string, data: string): void;
  exists(key: string): boolean;
}
﻿

// ============================================================================
// PersonEnrichmentStats — bookkeeping fields written to Person nodes by the
// enrichment pipeline. NOT part of PersonMentalModel (not used by any signal
// scorer). Typed here for consumers that need to read enrichment metadata.
// ============================================================================

export interface PersonEnrichmentStats {
  personId: string;
  /** Count of style override corrections applied by the user (style-learning.ts) */
  styleCorrections?: number;
  /** Computed observability score [0-1] — how well Helios can observe this person (platform-awareness.ts) */
  observabilityScore?: number;
  /** Raw emotion profile computed by emotion-aggregator.ts. NOT the same as
   *  qualityProfile.emotionalRatio on the KNOWS edge (that is the triage consumption form).
   *  This is the raw intermediate computation stored on the Person node. */
  emotionProfile?: {
    dominantEmotion?: string;
    valence?: number;       // -1 to +1
    arousal?: number;       // 0 to 1
    updatedAt?: string;     // ISO datetime
  };
  /** FAVEE dimension time-series snapshots (favee-snapshots.ts).
   *  NOT the same as directConnections[].favee (that is the per-relationship FAVEE score).
   *  This tracks how the person's overall FAVEE profile changes over time. */
  faveeTrajectory?: {
    snapshots?: Array<{
      timestamp: string;
      formality: number;
      activeness: number;
      valence: number;
      exchange: number;
      equality: number;
    }>;
  };
  /** When conversation facts (key facts) were last extracted for this person.
   *  NOT the same as keyFactsUpdatedAt (in PersonMentalModel) which tracks the
   *  broader knowledge freshness. This tracks the conversation-extractor specifically. */
  conversationFactsUpdatedAt?: string;
}

// ============================================================================
// PersonSchedulingMetadata — operational metadata for the enrichment daemon
// scheduler. NOT part of PersonMentalModel. Used by mental-model-daemon-v2.ts
// and named-entity-resolver.ts to tier people by refresh priority.
// ============================================================================

export interface PersonSchedulingMetadata {
  personId: string;
  /** When this person's model was last assembled by the triage pipeline.
   *  Written by model-assembler.ts on every assembleMentalModel() call.
   *  Read by mental-model-daemon-v2.ts to prioritize which people need re-scoring. */
  lastTriageAccessAt?: string;  // ISO datetime
}
