# `@oshun/workbench-kit`

The domain-neutral workbench layer: UI shell and primitives, plus the invariants
a domain plugin may not override.

## Ownership

| Field           | Value                                                                                                                                                                                                 |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Package         | `@oshun/workbench-kit`                                                                                                                                                                                |
| Layer           | `layer:workbench-kit` (ADR-S1)                                                                                                                                                                        |
| Scope           | `scope:oshun`                                                                                                                                                                                         |
| Owning decision | ADR-S1 (package ownership), ADR-S2 (extension model)                                                                                                                                                  |
| Ledger cells    | S1.1–S1.14 + S2.1: scaffold, exports, tags, identity, envelopes, states, registry, routes, plugins, errors, negotiation, deprecation, test-utility separation, generated API reference, stage machine |
| Approval        | S0.6.f — named architecture/owner approval is **pending**, not implied                                                                                                                                |

## Dependency direction

The kit may depend **downward** only:

```
layer:app          deployable applications        ──┐
layer:bff          transport/durability           ──┤ may reach the kit
layer:domain       domain semantics + plugins     ──┘
layer:workbench-kit ──► layer:shared-logic ──► layer:contracts
```

It may import `@oshun/studio-authoring` (`layer:shared-logic`) and
`@oshun/contracts` (`layer:contracts`). It may **not** import a domain library,
a BFF, an app, or a peer at its own layer.

Enforced by two `@nx/enforce-module-boundaries` constraints keyed on
`layer:workbench-kit`, stated twice over on purpose: an allow-list naming the
two layers beneath it, plus a deny-list naming the three above it, so widening
the allow-list — the easy, plausible-looking edit — does not silently re-open an
upward edge.

`pnpm nx boundary-fixtures @oshun/workbench-kit` proves it rather than asserting
it: five real TypeScript files are written into the package, linted by the
repository's real ESLint, and deleted. Two permitted edges must be accepted and
three forbidden ones rejected — a constraint that has never refused anything is
indistinguishable from one that is not running.

## Public API

| Subpath                           | Runtime class | Authorized by   |
| --------------------------------- | ------------- | --------------- |
| `@oshun/workbench-kit`            | `contract`    | ADR-S2 / S1.1.a |
| `.../capability-state`            | `contract`    | S1.6            |
| `.../concurrency`                 | `contract`    | S2.3            |
| `.../decisions`                   | `contract`    | S2.7            |
| `.../deprecation`                 | `contract`    | S1.12           |
| `@oshun/workbench-kit/diff`       | `contract`    | S2.5            |
| `@oshun/workbench-kit/envelopes`  | `contract`    | S1.5            |
| `@oshun/workbench-kit/errors`     | `contract`    | S1.9 / S1.10    |
| `.../gate-definitions`            | `contract`    | S2.9            |
| `@oshun/workbench-kit/gate-runs`  | `contract`    | S2.10           |
| `.../gate-waivers`                | `contract`    | S2.11           |
| `@oshun/workbench-kit/identity`   | `contract`    | S1.4            |
| `@oshun/workbench-kit/invariants` | `contract`    | ADR-S2 / S0.7.g |
| `.../negotiation`                 | `contract`    | S1.11           |
| `@oshun/workbench-kit/plugins`    | `contract`    | S1.9            |
| `@oshun/workbench-kit/registry`   | `contract`    | S1.7            |
| `.../restore`                     | `contract`    | S2.6            |
| `.../revisions`                   | `contract`    | S2.4            |
| `@oshun/workbench-kit/routes`     | `contract`    | S1.8            |
| `@oshun/workbench-kit/scores`     | `contract`    | S2.8            |
| `@oshun/workbench-kit/audit`      | `contract`    | S2.14           |
| `@oshun/workbench-kit/deletion`   | `contract`    | S3.3            |
| `.../idempotency`                 | `contract`    | S3.4            |
| `@oshun/workbench-kit/indexes`    | `contract`    | S3.5            |
| `@oshun/workbench-kit/outbox`     | `contract`    | S3.6            |
| `@oshun/workbench-kit/inbox`      | `contract`    | S3.7            |
| `.../quarantine`                  | `contract`    | S3.8            |
| `@oshun/workbench-kit/jobs`       | `contract`    | S3.9            |
| `.../job-graphs`                  | `contract`    | S3.10           |
| `@oshun/workbench-kit/blobs`      | `contract`    | S3.11           |
| `.../reclamation`                 | `contract`    | S3.12           |
| `.../protection`                  | `contract`    | S3.13           |
| `@oshun/workbench-kit/probes`     | `contract`    | S3.14           |
| `@oshun/workbench-kit/tenancy`    | `contract`    | S3.2            |
| `@oshun/workbench-kit/signoff`    | `contract`    | S2.13           |
| `.../staleness`                   | `contract`    | S2.12           |
| `@oshun/workbench-kit/stages`     | `contract`    | S2.1            |
| `@oshun/workbench-kit/synthetic`  | `contract`    | S1.13.d         |

Test-only subpaths, which no production source may import:

| Subpath                        | Runtime class   | Authorized by             |
| ------------------------------ | --------------- | ------------------------- |
| `@oshun/workbench-kit/testing` | `test-contract` | S1.2.c / S1.2.f / S1.13.b |
| `.../testing/assertions`       | `test-contract` | S1.13.b                   |
| `.../testing/concurrency`      | `test-contract` | S2.3.b                    |
| `.../testing/doubles`          | `test-contract` | S1.13.b                   |
| `.../testing/envelopes`        | `test-contract` | S1.13.b                   |
| `.../testing/identity`         | `test-contract` | S1.13.b                   |
| `.../testing/invariants`       | `test-contract` | S1.2.c / S1.2.f           |

This table is **derived, not authored**. `package.json` `exports`,
`tsconfig.base.json` aliases and this list all have to equal what
`scripts/v1-workbenches/generate-workbench-kit-export-map.mjs` computes, and an
entry point is only computed when one of two things authorizes it: a ratified
row in the S0.12 shared capability matrix accepted by at least two domains
(S0.11), or a named kit-owned ADR decision. Adding a line to the manifest fails
the gate.

There is deliberately **no wildcard subpath and no `@oshun/workbench-kit/*`
alias**. Either one re-opens every internal module as a deep import, and the
alias is the easier one to miss: an in-workspace consumer never reads
`package.json` at all. (The kit's ADR-S1 sibling `@oshun/studio-authoring` does
ship that wildcard, which is why its absence here is asserted rather than
assumed.)

### Runtime classes

`contract` ⊂ `neutral` ⊂ `browser` ⊂ `react`, with `server` a sibling of
`browser`/`react`. A `browser` entry point may not reach a Node built-in, a
`server` one may not touch the DOM, and **no production entry point may reach a
test-only module** — checked over the real module graph, again over the emitted
package, and again by a lint rule over every consumer in the repository.

The test classes are two, not one. `test-contract` is runtime-agnostic and
genuinely constrained — no Node built-in, no DOM global, no React — so the
conformance corpus runs under whatever runner a domain already has. That
constraint has already caught something: a `process.env` read in the runtime
guard, which compiles only where `@types/node` is installed. `test-server` is
declared with **no members**, so the first helper that needs a temp directory or
a child process is placed there rather than smuggled into the agnostic class.

## What is here, and why so little

`src/invariants.ts` — the six kit-owned invariants from ADR-S2, as a
runtime-checked closed mutation envelope rather than a document a plugin is
trusted to have read. The load-bearing one is `no-escape-hatch`: every other
invariant is defeated by a single `any`/`raw` field on the envelope, so
`assertMutationEnvelope` rejects **unknown** keys as well as missing ones.

`src/identity.ts` — the branded types every capability is addressed by
(`TenantId`, `WorkbenchId`, `ActorRef`, `ArtifactRef`, `RevisionRef`,
`StableAnchor`). The failure mode these exist to stop is not a missing type but
a `string`: a tenant id assigned to a workbench parameter typechecks, reaches
the database, and returns somebody else's rows. Three decisions shape it —
parsing is strict and normalising is explicit, there is **no** unsafe cast, and
parsers return results rather than throwing. All three are argued in the module
header.

