Task — engineering-spec@1

"CRM win/loss analysis CUO draft — auto-generate analysis at deal close + memory memory persistence for institutional learning"

draftTASK-CRM-007
module crm · class product · priority p1 · created 2026-05-17 · shipped null
depends on TASK-CRM-001, TASK-CUO-101 · blocks none

§1 — Description (BCP-14 normative)

The CRM service MUST ship win/loss analysis at services/crm/src/win_loss/ triggered on deal close, drafted via TASK-AI-003, queued for CDO review, persisted to memory on approval, 4 memory audit kinds.

  1. MUST hook into deal stage transition (services/crm/src/deals.rs): on transition to won or lost_*, enqueue draft generation per DEC-1670.
  1. MUST validate wl_outcome against closed enum per DEC-1671.
  1. MUST draft via draft_generator.rs::draft(deal, activities) calling TASK-AI-003 with structured prompt — output: {outcome, turning_points[], customer_feedback, competitor_mentions[], lessons[], future_actions[]}.
  1. MUST queue draft for CDO review per DEC-1674 — NEVER auto-persist to memory.
  1. MUST on approval, call memory_writer.rs::write(draft) to persist memory memory per DEC-1673 — kind=lessons, searchable via TASK-MEMORY-108 query.
  1. MUST define table at migration 0007: ``sql CREATE TABLE crm_win_loss_drafts ( draft_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, deal_id UUID NOT NULL UNIQUE, outcome TEXT NOT NULL CHECK (outcome IN ('won','lost_no_decision','lost_competitor','lost_budget','lost_timing','lost_other')), analysis_body JSONB NOT NULL, status TEXT NOT NULL DEFAULT 'pending_review' CHECK (status IN ('pending_review','approved','dismissed','persisted')), reviewed_by UUID, reviewed_at TIMESTAMPTZ, memory_memory_path TEXT, trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE crm_win_loss_drafts ENABLE ROW LEVEL SECURITY; CREATE POLICY wl_drafts_rls ON crm_win_loss_drafts USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON crm_win_loss_drafts FROM cyberos_app; GRANT UPDATE (status, reviewed_by, reviewed_at, memory_memory_path) ON crm_win_loss_drafts TO cyberos_app; ``
  1. MUST emit 4 memory audit kinds per DEC-1675. PII per TASK-MEMORY-111: analysis_body text hashed.
  1. MUST thread trace_id from deal close → draft → CDO review → memory write → audit.
  1. MUST NOT auto-persist memory memory per DEC-1674.
  1. MUST NOT skip outcome categorization per DEC-1671 — required field on draft.

§2 — Why this design

Why on close (DEC-1670)? Knowledge is freshest immediately — capture before memory fades.

Why manual review (DEC-1674)? AI may misattribute reasons; CDO confirms accuracy before institutional memory.

Why memory persistence (DEC-1673)? Future similar deals query lessons via TASK-MEMORY-108; without persist, lessons stay siloed in deal record.

Why 6-outcome enum (DEC-1671)? Industry-standard win/loss taxonomy; enables aggregate reporting ("we lose 40% on budget").


§3 — API contract

GET    /v1/crm/win-loss/drafts             (list pending review)
POST   /v1/crm/win-loss/drafts/{id}/approve  (writes to memory)
POST   /v1/crm/win-loss/drafts/{id}/dismiss

Sample draft:

{
  "draft_id": "uuid",
  "deal_id": "uuid",
  "outcome": "lost_competitor",
  "analysis_body": {
    "outcome": "lost_competitor",
    "turning_points": [
      {"date": "2026-04-10", "event": "Competitor demo went well"},
      {"date": "2026-04-20", "event": "Customer asked for our price match"}
    ],
    "customer_feedback": "Liked our product but went with cheaper option",
    "competitor_mentions": ["CompetitorX"],
    "lessons": [
      "Price-sensitive segment requires upfront pricing discussion",
      "Demo-to-close gap of 10d gives competitors window"
    ],
    "future_actions": ["Build price-match playbook", "Tighten demo-to-proposal cycle"]
  }
}

§4 — Acceptance criteria

  1. Auto-drafted on close. 2. 6-outcome enum + cardinality test. 3. Queued for review, never auto-persist. 4. Approve writes to memory memory. 5. Dismiss → status=dismissed. 6. 4 memory audit kinds emitted. 7. PII scrubbed (analysis_body SHA256). 8. RLS denies cross-tenant. 9. CDO/CRO role only. 10. Trace_id preserved. 11. Idempotent (UNIQUE on deal_id). 12. Append-only via REVOKE except review/path cols. 13. AI failure → status=failed + sev-2 + retry. 14. memory_memory_path populated post-approve. 15. GET endpoint lists pending drafts. 16. Outcome required. 17. Lessons text non-empty. 18. Customer feedback may be empty if no signals. 19. Re-open deal → no auto-redraft (manual trigger). 20. memory memory tagged kind=lessons searchable via TASK-MEMORY-108.

§5 — Verification

#[tokio::test]
async fn auto_drafts_on_close() {
    let ctx = TestContext::with_deal_in_proposal().await;
    ctx.change_deal_stage(ctx.deal_id, "lost_competitor").await;
    tokio::time::sleep(Duration::from_secs(1)).await;
    let drafts = ctx.fetch_drafts_for_deal(ctx.deal_id).await;
    assert_eq!(drafts.len(), 1);
    assert_eq!(drafts[0].outcome, "lost_competitor");
}

#[tokio::test]
async fn never_auto_persists() {
    let ctx = TestContext::with_closed_deal().await;
    tokio::time::sleep(Duration::from_secs(2)).await;
    let memory_memories = ctx.memory_memory_count_for_deal(ctx.deal_id).await;
    assert_eq!(memory_memories, 0);
    let drafts = ctx.fetch_drafts_for_deal(ctx.deal_id).await;
    assert_eq!(drafts[0].status, "pending_review");
}

#[tokio::test]
async fn approve_writes_memory() {
    let ctx = TestContext::with_pending_draft().await;
    ctx.approve_draft(ctx.draft_id).await;
    let row = ctx.fetch_draft(ctx.draft_id).await;
    assert_eq!(row.status, "persisted");
    assert!(row.memory_memory_path.is_some());
    let mem = ctx.memory_fetch(row.memory_memory_path.unwrap()).await;
    assert!(mem.body.contains("lessons"));
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-CRM-001, TASK-CUO-101. Cross-module: TASK-CRM-002 (activity context), TASK-AI-003 (draft), TASK-MEMORY-111 (PII + memory write), TASK-AUTH-101 (CDO role), TASK-MEMORY-108 (search).

§10 — Failure modes

FailureDetectionOutcomeRecovery
AI timeoutretry 1xsev-2 fallback to skeletonmanual fill
Duplicate draft raceUNIQUE on deal_idskip secondinherent
Outcome enum mismatchvalidaterejectbug fix
memory write fail on approvesev-1; status=approved-pending-retryretryinherent
User dismisses then re-opens dealnew draft only on manual triggerinherentinherent
Activity feed emptyproceed with deal-only contextinherentinherent
AI hallucinates competitorCDO catches in reviewinherentinherent
Lost stage but no specific reasonuse 'lost_other' defaultinherentinherent
Cross-tenant approveRLS403inherent
Concurrent approveUPDATE WHERE pendingfirst winsinherent

§11 — Implementation notes


End of TASK-CRM-007 spec.