Principle: One fact, one owner, one source of truth, one read path: L0 owns generic engines; L1 owns metadata assembly.

# CLAUDE.md - NexusCRM Agent Guide

## Repository Shape

NexusCRM is a Salesforce-compatible CRM platform with a Go backend, React
frontend, and TiDB runtime target. This repository is a public-core baseline:
greenfield startup must not depend on tenant-specific metadata, data, overlays,
or product/legal-entity names.

Use `README.md`, `go.mod`, and `package.json` to discover runnable commands.
Keep this file focused on rules that are easy to miss from code structure alone.

---

## Architecture: Four Layers

NexusCRM is metadata-driven. Platform code is an interpreter, not a place to
author tenant business behavior.

The long-term target is an application-neutral `L0` that does not predefine any
`L1` schema, metadata, lifecycle, UI, or reporting facts. Salesforce-compatible
CRM is the first `L1` app pack; Jira-like, ITSM, ERP, or other apps must be
expressible on the same `L0` substrate through their own packs.

> **Ownership decisions: cite the standard.** For any "does this belong in `L0`
> or `L1`?" question, [`docs/design/l0-l1-standard.md`](docs/design/l0-l1-standard.md)
> is authoritative. It gives the positive definition (L0 = the interpreter:
> metamodel + engines + seams), the ordered ownership tests (T0 standing → T5
> asymmetric default), and reproducible calibration anchors. On ownership
> questions it supersedes the prose in this section and the three disqualifiers
> of issue #2229; the material below stays as the day-to-day quick reference.

- **`L0`** platform code: generic engines/interpreters. Owns metadata storage,
  schema/runtime interpreters, formula/flow/validation engines, security
  primitives, CRUD/query APIs, import/apply tooling, and drift checks.
- **`L1`** application/distribution metadata packs: installable product shape
  such as the Salesforce-compatible CRM pack. Standard objects, fields,
  layouts, picklists, flows, and validation rules belong here when they are
  product defaults, not tenant decisions.
- **`L2`** org/tenant metadata: admin-editable configuration, custom objects,
  custom fields, tenant overlays, and tenant business policy.
- **`L3`** record data: user-created business rows.

Every fact belongs in one layer, has one owner, and is read through one
authoritative path.

The four commitments:

1. **Author decides layer.** Engineer-authored facts live in `L0` or `L1`;
   admin-authored facts live in `L2`; user-authored facts live in `L3`.
2. **`L0` is blind to `L2` instances.** Generic runtime may inspect schema
   shape, but must not encode specific `__c` fields, profile literals, picklist
   values, tenant tables, RecordType DeveloperNames, or ApprovalProcess names.
3. **One fact, one SSoT, one read path.** If Salesforce API `v63.0` already
   models a fact, reuse that representation instead of inventing a substitute.
4. **Exhaust reuse before inventing.** Use this order: Salesforce primitive ->
   existing platform primitive -> generic extension of an existing primitive ->
   new primitive.

If a business workflow is hard to express through existing metadata, treat
that as a generic substrate gap. Extend the metadata/runtime substrate; do not
implement the workflow as a one-off Go or TypeScript branch.

### Decision tests for any new fact

- **Multi-app:** would the same `L0` contract host both the CRM pack and a
  Jira-like pack without changing platform code? If no, the contract is still
  application-shaped and belongs in `L1` or in a transitional adapter.
- **Greenfield:** would this work in a brand-new org with no tenant metadata?
- **Admin self-service:** can an admin change this rule without a code change?
- **Two-path consistency:** does every caller read this fact through the same
  helper / source of truth?

### Strong vs transitional reading

The strong reading is the target architecture: the platform binary ships only
generic engines and substrate. Salesforce-shaped objects, fields, flows,
validation rules, and picklists live in external `L1` app packs; tenant/org
overlays and business policy live in `L2` config applied through runtime/admin
APIs. CRM vocabulary (`Lead`, `Account`, `Opportunity`, `Order`, `Contract`,
`Campaign`, …) is application scope, not platform scope. The same rule
applies to every future app pack: Jira-like concepts (`Issue`, `Project`,
`Sprint`, boards, workflow states) are `L1` facts and must not be baked into
platform code.

The repo is in transition. A PR may move behavior out of `L0` hardcoded
branches into `L1` metadata without solving the full extraction of standard
shape from the binary, but it must name the remaining boundary work: which
metadata still lives in the platform binary, why it is temporarily accepted,
and the future extraction path.

Concrete examples: PR #22 (centralizing `Order` activation in Go helpers) is
unacceptable under any reading — `L0` still knows about `Order`, activation
statuses, Contract status/date policy, and Order-specific field writes. PR #28
(moving the same logic to standard validation rules + a before-save Flow)
fixes the `L0` hardcoding but still places CRM facts in platform-binary
`standard_*` metadata, unacceptable under the strong reading — those rules
belong in an external `L1` CRM pack or `L2` tenant/org config.

