CedarCopilot wants to merge 2 commits into staging from fix/hubspot-picklist-value-normalization-20260911-101702
Not deployed
Libra has no production signal for this change yet because it has not deployed. Libra checks hourly for 3 days after each deploy.
crm-field-sync.ts, resolveCrmFieldValue's label-to-value case) and the outbound push (hubspot.ts, updateOpportunityFields) normalize a picklist value (lowercase + underscores) purely to MATCH it against the field's allowedValues , then returned that normalized string as the value to store/send, instead of resolving back to the real, correctly-cased allowedValues entry.sourceFromCrm , those keep the source CRM's real casing (e.g. HubSpot's deal_source options are Inbound, Cold Email, not inbound/cold_email), so the write lands as an unrecognized lowercase string matching no defined option.deal_source field on a HubSpot-synced account had 9 of its 10 populated values silently mismatched this way. Confirmed via the live HubSpot API that HubSpot's own picklist values (and, separately, all 301 of that account's live deal records) are correctly cased , the corruption originates entirely in Cedar's sync code, not the source data.resolveAllowedValueByNormalizedMatch) since the single-select and multi-select branches in hubspot.ts had duplicated the same normalize/match code with the same bug in both places.apps/server/src/services/crm/crm-field-sync.ts , resolveCrmFieldValue's label-to-value case (both the single-value and semicolon-joined multi-select branches) now returns the matched entry from allowedValues, not the normalized comparison key. Exported for testing.apps/server/src/services/integrations/crm/hubspot.ts , updateOpportunityFields's picklist-reversal logic extracted into resolveAllowedValueByNormalizedMatch, fixing the same bug on the outbound (Cedar → HubSpot) path and removing ~15 lines of duplicated normalize logic.crm-field-sync-* and hubspot-label-to-value suites re-run to confirm no regression.vitest run on both new test files + the 3 existing suites touching this code , 26/26 passingtsc -b on @zero/server , cleanallowedValues-constrained mapped field (not just the reported one) and confirm no new lowercase/underscored values land in crm_conversation_field_values🤖 Generated with Claude Code
https://claude.ai/code/session_01DWbTovQuTH4TDdhFZ9JmcD
This PR fixes HubSpot picklist synchronization so normalized strings are used only for comparison while the canonical, correctly cased allowed value is stored or sent.
label-to-value resolution.The PR should not merge until inbound resolution preserves exact allowed-value matches when multiple options share a normalized key.
The inbound Map silently applies last-write-wins semantics to normalized collisions, so a valid exact CRM option can still be replaced with a different allowed value despite this PR’s goal of preserving canonical picklist identity.
Files Needing Attention: apps/server/src/services/crm/crm-field-sync.ts
| Filename | Overview |
|---|---|
| apps/server/src/services/crm/crm-field-sync.ts | Returns canonical allowed values during inbound synchronization, but normal |
Libra has not measured any production surfaces for this change yet.