Engagement Contract Model: team docs + pre-rollout review

A documentation-only PR landing seven team-facing docs for the Account→Engagement→Contract hierarchy alongside a severity-ranked RFC of findings ahead of enabling the system for Langchain's main org.

PR: dealops#5888 Author: @mileszim Branch: miles/engagement-rollout-review-doc Files: 8 added +886 / −0 State: Open (draft for review) Type: docs-only

What it adds
Seven team docs under docs/engagement-contract-model/ describing the system as-is — data model, lifecycle, renewal pipeline, SF writeback, org-agnostic vs Langchain-specific split, and the migration/onboarding flow.
And a pre-rollout RFC
A separate review document at rfcs/2026-06-09-…rollout-review.md with 14 severity-ranked findings (4 🔴, 4 🟠, 3 🟡, plus minor) and a 9-item pre-rollout checklist for Langchain.
Why now
Three feature flags are about to flip on for the Langchain main org. The team needs onboarding docs that survive the rollout, and the rollout team needs a shared list of known risks before flipping switches.
What it does not change
No code paths are touched. Prettier-formatted markdown only; Mermaid blocks use GitHub-safe <br/> line breaks. All TypeScript/Prisma/listener behavior is described, not modified.
Account / Engagement / Contract (core)
Renewal pipeline
Salesforce writeback (LC-specific)
Feature flags
🔴 Pre-rollout blocker

1. Why this exists

The engagement contract model adds a real three-level customer hierarchy on top of the legacy flat DealGroup table: AccountEngagementContract. It also introduces an automated renewal pipeline: when a contract's opportunity goes Closed Won, a PENDING successor contract is scaffolded with a draft quote seeded from the prior contract's signed state.

The system is gated behind three flags and has been incubating for months. Langchain is the first production user. This PR ships the two artifacts the team needs before flipping switches:

Artifact 1 — docs/engagement-contract-model/
Knowledge-sharing docs written so an engineer can onboard onto the system or onboard the next customer org without rereading every PR. As-is, no design speculation, lots of mermaid.
Artifact 2 — rfcs/2026-06-09-…rollout-review.md
A pre-rollout review, kept separate from the team docs so debate over severity calls doesn't muddy the reference material. Severity-ranked findings + a checklist.

2. The seven team docs

DocLinesCovers
README.md87Big-picture overview, the three flags, glossary, system map
01-data-model.md136Account/Engagement/Contract entities, statuses, DB invariants, worked LangCo example
02-contract-lifecycle.md82CRM enrichment → hydration decider → EngagementContractService
03-renewal-pipeline.md124Closed-Won scaffolding, seed derivation (state as-of date), reseed triggers, walker cutover
04-salesforce-writeback.md94Deal_Group__c sync: triggers, field derivations, status mapping, chain linkage
05-langchain-vs-general.md72Org-agnostic core vs Langchain-specific adapters
06-migration-and-onboarding.md74The 3-pass backfill, flag flip order, new-customer onboarding checklist

The hierarchy at a glance

Level 1
Account — persistent customer identity. Survives churn and win-back. crmAccountId deliberately NOT unique (NULL-heavy for orgs without SF Account); enforced at service layer.
Level 2
Engagement — one continuous commercial relationship. ACTIVE or CHURNED. A win-back opens a new Engagement under the same Account.
Level 3
Contract — a DealGroup with type=CONTRACT. One signed term. Has contractType (NEW_BUSINESS / RENEWAL), engagementSequence (1-based), status (PENDING / ACTIVE / CLOSED / ARCHIVED).
Within a contract
Memberships — sequence 1 is the head (NB or renewal opp), sequence 2+ are amendments of that term. The chain walker starts here.

The three feature flags

isEngagementContractModelEnabled
Write path
Enrichment routes DealGroup creation through EngagementContractService (lazy Account/Engagement, sequence allocation, invariants). Also the manual dealGroup.create path.
useEngagementRenewalChain
Read / seed path
Engagement-aware chain walker, prior-contract-state seeding, reseed reactors, and the client's canReseedFromPriorContract signal.
isLangchainDealGroupWritebackEnabled
Salesforce push
The Deal_Group__c sync listener and the scaffold reactor's best-effort chain sync. Decoupled by design — the model runs without any SF mirror.

3. How the system reads (a tour through the docs)

Pick a tab to see how that doc frames its slice of the system.

01 — Data model

All engagement fields on DealGroup are nullable, so legacy rows remain valid and legacy read paths keep working. The model layers on top, not over.

The invariants that matter:

The PENDING→ACTIVE transition ("supersession") is a separate workstream. Service primitives exist (createRenewalContract, closeContractDealGroup) but no production path calls them yet — flagged in F7 of the review.

4. The pre-rollout review (the RFC)

The second artifact is rfcs/2026-06-09-engagement-contract-model-rollout-review.md — kept deliberately separate so disagreement over severity calls doesn't bleed into the reference docs. 14 findings, ranked.

