# Runbook: V1 Reconciliation and Replay

Owner: production operations duty officer (`rota:prod-ops`). Last reviewed:
2026-08-14. Version: 1.

> A restore brings back a copy of the estate as of some instant. This document
> is about everything that happened after that instant, and about the work that
> was in flight when the estate stopped: which of it can be re-driven, which of
> it must never be re-driven, and how an operator establishes which is which.
>
> It is the step `infra/hetzner/backup/box-loss-restore-drill.sh` names in its
> report as `nextRequiredControl`, and the step every other runbook hands off
> to. The nine state classes below are the ones §S10.12.c names.

## 0. Where the state actually is, because it is not where the table names say

**`admin_store_snapshot` is the V1 durable state store, and reading it as one
table is the mistake this document exists to prevent.** It holds one row per
`(store_key, scope_id)` carrying a whole `document` JSONB, written by
`createSnapshotSink` (`libs/oshun/persistence/src/durable-snapshot-store.ts`).
On the development box on 2026-08-14 it held **415 rows under 52 store keys**. A
second job store, thirty-eight crypto invoices carrying on-chain transaction
ids, and the editorial release streams are all in there. None of them is visible
to a search for a table name.

**Two conclusions recorded against this estate were reached that way, and both
are corrected below.** The §S10.12.a groundwork recorded "no `CREATE TABLE`
matching `release` anywhere", which is true of the table names and false about
the estate — `v1_metis_publication_package` carries `release_id` and
`release_history` as columns (§9). And **the first draft of §§3–4 of this
document said V1 has no outbox and no inbox**, on the same evidence, while V1
has both: an append-only operation log that commits a notification intent with
the state change that produced it, a delivered-set the drain subtracts, and a
reconcile that runs at boot. That error was made inside the document that exists
to warn about it, which is the strongest argument for the rule that this page
could have produced. It is corrected in place rather than rewritten away.

So every procedure below names its stores as **store keys as well as tables**,
and `scripts/operations/v1-replay-inventory.mjs` enumerates both. A search that
can only find one of the two shapes is the reason a state class goes unnoticed
until a restore drops it.

Two more consequences of the snapshot store's design, both of which decide
procedures further down:

1. **The document is written whole.** A durable write serializes the entire
   collection, so the recovery unit is a store key and not a record: there is no
   partial restore of one job inside `generation-jobs`.
2. **The version column orders two readings of one store key.**
   `admin_store_snapshot.version` is `(existing ?? 0) + 1` on every save
   (`durable-snapshot-store.ts`), so it strictly increases per
   `(store_key, scope_id)` and a source-side inventory can be ordered against a
   restored-side one without trusting either host's clock.

   **It is not the recovery-point marker §S10.11 found missing, and reading it
   as one would be a mistake.** That finding was about which committed
   transactions are inside a dump — a question this counter cannot answer,
   because it says nothing about where the `pg_dump` boundary fell or how the
   relational capture is ordered against the Redis checkpoint. What it gives is
   narrower and still useful: a per-store-key sequence, so "this store key came
   back at an older revision than the source held" is a checkable sentence.

## 1. jobs

**Stores.** `admin_store_snapshot` store key `generation-jobs`
(`apps/oshun/bff/src/generation/jobs-route.ts`) and store key
`metis-ingest-jobs` (`apps/oshun/bff/src/metis/ingest-job-store.ts`). Both rest
on `dep:primary-store`; `dep:job-queue` on this estate is the in-process map in
front of the first of them.

**What accrues.** Accepted work that has not reached a terminal state. Measured
on 2026-08-14: 8 generation jobs (7 `failed`, 1 `queued`) and 55 Metis ingest
jobs (32 `completed`, 23 `awaiting_source_content`) — **24 records in flight
across the two stores**.

**The procedure.** For `generation-jobs` it is already automatic and it runs at
boot: `wireDurableGenerationJobs` loads the snapshot and converts every job
still in `running` to `needs_review` with the error
`generation_interrupted_after_durable_claim`, then persists that recovery before
accepting traffic. This is correct and it is the reason the job path is
re-drivable at all — a durable `running` claim is written _before_ the provider
is called (`persistJobs()` at the external-effect barrier), so a `running` job
in a restored snapshot proves that dispatch may already have happened and must
not be repeated.