`src/envelopes.ts` — the five versioned envelopes every workbench message
travels in (command, query, event, job update, capability probe). Closed shapes
with exactly **one** open field: `extensions`, namespaced `<vendor>.<name>`,
which the kit never reads. That one field is the whole compatibility story — it
is what lets a NEWER envelope be tolerated when it parses (the producer only
added extensions) and refused when it does not (the producer added a real field
this build cannot honour). Golden wire fixtures live in
`evidence/v1-workbenches/workbench-kit-envelope-goldens.json`: 57 committed
cases, each rejection naming the error code it must produce.

`src/capability-state.ts` — the five states a capability can honestly be in.
`./invariants.ts` ships honest-capability as a minimal two-state shape; this is
the full vocabulary, and the three states in the middle are the ones a caller
gets wrong. `degraded` must name **both** what broke and what still works —
nothing retained is `unavailable`, not degraded, and that is the whole
distinction. `unavailable` has structurally nowhere to put a substitute value.
`failed` means a probe completed and said no, which has the opposite retry
semantics from "we could not ask". `matchCapabilityState` is total, and
`usability()` is deliberately three-valued because a boolean cannot represent
`degraded` without rounding it to a neighbour.

`src/registry.ts` — the typed capability registry: what each capability needs,
who owns it, and what breaks if it goes away. Two decisions worth arguing with.
**Collisions are errors, not merges** — `mergeCapabilitySources` refuses two
entries with the same id and names both sources, because last-write-wins is how
a domain plugin silently replaces a shared capability in an order that depends
on directory iteration. And the **probe and reference projections are derived**,
not maintained beside it; both are easier to hand-edit than to regenerate, which
is exactly why they drift.

`src/routes.ts` — the route descriptor. A route is the one artifact three
audiences read: a router mounts it, a menu renders it, and a share link resolves
it, and those three disagree constantly. Three rules carry the weight. A
tenant-scoped route must say whether its scope comes from the **path or the
session, never both** — a route taking a tenant from two places is one where
they can disagree and whichever the handler reads first wins. Share safety is
checked against the **data classification**, because a descriptor is where
somebody sets `public` for convenience months after the classification was
decided elsewhere. And a flag carries **either an expiry or a written reason it
is permanent**, never neither. The navigation projection returns its
**exclusions with reasons**, so a route is never silently dropped or silently
mounted.

`src/plugins.ts` — the seven domain plugin extension points (stage models,
gates, blocks, previews, inspectors, importers, publishers) and the checks over
them. The kit owns the **shape** of each contract and never its **vocabulary**:
a format is a spelling rule, a vocabulary is a decision about the domain's
model, and the kit enforces only the first. Four rules carry the weight. A
**declaration and a contribution must agree in both directions** — contributing
to an undeclared capability widens a plugin's own surface, and declaring one
with no contribution is a capability that resolves to nothing. A **measurement
is not a verdict**: the evaluator measures, the policy decides, and
`gateVerdict` reports which of the two decided in `decidedBy`, because the kit —
not the policy — resolves absence, failure and staleness, so no domain can write
a policy that treats "the gate did not run" as "the gate passed". **Nobody
verifies their own work**: a publisher's verification names its source and the
executor's own report is not a member of the type. And **absence is a value,
never an exception**.

`src/errors.ts` — the stable error codes and the safe public envelope. An error
crosses the boundary twice: once as a CAUSE (rich, internal, holding the stack
and the SQL and whatever the provider said) and once as an ENVELOPE (stable,
safe, public). The failure this prevents is the two being written as one object
with the safe one produced by **deleting** fields from the rich one — deletion
is a blocklist, and a blocklist is wrong the first time somebody adds a field.
So the envelope is **constructed**: only scalars, only under parameter keys the
registry declared for that code, scanned for canary shapes, and joined to the
cause by a correlation id an authorized support lookup resolves. Mapping is on a
**classifier, never a message** — a provider's prose changes in a release nobody
here reads, and a mapping keyed on it fails open with different retry semantics.
Transport and retry are properties of the code and are checked against each
other: a 4xx that says "retry after backoff" tells a client to hammer a request
that can never succeed. A retired code is never reused, because an old client
still has the old meaning compiled in.

`src/negotiation.ts` — schema-version negotiation and discovery. A contract
version travels over six transports that agree on almost nothing, so the kit
names **one carrier per transport** and refuses a second: two places a version
can be written are two places it can disagree. **Negotiation needs a live
counterparty** — HTTP and the desktop bridge have one, an event and a job and a
bundle do not, so those may not declare themselves negotiable and their
producers have to be conservative instead. This asymmetry is the reason a single
version-negotiation story usually goes wrong: it is written for request/response
and then applied to a queue. Refusals distinguish **too old** from **too new**
from **unsupported**, because only the first two tell anybody which side to
upgrade. There is **no silent downgrade**: a negotiated version that is not the
client's first choice reports the departure and its direction. And the
negotiated version is **bound to everything downstream** — a cache key without
it serves a v1 body to a v2 client, and an idempotency key without it dedupes a
v2 retry against the v1 original and returns the old shape with a 200.

`src/deprecation.ts` — deprecation metadata and telemetry. The cell's two halves
pull against each other: anything loud enough for a consumer to notice is loud
enough to change what their code does. The resolution is that the warning is
always **out of band** (headers, envelope extensions, stderr — never the success
payload) and **caller-independent**, proven by comparing two warnings rather
than by reading the implementation; the OBSERVATION is the server's, so a
consumer that never reads a header still shows up in the telemetry. Usage is
recorded against **cohorts, never tenants**, with a below-threshold bucket for
cohorts small enough to identify somebody. An **exception expires by itself** —
an expired one stops permitting a removal rather than continuing to block it —
and our own staging traffic does not block one at all. The closure checklist
refuses to be completed while the telemetry counter or the compatibility adapter
remains: a counter for a removed thing sits at zero forever and reads as
success.

`src/stages.ts` — the stage machine. S1.9 lets a plugin DECLARE a stage model
and has the host validate it at boot; this decides whether a transition may
happen. The two are separate because they fail differently — a bad model is a
boot error somebody fixes before shipping, and a bad transition is a published
document nobody can unpublish.

The property the module exists for is that **an illegal transition cannot be
expressed**. Not rejected at runtime — expressed. `plan()` takes only a
transition id the graph contains, and moving an object anywhere else requires
`PrivilegedBypass`, a separate type carrying an authority, a reason, a scope,
its evidence and the audit event it is linked to. There is no third path,
because the third path is what every workflow engine grows under deadline: a
`force` boolean, then a `force` boolean with a comment.

Three more decisions carry weight. Nothing here **writes** a stage — the engine
returns a decision and a planned write, and the caller applies it under the
expected revision the plan carries, because a module that can both decide and
write is one where the decision can be skipped by calling the writer. A command
missing a verdict is **indeterminate, not passing**: two verdicts for a
three-guard transition means a guard nobody ran, and folding what was carried
reports success. And an **undeclared cycle is refused** — a loop is legitimate
(rework, reopen) and is declared by marking the edge that closes it, because an
undeclared one is invisible in a list of transitions and shows up in production
as an object bouncing between two stages while the time-in-stage metric looks
healthy.

The two graph analyses — reachability and cycles — are the extraction the cell
asks for: `plugins.ts` used to carry its own reachability loop, and two
implementations of "can you get there from here?" is one that disagrees with the
other the first time a source set is involved. A plugin model is a weaker shape
than an engine transition, so it lifts into the untyped `EdgeSet` the analyses
work over rather than into a full graph, and nothing invents the fields the
plugin never stated.

`src/concurrency.ts` — optimistic concurrency (S2.3). A `Precondition` is a
revision or an AUTHORIZED create-if-absent, and there is no third value: a
nullable `expectedRevision` makes "I mean to create this" and "I do not know
what revision I am editing" the same thing, and the second one is a lost update.
The other two holes it closes are the ones that survive having a precondition at
all. A conflict carries the target (WHICH row of a 500-line import failed), safe
scalar metadata about the current state, and a token reaching the submission
that was refused — kept verbatim, never merged, and reachable only through
`resumeIntent`, which requires a fresh observation and a RESTATED precondition.
And an automatic retry may not repair the thing it is retrying: after a conflict
`retryDecision` returns `stop-conflict` rather than a refreshed attempt, because
re-reading the revision and trying again is a last-write-win wearing an
optimistic-concurrency costume — it defeats the check on exactly the requests
the check existed for.

