# V1a-M: Runtime metadata closure for record access

Status: serving through the request-bound Runtime metadata path. The
record-access view is an internal Catalog/Runtime contract; it does not change
the public record-access protobuf.

The original inventory was derived at
`964150890e86fccbea8268621bb76ea5551fb90f`; the current-tree serving behavior
and schema gates below supersede that historical execution note. The executable inventory is
[`runtime-record-access-metadata-closure-v1a-m.json`](runtime-record-access-metadata-closure-v1a-m.json).

## Current behavior and call graph

The current in-process adapter is
`ContractService.BatchRecordAccess` in
`nexuscrm-records/contract_adapter.go`. For a named object it calls
`PersistenceService.RetrieveManyRaw`, which enforces object Read, loads raw
records, and masks RLS-denied rows as absent. It then calls
`PermissionService.BatchCheckRecordAccess` to compute Read, Edit, Delete, and
Locked. When the object is omitted, `ContractService` first resolves
Salesforce prefix-addressed IDs from the request-bound Catalog view. Only
UUID/opaque IDs, which deliberately carry no KeyPrefix identity, retain
repository discovery against live object/read-table candidates.

```text
ContractService.BatchRecordAccess
  -> [optional] request-bound Catalog KeyPrefix resolution
  -> [opaque IDs only] RecordRepository.FindObjectTypesForIDs
  -> PersistenceService.RetrieveManyRaw
       -> object Read policy
       -> RecordRepository.FindMany
       -> PermissionService.CheckRecordAccess (RLS/masking)
  -> PermissionService.BatchCheckRecordAccess
       -> object Edit/Delete policy
       -> row visibility, parent, sharing rules/shares/team
       -> approval lock + LockRule
       -> generic delete blocker
```

The route therefore needs more immutable metadata than selected-root read, but
it does not make dynamic permission grants, shares, locks, or records into
metadata.

## One fact, one owner

| Fact group | Owner | RuntimeMetadataView |
|---|---|---|
| Object identity (`APIName` + `KeyPrefix`), resolved read table, and runtime field shape | one `CatalogRevision` | included |
| `RequiredReadPermission` and `RequiredWritePermission` | one `CatalogRevision` | included |
| object Read/Edit/Delete expressions and their inherit closure | one `CatalogRevision` | included |
| row-read policy, sharing model/hierarchy, and sharing-parent edge | one `CatalogRevision` | included |
| applicable sharing-rule declarations and criteria | one `CatalogRevision` | included |
| LockRule plus the fields needed to hydrate/evaluate it | one `CatalogRevision` | included |
| the one before-delete lifecycle binding that disables generic Delete | one `CatalogRevision` | included |
| effective object/system permissions, permission-grant virtual values, and role/group authority | `SecurityEpoch` security owner | excluded |
| root/related records, shares/team/folder rows, approval instances, and ID probes | Runtime database | excluded |
| org, AppInstallation, workload, user/session, capability, epoch | request `TrustedCall` | excluded |

The JSON inventory fixes each individual fact to exactly one of these owners and
the boundary test rejects duplicates, missing owners, or a dynamic fact marked
as projected.

## Projection profile and artifact version

The compiler now requires an internal `ProjectionProfile` on each Runtime
projection:

- `selected-root-read/v1` preserves the P7b least-privilege projection;
- `record-access/v1` adds only the immutable record-access closure.

This profile is compiler configuration. It is not an L1 capability, protobuf,
RPC request, or competing Task B semantic type. The profile is encoded into the
canonical document and therefore into both the Catalog snapshot and Runtime
view digests.

The original profile boundary introduced:

- `nexuscrm.catalog-snapshot/v2`;
- `nexuscrm.runtime-metadata-view/v2`.

The serving record-access identity closure is now
`nexuscrm.runtime-metadata-view/v8`. V8 adds only the Catalog-owned `KeyPrefix`
projection. Older record-access views remain readable as historical artifacts
but are not serving eligible, so Runtime cannot silently fall back to physical
discovery. The Catalog snapshot grammar already owned the source fact and does
not need a parallel identity field or source-schema bump.

