# P7a: Independent Runtime metadata closure

> **Status:** current-tree inventory and build/cutover specification. This is
> not a RuntimeMetadataView implementation and does not enable serving.
>
> **Evidence revision:** NexusCRM
> `41061972fa7796e1d763f9e1737bb500fd39f3b8`.
> **Alignment revision:** rebased onto NexusCRM main
> `92702c5cec8fdb751994dfd1023403120806bdf6`; the inventoried production Go
> paths were byte-identical across those historical revisions.
> **V1a extension:** the v2 machine companion and current text evolve that
> candidate closure with the C0 record-access operation; the two revisions
> above remain the original P7a evidence anchors, not claims about the evolved
> source tree.
>
> **Machine-readable companion:**
> [`runtime-metadata-closure-p7a.json`](runtime-metadata-closure-p7a.json).

## 1. Decision and scope

P7a inventories the metadata closure reachable from the independent Runtime
composition that is being prepared for cutover. In the current tree that means:

```text
runtimeapi.AssembleProduction
  -> runtimeassembly.NewRecordsReadRuntime
  -> runtimeapi.RecordsServer
       -> ReadSelectedRoot -> ContractService.ReadSelectedRecord
       -> EvaluateRecordAccess -> ContractService.EvaluateRecordAccess
  -> PersistenceService
       -> QueryService.ReadSelectedRootProjection -> QueryRepository.Find
       -> ResolveObjectTypes/RetrieveManyRaw -> RecordRepository
```