The module decides and never writes. The atomicity half is a claim about a
database engine, so it is settled by `pnpm nx revision-race`, which opens real
concurrent connections and drives the kit's own harness against them — see
Targets.

`src/gate-definitions.ts` — the gate as a **catalogue entry** (S2.9), which is a
different thing from S1.9's running contribution and fails differently: a bad
contribution is a bug somebody fixes, and a gate with no owner, no appeal path
and a severity nobody agreed is a governance hole that shows up the first time
it blocks a release at 2am and there is nobody to ask.

Four decisions. **Severity varies by lifecycle stage** — the same failing
measurement is a blocker on a published asset and a warning on a draft, and one
field forces the strictest reading everywhere, which is how a drafting surface
acquires eleven blockers and people learn to work around gates. **Unavailable
evidence has no `pass` option**, as a type rather than a rule: a gate that
passes when its evidence is missing passes when the thing it checks is broken in
the way that hides the evidence. **The verdict vocabulary is S1.9's**, with the
cell's wording published as a mapping rather than restated as a parallel enum.
And **a blocker without an appeal is a dead end** — it produces exactly one
behaviour, which is somebody turning it off.

`conformsToDefinition` is why the cell exists: a definition says what a gate
does when its evidence is stale and a contribution decides what it actually
does. A release manager reads the first and a workflow obeys the second, and
when they differ nothing fails.

`src/gate-runs.ts` — the row a gate **run** writes, and the fold from those rows
to a release decision (S2.10). Both failures this guards are arithmetic dressed
up as governance.

The cell's ten words — blocker, warning, pass, fail, unavailable, unsupported,
stale, missing, waived, error — are **four different kinds of thing**, so there
is no tenth enum: severity belongs to the definition, the verdict to the
evaluator, the non-values to the measurement, and `waived` to none of them
because it is a decision taken after all three. **The fold starts from the
applicable definitions, not from the rows that exist**, because a fold over the
results in hand cannot tell "every gate passed" from "the gate crashed before it
wrote a row" — under which the cheapest way to pass a release is to fail early
enough. **A strategy decides satisfaction and may not decide blocking**: an
`any-of` that covers a failure leaves that component's `blocks` effect intact
and forces the headline to `eligible-by-composition`, which is a different
sentence from `eligible`. **Unknown is a third value**, so `any-of` over a
failure and a check that could not run is neither satisfied nor refuted. And **a
waiver narrows and never creates** — a release policy has no field that makes a
gate waivable when its definition says otherwise.

A domain's own fold is run twice, over the members and over the same members
reversed, and disbelieved if it disagrees with itself; the members are frozen
rather than compared afterwards, so a strategy that writes to them fails in the
fold instead of in a release. `visibilityFaults` is the other half: a correct
fold and a wrong screen are different failures, and rendering a signed exception
in the same green as a pass is how waivers become routine.

`src/idempotency.ts` — the constraints that make a retry safe (S3.4). An
idempotency key is the thing everybody agrees they have and almost nobody scopes
correctly, and the failure is asymmetric: scoped too widely it returns one
caller another's result, scoped too narrowly it runs an irreversible operation
twice. Both are silent.

**The key is not the constraint** — uniqueness is over tenant, client,
operation, operation version and key, plus the target where one is named, and
the encoding is injective because a key two scopes can forge is the exact
failure the module is about. **Same key, different request is a conflict and
never a replay**: returning the stored result reports success for something that
never ran, to a caller who will never look again, so the digest is compared
before the state is read. **An idempotency record's retention is a property of
the operation's reversibility, not a global TTL** — expiring the record for an
irreversible command re-enables a second charge, publish or delete, and a
uniform 24-hour default is how that ships. The module also states **where each
revision constraint can actually be held**, and marks the two that are graph
properties (`parent-is-in-the-same-entity`, `single-head-per-entity`) as
application-enforced rather than writing them down as if a `CHECK` covered them.

`pnpm nx idempotency-contention` settles the half no unit test can: 32 real
connections fired at one key, where exactly one claims the work and the rest are
told it is already running — never nothing. Its negative control is the naive
read-then-write everybody writes first, raced identically, which **must**
perform the effect many times.

`src/quarantine.ts` — poison quarantine and replay (S3.8). S3.6 and S3.7 both
end in the word `quarantined` and neither says what happens next; this is what
happens next.

**The blast radius is the smallest scope that preserves the order, and both
directions are wrong.** Stopping the whole consumer for one bad message is an
outage; stopping only that message and letting the next one through applies
position 4 over a missing 3, which nothing downstream reports — so the scope is
DERIVED from the event kind's commutativity rather than chosen per incident.
**Repeated transient is its own condition**, because a timeout that has failed
twelve times is not a timeout. **Unknown is not a bucket** — it reports itself
as unclassified, since a growing unknown pile is a taxonomy that has stopped
describing reality and nothing about it looks wrong on a dashboard. **The record
holds a reference and a hash, never a second copy of the truth.** **Skip and
suppress are different actions**: skipping abandons one event, suppressing stops
a class of them, and a suppression scoped to a single event is refused as a skip
wearing a standing rule's clothes. **Replaying without a change repeats the
failure**, so a replay is refused unless the handler version moved or the cause
was environmental. **The event identity is preserved and the attempt identity is
new** — the first so the inbox recognises it, the second so the audit can tell
the two runs apart.

`pnpm nx quarantine-blast-radius` is the rare harness with THREE configurations
and two of them required to be visibly wrong: isolating the event left the
projection at [1,2,4,5] with position 3 missing; isolating the consumer left an
unrelated subject with nothing applied; isolating the subject left the poisoned
one waiting and the unrelated one moving. It then replays under the original
event id (the receipt recognises it, one effect) and under a re-minted one (the
receipt misses it, two effects).

`src/jobs.ts` — durable jobs (S3.9). S3.6 and S3.7 move an event between two
processes that are both alive; this is the case where one of them is not, and
does not know it yet.

**A lease is not a lock, and what makes it safe is the fencing token.** A worker
whose lease expired does not know its lease expired — it was paused, the
coordinator handed the job to somebody else, and now it wakes up and writes.
Expiry protects the QUEUE; only a monotone token checked at the WRITE protects
the DATA, so `fenceWrite` is the only thing that produces a `FencedWrite` and
`FENCING_SOURCES` has no `wall-clock` member (two claims inside one millisecond
produce equal tokens, and that looks exactly like a correct implementation).
**Progress is the field whose failure mode reads as success**: `jobProgress`
returns no `done`, an unknown total is `null` and never `0` — a total of zero
renders as a full bar — and a stage at its total on a still-running job is
explicitly NOT a fault. **A checkpoint written before its effect loses the
effect forever**, so `CHECKPOINT_ORDERINGS` has no `checkpoint-first`:
effect-first can only ever repeat, which idempotence answers. **Cancellation is
a request** — `cancelling` is a state with a duration, and a noncancellable
region longer than the lease is refused, because inside it the worker is
guaranteed to become a zombie. **The deadline and the budget are different
refusals**, and the spend check runs before the attempt with the attempt's own
estimate, since one that runs after is an accounting entry. **An orphaned lease
is not a failed job**: an at-most-once effect whose outcome nobody can ask about
is dead-lettered for a person rather than requeued, because the two available
guesses are a double charge and a dropped job. The dead-letter replay keeps the
JOB identity, mirroring S3.8.f — minting a new id forces either reusing the
idempotency key (answered with the dead job's outcome) or dropping it (two runs
of the same work).

`src/job-graphs.ts` — job parent/child graphs (S3.10). S3.9 is one job and one
worker; this is the shape real work has, and the whole difficulty is that a
parent's answer is not any single child's answer.

