Langchain ECM contract-history backfill

PR #6397 adds a Dealops 2 import pipeline that reconstructs Langchain’s Salesforce-loaded historical contracts into native frozen Dealops quote graphs, with an engine-replay gate before anything is persisted.

Author: @mileszim PR: dealops#6397 Area: Dealops 2 ECM State: open Files: 28 Diff: +7425 / -5 Branch: claude/backfill-salesforce-langchain-a8945b Note: diff provided to this explainer was truncated

What it adds

A one-off Langchain ECM backfill under apps/server/src/dealops2/scripts/2026_07_17__langchain_ecm_backfill.

It extracts Salesforce custom objects, reconstructs PricingQuote.input, validates TCV through the real engine, then builds the native graph.

What it protects

Frozen quotes now stay frozen when opened in the pricing UI or loaded through pricingQuote/get.ts.

The PR blocks several existing auto-mutation paths that could reprice historical quotes to current catalog defaults.

Correctness gate

The import is self-validating: reconstructed quote TCV must match Order_Form_Total__c within ±$1.

Failures are quarantined and skipped, not partially imported.

Local proof point

Against UAT sandbox copies: 591/591 contracts and 629/629 members pass the engine-replay gate.

Salesforce ECM objects
Inverter
Engine gate
Native Dealops graph
Frozen quote guards
Rollback scope

1. Why this exists

Problem

Langchain loaded historical contracts directly into Salesforce as ECM custom objects:

Deal_Group__c Order_Form__c Order_Form_Line_Item__c Rate_Table__c

Those contracts do not exist as native Dealops records.

Why native records matter

The ECM renewal seeder does not read Salesforce order forms.

It walks a real Dealops chain and reads stored quote inputs through resolveEngagementRenewalSeedaccumulateChainProducts.

Target graph
Account └─ Engagement └─ DealGroup(type = CONTRACT, engagementSequence) └─ DealGroupMembership(sequence) └─ OpportunityV2(Closed Won) ├─ PricingQuote(isPrimary, frozen, valid input/output) └─ InvoiceDetail
Design constraint: this is the inbound inverse of Langchain writeback. Salesforce has engine output, but Dealops renewal needs engine input, so the pipeline must invert a lossy shape and then prove the result.

2. What changes

22pipeline unit tests
4CLI phases
±$1TCV tolerance
0schema migrations

Server: backfill pipeline

Area Files Material change
server pipeline 01_extract.ts, 02_validate.ts, 03_build.ts, 04_rollback.ts CLI entrypoints for staged extraction, engine validation, graph creation, and pre-cutover rollback.
core import logic extract.ts, invert.ts, engineContext.ts, plan.ts, buildGraph.ts Turns Salesforce rows into Dealops quote input, checks it with the real pricing engine, and persists the ECM graph.
tests invert.test.ts, plan.test.ts, reconstruct.test.ts Pure Mocha tests for inversion math, engagement planning, and prior-contract reconstruction.
client guards CategorySelectionStep.tsx, CategoryStep.tsx, ProductSelectionStep.tsx, pricingQuote.util.ts Bail out of auto-add, auto-remove, multi-period rewrite, and suggested-price application when isQuoteFrozen is true.
server read guard apps/server/src/trpc/router/pricingQuote/get.ts Skips amendment recompute and runtime date/endDate rewrites for frozen quotes.
flags/docs packages/feature-flags/flags.ts, CONTEXT.md, rfcs/2026-07-17-langchain-sf-history-backfill.md Enables Langchain engagement/contract-model rendering and documents the ECM import design.

Before / after

Before
  • Langchain historical contracts live in Salesforce only.
  • Renewal seeding cannot use them because there is no native PricingQuote.input chain.
  • Opening a frozen quote can silently mutate products, prices, dates, or commitments through existing effects.
After
  • Passing contracts become native Account → Engagement → DealGroup → OpportunityV2 → PricingQuote records.
  • Imported quotes are frozen to the current catalog and stamped as already CRM-submitted.
  • Frozen quotes are served and displayed exactly as stored.

3. How it works

Extract

01_extract.ts reads Salesforce and writes staged JSON only.

  • Seeds accounts from worklist opportunities.
  • Pulls all opportunities on those accounts in full-engagement mode.
  • Discovers deal groups via direct links, reverse opportunity links, and primary-opportunity links.
Invert

invert.ts reconstructs a native PricingQuoteInput from Salesforce output rows.

  • Commit OLIs become multiCommitments.
  • Usage rate rows become per-unit product prices.
  • Recurring lines become platform-subscription flat fees.
Validate

02_validate.ts runs the real pricing engine through EngineContext.

Computed tcv.all.value must match the Salesforce order-form total within ±$1.

Build

03_build.ts is dry-run by default and writes only with --commit.

It is idempotent on the opener opportunity CRM id, so reruns skip already-imported contracts.

Key inversion rules

Salesforce shape Dealops shape Reason
Commit Order_Form_Line_Item__c multiCommitments[].tiers[] Each commit line carries tier bounds, order, future-option status, and committed amount.
Rate_Table__c Usage Rate rows products[].quotePriceFlat plus commitmentTierPrice The real usage product and per-tier rates live on rate rows, not on the generic commitment line.
Recurring line item Flat-fee product with term-adjusted quotePriceFlat Full-term recurring amount is divided by annual billing boundary count, so engine replay returns the Salesforce total.
Order-form replacement edges Synthesized prior contract terms Older prior contracts often have no Salesforce Deal Group; the pipeline reconstructs them from replacement links.
SF owner ids Dealops User ids, with fallback The builder resolves salesforceUserId and requires --owner-user-id when needed.

Frozen quote protection

Guardrail added before import

The PR fixes three mutation paths found by opening imported quotes:

Frozen quote opens auto effects check isQuoteFrozen stored input survives
Important file groups touched

4. What it doesn't change

Explicit non-goals

5. Risks / rollback / open questions

Rollback is destructive and pre-cutover only. 04_rollback.ts deletes the imported graph by natural CRM keys and clears app-generated quote children scoped to the imported quote/opportunity ids. Do not run it after reps edit imported contracts or after other data references those rows.
Risk: current catalog assumption

The gate prices against the latest catalog. That is intentional because Langchain prices are absolute, but structural drift in calculation specs would show up as quarantine failures.

Mitigation engine replay must match Salesforce TCV before build.

Risk: org mismatch

EngineContext warns if the org name is not exactly Langchain, because the variable registry is keyed by org name.

Reviewer check verify UAT vs prod org selection before trusting reports.

Risk: UI regression

Frozen-quote guards touch broad pricing-flow components.

Reviewer check confirm non-imported, non-frozen draft quote editing still auto-selects products and terms as before.

Open reviewer tasks
  • Spot-check an imported quote after a 03_build --commit run.
  • Verify products, net unit price, dates, and TCV in the UI.
  • Confirm imported frozen quotes do not mutate on open.
Diff visibility note: the supplied diff was truncated, so this explainer is based on the PR description, file list, and visible portions of the diff. It does not claim line-level coverage of omitted sections.