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.

Author: @mehulshinde PR: #6570 Status: draft / open Base: main Head: claude/dealops-jdo7-10-2-c7d65f Files: 43 Diff: +18,902 / -0 Scope: apps/server Dealops 2 scripts

What it adds

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.

Why it exists

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.

Safety shape

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.

Evidence included

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.

Manifest
Dealops graph
Salesforce
Safety gates
Rollback
Test evidence
Diff visibility: the user-provided diff is truncated. This explainer is based on the PR description, complete file list, and the visible phase entrypoints/documentation snippets; it does not pretend to inspect every added line.

1. Why this exists

Business proof

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.

Engineering proof

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.

Pricing claim under test

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
6pilot phases
7,072Mocha tests passing
Salesforce syncs verified
0Salesforce writes in phase 2

2. What changes

0000_preflight.tswrites nothing; creates evidence + manifest
0101_approve.tsmanifest only; immutable human approval
0202_build_dealops.tsDealops graph; zero Salesforce writes
0303_verify_seed.tsread-only gate over the seeded graph
0404_verify_writeback.tsSalesforce sync twice; no quote minting
0505_rollback.tsdeletes only run-owned rows + writeback quote/lines

File groups

Phase shells

Thin CLI entrypoints own arguments, process exit, environment gates, and reports.

00_preflight.ts+560
04_verify_writeback.ts+541
05_rollback.ts+601
Pure / injectable modules

Decision logic is split away from I/O so it can be tested without production access.

graphPlan.ts+464
renewalWriteback.ts+1269
rollbackPlan.ts+472
Docs + artifacts

The PR carries run instructions, testing rationale, and concrete output from the run of record.

README.md+438
RUNBOOK-neon.md+281
runs/*.json5 files

3. How it works

Execution model
Before this PR
  • Rollback commit path for this pilot shape had not executed end to end.
  • Some invalid Prisma queries could typecheck because generated where/select inputs are not excess-property checked.
  • A script-authored quote would not prove the normal app quote path.
After this PR
  • 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

Sandbox / Neon cycle
29/29preflight
14created; 0 adopted
23/23verify seed
25/25 ×2writeback checks
19rollback deletes
0residual records
quoteIdStable      true
lineIds.refreshed  true
staleLineIds       []
lineValueChanges   []
orphans            none
submittedToCrmAt   stamped only after Salesforce success

Testing layers

Pure decision tests

graphPlan, seedVerification, renewalWriteback, and rollbackPlan cover ordering, coalescing, preconditions, ownership, and parentage decisions.

Fake-client call logs

graphWriter.test.ts, writebackRunner.test.ts, and cascade.test.ts assert exact Prisma/effect ordering without live services.

Real execution

queryExecution.test.ts executes Prisma queries against a disposable DB; soqlExecution.test.ts executes SOQL against a real org.

Important test finding: TypeScript alone is not evidence for Prisma query validity here. The PR adds knowledge/concepts/prisma-query-validity-untyped.md because invalid where/select shapes typechecked and failed only when executed.

4. What it doesn't change

Explicit non-goals

5. Risks / rollback / open questions

Draft state: the PR description says this branch needs a rebase. main is ahead, #6569 was squash-merged, and the branch carries its own copy of that work with six conflicting files.
Known gap

preflight does not populate manifest.pricing.

Practical effect: phase 4 checks TCV/ARR presence and stability, not equality to an independently replayed total.

Blocked targets

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.

Operational blast radius

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.

Rollback model: every phase is dry-run by default. Commit needs --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.