# NexusCRM L0 Cleanup Final Plan

Status: final execution plan
Date: 2026-06-23
Code anchor: `origin/main` `00ffd54e5`
Input plans: **B = original full plan**, **A = revised 4-PR plan**

## Executive Decision

The correct plan is **not A or B as-is**.

- **B is safer and more accurate on boundary protection**, especially the need for a front-loaded seal and the need to isolate security.
- **A is more executable**, because it compresses the work into a small number of visible, high-impact cuts.
- **A has one unacceptable flaw**: it bundles security schema-binding, records lifecycle rewrite, and component registry work into one PR. Lifecycle and component registry must also stay separate because lifecycle changes persistence semantics.
- **B has one execution flaw**: it risks becoming too conservative and too many PRs.

The final version is:

> **A's execution philosophy + B's two safety mechanisms:**
> thin boundary seal first, security standalone, lifecycle standalone, then a small number of large coherent PRs.

The plan should be **1 small preflight PR + 6 main PRs**.

This is aggressive enough to clean L0 visibly, but accurate enough not to mix security-sensitive or persistence-sensitive changes with unrelated runtime refactors.

---

## Core Boundary Rule

Use one rule everywhere:

> **Mechanism stays in L0. Salesforce shape moves out of L0.**

| Keep in L0 | Move above L0 |
|---|---|
| metadata engine | CRM/Salesforce metadata payload |
| records CRUD engine | v63 SObject wire envelope |
| policy evaluator | Salesforce profile/default grants/schema bindings |
| query executor/planner | SOQL/SOSL grammar and response envelope |
| formula VM | Salesforce formula spelling, catalog profile, vendor functions |
| flow/orchestration runtime | Tooling API `FlowDefinition`/`Flow` DTOs |
| lifecycle phase engine | Salesforce order-of-execution profile |
| pack install/drift engine | product component vocabulary and handlers |
| dialect transport substrate | concrete Salesforce REST route ownership |

This is not de-Salesforce. It is **Salesforce mechanism internalization + Salesforce compatibility externalization**.

---

## Non-Negotiable Corrections From A and B

### 1. Security must be a standalone PR

A's PR3 grouped:

- security schema-binding
- lifecycle profile rewrite
- component registry convergence

That grouping is unsafe.

Policy/FLS/RLS/sharing is the highest-risk part of this program. A regression here can become silent over-permission or data leakage. It needs its own golden tests, its own rollback unit, and its own review focus.

Final decision:

> **Security schema-binding gets its own PR. No lifecycle or component registry work in that PR.**

### 2. Boundary seal must come first, but it must stay thin

B is right that migration needs a front-loaded seal. But Phase 0 should not become a large testing platform project.

Final decision:

> **PR0 is a small seal, not a full framework rewrite.**

It should include:

- forbidden import/literal checks
- route ownership checks
- contract DTO leak checks
- minimal golden harness for touched compatibility surfaces
- a lightweight boundary ledger

It should not attempt to model the entire system before migration starts.

### 3. REST wire skin should move early

REST/v63/SObject skin is visible Salesforce shape, and much of the execution beneath it already goes through neutral contracts. Moving it early gives the biggest L0 cleanliness win with relatively low engine risk.

Final decision:

> **Move REST wire ownership in PR1, not at the end.**

Query-dependent endpoints can be completed in PR2, but route ownership and obvious SObject/v63 projection should move early.

SOSL should not move twice: PR1 may move route ownership while leaving the existing parser as a ledgered temporary shim; PR2 moves the parser/grammar boundary.

### 4. Formula is the hardest track

The current function-bundle seam is useful, but it does not fully solve formula ownership.

There are three separate ownership layers:

1. declaration/catalog ownership
2. runtime evaluator ownership
3. SQL emitter / analytics compiler ownership

A function being absent from `DefaultRegistry` is not enough. If app-blind L0 still has Salesforce-only function-name branches in evaluator or SQL walker code, the function has not truly moved out.

Final decision:

> **Formula is the final hard PR. It needs a native evaluator and SQL-emitter registration seam, not only bundle data.**

### 5. Separate contract-neutrality proof from real multi-App proof

The issue-tracker is a sealed CI-only conformance fixture, not a product App. It can
prove that a seam does not encode CRM semantics, but it cannot prove production
packaging, deployment, lifecycle, ownership, or operational independence for a
second App. P1x physically removed its former release-shaped pack, dialect,
distribution, frontend, install, and deploy residue.

