{
  "description": "Deterministic contract checks for the PingCAP-HubSpot-Companion field mappings. Pins the signed-off acceptance decisions (Appendix B / B1-B11 of docs/hubspot-sync-business-spec.md) against the committed config so drift is caught as a hard, environment-free PR gate. Update this manifest deliberately when a mapping decision changes.",
  "configDir": "packs/PingCAP-HubSpot-Companion/field_mappings",
  "files": {
    "contact_lead_inbound.json":  { "sourceObject": "contacts", "targetObject": "Lead",       "actionType": "hubspotContactLeadInbound", "direction": "inbound",  "mappingCount": 53 },
    "contact_lead_outbound.json": { "sourceObject": "Lead",     "targetObject": "contacts",   "actionType": "PushToConnector", "direction": "outbound", "mappingCount": 42 },
    "contact_inbound.json":       { "sourceObject": "contacts", "targetObject": "Contact",    "direction": "inbound",  "mappingCount": 13 },
    "contact_contact_outbound.json": { "sourceObject": "Contact", "targetObject": "contacts", "direction": "outbound", "mappingCount": 12 },
    "company_account_outbound.json": { "sourceObject": "Account", "targetObject": "companies", "direction": "outbound", "mappingCount": 10 },
    "deal_opportunity_outbound.json": { "sourceObject": "Opportunity", "targetObject": "deals", "direction": "outbound", "mappingCount": 13 }
  },
  "assertions": [
    { "id": "legacy-id-inb-cl-salesforceleadid-projection", "decision": "#4144", "kind": "mappingExists", "file": "contact_lead_inbound.json", "sourceField": "salesforceleadid", "targetField": "", "projectionOnly": true, "transformAbsent": true, "note": "Explicit legacy Lead identity must be projected into inbound payloads without becoming an executable CRM field write." },
    { "id": "legacy-id-inb-cl-salesforcecontactid-projection", "decision": "#4144", "kind": "mappingExists", "file": "contact_lead_inbound.json", "sourceField": "salesforcecontactid", "targetField": "", "projectionOnly": true, "transformAbsent": true, "note": "Explicit legacy Contact identity must be projected into inbound payloads without becoming an executable CRM field write." },
    { "id": "rt-nx-05-inb-cl-firstname-untruncated", "decision": "rt-nx-05", "kind": "mappingExists", "file": "contact_lead_inbound.json", "sourceField": "firstname", "targetField": "FirstName", "transformAbsent": true, "note": "HubSpot firstname tokens must not be truncated before CRM convergence." },
    { "id": "bi-loop-inb-cl-delta-firstname-sparse", "decision": "bi-loop", "kind": "mappingExists", "file": "contact_lead_inbound.json", "actionType": "hubspotContactLeadInboundDeltaFirstname", "sourceField": "firstname", "targetField": "FirstName", "transformAbsent": true, "note": "A firstname propertychange must update only Lead.FirstName so the local Title survives the anti-loop convergence leg." },
    { "id": "bi-loop-inb-cl-delta-jobtitle-sparse", "decision": "bi-loop", "kind": "mappingExists", "file": "contact_lead_inbound.json", "actionType": "hubspotContactLeadInboundDeltaJobtitle", "sourceField": "jobtitle", "targetField": "Title", "conflictPolicy": "overwrite", "transformIncludes": ["LEFT(", "$Record.jobtitle", "255"], "note": "A jobtitle propertychange must be available as a one-field Lead.Title delta action." },
    { "id": "bi-blast-inb-cl-delta-lastname-sparse", "decision": "bi-blast", "kind": "mappingExists", "file": "contact_lead_inbound.json", "actionType": "hubspotContactLeadInboundDeltaLastname", "sourceField": "lastname", "targetField": "LastName", "conflictPolicy": "overwrite", "transformIncludes": ["LEFT(", "$Record.lastname", "80"], "note": "A lastname propertychange must be available as a one-field Lead.LastName delta action without reapplying FirstName." },
    { "id": "bi-blast-out-lc-delta-firstname-sparse", "decision": "bi-blast", "kind": "mappingExists", "file": "contact_lead_outbound.json", "actionType": "PushToConnectorDeltaFirstName", "sourceField": "FirstName", "targetField": "firstname", "note": "A Nexus Lead FirstName edit must not PATCH stale LastName back to HubSpot." },
    { "id": "bi-blast-out-lc-delta-lastname-sparse", "decision": "bi-blast", "kind": "mappingExists", "file": "contact_lead_outbound.json", "actionType": "PushToConnectorDeltaLastName", "sourceField": "LastName", "targetField": "lastname", "note": "A Nexus Lead LastName edit must use a one-field PATCH while default linked updates avoid replaying stale lastname." },
    { "id": "bi-blast-out-lc-nonidentity-no-firstname", "decision": "bi-blast", "kind": "mappingAbsent", "file": "contact_lead_outbound.json", "actionType": "PushToConnectorNonIdentityUpdate", "sourceField": "FirstName", "targetField": "firstname", "note": "Linked Lead default PATCH must not carry identity fields during bi-directional races." },
    { "id": "bi-blast-out-lc-nonidentity-no-lastname", "decision": "bi-blast", "kind": "mappingAbsent", "file": "contact_lead_outbound.json", "actionType": "PushToConnectorNonIdentityUpdate", "sourceField": "LastName", "targetField": "lastname", "note": "Linked Lead default PATCH must not reapply stale LastName after HubSpot propertychange." },
    { "id": "bi-blast-out-lc-nonidentity-no-title", "decision": "bi-blast", "kind": "mappingAbsent", "file": "contact_lead_outbound.json", "actionType": "PushToConnectorNonIdentityUpdate", "sourceField": "Title", "targetField": "jobtitle", "note": "Title keeps its dedicated sparse outbound path instead of default full PATCH." },
    { "id": "k-conflict-out-lc-nonidentity-no-annualrevenue", "decision": "k-conflict-02", "kind": "mappingAbsent", "file": "contact_lead_outbound.json", "actionType": "PushToConnectorNonIdentityUpdate", "sourceField": "AnnualRevenue", "targetField": "annual_revenue__sfdc_", "note": "Inbound write-once AnnualRevenue must not be replayed to HubSpot from a same-origin non-identity echo." },
    { "id": "bi-loop-out-lc-delta-title-sparse", "decision": "bi-loop", "kind": "mappingExists", "file": "contact_lead_outbound.json", "actionType": "PushToConnectorDeltaTitle", "sourceField": "Title", "targetField": "jobtitle", "note": "A Nexus Lead Title edit must not PATCH unrelated contact fields back to HubSpot." },
    { "id": "out-contact-cc-title-jobtitle", "decision": "out-contact", "kind": "mappingExists", "file": "contact_contact_outbound.json", "sourceField": "Title", "targetField": "jobtitle", "note": "Contact Title must be included in the HubSpot contact PATCH payload as jobtitle." },

    { "id": "B2-inb-cl-country-source", "decision": "B2", "kind": "mappingExists", "file": "contact_lead_inbound.json", "sourceField": "country_picklist__sfdc_", "targetField": "Country", "onUnmappedValue": "passthrough", "valueMapKeyCount": 252 },
    { "id": "B2-inb-cc-country-source", "decision": "B2", "kind": "mappingExists", "file": "contact_inbound.json", "sourceField": "country_picklist__sfdc_", "targetField": "MailingCountry" },
    { "id": "B2-out-lc-country-target", "decision": "B2", "kind": "mappingExists", "file": "contact_lead_outbound.json", "sourceField": "Country", "targetField": "country_picklist__sfdc_" },

    { "id": "B1-inb-cl-industry-case", "decision": "B1", "kind": "mappingExists", "file": "contact_lead_inbound.json", "sourceField": "salesforce_industry", "targetField": "Industry", "transformIncludes": ["CASE(", "FSI-FinTech", "ISV/SaaS-Software"] },
    { "id": "B1-out-ac-industry-case", "decision": "B1", "kind": "mappingExists", "file": "company_account_outbound.json", "sourceField": "Industry", "targetField": "salesforce_industry__icp_", "transformIncludes": ["CASE(", "DNB-Software", "Software"] },

    { "id": "B3-inb-cl-region-apac-in", "decision": "B3", "kind": "mappingExists", "file": "contact_lead_inbound.json", "sourceField": "region__c", "targetField": "Region__c", "transformIncludes": ["CASE(", "APAC-IN", "APAC"] },
    { "id": "prod-disqualified-inb-cl-date", "decision": "#3529/#3883 RCA", "kind": "mappingExists", "file": "contact_lead_inbound.json", "sourceField": "hs_v2_date_entered_68053680", "targetField": "Disqualified_Date__c", "conflictPolicy": "skip_if_set", "transformIncludes": ["IF(ISBLANK($Record.hs_v2_date_entered_68053680)", "DATETIMEVALUE(", "DATETIMEFROMEPOCHMILLIS(", "hs_v2_date_entered_68053680"], "note": "Normal HubSpot full records carry an ISO datetime string while the #3883 coalesced lifecycle reducer materializes the winning occurredAt as epoch milliseconds; the lazy transform accepts both representations, preserves blank/null as null, and fails closed on malformed nonblank values without changing the reducer." },
    { "id": "set-campaign-inb-cl-lead-stage-date", "decision": "#5667 deep-equivalence RCA", "kind": "mappingExists", "file": "contact_lead_inbound.json", "actionType": "hubspotContactLeadInbound", "sourceField": "hs_v2_date_entered_lead", "targetField": "Lead_Stage_DateV2__c", "conflictPolicy": "skip_if_set", "transformIncludes": ["IF(ISBLANK($Record.hs_v2_date_entered_lead)", "DATETIMEVALUE(", "DATETIMEFROMEPOCHMILLIS(", "hs_v2_date_entered_lead"], "note": "Hydrated HubSpot records carry ISO timestamps while lifecycle property-change events carry epoch milliseconds; both representations must converge to the same Lead timestamp." },
    { "id": "set-campaign-inb-cl-delta-lead-stage-date", "decision": "#5667 deep-equivalence RCA", "kind": "mappingExists", "file": "contact_lead_inbound.json", "actionType": "hubspotContactLeadInboundDeltaHsV2DateEnteredLead", "sourceField": "hs_v2_date_entered_lead", "targetField": "Lead_Stage_DateV2__c", "conflictPolicy": "skip_if_set", "transformIncludes": ["IF(ISBLANK($Record.hs_v2_date_entered_lead)", "DATETIMEVALUE(", "DATETIMEFROMEPOCHMILLIS(", "hs_v2_date_entered_lead"], "note": "The sparse lifecycle delta must accept the epoch-millisecond representation emitted by HubSpot property-change webhooks." },

    { "id": "prod-inb-cl-subscriber-date", "decision": "#5434 reconciliation", "kind": "mappingExists", "file": "contact_lead_inbound.json", "actionType": "hubspotContactLeadInbound", "sourceField": "hs_v2_date_entered_subscriber", "targetField": "Suspect_Stage_DateV2__c", "conflictPolicy": "skip_if_set", "transformIncludes": ["IF(ISBLANK($Record.hs_v2_date_entered_subscriber)", "DATETIMEVALUE(", "DATETIMEFROMEPOCHMILLIS(", "hs_v2_date_entered_subscriber"], "note": "Subscriber dates must preserve ISO hydration and accept epoch-millisecond webhook values without overwriting an existing Suspect stage date." },
    { "id": "prod-inb-cl-delta-subscriber-date", "decision": "#5434 reconciliation", "kind": "mappingExists", "file": "contact_lead_inbound.json", "actionType": "hubspotContactLeadInboundDeltaHsV2DateEnteredSubscriber", "sourceField": "hs_v2_date_entered_subscriber", "targetField": "Suspect_Stage_DateV2__c", "conflictPolicy": "skip_if_set", "transformIncludes": ["IF(ISBLANK($Record.hs_v2_date_entered_subscriber)", "DATETIMEVALUE(", "DATETIMEFROMEPOCHMILLIS(", "hs_v2_date_entered_subscriber"], "note": "The sparse subscriber-date delta must use the same mixed-representation and write-once contract as full hydration." },
    { "id": "prod-inb-cl-status-nurture", "decision": "#5434 reconciliation", "kind": "mappingExists", "file": "contact_lead_inbound.json", "actionType": "hubspotContactLeadInbound", "sourceField": "hs_lead_status", "targetField": "Status", "conflictPolicy": "overwrite", "onUnmappedValue": "skip", "valueMapContains": { "BAD_TIMING": "Nurture", "Nurture": "Nurture" }, "note": "Accept the valid Nurture literal emitted by outbound Lead Status sync while retaining BAD_TIMING compatibility and skipping unknown statuses." },
    { "id": "prod-inb-cl-delta-status-nurture", "decision": "#5434 reconciliation", "kind": "mappingExists", "file": "contact_lead_inbound.json", "actionType": "hubspotContactLeadInboundDeltaHsLeadStatus", "sourceField": "hs_lead_status", "targetField": "Status", "conflictPolicy": "overwrite", "onUnmappedValue": "skip", "valueMapContains": { "BAD_TIMING": "Nurture", "Nurture": "Nurture" }, "note": "The sparse Status delta must accept the same Nurture aliases as full hydration." },

    { "id": "B9-inb-cl-annualrevenue-writeonce", "decision": "B9", "kind": "mappingExists", "file": "contact_lead_inbound.json", "targetField": "AnnualRevenue", "conflictPolicy": "skip_if_set" },
    { "id": "B11-out-od-amount-present", "decision": "B11", "kind": "mappingExists", "file": "deal_opportunity_outbound.json", "sourceField": "Amount", "targetField": "amount", "note": "Contract catch-up for the current main mapping shape." },
    { "id": "B11-out-od-net-new-arr", "decision": "#2864", "kind": "mappingExists", "file": "deal_opportunity_outbound.json", "sourceField": "ARR_Amount_USD__c", "targetField": "net_new_arr_amount", "note": "ARR_Amount_USD__c is intentionally synced outbound by #2864 and asserted by out-opp." },

    { "id": "B5-out-od-pipeline-domain", "decision": "B5", "kind": "mappingExists", "file": "deal_opportunity_outbound.json", "sourceField": "Type", "targetField": "pipeline", "valueMapKeyCount": 3 },
    { "id": "B5-out-od-dealstage-composite", "decision": "B5", "kind": "mappingExists", "file": "deal_opportunity_outbound.json", "sourceField": "StageName", "targetField": "dealstage", "valueMapKeyCount": 24 },
    { "id": "out-od-closedate-transform", "decision": "J6.2", "kind": "mappingExists", "file": "deal_opportunity_outbound.json", "sourceField": "CloseDate", "targetField": "closedate", "transformIncludes": ["LEFT(", "10"] },

    { "id": "B6-out-od-product-family", "decision": "B6", "kind": "mappingExists", "file": "deal_opportunity_outbound.json", "sourceField": "Product_Family__c", "targetField": "product_family__c", "valueMapContains": { "TiDB Cloud": "TCSA" } }
  ]
}
