Libra CodeHub

CedarCopilot/cedar-mail

Branch: staging

fix(taxonomy): stop extracting the counterparty's own pitch as the deal

merged#2927CedarCopilot

CedarCopilot wants to merge 2 commits into staging from fix/taxonomy-deal-frame

Customer behavior verdict

Libra exercised 1 changed behavior; none regressed.

Who this touches

  • Shared email providers do not make every participant a seller: 384 of 163928 API requests in the last 7 days; 0 failed (0%).
  • Shared email providers do not make every participant a seller: 384 of 163928 API requests in the last 7 days; 0 failed (0%).
  • Taxonomy extraction uses the meeting roster to frame seller and buyer evidence: 384 of 163928 API requests in the last 7 days; 0 failed (0%).
  • cedar-configuration: 32 of 659 skill loads in the last 7 days.
  • Most common surface: families (72%).
  • Most common surface: harness (28%).
  • apply-config-change: no production calls in the last 7 days.
  • connection-write: 7 runs for 2 of 18 active accounts in the last 7 days; 0 failed (0%).
  • Capability questions do not create objections or objection responses: Held on this PR; the scenario fails when this is broken on purpose.
  • Taxonomy extraction evaluates the counterparty’s deal, not a reverse pitch: This scenario failed on both revisions. Libra has not established whether the cause is existing behavior or the scenario itself.

What Libra verified

  • Capability questions do not create objections or objection responses

    Libra ran this behavior against the change and confirmed the check detects when it breaks.

    Technical evidence
    • the objection definition explicitly excludes a product integration or support question when it contains no doubt, refusal, or deal condition
    • the objection-response definition does not extract a response to a capability or integration question without a real objection
    • the objection definition still permits capability questions to become objections when the speaker adds doubt, refusal, or a deal condition

    Changed code: discovery.ts.

Live on prod, watching, 1 day leftTimeline and evidence
  1. Opened
    Sep 23, 2026, 3:54 PM
  2. Sep 23, 2026, 4:27 PM
  3. Merged
    Sep 23, 2026, 4:41 PM
  4. Live on prod
    Sep 23, 2026, 4:41 PM
  5. Observed 0 hours
    Sep 23, 2026, 4:41 PM
  6. Watching

    Live on prod, watching, 1 day left

    Sep 23, 2026, 4:41 PM
  7. Pipelines steady after this deploy
    Sep 23, 2026, 4:41 PM

Behaviors Libra is checking

Competitor extraction must exclude tools the prospect currently uses, connects to, feeds, or would use alongside the seller's product unless the prospect explicitly presents the tool as an alternative.Not checked
prod
When multiple distinct products are genuinely competitors, extraction must preserve one separately spelled instance per named product rather than merging them.Not checked
prod
Taxonomy instance and judgment extraction must treat each event as the counterparty evaluating the seller's product and ignore seller-side reverse-pitch pricing, competitors, features, stakeholders, criteria, deliverables, and next steps.Not checked
prod
An integration or support question must not be recorded as product feedback unless the prospect independently states the capability is missing or wishes it existed.Not checked
prod
Versioned judgment extraction must receive the meeting participant roster, including names, emails, titles, and rep/buyer roles, in both preview and persisting runs.Not checked
prod
A question about whether the product integrates with or supports another system must remain a capability question and produce no objection unless the prospect voices doubt, refusal, or a deal condition.Not checked
prod

Libra has verdicts on 0 of 14 tracked behaviors on prod; 14 are still being checked. Libra checks hourly for 3 days after each deploy.