Final decision:

> **Use the issue-tracker fixture as a contract-neutrality gate. Require a real independently owned non-CRM App before claiming production multi-App proof.**

### 6. Lifecycle persistence semantics must be standalone

Lifecycle ordering decides which record image validation rules, uniqueness checks, hooks, and SQL persistence see. That is persistence semantics, not a mechanical runtime-registry cleanup.

Final decision:

> **Records lifecycle gets its own PR. Component registry convergence gets a separate PR.**

---

# Final PR Plan

## PR0 — Thin Boundary Seal and Golden Baseline

**Type:** small preflight PR
**Purpose:** stop backflow before migration starts
**Risk:** low
**Must land before all other PRs**

### Scope

1. Add a boundary ledger for Salesforce-derived mechanisms touched by this program:
   - mechanism
   - current owner
   - target owner
   - seam
   - migration PR
   - allowed temporary compatibility shim, if any

2. Add static CI guards:
   - no `dialect-sf-crm` imports from app-blind L0 packages
   - no new `/services/data` route literals outside dialect/distribution allowlist
   - no new v63/Tooling DTO names in `shared/pkg/contract` without ledger approval
   - no new `FlowDefinition`, `ProcessType`, `TriggerType`, `TriggerObjectOrEvent` leaks in neutral contracts
   - no new Salesforce-only function-name branches in app-blind formula evaluator/SQL-emitter code

3. Add minimal golden baselines:
   - v63 route surface snapshot for CRM distribution
   - SObject envelope compatibility snapshot
   - representative SOQL/SOSL/query/search snapshot
   - representative access decision matrix for object, field, and row access
   - representative formula compatibility snapshot

4. Keep default server app-blind closure guard and strengthen it if needed.

### Out of Scope

- No large route migration.
- No contract rewrite.
- No policy behavior changes.
- No formula rewrite.

### Acceptance Criteria

- CI fails if new Salesforce public surface enters app-blind L0.
- CRM compatibility golden baselines exist before behavior-moving PRs begin.
- The default app-blind server still builds without CRM dialect/app-pack source.

---

## PR1 — Cut the Visible Salesforce Wire Skin

**Type:** large but mostly mechanical
**Purpose:** remove the most visible Salesforce surface from app-blind L0
**Risk:** medium
**Primary owner:** REST/dialect boundary

### Scope

1. Delete already-extracted dead Salesforce twins:
   - backend DTOs/projections with zero live non-test route ownership
   - duplicate describe/SObject response structs already implemented in `dialect-sf-crm`
   - retired compatibility helpers no longer reachable from live CRM routes

2. Move active Salesforce REST route ownership above L0:
   - `/services/data/...`
   - SObject record wire projection
   - `attributes.type/url`
   - `Id` casing/envelope
   - Salesforce error envelope/code projection
   - Tooling/UI API route ownership where the backing engine is already neutral
   - SOSL/search route ownership where safe, with the parser recorded as a temporary shim if it remains in tagless REST code until PR2

3. Keep neutral helpers in backend only when they are true engine services:
   - record retrieval
   - metadata lookup
   - policy checks
   - query execution
   - transaction helpers

4. Route registration should happen through product/dialect composition, not generic server wiring.

### Important Nuance

Do not block all REST migration on query IR completion. Split internally:

- Move obvious wire projection now.
- For handlers still carrying SOQL/SOSL text, move ownership now where safe.
- Keep any remaining SOQL/SOSL parser as a ledgered temporary shim; complete parser/IR cleanup in PR2.

### Out of Scope

- No policy schema-binding rewrite.
- No records lifecycle rewrite.
- No formula evaluator rewrite.
- No broad query IR redesign beyond what is needed to move route ownership.

### Acceptance Criteria

- No tagless app-blind platform route owns a Salesforce public endpoint unless explicitly listed as a temporary shim in the ledger.
- CRM distribution still produces the same v63 route behavior according to golden tests.
- Default app-blind server runs without CRM public route ownership.
- Deleted dead code has no live non-test references.

---

## PR2 — Neutralize Contract and Query/Search Boundary

**Type:** medium-large
**Purpose:** stop Salesforce schema and grammar from crossing neutral contracts
**Risk:** medium
**Primary owner:** `shared/pkg/contract`, `queryspec`, query/search dialect boundary

### Scope