`record-access/v1` still requires the complete policy/inheritance object closure
from one Catalog revision. The compiler rejects a partial list instead of
silently widening it. Catalog identity determines an object shape; the later
known-object read remains the sole proof of record existence and authorization.

The current path hydrates complete raw rows (`FindMany` selects `*`) and then
projects dynamic permission-set grant values into declared SystemPermission
virtual fields. That projection remains Security-owned; only each virtual
field's shape is Catalog metadata. Complete rows are currently required because
sharing criteria, LockRule, owner visibility, parent traversal, and derived
policy can consume record fields before FLS. The record-access view therefore
retains the existing runtime field shape, not UI/presentation or mutation-hook
metadata. A later projection-pushdown optimization may use the existing formula
field-reference collector, but it must first normalize every sharing-criteria
grammar and prove parity; V1a-M does not pretend that optimization already
exists.

## Fail-closed publication gates

Before a current artifact can be opened or published, the implementation rejects:

- missing or unknown projection profiles;
- missing full object closure for record access;
- source facts from a different or absent `CatalogRevision`;
- access expressions with ambiguous forms, unknown predicates, invalid object
  operations, missing fields/objects, or inherit cycles;
- unsupported or field-invalid row-read policies;
- LockRule syntax, function, relationship/global, result-type, or field errors
  using the same authoritative formula validator used at metadata authoring;
- duplicate generic-delete blocker declarations;
- non-canonical encoding, digest mismatch, source-digest mismatch, stale schema
  generation, or mismatched engine generation.

The generic delete capability literal now has one Go owner in
`shared/pkg/constants`; records registration, permission evaluation, and this
projection compiler consume that owner.

## Behavior parity and no fallback

`TestRuntimeMetadataViewRecordAccessParityUsesOneBoundMetadataReadPath` runs the
current `PermissionService` twice: once over a controlled legacy effective
metadata provider and once over exactly one bound `RuntimeMetadataView`. It
asserts identical golden Read/Edit/Delete/Locked results for:

- derived object policy;
- LockRule and dynamic approval lock;
- metadata sharing rules plus dynamic group membership;
- ControlledByParent metadata plus a dynamic parent row;
- the generic delete blocker.

The second provider has no MetadataService or metadata repository. Its only
shape source is the bound immutable view; its repository fake owns only dynamic
permission/record/lock inputs. This proves projection sufficiency without
creating a serving adapter.

The current object-unspecified branch resolves an object's API name and read
table; it does not read Salesforce `RecordType` rows. RecordType definitions are
therefore not added to this capability view. If Task B or a later real consumer
requires a RecordType decision, that is a new evidenced input rather than an
implicit expansion of V1a-M.

## Non-serving boundary and later cutover

The production and command-test dependency closures of every backend command
must exclude `metadata/catalogsnapshot` and future subpackages. Runtime command,
assembly, process, and manifest paths must not name `record-access/v1` or either
v2 schema. Existing no-selector/no-environment/no-clock guards remain in force.

P7c/S1 + V1b still owns the one atomic serving flip:

1. SecurityAdmission validates authority and installs `TrustedCall`.
2. ServingAdmission binds one exact v2 view and receipt.
3. The record-access assembly receives that bound view.
4. Repository/global MetadataService shape fallbacks, including live object
   enumeration for type candidates, are deleted from the independent closure.
5. Mixed-revision, stale receipt, missing-view, and digest mismatch requests
   fail closed.

The finite current-tree switch/deletion list is executable in the JSON
inventory. It covers: the adapter's root-schema read; RetrieveManyRaw's shape
read; permission-grant virtual-field shape; RecordRepository's injected and
direct-database schema fallback; live `_System_Object` enumeration for ID type
candidates; every method on PermissionService's metadata provider; and its
independent five-minute sharing-rule cache. Folder access remains a dynamic
Runtime/Security dependency and is not copied into Catalog metadata.

Until that unit lands, no production route, command, runtime process, or global
selector consumes the v2 view.
