From db630be0b392214ea201f7fdb448b741654be79e Mon Sep 17 00:00:00 2001 From: Santiago Date: Sun, 23 Aug 2026 13:23:41 -0300 Subject: [PATCH 1/5] =?UTF-8?q?docs:=20adopt=20cairn=20=E2=80=94=20dossier?= =?UTF-8?q?,=20core=20manifests,=20verifier=20verbs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Survey (cairn-survey): census, module topology + instability, internal API surface snapshot, life map (lifetime basis; repo dormant since 2025-05), four curated flow docs (confirm nominated for a flow contract). Charter (cairn-charter): WORKSPACE.md + MODULE.md for the active core (pipeline, storage, queue); justfile verifier verbs (deps allowlist check, test-backed invariants); report-only routing policy. Findings recorded as requirements: clean checkout of main does not compile (floating pallas pre-release + gitignored Cargo.lock, REQ-WS-004); gasket git-fork pin (REQ-WS-002); mock relay adapter in production wiring (REQ-PIPE-001); dead Validated status (REQ-WS-003). Co-Authored-By: Claude Fable 5 --- WORKSPACE.md | 45 +++++ docs/architecture/00-shape.md | 44 +++++ docs/architecture/01-crate-graph.svg | 18 ++ docs/architecture/01-topology.md | 50 +++++ .../02-api/boros-internal-surface.txt | 171 ++++++++++++++++++ docs/architecture/03-life-map.md | 37 ++++ docs/architecture/ARCHITECTURE.md | 22 +++ docs/architecture/context-map.md | 27 +++ docs/architecture/flows/confirm.md | 38 ++++ docs/architecture/flows/fanout.md | 51 ++++++ docs/architecture/flows/peer-discovery.md | 33 ++++ docs/architecture/flows/submit.md | 45 +++++ justfile | 31 ++++ src/pipeline/MODULE.md | 42 +++++ src/queue/MODULE.md | 35 ++++ src/storage/MODULE.md | 39 ++++ tools/cairn_check_deps.py | 65 +++++++ 17 files changed, 793 insertions(+) create mode 100644 WORKSPACE.md create mode 100644 docs/architecture/00-shape.md create mode 100644 docs/architecture/01-crate-graph.svg create mode 100644 docs/architecture/01-topology.md create mode 100644 docs/architecture/02-api/boros-internal-surface.txt create mode 100644 docs/architecture/03-life-map.md create mode 100644 docs/architecture/ARCHITECTURE.md create mode 100644 docs/architecture/context-map.md create mode 100644 docs/architecture/flows/confirm.md create mode 100644 docs/architecture/flows/fanout.md create mode 100644 docs/architecture/flows/peer-discovery.md create mode 100644 docs/architecture/flows/submit.md create mode 100644 justfile create mode 100644 src/pipeline/MODULE.md create mode 100644 src/queue/MODULE.md create mode 100644 src/storage/MODULE.md create mode 100644 tools/cairn_check_deps.py diff --git a/WORKSPACE.md b/WORKSPACE.md new file mode 100644 index 0000000..b989f90 --- /dev/null +++ b/WORKSPACE.md @@ -0,0 +1,45 @@ ++++ +schema = "cairn/v0" +id = "boros" + +[workspace] +verify-all = "just gate" + +[process] +mode = "report-only" +auto-accept = { max-radius = 2, hotspot-overlap = false, api-delta = false } +sample-rate = 0.15 +mandatory = ["hotspot-overlap", "unsafe-touched", "tx-status-semantics-touched"] ++++ + +# Boros — workspace manifest + +## Purpose + +Boros is a Cardano transaction submission service ("tx omnivore"): accept raw +transactions over gRPC, queue them with priority and chaining semantics, fan +them out to Ouroboros tx-submission peers, and settle their status against the +chain (confirm, retry, rollback). See `docs/architecture/ARCHITECTURE.md`. + +## Invariants + +- **INV-WS-001** `[llm-judged]` — All `Transaction.status` mutations flow through the `storage` write API; no module manipulates persisted status by other means. The legal lifecycle is `Pending → InFlight → {Confirmed, Failed}`, plus `InFlight → Pending` (retry) and `Confirmed → InFlight` (rollback). *(`Validated` exists in the enum but no production path uses it — see REQ-WS-003.)* + +## Requirements + +- **REQ-WS-001** `[unverified]` — TODO(human): pin a `rust-version` (MSRV) in `Cargo.toml`; currently unpinned. +- **REQ-WS-002** `[unverified]` — Return `gasket` to a published upstream/crates.io release; the `construkts/gasket-rs` git fork is a stopgap. *(Decided by human, 2026-08-23.)* +- **REQ-WS-003** `[unverified]` — TODO(human): retire or wire the `Validated` transaction status; a dead state in the lifecycle enum invites drift. +- **REQ-WS-004** `[unverified]` — Commit `Cargo.lock` (this is a binary; the current `.gitignore` excludes it) and pin `pallas` exactly (`=1.0.0-alpha.2`) until upgraded deliberately. **Found 2026-08-23: a clean checkout of `main` does not compile** — the floating `1.0.0-alpha.2` requirement resolves to `pallas 1.1.1`, whose u5c/pparams API differs. The gate only ran after a local lockfile downgrade. + +## Non-goals + +- Boros does not build or balance transactions; it accepts fully-formed CBOR. +- Boros is not a general mempool: it tracks only transactions it accepted. + +## Language + +*queue* (named submission lane with weight), *chained queue* (lane serializing +dependent txs via lock tokens), *fanout* (broadcast to peers), *InFlight* +(broadcast, awaiting on-chain observation), *cursor* (last processed chain +point), *tip* (latest known chain point), *u5c* (UtxoRPC chain access). diff --git a/docs/architecture/00-shape.md b/docs/architecture/00-shape.md new file mode 100644 index 0000000..0983828 --- /dev/null +++ b/docs/architecture/00-shape.md @@ -0,0 +1,44 @@ +# 00 — Shape (census) + + + +## Generated numbers + +### Size per module (tokei, lines of code) + +| Module | Code lines | Files | +|---|---:|---:| +| `network` | 1,112 | 4 | +| `pipeline` | 907 | 5 | +| `storage` | 846 | 3 | +| `queue` | 642 | 4 | +| `ledger` | 501 | 5 | +| `server` | 305 | 3 | +| `signing` | 242 | 3 | +| `validation` | 96 | 1 | +| `main.rs` | 100 | 1 | +| **Total (src/)** | **3,972** | **27** | + +Generated `spec` crate (protobuf bindings, excluded from analysis): 1,950 code lines / 8 files. + +### Ratios + +- Comment lines in `src/`: 44 (~1.1% of code) — effectively uncommented. +- Test functions: 33, concentrated in 6 files: + `storage/sqlite.rs` (15), `queue/chaining.rs` (8), `pipeline/ingest.rs` (5), `queue/priority.rs` (3), `ledger/relay/mod.rs` (1), `network/mock_ouroboros_tx_submit_server.rs` (1). + No integration-test target; `test/` holds fixtures only (CBOR tx files, block data). +- `unsafe`: 1 expression in first-party code (`signing/key/derive.rs:85`, `SecretKeyExtended::from_bytes_unchecked`). + Transitive unsafe (cargo-geiger): **pending — not yet run** (full-graph compile deferred). + +### Dependency weight + +- 461 packages in the resolved graph for ~4k first-party lines (~8.7 lines per dependency package). +- Notable heavy subtrees: `tonic`/`prost` (gRPC), `sqlx` (SQLite), `pallas` 1.0.0-alpha.2 (Cardano primitives, `phase2` feature), `vaultrs` (HashiCorp Vault), `gasket` (pinned to a git fork: `construkts/gasket-rs`). + +## Interpretation (curated) + +Boros is a small, young service (v0.1.0, bin-only crate) whose weight is in its dependencies, not its own code. Three facts stand out: + +1. **Test coverage is bimodal.** `storage` and `queue` are meaningfully tested; `network`, `server`, `signing`, `validation` are essentially untested. The untested set includes the one `unsafe` block (key derivation) and all I/O boundaries. +2. **The comment ratio (~1%) means the code carries no embedded rationale.** Manifests and flow docs are the only place intent can live — chartering matters more than usual here. +3. **`gasket` is pinned to a personal git fork**, which is a supply-chain and bus-factor liability worth an explicit decision record. diff --git a/docs/architecture/01-crate-graph.svg b/docs/architecture/01-crate-graph.svg new file mode 100644 index 0000000..e2fdb62 --- /dev/null +++ b/docs/architecture/01-crate-graph.svg @@ -0,0 +1,18 @@ + + + + + + + + + +0 + +boros + + + diff --git a/docs/architecture/01-topology.md b/docs/architecture/01-topology.md new file mode 100644 index 0000000..2cfa43c --- /dev/null +++ b/docs/architecture/01-topology.md @@ -0,0 +1,50 @@ +# 01 — Topology + + + +> **Deviation note:** SPEC §2 expects crate-level topology. Boros is a single bin crate + one generated `spec` crate, so the crate graph (`01-crate-graph.svg`) is trivial. This file carries the *module-level* topology, which is where the structure actually lives. + +## Generated: module dependency edges + +Top-level module edges (submodules collapsed into parents; derived from `cargo modules dependencies`): + +```text +main → ledger, network, pipeline, queue, server, storage +pipeline → main, ledger, network, queue, signing, storage, validation +server → main, ledger, queue, storage, validation +queue → storage +validation → ledger +ledger, network, signing, storage → (no internal deps) +``` + +## Generated: instability table (module level) + +Ca = afferent (modules depending on it), Ce = efferent (internal modules it depends on), I = Ce/(Ca+Ce). + +| Module | Ca | Ce | I | Reading | +|---|---:|---:|---:|---| +| `ledger` | 4 | 0 | 0.00 | maximally stable | +| `storage` | 4 | 0 | 0.00 | maximally stable | +| `network` | 2 | 0 | 0.00 | stable | +| `signing` | 1 | 0 | 0.00 | stable | +| `queue` | 3 | 1 | 0.25 | stable-ish | +| `validation` | 2 | 1 | 0.33 | stable-ish | +| `main` (root) | 2 | 6 | 0.75 | orchestrator — **but Ca should be 0** | +| `server` | 1 | 5 | 0.83 | orchestrator | +| `pipeline` | 1 | 7 | 0.88 | orchestrator | + +## Generated: cycle detection + +One cycle at module level, through the crate root: + +- `main → pipeline → main` (and `main → server → main`): `Config` is defined in `main.rs` (`src/main.rs:72`) and imported by `pipeline::ingest` (`src/pipeline/ingest.rs:12`). The composition root is also a shared-type supplier. + +No cycles among the eight domain modules per `cargo-modules` — the domain graph is a clean DAG. **Tooling caveat:** `cargo-modules` misses const-only uses; source text shows `storage/mod.rs:6` importing `queue::DEFAULT_QUEUE`, making `storage ⇄ queue` a weak two-way coupling (queue → storage is the load-bearing direction; storage → queue is one default-value const). + +## Curated: layer labels (confirmed by human, 2026-08-23) + +| Layer | Modules | Intent | +|---|---|---| +| **Orchestration** | `main`, `pipeline`, `server` | composition root; gasket stage graph; gRPC API | +| **Domain** | `queue`, `validation` | tx queueing policy (priority, chaining); tx validation | +| **Foundation / adapters** | `ledger`, `network`, `storage`, `signing` | chain access (u5c/relay), Ouroboros peers, SQLite, key/Vault signing | diff --git a/docs/architecture/02-api/boros-internal-surface.txt b/docs/architecture/02-api/boros-internal-surface.txt new file mode 100644 index 0000000..07027e2 --- /dev/null +++ b/docs/architecture/02-api/boros-internal-surface.txt @@ -0,0 +1,171 @@ +GENERATED 2026-08-23 by cairn-survey @ fdec4da via `cargo modules structure --bin boros`. +NOTE: internal public-item surface of a bin-only crate — NOT a semver API surface (cargo public-api inapplicable; see 00-shape.md deviation notes). + + +crate boros +├── struct Config: pub(crate) +│ └── fn new: pub +├── mod ledger: pub(crate) +│ ├── mod relay: pub +│ │ ├── struct MockRelayDataAdapter: pub +│ │ │ ├── async fn get_relays: pub(self) +│ │ │ └── fn new: pub +│ │ └── trait RelayDataAdapter: pub +│ └── mod u5c: pub +│ ├── type ChainSyncStream: pub +│ ├── struct Config: pub +│ ├── enum Event: pub +│ ├── type Point: pub +│ ├── trait U5cDataAdapter: pub +│ ├── struct U5cDataAdapterImpl: pub +│ │ ├── async fn fetch_pparams: pub(self) +│ │ ├── async fn fetch_tip: pub(self) +│ │ ├── async fn fetch_utxos: pub(self) +│ │ ├── fn interceptor: pub(self) +│ │ ├── async fn stream: pub(self) +│ │ └── async fn try_new: pub +│ ├── fn map_conway_pparams: pub(self) +│ └── fn map_multi_era_pparams: pub(self) +├── async fn main: pub(crate) +├── mod network: pub(crate) +│ ├── mod mempool: pub +│ │ ├── struct Event: pub +│ │ ├── struct Mempool: pub +│ │ │ ├── fn acknowledge: pub +│ │ │ ├── fn find_inflight: pub +│ │ │ ├── fn new: pub +│ │ │ ├── fn notify: pub +│ │ │ ├── fn receive: pub(self) +│ │ │ └── fn receive_raw: pub +│ │ ├── enum MempoolError: pub +│ │ ├── struct MempoolState: pub(self) +│ │ ├── struct Tx: pub +│ │ ├── type TxHash: pub(self) +│ │ └── enum TxStage: pub +│ ├── mod mock_ouroboros_tx_submit_server: pub +│ │ └── struct MockOuroborosTxSubmitPeerServer: pub +│ │ ├── async fn init: pub +│ │ ├── fn new: pub +│ │ └── async fn start_background_task: pub(self) +│ ├── mod peer: pub +│ │ ├── struct Peer: pub +│ │ │ ├── async fn collect_transactions: pub(self) +│ │ │ ├── async fn discover_peers: pub +│ │ │ ├── async fn init: pub +│ │ │ ├── fn new: pub +│ │ │ ├── async fn propagate_txs: pub(self) +│ │ │ ├── async fn query_peer_sharing_mode: pub +│ │ │ └── fn start_background_task: pub(self) +│ │ └── enum PeerError: pub +│ └── mod peer_manager: pub +│ ├── struct PeerManager: pub +│ │ ├── async fn add_peer: pub +│ │ ├── async fn connected_peers_count: pub +│ │ ├── async fn init: pub +│ │ ├── async fn is_peer_exist: pub +│ │ ├── fn new: pub +│ │ └── async fn pick_peer_rand: pub +│ ├── struct PeerManagerConfig: pub +│ └── enum PeerManagerError: pub +├── mod pipeline: pub(crate) +│ ├── mod ingest: pub +│ │ ├── struct Stage: pub +│ │ │ └── fn new: pub +│ │ └── struct Worker: pub +│ │ ├── async fn bootstrap: pub(self) +│ │ ├── async fn execute: pub(self) +│ │ └── async fn schedule: pub(self) +│ ├── mod monitor: pub +│ │ ├── struct Config: pub +│ │ ├── struct Stage: pub +│ │ │ └── fn new: pub +│ │ └── struct Worker: pub +│ │ ├── async fn bootstrap: pub(self) +│ │ ├── async fn execute: pub(self) +│ │ └── async fn schedule: pub(self) +│ ├── mod peer_discovery: pub +│ │ ├── enum FanoutError: pub +│ │ ├── struct Stage: pub +│ │ │ └── fn new: pub +│ │ └── struct Worker: pub +│ │ ├── async fn bootstrap: pub(self) +│ │ ├── async fn execute: pub(self) +│ │ └── async fn schedule: pub(self) +│ └── async fn run: pub +├── mod queue: pub(crate) +│ ├── struct Config: pub +│ ├── mod chaining: pub +│ │ ├── type LockEvent: pub(self) +│ │ └── struct TxChaining: pub +│ │ ├── fn is_chained_queue: pub +│ │ ├── async fn is_valid_token: pub +│ │ ├── async fn lock: pub +│ │ ├── fn new: pub +│ │ └── async fn unlock: pub +│ └── mod priority: pub +│ └── struct Priority: pub +│ ├── fn new: pub +│ ├── async fn next: pub +│ └── fn quota: pub(self) +├── mod server: pub(crate) +│ ├── struct Config: pub +│ ├── async fn run: pub +│ └── mod submit: pub(self) +│ └── struct SubmitServiceImpl: pub +│ └── fn new: pub +├── mod signing: pub(crate) +│ ├── struct Config: pub +│ ├── struct Secret: pub(self) +│ ├── trait SigningAdapter: pub +│ ├── enum SigningError: pub +│ ├── mod hashicorp: pub +│ │ └── struct HashicorpVaultClient: pub +│ │ ├── async fn get_mnemonic: pub(self) +│ │ ├── fn new: pub +│ │ ├── async fn sign: pub(self) +│ │ └── async fn verify: pub(self) +│ └── mod key: pub +│ ├── mod derive: pub +│ │ ├── fn from_bip39_mnenomic: pub(self) +│ │ ├── fn generate_account_key: pub +│ │ ├── fn generate_address: pub +│ │ ├── fn generate_address_with_delegation: pub +│ │ ├── fn generate_delegation_keypair: pub +│ │ ├── fn generate_payment_keypair: pub +│ │ ├── fn get_ed25519_keypair: pub +│ │ └── fn to_ed25519_keypair: pub +│ └── mod sign: pub +│ ├── fn sign_transaction: pub +│ └── fn to_built_transaction: pub +├── mod storage: pub(crate) +│ ├── struct Config: pub +│ ├── struct Cursor: pub +│ │ ├── fn from_row: pub(self) +│ │ └── fn new: pub +│ ├── struct Transaction: pub +│ │ ├── fn from_row: pub(self) +│ │ └── fn new: pub +│ ├── struct TransactionState: pub +│ │ └── fn from_row: pub(self) +│ ├── enum TransactionStatus: pub +│ └── mod sqlite: pub +│ ├── struct SqliteCursor: pub +│ │ ├── async fn current: pub +│ │ ├── fn new: pub +│ │ └── async fn set: pub +│ ├── struct SqliteStorage: pub +│ │ ├── async fn migrate: pub +│ │ └── async fn new: pub +│ └── struct SqliteTransaction: pub +│ ├── async fn create: pub +│ ├── async fn find: pub +│ ├── async fn find_to_rollback: pub +│ ├── async fn latest: pub +│ ├── fn new: pub +│ ├── async fn next: pub +│ ├── async fn state: pub +│ ├── async fn update: pub +│ └── async fn update_batch: pub +└── mod validation: pub(crate) + ├── async fn evaluate_tx: pub + └── async fn validate_tx: pub diff --git a/docs/architecture/03-life-map.md b/docs/architecture/03-life-map.md new file mode 100644 index 0000000..b468eba --- /dev/null +++ b/docs/architecture/03-life-map.md @@ -0,0 +1,37 @@ +# 03 — Life map + + + +> **Deviation note:** the skill's 12-month churn window is empty — the repo's last commit is 2025-05-27 (~15 months before this survey). 42 of 44 lifetime commits landed Jan–May 2025. The partition below is derived from **lifetime** churn × complexity instead, and the whole repo carries a `dormant` flag. Whether dormancy demotes everything to fossil is a human judgment recorded after this survey. + +## Generated: churn × complexity (lifetime) + +Top files by churn (total lines added+deleted across history; renames noted): + +| File | Churn | Complexity (scc) | Note | +|---|---:|---:|---| +| `pipeline/ingest.rs` | 1,086 | 11 | churn leader | +| `storage/sqlite.rs` | 914 | 14 | | +| `network/peer.rs` | ~700 | **23** | complexity leader; churned as `pipeline/fanout/tx_submit_peer.rs` pre-rename | +| `ledger/u5c/mod.rs` | 580 | 12 | | +| `queue/chaining.rs` | 425 | 8 | | +| `network/mempool.rs` | ~314 | — | churned as `pipeline/fanout/mempool.rs` | +| `server/submit.rs` | 225 | 11 | | + +Commits per top module (lifetime): pipeline 25, server 12, storage 11, ledger 6, network 6, queue 3, signing 3, validation 2. + +## Curated: partition + +| Tier | Modules | Basis | +|---|---|---| +| **Active core** | `pipeline`, `storage`, `network`, `queue` | high churn × complexity intersection; `network` inherits the churn of its pre-rename `fanout` history | +| **Stable periphery** | `ledger`, `server` | moderate churn, adapter/API shells | +| **Thin periphery** | `signing`, `validation`, `main` | low churn, small, single-purpose | +| **Fossil** | *(none within repo)* | repo is young; but **entire repo dormant since 2025-05** | + +## Chartering nomination + +Per the adoption path (charter the 2–3 active-core modules first): **`pipeline`**, **`storage`**, **`queue`** — with `network` as the fourth if appetite allows (highest complexity, weakest tests: 1 test function, and it's in the mock). + +Change-coupling analysis (code-maat): **pending — tool not installed**; noted per skill guardrail. +Age strata (git-of-theseus): **pending — tool not installed**. diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md new file mode 100644 index 0000000..f9b6994 --- /dev/null +++ b/docs/architecture/ARCHITECTURE.md @@ -0,0 +1,22 @@ +# Boros — Architecture + +Boros ("tx omnivore") is a Cardano transaction submission service: it accepts raw transactions over gRPC, queues them with priority and chaining semantics in SQLite, fans them out to Ouroboros tx-submission peers, and tracks their fate on-chain (confirmation, retry, rollback) via UtxoRPC chainsync. + +This dossier is maintained under the [Cairn](https://github.com/txpipe) discipline: derived files are regenerated, never hand-edited; intent lives in module manifests (`MODULE.md`) and `WORKSPACE.md`. + +## The dossier + +- [00 — Shape (census)](00-shape.md) — size, test distribution, unsafe count, dependency weight. +- [01 — Topology](01-topology.md) — module graph, instability table, the one root cycle, confirmed layer labels. Rendered crate graph: [01-crate-graph.svg](01-crate-graph.svg). +- [02 — API surface](02-api/boros-internal-surface.txt) — internal public-item snapshot (bin-only crate; see deviation note inside). +- [03 — Life map](03-life-map.md) — lifetime churn × complexity partition; **repo dormant since 2025-05, judged "will resume" (human, 2026-08-23)**; chartering nominations. +- Flows (curated sequence diagrams, type-labeled): + - [submit](flows/submit.md) — gRPC → validated → queued. + - [fanout](flows/fanout.md) — priority drain → sign → broadcast → InFlight. + - [confirm](flows/confirm.md) — chainsync → Confirmed/retry/rollback + cursor. **Flow-contract nominee.** + - [peer-discovery](flows/peer-discovery.md) — relay/peer-sharing pool top-up (relay side currently mocked). +- [context-map.md](context-map.md) — generated from manifests (pending charter). + +## Reading order for newcomers + +Life map → topology → the four flows. The tx lifecycle (`Pending → Validated → InFlight → Confirmed | Failed`, defined in `src/storage/mod.rs`) is the spine: submit produces `Pending`, fanout moves to `InFlight`/`Failed`, monitor settles `Confirmed`/retries. (`Validated` is defined but currently unused by any flow — see survey findings.) diff --git a/docs/architecture/context-map.md b/docs/architecture/context-map.md new file mode 100644 index 0000000..471dcfc --- /dev/null +++ b/docs/architecture/context-map.md @@ -0,0 +1,27 @@ +# Context map + + + +```mermaid +flowchart TD + pipeline -->|customer-supplier| storage + pipeline -->|customer-supplier| queue + pipeline -->|customer-supplier| signing + pipeline -->|customer-supplier| network + pipeline -->|conformist| ledger + queue -->|customer-supplier| storage + storage:::openhost + classDef openhost stroke-dasharray: 5 5 +``` + +| Consumer | Relationship | Supplier | Declared in | +|---|---|---|---| +| `queue` | `customer-supplier` | `storage` | `src/queue/MODULE.md` | +| `pipeline` | `customer-supplier` | `storage` | `src/pipeline/MODULE.md` | +| `pipeline` | `customer-supplier` | `queue` | `src/pipeline/MODULE.md` | +| `pipeline` | `customer-supplier` | `signing` | `src/pipeline/MODULE.md` | +| `pipeline` | `customer-supplier` | `network` | `src/pipeline/MODULE.md` | +| `pipeline` | `conformist` | `ledger` (u5c vocabulary) | `src/pipeline/MODULE.md` | +| `storage` | `open-host` (supplier-side declaration) | — | `src/storage/MODULE.md` | + +Modules without manifests yet (`ledger`, `network`, `signing`, `server`, `validation`) appear only as referenced suppliers; their consumer-side declarations are pending their own charter. diff --git a/docs/architecture/flows/confirm.md b/docs/architecture/flows/confirm.md new file mode 100644 index 0000000..e13e16a --- /dev/null +++ b/docs/architecture/flows/confirm.md @@ -0,0 +1,38 @@ ++++ +schema = "cairn/v0" +flow = "confirm" +short = "CONFIRM" +participants = ["pipeline", "ledger/u5c", "storage"] ++++ + +# Flow — confirm/rollback (monitor) + + + +The `monitor` gasket stage follows the chain through the u5c `ChainSyncStream` and drives the tail of the tx lifecycle: `InFlight → Confirmed` on inclusion, `InFlight → Pending` on retry timeout, and un-confirmation on rollback. The chain cursor is persisted after each event for crash recovery. + +**⚑ Flow-contract nominee (SPEC §12):** this flow's defining invariants are temporal and multi-module (crash recovery, rollback correctness, retry/confirm races). It is the survey's nomination for the first Quint model, if/when one is written. + +```mermaid +sequenceDiagram + participant L as ledger::u5c
ChainSyncStream + participant M as pipeline::monitor
Stage/Worker + participant DB as storage::sqlite
SqliteTransaction + participant CU as storage::sqlite
SqliteCursor + + L-->>M: Event::RollForward(Point, Vec) + M->>DB: find(InFlight) → Vec + M->>DB: update_batch(included ⇒ Confirmed, slot = block slot) + M->>DB: update_batch(stale beyond retry_slot_diff ⇒ Pending, slot = None) + L-->>M: Event::Rollback(Point) + M->>DB: find_to_rollback(slot) + M->>DB: update_batch(confirmed after slot ⇒ InFlight, slot = None) + M->>CU: set(Cursor { slot, hash }) — after status updates +``` + +## Invariants + +- **INV-CONFIRM-001** `[unverified]` — A transaction is `Confirmed` only when observed in a rolled-forward block; its `slot` then records the confirmation slot. +- **INV-CONFIRM-002** `[unverified]` — An `InFlight` transaction not confirmed within `retry_slot_diff` slots returns to `Pending` (and will be re-broadcast by fanout), so no transaction is stranded `InFlight` forever while the chain advances. +- **INV-CONFIRM-003** `[unverified]` — On rollback to slot S, transactions confirmed after S revert to `InFlight` and are eligible for re-confirmation; no transaction remains `Confirmed` at a slot beyond the new tip. +- **INV-CONFIRM-004** `[unverified]` — The cursor is persisted only after the corresponding status updates, so a crash between the two replays the event (at-least-once) rather than skipping it. diff --git a/docs/architecture/flows/fanout.md b/docs/architecture/flows/fanout.md new file mode 100644 index 0000000..8157fec --- /dev/null +++ b/docs/architecture/flows/fanout.md @@ -0,0 +1,51 @@ ++++ +schema = "cairn/v0" +flow = "fanout" +short = "FANOUT" +participants = ["pipeline", "queue", "signing", "validation", "ledger/u5c", "network", "storage"] ++++ + +# Flow — fanout (queue → peers) + + + +The `ingest` gasket stage drains `Pending` transactions by queue priority, optionally signs them server-side, re-validates, and broadcasts them to all connected Ouroboros tx-submission peers. Broadcast marks the tx `InFlight` stamped with the tip slot. + +```mermaid +sequenceDiagram + participant P as pipeline::ingest
Stage/Worker + participant PR as queue::priority
Priority + participant SG as signing
dyn SigningAdapter (Vault) + participant V as validation + participant L as ledger::u5c
dyn U5cDataAdapter + participant DB as storage::sqlite
SqliteTransaction + participant N as network
PeerManager → Peer/Mempool + + loop schedule (cap = 50 − queued) + P->>PR: next(Pending, cap) + PR->>DB: weighted pick per queue config + PR-->>P: Vec + end + loop per Transaction + opt queue.server_signing + P->>SG: sign(tx.raw) + SG-->>P: signed CBOR (Vec) + end + P->>V: validate_tx + evaluate_tx (MultiEraTx) + V->>L: ledger state + alt validation fails + P->>DB: update(status = Failed) + else ok + P->>N: broadcast Message> (gasket channel) + N->>N: Peer mempools serve ouroboros tx-submission + P->>L: fetch_tip() + P->>DB: update(status = InFlight, slot = tip) + end + end +``` + +## Invariants + +- **INV-FANOUT-001** `[unverified]` — A transaction reaches `InFlight` only after a successful broadcast, and its `slot` is set to the tip at broadcast time (this is what `monitor`'s retry window keys off). +- **INV-FANOUT-002** `[unverified]` — A transaction on a `server_signing` queue is never broadcast unsigned; absence of a signing adapter causes retry, not passthrough. +- **INV-FANOUT-003** `[unverified]` — In-flight output never exceeds the channel cap (50); scheduling backs off rather than dropping. diff --git a/docs/architecture/flows/peer-discovery.md b/docs/architecture/flows/peer-discovery.md new file mode 100644 index 0000000..fbe165f --- /dev/null +++ b/docs/architecture/flows/peer-discovery.md @@ -0,0 +1,33 @@ ++++ +schema = "cairn/v0" +flow = "peer-discovery" +short = "DISCOVERY" +participants = ["pipeline", "ledger/relay", "network"] ++++ + +# Flow — peer discovery + + + +The `peer_discovery` gasket stage tops up the peer pool toward `desired_peer_count`, choosing 50/50 between on-chain relays (via `RelayDataAdapter` — **currently the mock implementation**, see `src/pipeline/mod.rs:33`) and peers learned from existing peers' peer-sharing. + +```mermaid +sequenceDiagram + participant R as ledger::relay
dyn RelayDataAdapter (MOCK) + participant D as pipeline::peer_discovery
Stage/Worker + participant PM as network::peer_manager
PeerManager + participant PE as network::peer
Peer (ouroboros) + + loop while connected < desired_peer_count + D->>R: get_relays() → Vec + D->>PM: pick_peer_rand(peers_per_request) → Option + Note over D: coin flip chooses relay vs peer-shared address + D->>PM: add_peer(addr) + PM->>PE: connect + start tx-submission protocol + end +``` + +## Invariants + +- **INV-DISCOVERY-001** `[unverified]` — The pool converges toward `desired_peer_count` and does not add peers beyond outstanding need (`peer_discovery_queue` bounds in-flight additions). +- **INV-DISCOVERY-002** `[unverified]` — Relay data currently comes from `MockRelayDataAdapter`; production readiness requires a real adapter (this is declared intent, recorded so the gap is visible). diff --git a/docs/architecture/flows/submit.md b/docs/architecture/flows/submit.md new file mode 100644 index 0000000..7e795db --- /dev/null +++ b/docs/architecture/flows/submit.md @@ -0,0 +1,45 @@ ++++ +schema = "cairn/v0" +flow = "submit" +short = "SUBMIT" +participants = ["server", "validation", "ledger/u5c", "queue", "storage"] ++++ + +# Flow — submit (gRPC → queued) + + + +A client submits one or more raw transactions over gRPC. Each is decoded, optionally validated against ledger state, checked against queue lock tokens, and persisted as `Pending`. + +```mermaid +sequenceDiagram + participant C as Client (gRPC) + participant S as server::submit
SubmitServiceImpl + participant V as validation + participant L as ledger::u5c
dyn U5cDataAdapter + participant Q as queue::chaining
TxChaining + participant DB as storage::sqlite
SqliteTransaction + + C->>S: SubmitTxRequest { tx: [{raw, queue?, lock_token?}] } + S->>S: MultiEraTx::decode(raw) — err ⇒ failed_precondition (whole request) + alt queue is not server-signing + S->>V: validate_tx(&MultiEraTx) + V->>L: utxos / pparams + L-->>V: ledger state + S->>V: evaluate_tx(&MultiEraTx) + Note over S: validation/evaluation error ⇒ tx silently skipped (not in response) + end + opt chained queue + S->>Q: is_valid_token(queue, lock_token) + Q-->>S: false ⇒ permission_denied (whole request) + end + S->>DB: create(&[Transaction { status: Pending }]) + S->>Q: unlock(chained queues) + S-->>C: SubmitTxResponse { ref: [tx hashes] } +``` + +## Invariants + +- **INV-SUBMIT-001** `[unverified]` — A transaction accepted into storage always enters with status `Pending` and the queue name resolved (unknown queue falls back to the default queue). +- **INV-SUBMIT-002** `[unverified]` — A submission to a chained queue is persisted only if its lock token is valid; the queue is unlocked only after the batch is persisted. +- **INV-SUBMIT-003** `[unverified]` — Every hash in `SubmitTxResponse.ref` corresponds to a transaction that was persisted. *(Note the converse is currently false by design: txs failing validation are skipped silently and produce no error — an intentional-or-not behavior worth a human decision.)* diff --git a/justfile b/justfile new file mode 100644 index 0000000..7f2c6a6 --- /dev/null +++ b/justfile @@ -0,0 +1,31 @@ +set shell := ["bash", "-o", "pipefail", "-cu"] + +# Cairn verifier verbs (SPEC §7): exit 0 = pass, stdout = evidence. +# Every verb here is referenced by a manifest; renaming one without +# updating its references is a lint error (L5). + +# Run all bound constraints (workspace verify-all) +gate: check-deps-all test-storage test-queue test-ingest + @echo "gate: all bound constraints passed" + +# --- dependency constraints ------------------------------------------------- + +# Check a module's internal deps against its MODULE.md allowlist +check-deps id: + python3 tools/cairn_check_deps.py {{id}} + +check-deps-all: + python3 tools/cairn_check_deps.py storage + python3 tools/cairn_check_deps.py queue + python3 tools/cairn_check_deps.py pipeline + +# --- test-backed invariants ------------------------------------------------- + +test-storage: + cargo test storage:: -- --nocapture 2>&1 | tail -20 + +test-queue: + cargo test queue:: -- --nocapture 2>&1 | tail -20 + +test-ingest: + cargo test ingest_tests:: -- --nocapture 2>&1 | tail -20 diff --git a/src/pipeline/MODULE.md b/src/pipeline/MODULE.md new file mode 100644 index 0000000..6c35d18 --- /dev/null +++ b/src/pipeline/MODULE.md @@ -0,0 +1,42 @@ ++++ +schema = "cairn/v0" +id = "pipeline" +role = "core-domain" +parnas-secret = "the runtime topology — which gasket stages exist, how they are scheduled, and how backpressure (channel cap) is applied" + +[deps] +internal-allowed = ["ledger", "network", "queue", "signing", "storage", "validation"] +external-policy = "review-new" +verify = "just check-deps pipeline" ++++ + +# pipeline — module manifest + +## Purpose + +The orchestrator: three gasket stages wired in `run()` — `ingest` (drain +Pending by priority → sign → validate → broadcast → InFlight), `monitor` +(chainsync → Confirmed / retry / rollback + cursor), `peer_discovery` (peer +pool top-up). Temporal behavior across these stages is specified by the flow +contracts: `fanout`, `confirm`, `peer-discovery` (see `docs/architecture/flows/`). + +## Invariants + +- **INV-PIPE-001** `[bound → just test-ingest]` — Phase-1 validation and phase-2 evaluation accept known-valid Conway transactions and reject invalid ones (fixture-backed, `ingest_tests`). +- **INV-PIPE-002** `[llm-judged]` — Stage failures are contained by gasket retry policy; a failing transaction is marked `Failed` rather than wedging the stage (refines INV-WS-001). +- **INV-PIPE-003** `[unverified]` — Temporal invariants of this module's stages are owned by the flow docs (INV-FANOUT-*, INV-CONFIRM-*, INV-DISCOVERY-*); this entry records that they are not yet verified anywhere. + +## Requirements + +- **REQ-PIPE-001** `[unverified]` — `run()` wires `MockRelayDataAdapter` into production peer discovery (`src/pipeline/mod.rs:33`); this is a known gap that must be replaced with a real on-chain relay source. *(Decided by human, 2026-08-23.)* +- **REQ-PIPE-002** `[unverified]` — TODO(human): `pipeline` imports `Config` from the crate root (the `main ⇄ pipeline` cycle in `01-topology.md`); consider moving shared config types out of `main.rs`. + +## Relationships + +- `customer-supplier` with **storage**, **queue**, **signing** (pipeline is the customer). +- `conformist` toward **ledger** (u5c): pipeline consumes the UtxoRPC event/point vocabulary as-is, no translation layer. +- `customer-supplier` with **network** (pipeline feeds the broadcast channel network peers consume). + +## Non-goals + +- No policy decisions: what to pick (queue), what is valid (validation), how to persist (storage) are supplied, not owned. diff --git a/src/queue/MODULE.md b/src/queue/MODULE.md new file mode 100644 index 0000000..a65cbd6 --- /dev/null +++ b/src/queue/MODULE.md @@ -0,0 +1,35 @@ ++++ +schema = "cairn/v0" +id = "queue" +role = "core-domain" +parnas-secret = "the scheduling policy — how weighted priority picks the next transactions, and how chained queues serialize dependent submissions via lock tokens" + +[deps] +internal-allowed = ["storage"] +external-policy = "review-new" +verify = "just check-deps queue" ++++ + +# queue — module manifest + +## Purpose + +The scheduling heart of Boros. `priority` allocates each fanout batch across +named queues by weight; `chaining` gives dependent-transaction queues +exclusive-lock semantics (token-gated submission, streamed lock state). + +## Invariants + +- **INV-QUEUE-001** `[bound → just test-queue]` — Batch quota is allocated proportionally to queue weights and fully distributed (remainder included) (`it_should_calculate_quota`). +- **INV-QUEUE-002** `[bound → just test-queue]` — Transactions in queues removed from config are still drained, not stranded (`it_should_return_next_transactions_when_a_queue_is_removed_from_config`). +- **INV-QUEUE-003** `[bound → just test-queue]` — A chained queue admits one lock holder at a time; competing lockers wait or time out (`it_should_lock_queue`, `it_should_wait_timeout_to_lock_queue`, `it_should_lock_many_queue`). +- **INV-QUEUE-004** `[bound → just test-queue]` — Only the current lock token is valid for submission to a chained queue; unlock invalidates it (`it_should_return_token_*`, `it_should_unlock_queue`). +- **INV-QUEUE-005** `[llm-judged]` — Queue identity is its name alone (`Config` hashes/compares by name), so config reload with changed weights re-targets the same queue rather than creating a phantom. + +## Relationships + +- `customer-supplier` with **storage** (queue is the customer: priority and chaining read/write through storage's store types). + +## Non-goals + +- No transport, no validation, no signing: queue decides *order and admission*, nothing else. diff --git a/src/storage/MODULE.md b/src/storage/MODULE.md new file mode 100644 index 0000000..5f21da6 --- /dev/null +++ b/src/storage/MODULE.md @@ -0,0 +1,39 @@ ++++ +schema = "cairn/v0" +id = "storage" +role = "generic-subdomain" +parnas-secret = "how transaction/cursor state is persisted — SQLite, the schema, and all SQL live only here" + +[deps] +internal-allowed = ["queue#const-only"] +external-policy = "review-new" +verify = "just check-deps storage" ++++ + +# storage — module manifest + +## Purpose + +Persistence for the two durable facts Boros owns: the transaction queue +(`Transaction`, `TransactionStatus`) and the chainsync `Cursor`. Exposes typed +stores (`SqliteTransaction`, `SqliteCursor`) over a migrated SQLite database. + +## Invariants + +- **INV-STORE-001** `[bound → just test-storage]` — Creating transactions with declared dependencies fails unless every dependency is already present (`it_should_fail_create_with_invalid_dependencies`). +- **INV-STORE-002** `[bound → just test-storage]` — `next(status, quotas)` honors per-queue quotas and status filtering; transactions from queues no longer in config still drain (`it_should_find_next*`). +- **INV-STORE-003** `[bound → just test-storage]` — `find_to_rollback(slot)` returns only transactions whose recorded slot is affected by a rollback to `slot` (`it_should_find_to_rollback*`). +- **INV-STORE-004** `[bound → just test-storage]` — Cursor `set` upserts: a second write updates rather than duplicates (`it_should_set_when_it_updates`). +- **INV-STORE-005** `[llm-judged]` — No SQL and no `sqlx` usage outside this module; consumers speak `Transaction`/`Cursor`, never rows. + +## Requirements + +- **REQ-STORE-001** `[unverified]` — Migrations run to completion before any query is served (currently by call order in `main.rs`; nothing enforces it). + +## Relationships + +- *(supplier)* `open-host` — storage is the shared persistence supplier for `queue`, `pipeline`, and `server`; its store types are the published surface. + +## Non-goals + +- No domain policy: which tx goes next (queue), when to retry (pipeline) are not storage's decisions. diff --git a/tools/cairn_check_deps.py b/tools/cairn_check_deps.py new file mode 100644 index 0000000..5c82bb3 --- /dev/null +++ b/tools/cairn_check_deps.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Bound verifier for MODULE.md [deps].internal-allowed (cairn SPEC §3.1). + +Derives the module's actual internal dependencies from `cargo modules +dependencies` and asserts they are a subset of the manifest allowlist. + +Known blind spot: cargo-modules does not report const-only uses (see +docs/architecture/01-topology.md); such edges must be policed by review. +""" + +import re +import subprocess +import sys +import tomllib +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +TOP_MODULES = { + "ledger", "network", "pipeline", "queue", + "server", "signing", "storage", "validation", +} + + +def manifest_allowlist(module_id: str) -> list[str]: + manifest = ROOT / "src" / module_id / "MODULE.md" + if not manifest.exists(): + sys.exit(f"FAIL: no manifest at {manifest}") + text = manifest.read_text() + m = re.match(r"\+\+\+\n(.*?)\n\+\+\+", text, re.DOTALL) + if not m: + sys.exit(f"FAIL: no +++ frontmatter in {manifest}") + front = tomllib.loads(m.group(1)) + return [d.split("#")[0] for d in front.get("deps", {}).get("internal-allowed", [])] + + +def actual_deps(module_id: str) -> set[str]: + out = subprocess.run( + ["cargo", "modules", "dependencies", "--bin", "boros", + "--no-externs", "--no-sysroot"], + cwd=ROOT, capture_output=True, text=True, check=True, + ).stdout + deps: set[str] = set() + for src, dst in re.findall(r'"(boros[^"]*)" -> "(boros[^"]*)".*"uses"', out): + src_mod = (src.split("::") + [None])[1] + dst_mod = (dst.split("::") + [None])[1] + if src_mod == module_id and dst_mod in TOP_MODULES and dst_mod != module_id: + deps.add(dst_mod) + return deps + + +def main() -> None: + module_id = sys.argv[1] + allowed = set(manifest_allowlist(module_id)) + actual = actual_deps(module_id) + illegal = actual - allowed + print(f"module: {module_id}") + print(f"allowed: {sorted(allowed) or '(none)'}") + print(f"actual: {sorted(actual) or '(none)'}") + if illegal: + sys.exit(f"FAIL: undeclared internal deps: {sorted(illegal)}") + print("PASS: actual deps ⊆ allowlist") + + +if __name__ == "__main__": + main() From 0a01b5679b275169b27dadc6ed1da6c07370e72d Mon Sep 17 00:00:00 2001 From: Santiago Date: Sun, 23 Aug 2026 21:17:25 -0300 Subject: [PATCH 2/5] docs: rearrange cairn artifacts to amended spec (.cairn/, no REQ, ADRs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dossier moves from docs/architecture/ to .cairn/ — the framework's honestly-branded namespace — with unnumbered filenames; reading order now lives in .cairn/README.md (merges the old ARCHITECTURE.md narrative with the ownership note). Committed .cairn/.gitignore reserves out/ for machine exhaust. Requirements sections retired per the eviction test: - die-on-fix items became ADRs: 0001 gasket fork (accepted), 0002 toolchain/dependency pinning (proposed; clean-checkout build failure), 0003 real relay source (accepted), 0004 Validated status (open) - REQ-STORE-001 survives as INV-STORE-006 [unverified] - the Config-cycle note moved to pipeline's non-normative Notes - INV-DISCOVERY-002 (mock relay) evicted from the flow doc to ADR 0003 Dependency gates re-verified green after the move. Co-Authored-By: Claude Fable 5 --- .cairn/.gitignore | 1 + .cairn/README.md | 17 ++++++++++++++ .../api}/boros-internal-surface.txt | 2 +- {docs/architecture => .cairn}/context-map.md | 0 .../crate-graph.svg | 0 ...0001-return-gasket-to-published-release.md | 15 +++++++++++++ .../0002-pin-toolchain-and-dependencies.md | 15 +++++++++++++ ...03-real-relay-source-for-peer-discovery.md | 15 +++++++++++++ .../0004-fate-of-the-validated-status.md | 15 +++++++++++++ .../architecture => .cairn}/flows/confirm.md | 0 {docs/architecture => .cairn}/flows/fanout.md | 0 .../flows/peer-discovery.md | 3 +-- {docs/architecture => .cairn}/flows/submit.md | 0 .../03-life-map.md => .cairn/life-map.md | 2 +- .../00-shape.md => .cairn/shape.md | 4 ++-- .../01-topology.md => .cairn/topology.md | 4 ++-- WORKSPACE.md | 18 +++++++-------- docs/architecture/ARCHITECTURE.md | 22 ------------------- src/pipeline/MODULE.md | 15 ++++++++----- src/storage/MODULE.md | 5 +---- tools/cairn_check_deps.py | 2 +- 21 files changed, 105 insertions(+), 50 deletions(-) create mode 100644 .cairn/.gitignore create mode 100644 .cairn/README.md rename {docs/architecture/02-api => .cairn/api}/boros-internal-surface.txt (98%) rename {docs/architecture => .cairn}/context-map.md (100%) rename docs/architecture/01-crate-graph.svg => .cairn/crate-graph.svg (100%) create mode 100644 .cairn/decisions/0001-return-gasket-to-published-release.md create mode 100644 .cairn/decisions/0002-pin-toolchain-and-dependencies.md create mode 100644 .cairn/decisions/0003-real-relay-source-for-peer-discovery.md create mode 100644 .cairn/decisions/0004-fate-of-the-validated-status.md rename {docs/architecture => .cairn}/flows/confirm.md (100%) rename {docs/architecture => .cairn}/flows/fanout.md (100%) rename {docs/architecture => .cairn}/flows/peer-discovery.md (77%) rename {docs/architecture => .cairn}/flows/submit.md (100%) rename docs/architecture/03-life-map.md => .cairn/life-map.md (99%) rename docs/architecture/00-shape.md => .cairn/shape.md (91%) rename docs/architecture/01-topology.md => .cairn/topology.md (94%) delete mode 100644 docs/architecture/ARCHITECTURE.md diff --git a/.cairn/.gitignore b/.cairn/.gitignore new file mode 100644 index 0000000..89f9ac0 --- /dev/null +++ b/.cairn/.gitignore @@ -0,0 +1 @@ +out/ diff --git a/.cairn/README.md b/.cairn/README.md new file mode 100644 index 0000000..540492e --- /dev/null +++ b/.cairn/README.md @@ -0,0 +1,17 @@ +# Boros — Cairn dossier + +This tree is maintained under the [Cairn](https://github.com/txpipe) discipline and its layout is framework-defined: derived files are regenerated, never hand-edited; intent lives in the colocated manifests (`WORKSPACE.md`, `src/*/MODULE.md`); decisions live in [`decisions/`](decisions/). `out/` is machine exhaust and never committed. + +Boros ("tx omnivore") is a Cardano transaction submission service: it accepts raw transactions over gRPC, queues them with priority and chaining semantics in SQLite, fans them out to Ouroboros tx-submission peers, and tracks their fate on-chain (confirmation, retry, rollback) via UtxoRPC chainsync. + +## Reading order + +1. [life-map.md](life-map.md) — where the action is: churn × complexity partition, chartering nominations. **Repo dormant since 2025-05, judged "will resume" (human, 2026-08-23).** +2. [topology.md](topology.md) — module graph, instability table, the one root cycle, confirmed layer labels. Rendered crate graph: [crate-graph.svg](crate-graph.svg). +3. The four flows — the tx lifecycle (`Pending → InFlight → Confirmed | Failed`, defined in `src/storage/mod.rs`) is the spine; each flow drives one leg: + - [flows/submit.md](flows/submit.md) — gRPC → validated → queued (`Pending`). + - [flows/fanout.md](flows/fanout.md) — priority drain → sign → broadcast (`InFlight`/`Failed`). + - [flows/confirm.md](flows/confirm.md) — chainsync → `Confirmed`/retry/rollback + cursor. **Flow-contract nominee.** + - [flows/peer-discovery.md](flows/peer-discovery.md) — relay/peer-sharing pool top-up (relay side mocked — [decision 0003](decisions/0003-real-relay-source-for-peer-discovery.md)). + +Reference material, consulted rather than read in order: [shape.md](shape.md) (census: size, tests, unsafe, dependency weight), [api/](api/) (internal public-item surface — bin-only crate, so this is the operational-comprehension snapshot, not a semver surface), [context-map.md](context-map.md) (generated from manifest relationships), [decisions/](decisions/) (ADRs 0001–0004: gasket fork, dependency pinning, relay mock, `Validated` status). diff --git a/docs/architecture/02-api/boros-internal-surface.txt b/.cairn/api/boros-internal-surface.txt similarity index 98% rename from docs/architecture/02-api/boros-internal-surface.txt rename to .cairn/api/boros-internal-surface.txt index 07027e2..5ecbdcf 100644 --- a/docs/architecture/02-api/boros-internal-surface.txt +++ b/.cairn/api/boros-internal-surface.txt @@ -1,5 +1,5 @@ GENERATED 2026-08-23 by cairn-survey @ fdec4da via `cargo modules structure --bin boros`. -NOTE: internal public-item surface of a bin-only crate — NOT a semver API surface (cargo public-api inapplicable; see 00-shape.md deviation notes). +NOTE: internal public-item surface of a bin-only crate — NOT a semver API surface (cargo public-api inapplicable; see shape.md deviation notes). crate boros diff --git a/docs/architecture/context-map.md b/.cairn/context-map.md similarity index 100% rename from docs/architecture/context-map.md rename to .cairn/context-map.md diff --git a/docs/architecture/01-crate-graph.svg b/.cairn/crate-graph.svg similarity index 100% rename from docs/architecture/01-crate-graph.svg rename to .cairn/crate-graph.svg diff --git a/.cairn/decisions/0001-return-gasket-to-published-release.md b/.cairn/decisions/0001-return-gasket-to-published-release.md new file mode 100644 index 0000000..9873093 --- /dev/null +++ b/.cairn/decisions/0001-return-gasket-to-published-release.md @@ -0,0 +1,15 @@ +# 0001 — Return gasket to a published release + +**Status:** accepted (human decision, 2026-08-23) + +## Context + +`Cargo.toml` pins `gasket` to a personal git fork (`construkts/gasket-rs`), a supply-chain and bus-factor liability surfaced by the census (`.cairn/shape.md`). + +## Decision + +The fork is a stopgap. Boros returns to a published upstream/crates.io `gasket` release as soon as one carries what the fork provides. + +## Consequences + +Until executed, the fork pin is visible debt: `external-policy = "review-new"` plus a future `cargo deny` sources check (SPEC §7.1) will keep flagging it. Executing this decision closes that finding. diff --git a/.cairn/decisions/0002-pin-toolchain-and-dependencies.md b/.cairn/decisions/0002-pin-toolchain-and-dependencies.md new file mode 100644 index 0000000..68c4bd3 --- /dev/null +++ b/.cairn/decisions/0002-pin-toolchain-and-dependencies.md @@ -0,0 +1,15 @@ +# 0002 — Pin toolchain and dependencies + +**Status:** proposed (finding from the first cairn survey, 2026-08-23; awaiting execution sign-off) + +## Context + +A clean checkout of `main` does not compile: `pallas = "1.0.0-alpha.2"` is a floating pre-release requirement that now resolves to `pallas 1.1.1`, whose u5c/pparams API differs from what the code was written against — and `Cargo.lock` is gitignored (the `.gitignore` template's library advice, applied to a binary), so nothing pins the resolution. The first gate run only succeeded after a local lockfile downgrade (`cargo update -p pallas --precise 1.0.0-alpha.2`). `rust-version` (MSRV) is also unpinned. + +## Decision (proposed) + +Commit `Cargo.lock`; pin `pallas = "=1.0.0-alpha.2"` exactly until upgraded deliberately; declare `rust-version` in `Cargo.toml`. + +## Consequences + +Builds become reproducible from a clean checkout; dependency upgrades become explicit diffs routed through the gate instead of silent re-resolution during dormancy. diff --git a/.cairn/decisions/0003-real-relay-source-for-peer-discovery.md b/.cairn/decisions/0003-real-relay-source-for-peer-discovery.md new file mode 100644 index 0000000..ddeeabd --- /dev/null +++ b/.cairn/decisions/0003-real-relay-source-for-peer-discovery.md @@ -0,0 +1,15 @@ +# 0003 — Peer discovery requires a real relay source + +**Status:** accepted (human decision, 2026-08-23) + +## Context + +Production wiring injects `MockRelayDataAdapter` into the peer-discovery stage (`src/pipeline/mod.rs:33`); the on-chain half of peer discovery is fake, leaving only config-listed peers and peer-sharing. + +## Decision + +This is a known gap, not intended design: a real on-chain relay data source must replace the mock before Boros is production-ready. + +## Consequences + +The `peer-discovery` flow doc notes the gap; the `ledger/relay` adapter trait is the implementation seam. Closing this decision removes the mock from `pipeline::run()`. diff --git a/.cairn/decisions/0004-fate-of-the-validated-status.md b/.cairn/decisions/0004-fate-of-the-validated-status.md new file mode 100644 index 0000000..a3647ed --- /dev/null +++ b/.cairn/decisions/0004-fate-of-the-validated-status.md @@ -0,0 +1,15 @@ +# 0004 — Fate of the `Validated` transaction status + +**Status:** open (deliberately undecided, 2026-08-23) + +## Context + +`TransactionStatus::Validated` is defined in `src/storage/mod.rs` and exercised only by a storage test; no production path ever sets it. The live lifecycle is `Pending → InFlight → {Confirmed, Failed}` (plus retry and rollback re-entries). A dead state in the lifecycle enum invites drift. + +## Decision + +None yet. Options: retire the variant, or wire it as a real stage (decoupling validation from broadcast in the ingest flow). + +## Consequences + +Until decided, INV-WS-001 documents the lifecycle without `Validated`, and this ADR is the pointer explaining the extra variant. diff --git a/docs/architecture/flows/confirm.md b/.cairn/flows/confirm.md similarity index 100% rename from docs/architecture/flows/confirm.md rename to .cairn/flows/confirm.md diff --git a/docs/architecture/flows/fanout.md b/.cairn/flows/fanout.md similarity index 100% rename from docs/architecture/flows/fanout.md rename to .cairn/flows/fanout.md diff --git a/docs/architecture/flows/peer-discovery.md b/.cairn/flows/peer-discovery.md similarity index 77% rename from docs/architecture/flows/peer-discovery.md rename to .cairn/flows/peer-discovery.md index fbe165f..b2a2c33 100644 --- a/docs/architecture/flows/peer-discovery.md +++ b/.cairn/flows/peer-discovery.md @@ -9,7 +9,7 @@ participants = ["pipeline", "ledger/relay", "network"] -The `peer_discovery` gasket stage tops up the peer pool toward `desired_peer_count`, choosing 50/50 between on-chain relays (via `RelayDataAdapter` — **currently the mock implementation**, see `src/pipeline/mod.rs:33`) and peers learned from existing peers' peer-sharing. +The `peer_discovery` gasket stage tops up the peer pool toward `desired_peer_count`, choosing 50/50 between on-chain relays (via `RelayDataAdapter` — **currently the mock implementation**, an acknowledged gap: see `src/pipeline/mod.rs:33` and [decision 0003](../decisions/0003-real-relay-source-for-peer-discovery.md)) and peers learned from existing peers' peer-sharing. ```mermaid sequenceDiagram @@ -30,4 +30,3 @@ sequenceDiagram ## Invariants - **INV-DISCOVERY-001** `[unverified]` — The pool converges toward `desired_peer_count` and does not add peers beyond outstanding need (`peer_discovery_queue` bounds in-flight additions). -- **INV-DISCOVERY-002** `[unverified]` — Relay data currently comes from `MockRelayDataAdapter`; production readiness requires a real adapter (this is declared intent, recorded so the gap is visible). diff --git a/docs/architecture/flows/submit.md b/.cairn/flows/submit.md similarity index 100% rename from docs/architecture/flows/submit.md rename to .cairn/flows/submit.md diff --git a/docs/architecture/03-life-map.md b/.cairn/life-map.md similarity index 99% rename from docs/architecture/03-life-map.md rename to .cairn/life-map.md index b468eba..bd06db3 100644 --- a/docs/architecture/03-life-map.md +++ b/.cairn/life-map.md @@ -1,4 +1,4 @@ -# 03 — Life map +# Life map diff --git a/docs/architecture/00-shape.md b/.cairn/shape.md similarity index 91% rename from docs/architecture/00-shape.md rename to .cairn/shape.md index 0983828..bf63b13 100644 --- a/docs/architecture/00-shape.md +++ b/.cairn/shape.md @@ -1,4 +1,4 @@ -# 00 — Shape (census) +# Shape (census) @@ -41,4 +41,4 @@ Boros is a small, young service (v0.1.0, bin-only crate) whose weight is in its 1. **Test coverage is bimodal.** `storage` and `queue` are meaningfully tested; `network`, `server`, `signing`, `validation` are essentially untested. The untested set includes the one `unsafe` block (key derivation) and all I/O boundaries. 2. **The comment ratio (~1%) means the code carries no embedded rationale.** Manifests and flow docs are the only place intent can live — chartering matters more than usual here. -3. **`gasket` is pinned to a personal git fork**, which is a supply-chain and bus-factor liability worth an explicit decision record. +3. **`gasket` is pinned to a personal git fork**, a supply-chain and bus-factor liability — decision recorded in [decisions/0001](decisions/0001-return-gasket-to-published-release.md). diff --git a/docs/architecture/01-topology.md b/.cairn/topology.md similarity index 94% rename from docs/architecture/01-topology.md rename to .cairn/topology.md index 2cfa43c..7324516 100644 --- a/docs/architecture/01-topology.md +++ b/.cairn/topology.md @@ -1,8 +1,8 @@ -# 01 — Topology +# Topology -> **Deviation note:** SPEC §2 expects crate-level topology. Boros is a single bin crate + one generated `spec` crate, so the crate graph (`01-crate-graph.svg`) is trivial. This file carries the *module-level* topology, which is where the structure actually lives. +> **Deviation note:** SPEC §2 expects crate-level topology. Boros is a single bin crate + one generated `spec` crate, so the crate graph (`crate-graph.svg`) is trivial. This file carries the *module-level* topology, which is where the structure actually lives. ## Generated: module dependency edges diff --git a/WORKSPACE.md b/WORKSPACE.md index b989f90..0e8cfa7 100644 --- a/WORKSPACE.md +++ b/WORKSPACE.md @@ -19,18 +19,11 @@ mandatory = ["hotspot-overlap", "unsafe-touched", "tx-status-semantics-touched"] Boros is a Cardano transaction submission service ("tx omnivore"): accept raw transactions over gRPC, queue them with priority and chaining semantics, fan them out to Ouroboros tx-submission peers, and settle their status against the -chain (confirm, retry, rollback). See `docs/architecture/ARCHITECTURE.md`. +chain (confirm, retry, rollback). See `.cairn/README.md`. ## Invariants -- **INV-WS-001** `[llm-judged]` — All `Transaction.status` mutations flow through the `storage` write API; no module manipulates persisted status by other means. The legal lifecycle is `Pending → InFlight → {Confirmed, Failed}`, plus `InFlight → Pending` (retry) and `Confirmed → InFlight` (rollback). *(`Validated` exists in the enum but no production path uses it — see REQ-WS-003.)* - -## Requirements - -- **REQ-WS-001** `[unverified]` — TODO(human): pin a `rust-version` (MSRV) in `Cargo.toml`; currently unpinned. -- **REQ-WS-002** `[unverified]` — Return `gasket` to a published upstream/crates.io release; the `construkts/gasket-rs` git fork is a stopgap. *(Decided by human, 2026-08-23.)* -- **REQ-WS-003** `[unverified]` — TODO(human): retire or wire the `Validated` transaction status; a dead state in the lifecycle enum invites drift. -- **REQ-WS-004** `[unverified]` — Commit `Cargo.lock` (this is a binary; the current `.gitignore` excludes it) and pin `pallas` exactly (`=1.0.0-alpha.2`) until upgraded deliberately. **Found 2026-08-23: a clean checkout of `main` does not compile** — the floating `1.0.0-alpha.2` requirement resolves to `pallas 1.1.1`, whose u5c/pparams API differs. The gate only ran after a local lockfile downgrade. +- **INV-WS-001** `[llm-judged]` — All `Transaction.status` mutations flow through the `storage` write API; no module manipulates persisted status by other means. The legal lifecycle is `Pending → InFlight → {Confirmed, Failed}`, plus `InFlight → Pending` (retry) and `Confirmed → InFlight` (rollback). *(`Validated` exists in the enum but no production path uses it — see `.cairn/decisions/0004`.)* ## Non-goals @@ -43,3 +36,10 @@ chain (confirm, retry, rollback). See `docs/architecture/ARCHITECTURE.md`. dependent txs via lock tokens), *fanout* (broadcast to peers), *InFlight* (broadcast, awaiting on-chain observation), *cursor* (last processed chain point), *tip* (latest known chain point), *u5c* (UtxoRPC chain access). + +## Notes + +Open workspace-level concerns live as ADRs, not constraints: gasket git-fork +pin (`.cairn/decisions/0001`, accepted), toolchain/dependency pinning — clean +checkouts of `main` currently do not compile (`.cairn/decisions/0002`, +proposed), dead `Validated` status (`.cairn/decisions/0004`, open). diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md deleted file mode 100644 index f9b6994..0000000 --- a/docs/architecture/ARCHITECTURE.md +++ /dev/null @@ -1,22 +0,0 @@ -# Boros — Architecture - -Boros ("tx omnivore") is a Cardano transaction submission service: it accepts raw transactions over gRPC, queues them with priority and chaining semantics in SQLite, fans them out to Ouroboros tx-submission peers, and tracks their fate on-chain (confirmation, retry, rollback) via UtxoRPC chainsync. - -This dossier is maintained under the [Cairn](https://github.com/txpipe) discipline: derived files are regenerated, never hand-edited; intent lives in module manifests (`MODULE.md`) and `WORKSPACE.md`. - -## The dossier - -- [00 — Shape (census)](00-shape.md) — size, test distribution, unsafe count, dependency weight. -- [01 — Topology](01-topology.md) — module graph, instability table, the one root cycle, confirmed layer labels. Rendered crate graph: [01-crate-graph.svg](01-crate-graph.svg). -- [02 — API surface](02-api/boros-internal-surface.txt) — internal public-item snapshot (bin-only crate; see deviation note inside). -- [03 — Life map](03-life-map.md) — lifetime churn × complexity partition; **repo dormant since 2025-05, judged "will resume" (human, 2026-08-23)**; chartering nominations. -- Flows (curated sequence diagrams, type-labeled): - - [submit](flows/submit.md) — gRPC → validated → queued. - - [fanout](flows/fanout.md) — priority drain → sign → broadcast → InFlight. - - [confirm](flows/confirm.md) — chainsync → Confirmed/retry/rollback + cursor. **Flow-contract nominee.** - - [peer-discovery](flows/peer-discovery.md) — relay/peer-sharing pool top-up (relay side currently mocked). -- [context-map.md](context-map.md) — generated from manifests (pending charter). - -## Reading order for newcomers - -Life map → topology → the four flows. The tx lifecycle (`Pending → Validated → InFlight → Confirmed | Failed`, defined in `src/storage/mod.rs`) is the spine: submit produces `Pending`, fanout moves to `InFlight`/`Failed`, monitor settles `Confirmed`/retries. (`Validated` is defined but currently unused by any flow — see survey findings.) diff --git a/src/pipeline/MODULE.md b/src/pipeline/MODULE.md index 6c35d18..634de74 100644 --- a/src/pipeline/MODULE.md +++ b/src/pipeline/MODULE.md @@ -18,7 +18,7 @@ The orchestrator: three gasket stages wired in `run()` — `ingest` (drain Pending by priority → sign → validate → broadcast → InFlight), `monitor` (chainsync → Confirmed / retry / rollback + cursor), `peer_discovery` (peer pool top-up). Temporal behavior across these stages is specified by the flow -contracts: `fanout`, `confirm`, `peer-discovery` (see `docs/architecture/flows/`). +contracts: `fanout`, `confirm`, `peer-discovery` (see `.cairn/flows/`). ## Invariants @@ -26,11 +26,6 @@ contracts: `fanout`, `confirm`, `peer-discovery` (see `docs/architecture/flows/` - **INV-PIPE-002** `[llm-judged]` — Stage failures are contained by gasket retry policy; a failing transaction is marked `Failed` rather than wedging the stage (refines INV-WS-001). - **INV-PIPE-003** `[unverified]` — Temporal invariants of this module's stages are owned by the flow docs (INV-FANOUT-*, INV-CONFIRM-*, INV-DISCOVERY-*); this entry records that they are not yet verified anywhere. -## Requirements - -- **REQ-PIPE-001** `[unverified]` — `run()` wires `MockRelayDataAdapter` into production peer discovery (`src/pipeline/mod.rs:33`); this is a known gap that must be replaced with a real on-chain relay source. *(Decided by human, 2026-08-23.)* -- **REQ-PIPE-002** `[unverified]` — TODO(human): `pipeline` imports `Config` from the crate root (the `main ⇄ pipeline` cycle in `01-topology.md`); consider moving shared config types out of `main.rs`. - ## Relationships - `customer-supplier` with **storage**, **queue**, **signing** (pipeline is the customer). @@ -40,3 +35,11 @@ contracts: `fanout`, `confirm`, `peer-discovery` (see `docs/architecture/flows/` ## Non-goals - No policy decisions: what to pick (queue), what is valid (validation), how to persist (storage) are supplied, not owned. + +## Notes + +`run()` currently wires `MockRelayDataAdapter` into peer discovery +(`src/pipeline/mod.rs:33`) — an acknowledged gap, see `.cairn/decisions/0003`. +The `main ⇄ pipeline` cycle through the root `Config` type is documented in +`.cairn/topology.md`; moving shared config out of `main.rs` is an open idea, +not a decision. diff --git a/src/storage/MODULE.md b/src/storage/MODULE.md index 5f21da6..f2d1dae 100644 --- a/src/storage/MODULE.md +++ b/src/storage/MODULE.md @@ -25,10 +25,7 @@ stores (`SqliteTransaction`, `SqliteCursor`) over a migrated SQLite database. - **INV-STORE-003** `[bound → just test-storage]` — `find_to_rollback(slot)` returns only transactions whose recorded slot is affected by a rollback to `slot` (`it_should_find_to_rollback*`). - **INV-STORE-004** `[bound → just test-storage]` — Cursor `set` upserts: a second write updates rather than duplicates (`it_should_set_when_it_updates`). - **INV-STORE-005** `[llm-judged]` — No SQL and no `sqlx` usage outside this module; consumers speak `Transaction`/`Cursor`, never rows. - -## Requirements - -- **REQ-STORE-001** `[unverified]` — Migrations run to completion before any query is served (currently by call order in `main.rs`; nothing enforces it). +- **INV-STORE-006** `[unverified]` — Migrations run to completion before any query is served (currently upheld only by call order in `main.rs`; nothing enforces it). ## Relationships diff --git a/tools/cairn_check_deps.py b/tools/cairn_check_deps.py index 5c82bb3..8d63e7a 100644 --- a/tools/cairn_check_deps.py +++ b/tools/cairn_check_deps.py @@ -5,7 +5,7 @@ dependencies` and asserts they are a subset of the manifest allowlist. Known blind spot: cargo-modules does not report const-only uses (see -docs/architecture/01-topology.md); such edges must be policed by review. +.cairn/topology.md); such edges must be policed by review. """ import re From 7b092d6042bbd3516398195c79ba2ed3538aa7d9 Mon Sep 17 00:00:00 2001 From: Santiago Date: Tue, 6 Oct 2026 12:10:16 -0300 Subject: [PATCH 3/5] fix(cairn): read the deps allowlist without tomllib `python3` resolves to 3.9 on macOS, where `tomllib` does not exist, so `just check-deps-all` failed before checking anything. Parse the one key the verifier needs and document the blind spot; unknown forms fail closed. Co-Authored-By: Claude Opus 5.5 --- tools/cairn_check_deps.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tools/cairn_check_deps.py b/tools/cairn_check_deps.py index 8d63e7a..1cbffda 100644 --- a/tools/cairn_check_deps.py +++ b/tools/cairn_check_deps.py @@ -4,14 +4,16 @@ Derives the module's actual internal dependencies from `cargo modules dependencies` and asserts they are a subset of the manifest allowlist. -Known blind spot: cargo-modules does not report const-only uses (see -.cairn/topology.md); such edges must be policed by review. +Known blind spots: cargo-modules does not report const-only uses (see +.cairn/topology.md); such edges must be policed by review. The frontmatter +reader understands only a single string array assigned to `internal-allowed` +inside `[deps]` (no tomllib before Python 3.11); any other form reads as an +empty allowlist, which fails closed. """ import re import subprocess import sys -import tomllib from pathlib import Path ROOT = Path(__file__).resolve().parent.parent @@ -29,8 +31,11 @@ def manifest_allowlist(module_id: str) -> list[str]: m = re.match(r"\+\+\+\n(.*?)\n\+\+\+", text, re.DOTALL) if not m: sys.exit(f"FAIL: no +++ frontmatter in {manifest}") - front = tomllib.loads(m.group(1)) - return [d.split("#")[0] for d in front.get("deps", {}).get("internal-allowed", [])] + deps = re.search(r"^\[deps\]\n(.*?)(?=^\[|\Z)", m.group(1), re.DOTALL | re.MULTILINE) + allowed = deps and re.search(r"^internal-allowed\s*=\s*\[(.*?)\]", deps.group(1), re.DOTALL | re.MULTILINE) + if not allowed: + return [] + return [d.split("#")[0] for d in re.findall(r'"([^"]*)"', allowed.group(1))] def actual_deps(module_id: str) -> set[str]: From 2ce851b2c050103d7ea07a6ace07a3647e4ec6fb Mon Sep 17 00:00:00 2001 From: Santiago Date: Tue, 6 Oct 2026 12:10:16 -0300 Subject: [PATCH 4/5] docs(cairn): recharter manifests to structured purpose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring WORKSPACE.md and the pipeline, storage and queue manifests up to the current cairn/v0 draft: declare short codes, add the six SPEC §3.5 Purpose fields with purpose-status = "inferred", give the workspace a Composition table, and move operation-level invariants to Guarantees with Surface/When/ Then fields and a migration note (§3.4). Every ID, tier and claim is kept. The context map records that no expectations are declared yet. Co-Authored-By: Claude Opus 5.5 --- .cairn/context-map.md | 6 ++- WORKSPACE.md | 74 +++++++++++++++++++++++++++++++++++ src/pipeline/MODULE.md | 76 ++++++++++++++++++++++++++++++++++-- src/queue/MODULE.md | 79 ++++++++++++++++++++++++++++++++++++-- src/storage/MODULE.md | 87 ++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 311 insertions(+), 11 deletions(-) diff --git a/.cairn/context-map.md b/.cairn/context-map.md index 471dcfc..eb51191 100644 --- a/.cairn/context-map.md +++ b/.cairn/context-map.md @@ -1,6 +1,6 @@ # Context map - + ```mermaid flowchart TD @@ -25,3 +25,7 @@ flowchart TD | `storage` | `open-host` (supplier-side declaration) | — | `src/storage/MODULE.md` | Modules without manifests yet (`ledger`, `network`, `signing`, `server`, `validation`) appear only as referenced suppliers; their consumer-side declarations are pending their own charter. + +## Expectations + +No manifest declares `Expectations` yet, so there are no consumer → provider links and the reverse index by provider is empty. Collaborator obligations are recorded only as gaps in each manifest's `Boundary allocation`. diff --git a/WORKSPACE.md b/WORKSPACE.md index 0e8cfa7..f413700 100644 --- a/WORKSPACE.md +++ b/WORKSPACE.md @@ -1,6 +1,8 @@ +++ schema = "cairn/v0" id = "boros" +short = "WS" +purpose-status = "inferred" [workspace] verify-all = "just gate" @@ -16,14 +18,86 @@ mandatory = ["hotspot-overlap", "unsafe-touched", "tx-status-semantics-touched"] ## Purpose +### System contribution + Boros is a Cardano transaction submission service ("tx omnivore"): accept raw transactions over gRPC, queue them with priority and chaining semantics, fan them out to Ouroboros tx-submission peers, and settle their status against the chain (confirm, retry, rollback). See `.cairn/README.md`. +- OUT-WS-001: A submitter that hands Boros a fully-formed transaction has it delivered to the Cardano network without running its own node connections. +- OUT-WS-002: Submitters control the relative order of their submissions: weighted queues share dispatch capacity, and chained queues serialize dependent transactions. +- OUT-WS-003: An accepted transaction is pursued until it settles on-chain: rebroadcast while not included, confirmed when included, reopened when a rollback removes it. + +Whether submitters must be able to observe settlement is unknown: the UtxoRPC +`WaitForTx` and `WatchMempool` methods are unimplemented, and no status query +exists. + +### Beneficiaries + +| Beneficiary | Connection to outcomes | Provenance | +|---|---|---| +| Clients of the `boros.v1` submit service | Submit transactions (OUT-WS-001) and use queues and chain locks (OUT-WS-002) | Observed service surface; actual clients (dApp backends, wallets) are inferred | +| Clients of the UtxoRPC submit service | Submit transactions to the default queue (OUT-WS-001) | Observed service surface | +| Operators | Configure queues, peers, the UtxoRPC endpoint and Vault signing; run the service | Observed config schema; operational needs are unknown | +| Cardano network peers | Receive transactions over tx-submission | Observed protocol use | + +### Responsibilities + +| ID | Outcome | Responsibility | Realization | Coverage | +|---|---|---|---|---| +| RESP-WS-001 | OUT-WS-001 | Accept, decode, optionally validate and durably queue submitted transactions | `server` (no manifest; flow claims INV-SUBMIT-001..003 unverified); storage: RESP-STORE-001 | partial | +| RESP-WS-002 | OUT-WS-002 | Order dispatch across weighted queues and serialize chained queues | queue: RESP-QUEUE-001, RESP-QUEUE-002, RESP-QUEUE-003; pipeline: RESP-PIPE-001 | partial | +| RESP-WS-003 | OUT-WS-001 | Sign for server-signing queues, re-validate and broadcast to peers | pipeline: RESP-PIPE-001, RESP-PIPE-003; `signing`, `validation`, `network` have no manifests | partial | +| RESP-WS-004 | OUT-WS-003 | Settle transactions against the chain and resume after restart | pipeline: RESP-PIPE-002; storage: RESP-STORE-003, RESP-STORE-004 | uncovered | +| RESP-WS-005 | OUT-WS-001, OUT-WS-003 | Own the transaction lifecycle | INV-WS-001; decision 0004 leaves `Validated` open | partial | + +### Owned information + +| Information | Authority and representation ownership | +|---|---| +| Transaction lifecycle (status meaning and legal transitions) | Authoritative here (INV-WS-001); `storage` owns the representation | +| Service configuration (`boros.toml`, `/etc/boros/config.toml`, `BOROS_*` environment) | Root `Config` in `main.rs`; each module owns its section's schema | + +### Boundary allocation + +| Responsibility | This module | Collaborator obligation and owner | Contract gap | +|---|---|---|---| +| RESP-WS-001, RESP-WS-003 | Validate against current ledger state | A UtxoRPC endpoint (external, operator-chosen) supplies protocol parameters, UTxOs, tip and chain events | No workspace adapter manifest for `ledger` | +| RESP-WS-003 | Sign for server-signing queues | Hashicorp Vault (external) holds keys | No adapter manifest for `signing` | +| RESP-WS-003 | Broadcast | Cardano peers (external) accept tx-submission; a relay source supplies candidates: `unallocated` (mocked, decision 0003) | No manifest for `network` | +| RESP-WS-001 | Accept fully-formed transactions | Submitters build, balance and (except on server-signing queues) sign them | Stated as a non-goal | + +### Operating envelope + +| Responsibility | Mode / condition | Scope | Scenarios / gaps | +|---|---|---|---| +| RESP-WS-001 | Submission over either gRPC service | in-scope | [submit flow](.cairn/flows/submit.md) | +| RESP-WS-002, RESP-WS-003 | Steady-state dispatch | in-scope | [fanout flow](.cairn/flows/fanout.md) | +| RESP-WS-004 | Chain advance, retry, rollback | in-scope | [confirm flow](.cairn/flows/confirm.md) | +| RESP-WS-004 | Restart or crash recovery | in-scope | Cursor resume is observed; INV-CONFIRM-004 is unverified | +| RESP-WS-003 | Peer discovery from real relays | unknown | Mocked (decision 0003) | +| RESP-WS-001, RESP-WS-004 | UtxoRPC endpoint, Vault or peers unavailable | unknown | No accepted degraded behavior | +| RESP-WS-002 | Several Boros instances sharing one database | unknown | Chain locks are per process | +| RESP-WS-005 | `Validated` status | unknown | Decision 0004 | +| RESP-WS-001 | Building or balancing transactions | excluded | [Non-goals](#non-goals); owned by submitters | + +## Composition + +| Child | Manifest | Allocation | Exposed surface | Integration | +|---|---|---|---|---| +| pipeline | [pipeline](src/pipeline/MODULE.md) | RESP-WS-002 → RESP-PIPE-001; RESP-WS-003 → RESP-PIPE-001, RESP-PIPE-003; RESP-WS-004 → RESP-PIPE-002 | `pipeline::run` (internal) | Runs beside `server` in `main`; reads and writes through `storage`, schedules through `queue` | +| storage | [storage](src/storage/MODULE.md) | RESP-WS-001 → RESP-STORE-001; RESP-WS-004 → RESP-STORE-003, RESP-STORE-004 | `storage::{Transaction, Cursor}`, `storage::sqlite::{SqliteStorage, SqliteTransaction, SqliteCursor}` (internal) | One SQLite pool shared by all writers; no cross-module transaction | +| queue | [queue](src/queue/MODULE.md) | RESP-WS-002 → RESP-QUEUE-001, RESP-QUEUE-002, RESP-QUEUE-003 | `queue::{Config, priority::Priority, chaining::TxChaining}` (internal) | Shared by `server` and `pipeline`; lock state lives in one process | + +`server`, `network`, `ledger`, `signing`, `validation` and `main` have no +manifests yet; their work stays under this manifest and appears above as gaps. +No joint transaction spans the children. + ## Invariants - **INV-WS-001** `[llm-judged]` — All `Transaction.status` mutations flow through the `storage` write API; no module manipulates persisted status by other means. The legal lifecycle is `Pending → InFlight → {Confirmed, Failed}`, plus `InFlight → Pending` (retry) and `Confirmed → InFlight` (rollback). *(`Validated` exists in the enum but no production path uses it — see `.cairn/decisions/0004`.)* + - Scope and observation: every write of a persisted transaction status, across the workspace. ## Non-goals diff --git a/src/pipeline/MODULE.md b/src/pipeline/MODULE.md index 634de74..252da74 100644 --- a/src/pipeline/MODULE.md +++ b/src/pipeline/MODULE.md @@ -1,7 +1,9 @@ +++ schema = "cairn/v0" id = "pipeline" +short = "PIPE" role = "core-domain" +purpose-status = "inferred" parnas-secret = "the runtime topology — which gasket stages exist, how they are scheduled, and how backpressure (channel cap) is applied" [deps] @@ -14,17 +16,82 @@ verify = "just check-deps pipeline" ## Purpose +### System contribution + The orchestrator: three gasket stages wired in `run()` — `ingest` (drain Pending by priority → sign → validate → broadcast → InFlight), `monitor` (chainsync → Confirmed / retry / rollback + cursor), `peer_discovery` (peer -pool top-up). Temporal behavior across these stages is specified by the flow -contracts: `fanout`, `confirm`, `peer-discovery` (see `.cairn/flows/`). +pool top-up). Contributes to the workspace outcomes +[OUT-WS-001 and OUT-WS-003](../../WORKSPACE.md#system-contribution): queued +transactions reach the network and are pursued until settled. Temporal +behavior across these stages is specified by the flow docs `fanout`, +`confirm` and `peer-discovery` (see `.cairn/flows/`). -## Invariants +### Beneficiaries + +| Beneficiary | Connection to outcomes | Provenance | +|---|---|---| +| `main` | Starts the pipeline beside the gRPC server | Observed caller of `run` | +| Submitters | Their queued transactions are broadcast and settled (OUT-WS-001, OUT-WS-003) | Inferred indirect beneficiaries | +| Cardano peers | Receive transactions over Ouroboros tx-submission | Observed through `network`; peer-side needs are unknown | +| Operators | Configure peers, retry window and signing | Observed config; operational needs (metrics, health) are unknown | + +### Responsibilities + +| ID | Outcome | Responsibility | Realization | Coverage | +|---|---|---|---|---| +| RESP-PIPE-001 | OUT-WS-001 | Drain pending transactions, sign those on server-signing queues, re-validate, broadcast, and mark each `InFlight` or `Failed` | INV-PIPE-001, INV-PIPE-002; flow claims INV-FANOUT-001..003 are unverified | partial | +| RESP-PIPE-002 | OUT-WS-003 | Follow the chain and settle in-flight transactions: confirm, retry, revert on rollback, persist the cursor | No module guarantee; flow claims INV-CONFIRM-001..004 are unverified | uncovered | +| RESP-PIPE-003 | OUT-WS-001 | Keep the tx-submission peer pool topped up | Flow claim INV-DISCOVERY-001 is unverified; the relay source is mocked (decision 0003) | uncovered | +| RESP-PIPE-004 | OUT-WS-001, OUT-WS-003 | Own the runtime topology: stage wiring, scheduling, failure policy and backpressure | INV-PIPE-002, INV-PIPE-003; INV-FANOUT-003 is unverified | partial | + +### Owned information + +| Information | Authority and representation ownership | +|---|---| +| Stage topology and the broadcast channel cap (50) | Authoritative, in code | +| Monitor retry window (`retry_slot_diff`) | Owns the config schema; operators supply the value | +| Transaction status and cursor values | Decides the transitions it writes; `storage` owns their representation and the workspace owns the lifecycle (INV-WS-001) | + +### Boundary allocation + +| Responsibility | This module | Collaborator obligation and owner | Contract gap | +|---|---|---|---| +| RESP-PIPE-001 | Choose, sign, validate and dispatch | `queue` picks the batch; `signing` signs (Hashicorp Vault, external); `validation` judges against ledger state from `ledger` (UtxoRPC endpoint, external) | `signing`, `validation`, `ledger` have no manifests; no expectations declared | +| RESP-PIPE-001 | Put the transaction on the broadcast channel | `network` delivers it to connected peers | Delivery is not acknowledged back; `InFlight` means sent to the channel | +| RESP-PIPE-002 | Map chain events to status updates and the cursor | `ledger::u5c` supplies roll-forward and rollback events in order from the cursor; `storage` persists | Ordering and gap-freedom of the event stream are not declared | +| RESP-PIPE-003 | Schedule pool top-up | `network::PeerManager` connects; a relay source supplies candidates: `unallocated` (mock in use) | Decision 0003 | + +### Operating envelope + +| Responsibility | Mode / condition | Scope | Scenarios / gaps | +|---|---|---|---| +| RESP-PIPE-001 | Normal drain and broadcast | in-scope | [fanout flow](../../.cairn/flows/fanout.md) | +| RESP-PIPE-001 | Transaction fails validation or evaluation | in-scope | Marked `Failed` (INV-PIPE-002) | +| RESP-PIPE-001 | Transaction that does not decode, or whose queue is missing from config | unknown | The stage errors or retries the whole batch; whether that wedges the stage is unassessed | +| RESP-PIPE-001 | Server-signing queue with no signer configured | in-scope | Retry, never unsigned broadcast (INV-FANOUT-002, unverified) | +| RESP-PIPE-002 | Roll-forward, retry timeout, rollback | in-scope | [confirm flow](../../.cairn/flows/confirm.md); flow-contract nominee | +| RESP-PIPE-002 | Crash between status updates and cursor write | in-scope | INV-CONFIRM-004, unverified | +| RESP-PIPE-003 | Peer pool below target | in-scope | [peer-discovery flow](../../.cairn/flows/peer-discovery.md); relay side mocked | +| RESP-PIPE-004 | UtxoRPC endpoint or all peers unavailable | unknown | Gasket retry policy is the default; no accepted behavior | + +## Guarantees - **INV-PIPE-001** `[bound → just test-ingest]` — Phase-1 validation and phase-2 evaluation accept known-valid Conway transactions and reject invalid ones (fixture-backed, `ingest_tests`). + - Surface: `validation::{validate_tx, evaluate_tx}` as called by the `ingest` stage. + - When: Ledger state comes from the test UtxoRPC mock; fixtures are Conway transactions. + - Then: The valid fixture passes both; the unwitnessed fixture fails validation; the bad-script fixture fails evaluation. + - Migration: moved from Invariants under SPEC §3.4; ID and tier unchanged. The functions live in `validation`, which has no manifest. - **INV-PIPE-002** `[llm-judged]` — Stage failures are contained by gasket retry policy; a failing transaction is marked `Failed` rather than wedging the stage (refines INV-WS-001). + - Surface: the `ingest` stage. + - When: A transaction fails validation or evaluation. + - Then: It is written `Failed` and the stage continues with the next transaction. + - Migration: moved from Invariants under SPEC §3.4; ID and tier unchanged. + +## Invariants + - **INV-PIPE-003** `[unverified]` — Temporal invariants of this module's stages are owned by the flow docs (INV-FANOUT-*, INV-CONFIRM-*, INV-DISCOVERY-*); this entry records that they are not yet verified anywhere. + - Scope and observation: every execution of the three stages. ## Relationships @@ -43,3 +110,6 @@ contracts: `fanout`, `confirm`, `peer-discovery` (see `.cairn/flows/`). The `main ⇄ pipeline` cycle through the root `Config` type is documented in `.cairn/topology.md`; moving shared config out of `main.rs` is an open idea, not a decision. + +INV-PIPE-003 is a pointer rather than an enduring constraint; it is kept +because IDs are immutable, and retiring it is an owner's call. diff --git a/src/queue/MODULE.md b/src/queue/MODULE.md index a65cbd6..0fd005f 100644 --- a/src/queue/MODULE.md +++ b/src/queue/MODULE.md @@ -1,7 +1,9 @@ +++ schema = "cairn/v0" id = "queue" +short = "QUEUE" role = "core-domain" +purpose-status = "inferred" parnas-secret = "the scheduling policy — how weighted priority picks the next transactions, and how chained queues serialize dependent submissions via lock tokens" [deps] @@ -14,17 +16,88 @@ verify = "just check-deps queue" ## Purpose -The scheduling heart of Boros. `priority` allocates each fanout batch across -named queues by weight; `chaining` gives dependent-transaction queues +### System contribution + +The scheduling heart of Boros. Contributes to the workspace outcome +[OUT-WS-002](../../WORKSPACE.md#system-contribution): submitters control the +relative order of their submissions. `priority` allocates each dispatch batch +across named queues by weight; `chaining` gives dependent-transaction queues exclusive-lock semantics (token-gated submission, streamed lock state). -## Invariants +### Beneficiaries + +| Beneficiary | Connection to outcomes | Provenance | +|---|---|---| +| `pipeline::ingest` | Asks `Priority::next` for each dispatch batch (OUT-WS-002) | Observed caller | +| `server` (boros submit service) | Locks, validates tokens on and unlocks chained queues (OUT-WS-002) | Observed caller of `TxChaining` | +| Submitters building transaction chains | Need to read the latest queued transaction and append the next one without a competing writer | Inferred from the `LockState` RPC and the token check; no client was inspected | +| Operators | Declare queues, weights, chaining and server signing in config | Observed config schema; operator needs beyond that are unknown | + +### Responsibilities + +| ID | Outcome | Responsibility | Realization | Coverage | +|---|---|---|---|---| +| RESP-QUEUE-001 | OUT-WS-002 | Allocate each dispatch batch across queues in proportion to their weights | INV-QUEUE-001, INV-QUEUE-002. Fairness across successive batches is not declared. | partial | +| RESP-QUEUE-002 | OUT-WS-002 | Serialize submissions to chained queues through exclusive, expiring lock tokens | INV-QUEUE-003, INV-QUEUE-004. Lock state is held in memory only. | partial | +| RESP-QUEUE-003 | OUT-WS-002 | Define queue identity and the default queue | INV-QUEUE-005 | partial | + +### Owned information + +| Information | Authority and representation ownership | +|---|---| +| Queue configuration (`name`, `weight`, `chained`, `server_signing`) and the `default` queue | Owns the schema and identity rules; operators supply values; `main` inserts the default queue when absent | +| Chained-queue lock tokens | Authoritative, in memory, per process; lost on restart | +| Per-queue batch quotas | Derived per call from storage counts; not persisted | + +### Boundary allocation + +| Responsibility | This module | Collaborator obligation and owner | Contract gap | +|---|---|---|---| +| RESP-QUEUE-001 | Compute per-queue limits | `storage` returns counts and selections honoring them (its INV-STORE-002) | No expectation declared | +| RESP-QUEUE-001 | Hand back the batch | `pipeline::ingest` dispatches it; it retries a transaction whose queue is missing from config | Interaction with INV-QUEUE-002 is unassessed | +| RESP-QUEUE-002 | Issue, check and expire tokens | `server` checks the token before `create` and unlocks after it | `server` has no manifest; no expectation declared | +| RESP-QUEUE-002 | Stream the latest queued transaction with the token | `storage::latest` returns the most recent submission in the queue | It may return a `Failed` transaction (TODO in source) | + +### Operating envelope + +| Responsibility | Mode / condition | Scope | Scenarios / gaps | +|---|---|---|---| +| RESP-QUEUE-001 | Normal drain across configured queues | in-scope | [fanout flow](../../.cairn/flows/fanout.md) | +| RESP-QUEUE-001 | Queue removed from config while it holds transactions | in-scope | INV-QUEUE-002; downstream signing lookup unassessed | +| RESP-QUEUE-001 | Starvation of low-weight queues under sustained load | unknown | No claim or scenario | +| RESP-QUEUE-002 | Lock, submit, unlock within the timeout | in-scope | [submit flow](../../.cairn/flows/submit.md) | +| RESP-QUEUE-002 | Lock holder exceeds the 30 s timeout | in-scope | Lock expires; a late submission is rejected (INV-QUEUE-004) | +| RESP-QUEUE-002 | Process restart while a lock is held | unknown | Tokens are lost; intended behavior undecided | +| RESP-QUEUE-002 | Several Boros instances serving one queue | unknown | Locks are per process | +| RESP-QUEUE-003 | Config change at runtime | unknown | Config is read once at startup | + +## Guarantees - **INV-QUEUE-001** `[bound → just test-queue]` — Batch quota is allocated proportionally to queue weights and fully distributed (remainder included) (`it_should_calculate_quota`). + - Surface: `Priority::next` (quota computation). + - When: At least one queue holds transactions in the requested status. + - Then: Each queue's share of the cap is proportional to its weight; capacity a queue cannot use passes to the next queue. + - Migration: moved from Invariants under SPEC §3.4; ID and tier unchanged. - **INV-QUEUE-002** `[bound → just test-queue]` — Transactions in queues removed from config are still drained, not stranded (`it_should_return_next_transactions_when_a_queue_is_removed_from_config`). + - Surface: `Priority::next`. + - When: Stored transactions belong to a queue absent from config. + - Then: That queue is scheduled with the default weight. + - Migration: moved from Invariants under SPEC §3.4; ID and tier unchanged. - **INV-QUEUE-003** `[bound → just test-queue]` — A chained queue admits one lock holder at a time; competing lockers wait or time out (`it_should_lock_queue`, `it_should_wait_timeout_to_lock_queue`, `it_should_lock_many_queue`). + - Surface: `TxChaining::lock`. + - When: The queue is configured `chained`. + - Then: A second locker receives its token only after the first unlocks or its lock times out. + - Migration: moved from Invariants under SPEC §3.4; ID and tier unchanged. - **INV-QUEUE-004** `[bound → just test-queue]` — Only the current lock token is valid for submission to a chained queue; unlock invalidates it (`it_should_return_token_*`, `it_should_unlock_queue`). + - Surface: `TxChaining::{is_valid_token, unlock}`. + - When: The queue is configured `chained`. + - Then: `is_valid_token` is true only for the token most recently issued and not yet released or expired. + - Migration: moved from Invariants under SPEC §3.4; ID and tier unchanged. + +## Invariants + - **INV-QUEUE-005** `[llm-judged]` — Queue identity is its name alone (`Config` hashes/compares by name), so config reload with changed weights re-targets the same queue rather than creating a phantom. + - Scope and observation: every set of queue `Config` values, at construction. ## Relationships diff --git a/src/storage/MODULE.md b/src/storage/MODULE.md index f2d1dae..097446e 100644 --- a/src/storage/MODULE.md +++ b/src/storage/MODULE.md @@ -1,7 +1,9 @@ +++ schema = "cairn/v0" id = "storage" +short = "STORE" role = "generic-subdomain" +purpose-status = "inferred" parnas-secret = "how transaction/cursor state is persisted — SQLite, the schema, and all SQL live only here" [deps] @@ -14,18 +16,95 @@ verify = "just check-deps storage" ## Purpose -Persistence for the two durable facts Boros owns: the transaction queue -(`Transaction`, `TransactionStatus`) and the chainsync `Cursor`. Exposes typed -stores (`SqliteTransaction`, `SqliteCursor`) over a migrated SQLite database. +### System contribution -## Invariants +Within Boros, keep the submission state durable so the service can keep +pursuing every accepted transaction and resume following the chain where it +left off. Contributes to the workspace outcomes +[OUT-WS-001, OUT-WS-002 and OUT-WS-003](../../WORKSPACE.md#system-contribution): +accepted transactions are recorded, queues can be drained in order, and +settlement state survives restarts. Storage supplies representation and +queries; it does not decide lifecycle transitions. + +### Beneficiaries + +| Beneficiary | Connection to outcomes | Provenance | +|---|---|---| +| `server` (boros and UtxoRPC submit services) | Records submitted batches for OUT-WS-001 | Observed caller of `create` | +| `queue` (`priority`, `chaining`) | Reads per-queue counts, ordered selections and the latest submission for OUT-WS-002 | Observed caller of `state`, `next`, `latest` | +| `pipeline` (`ingest`, `monitor`) | Writes status transitions and the cursor for OUT-WS-001 and OUT-WS-003 | Observed caller of `update`, `update_batch`, `find`, `find_to_rollback`, `SqliteCursor` | +| Operators | Need a database that migrates in place and survives restarts | Inferred; durability and retention needs were not stated anywhere | + +### Responsibilities + +| ID | Outcome | Responsibility | Realization | Coverage | +|---|---|---|---|---| +| RESP-STORE-001 | OUT-WS-001 | Record accepted transactions with their queue, status and declared dependencies | INV-STORE-001. Batch atomicity of `create` is observed (one database transaction) but not declared; duplicate submissions are unassessed. | partial | +| RESP-STORE-002 | OUT-WS-002, OUT-WS-003 | Answer lifecycle queries: per-queue counts and ordered selection, the in-flight set, rollback candidates, the latest submission per queue | INV-STORE-002, INV-STORE-003. `find`, `state` and `latest` have tests but no declared guarantee. | partial | +| RESP-STORE-003 | OUT-WS-003 | Persist status transitions written by callers | `update` and `update_batch` exist; no guarantee declared. Transition legality is the workspace's INV-WS-001, not checked here. | uncovered | +| RESP-STORE-004 | OUT-WS-003 | Persist the chain-follow cursor | INV-STORE-004. Atomicity with the status updates of the same chain event is unallocated. | partial | +| RESP-STORE-005 | OUT-WS-001, OUT-WS-003 | Own the schema, its migrations and the confinement of SQL | INV-STORE-005, INV-STORE-006 | partial | + +### Owned information + +| Information | Authority and representation ownership | +|---|---| +| Transaction records (id = transaction hash hex, raw CBOR, status, queue, slot, timestamps) | Owns the `tx` table representation; the meaning of `status` belongs to the workspace lifecycle (INV-WS-001) | +| Dependency edges | Owns `tx_dependence`; written by `create`, never read back (`Transaction.dependencies` loads as `None`) | +| Chain-follow cursor | Owns the single-row `cursor` table; `pipeline` decides when it advances | +| Schema | Authoritative: `src/storage/migrations/` | + +### Boundary allocation + +| Responsibility | This module | Collaborator obligation and owner | Contract gap | +|---|---|---|---| +| RESP-STORE-001 | Insert the batch and its dependency edges | `server` decodes, validates and assigns queues before calling `create` | `server` has no manifest; no expectation declared | +| RESP-STORE-001, RESP-STORE-002 | Record dependency edges | Dispatch in dependency order: `unallocated` (`next` does not consult `tx_dependence`, and no other module does) | Whether dependencies are meant to order dispatch is unknown | +| RESP-STORE-002 | Execute selections with caller-supplied limits | `queue` computes per-queue limits | No expectation declared | +| RESP-STORE-003, RESP-STORE-004 | Apply writes as given | `pipeline` chooses legal transitions and writes the cursor after its status updates | Flow claim INV-CONFIRM-004 is unverified | +| RESP-STORE-005 | Run migrations when asked | The binary entry point (`main`) migrates before serving | No manifest owns `main`; see INV-STORE-006 | + +### Operating envelope + +| Responsibility | Mode / condition | Scope | Scenarios / gaps | +|---|---|---|---| +| RESP-STORE-001 | Submission batch with valid dependencies | in-scope | [submit flow](../../.cairn/flows/submit.md) | +| RESP-STORE-001 | Resubmission of a transaction already stored | unknown | The primary key rejects the whole batch; intended behavior undecided | +| RESP-STORE-002 | Draining pending transactions under load | in-scope | [fanout flow](../../.cairn/flows/fanout.md) | +| RESP-STORE-002, RESP-STORE-003 | Concurrent writers (`server`, `ingest`, `monitor`) on one pool | unknown | No isolation guarantee declared | +| RESP-STORE-003, RESP-STORE-004 | Crash between status updates and cursor write | unknown | [confirm flow](../../.cairn/flows/confirm.md), INV-CONFIRM-004 unverified | +| RESP-STORE-005 | Migrating an existing database | in-scope | No scenario or test beyond the in-memory database | +| RESP-STORE-001, RESP-STORE-003 | Retention of settled transactions | unknown | No deletion path exists; growth is unbounded | + +## Guarantees - **INV-STORE-001** `[bound → just test-storage]` — Creating transactions with declared dependencies fails unless every dependency is already present (`it_should_fail_create_with_invalid_dependencies`). + - Surface: `SqliteTransaction::create`. + - When: The database is migrated; a transaction in the batch declares `dependencies`. + - Then: `create` returns `Err` when a declared dependency id is not stored. + - Migration: moved from Invariants under SPEC §3.4; ID and tier unchanged. - **INV-STORE-002** `[bound → just test-storage]` — `next(status, quotas)` honors per-queue quotas and status filtering; transactions from queues no longer in config still drain (`it_should_find_next*`). + - Surface: `SqliteTransaction::next`. + - When: The caller supplies a status and a per-queue limit map. + - Then: The result holds only transactions in that status, at most the limit per listed queue, oldest first within a queue. + - Migration: moved from Invariants under SPEC §3.4; ID and tier unchanged. - **INV-STORE-003** `[bound → just test-storage]` — `find_to_rollback(slot)` returns only transactions whose recorded slot is affected by a rollback to `slot` (`it_should_find_to_rollback*`). + - Surface: `SqliteTransaction::find_to_rollback`. + - When: The caller supplies the rollback slot. + - Then: The result holds exactly the `Confirmed` transactions whose slot is after it. + - Migration: moved from Invariants under SPEC §3.4; ID and tier unchanged. - **INV-STORE-004** `[bound → just test-storage]` — Cursor `set` upserts: a second write updates rather than duplicates (`it_should_set_when_it_updates`). + - Surface: `SqliteCursor::{set, current}`. + - When: The database is migrated. + - Then: After `set` returns `Ok`, `current` returns that cursor; there is never more than one. + - Migration: moved from Invariants under SPEC §3.4; ID and tier unchanged. + +## Invariants + - **INV-STORE-005** `[llm-judged]` — No SQL and no `sqlx` usage outside this module; consumers speak `Transaction`/`Cursor`, never rows. + - Scope and observation: the workspace source, at every revision. - **INV-STORE-006** `[unverified]` — Migrations run to completion before any query is served (currently upheld only by call order in `main.rs`; nothing enforces it). + - Scope and observation: process lifetime, from startup to the first query. ## Relationships From 5018a6fab8fac39cd61cdd8dc338842a8b9817ae Mon Sep 17 00:00:00 2001 From: Santiago Date: Tue, 6 Oct 2026 12:23:42 -0300 Subject: [PATCH 5/5] docs(cairn): record where the queue quota contradicts INV-QUEUE-001 The quota rounds each share independently and walks queues in hash order, so a batch can exceed or fall short of the cap and the last queue's leftover is dropped. The Then clause now states only what the code and test establish; the contradiction sits beside the unchanged claim and as gaps in RESP-QUEUE-001 and its operating envelope. Also drops the justfile's decorative separator comments. Co-Authored-By: Claude Opus 5.5 --- justfile | 4 ---- src/queue/MODULE.md | 7 +++++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/justfile b/justfile index 7f2c6a6..2b1921d 100644 --- a/justfile +++ b/justfile @@ -8,8 +8,6 @@ set shell := ["bash", "-o", "pipefail", "-cu"] gate: check-deps-all test-storage test-queue test-ingest @echo "gate: all bound constraints passed" -# --- dependency constraints ------------------------------------------------- - # Check a module's internal deps against its MODULE.md allowlist check-deps id: python3 tools/cairn_check_deps.py {{id}} @@ -19,8 +17,6 @@ check-deps-all: python3 tools/cairn_check_deps.py queue python3 tools/cairn_check_deps.py pipeline -# --- test-backed invariants ------------------------------------------------- - test-storage: cargo test storage:: -- --nocapture 2>&1 | tail -20 diff --git a/src/queue/MODULE.md b/src/queue/MODULE.md index 0fd005f..f91269c 100644 --- a/src/queue/MODULE.md +++ b/src/queue/MODULE.md @@ -37,7 +37,7 @@ exclusive-lock semantics (token-gated submission, streamed lock state). | ID | Outcome | Responsibility | Realization | Coverage | |---|---|---|---|---| -| RESP-QUEUE-001 | OUT-WS-002 | Allocate each dispatch batch across queues in proportion to their weights | INV-QUEUE-001, INV-QUEUE-002. Fairness across successive batches is not declared. | partial | +| RESP-QUEUE-001 | OUT-WS-002 | Allocate each dispatch batch across queues in proportion to their weights | INV-QUEUE-001, INV-QUEUE-002. INV-QUEUE-001's claim that the cap is fully distributed is contradicted by the code: rounding can over- or undershoot the cap, and leftover from the last queue visited is dropped. Fairness across successive batches is not declared. | partial | | RESP-QUEUE-002 | OUT-WS-002 | Serialize submissions to chained queues through exclusive, expiring lock tokens | INV-QUEUE-003, INV-QUEUE-004. Lock state is held in memory only. | partial | | RESP-QUEUE-003 | OUT-WS-002 | Define queue identity and the default queue | INV-QUEUE-005 | partial | @@ -64,6 +64,8 @@ exclusive-lock semantics (token-gated submission, streamed lock state). |---|---|---|---| | RESP-QUEUE-001 | Normal drain across configured queues | in-scope | [fanout flow](../../.cairn/flows/fanout.md) | | RESP-QUEUE-001 | Queue removed from config while it holds transactions | in-scope | INV-QUEUE-002; downstream signing lookup unassessed | +| RESP-QUEUE-001 | Weights do not split the cap into whole shares | in-scope | Gap: shares are rounded independently, so the batch can exceed or fall short of the cap; no claim or test covers it | +| RESP-QUEUE-001 | A queue holds fewer transactions than its share | in-scope | Gap: unused capacity passes to queues in hash order, and the last queue's leftover is dropped, so the batch size depends on that order; no test covers it | | RESP-QUEUE-001 | Starvation of low-weight queues under sustained load | unknown | No claim or scenario | | RESP-QUEUE-002 | Lock, submit, unlock within the timeout | in-scope | [submit flow](../../.cairn/flows/submit.md) | | RESP-QUEUE-002 | Lock holder exceeds the 30 s timeout | in-scope | Lock expires; a late submission is rejected (INV-QUEUE-004) | @@ -76,7 +78,8 @@ exclusive-lock semantics (token-gated submission, streamed lock state). - **INV-QUEUE-001** `[bound → just test-queue]` — Batch quota is allocated proportionally to queue weights and fully distributed (remainder included) (`it_should_calculate_quota`). - Surface: `Priority::next` (quota computation). - When: At least one queue holds transactions in the requested status. - - Then: Each queue's share of the cap is proportional to its weight; capacity a queue cannot use passes to the next queue. + - Then: Each queue's share is its weight's fraction of the cap, rounded to the nearest whole transaction; `it_should_calculate_quota` checks one exact split (weights 1/2/2, cap 10 → 2/4/4). Capacity a queue cannot use is added to the share of the queue visited after it. + - Contradiction: the claim's "fully distributed (remainder included)" does not hold. Rounding each share can exceed or fall short of the cap (weights 1/1, cap 5 → 3 + 3 = 6; weights 1/1/1, cap 10 → 9). Queues are visited in hash order and the last one's leftover is dropped (cap 50, two weight-1 queues holding 100 and 1 → a batch of 26 or 50). The claim and tier stay as declared: demoting needs a human-approved change (§4), and fixing the code is outside this charter. - Migration: moved from Invariants under SPEC §3.4; ID and tier unchanged. - **INV-QUEUE-002** `[bound → just test-queue]` — Transactions in queues removed from config are still drained, not stranded (`it_should_return_next_transactions_when_a_queue_is_removed_from_config`). - Surface: `Priority::next`.