1. Diet Flow authoring contracts:
   - move Salesforce Tooling API DTOs out of neutral contract packages
   - replace `DeveloperName`, `ProcessType`, `TriggerType`, `TriggerObjectOrEvent`, and similar Tooling-shaped fields with neutral IR names where the concept belongs in L0
   - put Salesforce Tooling projection in `dialect-sf-crm`

2. Complete query/search IR boundary:
   - external SOQL/SOSL text must not be the engine-facing representation
   - replace opaque external WHERE/HAVING formula text carriers with neutral predicate tree nodes
   - keep formula predicates only for real platform formula expressions, not disguised SOQL carriers
   - move remaining SOSL parsing out of tagless REST helpers and into the Salesforce dialect/search module; this is where SOSL grammar moves, even if PR1 moved route ownership first

3. Audit neutral error/result contracts:
   - keep neutral error categories in L0
   - project Salesforce wire error names/codes in dialect

4. Add issue-tracker fixture coverage for neutral query/search contracts where
   practical; do not count it as production multi-App coverage.

### Out of Scope

- No policy storage binding rewrite.
- No lifecycle phase rewrite.
- No formula SQL-emitter redesign.

### Acceptance Criteria

- L0 query/search execution receives neutral `QuerySpec`/`SearchSpec`, not SOQL/SOSL text.
- `shared/pkg/contract` no longer exposes Salesforce Tooling API DTOs as neutral engine facts.
- CRM dialect preserves existing SOQL/SOSL/v63 compatibility through projection.
- The issue-tracker fixture can exercise the same neutral query/search engine
  without pretending to be Salesforce; a real non-CRM App remains required for
  a production multi-App claim.

---

## PR3 — Security Schema Binding and Policy Fact Ownership

**Type:** standalone high-safety PR
**Purpose:** keep one L0 policy evaluator while moving Salesforce schema/default policy shape above L0
**Risk:** high
**Primary owner:** policy/FLS/RLS/sharing

### Scope

1. Add or formalize a `PolicySchemaBinding`-style seam:
   - subject/principal/group/role binding
   - object permission source binding
   - field permission source binding
   - row visibility/sharing source binding
   - default grant source binding
   - admin projection binding
   - neutral permission admin contract binding, including object/field permission result shapes

2. Move Salesforce-specific schema names, permission wire structs, and default grants out of app-blind policy evaluation:
   - L0 policy evaluator should not infer semantics from Salesforce table/column names
   - neutral contracts should not expose Salesforce-shaped `ObjectPermissions` / `FieldPermissions` DTOs as the cross-boundary policy result
   - CRM distribution or dialect/app pack registers Salesforce-compatible bindings

3. Preserve one authoritative policy read path:
   - no duplicate L1 policy evaluator
   - no fail-open fallback
   - no second source of truth for FLS/RLS/sharing decisions

4. Golden-first tests:
   - object CRUD matrix
   - field read/write matrix
   - row visibility matrix
   - sharing inheritance/override cases
   - admin projection compatibility cases

5. Use the issue-tracker conformance fixture as a non-CRM binding smoke test;
   use a real product distribution only when one independently exists.

### Out of Scope

- No lifecycle phase reordering.
- No component registry migration.
- No REST route migration except compatibility projection needed by policy admin surfaces.
- No formula changes.
- No separate PR2 cleanup of permission-admin wire DTOs; policy contract cleanup belongs in this PR's rollback unit.

### Acceptance Criteria

- Access decisions are identical before/after for CRM golden cases.
- Missing policy binding fails closed.
- App-blind L0 policy code no longer treats Salesforce schema names as the source of semantics.
- Permission admin contracts no longer expose Salesforce-shaped permission wire structs as neutral facts.
- There remains exactly one authoritative L0 policy decision path.
- This PR can be reverted independently without reverting lifecycle/component changes.

---

## PR4 — Records Lifecycle Un-Welding

**Type:** standalone persistence-semantics PR
**Purpose:** remove Salesforce lifecycle assumptions from the records save pipeline
**Risk:** medium-high
**Primary owner:** records lifecycle

This PR is isolated because lifecycle ordering controls which record image validation, uniqueness, hooks, and SQL persistence observe.

### Scope

1. Start with a short spike against the issue-tracker conformance fixture, then
   validate any product-level lifecycle claim against a real independently
   owned non-CRM App when one exists:
   - decide whether a fully reorderable lifecycle profile is actually needed
   - if not needed, prefer un-welding Salesforce-specific hook placement over building an arbitrary phase-profile engine

