cellos-projector 0.5.1

Projection layer for CellOS — consumes JetStream CloudEvents into in-memory cell/formation state. Used by cellos-server.
Documentation
# cellos-projector

Project CellOS CloudEvents into current state. Reads the event log, runs
`CellStateProjection`, emits snapshots, renders audit docs.

## What it is

`cellos-projector` ships three binaries, one library, and zero ambition to
talk to a host. It is a pure consumer of CloudEvents — given a JSONL file
or a live JetStream stream, it folds events into `CellStateProjection`s
(defined in `cellos-core`) and answers two questions: "what is the current
state of this cell?" and "what does the audit document for this evidence
bundle look like?"

The crate sits at L8 of the layer model — strictly read-side, downstream
of everything else. It consumes the CloudEvent vocabulary owned by
`cellos-core`, the per-event signing wrapper introduced by
`cellos-supervisor` (I5), and the signed-trust-keyset envelopes verified
inside `cellos-core::verify_signed_trust_keyset_envelope`. ADR-0011
(HTTP control plane) explicitly delegates the replay-and-project work to
this crate, and ADR-0014 (formation CloudEvent state model) is the
contract for the formation aggregations.

What `cellos-projector` deliberately does NOT do:

- It does not write to the event log. Every binary is read-only.
- It does not run cells, admit specs, or mutate authority. The state
  model it produces is *derived* — purely a function of the event log.
- It does not depend on `cellos-supervisor`. The supervisor's per-event
  signing wrapper is decoded by a transport-type string constant in
  `src/main.rs:16`, not by linking the producer crate.
- It does not bundle `cellos-host-telemetry`. The audit-doc renderer
  reads only the on-wire JSON shape, not upstream typed schemas
  (`src/lib.rs:20`).

## Public API surface

The library is intentionally small.

- `audit_doc::render_audit_doc(&serde_json::Value) -> String` — pure,
  deterministic Markdown projection of a single
  `cell.evidence_bundle.v1.emitted` CloudEvent. Returns a single-line
  Markdown error sentinel rather than panicking on bad input (D11).
  `src/audit_doc.rs:50`.
- `audit_doc::EXPECTED_TYPE` — the canonical CloudEvent type string the
  renderer accepts. `src/audit_doc.rs:39`.
- `audit_doc::ERROR_SENTINEL_PREFIX` — stable single-line error marker
  tests rely on. `src/audit_doc.rs:43`.
- `event_decode::decode_event(&[u8], &EventVerifierConfig) -> Result<CloudEventV1>`
  — shared decode path used by both the JSONL projector and
  `cellos-state-server`. Accepts raw `CloudEventV1` lines and
  `SignedEventEnvelopeV1` wrappers transparently; verifies envelopes when
  a keyring is configured. `src/event_decode.rs:100`.
- `event_decode::EventVerifierConfig` — verification config sourced from
  process env. `src/event_decode.rs:31`.
- `build_info::BUILD_SHA`, `short_sha`, `version_line`,
  `print_version_if_requested` — the `--version` / `-V` contract every
  binary in this crate honours. `src/build_info.rs:20`.

### Binaries

- `cellos-projector` — read a JSONL stream, project it, print snapshots.
  Default: print all snapshots as JSON; `--cell-id ID` / `--spec-id ID`
  selects a single snapshot; `--pretty` enables pretty-printing.
  `src/main.rs`.
- `cellos-state-server` — live fleet state projector over NATS
  JetStream. Replays all events, tails new arrivals, serves current
  snapshots via HTTP (`GET /healthz`, `GET /cells`, `GET /cells/<id>`,
  `GET /compliance/export`). `src/bin/cellos-state-server.rs`.
- `cellos-audit-justification` — SEC-19 fleet audit. Replays
  `cell.compliance.v1.summary` events from JetStream and reports
  monoculture / template patterns in `dnsEgressJustification` strings.
  Read-only — does not change admission gates.
  `src/bin/cellos-audit-justification.rs`.

## Architecture / how it works

```
                ┌────────────────────────────────────────┐
                │  JSONL file or NATS JetStream stream   │
                │  (cellos.events.>)                     │
                └────────────────────┬───────────────────┘
                 ┌───────────────────────────────────────┐
                 │  event_decode::decode_event           │
                 │   - parse CloudEventV1                │
                 │   - unwrap SignedEventEnvelopeV1      │
                 │   - verify if keyring configured      │
                 └────────────────────┬──────────────────┘
       ┌─────────────────────────────────────────────────────────────┐
       │  cellos_core::CellStateProjection (one per cell or spec)    │
       │     apply(event) → ProjectionLifecycleStage,                │
       │                    ProjectionIdentityStage,                 │
       │                    ProjectionExportStage, ...               │
       │     snapshot()  → CellStateSnapshot                         │
       └─────────────────────────────────────────────────────────────┘
        ┌─────────────────────────────────┐       ┌──────────────────┐
        │ cellos-projector (binary)       │       │ cellos-state-    │
        │   JSON / pretty stdout          │       │  server (HTTP)   │
        └─────────────────────────────────┘       └──────────────────┘
        ┌─────────────────────────────────┐
        │ audit_doc::render_audit_doc     │
        │   Markdown audit page           │
        └─────────────────────────────────┘
```

The on-disk projection is keyed by `cell:<cellId>` or `spec:<specId>`
(`src/main.rs:233`), preserving correlation when events carry only one
of the two. Lines that fail to parse abort the run with a line-numbered
error; signed envelopes that fail verification are fatal when any
verifier keyring is configured (the producer-opt-in / consumer-opt-in
pair from `src/main.rs:152`).

The audit-doc renderer (`src/audit_doc.rs`) writes six sections — header,
lifecycle table, host-probe series summary, guest-event sample, residue
class footer, integrity attestations — in a deterministic order. The
output is byte-identical for byte-identical input, so it round-trips
cleanly through git.