Summary

  • On a two-way call (the counterparty pitches its own product back), the extractor recorded the reverse pitch as this deal's pricing, competitors, stakeholders, decision process and next steps. It also told the model every non-owner speaker was buyer side, so a co-seller who ran the call was treated as the buyer.
  • New shared seller point-of-view block for both the instance and judgment prompts: the whole seller domain is seller side (personal-mail reps fall back to the roster's rep tags), every field is read as "the counterparty buying the seller's product", the reverse pitch and seller-side self-description are excluded, and the counterparty's prior experience with the seller stays in scope. The judgment prompt now also gets the participant roster (preview and persisting runs).
  • Precision fixes to 8 default field definitions from a per-field recall/precision audit of one real call: objection.raised/objection.response (capability questions are not objections; default-empty), feedback.moment, competitor.mentioned (the counterparty's existing stack is not a competitor), prospect.call_ran_over (default-empty), stakeholder.engaged (roles/dispositions must be evidenced), qual.gap, tactic.rapport_building. Content-pin hash re-pinned.
  • PERSONAL_EMAIL_DOMAINS extended (googlemail, regional yahoo/hotmail/live/outlook) since it now gates the seller-domain rule.

Rollout already done (not in this diff)

The runtime reads definitions from each org's stored AOP, so editing the defaults only affects newly seeded orgs. The same 8 descriptions plus the objection.response.responds_to metadata description were already patched in production on the six orgs that carry taxonomy fields (Pirros, AspireIQ, Warp, Vooma, Admyral, Cedar) with a guarded, hash-pinned jsonb_set (each write only fires if the stored text still equalled the old shipped default; nothing was customized). Full pre-change backups were saved locally; a verification diff against the backups showed only those description paths changed. Warp has no qual.gap field (older instance-only shape), so it received 8 patches. Only Cedar has taxonomy extraction enabled today, so the other five see no change until their flag is on or a backfill runs. Existing extracted rows are unchanged; re-extracting them is a paid backfill and a separate decision.

Test plan

  • pnpm run autofix:local chain: brand check, oxlint, deps:check, mail jest, mail tz tests, server vitest. Everything passes except apps/mail chatHistoryList.test.ts (date-sensitive, fails without any mail changes here) and a flaky LinkedColumns.test.tsx that passes in isolation.
  • Server: full vitest run src/ green after merging staging (14,470 tests); tsc -b clean.
  • New tests: participant-roster-pov.test.ts (domain rule, DEAL FRAME text, personal-mail fallback, roster tagging on a shared provider) and taxonomy-extraction-run.test.ts (judgment prompt includes MEETING PARTICIPANTS on both the dry-run preview and the persisting run).
  • Battle-tested against the real prompt on one real two-way call with a GLM dry run (nothing persisted, ~13 runs, plus independent recall/precision audits). A 21-check scorecard went from 5 (baseline) to 20-21 on the final prompts; the remaining misses were single-run flakes that moved between runs.
  • Review: three fresh thermo-review rounds; every finding was fixed. Round 3 still had four non-blocking 🟡 items, which were also addressed, but no fourth review was run (loop cap).

Verify in prod

This changes prompt text, not runtime branching, so there is no new log line. Use the existing structured log and the data itself:

  • Axiom (cedar-prod):
    ['cedar-prod'] | where _time > ago(1h) and message contains "[taxonomy-stream] complete"
    | project _time, conversationId, eventsProcessed, instanceRowsWritten, judgmentChangesWritten
    
    CloudWatch: /aws/ecs/aws-prod-api/worker-service, same message.
  • Data check on a two-way call after
Show production surfaces and changed-file mapping

Production surfaces

Libra has not measured any production surfaces for this change yet.

Changed files → surfaces

  • .gitignoreno production surface mapped
  • apps/server/src/services/crm/__tests__/taxonomy-extraction-run.test.tsno production surface mapped
  • apps/server/src/services/crm/email-domains.tsno production surface mapped
  • apps/server/src/services/crm/taxonomy-extraction-step.tsno production surface mapped
  • apps/server/src/services/crm/taxonomy-judgments.tsno production surface mapped
  • apps/server/src/services/field-values/__tests__/defaults-equivalence.test.tsno production surface mapped
  • apps/server/src/services/field-values/__tests__/participant-roster-pov.test.tsno production surface mapped
  • apps/server/src/services/field-values/crm-custom-field-values.tsno production surface mapped
  • apps/server/src/services/field-values/defaults/discovery.tsno production surface mapped
  • apps/server/src/services/field-values/defaults/product.tsno production surface mapped
  • apps/server/src/services/field-values/defaults/qualification.tsno production surface mapped
  • apps/server/src/services/field-values/defaults/stakeholder.tsno production surface mapped
  • apps/server/src/services/field-values/defaults/tactics.tsno production surface mapped
  • apps/server/src/services/field-values/defaults/voice.tsno production surface mapped
  • apps/server/src/services/field-values/participant-roster.tsno production surface mapped