For `metis-ingest-jobs` there is no boot recovery, and none is needed:
`MetisIngestJobStatus` is `completed | awaiting_source_content`, and the second
is a job waiting for a person to supply content rather than a machine claim in
flight. A restore leaves it exactly where it was.

**The gap, stated.** The boot recovery reconciles the JOB and not the two
durable ledgers the job wrote to before dispatch. `activeQuotaGate.admit()` and
`activeBudgetGate.reserve()` both run before the provider call and both write to
Redis (`RedisGenerationBudgetStore`, `RedisGenerationQuotaStore`); nothing in
`wireDurableGenerationJobs` releases either. A job held for review after a
restore therefore keeps its quota slot and its estimated spend. That is the
conservative direction for money and the wrong direction for the actor, and no
procedure compensates it.

**Verification.** The count of `needs_review` jobs carrying
`generation_interrupted_after_durable_claim` after boot equals the count of
`running` jobs in the source-side inventory, and the totals agree.

**External visibility.** True. A `running` job may have reached
`dep:provider-gateway`, and **the estate has no provider-side receipt to compare
against** — the outcome exists only as a field on our own job record, which the
restore rebuilt. That is a declared gap, not an omission; see §10.

## 2. leases

**Store.** `live_media_pipeline_job_attempts`
(`libs/shared/live-media/src/sql-media-pipeline-job-store.ts`, migration in
`libs/aphrodite/database/prisma/migrations/00009_live_media_pipeline_jobs/`).

**This is the estate's only real fence, and it works.** `lease_token` is carried
in the `UPDATE ... WHERE` of every heartbeat, expiry and completion
(`AND lease_token = $4 AND status = 'leased'`), so a worker holding a superseded
lease updates zero rows. `recoverExpiredLeases` selects leased attempts whose
`lease_expires_at` has passed **using the database clock**, moves the attempt to
`lease_expired` under that same fenced predicate, and returns the job to
`retry_wait`, `failed` or `cancelled` with exponential backoff, all under
`FOR UPDATE OF j, a SKIP LOCKED`.

**Why the window is the recovery point and not a clock.** The reclaim set is
"every attempt whose expiry has passed", and a restore can only move the clock
forward relative to the captured expiries — so after a restore the set is every
restored leased attempt, bounded by what came back rather than by an interval
somebody chose. The procedure needs no window parameter for that reason.

**The gap, stated, and it is the whole procedure.** **Nothing on this estate
runs it.** `recoverExpiredLeases` is called only by
`MediaPipelineWorker.runOnce`
(`libs/shared/live-media/src/media-pipeline-worker.ts`), constructed only by
`createLiveMediaPipelineRuntime`, used only by `apps/aphrodite/streaming` — and
`apps/aphrodite/streaming` appears in no compose file, no infrastructure
manifest and no workflow in this repository. `docker/docker-compose.yml`
declares no such service. Measured on 2026-08-14, the relation
`live_media_pipeline_job_attempts` **does not exist in any database on the
box**: the compose's `POSTGRES_MULTIPLE_DATABASES` list names `aphrodite`, and
the running server has no `aphrodite` database. The fence is real, the reclaim
loop is correct, and neither is deployed where the V1 recovery path can reach
them.

The inventory producer reports this subject `unmeasurable` rather than zero, for
the reason §0 gives: an absent store and an empty one are the same number and
opposite facts.

## 3. outbox

**The first draft of this section said "there is no outbox in V1", on the
strength of no table matching `outbox` in the 146 `CREATE TABLE` statements
under `libs/oshun/persistence/prisma/migrations/`. That is §0's error committed
in the document that exists to warn about it, and it is corrected here rather
than quietly rewritten, because it is the strongest evidence for the rule.**

**V1 has an outbox and it is complete.** The academic-records store
(`apps/oshun/bff/src/metis/academic-integrity-appeal-store.ts`, snapshot store
key `metis-academic-records`) keeps `operations` — an append-only log carrying a
`revision` — and derives `notificationIntents` from it by replay. A verdict or
appeal transition and the notification intent it produces are one durable
operation, which is the property transactional publication exists to give: an
intent cannot exist without the state change that made it, and a state change
cannot commit without its intent.