## Configuration

### `cellos-projector` (JSONL binary)

| Env var | Effect |
|---|---|
| `CELLOS_EVENT_VERIFY_KEYS_PATH` | JSON keyring (kid → base64url Ed25519 pubkey). Empty/unset → unwrap signed envelopes without verifying. `src/main.rs:182`. |
| `CELLOS_EVENT_VERIFY_HMAC_KEYS_PATH` | HMAC-SHA256 keyring. Same shape; value is base64url raw bytes. `src/main.rs:198`. |

Argv: `cellos-projector <events.jsonl> [--cell-id ID | --spec-id ID] [--pretty]`. Also: `--version` / `-V`, `--help` / `-h`.

### `cellos-state-server`

| Env var | Default | Effect |
|---|---|---|
| `CELLOS_NATS_URL` | `nats://localhost:4222` | NATS server URL. |
| `CELLOS_NATS_STREAM` | `CELLOS` | JetStream stream name. |
| `CELLOS_NATS_CONSUMER` | `cellos-state` | Durable consumer name. |
| `CELLOS_EVENT_SUBJECT` | `cellos.events.>` | Subject filter. |
| `CELLOS_STATE_ADDR` | `0.0.0.0:8080` | HTTP listen address. |
| `CELLOS_EVENT_VERIFY_KEYS_PATH` | unset | Same as the JSONL binary; envelope verification keys. |
| `CELLOS_EVENT_REQUIRE_SIGNED` | unset | When `1`/`true`/`yes`/`on`, reject raw `CloudEventV1` lines. `src/event_decode.rs:1`. |

### `cellos-audit-justification`

| Env var | Default | Effect |
|---|---|---|
| `CELLOS_NATS_URL` | `nats://localhost:4222` | NATS server URL. |
| `CELLOS_NATS_STREAM` | `CELLOS_EVENTS` | JetStream stream name. |
| `CELLOS_EVENT_SUBJECT` | `cellos.events.>` | Subject filter. |
| `CELLOS_AUDIT_MONOCULTURE_THRESHOLD` | `5` | Min distinct cells per justification to flag. |
| `CELLOS_AUDIT_DRAIN_TIMEOUT_MS` | `2000` | Per-message timeout to detect stream drain. |

Stdout: JSON report. Stderr: human summary plus structured logs.

## Examples

Project a JSONL stream and print every snapshot pretty:

```bash
cargo run -p cellos-projector --bin cellos-projector -- \
    /tmp/events.jsonl --pretty
```

Render an audit document from a single evidence-bundle event:

```rust
use cellos_projector::audit_doc::{render_audit_doc, EXPECTED_TYPE};
use serde_json::json;

let event = json!({
    "specversion": "1.0",
    "type":        EXPECTED_TYPE,
    "id":          "uuid-1",
    "source":      "//cellos/supervisor",
    "data": {
        "cellId":            "cell-abc",
        "runId":             "run-2026-05-16-001",
        "specId":            "spec-789",
        "specSignatureHash": "sha256:...",
        "lifecycle":         [],
        "hostSeries":        {},
        "guestEvents":       [],
        "residueClass":      "none",
        "attestations":      {},
    }
});

let markdown = render_audit_doc(&event);
println!("{markdown}");
```

Replay JetStream into live cell snapshots:

```bash
CELLOS_NATS_URL=nats://127.0.0.1:4222 \
CELLOS_NATS_STREAM=CELLOS \
CELLOS_STATE_ADDR=127.0.0.1:9090 \
cargo run -p cellos-projector --bin cellos-state-server
# in another shell
curl http://127.0.0.1:9090/cells
```

Audit DNS-egress justification entropy across a fleet:

```bash
CELLOS_NATS_URL=nats://127.0.0.1:4222 \
CELLOS_NATS_STREAM=CELLOS_EVENTS \
CELLOS_AUDIT_MONOCULTURE_THRESHOLD=10 \
cargo run -p cellos-projector --bin cellos-audit-justification \
    > /tmp/justification-report.json
```

## Testing

```bash
cargo test -p cellos-projector
```

Tests under `crates/cellos-projector/tests/` exercise the pure paths:

- `audit_doc_round_trip.rs` — pin the byte-deterministic Markdown output
  against a golden file.
- `i5_sign_emit_to_projector_e2e.rs` — end-to-end signing path: the
  supervisor's `SigningEventSink` emits a wrapped envelope, the
  projector decodes and verifies it.
- `signed_envelope_round_trip.rs``decode_event` against both raw and
  wrapped lines.
- `smoke.rs` — projection key resolution and snapshot shape.

The two NATS-backed binaries (`cellos-state-server`,
`cellos-audit-justification`) are smoke-checked in workspace integration
tests; running them locally needs `nats-server -js` on the configured
URL.

## Related crates

- [`cellos-core`]../cellos-core/README.md — owns
  `CellStateProjection`, `CellStateSnapshot`, `CloudEventV1`,
  `SignedEventEnvelopeV1`, the event-decoding primitives.
- [`cellos-supervisor`]../cellos-supervisor/README.md — producer of
  the wrapped `dev.cellos.events.signed_envelope.v1` transport this
  crate unwraps.
- [`cellos-server`]../cellos-server/README.md — the online HTTP API
  that mirrors what `cellos-state-server` does over NATS directly.

## ADRs

- [ADR-0011]../../docs/adr/0011-cellos-server-http-control-plane.md  HTTP control plane; `cellos-server` delegates the replay/projection
  contract this crate implements offline.
- [ADR-0014]../../docs/adr/0014-formation-cloudevent-state-model.md  formation event model; the `CellStateProjection` reducer honours it.