Validation rules, flows, and standard-field semantics that encode tenant
business policy (status allowlists, contract-window checks, customer-specific
error messages, …) are `L2` tenant/org metadata, not platform-binary `L1` —
even if they currently live under `standard_*` files.

Standard-field stamping and lifecycle semantics qualify as `L1` only when
confirmed Salesforce-standard product behavior; otherwise they are `L2`
tenant/org metadata.

Before changing a boundary, registry, compatibility surface, or consumer
path, refresh the **current-tree real consumer map**. Issue text, old review
comments, stale design docs, and historical file paths are supporting
context, not authority. If the named path no longer exists in the current
tree, re-derive the plan from live consumers and current read/write paths.

### Registry / declaration discipline

Registries, declarations, and runtime caches are high-risk because they can
silently become a second source of truth.

1. A new registry must not take over live metadata shape authority unless
   that authority move is explicit, complete, and all old read paths are
   removed in the same PR.
2. Object, relationship, and shape facts must have one authoritative
   authoring surface.
3. Runtime registries may cache or declare runtime-consumed facts, but must
   not silently compete with live metadata, MetadataService, or another
   authoring surface for the same fact.
4. Conflicts between declaration surfaces and live metadata must fail closed.
   "Emit a warning and continue" is not an acceptable steady state.

For any new registry, state: what fact it owns, which surface authors that
fact, which callers read it, what old read path is removed, how conflicts
fail closed.

---

## Public-Core Boundary

Do not add tenant-specific payload to this public repo. That includes:

- concrete tenant `__c` fields, custom objects, layouts, flows, validation
  rules, approval processes, RecordTypes, or picklist/status values
- tenant product, channel, customer, or legal-entity names; tenant emails or
  user identities embedded in pack data
- tenant-specific SQL tables or bootstrap rows
- runtime branches that make greenfield behavior depend on tenant data

Generic mechanisms are allowed but their payload is not. For example,
`backend/internal/controlplane/tenantoverlay` may exist as machinery, but
this repo must not check in tenant overlay payload or require one for
startup. `controlplane/packs` and `controlplane/packspec` likewise
distinguish mechanism (parse/validate/apply/version/reconcile/drift code —
substrate) from payload (specific object/field/RT/flow JSON — application
pack content).

Historical extraction docs may mention tenant context as evidence; runtime
code, bootstrap defaults, and public-core behavior must not depend on that
context.

Historical cleanup must not become permanent runtime logic. Clean obsolete
rows, columns, or tenant residue through an explicit one-time
migration/runbook or by removing the public-core artifact; do not add
generic-engine special cases that only exist to repair a past repository
state.

---

## Coding Red Lines

Scope: backend platform code (`backend/internal/**`), frontend runtime code
(`frontend/src/**`), and L1 bootstrap/default metadata. Tests and fixtures
are exempt, but examples should still avoid tenant-shaped names unless they
are teaching generic suffix behavior.

Block these patterns:

- concrete `__c` field/object names in platform Go/TS or L1 bootstrap
- hardcoded ProfileId/Profile-name behavior
- hardcoded RecordType DeveloperName or ApprovalProcess DeveloperName
  behavior
- business enum/status/picklist branches such as lifecycle stage checks
- tenant table SQL such as `FROM some_object__c`
- field read/write allowlists in code; use FieldPermissions and
  ValidationRule metadata
- SObject-name branches inside generic engines (records, automation,
  permissions, RLS, seeding)
- parallel substitutes for Salesforce-modeled facts such as `SharingModel`,
  `RecordType`, `FieldPermissions`, or object/field shape facts
- multiple read paths for the same `(user, object, record)` decision
- feature modules owning derived SObject-shape registries that should live
  in metadata service helpers

Allowed in platform code as generic substrate (not CRM-specific app shape):

- generic infrastructure fields (`RecordTypeId`, `OwnerId`, `CreatedDate`)
  and `_System_*` infrastructure tables/fields
- standard permission keys (`ModifyAllData`, `ViewSetup`, …) not tied to a
  CRM application object
- generic suffix handling for `__c`, `__r`, custom objects, and custom
  fields
- security hardening lists for secrets/tokens/password-like fields

Useful scan:

```bash
rg -n '"[A-Za-z0-9_]+__c"|FROM\s+[A-Za-z0-9_]+__c|ProfileId\s*[=!]=|Profile\s*[=!]=|DeveloperName\s*[=!]=' backend frontend shared \
  --glob '!**/*_test.go' \
  --glob '!frontend/node_modules/**' \
  --glob '!frontend/dist/**' \
  --glob '!frontend/package-lock.json'
```

Non-test hits need a clear generic-platform justification or removal.

---

## Salesforce Boundary

Salesforce API `v63.0` is the external product oracle. Frontend API usage is
a strong signal for the public contract; backend changes should move toward
that contract, not shrink the frontend to fit a backend gap.

NexusCRM may differ from Salesforce, but deltas must be explicit, justified,
metadata-driven, and verifiable. Visual presentation may differ more freely
than API shape, object/field/permission semantics, and admin authoring
model.