**The drain and its reconcile.**
`reconcilePendingAcademicIntegrityNotifications()`
(`apps/oshun/bff/src/metis/academic-integrity-notification-worker.ts`) is called
from `apps/oshun/bff/src/server.ts` at boot, deliberately positioned after both
durable stores hydrate, and dispatches every pending intent. **The idempotency
key is the intent's stable `messageId`** — the module says so itself: "a crash
after message commit but before outbox acknowledgement is retried as a harmless
duplicate on restart". That is the whole procedure, and it already runs.

**Verification.** The reconcile returns `{ attempted, pending }`, and `pending`
is recomputed AFTER the drain rather than assumed — so a run that dispatched
nothing and a run that drained everything are distinguishable in its return
value. Requiring `pending === 0` is the check.

**Externally visible: no,** and the reason is specific rather than assumed. The
dispatch calls `sendMessageWithReceiptsDurably` with `channel: 'in-app'`, so the
message lands in this estate's own customer message centre. A duplicate is
visible to the recipient and not to anything outside; the repeat is bounded by
the delivered set below.

Measured on 2026-08-14: the store exists and holds 0 operations at revision 0 —
measured, not absent, which is the distinction §10's producer keeps.

## 4. inbox

**Also present, in the same store, and it is the acknowledgement half.**
`deliveredNotificationIds` is the set of intents already dispatched, and
`listPendingNotifications()` is literally the intents minus that set. That is
the inbox pattern: a consumer-side record of what has been processed, consulted
before processing again, so a redelivery is dropped rather than repeated.

**Why it is stronger than a marker, and where it is weaker.** A last-processed
sequence marker assumes the consumer processes in order and never skips; a
delivered SET makes no such assumption and survives out-of-order dispatch. What
it does not carry is `study.domain_event_ack`'s `attempts`, `last_error` and
`dead_letter` — so an intent that fails to dispatch every time is retried at
every boot forever, with nothing counting the failures and nothing parking it.
**A poison intent wedges nothing and is never noticed**, which is the failure
mode to watch for after a restore that brings back a recipient the notifier
cannot resolve.

**The window is the delivered set, not the recovery point**, and that is the
honest description: the drain re-derives what is pending from what came back, so
it is bounded by the restored state on both sides.

**A trap worth naming, because it was nearly written into this document.**
`admin_store_snapshot` also carries a store key called
`operator-inbox-decisions`. It is an operator decision log (`decisionId`,
`verdict`, `assignee`, `auditEventId`), not an inbox-pattern dedup record, and
claiming it as this subject's store would be an answer about the wrong subject
in the right shape. The real one is not called an inbox at all.

**The pattern elsewhere, for contrast.** `study.domain_event_outbox` and
`study.domain_event_ack` in another application
(`apps/yemaya/svc-study-workspace/migrations/0022_study_domain_event_outbox.sql`)
do this relationally: `outbox_seq bigint GENERATED ALWAYS AS IDENTITY` as the
total order, `event_id uuid UNIQUE` as the consumer idempotency key, and an ack
table with `attempts` and `dead_letter` per consumer. That is what V1's would
look like if it needed multiple consumers or a dead-letter path; today it has
one consumer and no parking.

## 5. events

**Store.** `workbench_event` (`apps/oshun/bff/src/workbench/intent-store.ts`),
`seq BIGSERIAL` primary key, append-only, with `entity_kind`/`entity_id`/`actor`
constrained at the table.

**This is the estate's one genuine replay, and it is verifiable by comparison
rather than by reading code.** Every write appends the event and applies the
same reducer the replay path uses, in one transaction, so the projection is
`reduce(ledger)` by construction. `replayLedger` recomputes the rows from
scratch with no wall clock inside the reducer, and
`apps/oshun/bff/scripts/crosscheck-intent-plane.ts` runs the comparison against
the real database: it rebuilds from the ledger and asserts `replay(ledger)`
equals the rows. That is the verification, and it catches the specific damage a
partial restore does — a projection row lost while its events survive, which
replay rebuilds, and a row deleted straight out of the projection, which replay
also rebuilds and thereby exposes.