The executable `runtime-api` command is still deliberately non-serving: it
calls `AssembleNonServingBootstrap`, not `AssembleProduction`
([`backend/cmd/runtime-api/main.go:23-33`](../../../backend/cmd/runtime-api/main.go#L23-L33)).
`AssembleProduction` is nevertheless the exact candidate graph that opens the
Runtime database, prepares the read-only Records graph, and passes its real Records
adapter to the gRPC process
([`backend/internal/runtimeapi/process.go:34-55`](../../../backend/internal/runtimeapi/process.go#L34-L55)).

“All Runtime metadata consumers” means every consumer reachable from this
candidate independent process closure. It does **not** mean migrating every
metadata consumer in the legacy monolith. A later V1/V2 capability slice removes
the corresponding monolith path only after its ownership flip.

This inventory observes current behavior. The P7b and P7c/S1 sections specify
future gates; they do not claim that those gates exist today.

C0 subsequently selected
`POST /services/data/:version/ui-api/record-access` as the first production CRM
slice. That route needs a separate coarse capability and is not yet reachable
from this independent Runtime composition. Its unknown-object branch currently
resolves types through the Records repository, not key-prefix metadata. The
manifest therefore records two different facts: selected-root is the current
independent capability; record access is the first production capability
selected for a later V1 slice. P7a does not pretend that future code is already
in the current closure or move an L3 lookup into Catalog.

The distinction is executable in the current tree: `ObjectTypesForIDs`
delegates to `PersistenceService.ResolveObjectTypes`
([`nexuscrm-records/contract_adapter.go:115-117`](../../../nexuscrm-records/contract_adapter.go#L115-L117)),
which calls `RecordRepository.FindObjectTypesForIDs` once
([`nexuscrm-records/persistence_bulk_ops.go:173-188`](../../../nexuscrm-records/persistence_bulk_ops.go#L173-L188)).
It does not read `MetadataService.ResolveObjectForRecordID`.

## 2. Composition closure

The candidate graph has five repository ports and one shared SQL database
handle/pool:

| Dependency | Concrete implementation | Current purpose |
|---|---|---|
| `MetadataRepository` | `persistence.MetadataRepository` | schema/field/relationship cache build, broad ancillary cache refresh, sharing-rule fallback |
| `PermissionRepository` | `persistence.PermissionRepository` | permission catalog decoration, CRUD/FLS grants, RLS inputs, roles/groups/shares |
| `UserRepository` | `persistence.UserRepository` | role projection and hierarchy-derived user sets |
| `QueryRepository` | `persistence.QueryRepository` | the one projected record query with SQL RLS |
| `RecordRepository` | `persistence.RecordRepository` | record-access type resolution and bounded root-row hydration |

The exact constructor is
[`backend/internal/runtimeassembly/records_read.go`](../../../backend/internal/runtimeassembly/records_read.go).
It builds one process-global `MetadataService`, one `PermissionService`, a
formula-view resolver, `QueryService`, `PersistenceService`, and the neutral
Records adapter
([`backend/internal/runtimeassembly/records_read.go`](../../../backend/internal/runtimeassembly/records_read.go)).
The graph starts no EventBus listener or worker, but it does construct two inert
read dependencies: list-view RLS wiring and `ValidationService`.

`Prepare` calls the broad `MetadataService.RefreshCache` and then refreshes role
hierarchy
([`backend/internal/runtimeassembly/records_read.go`](../../../backend/internal/runtimeassembly/records_read.go)).
The machine-readable guard pins every constructor call and fails when this root
changes without an inventory update.

## 3. Current request flow

### 3.1 Admission and receipts

P6 security admission remains a separate authority. Before the Records handler
runs it verifies workload and delegation, reads Runtime security state, validates
the durable session/AuthEpoch, and installs `TrustedCall`. These reads are
security-authoritative; they are not ordinary Catalog metadata and must not be
copied into `CatalogSnapshot`.

The Records handler then independently invokes `ReceiptSource.Receipts` before
the engine
([`backend/internal/runtimeapi/records_server.go:53-79`](../../../backend/internal/runtimeapi/records_server.go#L53-L79)).
No current production binding couples those receipts to the schema and policy
inputs used below. `BootstrapReceipts` contains only protocol and capability
versions and belongs to the non-serving skeleton
([`backend/internal/runtimeapi/records_server.go:21-37`](../../../backend/internal/runtimeapi/records_server.go#L21-L37)).

### 3.2 Root schema, CRUD, and FLS

Selected-root obtains one root schema through `prepareOperation`, performs
object Read, normalizes requested fields, then evaluates field read protection
and FLS before calling the projected query
([`nexuscrm-records/selected_root_read.go:80-132`](../../../nexuscrm-records/selected_root_read.go#L80-L132)).

The root schema pointer is carried into query planning and typed projection,
which is the correct local shape. The problem is that permission helpers do not
consume only that pointer. They repeatedly call their injected metadata provider
for the object, fields, parents, and target objects. The provider is the
process-global `MetadataService`, so each helper can load whichever atomic
pointer is current at that moment.

Object authorization consumes all of the following schema facts:

- `RequiredReadPermission` and setup/security gates;
- system versus ordinary table classification;
- derived `AccessPolicy`, including object-permission recursion;
- sharing model and parent-controlled relationships.

Those reads and the database fallback for effective object grants are visible
in [`nexuscrm-permissions/service.go:476-610`](../../../nexuscrm-permissions/service.go#L476-L610).
FLS likewise combines metadata-declared read protection, derived field policy,
effective field grants, and the existing absent-FLS-row fallback to object Read.

### 3.3 RLS and query projection

`ReadSelectedRootProjection` compiles query-level RLS, executes one projected
repository query, and hydrates storage values through the same root schema
([`nexuscrm-analytics/selected_root_projection.go:24-98`](../../../nexuscrm-analytics/selected_root_projection.go#L24-L98)).

The RLS metadata closure is not necessarily one object:

- parent-controlled objects recursively read the sharing-parent relationship
  and every parent schema;
- `targetRecordRead` policy scans all schemas and recursively builds eligible
  target RLS
  ([`nexuscrm-permissions/rls_filter.go:348-428`](../../../nexuscrm-permissions/rls_filter.go#L348-L428));
- criteria sharing rules compile expressions against the effective schema;
- SQL read-source, physical-name, computed-field, and RLS-field routing derives
  from schema and relationships.

The permission half remains live security authority: effective system/object/
field permissions, group membership, manual shares, team membership, role
hierarchy, and subordinate users. It must be compatible with the admitted
`SecurityEpoch`; it must not be frozen into ordinary Catalog metadata.

The independent graph does not call `PermissionService.SetCache`, and the gRPC
admission path does not install the legacy HTTP request's access-input cache.
Consequently, effective object/field/system permissions and RLS inputs take
their repository fallbacks unless a narrower helper memoizes them. This is
current behavior to preserve semantically, not permission data to move into
CatalogSnapshot.

Two process-global caches are especially important:

1. sharing rules are cached for five minutes and fall back to
   `MetadataService.GetSharingRules`, which queries the metadata repository
   ([`nexuscrm-permissions/sharing_rules.go:17-42`](../../../nexuscrm-permissions/sharing_rules.go#L17-L42));
2. role hierarchy is cached for 60 seconds and may refresh with a
   `context.Background()` path
   ([`nexuscrm-permissions/role_hierarchy.go:11-76`](../../../nexuscrm-permissions/role_hierarchy.go#L11-L76)).

The first is Catalog metadata and must move into the bound view. The second is
security-authoritative and must remain on the SecurityEpoch side of the serving
barrier. Treating both as one generic metadata cache would merge two owners.

## 4. Current metadata cache and fallback model

`MetadataService` holds two atomic pointers: a full cache and a separately
published core cache for schema/field/relationship authorization facts
([`nexuscrm-metadata/metadata_service.go:95-145`](../../../nexuscrm-metadata/metadata_service.go#L95-L145)).
The effective core snapshot is built from all schemas, three registered schema
decorators, and all relationships
([`nexuscrm-metadata/metadata_service.go:332-379`](../../../nexuscrm-metadata/metadata_service.go#L332-L379)).

Its members do not all belong to the selected-root closure:

| Core snapshot member | Reachability from selected-root |
|---|---|
| `schemas` / `schemaMap` and fields carried by each schema | Required for root, parent, and target-record authorization and projection. |
| `relationshipsMap` | Required for sharing-parent resolution. |
| global `fieldMap` | Built, but not read; selected-root resolves fields from its carried schema. |
| key-prefix maps | Built and duplicate-checked, but selected-root does not call record-ID prefix resolution. C0 record-access uses a Records-repository type resolver instead. |
| `childRelationshipsMap` | Built, but child traversal is outside the root-only operation. |

The latter three are classified as outside the current independent closure.
C0 does not provide evidence for copying them: its object-type resolver calls
`RecordRepository.FindObjectTypesForIDs`, and record access has not yet been
composed into Runtime. A1b may add only a metadata delta proved by the future
coarse contract. Presence in the legacy cache is not evidence for copying any
of these indexes.

The decorators in the independent graph are:

| Decorator | Input | Effective output |
|---|---|---|
| Permission schema decorator | active system permission catalog | `PermissionSet.Permissions<X>` virtual fields |
| Compound virtual decorator | platform declarations plus physical fields | platform compound fields such as `User.Name` |
| Server-defaulted decorator | platform declarations plus field metadata | `DefaultedOnCreate` effective flags |

After the core snapshot is built, `RefreshCache` publishes it early and then
loads flows, validation rules, autonumbers, and identifier rules before the
full pointer is published
([`nexuscrm-metadata/metadata_service.go:384-499`](../../../nexuscrm-metadata/metadata_service.go#L384-L499)).
Selected-root consumes none of those four ancillary groups. The broad refresh
therefore creates startup work and failure coupling outside the capability's
actual read closure.

Most importantly, `GetSchema` does not treat a cache miss as authoritative
absence. It directly calls `GetSchemaByAPIName` and can return a raw schema that
did not pass the snapshot decorators or relationship build
([`nexuscrm-metadata/metadata_service.go:557-590`](../../../nexuscrm-metadata/metadata_service.go#L557-L590)).
That path can mix revisions and effective/raw schema shapes inside one request.
It is a P7c/S1 deletion edge, not a behavior to preserve in the new view.

### 4.1 Refresh, invalidation, and process-global state

| State or transition | Current behavior in the independent graph | P7 disposition |
|---|---|---|
| Metadata `cache` / `coreCache` | Process-global atomic pointers; `coreCache` is published before the full refresh completes. | Delete from the independent serving closure at P7c/S1. |
| Decorator registry | Three constructor-time registrations invalidate both pointers; `Prepare` later performs the rebuild. | P7b compiler owns deterministic decorator output, not a serving registry. |
| Metadata invalidation hooks / trigger-subscription cache | Refresh touches both, but this composition registers no hook and selected-root reads no trigger subscription. | Outside the independent closure; do not move them into P7b. |
| Catalog refresh | One explicit broad startup refresh; this composition installs no Catalog watch or periodic refresh. | P7b store owns complete non-serving publication; P7c/S1 binds one admitted revision. |
| Role hierarchy | Constructor refresh plus a second `Prepare` refresh; request-triggered expiry can refresh with `context.Background()`. | Keep security-authoritative and prove `SecurityEpoch` compatibility. |
| Sharing-rule cache | Five-minute process-local TTL; an explicit invalidator exists but this composition wires no metadata invalidation hook. | Put rule definitions in the bound view and delete this cache from the independent closure. |
| Optional object/field permission cache | `SetCache` is never called, so effective grants use authoritative repository paths. | Keep the facts outside Catalog metadata. |
| Formula-view `viewSet` | Constructed but not warmed by `Prepare`; table routing derives from the carried schema and fails closed to the view name. | Derive routing from the bound view/schema; do not create a second registry. |
| Formula validation-projection cache / executable registry | Both are constructed with `FormulaViewService`, but selected-root table routing does not consult them. | Outside the independent closure; later mutation/automation slices own any move. |
| Runtime security replica/readiness | Process-global lease/epoch state owned by P6 admission. | Remains security-authoritative, separate from Catalog publication. |

The JSON companion anchors each state and transition to its current source.
There is no implicit “current Catalog” accessor that P7b may leave unclassified:
all such access in the candidate closure is either a metadata atomic pointer,
repository fallback, derived view, or separate security authority.
The one context-free Catalog accessor on the constructed `MetadataService`,
`GetField`, is also inventoried: selected-root does not reach it. The reachable
context-free role ancestor/descendant accessors read the security-authoritative
role cache and therefore remain on the `SecurityEpoch` side.

## 5. Classification

The JSON companion contains the complete per-accessor list and source anchors.
The five classes mean:

| Class | Meaning at P7b/P7c |
|---|---|
| `runtime_metadata_view_required` | Must be represented in the immutable view or its exact compiler input/output. |
| `security_authoritative_not_ordinary_metadata` | Remains owned by P6/Identity/permission authorities and must be validated against `SecurityEpoch`. |
| `derived_from_bound_view` | Must be recomputed only from the request's bound view, never a global current accessor. |
| `legacy_fallback_to_delete` | The independent Runtime edge must become unreachable in the atomic cutover. The legacy monolith method may remain until its own V1/V2 slice. |
| `outside_independent_runtime_closure` | Constructed or available in a package but not reachable from the selected-root serving operation. |

The classification intentionally separates catalog facts from user/security
facts. `ObjectMetadata.AccessPolicy`, row policy, sharing model, relationships,
and sharing-rule definitions are view facts. The active user/profile/session,
effective grants, roles, groups, shares, AppInstallation, revocation, and epochs
are security facts.

## 6. P7b complete snapshot specification

P7b must build a non-serving, immutable, content-addressed artifact containing
at least:

1. `CatalogRevision`, content checksum, capability version, schema/engine
   compatibility generations;
2. the complete effective `ObjectMetadata` and `FieldMetadata` set after the
   current decorators, not raw repository rows;
3. the complete effective relationship graph, including inferred field
   relationships and explicit sharing-parent edges;
4. all sharing-rule definitions grouped deterministically by object;
5. deterministic schema and relationship indexes plus physical read-source
   derivation required by the current selected-root engine;
6. no secret values, user grants, sessions, shares, or other
   security-authoritative rows.

The current graph has no authoritative `CatalogRevision` or
`SchemaGeneration`. `BootstrapReceipts` deliberately reports zero for those
axes. P7b must therefore accept revision, generation, and digest lineage as
explicit input from the Catalog publication owner defined by the target
architecture. It must not derive authority from refresh counts, timestamps,
database maxima, or snapshot insertion order. Until that input exists and
P7c/S1 admits it through Runtime `ServingState`, the artifact remains
non-serving.

The whole schema set is required even for a point read because
`targetRecordRead` can inspect every eligible target. Parent chains and sharing
rules similarly expand beyond the requested object. A lazy per-object fetch
would recreate the repository fallback and is rejected.

C0's selected record-access path reads an object schema, applies the same
CRUD/RLS/policy metadata family, and computes object types through the Records
repository. Approval-lock and folder-access inputs are record/security state,
not ordinary Catalog facts. A1b must prove any additional metadata delta from
the future coarse contract; it may not move those other authorities into the
view or copy unrelated legacy cache members.

### 6.1 Immutability and conflict rules

- Every published object, field, relationship, rule, map, and slice is deeply
  immutable to readers.
- Canonical ordering and case-folded keys are deterministic.
- The checksum covers canonical effective semantics, not Go pointer identity,
  map iteration order, logs, or cache timestamps.
- A complete view is published atomically. Partial object-by-object visibility
  is forbidden.

Current error behavior is not uniformly fail-closed. Examples include the
PermissionSet decorator degrading to the undecorated schema when its catalog
read fails, sharing-rule read failure becoming an empty rule set, and duplicate
object names being collapsed by map construction. P7b must record those exact
legacy outcomes in its parity report. The **proposed publication rule** for the
new artifact is stricter: duplicate/ambiguous identities, malformed
sharing-parent edges, invalid read sources, compiler/decorator input failure,
and rule parse/compile failure do not publish a complete view. Every resulting
legacy delta must be named and approved at P7c/S1; it must not be hidden behind
the word "parity."

### 6.2 Full parity oracle

P7b parity compares the non-serving artifact against the current effective
graph at one controlled repository state. It must cover every installed schema,
not only CRM happy paths:

- object existence, canonical name, fields, types, physical names, virtual/
  compound flags, read protection, default flags, and exact-decimal metadata;
- required permission declarations, table type, access policy, row policy,
  sharing model, hierarchy flags, and read source;
- relationship direction, foreign key, polymorphic targets, child indexes, and
  sharing-parent selection;
- sharing-rule identity, targets, sources, access level, and criteria;
- decorator output and deterministic ordering;
- derived formula-view/base-table routing and storage hydration inputs;
- an explicit observed-versus-proposed result for every malformed metadata and
  dependency-failure case, with no unclassified difference.

The oracle also replays the selected-root authorization/projection matrix with
the same security inputs: object forbidden, FLS omission, public/private/
parent/target-record RLS, missing/RLS masking, null versus absent, scalar types,
and cancellation. This proves semantic parity without allowing a production
consumer to select the artifact.

### 6.3 Non-serving guard

Until P7c/S1, P7b must satisfy all of the following:

- the `runtime-api` command remains on `AssembleNonServingBootstrap`;
- no production import from `runtimeapi`, `runtimeassembly`, Records,
  Permissions, Analytics, or Persistence can select the new artifact;
- no feature flag, environment variable, global pointer, or test hook can make
  it the current serving source;
- the old graph remains the only serving candidate and the new compiler/store
  is reachable only from preparation, verification, and tests;
- the P7b PR adds a closure guard naming its concrete package/symbols so future
  subpackages cannot enter a production dependency closure accidentally.

This P7a PR contains no new artifact type or second read path. Its guard only
pins the current closure and the finite future switch/delete inventory.

## 7. P7c/S1 atomic ownership flip

P7c/S1 is one serving ownership change. Mechanically separate PRs are allowed
only if every predecessor remains non-serving.

### 7.1 Required order

```text
P6 SecurityAdmission installs TrustedCall
  -> ServingAdmission validates compatible SecurityEpoch + CatalogRevision
  -> bind one immutable RuntimeMetadataView handle and receipts to the request
  -> Records CRUD/FLS/RLS/query/projection consumes that handle
  -> response returns the same receipts
```

The serving handle may be carried internally by the existing `context.Context`.
It does not enter protobuf, `contract.Deps`, or public engine interfaces.
Missing, stale, checksum-invalid, or incompatible state fails before record
repository access. No global-current fallback is allowed.

### 7.2 Finite consumer switch list

1. **Runtime API:** replace the independent `ReceiptSource.Receipts` request
   read with the receipts on the admitted serving handle.
2. **Records:** make root schema lookup use the request-bound view while
   preserving the existing schema pointer through normalization, query, and
   typed projection.
3. **Permissions:** switch object, field, parent-controlled, target-record, and
   sharing-rule metadata access to the bound view. Keep live user/grant/share
   inputs on the security-authoritative side and prove compatibility with the
   accepted `SecurityEpoch`.
4. **Analytics:** ensure RLS metadata and all-schema iteration use the bound
   view; keep passing the same schema into the projected query.
5. **Persistence/formula routing:** derive physical read source, selected
   columns, computed-field routing, and hydration only from that carried schema.

The V1a independent graph wires the existing `FolderAccessService` against the
same Runtime database because record-access can reach folder-scoped RLS. That
service remains a security/data authority, not Catalog metadata; the future
bound metadata view must carry the folder-policy shape without copying dynamic
membership or access decisions into Catalog.

### 7.3 Finite unreachable/deletion list

After the ownership flip, the independent Runtime closure must have no reachable
edge to:

- `RecordsReadDependencies.MetadataRepo` or
  `metadata.NewMetadataService`;
- `MetadataService.RefreshCache` and its selected-root-irrelevant flow,
  validation, autonumber, and identifier startup loads;
- any `MetadataService` atomic "current" schema/relationship accessor;
- `MetadataService.GetSchema` repository miss fallback;
- `MetadataService.GetSharingRules` repository read;
- `PermissionService`'s five-minute sharing-rule cache;
- the independent request-time `ReceiptSource`;
- the inert list-view RLS and validation-service wiring in the read-only graph.

“Delete” here means delete or make structurally unreachable from the independent
Runtime production composition in the same cutover. It does not require deleting
the methods from the legacy monolith before their V1/V2 capability owners move.

### 7.4 Security consistency gate

P7b must not place effective grants, roles, groups, shares, sessions, or Runtime
security bindings in `RuntimeMetadataView`. P7c/S1 must nevertheless prove that
the engine's security-authoritative reads cannot silently mix with a different
accepted `SecurityEpoch`. Acceptable implementations may use the existing
authoritative read path, a request-scoped security projection, or a database
snapshot owned by the same authority, but all must preserve P6 timing and
revocation semantics. This is an enforcement decision for P7c/S1, not a new
Catalog fact.

## 8. Guard behavior

`TestRuntimeMetadataClosureManifestIsCompleteAndAnchored` validates:

- the five classifications and unique IDs across reads, process-global state,
  and refresh/invalidation paths;
- every source file and source anchor;
- a finite snapshot fact list, consumer switch list, and unreachable-edge list;
- all non-serving assertions.

`TestRuntimeMetadataClosurePinsCurrentCompositionRoots` parses the Go AST for
`AssembleProduction`, dependency construction, graph construction, and
`Prepare`. Any added/removed call fails until this inventory is reviewed. It
also proves the executable command remains non-serving.

The existing `go-ci` “backend Salesforce-derived L0 boundary seal” runs the
whole `backend/internal/boundary` package, so this guard is enforced without a
parallel workflow or benchmark framework.

The guard is intentionally not a claim that static analysis can discover every
future dynamic call automatically. It makes the current entrypoints and evidence
explicit, and forces any composition-root change to reopen the closure. P7b must
add a second concrete guard for its new package and future subpackages before
that code lands.

## 9. P7a completion result

P7b now has a finite build and parity target: effective schemas, relationships,
sharing rules, deterministic indexes, receipts, conflicts, and the complete
selected-root semantic matrix. A1b may add only record-access metadata facts
proved by its capability contract; Records-repository type resolution is not a
Catalog fact and does not reopen this inventory. P7c/S1 now has a finite
five-consumer switch and ten-edge unreachable list for the current independent
closure.

P7a does not approve production serving. P7c/S1 remains blocked until P7b builds
and verifies the non-serving artifact. C0 has named the real product consumer;
V1 remains blocked on A1b, the record-access capability-specific contract and
conformance proof, P7c/S1, and the later legacy route ownership/deletion gate.
