# Non-request-serving Catalog App authority

`catalogapp` is the A1c Catalog-owned authority mechanism for publishers,
immutable App releases, global App-owned layout-bundle slot registration, App
installations, handler/frontend bindings, lifecycle journals, complete Runtime
security-projection publication, and one-time legacy pack conversion.

An App slot reservation binds `(AppID, LocalComponentKey)` to one immutable
declaration identity and namespace. Its append-only definition-version ledger
and per-AppRelease version binding commit atomically with the verified
`ImportReleaseArtifact` command. Callers cannot register an asserted package or
attestation directly: Catalog verifies the detached publisher signature against
its durable key and re-binds that exact locked key before committing the
release.
Historical reuse writes only the new release binding; omission writes nothing;
and a successor advances the integrity head only through an exact prior
version-and-digest CAS. That head is lineage state, not a serving selector:
installation, publication, and request routing cannot infer an active layout
from registration order.

Catalog also recognizes the contract-defined, source-only App ProfileKey and
RoleKey declaration components. Import, durable release reads, sealed reads,
and stopped-state App inspection all structurally validate the same canonical
package. `ReadSealedIdentityKeyDeclarations` exposes those positive source
facts only after revalidating the retained signed artifact evidence; it rejects
pre-receipt releases. A declaration is not namespace ownership, an issuer
capability, organization admission, a local-ID mapping, an identity ledger
entry, or a routing-security binding.

The org-scoped scalar lineage and writer fence are owned by the neutral
`internal/catalogwriter` substrate. App lifecycle reserves and completes that
same command fence; future L2/schema/layout/security publication cannot create
another org writer or revision allocator. The shared substrate intentionally
has no publication tuple, command journal, or selector yet, so this extraction
does not claim the generalized publication authority is complete.

It is intentionally absent from request serving:

- no route, Gateway owner, Runtime metadata reader, or request admission path
  imports this package;
- the single `catalogreconcile` lifecycle job may call it to publish an exact,
  immutable serving selection before Runtime starts; Runtime never calls that
  job or Catalog synchronously while serving a request;
- conversion is an explicit operation over caller-supplied, verified legacy
  facts and never reads or mutates `_System_InstalledPack`;
- the only Runtime write port publishes a complete, newer security projection;
  it cannot issue leases, read Runtime tables, or author an AppInstallation.

The authority keeps active and candidate release/security inputs separately.
Catalog commits a candidate and its expected projection checksum first, Runtime
publishes that complete projection, Catalog records the acknowledgement, and
only then can `CompleteOperation` flip the active tuple. Every non-removal
operation must first stage the complete release-required HandlerBinding and
FrontendBinding set. Catalog canonically commits that exact multi-plane set at
`SERVING_ARTIFACTS_COMMITTED`; no later binding mutation, projection
acknowledgement, or activation may bypass or disagree with the commitment.
The binding rows remain the owners of their per-capability receipts while the
operation journal owns only the set commitment. Retry readers can therefore
recover each exact snapshot/view selection without consulting live metadata.
Before that serving commitment, the operation journal also owns exactly one
`METADATA_SELECTION_COMMITTED` CatalogSnapshot digest. It is written only
after the content-addressed snapshot exists, so an interrupted reconciler can
recover the selected source without reopening the mutable repository.
Upgrade compatibility is explicit rather than a bypass: a pre-upgrade
non-removal operation already durably at `PROJECTION_ACKNOWLEDGED` may have no
serving-selection journal because that step did not previously exist. Before
activation, Catalog validates its complete staged binding coverage and appends
one `LEGACY_SERVING_ARTIFACTS_BACKFILLED` commitment. New operations cannot use
this path because projection admission requires `SERVING_ARTIFACTS_COMMITTED`.
Failed operations retain
the per-org fence and are explicitly resumable; conflicting retries fail closed.
Namespace reservations use the same active/candidate separation, so a failed
upgrade cannot overwrite the namespace facts of the last active release.

Catalog owns publication of the neutral Runtime security projection and its
mutation port; nexuscrm-contract owns the value identity. A Runtime persistence
implementation consumes that same contract type and therefore satisfies the
port directly after the coordinated consumer migration. `catalogapp` neither
aliases nor imports a persistence implementation. This keeps the Catalog
authority, including `ReadActiveHandlerBinding`, consumable from a clean
external Go module without inheriting NexusCRM workspace replacements.

ServiceRelease and FrontendRelease remain independent release planes. A binding
for the current active AppRelease can be staged and activated with its own
fence; that ownership flip does not manufacture an App upgrade or change the
Catalog revision, App release, or Runtime security projection.

Frontend artifact contents are not a lifecycle identity. `FrontendBindingID`
is the immutable audit identity, `ActiveKey` selects the current owner, and the
nullable operation/staging keys select only the current attempt. Failure or
activation releases the staging key, so an exact artifact can be rolled back
through a new immutable binding without overwriting history. The installation
row serializes standalone binding candidates with AppRelease operations; an
AppRelease flip fails closed until those candidates are activated or failed.
Handler `OwnershipEpoch` values are fencing tokens and are never reused: a
failed staged handler burns its candidate epoch, an exact retry reports the
failed transition, and a replacement must advance from that epoch. This keeps
stale service owners fenced while retaining every immutable attempt.

Legacy conversion consumes a sealed legacy-authority digest plus a signed
mapping verification receipt. The sealed input includes the selected legacy
source/incarnation, converged payload hashes, host/runtime declaration digests,
an upstream deep-verification receipt, and the complete component mapping.
Catalog never reads or writes the legacy installed-pack table. Conversion
stages its complete binding set inside the conversion transaction, but it must
pass through the same serving-selection commitment before projection or
activation; it has no lifecycle bypass.

The conversion journal owns only the immutable legacy-to-operation mapping and
retry identity. The referenced App operation journal is the sole lifecycle
state owner; conversion reads join that operation instead of maintaining a
second, eventually inconsistent copy of `State` and `Step`.
Each operation also carries a constrained initiation kind. Direct lifecycle
requests and legacy conversion therefore cannot reuse one another through an
otherwise identical org/App/idempotency tuple; a conversion can only retry
through its already-committed immutable conversion mapping.

This package proves a durable, neutral authority can exist. It does not prove a
serving metadata cutover, a production CRM migration, a deployment artifact, or
a second product App. P7c/S1 remains the only serving ownership flip.

## Validation

```bash
GOWORK=off go test ./catalogapp -count=1
GOWORK=off go test -race ./catalogapp -count=1
GOWORK=off go vet ./catalogapp
GOWORK=off go test -tags=integration ./catalogapp -run TestCatalogAppTiDB -count=1
```

Set `NEXUSCRM_REQUIRE_CATALOGAPP_TIDB=1` in the real TiDB gate so an unavailable
database fails instead of skipping.

The environment gate also sets `NEXUSCRM_CATALOGAPP_RUN_ID`; every test database
then has a marker-owned prefix that an `always()` cleanup can remove after a
canceled run. No test reads an existing application or tenant database.

## Finite deletion ledger

The legacy CRM serving path is not deleted here. Its remaining gates are:

1. R1b publishes the immutable CRM App artifact and signed legacy mapping.
2. P7b proves the non-serving Runtime metadata snapshot/store and full parity.
3. A real conversion run seals exact desired/applied legacy hashes and completes
   the Catalog/Runtime acknowledgement journal.
4. P7c/S1 binds each request to one Catalog revision, switches every selected
   consumer, and proves no repository/global fallback or mixed revision remains.
5. Only after all legacy readers have an explicit disposition may the sealed
   compatibility authority and conversion-only code be removed.
