# C0 evidence: first real CRM serviceification consumer

> **Decision status:** C0 evidence for review. This document and the target
> architecture update in the same change select a current production consumer;
> they do not approve a new semantic or wire contract and do not move route
> ownership.
>
> **Evidence revisions:** NexusCRM
> `41061972fa7796e1d763f9e1737bb500fd39f3b8`; nexuscrm-app-packs
> `a84ba803073c90e279af21f35eeeadd12962cac4`.

## Decision

No current production CRM consumer is behaviorally equivalent to
`ReadSelectedRoot`.

The first real ownership slice should be the Salesforce UI API record-access
operation:

```text
POST /services/data/:version/ui-api/record-access
-> UIAPIBatchRecordAccessNeutralHandler
-> one bounded neutral Runtime access-evaluation use case
-> current Records/Permissions implementation
```

This candidate is **not** a selected-root consumer. It requires a different
coarse Runtime capability that preserves the existing bounded batch operation:
optional object-type resolution plus caller-effective Read, Update, Delete, and
Locked decisions for at most 200 submitted record identifiers. The future
capability must be one use case and one L1-to-Runtime RPC. It must not remote the
three methods of `contract.RecordAccessReader` independently. The current
in-process handler completes an empty-list success without calling the Records
seam. V1a-C makes an empty batch valid at the neutral semantic boundary for
complete conformance coverage, but does not turn that local no-op into a
Runtime availability dependency.