**A parent's outcome is not the worst child's outcome, which is why `partial`
exists.** Without it, every graph with an optional child forces one of two lies:
`failed`, and the required work that did finish is thrown away by whoever reads
it, or `succeeded`, and the thing that was lost is recorded nowhere. **An
optional child is one the parent may finish WITHOUT, not one it may finish
BEFORE** — the parent stays non-terminal until every child is terminal, because
a child running under a completed parent is a job nobody reads and everybody
pays for. **Weights are a declaration and an unknown child is a range, not a
zero**: the parent knows the unknown child's weight, so it reports the interval
— "between 53% and 55%" is true and "54%" is a guess dressed as a measurement.
`FAILURE_POLICIES` has no `ignore` (and is also the required/optional flag,
since two fields that must agree are two places to get it wrong);
`CANCELLATION_PROPAGATIONS` has no `orphan-children`. **The edges are immutable
once anything has run.** **Reuse and re-run are both wrong by default and the
input hash decides** — and reuse is TRANSITIVE, because a child downstream of
one being re-run has inputs that were that child's output. **The critical path
is over declared dependencies, not over what happened to be slow.** The cycle
check is the S2.1 stage engine's Tarjan, reused over a lifted edge set that
carries the sibling dependencies as well — a cycle among siblings is the one a
parent/child check alone would miss.

`src/blobs.ts` — blob references (S3.11). Every other module here describes
something this system decided; this one describes bytes somebody else produced,
in a store somebody else runs, and almost every rule follows from not believing
either of them.

**The declared and the observed are different fields and never merge.** There is
no single `mediaType` on the record: an upload's `Content-Type` is evidence
about the UPLOADER and the sniffed type is evidence about the BYTES, and one
field means the claim quietly becomes the observation. `sniffMediaType` is a
real magic-number table — including the formats that execute — and it returns
EVERY match rather than the first, because a GIF header with a zip's central
directory in the tail is a valid image to one decoder and a valid archive to
another, and a sniffer that returns one hands each of them a different file.
**The digest is of the stored bytes read back**, not of the inbound stream:
`verifyDigest` names the two sources that produce a green tick without checking
anything — `metadata` (comparing the stored digest to itself) and a prefix read
(`partial-read`, and corruption lives at the END of a file, where a truncated
write leaves it). **The record names a key and never holds one** —
`KEY_MATERIAL_LOCATIONS` has no `inline`, and `APPROVED_DIGEST_ALGORITHMS` has
no `md5` or `sha1`, because S3 hands back an MD5 as the ETag and an ETag is a
cache token. **A blob is admitted to nothing by default**: an `unavailable`
check holds it in quarantine rather than letting it through, which is the
difference between "nothing objected" and "everything approved". **This module
does not own a second retention machine** — retention, legal hold, purge steps
and tombstones are S3.3's, and `deletionStateFor` is the entire interface
between the two.

`src/reclamation.ts` — orphan-blob discovery and safe reclamation (S3.12). Every
other module here is careful about being wrong; this one is careful about being
wrong in ONE DIRECTION. A blob wrongly kept costs storage, and a blob wrongly
deleted is gone — no compensating action, no retry, and usually no way to find
out until somebody opens the thing it was attached to months later.

**"Unreferenced" is a claim about what you looked at**, never a property of the
blob, so `REFERENCE_SOURCES` is a closed list of all twelve places a reference
can live and a sweep whose register could not be fully scanned is refused rather
than run over what was reachable. **The orphan classes fail in opposite
directions**, so `ORPHAN_ACTIONS` has no `delete`: a row pointing at bytes that
are gone is repaired by deleting the ROW, an object nothing knows about by
deleting the BYTES, and one verb over both is how a reclaimer deletes bytes a
live row still points at. **The recheck has to be in the same statement as the
delete** — `planDeletion` returns a PREDICATE rather than a permission, because
everything it checked can change between returning and the statement landing.
**An unknown deletion outcome is not a deletion**, and `already-absent` is an
ordinary success, because a reclaimer runs twice. **A finding is not closed
until somebody looked for the false positives**: a restore, a relink or a broken
render is the only evidence that ever arrives that the sweep was wrong, and it
arrives after.

`pnpm nx reclamation-race` runs three controls, each of which must delete a
referenced blob: mark-then-delete with no recheck; recheck-then-delete, the
conscientious version, where the reference lands in the window the separate
SELECT opened; and a register missing one source, where a blob referenced only
from `provenance` comes out classified `unreferenced-active` and marked
`delete-bytes`. Beside them, one statement carrying the token and `NOT EXISTS`
matched zero rows and the blob survived — and the same statement does delete a
genuinely unreferenced one, so the guard is not simply always refusing.

`src/protection.ts` — encrypted sensitive-field storage and vault references
(S3.13). S3.11 describes an encryption arrangement somebody ELSE performs, on
bytes in a store somebody else runs. This is the other half: the plaintext is in
this process, the sealing happens before the INSERT, and the thing being
protected is a COLUMN — queried, indexed, logged, echoed back in constraint
violations, exported to support, and copied into every backup.

**A classification is a constraint on what the field may be used for**, not a
label on how secret it feels, so `STRATEGY_CAPABILITIES` is derived from the
operations somebody declared they need — and three of them
(`UNSUPPORTABLE_OPERATIONS`) are supported by nothing, because the schemes that
claim to sort or prefix-search ciphertext leak exactly the property the
operation needs. **The field is not the only copy**: an index, a search vector,
a display surrogate and a metric label are declared as derivatives and checked,
because a protected column with a plaintext derivative is a plaintext column
with a ceremony in front of it. **The binding goes in the AAD, not the WHERE
clause** — `openField` builds its associated data from where the row IS and
never from `sealed.context`, because a ciphertext copied into another row
arrives with its own envelope and opening against the envelope's claim would
authenticate the copy. **A resolved secret is a secret that now lives somewhere
else**, and this runtime cannot wipe a string, so what is on offer is a bounded
lease plus a usage record that makes "it was not cached" checkable — saying
which of the two you have is the point. **Redaction is driven by the matrix, not
by what the value looks like**: a diagnosis, a home address and a source's name
have no signature, so S1.10's `scanText` is the CALIBRATION and the field matrix
is the mechanism. **You cannot roll back past the point where the plaintext was
dropped**, and dropping the column does not remove the bytes —
`PLAINTEXT_RESIDUES` names the ten places it still is. **The failure modes must
be distinguishable**: an unavailable KMS and a destroyed key are identical to a
caller and opposite to an operator, and `modes-share-one-code` is the finding
this whole section exists for.

**This module does not own a second human-gate machine** — break-glass dual
control is S2.13's `validateHumanGate`, and what `authorizeBreakGlass` adds is
the three rules specific to keys: the audit record is a precondition rather than
a consequence, the emergency identity is not the routine one, and a grant over
every key is a second production credential with a form in front of it. **It
does not own a second deletion machine either**: destroying a key is a deletion,
so `authorizeShred` asks S3.3's legal holds the same question a purge is asked.

`pnpm nx canary-leak` plants one canary through the real write path and runs
five controls, every one of which must expose it. A plaintext column, which the
dump carries. A unique violation, where Postgres answers with
`Key (contact_plain)=(canary-…@leak.invalid) already exists.` — and
`redactionFaults` is calibrated against that exact string rather than against a
message written to be caught. A UNIQUE index over randomized ciphertext, where
one value sealed twice inserts twice. A deterministic token over three
countries, where one `GROUP BY` recovers the 31/12/4 frequency ranking and the
mapping falls out of it. And the plausible wrong `openField`, taking its
associated data from the envelope, which opens a row copied into another tenant.
Beside them: a full dump of the sealed column does not contain the canary, the
copied row does not open against where it now sits, and the untouched row does.

The engine also volunteered a fact worth keeping — the same UNIQUE index that
admitted one value sealed twice REFUSED a verbatim copy of one ciphertext. It
constrains the nonce, not the value, so it is useless against the case it was
added for and effective only against a duplication nobody performs by accident.

`blastRadius` on a `RevocationPlan` exists because of the mutation pass: one
branch survived BOTH mutations, which is the pass saying the two crypto-shred
arms were the same answer in different words. An operator authorising a shred
reads the fields, not the paragraph, and the difference between "the rows this
key sealed" and "every key beneath it" is the difference between one column and
a tenant.

`src/probes.ts` — database health and schema-compatibility probes (S3.14). The
smallest piece of code in a deployment, deciding whether every other piece
receives traffic. Almost every failure here is a probe that was RIGHT about
something unimportant and, by being right, took a working service out of
rotation or kept a broken one in.