**The window is the restored ledger itself**, because the replay is total: it
starts from the lowest sequence present. Measured on 2026-08-14 the ledger held
0 rows on this box, which is a measurement and not an absence — the relation
exists.

**The gap, stated.** `crosscheck-intent-plane.ts` is not wired into any
package.json script or workflow; it is run by hand with a database URL. It is
named in §10's statement so that a restore procedure points at it.

## 6. provider-outcomes

**Store.** `admin_store_snapshot` store key `crypto-invoices`
(`apps/oshun/bff/src/payments/invoice-store.ts`).

**Why this is the subject's store and there is no other.** V1 has no generic
provider-effect ledger — the reconciler's `providerEffects` collection has no
table behind it anywhere in the schema. What it does have is the crypto invoice
store, and that store is the estate's single most consequential record of an
outcome produced outside it: `CryptoInvoiceStatus` is
`pending | confirmed | expired | cancelled`, a confirmed invoice carries `txId`,
`blockHash` and `blockHeight`, and the entitlement it grants is reconciled
through a durable completion marker. Measured on 2026-08-14: **38 invoices — 20
expired, 10 `pending`, 8 `confirmed`, and all 8 confirmed rows carry a `txId`.**

**The procedure.** Compare each restored invoice against the chain, keyed by
`invoiceId`, and specifically:

1. Every restored `confirmed` invoice must still carry `txId`,
   `confirmedAtUnixSeconds` and its signed receipt; the store's own validation
   already refuses a `confirmed` row with any confirmation field null, so a
   restored row that fails it is a corrupted restore rather than a lost one.
2. Every restored `pending` invoice must be re-checked against the chain before
   it is expired. A payment that confirmed after the capture instant is
   invisible in the restored copy and visible on the chain forever; expiring it
   on our record alone takes money for nothing.
3. An invoice present on the chain and absent from the restore is data loss and
   must block resumption, not be re-minted.

**External authority.** The chain transaction named by `txId`. This is the one
subject in the estate whose external side genuinely outlives any restore of ours
and can be queried independently, which is what makes its reconciliation
meaningful rather than self-referential.

## 7. pending-uploads — ABSENT

**V1 has no durable upload session.**
`libs/oshun/studio-authoring/src/asset-metadata/bulk-upload.ts` plans resumable
chunks and persists nothing; it has no route in the BFF. There is no upload
table in the V1 schema and no store key holding upload sessions. An interrupted
upload is therefore lost rather than left half-applied, and the actor repeats
it.

**Where the pattern exists.** `study.upload_session`
(`apps/yemaya/svc-study-workspace/migrations/0016_study_upload_session.sql`) in
another application: `declared_sha256` and `received_bytes` as the resumption
ledger, with a table invariant that received can never exceed declared. Note for
whoever builds V1's: **that table has no expiry column**, so a session
interrupted by a restore stays open indefinitely. `dep:blob-store` holds the
bytes either way.

## 8. gates

**Store.** `v1_tara_workbench_review_gate_result`
(`libs/oshun/persistence/prisma/migrations/20260720120000_tara_content_workbench/`),
carrying `gate_id`, `verdict`, `evidence`, `source_record_id`, `payload_hash`,
`contract_schema` and `contract_version`.

**The procedure is a revalidation, and the schema is what makes it possible.** A
gate result is a function of a source record and a contract version, and the row
records both: `source_record_id` says what was judged and `payload_hash` +
`contract_version` say what it was judged as, under which rules. So after a
restore the operator does not ask whether the gate rows came back — the rows are
derived and can be recomputed — but whether recomputing them produces the same
verdicts. A verdict that changes on recompute means the source record or the
contract moved across the restore boundary, and that is the finding.

**Recompute rather than trust, for one specific reason.** The generation release
gate is fail-closed: `evaluateGenerationRelease`
(`apps/oshun/bff/src/generation/release-gate.ts`) blocks on
`governance_measurement_absent` when the evidence is missing rather than
releasing. A restored gate row that says `pass` while its evidence did not come
back is the one input that turns a fail-closed gate into an open one, and only a
recompute distinguishes it from a genuine pass.