🔴 Fix / mitigate before rollout
4 findings — F1, F2, F3, F4. Active correctness or data-corruption risks.
🟠 Decide / monitor before rollout
4 findings — F5, F6, F8, plus F13 nuance. Need a position, may not need code.
🟡 Known gaps, schedule
3 findings — F7, F10. Acknowledged debt with concrete future fixes.
⚪ Minor / nit
4 findings — F9, F11, F12, F14. Worth fixing eventually; not gating.

The four 🔴 blockers, in detail

F1 · 🔴 fix or mitigate

Duplicate PENDING renewal race — three writers, no DB constraint

The PENDING renewal successor can be created by three independent code paths: scaffold reactor, Deal_Group__c reconcile reactor, CRM-sync enrichment. All run on independent queues. All dedupe via read-then-write.

The (engagementId, engagementSequence) unique index does not reject a second PENDING — createContractDealGroup's P2002 retry loop re-reads the max and lands the loser at seq+2. The single-active partial index is ACTIVE-scoped and never applies.

Aggravator: enrichment's dedupe is head-dependent (findPendingRenewalSuccessorOpp), so a half-finished scaffold (contract row created, head not yet attached) is invisible to it. The scaffold's own orphan-reuse is head-independent. Asymmetric.

Fix: Postgres advisory lock keyed on prior DealGroup id inside createPendingRenewalContract, with successor re-check inside the lock. Switch enrichment to the head-independent lookup.

F2 · 🔴 fix or mitigate

Any SF edit to the prior's Deal_Group__c row wholesale-overwrites the renewal draft

renewalReconcileFromDealGroupReactor has no field/stage gate. Every Deal_Group__c UPDATE webhook for an ACTIVE contract reaches ensureRenewalForClosedWonContract → refresh branch → reseedRenewalQuote.

findRenewalDraftQuote picks primary-else-most-recent — so a quote the rep built later (or marked primary) is selected over the scaffold draft and overwritten wholesale.

Fix: gate the reconcile reactor's refresh on the renewal link actually changing (the payload already carries it); and/or restrict the refresh target to the scaffold-created draft.

F3 · 🔴 fix or mitigate

Closed Lost is handled nowhere

NB contracts are created at sync time at any stage — the decider has no stage check. A pipeline-stage "New to Langchain" opp opens an ACTIVE Contract immediately. If the deal is lost, the contract stays ACTIVE forever.

A dead ACTIVE contract permanently blocks future NB contracts for the account: assertNoActiveContractForEngagement throws, enrichment swallows to logWarning. The new opp is silently stranded with no DealGroup on every re-sync.

Fix: gate NB contract creation on Closed Won, and/or build a Closed-Lost reactor that closes/archives the contract.

F4 · 🔴 fix or mitigate

Model ON + chain OFF actively corrupts renewal quotes

Renewal contract creation is gated only on isEngagementContractModelEnabled. Renewal seeding only on useEngagementRenewalChain. In the intermediate state, renewal opps become sole sequence-1 heads, the legacy walker finds no prior, and renewal quotes seed with catalog defaults — visibly wrong, not a no-op.

Enforced only by a code comment. Also: reactor flag evals use sentinel users ('system', ''). If PostHog targets by user properties, reactors and tRPC requests split-brain.

Rule: target by org ID only; flip chain ≤ model; never run model-only.

Pre-rollout checklist (9 items)

#ActionTies to
1Run duplicate-crmAccountId audit SQL on LC prod; merge dupes.F5
2Land advisory-lock + head-independent-dedupe fix (or accept race + monitor).F1
3Gate reconcile reactor's refresh on renewal-link change, or restrict reseed target.F2
4Decide the Closed-Lost story; audit LC data for accounts with 2+ open NB opps.F3
5PostHog: all three flags org-ID-only; flip order chain → model → writeback.F4
6Confirm LC automation: when does it set Opportunity_Replaced_By__c vs Closed-Won webhook?F8
7Fix record_type on eagerly-created renewal opps (or normalize exclude-filter matching).F6
8Comms to LC reps: the PENDING draft quote is system-maintained and will be overwritten.F2, F14
9Monitor walker diff, P2002 retries, addOpportunityToContract paged errors, enrichment warn strands.

5. What this PR does not change

6. Open questions raised for the team

Severity calls to debate. The RFC author marked F1–F4 as 🔴 (blocker). Reasonable people may disagree:
Questions for Langchain (F8). The scaffold trigger assumes a particular ordering of LC automation. Confirm with LC:

7. Reviewer guidance

For docs reviewers
Read for accuracy against the current codebase. Each doc lists its key files at the top — spot-check that the described behavior matches what's in main. Mermaid renders correctly on GitHub; verify any diagram you're skeptical of.
For rollout reviewers
Focus on the RFC. Push back on severity calls. Suggest checklist items the author missed. The goal of merging this draft is to get the team's agreed-upon list of blockers, not to ratify the author's calls verbatim.