# L0 Repo Decomposition — Split-First Plan

**Status:** repo-first split execution plan (supersedes the earlier ports-first sequencing in this doc's history).
**Baseline:** sizing and package notes below come from current-tree inspection
around `origin/main` `594e06216` (2026-06-24). Historical A1/A2 manifest/guard
artifacts were removed after they proved too heavyweight for repo-first
execution; live code and current-state design docs are the authority.
**Scope:** L0 platform code in `backend/internal`, `backend/pkg`, `shared/pkg`.
Any relocated production code that temporarily remains in this repo, including
root `nexuscrm-*` modules or `modules/**` extraction scaffolds, enters this
scope as soon as a relocation PR creates it. Excludes `backend/distributions`,
frontend, app-packs, and command wrappers except as public distribution/build
evidence.

> **Purpose — split first, fix later (先拆后修).** Cut each distinct platform
> concern into its own repo *now*, tolerating a rough public protocol, then
> narrow that protocol and slim the code *inside* each repo afterward. The repo
> split is what unlocks parallel, independent development; perfecting the
> boundary is follow-up work that no longer blocks anyone.
>
> This is not "move anything anywhere." A split-first PR is allowed only when
> the dependency direction is already downhill or the prerequisite port named
> below has landed. The current-tree consumer map remains binding; stale
> analysis artifacts are context, not authority.

---

## 1. Strategy: split first, fix later

The earlier plan gated every move on inverting kernel ports first. This version
inverts that, because the dependency *direction* — not the mere existence of
coupling — decides whether a split is safe.

- **Downhill modules (the kernel does NOT import them).** Verified by the import
  graph: kernel-owned files importing `interfaces`, `middleware`,
  `serverruntime`, or `connector` = 0. The direction is already correct
  (repo → kernel). These can be split to external repos *now*. Because a real
  external repo cannot legally import `backend/internal`, roughness is allowed in
  the public protocol shape, not in Go visibility: the split PR may introduce a
  coarse public contract/compat port and narrow it later, but it must not leave
  the extracted repo depending on `backend/internal`. **Split first; narrow the
  protocol inside afterward.**
- **The one uphill exception: automation.** The kernel record-save path calls
  automation's before/after hooks (kernel → automation). Splitting it before
  inverting that single `AutomationHook` port would create a kernel→extension
  dependency that cannot compile. So automation is split *after* its one inbound
  hook port is inverted. This is physics, not caution.
- **Shared neutral primitives are not split at all.** The expression/formula VM,
  the events/audit/outbox primitive, files/blob, and the generic query/rollup
  engine are depended on by many kernel paths. They **fold into the kernel**;
  only their Salesforce-shaped parts go to a dialect.

Boundary rule unchanged: **mechanism stays generic in L0; product and vendor
shape move to the dialect.**

The three destinations for every piece of code:

1. **Its own repo now** — a distinct platform concern, its own lifecycle, large
   (≳10k LOC), and either downhill or worth its one inbound port. A root
   `nexuscrm-*` module is the preferred in-repo staging shape once the module
   path has been proven: consumers use the final import path through local
   replaces, CI runs the module directly, and the later external repo move does
   not rewrite consumers. The split PR is not complete until the external repo,
   module path, version/release story, downstream module resolution, Docker, CI,
   and public-core smoke evidence all work against the real repo boundary.
2. **Fold into the kernel** — broadly-shared neutral primitives too small or too
   central to justify a repo (splitting them would buy fragmentation + uphill
   ports for little gain).
3. **Fold into a dialect (`app-packs`)** — any Salesforce-shaped part of any
   module.

---

## 2. End state — topology

```
   repos that depend ONLY on the contract
   ┌──────────────────────────────────────────────────────────────────────┐
   │  L0 concern repos          L1 product repos          frontend         │
   │  • connectors              • nexuscrm-app-packs       • @nexuscrm/     │
   │  • automation                (sf-crm · -soql ·          host-sdk       │
   │  • controlplane               issue-tracker fixture)  • app frontends  │
   │  • apihost                 • nexuscrm-pingcap-pack                     │
   └───────────────────────────────────┬──────────────────────────────────┘
                                        │  every box depends only on ↓
                          ┌─────────────┴──────────────┐
                          │      nexuscrm-contract       │  dependency root
                          │  neutral interface, imports  │  (imports nothing)
                          │  nothing                     │
                          └─────────────┬───────────────┘
                                        ▲  implements
                          ┌─────────────┴───────────────┐
                          │        nexuscrm-kernel        │
                          │  records · metadata · policy  │
                          │  · query · persistence        │
                          │  + folded primitives:         │
                          │    formula-VM · events/audit/ │
                          │    outbox · files · analytics │
                          └──────────────────────────────┘

   invariant: no box imports another · kernel implements contract ·
              the SF-shaped part of any box → app-packs dialect
```

This is one repo per distinct concern, **not** one per package. `formula`,
`observability`, `files`, and `analytics` are deliberately *not* first-wave
repos: their neutral cores fold into the kernel, their SF parts into a dialect.

---

## 3. Size baseline

Measured with `find … -name '*.go' ! -name '*_test.go' | xargs wc -l`:

| Area | Non-test Go LOC | Test Go LOC | Non-test files |
|---|---:|---:|---:|
| `backend/internal` | 259,879 | 305,204 | 823 |
| `backend/pkg` | 17,023 | 15,680 | 59 |
| `shared/pkg` | 19,726 | 6,082 | 91 |
| **Current L0 scope** | **296,628** | **326,966** | **973** |

Earlier feature-affinity LOC tags over-counted some repos (api-host around 23k
tagged but only ~13k real host; connectors around 32k tagged but only ~17k real
connector core). Judge by producer-vs-consumer + package atomicity, not raw tag
size.

---

## 4. The repos — contents and prerequisite gate

### Spine

**`nexuscrm-contract`** — the dependency root.
- **Contents:** the neutral capability interfaces and DTOs (today
  `shared/pkg/contract` + `shared/pkg/queryspec` + the neutral subset of
  `shared/pkg/models`).
- **Prerequisite gate:** extract a clean module from `shared` — its closure must
  drop the `database/sql`-carrying models and the Salesforce-literal constants;
  contract may import nothing from `backend/internal`.

**`nexuscrm-kernel`** — the small, strong core.
- **Contents:** records DML · metadata store/describe · policy (FLS/RLS/sharing)
  decision · query execution + `queryspec` · persistence primitives (DDL, txn,
  record repo) · `domain/ports` · errors. **Plus folded shared primitives:** the
  neutral expression/formula VM, events/audit/outbox, files/blob, and the generic
  query/rollup engine.
- **Prerequisite gate:** none to *be* the kernel; the gate is on what *leaves* it
  — as each module splits, its SF-shaped part is pushed to a dialect (SF function
  catalog, `<Object>History`, report types, v63 wire, SF component handlers).

### L0 concern repos

**`nexuscrm-connectors`** — external integration (~17k).
- **Contents:** `features/connector/*` (callout · inbound webhooks · capabilities
  · events · inboundrouting · syncstate · throttle · ingressreceipts) +
  `application/connectors` + `features/namedcredential` + `features/webhooks`.
- **Prerequisite gate:** none to split (downhill — kernel-inbound = 0). The repo
  now builds as `github.com/pingcap-inc/nexuscrm-connectors`; connector runtime
  code must not import `github.com/nexuscrm/backend/internal/**`. The split keeps
  automation-push / packs-handler / bootstrap / contract files that were
  historically mis-tagged "connectors" in their own backend packages.
- **Distribution gate:** every main module that builds through `GOWORK=off`
  (`backend/distributions`, conformance modules, Docker build stages) must
  resolve the external module explicitly, and CI must run build/vet/test for the
  extracted repo itself. Any current boundary test or scan that covered the old
  code path must keep covering the moved code path; do not add a generic
  manifest/guard layer just to model the move.

**`nexuscrm-apihost`** — serving surface (~13k real).
- **Contents:** `interfaces/rest` (v63 handlers) · `interfaces/middleware` ·
  `interfaces/dialect` transport binder · OAuth HTTP shell · the route-mount half
  of `serverruntime`.
- **Prerequisite gate:** re-tag the ~10k mis-tagged kernel producers
  (`application/services`, persistence repos, serverruntime composition) back to
  kernel; the already-contract-covered REST handlers move with a
  `ServiceManager` → `contract.*` field swap, but the connector-webhook and OAuth
  surfaces need their contract methods defined first.

**`nexuscrm-controlplane`** — management plane.
- **Contents:** the generic pack install/apply/drift/verify engine spine
  (`controlplane/packs` spine + `operations` + `source_pack_ops` +
  `installed_packs` + `verify_l0`) + the setup/bootstrap engine.
- **Prerequisite gate:** de-mix first — push the ~50 SF per-kind component
  handlers to the dialect, keep the `system_tables` metamodel in the kernel, and
  narrow the `ApplyDeps` service-locator to a `Store/Txn` port. Then split the
  generic engine.

**`nexuscrm-automation`** — behavior engine (~37k). **Uphill.**
- **Contents:** flow/orchestration executor · approval runtime · trigger
  subscription · autonumber · connector-push runtime.
- **Prerequisite gate:** invert the `AutomationHook` port (the kernel record-save
  path must call automation through a registered hook, not a concrete package) —
  this inbound port must exist before the split will compile. Also split the
  approval sub-package out of the `automation` package, and push the SF
  action/Flow projection to the dialect.

### Already separate (L1 + frontend)

`nexuscrm-app-packs` (holds dialect-sf-crm, dialect-sf-crm-soql,
salesforce-crm-standard, and the sealed CI-only selected-root conformance
fixture), `nexuscrm-pingcap-pack`, and `@nexuscrm/host-sdk`. P1x removed every
release-shaped path for the conformance fixture.

---

## 5. Graduation order

Downhill and cheap first; the de-mix one next; the uphill one last.

1. **`contract`** — extract the clean repo from `shared`. The foundation;
   nothing else can have a clean dependency closure without it.
2. **`connectors` ∥ `apihost`** — both downhill (kernel does not import them).
   Run in parallel; each only needs its kernel calls routed through the contract.
   `connectors` is in flight (PR #3084).
3. **`controlplane`** — after its de-mix (SF handlers → dialect, substrate →
   kernel, `ApplyDeps` → `Store/Txn` port).
4. **`automation`** — last, after the `AutomationHook` port is inverted (its one
   uphill edge).

Kernel folding (keep the neutral part, push the SF part) is **not** a separate
step — it happens per file as each module above splits.

Per repo, the discipline is always: **split first (rough) → then narrow the
protocol and slim inside.**

---

## 6. Non-negotiable PR gates

Every relocation PR must prove these before merge:

1. **Current-tree evidence moves with code.** Consumer rewrites, package-local
   tests, affected CI/Docker paths, and current-state design docs move in the
   same PR as the code. Frozen analysis artifacts are not revived or rewritten
   to chase relocations.
2. **Go module resolution is explicit.** If `backend` starts requiring an
   external module, every downstream main module that consumes `backend` with
   `GOWORK=off` must have a working resolution path. Local `replace` directives
   are allowed only for workspace development; they do not propagate through
   dependencies and cannot be the released contract.
3. **Build artifacts know the new repo.** Dockerfiles and release scripts must
   fetch/copy the external module's `go.mod`/`go.sum` before dependency download
   and its source before build. CI must run the extracted repo's package-local
   tests directly so moving a package cannot drop coverage.
4. **Public-core proof stays green.** The current `setup apply`, CRM
   `install-pack`/`verify-pack`, readiness verify, API host, and documented
   greenfield smoke path must keep working. A replacement command path must be
   documented and re-proven before the old path is removed.
5. **No second writer or second read path.** Rough extension→kernel imports are
   tolerated only as transitional consumers. Extension modules must not become
   independent authorities for metadata, records, policy, query, or transaction
   state; direct SQL/DB ownership moves only behind the named `Store/Txn` or
   equivalent single-writer port.
6. **Living docs stay coherent.** If a move changes an SSoT path named by a
   current-state design doc, update that doc in the same PR. Historical
   inventories and frozen analysis files are not current read paths.

---

## 7. Useful scaffolding already in place + progress

- **Current-tree consumer maps.** Derive them from imports and package users in
  the live tree before each move. Do not reuse stale CSV inventories as a merge
  gate.
- **External connector split.** The connector core now lives under
  `nexuscrm-connectors/**`; `backend/internal/modules/connectors/**` was only
  extraction scaffolding and is no longer the current read path.
- **Existing runtime gates.** `GOWORK=off` distribution/conformance builds,
  Docker smoke, and targeted boundary tests are the real guardrails for a split.
  Extend those only when the moved code actually touches their surface.

Progress:

- ✅ WorkflowAlert migration tool → `dialect-sf-crm` (#3081).
- ✅ History-SOQL → `dialect-sf-crm` (#3083).
- ✅ Flow Tooling PATCH parsing → `dialect-sf-crm` (#3086).
- ✅ `connectors` core → internal-fenced extraction scaffold (#3084). It is
  consumer-map evidence; it is not the accepted split until the repo boundary is
  real.

---

## 8. Acceptance invariants (every split must satisfy)

1. The split repo depends on the rest of the platform only through
   `nexuscrm-contract` or a deliberately coarse public compat port owned by the
   kernel. A temporary in-repo mirror must stay behind `backend/internal/**` and
   is scaffolding, not the accepted split.
2. The kernel builds and tests without the split repo except through the
   contract.
3. Missing registration fails closed.
4. One authoritative read path for records, policy, metadata, and transaction
   state — no second source of truth is created by the split.
5. Public-core greenfield self-host + verify, and the CRM distribution's external
   behavior, remain reproducible (golden tests) before any old path is removed.
6. Every moved repo has direct CI coverage, Docker/release resolution, and
   public-core boundary scan coverage.
7. A completed split deletes or empties the old implementation path. A port-only
   or protocol-only PR may hold flat, but it must name the follow-up move/delete
   PR that realizes the reduction.