Measured on 2026-08-14: 0 gate results on this box — measured, relation present.

## 9. releases

**Stores.** `v1_metis_publication_package` — `release_id TEXT NOT NULL` and
`release_history JSONB NOT NULL`, alongside `distribution` and `governance` —
and `admin_store_snapshot` store key `admin-editorial-release-streams`.

**This subject was previously recorded as absent from V1, and that was wrong.**
The search behind it was for a table whose NAME matches `release`, and V1's
release state is carried in COLUMNS on the publication package and in a snapshot
store key. It is the same error §0 describes and the third time this estate has
produced it.

**The procedure.** Reconcile by `release_id`: every restored publication package
must have its own `release_id` present in its `release_history`, and every
release the history records as published must be compared against
`dep:external-publish-target`. A publication is externally visible by definition
— it is the act of putting something where other people can see it — so a
restored package that believes it published something it did not, or does not
know about something it did, is the failure this reconciliation exists to catch.

**The gap, stated.** There is no readback from an external publish target in
this repository. `libs/oshun/workbench-kit/src/target-verification.ts` models
one — a `externalRef` read back from the target after a write — and nothing
calls it against a real target. So this procedure's external side is
**unavailable**, and that is declared in §10 rather than papered over with a
verification that reads our own rows.

Measured on 2026-08-14: 0 publication packages and 1 editorial release stream
bucket on this box.

## 10. The first action, which did not exist

**`scripts/operations/v1-restore-reconcile.mjs` is named as the required next
control by the restore drill and by every disaster-recovery runbook, and nothing
in this repository produces its input.** It takes `--input FILE` describing
seven collections on a restored side, the same seven on an external side, and a
per-collection expected count carrying its source and the instant it was
recorded. A search for the input schema string
`oshun.v1-restore-reconciliation-input.v1` returns three occurrences: the
validator that consumes it, that validator's unit test, and the kit scan that
builds a synthetic one. The operator following the runbook reaches the required
control and the step has no first action.

**And it has no first action for a reason worth stating, because it decides what
to build rather than who to blame.** The reconciler's seven collections are
`publications`, `providerEffects`, `queues`, `approvals`, `invoices`,
`artifacts` and `placements`, matched on `externalRef` or `idempotencyKey`. The
V1 schema has 146 tables and **no column named `external_ref` or
`idempotency_key` in any of them** — `external_refs`, plural and JSONB, exists
on two tables and is a different thing. Four of the seven collections have no
table at all, and `artifact_detection_report`, the nearest name match, is about
visual artifacts in generated media rather than build artifacts: an answer about
the wrong subject in the right shape. The input has no producer because it
describes an estate this is not, and writing one would mean inventing the
columns to fill it. What this estate has is the nine state classes above, and
what they need is an inventory of themselves.

**`scripts/operations/v1-replay-inventory.mjs` is that first action.** It reads
the nine state classes above out of the V1 database and writes them down:

```bash
# On the source, BEFORE the loss — or from the most recent restore of a
# capture taken before it. This is the expectation.
OSHUN_V1_DATABASE_URL=postgresql://oshun:...@host:5432/oshun_dev \
  node scripts/operations/v1-replay-inventory.mjs --output source.json --side source

# On the recovery host, AFTER the restore.
OSHUN_V1_DATABASE_URL=postgresql://oshun:...@recovery:5432/oshun_dev \
  node scripts/operations/v1-replay-inventory.mjs --output restored.json --side restored

# The comparison. Exits 2 unless every subject was compared and nothing is missing.
node scripts/operations/v1-replay-inventory.mjs \
  --compare source.json restored.json --output reconciliation.json
```

What it refuses to do is the point:

- **It never reports a count for a store it could not read.** A subject whose
  store is absent is `unmeasurable` with the reason. `0` and "there was nothing
  to look in" are the same number and opposite facts, and a reconciliation that
  cannot tell them apart is the one §S10.11 found passing over seven empty
  inventories.
- **It refuses a comparison whose two sides came from the same host**, and one
  where both sides carry the same label. Two readings of one machine are two
  readings of one machine whatever they are called.