**Liveness must not touch the database** — `LIVENESS_PERMITTED_CHECKS` has no
database check in it and the absence is the control, because a liveness probe
that queries the database restarts every process the moment the database has a
hiccup, and each restart opens fresh connections into a database already
struggling. **The probe needs its own connections and a deadline shorter than
the orchestrator's**: one sharing the request pool fails first under load, and
one killed before it answers reports a timeout, which is the only probe result
that carries no reason. **A write probe that mutates user data is a write**, and
a probe that only reads cannot see the thing it exists to see —
`WRITE_BLOCKING_STATES` is the list of states `SELECT 1` succeeds straight
through. **The schema comparison is not symmetric**: too old is fatal, too new
is expand/contract working, and a build declaring `maximum === minimum` refuses
readiness for the length of every rollout. **Fail-closed is about the write
path, not the process** — `readinessDecision` answers per capability, and
`migration-admin` survives every state that is fixable, because a deployment
that refuses the capability that would repair it has locked itself out. **The
public body is constructed, never the report with fields deleted**: there is
nowhere in `PublicProbeView` to put an object name. **One incompatible reading
is what a rolling deploy looks like**, so the alert is on duration, its
threshold must clear the deploy window, and a gap in the readings is not
continuity.

**This module does not own a second status ladder.** Readiness comes back as an
S1.6 `CapabilityState`, so `usability()` answers for it — a boolean would have
to round `too-new` to ready or not-ready and both roundings are wrong. It does
not own a second probe-kind vocabulary either: S1.7's `PROBE_KINDS` is the
service descriptor's, `DATABASE_PROBE_KINDS` is this one's, and the spec pins
where they agree and why they differ.

`pnpm nx probe-controls` runs four controls, each of which must report a healthy
database that is not. A read-only session, where `SELECT 1` returns 1 and the
INSERT comes back `cannot execute INSERT in a read-only transaction`. A row
policy still listed in `pg_policies` while RLS is disabled, where the tenant
reads both tenants' rows — a compatibility check that looks for the POLICY
passes while the isolation is off, and only `pg_class.relrowsecurity` catches
it. A rolled-back probe write to a user table, where a concurrent user UPDATE
waits behind the open transaction until its lock timeout: the rollback ends the
write and not the lock. And a missing migration table defaulted to 0, which
turns "this is probably somebody else's database" into "run the migrations".

The controls needed a role that does not bypass row-level security. The dev
superuser does, and CONTROL 2 run as it would have shown two rows in both arms —
a check that passes by never being able to fail.

`pnpm nx tenant-isolation-matrix` is S3.16, and it differs from
`pnpm nx tenant-isolation` (S3.2.f) in its enforcement mechanism. S3.2.f checks
that the queries carry their tenant predicates; this one **takes the predicates
away**. Every statement runs as a role that cannot bypass row-level security,
with no tenant clause in it at all, so what is under test is the database's
isolation rather than the application's discipline — an isolation rule enforced
by a predicate is one a missing predicate disables, and the missing predicate is
the entire failure mode.

Six matrices: 8 read shapes plus the count, from both identities; 10 mutations
aimed across the boundary, each verified against the OTHER tenant's row so that
a refusal which silently mutated still fails; 4 search shapes plus the result
count and the highlight snippet, which leak with no row attached; 7 artifact and
export paths; 5 asynchronous paths where the context is carried rather than
joined; and the diagnostic surface, where the assertion is that the collision
fired the **tenant-scoped** constraint — a global unique over (workspace, slug)
would have collided with the other tenant's row and turned the error into an
oracle.

The fixture is the negative control: both tenants hold the same workspace, slug,
taxonomy term, timestamp and content marker, and all six control statements must
return both tenants when run unrestricted. Calibrated by breaking it — a policy
of `USING (true)` produces 28 findings, and a unique key without the tenant
makes the colliding fixture impossible to seed at all, which the harness reports
rather than working around.

`pnpm nx blob-storage` runs a local filesystem adapter and a real S3-compatible
store side by side. Three verifications must every one of them report success
over corrupt data: reading the digest from the object METADATA passes while the
bytes underneath have been replaced; hashing the first eight bytes passes
because the corruption is in the last sixteen; and a line-ending-normalising
adapter returns 2,207 bytes where the others return 2,248. Beside them, a full
read-back through both adapters agrees on one digest and reports `corrupt`. An
ELF stored under a `.png` key and a GIF/zip polyglot both survive the round trip
still reading as what they are, because a sniffer's answer is only worth
anything if it survives the storage layer.

`pnpm nx graph-admission` is the half no unit test can settle: `readyChildren`
is a pure function that RECOMMENDS, and every rule in it can be defeated by a
second scheduler holding the same snapshot. Three controls, all required to come
back wrong: read-then-write admission started all six children and spent 600
against a ceiling of 250, leaving `reserved` at the last writer's number rather
than the sum; the per-child budget check spent the same 600 with no concurrency
at all, which is the point — that rule is wrong single-file; and an admission
that does not re-check the parent started two children under a cancelling one.
Beside them, one transaction that claims the child and reserves its cost
conditionally, which is also why the reserve-then-claim ordering question does
not arise.

`pnpm nx job-fencing` runs FOUR controls and requires every one of them to come
back visibly wrong: read-then-write let both workers believe they had claimed
the job; with expiry and no fence, the paused worker's result overwrote the live
one's; checkpoint-before-effect finished with unit 3 never applied by anybody
and the job reporting success; and reconciling an unknown at-most-once outcome
as a failure left two charges for one job. Beside each: one winner from the
conditional claim, a fenced write matching zero rows, every unit applied exactly
once, and one charge with the job parked.

`src/inbox.ts` — the idempotent inbox (S3.7). The outbox guarantees
at-least-once; the inbox is what makes at-least-once survivable, and everything
here follows from one asymmetry: **an event processed twice is a visible bug,
and an event suppressed as a duplicate and never applied is an invisible one** —
because the suppression is exactly the mechanism that would otherwise have fixed
it.

**The deduplication key carries the consumer**: one event reaching three
projections must be processed three times, and a receipt keyed on the event id
alone — which is what "have we seen this event?" naturally becomes — lets the
first consumer silently suppress the other two. **Same event id, different body
is a conflict, never a duplicate**, so the payload hash is compared before the
state. **The receipt and the effect are one transaction, and the effect goes
first when they cannot be** — on the producing side that order costs a
duplicate, here it costs the event permanently. **A gap is not a loss until it
is old**, and whether an event kind may be applied out of order is DECLARED per
kind, because the guess is always "probably". **The processing version is part
of the receipt**, so a fixed reducer actually runs on the history it was fixed
for. **Parity is compared field by field, never by a digest** — a drifted
reducer, a lost event and a row somebody wrote to directly all produce one bit
and three different investigations.

`pnpm nx inbox-idempotency` races it at a real PostgreSQL with four controls
that must come back red: the read-then-write dedup **must** apply one event more
than once (24 simultaneous deliveries applied it 24 times); the event-id-only
receipt **must** suppress the second projection; the receipt-before-effect order
**must** lose the event permanently; and applying on arrival **must** leave a
non-commutative history in the wrong state. It then hands the same shuffled
delivery to the kit's own fold and compares the two projections field by field.

`src/outbox.ts` — the transaction-bound outbox (S3.6). Two things have to happen
together and only one of them is in the database: the row changes, and somebody
outside has to be told. Getting that wrong is silent in both directions.

**The outbox row's id is the published event's id**, never re-minted per
attempt, because an outbox is at-least-once by construction and the second
delivery has to be recognisable as the same event or every consumer's
deduplication is decoration. **The payload is a snapshot, never a pointer to
mutable state** — there is no `lookup` member of `PayloadRef`, so an event that
says "go and read row 42" cannot be written. **Publish, then mark — never the
reverse**: the crash window cannot be closed, only pointed in a direction, and
`markPublished` takes a receipt that only a real broker acknowledgement
produces, so the tidier order, the one that loses the event, cannot be expressed
with these types. **Per-subject order is the only order there is**, and a claim
is partitioned by subject because two dispatchers holding two events of one
subject publish them in whatever order they finish. **Unknown is not retryable
and not permanent** — it retries a bounded number of times and then becomes a
person's problem. **A dispatched row is the only record of what was published**,
so it is kept for the incident window while its body is redacted on a shorter
clock of its own.

