CedarCopilot wants to merge 2 commits into staging from fix/coerce-select-list-option-casing
Libra ran this behavior against the change and confirmed the check detects when it breaks.
Changed code: conversation-field-definitions.ts.
Libra ran this behavior against the change and confirmed the check detects when it breaks.
Changed code: conversation-field-definitions.ts.
Live on prod, 0 of 10 surfaces working, 2 days left
Libra found 10 production surfaces on prod but could not judge any of them yet. 6 surfaces had under 20 requests, so Libra has not judged them; 4 surfaces had no requests at all. Libra checks hourly for 3 days after each deploy.
Nothing enforced a select/list custom field's declared options at write time, so a real picklist could still accumulate multiple cases/formats of the same value. Confirmed on Warp's current_provider field , a genuine 30-option picklist synced from Salesforce (Current_Provider__c) , which has raw values like gusto/Gusto and rippling/Rippling/rippling_peo all present for the same option. A broader scoping query found the same casing/punctuation-variant pattern on 74 fields across 17 of 43 Cedar orgs (ā„5% of distinct values collapsing to the same thing after normalization, on fields with real volume and short/categorical values).
coerceToCanonicalOptionValue/coerceSelectOrListValue (conversation-field-definitions.ts): when an incoming value normalizes (case + underscore/whitespace-insensitive) to exactly one declared option, persist that option's real spelling instead. Zero or multiple normalized matches are left unchanged , this corrects how a value is spelled, never what it means.normalizeOptionValueForMatch is used for this, not the existing normalizeStageKeyForMatch , that function's own doc comment warns it collapses meaningful punctuation ("Series B" vs "Series B+" would alias). The new normalizer only folds case and the underscore/whitespace separator convention this codebase actually mixes, leaving other punctuation untouched.updateConversationFieldsAndWorkingMemory, the single persist funnel for custom fields (agent tool, approval-task apply, updateConversation route, CRM mirror) , covered uniformly, not just for the agent path. Field-type classification (json vs. select/list) is resolved per-key with the user AOP's definition overriding a same-key org-level one, fixing a bug caught in review where a field typed differently at each level could keep a stale classification and have valid writes silently dropped.Scoped conservatively on purpose: this never rejects an unrecognized value, never guesses between ambiguous matches, and doesn't touch existing rows already in the database , only new writes going forward.
pnpm --filter @zero/server run types , cleanconversation-field-definitions.test.ts, 14 cases) covering: case/underscore coercion, exact-match no-op, no-match no-op, free-hand-text no-op, ambiguous-options no-op, the "Series B" vs "Series B+" non-aliasing case, list-type JSON-array per-item coercion, malformed-JSON and non-string-array safety, and no-options (free text) no-opconversations.json-persist.test.ts, conversations.field-approval-reconcile.test.ts) , all pass, no behavior change for non-select/list fields/code-review high , both findings (punctuation-aliasing risk, stale field-type classification) fixed and re-verifiedš¤ Generated with Claude Code
The PR is not yet safe to merge because list/select shape disambiguation remains incomplete and the new test code violates an explicit repository requirement.
| Surface | Requests | Errors | p95 | Users | Verdict |
|---|---|---|---|---|---|
| /api/trpc/crm.searchConversationsMinimal | 0 ā 3 | 0 ā 0 (0%) | not measured ā 135 ms | 0 | Insufficient traffic 3 requests, under the 20 Libra needs |
| /api/trpc/crm.listConversations | 0 ā 3 | 0 ā 0 (0%) | not measured ā 1016 ms | 0 | Insufficient traffic 3 requests, under the 20 Libra needs |
| POST /api/trpc/crm.searchConversationsMinimal | 0 ā 2 | 0 ā 0 (0%) | not measured ā 135 ms | 0 | Insufficient traffic 2 requests, under the 20 Libra needs |
| POST /api/trpc/crm.listConversations | 0 ā 2 | 0 ā 0 (0%) | not measured ā 1016 ms | 0 | Insufficient traffic 2 requests, under the 20 Libra needs |
| trpc.crm.searchConversationsMinimal | 0 ā 1 | 0 ā 0 (0%) | not measured ā 123 ms | 1 | Insufficient traffic 1 request, under the 20 Libra needs |
| crm.searchConversationsMinimal | 0 ā 1 | 0 ā 0 (0%) | not measured ā 122 ms | 1 | Insufficient traffic 1 request, under the 20 Libra needs |
| trpc.crm.loadCalendarConversationsBatch | 0 ā 0 | 0 ā 0 (0%) | not measured | 0 | No traffic No requests recorded since this deploy. |
| /api/trpc/crm.loadCalendarConversationsBatch | 0 ā 0 | 0 ā 0 (0%) | not measured | 0 | No traffic No requests recorded since this deploy. |
| POST /api/trpc/crm.loadCalendarConversationsBatch | 0 ā 0 | 0 ā 0 (0%) | not measured | 0 | No traffic No requests recorded since this deploy. |
| /api/trpc/crm.getConversations | 0 ā 0 | 0 ā 0 (0%) | not measured | 0 | No traffic No requests recorded since this deploy. |