- **A clean verdict requires coverage.** `reconciled` is false while any subject
  is `unusable`, so "nothing was lost" cannot be reported by an inventory that
  looked at nothing. On this box it is false today, because `leases` is
  unmeasurable — which is the honest answer.

**What it does not do, said plainly.** It does not produce
`v1-restore-reconcile.mjs`'s input, and it is not a drop-in for it — for the
reason two paragraphs up. It reads OUR side. Two of the three externally-visible
subjects have no external side to read: there is no provider receipt store for
generation jobs, and no readback from `dep:external-publish-target` for
releases. Those are declared as gaps in the statement below and are two of the
findings this cell hands to §S10.12.g. The third, provider outcomes, has a real
external authority in the chain, and reconciling against it is a procedure an
operator performs with the chain explorer rather than a script this repository
can run offline.

## 11. Machine-checkable statement

The nine procedures in the shape `replayFaults`
(`libs/oshun/workbench-kit/src/game-day.ts`) grades, plus the estate binding
`libs/oshun/workbench-kit/tools/replay-scan.mjs` checks against the repository:
every store named here must exist, every entry point must exist and have a
caller, and every subject declared absent must still be absent.

<!-- oshun:replay -->

```json
{
  "id": "replay:v1",
  "version": "1",
  "producer": "scripts/operations/v1-replay-inventory.mjs",
  "procedures": [
    {
      "subject": "jobs",
      "kind": "revalidate",
      "idempotencyKey": "jobId",
      "windowStart": "recovery-point",
      "fencingToken": null,
      "verification": "after boot, the count of needs_review jobs carrying generation_interrupted_after_durable_claim equals the count of running jobs in the source-side inventory",
      "externallyVisible": true,
      "externalAuthority": null
    },
    {
      "subject": "leases",
      "kind": "revalidate",
      "idempotencyKey": "attemptId",
      "windowStart": "recovery-point",
      "fencingToken": "lease_token",
      "verification": "recoverExpiredLeases returns the reclaimed attempts and a second call returns none",
      "externallyVisible": false,
      "externalAuthority": null
    },
    {
      "subject": "outbox",
      "kind": "replay",
      "idempotencyKey": "messageId",
      "windowStart": "recovery-point",
      "fencingToken": null,
      "verification": "reconcilePendingAcademicIntegrityNotifications returns pending 0, recomputed after the drain rather than assumed",
      "externallyVisible": false,
      "externalAuthority": null
    },
    {
      "subject": "inbox",
      "kind": "revalidate",
      "idempotencyKey": "notificationId",
      "windowStart": "last-processed-marker",
      "fencingToken": null,
      "verification": "listPendingNotifications returns the intents minus deliveredNotificationIds, and is empty after the drain",
      "externallyVisible": false,
      "externalAuthority": null
    },
    {
      "subject": "events",
      "kind": "recompute",
      "idempotencyKey": "workbench_event.seq",
      "windowStart": "recovery-point",
      "fencingToken": null,
      "verification": "crosscheck-intent-plane.ts rebuilds the projection with replayLedger and reports it equal to the rows",
      "externallyVisible": false,
      "externalAuthority": null
    },
    {
      "subject": "provider-outcomes",
      "kind": "reconcile-counts",
      "idempotencyKey": "invoiceId",
      "windowStart": "recovery-point",
      "fencingToken": null,
      "verification": "every restored confirmed invoice carries its txId and receipt, and every restored pending invoice is re-checked against the chain before it is expired",
      "externallyVisible": true,
      "externalAuthority": "the chain transaction named by invoice.txId, queried by blockHash and blockHeight"
    },
    {
      "subject": "pending-uploads",
      "kind": "none",
      "idempotencyKey": null,
      "windowStart": "none",
      "fencingToken": null,
      "verification": null,
      "externallyVisible": false,
      "externalAuthority": null
    },
    {
      "subject": "gates",
      "kind": "revalidate",
      "idempotencyKey": "payload_hash with contract_version",
      "windowStart": "recovery-point",
      "fencingToken": null,
      "verification": "recomputing each gate over its restored source_record_id under its recorded contract_version reproduces the stored verdict",
      "externallyVisible": false,
      "externalAuthority": null
    },
    {
      "subject": "releases",
      "kind": "reconcile-counts",
      "idempotencyKey": "release_id",
      "windowStart": "recovery-point",
      "fencingToken": null,
      "verification": "every restored publication package carries its own release_id in its release_history",
      "externallyVisible": true,
      "externalAuthority": null
    }
  ],
  "binding": [
    {
      "subject": "jobs",
      "dependencyIds": [
        "dep:primary-store",
        "dep:job-queue",
        "dep:provider-gateway"
      ],
      "stores": [
        { "kind": "snapshot-key", "name": "generation-jobs" },
        { "kind": "snapshot-key", "name": "metis-ingest-jobs" }
      ],
      "entryPoints": [
        "apps/oshun/bff/src/generation/jobs-route.ts",
        "apps/oshun/bff/src/metis/ingest-job-store.ts"
      ],
      "absent": false
    },
    {
      "subject": "leases",
      "dependencyIds": ["dep:primary-store"],
      "stores": [
        {
          "kind": "table",
          "name": "live_media_pipeline_job_attempts",
          "migrations": "libs/aphrodite/database/prisma/migrations",
          "database": "aphrodite"
        }
      ],
      "entryPoints": [
        "libs/shared/live-media/src/sql-media-pipeline-job-store.ts",
        "libs/shared/live-media/src/media-pipeline-worker.ts"
      ],
      "absent": false
    },
    {
      "subject": "outbox",
      "dependencyIds": ["dep:primary-store"],
      "stores": [{ "kind": "snapshot-key", "name": "metis-academic-records" }],
      "entryPoints": [
        "apps/oshun/bff/src/metis/academic-integrity-appeal-store.ts",
        "apps/oshun/bff/src/metis/academic-integrity-notification-worker.ts"
      ],
      "absent": false
    },
    {
      "subject": "inbox",
      "dependencyIds": ["dep:primary-store"],
      "stores": [{ "kind": "snapshot-key", "name": "metis-academic-records" }],
      "entryPoints": [
        "apps/oshun/bff/src/metis/academic-integrity-appeal-store.ts"
      ],
      "absent": false
    },
    {
      "subject": "events",
      "dependencyIds": ["dep:primary-store"],
      "stores": [
        {
          "kind": "table",
          "name": "workbench_event",
          "migrations": "libs/oshun/persistence/prisma/migrations",
          "database": "oshun_dev"
        }
      ],
      "entryPoints": [
        "apps/oshun/bff/src/workbench/intent-store.ts",
        "apps/oshun/bff/scripts/crosscheck-intent-plane.ts"
      ],
      "absent": false
    },
    {
      "subject": "provider-outcomes",
      "dependencyIds": ["dep:primary-store", "dep:provider-gateway"],
      "stores": [{ "kind": "snapshot-key", "name": "crypto-invoices" }],
      "entryPoints": ["apps/oshun/bff/src/payments/invoice-store.ts"],
      "absent": false
    },
    {
      "subject": "pending-uploads",
      "dependencyIds": ["dep:blob-store"],
      "stores": [],
      "entryPoints": [],
      "absent": true,
      "absenceEvidence": {
        "noTableMatching": "upload",
        "patternElsewhere": "apps/yemaya/svc-study-workspace/migrations/0016_study_upload_session.sql"
      }
    },
    {
      "subject": "gates",
      "dependencyIds": ["dep:primary-store"],
      "stores": [
        {
          "kind": "table",
          "name": "v1_tara_workbench_review_gate_result",
          "migrations": "libs/oshun/persistence/prisma/migrations",
          "database": "oshun_dev"
        }
      ],
      "entryPoints": ["apps/oshun/bff/src/generation/release-gate.ts"],
      "absent": false
    },
    {
      "subject": "releases",
      "dependencyIds": ["dep:primary-store", "dep:external-publish-target"],
      "stores": [
        {
          "kind": "table",
          "name": "v1_metis_publication_package",
          "migrations": "libs/oshun/persistence/prisma/migrations",
          "database": "oshun_dev"
        },
        { "kind": "snapshot-key", "name": "admin-editorial-release-streams" }
      ],
      "entryPoints": ["libs/oshun/workbench-kit/src/target-verification.ts"],
      "absent": false
    }
  ]
}
```