`pnpm nx outbox-dispatch` drives five properties at a real PostgreSQL, and every
one of them has the control that must come back red: the two-transaction write
**must** leave an authoritative row with no event; the unlocked
SELECT-then-UPDATE claim **must** hand one row to several dispatchers; the
unpartitioned claim **must** publish sequence 2 before sequence 1; and
mark-then-publish **must** lose the event outright. It also runs `selectBatch`
from the built package and the SQL claim over the same rows and fails if they
choose differently — a selection rule and a statement that disagree is how a
policy becomes a comment.

`src/indexes.ts` — the index as a claim about a query at a volume (S3.5). An
index list is the easiest artifact in a schema to write and the hardest to argue
with a year later, so this module does not ship one: it ships the workload first
— twenty-four queries with their pagination, their criticality and the row
distribution they run against — and every index names the queries it exists for.
Each dimension the ledger names (six kinds of ownership, eight kinds of time,
seven properties of a gate) gets a row saying which index answers it or why none
can: a plan covering five of six looks identical in a report to one covering
six.

**An index declares the query it serves, or nobody can ever remove it**, and the
mirror of that is declared too: `DECLINED_INDEXES` is the index deliberately not
created, with what pays for it. **A plan is a measurement of the data it ran
on** — a sequential scan over a four-hundred-row registry is the correct plan,
so every judgement is conditional on the entity's declared volume class and the
query's criticality. **The scope column leads**, derived from the ownership
class S3.2 already assigned rather than restated, and narrowed to what the
served queries actually pin, because an inbox is a tenant-wide read of
workspace-owned rows and an index led by the workspace is one it cannot enter.
**A sort without a unique tie break is a correctness bug the planner calls
fast**, checked against the uniqueness the entity declares so the audit stream's
`(objectId, sequence)` passes without an `id` bolted on. **An identifier lookup
is exact**, unique, and scoped by the issuing system as well as the tenant.

Two findings the module exists to carry. **A list and an ordering on the same
column are mutually exclusive**: asked to filter `stage` with an array and to
return `stage` in order, Postgres drops the array from the index condition and
filters, so the list narrows nothing — and asked to order by something else, it
sorts the whole match to return one page. **The C collation is not an
optimisation**: in the database collation a b-tree cannot serve `LIKE 'x%'` at
all, and the other spelling — `text_pattern_ops` — makes the prefix work and
then answers the ordering with an incremental sort.

`pnpm nx query-plans` asks Postgres rather than asserting: it seeds a skewed
948,000-row workbench, builds these declarations into it with `renderIndexDdl`,
and feeds every captured plan back through `judgePlan`. `coverageFor` predicts a
sort before each query runs and the plan either has a Sort node or it does not.
Its negative control is the same workload with every index dropped, where each
interactive query **must** come back blocking; and it runs the gate a second
time against a 500-row copy that has every index, where six of seventeen
interactive queries block on plans that are correct — which is why the harness
refuses to report a pass below a 50,000-row floor. **The fixture size is the
measurement**, and a toy fixture does not give a weaker answer, it gives a wrong
one.

`src/deletion.ts` — deletion, retention, legal hold and purge (S3.3). Every
other lifecycle in this kit is recoverable; this one ends in an irreversible
step performed by a machine at a scheduled time against data nobody is looking
at, which is why almost all of it is about refusing to take that step.

**A legal hold dominates an erasure request, and the conflict is recorded rather
than resolved.** The hold wins, the request is **suspended** rather than
refused, and the difference is the whole of the person's remaining rights — a
suspended request has a review date and resumes on its own. A confidential hold
still suspends it, and the subject is told it is under review without being told
why, which is a different sentence and therefore a different key. **An
unsatisfiable retention policy is a policy error, not a runtime decision**: a
regulatory minimum of seven years and a contractual maximum of two cannot both
be met, and resolving it at purge time means choosing which obligation to
breach, at 3am, in a job, with nobody told. **`failed` is a state, not an
exception** — a purge that half-succeeded and reported success is partly gone so
it cannot be restored, and recorded as gone so nobody looks. **Purge order is
not an optimisation**: the metadata goes last because it is the map to the
blobs, the derivatives and the provider-side copies. And **a content hash is not
anonymisation** — the surviving tombstone has nowhere to put one, because a hash
of a name or an address is a lookup key for anybody who can guess the input.

`src/tenancy.ts` — who owns a row, and what that means for every query that
touches it (S3.2). Isolation is the one property here where being right 99% of
the time is indistinguishable from being wrong.

**A workspace filter without a tenant filter is a cross-tenant query waiting for
a collision** — a workspace id is unique inside a tenant and nowhere else, so
`where workspaceId = ?` is correct until two tenants mint the same one, and it
survives review because it looks more careful than an unfiltered query. **Six
ownership classes**, because collapsing `cross-workspace-tenant` into
`workspace-owned` means either duplicating a shared taxonomy per workspace or
carrying a `workspaceId` that lies. **A child's scope must equal its parent's,
and a join row has two parents** — a link between one tenant's story and
another's asset is the leak neither foreign key can see. **An operator may read
operational state across tenants and may not read tenant content**: somebody
debugging a stuck queue needs the queue, not the story in it. And **an export
carries the scope it came from, so an import is a re-scope and never a restore**
— the one leak performed deliberately, by an administrator, using a supported
feature.

`pnpm nx tenant-isolation` proves the runtime half against a real PostgreSQL by
seeding two tenants with **deliberately identical** workspace ids, slugs,
external references and idempotency keys, then running nine query shapes twice:
the scoped form must return one tenant's rows, and the unscoped form **must**
return both — a fixture that fails to collide makes every green above it
meaningless.

`src/audit.ts` — the audit projection, and the one claim that makes it one
(S2.14). A projection is a **cache** — that sentence is either true or it is a
lie somebody tells about a second source of truth, and the difference is
entirely whether the thing can be thrown away and rebuilt. So `parityFaults`
compares a replay against a maintained read model **field by field rather than
by a digest**: a checksum answers "do they differ", and the question worth
asking is which field, because that distinguishes a drifted reducer from a lost
event from a stored copy somebody wrote to directly.

**Order comes from a sequence, never from a timestamp.** Two writers a hundred
milliseconds apart on skewed clocks produce a trail that reorders itself, and
both orders read as plausible. **A stalled or quarantined stream produces an
incomplete projection and says so** — skipping a poison event and carrying on
yields a timeline that reads as a complete account and is missing a fact, which
is the one thing an audit trail cannot be. **Redaction is applied to the
projection, not to the query**, because a filter in a query is one the next
endpoint forgets and every export is a next endpoint; a cross-tenant viewer gets
nothing rather than an empty result, since the existence of the object is itself
the disclosure. And the details a viewer IS allowed are run through **the kit's
own S1.10 leak detectors**, because permission to read a reason is not
permission to read a credential somebody pasted into it.

`src/signoff.ts` — the gate automation cannot answer for itself (S2.13). Every
other gate in the kit is a measurement; this one is a person taking
responsibility, and the whole cell is one word of its title: **self-complete**.
Automation may schedule it, remind about it, escalate it, show it and record it.
It may not produce one.

The failure is almost never an attacker. It is an agent holding a service token
that happens to carry the `editor-in-chief` claim, a seeded environment where
every reviewer is a fixture, a support engineer's impersonation session left
open in another tab — each of which satisfies every role check ever written. So
**the actor kind is checked before the role**, and where it can be a type it is
one: a completed record carries a `HumanPresence`, which only `admitSigner` can
build. Both halves of the S1.13.d synthetic mark are checked, because the symbol
mark does not survive JSON and the reserved id prefix does. **A delegated actor
may read everything and sign nothing.** **A step-up reused from login is not a
step-up** — it has to have happened after the review opened and inside a short
window, or it is a property of the session rather than of the judgement. **A
quorum without independence is one signature with a count**, and `same-team`
independence is reported as undecidable rather than silently passed, because the
kit has no org chart and inventing one from an id prefix is wrong in exactly the
cases it matters. **Renewal and reversal are new human actions**: `planRenewal`
has no arm that produces a valid signoff.

`src/staleness.ts` — dependency-triggered staleness propagation (S2.12). S2.9
declared what makes evidence stale and S2.10 computed it for one result; neither
can answer the question this module exists for, which is: a consent record moved
— **what else is now wrong**.