**Contract verification is mandatory.** Before changing any handler under
`backend/internal/interfaces/rest/` or any UI API endpoint, follow the
checklist in [`docs/design/salesforce-v63-api-contract.md`](docs/design/salesforce-v63-api-contract.md).
That document records the binding wire-shape rules, the legacy → v63
error-code mapping, and the catalog of known pitfalls (FLS leak in batch
record access, queryMore pagination, folder-shares envelope, formula
validator engine, and others). PRs that break v63 parity will be Codex-blocked.

### Formula context SSoT (session-bound, refreshed only on re-login)

`$User`, `$Profile`, and `$UserRole` formula globals — and the underlying
ProfileId / ProfileName / UserRoleID / RoleName fields on every
`UserSession` — are bound to the JWT minted at login. The session is the
SSoT for these facts; nothing in the request lifecycle re-reads them from
the database. This matches Salesforce v63's session-bound semantics for
`$Profile` / `$UserRole` and guarantees that validation rules, flow
formulas, approval criteria, and action expressions all see byte-identical
merge-field values within a single save transaction.

**Construction (read path):**
`backend/pkg/formula.BuildFormulaUserContext(*UserSession)` is the single
read path for formula `__User` / `__Profile` / `__UserRole` projections.
All in-tree consumers (validation rules in `records/persistence_service.go`,
flow formulas in `automation/executor_helpers.go`, approval criteria in
`automation/approval_service.go`, action expressions in
`automation/action_service.go`) call this helper. New formula consumers MUST
use it. DB lookups inside formula context construction are forbidden — they
would create an unannotated stricter-than-Salesforce delta.

**Invalidation (write path):**
Admin writes that mutate session-bound facts trigger session revocation in
`records/persistence_service.go:Update`. The next request from the affected
user fails JWT validation (revoked session) → user re-logs in → new JWT
carries fresh, atomically-consistent IDs and names.

| Write event | Revocation hook | API |
|-------------|-----------------|-----|
| `User.ProfileId` or `User.UserRoleID` change (reassignment) | per-user | `SessionRepository.RevokeAllSessionsForUser(userID)` |
| `Profile.Name` change (rename) | per-profile (batch JOIN with User) | `RevokeAllSessionsByProfileID(profileID)` |
| `UserRole.Name` change (rename) | per-role (batch JOIN with User) | `RevokeAllSessionsByUserRoleID(roleID)` |

**No mid-session DB refresh.**
`PermissionLoader` middleware does NOT refresh ProfileId/UserRoleID/Names
from the database mid-session. It loads system+custom permissions cached
by `(userID, JWT-baked profileID)` only. The previous "PermissionLoader
refreshes ID per request from DB" model has been removed in favor of the
session-revoke-on-write mechanism above, because partial mid-session
refresh (ID without Name, or one role's ID without the other's Name) lets
formula globals observe inconsistent facts within a single save.

**If a future feature genuinely requires live-DB Profile/Role data inside
a formula context:** that is a new, explicit Salesforce-Boundary delta —
document it here and add a separate construction surface. Do NOT modify
`BuildFormulaUserContext` to do DB reads, and do NOT re-introduce
mid-session refresh of session-bound IDs in middleware.

---

## Script Layer

NexusCRM does not clone Apex. Its script/automation surface is metadata-first:
Flow metadata, Formula/Expression evaluation, and Go-backed built-in
functions or invocable actions. Treat Go-backed actions as generic platform
capabilities with metadata authoring, permissions, lifecycle, and
observability; do not add tenant business logic as hardcoded Go branches.

Per-tenant Go packages or checked-in Go scripts are not the public-core
extension model. Tenant behavior should be metadata that invokes generic
platform actions/functions.

When comparing Apex parity, classify a surface as implemented, generic
extension path, unsupported gap, or out of target. Do not treat "not
Apex-compatible" as a finding by itself, and do not treat "possible to
implement in Go" as already implemented.

---

## Refactor Discipline

Use higher discipline when a change flips a source of truth, stages a larger
refactor, combines backfill with destructive migration, or touches shared
state read by permissions, metadata, records, automation, query/RLS, or
seeding.

Acceptable shapes:

- remove the wrong-shape piece and track the prerequisite, or
- land the new mechanism, switch all read paths, remove the old mechanism,
  and verify the cutover in one PR.

"Land now, fix the shape later" is not an acceptable shape.

---

## Working Standard

- Read the relevant code before designing the change.
- Prefer the existing local pattern over a new abstraction.
- Keep changes narrow and tied to the request.
- Do not revert user changes in a dirty worktree.
- Verify with evidence. Baseline commands are usually:

```bash
(cd backend && go test ./...)
(cd shared && go test ./...)
(cd frontend && npm run build)
git diff --check
```

`npm run build` rewrites `frontend/public/version.json`; revert that
generated change unless the version file is intentionally part of the task.
