Merge ECM renewal pilot: six guarded phases
This PR adds a Dealops 2 server-side pilot harness for one Merge account renewal, with manifest approval, two-run Salesforce writeback verification, rollback, run artifacts, and query-validity evidence.
Six phase entrypoints under apps/server/src/dealops2/scripts/2026_08_07__merge_prod_openai_ecm.
They move one reviewed Merge account through preflight, approval, graph build, seed verification, writeback verification, and rollback.
The pilot needs to prove the ECM renewal path on real account history, not on a script-created quote.
The pricing claim is specific: the renewal should seed one coalesced line, 843 × $600.
Targets are reviewed allowlist entries; shell-provided account, opportunity, quote, org, and host overrides are refused.
Writes require a named approval, exact run-id confirmation, and environment checks.
The PR carries run artifacts for a full sandbox/Neon cycle, plus Mocha layers for pure logic, fake-client call logs, schema drift, live Prisma query execution, and live SOQL parsing.
1. Why this exists
Merge needs a real Engagement Contract Model renewal seeded from reconstructed predecessor history.
The rep must use the normal quote-create path. Phase 4 explicitly refuses to mint the renewal quote itself.
The pilot must show Salesforce writeback is idempotent across two manual syncs and that rollback removes all run-owned rows.
The rebuild after rollback is treated as the proof that no hidden residue survived.
The predecessor has 500 units at $1,000/year. The Monthly Overage adds 343 at $600/year, on the same Product2, with an overlapping/co-terminating window. The amendment price wins.
843 × $1,000
$843,000
wrong: land price wins
500 × $1,000 + 343 × $600
$705,800
wrong: two lines
843 × $600
$505,800
correct: amendment price wins
2. What changes
--commit. It validates the target tuple before contacting Salesforce.
00_preflight.tswrites nothing; creates evidence + manifest01_approve.tsmanifest only; immutable human approval02_build_dealops.tsDealops graph; zero Salesforce writes03_verify_seed.tsread-only gate over the seeded graph04_verify_writeback.tsSalesforce sync twice; no quote minting05_rollback.tsdeletes only run-owned rows + writeback quote/linesFile groups
Thin CLI entrypoints own arguments, process exit, environment gates, and reports.
00_preflight.ts+56004_verify_writeback.ts+54105_rollback.ts+601Decision logic is split away from I/O so it can be tested without production access.
graphPlan.ts+464renewalWriteback.ts+1269rollbackPlan.ts+472The PR carries run instructions, testing rationale, and concrete output from the run of record.
README.md+438RUNBOOK-neon.md+281runs/*.json5 files3. How it works
- Reviewed target selected by
--target=<key>, never by free-form account/opportunity/host flags. - Manifest freezes discovered scope and approval checksum.
- Dealops graph reconstructs predecessor + amendment + pending renewal graph from adopted CRM history.
- Salesforce writeback syncs the rep-authored renewal quote twice through the production writeback path.
- Rollback deletes by manifest ownership plus verified parentage, then rebuild proves no residue.
- Rollback commit path for this pilot shape had not executed end to end.
- Some invalid Prisma queries could typecheck because generated
where/selectinputs are not excess-property checked. - A script-authored quote would not prove the normal app quote path.
- Each phase has a dedicated entrypoint and a report artifact.
- Query validity gets real database/SOQL execution tests.
- Phase 4 refuses until a rep-authored quote exists, then syncs it twice and checks idempotency.
| Guard | Where it lives | Reviewer takeaway |
|---|---|---|
| No target overrides | safety.ts |
Operators cannot supply account, opportunity, quote, org, or Salesforce host from the shell. |
| Approval required | manifest.ts, 01_approve.ts |
Write phases are closed until a named human freezes the discovered scope. |
| Render-only production writes | assertCommitEnvironment |
PROD.localNonProdDatabaseHostSuffix is intentionally null; laptops cannot write to prod. |
| Writeback idempotency | renewalWriteback.ts, writebackRunner.ts |
Same Quote id, no value changes, stale line ids rejected, and submittedToCrmAt stamped after success. |
| Rollback closure | cascade.ts, rollbackPlan.ts |
Deletes manifest-owned rows and rows proven by parentage; adopted Salesforce history is never deleted. |
Run of record embedded in the PR
quoteIdStable true
lineIds.refreshed true
staleLineIds []
lineValueChanges []
orphans none
submittedToCrmAt stamped only after Salesforce success
Testing layers
graphPlan, seedVerification, renewalWriteback, and rollbackPlan cover ordering, coalescing, preconditions, ownership, and parentage decisions.
graphWriter.test.ts, writebackRunner.test.ts, and cascade.test.ts assert exact Prisma/effect ordering without live services.
queryExecution.test.ts executes Prisma queries against a disposable DB; soqlExecution.test.ts executes SOQL against a real org.
knowledge/concepts/prisma-query-validity-untyped.md because invalid where/select shapes typechecked and failed only when executed.
4. What it doesn't change
- No tRPC route, REST endpoint, React UI, or product-facing workflow is added.
- No Prisma migration or schema change is listed.
- Phase 2 performs no Salesforce writes; it only reads CRM history and builds Dealops rows.
- Phase 4 does not create the renewal quote; the rep creates it in the app first.
- Rollback does not delete adopted Salesforce account, opportunity, or deal group history.
- Rollback intentionally leaves Activity and PlatformEvent audit rows; “zero residual” means zero FK-reachable graph rows and zero pilot-created Salesforce quote records.
- The dependency engine change
repriceFlatDeltasToLatestis not introduced here; the PR depends on the already-merged#6569.
5. Risks / rollback / open questions
main is ahead, #6569 was squash-merged, and the branch carries its own copy of that work with six conflicting files.
preflight does not populate manifest.pricing.
Practical effect: phase 4 checks TCV/ARR presence and stability, not equality to an independently replayed total.
fixture and openai still need input records: the AE’s fixture records and the real renewal Opportunity id.
The OpenAI run also needs the org-wide coalescing knob set before execution.
The knob is org-wide but inert until a graph exists.
The runbook requires an exposure audit before flipping useEngagementRenewalChain so only the allowlisted opportunity is affected.
--commit, --confirm-run-id=<exact run id>, approved manifest scope, and the environment check. Phase 5 is designed to run even when phase 4 failed, but commit must record --skip-writeback-reason if writeback was never verified.
| After merge | What earns the next step | Primary risk control |
|---|---|---|
| 1 — Test org on Render | Run phases 0–5 against sandbox-expansion after confirming flags resolve on Render. |
Production DB target tuple + sandbox target allowlist. |
| 2 — AE fixture in production org | Fill fixture target in a reviewed diff, then run the same phases on deletable records. |
Real prod org/DB/writeback path, but disposable business records. |
| 3 — OpenAI | Provide renewal Opportunity id, set coalescing knob, run exposure audit, then phases 0–5. | Manifest approval, exposure audit, manual rep-authored quote, and rollback proof. |