**Not every mutation invalidates, and the registry has to say why not.** A
source whose title was corrected does not invalidate a rights check; one whose
licence changed does. A registry where everything invalidates makes the
recompute queue the whole corpus every night, and the response to that is always
the same: somebody turns propagation off. `model/alias-repointed` is the entry
that justifies the subsystem — a provider moves a stable name onto new weights,
nothing here changed, and every answer that name gave is now an answer to a
different question. **A truncated walk is not a completed one, as a type**:
fan-out and depth caps are necessary and are also how a propagation silently
leaves half its dependents fresh, so `propagate` returns a union whose
incomplete arm carries a resume cursor. **The idempotency key may not contain
the event or the clock** — it names what the recomputation would do, so a replay
collapses onto the job already queued. **Withdrawn consent is never
auto-recomputed**, checked ahead of every other routing rule so no configuration
change can reach it first. And **reconciliation compares stored versions rather
than event logs**, because an event that was never emitted leaves no trace in
the log that would have carried it.

The outbox half of S2.12.b is an engine property, so it is settled by
`pnpm nx outbox-atomicity` against a real PostgreSQL — with the same failure
injected in one transaction and in two, where the two-transaction control MUST
come back with an orphaned mutation or the harness has proved only that it ran.

`src/gate-waivers.ts` — the authorized, time-bounded exception (S2.11). S2.10
folded waivers without deciding them; this is where one comes from and when it
stops being one, which is the seam where a gate quietly stops meaning anything.

**A request is not a grant** — one row with a status column makes "who asked"
and "who agreed" the same record and leaves the request editable after approval,
so the grant restates the request's identifying fields and carries a digest of
what was approved. **One-use is not a short expiry**: "an exception for this one
publish" modelled as a one-hour window becomes "an exception for everything that
shipped in that hour". **A gate version bump can never be tolerated** — an
approver may declare which changes a waiver survives, and
`REBINDABLE_DIMENSIONS` deliberately has no member for the gate version, because
S2.9 bumps that when the gate's meaning changes. **Renewal is a new decision**,
so there is no `renew()`, and `renewalChain` adds the instalments up: eight
consecutive forty-eight-hour waivers are a two-week exemption that no single
approval authorised, every link reads as within the limit, and nothing sees it
unless something sums them. **Non- authorizing is not deleted** — exactly one of
the eight standings authorises, and every other one still carries the approver,
the date, the reason and the accepted risk.

The cell's headline is proved through a real fold rather than asserted about a
status field: `toWaiverRecord` returns null for anything but an active standing,
so an expiry makes a release ineligible because the waiver stops being handed to
S2.10 — not because a job noticed.

`src/decisions.ts` — domain-neutral decision records (S2.7). A wiki page has
three properties that make it useless within a year: nobody can tell whether it
is still in force, the alternatives that were rejected are gone, and the
evidence it rested on is a link to something that has changed since. So a
decision here records the **options that lost** (exactly one chosen, every
rejection with a reason, and a single-option decision has to say it considered
nothing else), **evidence pinned by version and digest** with a summary run
through the S1.10 leak detectors, and **a review date or a stated reason there
is none** — because "nobody set one" and "this one stands" must not look the
same, and the first is how a temporary exception becomes permanent.

Its lifecycle runs on the S2.1 stage engine rather than a `switch`, which is
reuse in both directions: the decision gets the reachability, terminality and
effect-versus-reversal checks for free, and the engine gets its first consumer
that is not a test. Supersession never edits — exactly two fields move on the
original, and `supersessionHarms` checks it, because an archive that agrees with
the present is not an archive.

`src/diff.ts` — six semantic diff adapters over one operation contract (S2.5):
scalar, list, tree, graph, timeline and binary metadata. Every workbench shows
somebody what changed, and each implementation independently rediscovers the
same four mistakes. A move looks like a delete and an add — only identity tells
them apart, and a comparison with no item identity **cannot**, which is a real
limit reported by `listCanDetectMoves` rather than papered over by a similarity
heuristic. Absent, null and default get collapsed into one — they are three
different facts, and folding them makes "somebody cleared this" and "somebody
never touched this" the same row. Dragging a node counts as a change — it is
one, and it is not a change to the work, so every operation carries a
significance and the summary counts the two apart. And the diff pretends to
compare bytes — it cannot, so `diffBinaryMetadata` compares the twelve things
that are comparable and calls the bytes opaque, which is both honest and the
answer somebody wanted.

The sharpest one is the timeline. Frame 300 is ten seconds at 30fps and 10.01 at
29.97, so a comparison of the numbers reports nothing for a conform that moved
the entire sequence; positions are converted through the exact rationals S1.4.e
already carries. And one cut is one action: a clip split into two adjacent
pieces over the same span is a `split`, not a delete and two adds.

Ordering, summaries, redaction and pagination live outside the adapters, in
`assembleDiff` — an adapter that ordered its own output would be a second
ordering, and two orderings disagree the first time somebody adds a change type.
Redaction hides values and never rows: a reviewer who may not see a salary still
has to know it changed.

`src/revisions.ts` — the immutable history (S2.4), and mostly one function:
`checkHistoryIntegrity`, which is seventeen ways a history can be wrong. A
revision history is the part of a workbench everybody reads and nothing checks,
because append-only things are assumed to be fine — and a parent pointer into a
hard-deleted row, a sequence number two writers allocated, a merge with no
recorded base, or a hash left behind by an in-place typo fix are all silent.
Three decisions carry it. Nothing is denormalized: there is no `isHead` column,
because a second source of truth eventually disagrees with the first and the
symptom is an object nobody can edit. A merge REQUIRES its base, or "which side
won this field?" has no answer for the rest of the object's life. And the only
lawful mutation is a governed one that never clears the hash: a tombstone
removes the content and keeps the metadata and the original digest, which is how
an erasure request and a verifiable chain hold at once — "this was removed" is
not "this was never here", and neither is a gap.

The kit does not hash. It owns the canonical form, because two implementations
of canonical JSON disagree about key order and number formatting and then
produce mismatching digests for identical content; the digest comes from an
injected `ContentHasher`, and for artifact-backed content the kit compares the
pin rather than claiming to have read bytes it never saw.

`src/scores.ts` — score vectors whose absences stay absent (S2.8). A score is
the easiest thing in a system to fabricate, because every layer has a reason to:
the evaluator did not run, so the row is missing; the aggregate needs a number,
so missing becomes zero; the UI needs a bar, so zero becomes a bar at the
bottom; the gate needs a comparison, so a bar at the bottom becomes a fail.
Nobody lied — four reasonable local decisions produce a confident number
describing nothing.

So the type has **no numeric branch for absence**: a reading is a measured value
or one of eight named non-values, and `-1` is not something it can hold. The
value is not the verdict (thresholds derive one and hand the reading back by
reference). Two scores are not comparable because they share a name — a changed
evaluator version means the number moved because the RULER moved. And an
aggregate refuses across units, across directions and over an ordinal scale, and
reports what it excluded, because a mean of 0.9 over two measurements and eleven
absences is a different fact from a mean of 0.9 over thirteen.

`probeAdapter` is the adversarial half: it runs a real UI or BFF adapter over a
corpus containing all eight non-values and reports what it invented. A
declaration would not do — "this adapter does not fill missing values" is a
sentence in a review, and finding a `0` in the output is a fact.

`src/restore.ts` — restore as an append (S2.6). "Restore this old version" is
the feature most likely to be implemented as an UPDATE, and that implementation
is correct exactly once: after it, the thing it overwrote is the only record of
what was there and it is gone. So `historyHarms` compares the record set before
and after and reports anything that was not a pure append — a removed revision,
a mutated one, a re-pointed parent, a fork that leaves the intervening revisions
on a branch nothing references.

Three decisions carry the rest. The new revision's **parent is the head and its
base is the source**, which is exactly why S1.4.d has two fields: one answers
"what did this follow?" and the other "what is this a copy of?", and a model
with one makes one of them unanswerable. A **tombstoned revision cannot be
restored** — refused by identity rather than by policy, because a restore that
reinstated erased content would undo a legal obligation through a feature nobody
thinks of as a data path. And the **preview is not the diff**: what changed is
the easy half, and the half that decides whether the restore is safe is which
gates were passed against content about to stop being current, whose consent
covered material about to come back, and which effects cannot be taken back.