Selected-root remains an approved neutral foundation and a CI conformance
surface. Its only sfcrm adapter is explicitly dormant and is not a route owner
([app-packs `runtime_selected_root_client.go:20-35`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/runtime_selected_root_client.go#L20-L35)).

## Source-of-truth alignment

This decision distinguishes two facts that the previous plan conflated:

- selected-root is the first implemented and validated neutral Runtime record
  capability, with a dormant sfcrm client and CI-only issue-tracker fixture;
- record-access is the first real production CRM ownership slice.

The target architecture and selected-root ADR are updated atomically with this
evidence. Selected-root keeps its approved semantics and proof, but no longer
claims an exact production route. Record-access cannot inherit selected-root's
conformance result: its future contract must add a capability-specific non-CRM
CI consumer before V1. This restores one normative sequence rather than leaving
the learned log to compete with the target architecture.

## Evidence method and production assembly

The inventory used four independent current-tree sources:

1. every identifier-bearing or bounded read route in the CRM dialect manifest;
2. CRM `ProductHooks`/`RuntimeRouteSurfaces` and the generic host routes mounted
   by the CRM distribution;
3. every production sfcrm or generic-host call to `Retrieve`, `RetrieveByField`,
   `RetrieveMany`, `RetrieveWithAccess`, `BatchRecordAccess`, or object-type
   resolution;
4. every app-frontend call to the corresponding route family.

The CRM distribution composes `CompositeTreePartition` into its product
dialects
([NexusCRM `product.go:275-288`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/backend/distributions/crm/product.go#L275-L288)).
The generic host mounts active-pack dialects behind authentication, OAuth scope,
rate limiting, permission loading, and `ApiEnabled`
([NexusCRM `routes.go:120-178`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/backend/pkg/serverruntime/routes.go#L120-L178)).
The record-access route is therefore a production CRM route when the
`salesforce-crm-standard` pack is active; it is not a test-only registration.

NexusCRM currently supplies app-packs backend source at
`2f4a2ff36d473bba46750a0a247a98cd83f698e9`
([NexusCRM `source.go:3-13`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/backend/distributions/appsource/source.go#L3-L13)).
The candidate's route, handler, engine seam, and frontend consumers are
byte-identical between that production source revision and app-packs current
main. This was checked with:

```bash
git diff --exit-code \
  2f4a2ff36d473bba46750a0a247a98cd83f698e9..a84ba803073c90e279af21f35eeeadd12962cac4 \
  -- dialects/dialect-sf-crm/partition.go \
     dialects/dialect-sf-crm/ui_api.go \
     salesforce-crm-standard/frontend/src/platform/infrastructure/api/data/crud.ts \
     salesforce-crm-standard/frontend/src/platform/pages/ObjectView.tsx \
     salesforce-crm-standard/frontend/src/platform/features/record/components/MetadataRecordDetail.tsx
```

## Complete production bounded-read inventory

The manifest inventory is grouped by behavior rather than by URL spelling. A
route is a serious V1 candidate only when moving it would exercise a real L1 to
Runtime ownership boundary, its complete behavior is bounded, and its semantic
operation is smaller than the surrounding product workflow.

| Consumer family | Current production operations | Classification | Reason |
|---|---|---|---|
| Salesforce SObject row by ID | `GET /sobjects/:sobject/:id` | different coarse Runtime contract | Full record plus compound synthesis, lookup hydration, relationship nesting, CRM decoration, attributes, and activity; not selected-root. |
| Salesforce SObject row by external ID | `GET /sobjects/:sobject/:fieldName/:fieldValue` | different coarse Runtime contract | Lookup key is a field/value, not a record ID; returns the complete FLS-projected root plus CRM attributes and activity. |
| UI API record view | `GET /ui-api/record-ui/:recordIds` | different coarse Runtime contract; not first | Up to 200 records plus access, object info, layout, lookup display records, decoration, and activity. This is the primary normal record-detail frontend path, but it is too broad for the first slice. |
| UI API record access | `POST /ui-api/record-access` | **different coarse Runtime contract; selected first** | Real frontend consumer; bounded to 200 IDs; returns only four neutral access facts; no record values, relationships, files, or activity. |
| UI API clone defaults | `GET /ui-api/record-defaults/clone/:recordId` | different coarse Runtime contract; not first | Reads a source record, then applies Create permission, object info, layout, FLS/createability, record type, and defaulting semantics. |
| Query, search, list, recent-item, and related-list reads | SOQL query/query-more, SOSL/parameterized search, list-view records, relevant items, related-list candidates/counts | unsuitable as a point-read slice | These are query/list/search capabilities with filters, ordering, pagination or metadata-driven relationship selection; a point-record contract cannot preserve them. |
| Composite and multi-record facades | composite, composite batch, composite sobjects, and tree resources | unsuitable as first | The facade orchestrates heterogeneous operations and owns partial-success/transaction grammar. Moving the wrapper would not identify one data-ownership capability. |
| Recycle-bin reads | list/get plus restore/delete resources | unsuitable | Recycle lifecycle, deleted-row visibility, and recovery semantics are the operation; it is not an ordinary authorized root read. |
| Folder resource read | `GET /folders/:id` and the bounded child/share resource routes | different coarse Runtime contract; not first | Folder-specific sharing is checked before the record read; direct SQL-backed folder access is a separate authority input. The frontend exports `getFolder`, but current-tree production UI has no caller of that function. |
| Dashboard definition read | `GET /analytics/dashboards/:dashboardId` and status | different coarse Runtime contract; not first | Real dashboard UI consumer, but definition assembly tries DeveloperName then ID and reads a bounded child component collection. |
| Report read/execute | `GET /analytics/reports/:reportId` and `/describe` | unsuitable as first read slice | The GET operation is report execution/description, not a root record projection; it owns report IR and query execution. |
| Flow definition/version/interview | Flow action, Tooling Flow, and FlowInterview identifier routes | unsuitable | Automation-owned definition/runtime state with lifecycle and authoring semantics, not Records point reads. |
| Approval instance/step display | approval routes keyed by record ID | unsuitable | Reads a process graph and presentation metadata, not the root record. |
| Bulk job status/results | query and ingest job identifier routes | unsuitable | Job lifecycle, result streaming, and ownership semantics form a different service boundary. |
| Metadata/layout/permission resources | object metadata, layouts, deploy status, PermissionSet grants, CustomApplication | unsuitable | Catalog/security authoring or projections, not Runtime record data. |
| File download | ContentVersion and `_System_File` download routes | unsuitable | Streaming plus file authorization; a selected scalar record response cannot represent it. |
| CRM runtime route surfaces outside the dialect manifest | instance proxy, CometD, root identity, and related-list candidates | unsuitable | Proxy/streaming/identity do not read a CRM record. Related-list candidates is setup-gated metadata projection, not record data. |
| Generic host Records consumers present in the CRM process | notification mutation guard, connector sync status, delivery evidence authorization, and file-parent authorization | unsuitable as a CRM ownership slice | These are L0 host/integration/file capabilities. Their record reads are authorization or mutation inputs and moving them alone would split their owning operation; they are not an L1-to-Runtime CRM route capability. |
| Internal mutation/guard reads | standard actions, write guards, dashboard update read-after-write, approval lifecycle lookup | unsuitable | These reads are transaction, mutation, or lifecycle inputs. Moving them independently would split the owning operation. |

The route locations for these families are visible together in the manifest:
approval/job/dashboard/folder resources
([`partition.go:186-464`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/partition.go#L186-L464)),
metadata/permission resources
([`partition.go:476-584`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/partition.go#L476-L584)),
Flow and UI API resources
([`partition.go:760-880`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/partition.go#L760-L880)),
and SObject/file/query resources
([`partition.go:1004-1292`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/partition.go#L1004-L1292)).

The CRM distribution's four non-manifest runtime surfaces are enumerated in
one place
([NexusCRM `runtime_route_surfaces.go:20-64`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/backend/distributions/crm/runtime_route_surfaces.go#L20-L64)).
The generic host also mounts product notifications, connector/admin evidence,
and connector operational routes outside the Salesforce dialect
([NexusCRM `routes.go:213-305`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/backend/pkg/serverruntime/routes.go#L213-L305)).
Their direct Records reads are subordinate authorization or mutation steps, as
shown by notification ownership checking
([NexusCRM `product_notifications_handler.go:99-183`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/backend/internal/interfaces/rest/product_notifications_handler.go#L99-L183)),
delivery evidence authorization
([NexusCRM `external_delivery_admin_handler.go:167-199`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/backend/internal/interfaces/rest/admin/deliveries/external_delivery_admin_handler.go#L167-L199)),
connector sync-status assembly
([NexusCRM `connector_sync_status_contract.go:29-49`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/backend/internal/application/services/connector_sync_status_contract.go#L29-L49)),
and file-parent authorization
([NexusCRM `contract_service.go:617-678`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/nexuscrm-files/contract_service.go#L617-L678)).

## Serious-candidate traces

### 1. `GET /sobjects/:sobject/:id`

Current call graph:

```text
CRM dialect route
-> describe object
-> contract.Records.Retrieve
-> PersistenceService.Retrieve
-> object Read -> repository row -> RLS -> FLS
-> full v63 root projection
-> effective field access + virtual compound synthesis
-> describe root again
-> per target type: describe + RetrieveMany
-> per reference: target describe + relationship nesting
-> best-effort ContentDistribution/file decoration
-> Salesforce attributes
-> activity/MRU tracking
```

The handler performs the describe, root read, compound synthesis, lookup and
relationship work, decoration, attributes, and tracking explicitly
([`records.go:46-94`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/records.go#L46-L94)).
Lookup hydration batches IDs by target type but still describes targets and
reads related records
([`records.go:874-950`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/records.go#L874-L950)).

The response is the complete readable record, generated compound and
relationship fields, `attributes.type`, `attributes.url`, and `Id`. Object Read
is forbidden; RLS denial is masked as missing; FLS removes root fields before
the dialect sees them. Lookup failures are best effort, decoration failures
serve raw values, and successful reads update activity state.

This route is not selected-root. Preserving it through repeated selected-root
calls would make RPC count depend on lookup shape. A future route cutover needs
a separately approved coarse record-view contract or a product decision to keep
the view assembly co-located with Runtime; it is not V1.

### 2. `GET /sobjects/:sobject/:fieldName/:fieldValue`

The handler describes the object, validates the lookup field, calls
`RetrieveByField`, returns the full readable root with Salesforce attributes,
and tracks activity
([`records.go:97-134`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/records.go#L97-L134)).
It has no relationship expansion, but its semantic key is an arbitrary
field/value. Selected-root cannot express that lookup without changing its
frozen request. No current app-frontend call was found; the public route alone
does not make it a better first slice than record access.

### 3. `GET /ui-api/record-ui/:recordIds`

The primary ObjectView uses `record-ui` for normal objects and falls back to the
SObject route for metadata objects and Event, Task, and ListView
([`ObjectView.tsx:832-885`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/salesforce-crm-standard/frontend/src/platform/pages/ObjectView.tsx#L832-L885)).

Current backend graph:

```text
parse <= 200 IDs and resolve/require object type
-> concurrent RetrieveWithAccess per record
-> describe + field metadata + compound synthesis
-> lookup target Retrieve calls and display-value projection
|| object info/effective field and record-type access
-> resolve record-type-specific layout and related-list metadata
-> CRM file decoration
-> activity tracking per submitted ID
```

The graph is implemented by `getRecordUI` and `processRecordForUI`
([`ui_api.go:1057-1186`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/ui_api.go#L1057-L1186));
lookup display records add further root reads
([`ui_api.go:2203-2314`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/ui_api.go#L2203-L2314)).
The response carries `records`, `layouts`, and `objectInfos`, with per-field raw
and display values and per-record access. It is neither root-only nor a
single-record-only operation, so it needs a different coarse contract and is
too broad for the first slice.

### 4. `POST /ui-api/record-access` — selected

The handler accepts JSON `{objectApiName, recordIds}`, returns an empty success
for zero IDs, rejects more than 200, evaluates access, and returns only a
`recordAccess` map
([`ui_api.go:475-499`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/ui_api.go#L475-L499)).

The consumer is real. `dataAPI.getRecordAccess` always submits one object and
one record ID
([`crud.ts:102-119`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/salesforce-crm-standard/frontend/src/platform/infrastructure/api/data/crud.ts#L102-L119)).
ObjectView uses it for create-from-source authorization
([`ObjectView.tsx:700-719`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/salesforce-crm-standard/frontend/src/platform/pages/ObjectView.tsx#L700-L719)),
and record detail uses it whenever record-ui did not supply access, falling back
to a locked/read-only UI on absence or failure
([`MetadataRecordDetail.tsx:390-418`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/salesforce-crm-standard/frontend/src/platform/features/record/components/MetadataRecordDetail.tsx#L390-L418),
[`MetadataRecordDetail.tsx:427-455`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/salesforce-crm-standard/frontend/src/platform/features/record/components/MetadataRecordDetail.tsx#L427-L455)).

Complete current call graph when `objectApiName` is supplied:

```text
authenticated CRM data route
-> UIAPIBatchRecordAccessNeutralHandler
-> uiAPIServiceFromDeps composition gate
-> Policy.Access(object, Read)
-> contract.RecordAccessReader.BatchRecordAccess
-> MetadataService.GetSchema
-> PersistenceService.RetrieveManyRaw
   -> object Read
   -> metadata schema
   -> RecordRepository.FindMany
   -> RLS Read filtering; missing and denied rows omitted
   -> internal permission-grant projection
-> PermissionService.BatchCheckRecordAccess
   -> object Edit/Delete on each remaining row
   -> record Read/Edit/Delete
   -> approval-lock probe and lock computation
-> map neutral Read/Update/Delete/Locked facts
-> CRM hasReadAccess/hasEditAccess/hasDeleteAccess/isLocked wire
```

The current shared `uiAPIServiceFromDeps` constructor also requires Records,
record-default, metadata, layout, catalog, authoring, layout-authoring, and
policy contracts before any UI API handler can run
([`ui_api.go:983-1023`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/ui_api.go#L983-L1023)).
Only Policy and RecordAccessReader participate after construction for this
route. That broad constructor is current composition behavior, not evidence
that the Runtime operation needs metadata-authoring/default/layout inputs and
not a reason to remote them.

The existing neutral interface owns exactly those four facts
([NexusCRM `contract.go:85-100`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/shared/pkg/contract/contract.go#L85-L100)).
The Records adapter loads schema, calls the raw authorized batch read, and then
evaluates access
([NexusCRM `contract_adapter.go:124-145`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/nexuscrm-records/contract_adapter.go#L124-L145)).
`RetrieveManyRaw` enforces object Read and RLS, deliberately omits inaccessible
rows, and does no FLS projection
([NexusCRM `persistence_bulk_ops.go:855-916`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/nexuscrm-records/persistence_bulk_ops.go#L855-L916)).
The final access pass derives Edit/Delete and lock state
([NexusCRM `record_access.go:412-465`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/nexuscrm-permissions/record_access.go#L412-L465)).

When `objectApiName` is absent, the current L1 code deduplicates non-blank IDs,
calls `ObjectTypesForIDs`, groups IDs by resolved type, and calls
`BatchRecordAccess` once per type. A forbidden type is silently omitted while
other groups continue
([`ui_api.go:1189-1239`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/ui_api.go#L1189-L1239)).
Remoting those legacy methods directly would therefore create `1 + T` Runtime
RPCs for `T` resolved object types. The new coarse operation must absorb type
resolution and all group evaluation inside one Runtime call.

#### Observable semantics

- Success is HTTP 200 with
  `recordAccess[id] = {hasReadAccess, hasEditAccess, hasDeleteAccess, isLocked}`.
- Missing rows and rows denied by Read RLS are both absent from the map.
- With an explicit object, object Read denial is HTTP 403. Without an explicit
  object, a forbidden resolved object group is omitted.
- FLS is not consulted for projection because no field value is returned.
  Internal raw fields remain available only inside Runtime for policy
  evaluation.
- No lookup/relationship record, metadata payload, file decoration, or activity
  tracking occurs.
- Internal errors pass through the UI API sanitizer; recognized forbidden and
  not-found errors keep their public status
  ([`ui_api.go:2574-2618`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/ui_api.go#L2574-L2618)).

#### Consistency and RPC requirement

The current implementation uses the process-global metadata/security services;
that is an observation, not the target. The ownership slice must wait for
P7c/S1 and execute under one `TrustedCall`, one bound `RuntimeMetadataView`, and
one ServingState receipt set.

Every accepted non-empty public HTTP request must make exactly one
L1-to-Runtime RPC for all submitted IDs, including the object-unspecified
multi-type branch. The accepted empty no-op and rejected requests make zero.
Database reads and permission probes are Runtime-internal work and must be
counted separately. The future implementation may batch those probes, but it
must not change the four decisions or expose the raw record used to compute
them.

### 5. Clone defaults, folders, and dashboards

Clone defaults reads the source record and then performs Create authorization,
describe/object-info, record-type, layout, FLS/createability, and defaulting
work
([`ui_api.go:1794-1860`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/ui_api.go#L1794-L1860)).
It is a complete create-bootstrap use case, not selected-root.

Folder read performs a folder-sharing decision before `Records.Retrieve` and
masks both failures as not found
([`folders.go:544-557`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/folders.go#L544-L557),
[`folders.go:683-694`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/folders.go#L683-L694)).
The selected-root contract does not own folder-share authorization.

Dashboard detail is a real UI operation, but its service first queries by
DeveloperName, falls back to ID, and then queries up to 100 components
([`dashboard/service.go:53-140`](https://github.com/pingcap-inc/nexuscrm-app-packs/blob/a84ba803073c90e279af21f35eeeadd12962cac4/dialects/dialect-sf-crm/dashboard/service.go#L53-L140)).
It requires a dashboard-definition use case rather than a root-record contract.

## Selected-root comparison

The frozen selected-root request/projection semantics are defined in the
selected-record ADR
([`runtime-selected-record-read-contract.md:145-207`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/docs/design/runtime-selected-record-read-contract.md#L145-L207));
its missing/RLS categories and final v1 bounds are separately fixed in the same
ADR
([`runtime-selected-record-read-contract.md:320-360`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/docs/design/runtime-selected-record-read-contract.md#L320-L360),
[`runtime-selected-record-read-contract.md:1001-1024`](https://github.com/pingcap-inc/NexusCRM/blob/41061972fa7796e1d763f9e1737bb500fd39f3b8/docs/design/runtime-selected-record-read-contract.md#L1001-L1024)).

| Semantic fact | `ReadSelectedRoot` | selected record-access operation |
|---|---|---|
| Input cardinality | exactly one record | zero to 200 submitted IDs |
| Object type | required | optional; absent type triggers bounded type resolution |
| Field selection | one to 512 root fields | none |
| Returned values | typed root values, null, omission | no record values |
| Authorization returned | only success/failure of Read | Read, Update, Delete, Locked per visible record |
| RLS/missing | one masked `MISSING` outcome | both omitted from the result map |
| FLS | hidden selected field omitted | not applicable to the public result |
| Relationships/files/activity | excluded | excluded |
| Consistency | selected-root receipts | must use the same P7c/S1 bound serving axes, but needs its own capability receipt |
| L1-to-Runtime calls | one | one for the complete batch, including type resolution |

Using selected-root would either lose Update/Delete/Locked, add non-root access
facts to a frozen result, or require extra calls. All three are rejected.

## Golden and parity fixture requirements

The V1 implementation PRs must run the same route vectors against the legacy
in-process owner and the new Runtime-backed owner, comparing HTTP status and
canonical JSON body. The fixture must cover:

1. **Production assembly:** active CRM pack mounts the route through the actual
   CRM distribution; inactive pack does not.
2. **Accessible single record:** an end-to-end visible row has Read=true while
   object/record Edit, Delete, and approval lock independently drive the other
   three booleans. A seam-level projection vector covers all four boolean
   encodings without claiming that a Read-denied row is returned end to end.
3. **Missing/RLS masking:** missing and Read-RLS-denied IDs are identically
   absent; mixed batches retain only visible entries.
4. **Object Read:** explicit-object denial is the existing 403 envelope;
   object-unspecified denied groups are omitted without suppressing allowed
   groups.
5. **FLS independence:** hiding arbitrary fields does not alter access output,
   and no raw field appears on the public wire.
6. **Typed and untyped requests:** explicit object, omitted object, multiple
   resolved object types, unknown type resolution, blank IDs, duplicate IDs,
   and object-name case normalization, preserving the current differences
   between the two branches.
7. **Bounds:** malformed JSON, empty list, one ID, 200 IDs, and 201 IDs with the
   existing error codes and no Runtime/repository work for rejected input.
8. **Cancellation/deadline:** before admission, before repository access, during
   repository execution, and during permission/lock evaluation; no partial
   public response.
9. **One-RPC proof:** exactly one Runtime data RPC for every accepted non-empty
   HTTP request and zero for empty/rejected input; zero per-ID/per-type L1
   fan-out. Separately prove that the semantic operation accepts an empty batch
   without requiring the production route to call it. Record internal
   database/query and lock-probe counts separately.
10. **Serving consistency:** one TrustedCall, CatalogRevision,
    SchemaGeneration, EngineGeneration, and SecurityEpoch for the full batch;
    stale or incompatible state fails before record access.
11. **Security boundary:** cross-org, unauthorized AppInstallation, revoked
    session, and unsupported capability fail before repository access.
12. **No side effects:** no Activity/MRU event, file resolution, lookup read,
    relationship expansion, or metadata payload.
13. **Sanitization:** internal SQL/topology/permission causes never reach the
    UI API envelope.
14. **Frontend fail-closed behavior:** omitted/error access keeps mutation
    affordances locked while a valid response drives edit/delete/locked UI.
15. **App neutrality:** the future neutral access-evaluation semantic vectors
    run through a CI-only non-CRM consumer using public contracts and non-CRM
    object metadata, with no Salesforce field names, envelopes, errors, or
    response grammar. The existing selected-root fixture does not satisfy this
    different capability's gate. The proof may extend test-only issue-tracker
    conformance metadata, but it must add no production route, command, image,
    AppInstallation, manifest, deployment, or dependency closure.

## Follow-up decision and gates

C0 selects the consumer, not its wire. The next owner-aligned sequence is:

1. A1b freezes the minimum App capability/HandlerBinding for UI API
   record-access.
2. A separate contract decision defines one neutral bounded access-evaluation
   operation and its capability-specific non-CRM CI conformance consumer. It
   may reuse the semantics of the existing in-process seam but may not mirror
   `RecordAccessReader` method-for-method.
3. P7b/P7c-S1 supplies one immutable request-bound metadata/security view.
4. V1 implements the generated client/server, runs both the CRM parity and
   non-CRM conformance matrices, fences
   one active route owner, and deletes the legacy route read path after its
   rollback gate.

No protobuf, generated code, route registration, selected-root change, or
issue-tracker promotion belongs in C0.