2. Extract or clarify the generic lifecycle pipeline:
   - normalize input
   - pre-mutation hooks
   - validation
   - uniqueness/integrity checks
   - persistence
   - post-mutation hooks
   - audit/outbox/async dispatch

3. Move Salesforce order-of-execution assumptions out of app-blind code:
   - remove Salesforce order labels from the generic pipeline
   - remove Salesforce objectName branches from the generic lifecycle path
   - connect Salesforce-specific hook placement through registration
   - introduce a registered lifecycle profile only if the spike proves a real non-CRM ordering need

4. Keep L0 safety floors non-optional:
   - authorization
   - FLS enforcement
   - transaction atomicity
   - persistence integrity
   - audit/outbox durability
   - fail-closed validation

5. Use the issue-tracker fixture as contract-neutrality proof where practical:
   - same phase engine
   - different or minimal profile
   - no Salesforce order assumption required
   - no claim that the fixture proves production multi-App operation

### Important Nuance

This is not a pure extraction. The current records path is partly straight-line Go logic, so this is a contained rewrite. It should be treated as medium-high risk, but it is narrower than formula SQL-emitter extraction.

### Out of Scope

- No policy/FLS/RLS changes.
- No component registry migration.
- No formula evaluator/SQL-emitter changes.
- No route migration.

### Acceptance Criteria

- Records lifecycle no longer contains Salesforce order-of-execution as an app-blind singleton.
- Salesforce-specific lifecycle hook placement is supplied through registration.
- A non-CRM distribution can use the same records pipeline without Salesforce order assumptions.
- If a reorderable profile is introduced, CRM registers the Salesforce-compatible lifecycle profile and the spike evidence is recorded.
- If no reorderable profile is introduced, the DoD is un-welding Salesforce assumptions, not inventing unused phase flexibility.
- This PR can be reverted independently without reverting component registry changes.

---

## PR5 — Component Registry Convergence

**Type:** medium runtime registration PR
**Purpose:** move product component vocabulary and handlers out of app-blind L0
**Risk:** medium
**Primary owner:** pack component surface

This PR is separate from lifecycle because component-kind convergence is mostly registry/capability work, while lifecycle changes persistence semantics.

### Scope

1. Do not build a registry from scratch if one already exists. Use the existing injectable `ComponentKindRegistry`/capability seam where available.

2. Separate component kinds into:
   - platform-floor kinds compiled in L0
   - product/dialect kinds registered by product modules
   - data-only component contracts
   - code-backed component handlers

3. Move product-driven per-kind handlers to dialect/product registry providers.

4. Fix known soft leaks:
   - Salesforce-shaped install-plan phase ordering
   - PageLayout or similar prechecks hardcoded in app-blind install planning

5. Preserve one pack install/drift engine in L0.

### Out of Scope

- No policy/FLS/RLS changes.
- No formula evaluator/SQL-emitter changes.
- No route migration unless needed for pack/admin projection tests.
- No records lifecycle rewrite.

### Acceptance Criteria

- Adding a product component kind does not require app-blind L0 source changes.
- Unknown component kinds fail closed with a clear missing-registration error.
- Pack ownership/drift continues to use one authoritative engine.

---

## PR6 — Formula Hard Split

**Type:** hard rewrite track
**Purpose:** keep formula VM in L0 while moving Salesforce formula compatibility into registered profile/implementations
**Risk:** high
**Primary owner:** formula/expression/runtime SQL emitters

Formula is intentionally last because it is the hardest and least mechanical part of the cleanup.

### Step A — Mechanical Profile and Catalog Split

1. Classify every default formula member into:
   - core language primitive
   - internal lowering helper
   - Salesforce compatibility function
   - product/app function

2. Move Salesforce-only spelling and semantic profile out of app-blind defaults:
   - `$Record`
   - `__r`
   - `ISCHANGED`
   - `PRIORVALUE`
   - `ISNEW`
   - `ISCLONE`
   - Salesforce empty-string/null behavior
   - Salesforce date/time compatibility semantics where applicable

3. Keep the neutral VM and function registry in L0.

4. CRM formula tests explicitly opt into Salesforce formula profile and bundles.

### Step B — Native Evaluator and SQL-Emitter Capability

Existing function bundles are not enough for all Salesforce formula semantics. Some functions need native evaluator and SQL lowering support.

Start with a short spike before locking the interface:

1. prove the interface with a hard variable-arity function such as `CASE`
2. prove it with a Go/native algorithm function such as `CASESAFEID`
3. prove it with a collection/search semantics function such as `INCLUDES`