`src/archetypes/` — the two worked-example plugins whose compilation is S1.9's
evidence: `story-desk` (content authoring) and `incident-console` (operations).
They share no stage name, no block type and no extension-point set, which is
what makes "the kit assumes no domain vocabulary" checkable rather than
asserted. They are excluded from `tsconfig.lib.json` and included in
`tsconfig.spec.json`, so they never reach `dist` but the `typecheck` target
still compiles them.

`src/synthetic.ts` — the PRODUCTION half of the fixture boundary. Everything
else that keeps the builders out of a shipped bundle is static: the runtime
lattice reads the source graph, the lint ban reads the import, the emitted scan
reads dist. This is what a host does when a fixture arrives anyway — through a
seeded database that got promoted, or a container that resolved a double because
it was registered first. Two marks, because neither survives what the other
does: a global-registry symbol survives a spread and dies in JSON, and a
reserved id form survives JSON and says nothing about objects. The reservation
is **structural, not lexical** — a fixture id always carries a colon
(`tenant:synthetic-acme`, `synthetic:idempotency-1`) — because the first version
reserved the bare prefix and the emitted-package scan promptly found
`registry.ts` shipping the ratified `FixturePolicy` value `'synthetic-only'`.
Everything here refuses rather than repairs, and
`selectProductionImplementation` treats "the only registered candidate is a
double" as an error rather than a fallback: that boot is the incident.

`src/testing/` — six test-only subpaths, split by capability and by runtime
class. `invariants` holds the refusal corpus a plugin runs to prove it enforces
the six invariants, plus builders that refuse to construct a non-violation —
seven workbenches writing their own "invalid envelope" fixtures is seven chances
to write one that is invalid for the wrong reason, and the test that expected a
refusal reports green either way. `identity` and `envelopes` build values the
kit's own parsers validate before the builder returns them. `assertions` closes
the gap where a test passes for a reason other than the one it claims. `doubles`
holds the stateful ones — clock, id sequence, in-memory stores — and is the one
subpath the aggregate `./testing` does **not** re-export, because a suite that
asked for a builder and received a shared mutable clock with it is one stray
reference from a test that passes alone and fails in a run.

Two rules run through all of them. A builder **requires** every field whose
default would hide test intent — there is no `aTenantId()`, because one shared
tenant makes a whole suite of isolation tests pass against a query that never
filtered by tenant. And every sensitive field that IS defaulted appears in
`src/testing/register.ts` with the argument for it, so the reviewer question is
"is that reason true?" rather than "did anybody think about this?". The register
is checked against the modules in both directions: an export nobody registered
and a row naming a symbol nobody exports are both failures.

## Reference

The API reference is GENERATED from the contracts and lives at
`evidence/v1-workbenches/workbench-kit-api/` — eleven pages covering every
exported production symbol, with examples drawn from the committed fixture
corpora. It is not written beside the code, because a hand-written reference for
eighteen entry points is wrong within a week and the only reader who would
notice already knew the answer.

Three properties make it worth reading. Every production module is owned by
exactly ONE page, so a symbol nobody documents fails the gate rather than
quietly missing. Every page carries the same twelve sections, and the
cross-cutting ones — authorization, idempotency, pagination, state,
compatibility, deprecation — are derived by matching declared field and symbol
names against a named vocabulary, so a section says **not applicable** rather
than being omitted, and a new field carrying that concern appears without
anybody remembering to mention it. And the output is formatted through the
repository's own Prettier with the version recorded in it, so two runs are
byte-identical and a formatter upgrade shows up as drift instead of as a
mysterious diff.

The error page **redacts** its examples. That corpus deliberately carries
credential-shaped canaries so the S1.10.e leak scan can be proven to detect
them, and a reference page that reproduced one would put a working-looking token
in the documentation. The scan runs after the redaction, so a credential
arriving by a path the redactor does not cover still fails the build.

No **capability** ships yet, on purpose. S1.2 draws the export map from the
**ratified** shared capability matrix, and S0.12's owner ratification is pending
(see `evidence/v1-workbenches/shared-capability-review-packets.json`, where all
91 rows are `pending`). Shipping a capability before that would prejudge the
decision the review exists to make — and S0.11's deletion threshold
independently says no shared abstraction ships with fewer than two real
consumers.

## Targets

```bash
pnpm nx lint @oshun/workbench-kit
pnpm nx typecheck @oshun/workbench-kit
pnpm nx test @oshun/workbench-kit
pnpm nx build @oshun/workbench-kit
pnpm nx smoke @oshun/workbench-kit             # imports the BUILT package as an external consumer
pnpm nx alias-resolution @oshun/workbench-kit  # tsc resolves every approved subpath and no deep one
pnpm nx boundary-fixtures @oshun/workbench-kit # real ESLint accepts/rejects real edge fixtures
pnpm nx test-only-imports @oshun/workbench-kit # real ESLint rejects every import of a test subpath
pnpm nx production-scan @oshun/workbench-kit   # the BUILT package carries no builder, type or fixture id
pnpm nx revision-race @oshun/workbench-kit     # S2.3.b: real concurrent PostgreSQL connections
pnpm nx history-constraints @oshun/workbench-kit # S2.4.c/d: what the engine refuses, and what only the graph check sees
pnpm nx restore-transaction @oshun/workbench-kit # S2.6.c/f: a real restore race, and the revision + outbox as one write
pnpm nx query-plans @oshun/workbench-kit       # S3.5.i: the workload at volume, judged by the kit's own plan gate
pnpm nx outbox-dispatch @oshun/workbench-kit   # S3.6.b/c/d: atomicity, contention, per-subject order, the crash window
pnpm nx inbox-idempotency @oshun/workbench-kit # S3.7.c/e/g: the duplicate, the dedup scope, the crash window, the shuffled replay
pnpm nx quarantine-blast-radius @oshun/workbench-kit # S3.8.c/f: three isolation scopes, two of them wrong, and the replay identity
pnpm nx job-fencing @oshun/workbench-kit       # S3.9.b/c/e/i: four controls — a double claim, a zombie write, a lost effect, a double charge
pnpm nx graph-admission @oshun/workbench-kit   # S3.10.c/f: three controls — an over-admitted budget, a per-child check, a child started under a cancelled parent
pnpm nx blob-storage @oshun/workbench-kit      # S3.11.b/c/g: local FS + real S3, three verifications that pass over corrupt bytes
pnpm nx reclamation-race @oshun/workbench-kit  # S3.12.a/e: three controls, every one deleting a referenced blob
pnpm nx canary-leak @oshun/workbench-kit       # S3.13.c/e: five controls, every one exposing a planted canary
pnpm nx probe-controls @oshun/workbench-kit    # S3.14.b/d/f: four controls, every one reporting a healthy broken database
pnpm nx tenant-isolation-matrix @oshun/workbench-kit # S3.16: six matrices with no tenant predicate anywhere, under enforced RLS
node scripts/v1-workbenches/generate-workbench-kit-export-map.mjs --check
node scripts/v1-workbenches/generate-workbench-kit-dependency-constraints.mjs --check
node scripts/v1-workbenches/generate-workbench-kit-plugin-fixtures.mjs --check
node scripts/v1-workbenches/generate-workbench-kit-error-fixtures.mjs --check
node scripts/v1-workbenches/generate-workbench-kit-conflict-fixtures.mjs --check
node scripts/v1-workbenches/generate-workbench-kit-diff-fixtures.mjs --check
node scripts/v1-workbenches/generate-workbench-kit-negotiation-fixtures.mjs --check
node scripts/v1-workbenches/generate-workbench-kit-deprecation-fixtures.mjs --check
node scripts/v1-workbenches/generate-workbench-kit-test-utility-register.mjs --check
node scripts/v1-workbenches/generate-workbench-kit-api-reference.mjs --check
```

`revision-race`, `history-constraints`, `restore-transaction`,
`outbox-atomicity`, `idempotency-contention`, `tenant-isolation` and
`query-plans` are the targets that need something outside the repository: a
running PostgreSQL
(`docker compose -f docker/docker-compose.dev.yml up -d postgres`). It **fails**
rather than skipping when there is none, because a skipped race that reports
success would let "S2.3.b is green" mean "nobody has a database". It is
therefore not part of the CI inventory gate, which has no database service; it
is run locally and its result is recorded in the ledger cell. Every run includes
two deliberately non-atomic control stores that must come back RED — without
them a green result would be indistinguishable from a harness that measures
nothing.
