Admission
Status: design draft for review · decided, not built
Admission is the pipeline every write crosses before anyone but its author can read it:
capture → quarantine → deterministic scan → non-author review → ratification → journal → release.
This page owns that pipeline (decision D11): the lifecycle states, where held bodies live, what the scan does, what a review policy object contains, who supplies the review capacity, and what happens when that capacity is absent. The roles that act in the pipeline are defined in roles and keys; the workarea statuses that select which policy applies are defined in workareas; the transactional commit point and the outbox that performs release are defined in storage. The constitution behind all of it — the gate is not callable by the thing being gated, the reviewer is never the author, the gate changes visibility, not ranking, no silent deletion — is declared in ADR-0001 and not restated here.
Nothing below exists as code. Every mechanism is a design commitment with its exit criterion tracked in the roadmap.
Lifecycle
The durable state column on a memory row takes four values: held, promoted, demoted,
erased (the row shape is in the Postgres schema). Scanned
and under review are phases within held — the journal records the scan verdict and each
review verdict as events, but a claim that has been scanned and is waiting for a reviewer is
still held: invisible to everyone but its author. The gate changes visibility exactly once,
at ratification.
stateDiagram-v2
[*] --> held : capture (journalled, digest-only payload)
state held {
[*] --> captured
captured --> scanned : deterministic scan, same call
scanned --> under_review : enters the review queue
}
held --> rejected : scan veto (same call)
held --> rejected : rejecting verdict
held --> dead_letter : held-item expiry (journalled)
held --> promoted : ratification (k verdicts met, policy snapshot journalled)
rejected --> held : non-author reopen (journalled)
dead_letter --> held : journalled re-enqueue
promoted --> held : reassignment to a stricter-policy workarea
promoted --> demoted : demote (journalled, bytes kept)
promoted --> erased : erase (crypto-shred, journalled)
demoted --> erased : erase
Reading the diagram:
- Capture. A
remember/learncall commits one Postgres transaction: the signed journal event (digest or ciphertext reference only — never body plaintext), the metadata row in stateheld, and an outbox row. The write is durable iff that transaction commits; the commit protocol is storage's to define. The author can see and supersede their own held claim; nobody else can see it at all. Bans are checked at capture, before quarantine or scan: a banned author's or source's submission is refused at the door with a typed error, and nothing from it is held, stored or queued (roles and keys, security). - Scan happens in the same call as capture and can veto — see the deterministic scan below. A veto is a journalled rejection, not a dropped request: the caller gets a typed error naming the rule, and the event stays in the journal.
- Review and ratification. Reviewers record verdicts; when the workarea's policy is satisfied (k approving verdicts from identities meeting the independence axis), the server ratifies: a journalled promotion event carrying a snapshot of the policy it was decided under. Only ratification makes the claim readable by others.
- Release is asynchronous and follows promotion: the outbox drains to the lake (storage). Release never gates visibility — reads are served from Postgres — so a lagging lake delays analytics, not availability.
- Dead letter. A held item that reaches its policy's expiry without a decision is dead-lettered: a journalled event, never a silent drop. It is refused promotion in that state; a journalled re-enqueue (an admin or dreamer act) returns it to the queue. This is the admission-side dead letter; the lake-writer keeps a separate dead-letter table for rows that fail materialization after promotion, owned by storage and reported by reconciliation.
- Reassignment of a promoted claim into a workarea whose policy is stricter than the one
it was ratified under re-enters
heldand is reviewed again; the journalled move records both policy snapshots (roles and keys). - Demoted and erased are post-promotion states, not admission outcomes. Demotion removes a claim from every consumer surface and keeps the bytes; erasure is crypto-shredding with a named inventory, defined in audit and lineage. Both remain possible in a validated (frozen) workarea.
One rule holds across every workarea status: effective access decides who may write
(defined in workareas); status changes only which review policy applies,
never grant resolution. A development workarea may, by journalled tenant policy, permit
marked self_signed rows — the one bounded exception, scoped in
ADR-0001; such rows never promote beyond that workarea.
Simulation writes pass this same pipeline with their audience as the reviewer pool
(simulations).
Quarantine placement
A held body never reaches the lake. Until promotion, body bytes live in exactly one of two places:
- Postgres, alongside the metadata row, or
- the quarantine bucket —
oml-quarantine/<tenant>/<digest>in the object store, readable and writable only by the service's own credentials; no client, crew, or downstream component (lake-writer, indexer, reconciler) holds a credential that reaches it.
The body_ref column records which location class a body took; both placements are inside the
design, and the criterion for choosing between them per body (a size threshold is the obvious
candidate) is an implementation choice the schema accommodates, not a decision this page makes.
What is decided: bronze lake events mirror the journal with digest-only payloads, and silver
materialization is enqueued by promotion only — so no path exists by which held bytes appear in
any lake zone, checkpoint, or Delta version. The cost of this placement is a copy at release:
a promoted body moves from quarantine to its ciphertext object and lake row, paid once per
promotion rather than once per read.
Attachments upload into the same quarantine regime through resumable upload sessions, bound into the submission envelope by digest; their storage protocol is storage's.
The deterministic scan
The scan runs in the same call as capture and returns before the call does — the writer learns the scan outcome synchronously, and a veto means the submission is journalled as rejected and nothing waits in the queue. The scan is same-call by design: an asynchronous scan either delays the author's error to a moment they are no longer listening, or admits unscanned bytes into the queue.
Deterministic is a load-bearing word: the scan is rules, not a model. Same input, same verdict, reproducible from the journalled rule-set version — which is what lets a scan verdict appear in an evidence export without a caveat about sampling temperature. The cost is the complement: a deterministic scan catches what its rules name (secret patterns, format violations) and nothing else. Judgment belongs to review, which is why the scan can veto but can never promote.
For attachments, the scan covers what determinism can reach: where a deterministic text extractor exists for the type, the extracted text is scanned and gated; for opaque binaries the reviewed claim is provenance + digest + scan result, stated honestly per type (storage owns the attachment rules). Scan latency is unmeasured; a rule pass is expected to scale with body bytes, and no figure is asserted until one is measured.
Review policy objects
Each workarea carries a review policy object: a journalled record, versioned by the journal like everything else, snapshotted into every ratification event so an auditor can read a promotion against the exact policy it satisfied. Its fields:
| Field | Meaning |
|---|---|
k |
approving verdicts required for ratification |
required_roles |
which roles' verdicts count (dreamer-review; human where named) |
human_required_classes |
claim classes (by kind, scan finding class, or provenance) whose ratification requires at least one human verdict via the CLI |
independence_axis |
how far apart reviewer and author must be: reviewer principal ≠ author principal always (constitution R2, enforced at promotion against the row's lineage closure — roles and keys); optionally, for committed workareas by policy, reviewer crew / external manager ≠ author crew, resolved from journalled key→crew→manager metadata at promotion time (roles and keys) |
held_item_expiry |
how long an item may wait in the queue before it is dead-lettered |
Policy objects are per workarea because workareas mirror crews with different stakes: a
development area's policy may be permissive to the marked limit the constitution allows; a
committed area requires full non-author review; a validated area refuses new writes
entirely, so its policy governs nothing new (workareas owns the ladder).
Changing a policy is a journalled admin act and applies to decisions made after it — items
already under review ratify against the policy snapshot they will be journalled with, which is
the one in force at promotion time.
What the policy object deliberately does not contain: any ranking, scoring, or confidence field. The gate changes visibility, not ranking; a policy decides whether, never how prominently.
Review capacity: external crews
The provider runs no model. Review capacity is external crews holding dreamer-class keys,
recording verdicts over the same HTTP API every other caller uses — a crew's reviewer agent
is a client, with a key whose verb ceiling is exactly the dreamer surface
(roles and keys), and the server's job is to refuse verdicts that break
the rules (author in the lineage closure, no valid serve, policy not met), not to produce
verdicts. Human review through the CLI is the exception path, reserved by
human_required_classes and available whenever a person holds a dreamer key
(CLI reference owns the review ceremony).
A verdict counts only against a valid serve: the server binds a serve to (reviewer key, artifact version, attachment digest set), expires it, consumes it on first verdict, and invalidates it when the served content changes; the journalled verdict records the served digests, so the evidence export can show what the reviewer was shown, not just what they decided. The serve binding is defined with the CLI ceremony in the CLI reference and enforced server-side for every reviewer, crew or human. A reviewer is served every attachment of a claim before a verdict counts (storage).
This design trades operational simplicity for an honest dependency: review quality and review throughput are properties of the tenant's crews, not of the provider. The provider makes collusion visible rather than impossible — the evidence export surfaces reciprocal-approval statistics per key pair — and makes independence checkable at promotion time; it cannot make an external crew diligent. These decisions — review composition, the served-reviewer rule (a signature counts only from a reviewer the gate served), and the division of labour (crews review; reviewer keys record verdicts and nothing else) — are recorded in ADR-0012.
Fail closed
When review is unavailable, nothing promotes. Concretely:
- A workarea whose policy no live key can satisfy (no dreamer key with review capability in
scope, or none outside the author's lineage closure) accumulates held items until a key
exists or the items expire into the dead letter. The server does not relax
k, does not widen the role set, and does not auto-promote on timeout. - The lineage case deserves naming, because any writer can produce it: every artifact a
submission's
derives_fromnames expands the row's reviewer-exclusion set — R2 runs over the whole lineage closure — so a submission whose declared ancestry excludes every eligible reviewer in the workarea is unreviewable and therefore unpromotable. It is not refused at capture (reviewer unavailability is deliberately not an API error, errors.md); it lands held, surfaces inoml status/oml doctoras review unavailability, and dead-letters on expiry. Declaring ancestors can only shrink who may judge a row, never select a friendlier judge — and a false or padded lineage is itself grounds for rejection at review (ADR-0012). - A simulation whose audience contains no dreamer can promote nothing (simulations).
- A verdict whose serve has expired or whose served digests no longer match the artifact is refused; the reviewer is re-served.
- Errors in the pipeline are typed and name their rule (error taxonomy); an unclassifiable failure refuses the write rather than admitting it unscanned.
The cost of failing closed is availability: a tenant that under-provisions review capacity
experiences admission as an outage for new knowledge, visible in oml status as queue depth
and review availability (CLI reference). That is the intended shape —
the alternative, failing open, converts a capacity problem into a trust problem, and trust is
the product.
Reopening a rejection
Rejection is journalled, never silent, and not necessarily final. A non-author — a
dreamer(review) key whose key and principal do not appear in the row's lineage closure — may
reopen a rejected item as a journalled verb, returning it to review. Admin keys cannot reopen:
reopen is a review-family verb, and admin ≠ reviewer (roles-and-keys.md). The reopen event names who reopened and
why; the original rejecting verdict stays in the journal and in the evidence export. The
author cannot reopen their own rejection (that would be self-review by another door); they
can submit a new version under the same claim key, which enters the pipeline as a fresh held
item with its derives_from lineage intact.
Throughput is the honest open question
Review throughput for OML is unmeasured — no code exists to measure. The roadmap carries review-throughput measurement as FR-2, with an exit criterion (a measured drain rate for a stated policy and crew configuration, published beside the method).
One structural expectation can be stated without a measurement: ingress throughput is not end-to-end review capacity, and the queue, not the store, is where an admission-controlled system saturates. The binding constraints will be the tenant's crew capacity and the server's verdict path — both unmeasured until FR-2's exit criterion is met.