Add registered implementation seams for:

1. native runtime evaluator
2. SQL emitter / analytics compiler emitter
3. function-specific validation/lowering behavior where needed

Track ownership separately:

| Layer | Owner |
|---|---|
| function declaration/catalog | core, dialect, or pack bundle |
| runtime evaluator | core or registered native evaluator |
| SQL emitter | core or registered SQL emitter |
| language/profile semantics | core profile or Salesforce profile |

### Out of Scope

- No REST migration.
- No policy schema-binding work.
- No records lifecycle rewrite.
- No component registry migration.

### Acceptance Criteria

- App-blind formula tests pass with the core profile only.
- CRM formula compatibility tests pass only when Salesforce profile/bundles/native evaluators are active.
- Bundle/profile inactive means lookup/evaluation/SQL emission fail closed.
- App-blind L0 evaluator and SQL emitter do not branch on Salesforce-only function names.
- SQL lowering behavior is supplied by registered emitter capability where needed.

---

# Execution Rules

## Keep the PR Count Low, But Do Not Mix Risk Classes

The merge plan is:

1. PR0 — thin seal
2. PR1 — REST/wire skin
3. PR2 — contract + query/search boundary
4. PR3 — security schema-binding
5. PR4 — lifecycle un-welding
6. PR5 — component registry convergence
7. PR6 — formula hard split

This is the maximum planned split. Do not explode it into 12–20 small PRs.

Only split a PR further if one of these is true:

- it changes a security decision path
- it changes persistence semantics
- it changes public API compatibility in a way that cannot be golden-tested inside the PR
- it cannot be reverted as a coherent unit

## Use Shims, But Ledger Them

Temporary compatibility shims are allowed when they reduce risk, but every shim must have:

- owner
- allowed package path
- removal PR
- golden coverage
- CI guard preventing new callers

## Distinguish Fixture Gates from Product Gates

Use the issue-tracker conformance fixture as the genericity proof where
practical. It is deliberately test-only and must not acquire a production API,
service, frontend, deployment, or release obligation merely to strengthen a
test.

Use an independently owned and operated non-CRM App for production multi-App
proof. Until one exists, report that evidence gap explicitly instead of
promoting the fixture or inventing a toy product.

## Do Not Move Enforcement Behind Fail-Open Hooks

The following remain L0 safety floors:

- authorization
- object/field/row access enforcement
- transaction atomicity
- persistence integrity
- audit/outbox durability
- pack ownership/drift consistency
- missing-registration fail-closed behavior

Products may register profiles, bindings, handlers, and projections. They may not bypass these floors.

---

# Definition of Done

The program is complete when all of the following are true:

1. App-blind L0 has no ownership of Salesforce public REST routes.
2. `/services/data` route ownership is visible only through product/dialect composition.
3. `shared/pkg/contract` no longer exposes Salesforce Tooling DTOs as neutral facts.
4. L0 query/search execution receives neutral IR, not SOQL/SOSL text.
5. Policy/FLS/RLS/sharing decisions are made by one L0 evaluator over registered schema bindings.
6. Records lifecycle is un-welded from Salesforce order/objectName assumptions, with L0 safety floors enforced independently.
7. Product component kinds can be added without editing app-blind L0 source.
8. Formula declaration, evaluator, SQL-emitter, and language-profile ownership are separated.
9. App-blind L0 formula runtime does not hardcode Salesforce-only function-name semantics.
10. CRM/Salesforce compatibility remains externally intact through dialect/app-pack/profile registration.
11. The issue-tracker conformance fixture proves the same neutral L0 mechanisms
    do not require Salesforce semantics; a real non-CRM App separately proves
    production multi-App operation.
12. Missing registration fails closed everywhere.

---

# Final Summary

The final plan is intentionally aggressive but controlled:

- **Aggressive:** move the visible Salesforce REST/wire skin first, then contract/query, then security, lifecycle, and component registries.
- **Controlled:** security is standalone, formula is last, CI seals prevent backflow, and compatibility goldens protect public API behavior.
- **Accurate:** L0 keeps the generic engines; dialects/app packs/profiles own Salesforce spelling, grammar, wire shape, vendor functions, and product vocabulary.

The target is a clean L0 that is still strong:

> metadata + records + policy + query + formula VM + flow runtime + lifecycle engine + pack/drift + connectors remain in L0; Salesforce compatibility becomes a first-class registered product/dialect layer rather than a hidden assumption in app-blind code.
