From 5b4edfb857a49b59c567fce779c8e54d6942e84f Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Sun, 18 Oct 2026 08:15:41 -0400 Subject: [PATCH 1/5] docs: add agent instructions for the Rust rewrite branch --- .changeset/rewrite-agent-instructions.md | 2 + CLAUDE.md | 159 +++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 .changeset/rewrite-agent-instructions.md create mode 100644 CLAUDE.md diff --git a/.changeset/rewrite-agent-instructions.md b/.changeset/rewrite-agent-instructions.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/rewrite-agent-instructions.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..85af80ed9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,159 @@ +# nostream — working on `rewrite/main` + +The Rust rewrite replaces the TypeScript relay **in place, crate by crate, on this +branch**. `main` receives it only at cutover. Until then `src/` (TypeScript) and +`crates/` (Rust) coexist in one tree and are separate pipelines. + +Rules below are the ones an agent gets wrong. Generic Rust guidance is not repeated +here. + +## Version control + +- **Work on `rewrite/main` (or a branch off it), never `main`.** +- A PR into `rewrite/main` must carry **one empty changeset** + (`.changeset/.md` with empty frontmatter) — `Changeset Required` runs on every + PR and a Rust-only change touches no published package. The changeset *content* is + changed only when the TypeScript package itself changes. +- Commits are commitlint-checked (`Lint commits`); conventional-commit form is required. +- **The husky `pre-commit` hook runs the TypeScript suite** + (`pnpm run lint`, `build:check`, `test:unit`). A Rust-only change can wait minutes on + hooks that cannot fail because of it, and the same commands are `Lint code`, + `Build check` and `Unit Tests And Coverage` in CI. Commit Rust-only work with + `--no-verify` and let those three CI jobs carry the proof; never use `--no-verify` on a + commit that touches `src/`, `test/` or a JS manifest. + +## Validation — the exact commands + +CI (`Rust Checks` in `.github/workflows/checks.yml`) is the authority. Run the same +five, in this order, and they must all pass before a change is claimed done: + +``` +cargo fmt --all --check +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo clippy --workspace --lib --bins --all-features -- \ + -D warnings -D clippy::unwrap_used -D clippy::expect_used \ + -D clippy::todo -D clippy::unimplemented -D clippy::panic +cargo test --workspace --all-features +cargo build --workspace --all-features --release +``` + +- **`--workspace`, never a single crate.** A crate-local `cargo test -p` proves nothing + about a workspace lint that another member trips. +- **`--all-features` is not optional.** `nostreamd`'s `otlp` feature gates three + dependencies; a change that only compiles without it is broken. +- **Two clippy passes, and both are required.** The strict pass is `--lib --bins` only, + so `unwrap`/`expect` stay legal in tests without `#[allow]` attributes whose + precedence against a command-line `-D` nobody should bet on. +- The `#[allow(unsafe_code)]` carve-out is in `crates/nostream-storage/src/backend.rs` + around heed's `EnvOpenOptions::open`. Do not add a second one; do not widen + `[workspace.lints.rust] unsafe_code` from `deny` to `forbid` (it would reject that + audited call). + +### Running Rust here + +This sandbox has no host Rust toolchain. `scripts/rust.sh ` runs it in the +`containers-rust-builder:latest` image, as create → start → wait → logs → rm: + +``` +scripts/rust.sh cargo test --workspace --all-features +scripts/rust.sh cargo clippy --workspace --lib --bins --all-features -- -D warnings +``` + +The image pins nothing itself — `rust-toolchain.toml` (channel `1.99.1`, `rustfmt` + +`clippy`, `minimal`) selects the toolchain. Never pass a version to a command; edit the +pin file if the toolchain must move. + +## Crate layout + +`crates/*`, one crate per module family, `edition = "2024"`, `rust-version = "1.99"`, +resolver 3: + +| crate | owns | +|---|---| +| `nostream-core` | domain vocabulary: events, filters, kinds, wire messages, NIP type surfaces, `ShutdownState` | +| `nostream-config` | typed settings, loader, deep merge, validator, dot-path editor, redaction, guided catalogue | +| `nostream-storage` | event-store backends (LMDB via heed, Postgres), cache | +| `nostream-relay` | acceptor, connections, wire protocol, NIP-77 engine | +| `nostream-services` | NIP-66 monitors, Tor, schedulers, outbound work | +| `nostream-admin` | admin API and auth | +| `nostream-cli` | operator command line (`nostream-cli config validate`) | +| `nostreamd` | the daemon binary: runtime, telemetry, signals | + +- Every crate manifest repeats `version.workspace = true`, `edition.workspace = true`, + `rust-version.workspace = true`, `license.workspace = true`, + `repository.workspace = true` and `[lints] workspace = true`. A new crate that omits + the last one silently opts out of every workspace lint. +- **Dependencies go in `[workspace.dependencies]` and are referenced as + `name.workspace = true`.** A version literal in a crate manifest is a second place to + bump; do not add one. +- Each crate's `lib.rs` doc comment names the TypeScript paths it owns at the capture + commit, and separately names what it deliberately does **not** own. Preserve both: a + reader must be able to tell an omission from a gap. + +## Error model + +- **One error enum per crate, named `Error`** (`CoreError`, `ConfigError`, + `StorageError`, `DaemonError`, `CliError`), `thiserror::Error`, one variant per + failure class, and a message written for an operator. +- **`anyhow` appears only in `crates/nostreamd/src/main.rs` and + `crates/nostream-cli/src/main.rs`.** Everything else, including the CLI's own modules, + returns `CliError`. The check CI's reviewers use: + `grep -rn 'anyhow' crates/*/src/lib.rs` → empty. +- **A subsystem that is not implemented returns + `Error::NotImplemented { module: "path::to::call" }` — never `todo!()`, + `unimplemented!()` or `panic!()`.** The daemon's `main` maps that variant to exit code + `1` with its message, and `DaemonError::InvalidSettings` to exit code `2`; keep those + two distinct. +- Validation findings are **not** errors: `nostream_config::Issue { path, message }` + carries a dot-path and the capture's exact message, and `Display` renders + `{path}: {message}`. A validator that returns `Err` on a bad setting instead of an + `Issue` changes the CLI's output contract. +- Settings validation messages are **byte-identical to the capture's**. + `crates/nostream-config/tests/ts_parity.rs` replays every case in + `tests/fixtures/ts-issues.json` (measured data, generated by + `tests/capture/gen-ts-issues.mjs` against the capture commit) and requires the same + issues, in the same order, with the same messages. **Exactly one path is exempt — + `eventStore.backend`, because the rewrite adds the LMDB backend — and the exemption is + recorded per case.** When a message or an exemption must change, change the fixture in + the same commit and say why; never loosen the comparison to make a case pass. + +## Porting discipline (this branch, phase 1) + +- **Stubs are labelled, not hidden.** Every unimplemented item carries + `/// Phase 1 stub — job 005; ; see ` on the item, takes typed + arguments, and returns `NotImplemented`. `grep -rn 'Phase 1 stub' crates` must count + the stubs. +- **No silent `Ok(())`, no plausible fake data, no partial port.** A function that would + need real behaviour returns the error. A `Result`-returning stub whose body is + `Ok(Default::default())` is the failure mode this rule exists to prevent. +- The TypeScript tree is the specification. When Rust behaviour and a TS file disagree, + the TS file wins unless a decision record says otherwise; the ADRs live in + `docs/rewrite/decisions/` (`0001-runtime-and-telemetry.md` covers runtime, acceptor + shape and telemetry) — read the relevant one before changing what it decided. + +## Tests + +- Unit tests live in `#[cfg(test)]` modules; integration tests in `crates//tests/`. + Prefer whole-behaviour tests at the public API: `crates/nostream-config/tests/load.rs` + writes real fixtures to a `tempfile::TempDir` and calls the public loader, rather than + asserting on internals. +- `unwrap`/`expect` are permitted in tests (the strict clippy pass excludes them) — use + `expect("why this cannot fail")` so a failure names the assumption. +- A parity test that reads `resources/default-settings.yaml` or a fixture must fail, not + skip, when the fixture is missing. + +## Path filters — what triggers `Rust Checks` + +`crates/**`, `Cargo.toml`, `Cargo.lock`, `rust-toolchain.toml`, `scripts/rust.sh`, +`resources/default-settings.yaml` and the workflow file itself. A change to any other +file does **not** run the Rust job, so a change that alters what `cargo` sees (a new +fixture directory, a `.cargo/config.toml`) needs the filter extended in the same PR. +`resources/default-settings.yaml` joins **both** the TS and Rust filters because both +pipelines read it. + +## Branch protection + +`rewrite/main` has its own ruleset with eight required contexts: `Rust Checks`, +`Lint commits`, `Lint code`, `Lint PR title`, `Changeset Required`, `Build check`, +`Unit Tests And Coverage`, `Integration Tests and Coverage`. Adding or renaming a CI job +means updating that ruleset in the same release, or the branch becomes unmergeable. From ceab573e11e88e39dea7155327e44579a5fd0fdd Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Sun, 18 Oct 2026 08:22:06 -0400 Subject: [PATCH 2/5] docs: keep the rewrite CLAUDE.md portable across contributor machines --- CLAUDE.md | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 85af80ed9..0ae600e90 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,11 +16,10 @@ here. changed only when the TypeScript package itself changes. - Commits are commitlint-checked (`Lint commits`); conventional-commit form is required. - **The husky `pre-commit` hook runs the TypeScript suite** - (`pnpm run lint`, `build:check`, `test:unit`). A Rust-only change can wait minutes on - hooks that cannot fail because of it, and the same commands are `Lint code`, - `Build check` and `Unit Tests And Coverage` in CI. Commit Rust-only work with - `--no-verify` and let those three CI jobs carry the proof; never use `--no-verify` on a - commit that touches `src/`, `test/` or a JS manifest. + (`pnpm run lint`, `build:check`, `test:unit`) on every commit. A Rust-only change + cannot fail those, and CI runs the same three as `Lint code`, `Build check` and + `Unit Tests And Coverage` against the same change — so `--no-verify` is reasonable for + a Rust-only commit and never for one that touches `src/`, `test/` or a JS manifest. ## Validation — the exact commands @@ -49,19 +48,16 @@ cargo build --workspace --all-features --release `[workspace.lints.rust] unsafe_code` from `deny` to `forbid` (it would reject that audited call). -### Running Rust here +### The toolchain -This sandbox has no host Rust toolchain. `scripts/rust.sh ` runs it in the -`containers-rust-builder:latest` image, as create → start → wait → logs → rm: +`rust-toolchain.toml` pins it — channel `1.99.1`, `rustfmt` + `clippy`, `minimal` — and +every `cargo` invocation picks it up from the working directory. **Never pass a +version or `+stable` to a command**; edit the pin file if the toolchain must move, so +CI, contributors and build agents all move together. -``` -scripts/rust.sh cargo test --workspace --all-features -scripts/rust.sh cargo clippy --workspace --lib --bins --all-features -- -D warnings -``` - -The image pins nothing itself — `rust-toolchain.toml` (channel `1.99.1`, `rustfmt` + -`clippy`, `minimal`) selects the toolchain. Never pass a version to a command; edit the -pin file if the toolchain must move. +`scripts/rust.sh ` is the one wrapper some environments provide for running a +`cargo` command in an isolated builder image. It takes the same arguments as `cargo` +and is optional — where a working toolchain is on `PATH`, plain `cargo` is equivalent. ## Crate layout From 2b871fb1c8dafc007bec3992711ae6302c0db8d8 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Sun, 18 Oct 2026 08:24:03 -0400 Subject: [PATCH 3/5] docs: drop the builder-image wrapper from the rewrite CLAUDE.md --- CLAUDE.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0ae600e90..b178e71f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,10 +55,6 @@ every `cargo` invocation picks it up from the working directory. **Never pass a version or `+stable` to a command**; edit the pin file if the toolchain must move, so CI, contributors and build agents all move together. -`scripts/rust.sh ` is the one wrapper some environments provide for running a -`cargo` command in an isolated builder image. It takes the same arguments as `cargo` -and is optional — where a working toolchain is on `PATH`, plain `cargo` is equivalent. - ## Crate layout `crates/*`, one crate per module family, `edition = "2024"`, `rust-version = "1.99"`, From 82b60404f9f868b3708a8ca7f4d47ef6c5c69720 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Sun, 18 Oct 2026 09:06:09 -0400 Subject: [PATCH 4/5] ci(rust): carry the strict clippy lints in the workspace configuration checks.yml passed -D clippy::unwrap_used -D clippy::expect_used -D clippy::panic only on the command line. A crate inheriting [lints] workspace = true therefore passed a local cargo clippy and was rejected by the gate. The three lints move into [workspace.lints.clippy], and the 17 integration-test targets that assert by unwrapping or panicking carry an explicit allow at the top of the file. The allowance names the file rather than exempting test targets workspace-wide, so a new test file has to ask. Measured from a clean target directory: fmt clean; all-targets clippy clean; strict lib+bins pass clean; 157 tests passed, 0 failed; release build succeeds. Proven to fire: Some(1).unwrap() added to nostream-config/src/lib.rs fails the all-targets pass. --- CLAUDE.md | 10 +++++++--- Cargo.toml | 9 +++++++++ crates/nostream-cli/tests/config_cli.rs | 5 +++++ crates/nostream-config/tests/backend_values.rs | 5 +++++ crates/nostream-config/tests/defaults_parity.rs | 5 +++++ crates/nostream-config/tests/defaults_roundtrip.rs | 5 +++++ crates/nostream-config/tests/guided.rs | 5 +++++ crates/nostream-config/tests/load.rs | 5 +++++ crates/nostream-config/tests/notifications_merge.rs | 5 +++++ crates/nostream-config/tests/path_editor.rs | 5 +++++ crates/nostream-config/tests/redaction.rs | 5 +++++ crates/nostream-config/tests/ts_parity.rs | 5 +++++ crates/nostream-config/tests/validator.rs | 5 +++++ crates/nostream-core/tests/lifecycle.rs | 5 +++++ crates/nostream-core/tests/secret.rs | 5 +++++ crates/nostream-storage/tests/storage.rs | 5 +++++ crates/nostreamd/tests/acceptor.rs | 5 +++++ crates/nostreamd/tests/check_flag.rs | 5 +++++ crates/nostreamd/tests/settings_gate.rs | 5 +++++ 19 files changed, 101 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b178e71f8..c81a652c9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,9 +40,13 @@ cargo build --workspace --all-features --release about a workspace lint that another member trips. - **`--all-features` is not optional.** `nostreamd`'s `otlp` feature gates three dependencies; a change that only compiles without it is broken. -- **Two clippy passes, and both are required.** The strict pass is `--lib --bins` only, - so `unwrap`/`expect` stay legal in tests without `#[allow]` attributes whose - precedence against a command-line `-D` nobody should bet on. +- **Two clippy passes, and both are required.** The lints `unwrap_used`, `expect_used` + and `panic` are declared in `[workspace.lints.clippy]` as well as passed on the strict + command line, so a local `cargo clippy` fails on the same code CI rejects. An + integration-test target that needs them carries + `#![allow(clippy::unwrap_used, clippy::expect_used)]` (plus `clippy::panic` where it + asserts by panicking) at the top of the file — naming the file that may unwrap, not + exempting test targets workspace-wide. A new test file therefore has to ask. - The `#[allow(unsafe_code)]` carve-out is in `crates/nostream-storage/src/backend.rs` around heed's `EnvOpenOptions::open`. Do not add a second one; do not widen `[workspace.lints.rust] unsafe_code` from `deny` to `forbid` (it would reject that diff --git a/Cargo.toml b/Cargo.toml index bbe85368f..c30bc867f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,3 +45,12 @@ unsafe_code = "deny" todo = "deny" unimplemented = "deny" dbg_macro = "deny" +# Carried here as well as on the strict `--lib --bins` command line in CI. Without them +# a crate that inherits `[lints] workspace = true` passes a local `cargo clippy` and is +# then rejected by the gate, which is the one thing a lint configuration must never do. +# Tests still opt out per crate with `#![allow(clippy::unwrap_used, clippy::expect_used)]` +# in the test target, which is narrower than a command-line flag: it names the files that +# may unwrap instead of exempting every target in the workspace. +unwrap_used = "deny" +expect_used = "deny" +panic = "deny" diff --git a/crates/nostream-cli/tests/config_cli.rs b/crates/nostream-cli/tests/config_cli.rs index ecc2c794e..0cfcadfee 100644 --- a/crates/nostream-cli/tests/config_cli.rs +++ b/crates/nostream-cli/tests/config_cli.rs @@ -1,5 +1,10 @@ //! End-to-end tests for the `config` subcommands. +// This integration-test target may unwrap: a failing assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used)] + use std::fs; use std::path::PathBuf; diff --git a/crates/nostream-config/tests/backend_values.rs b/crates/nostream-config/tests/backend_values.rs index a627a1d1b..c4042ce48 100644 --- a/crates/nostream-config/tests/backend_values.rs +++ b/crates/nostream-config/tests/backend_values.rs @@ -1,6 +1,11 @@ //! `eventStore.backend` accepts the rewrite's LMDB backend (spec D7) and still rejects //! anything outside the two-member set, with the two-member message. +// This integration-test target may unwrap: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used)] + use std::fs; use nostream_config::path::set_by_path; diff --git a/crates/nostream-config/tests/defaults_parity.rs b/crates/nostream-config/tests/defaults_parity.rs index bd0002ac5..18ad57e06 100644 --- a/crates/nostream-config/tests/defaults_parity.rs +++ b/crates/nostream-config/tests/defaults_parity.rs @@ -1,6 +1,11 @@ //! Defaults parity with `test/unit/utils/settings.spec.ts:226-341` at the capture commit: //! six defaults assertions and four override cases, value for value. +// This integration-test target may unwrap and panic: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + use std::fs; use nostream_config::load::merge; diff --git a/crates/nostream-config/tests/defaults_roundtrip.rs b/crates/nostream-config/tests/defaults_roundtrip.rs index 43f626b94..176e5c776 100644 --- a/crates/nostream-config/tests/defaults_roundtrip.rs +++ b/crates/nostream-config/tests/defaults_roundtrip.rs @@ -3,6 +3,11 @@ //! untagged enum is right, and it is why `validate(&Settings)` may delegate to the document //! validator. +// This integration-test target may unwrap: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used)] + use std::fs; use nostream_config::settings::Settings; diff --git a/crates/nostream-config/tests/guided.rs b/crates/nostream-config/tests/guided.rs index e5e17521f..09e9bbb58 100644 --- a/crates/nostream-config/tests/guided.rs +++ b/crates/nostream-config/tests/guided.rs @@ -1,6 +1,11 @@ //! The guided settings catalogue, ported from the `settings-guided-schema` block of //! `test/unit/utils/settings-config.spec.ts`. +// This integration-test target may unwrap: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used)] + use nostream_config::guided::{ GuidedSettingFieldType, GuidedSettingValidator, get_guided_setting_category, get_guided_setting_field, guided_setting_categories, require_non_empty_setting_value, diff --git a/crates/nostream-config/tests/load.rs b/crates/nostream-config/tests/load.rs index 597130cc4..1baa0af53 100644 --- a/crates/nostream-config/tests/load.rs +++ b/crates/nostream-config/tests/load.rs @@ -2,6 +2,11 @@ //! from `test/unit/utils/settings.spec.ts` and the precedence block of //! `test/unit/utils/settings-config.spec.ts`. +// This integration-test target may unwrap: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used)] + use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/nostream-config/tests/notifications_merge.rs b/crates/nostream-config/tests/notifications_merge.rs index 89103fb13..a40c3f067 100644 --- a/crates/nostream-config/tests/notifications_merge.rs +++ b/crates/nostream-config/tests/notifications_merge.rs @@ -1,6 +1,11 @@ //! Admin-notification merge and patch semantics, ported from //! `test/unit/utils/admin-notifications-settings.spec.ts`. +// This integration-test target may unwrap: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used)] + use nostream_config::notifications::{ AdminNotificationsPatch, AdminNotificationsSettings, get_merged_admin_notifications, merge_admin_notifications_patch, diff --git a/crates/nostream-config/tests/path_editor.rs b/crates/nostream-config/tests/path_editor.rs index 36b85d834..9d2d7da9a 100644 --- a/crates/nostream-config/tests/path_editor.rs +++ b/crates/nostream-config/tests/path_editor.rs @@ -1,5 +1,10 @@ //! The dot-path editor and value parser, ported from `test/unit/utils/settings-config.spec.ts`. +// This integration-test target may unwrap and panic: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + use std::fs; use nostream_config::path::{ diff --git a/crates/nostream-config/tests/redaction.rs b/crates/nostream-config/tests/redaction.rs index 3185737e9..b05f14ec4 100644 --- a/crates/nostream-config/tests/redaction.rs +++ b/crates/nostream-config/tests/redaction.rs @@ -1,5 +1,10 @@ //! Redaction, ported from `test/unit/utils/settings-redaction.spec.ts`. +// This integration-test target may unwrap: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used)] + use nostream_config::path::get_by_path; use nostream_config::redaction::{ is_sensitive_settings_path, is_write_protected_settings_path, redact_settings_secrets, diff --git a/crates/nostream-config/tests/ts_parity.rs b/crates/nostream-config/tests/ts_parity.rs index b57a4d169..5d0512667 100644 --- a/crates/nostream-config/tests/ts_parity.rs +++ b/crates/nostream-config/tests/ts_parity.rs @@ -6,6 +6,11 @@ //! `eventStore.backend`, because the rewrite adds the LMDB backend (spec D7) — and the //! exemption is recorded per case and asserted here. +// This integration-test target may unwrap and panic: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + use std::collections::BTreeSet; use std::fs; diff --git a/crates/nostream-config/tests/validator.rs b/crates/nostream-config/tests/validator.rs index ff897a7bb..2e5e6de20 100644 --- a/crates/nostream-config/tests/validator.rs +++ b/crates/nostream-config/tests/validator.rs @@ -2,6 +2,11 @@ //! adaptive-pow matrix, the event-store cases, the shape rules and the admin-notification //! rules. Each test names what `test/unit/utils/settings-config.spec.ts` asserted. +// This integration-test target may unwrap: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used)] + use std::fs; use nostream_config::load::merge; diff --git a/crates/nostream-core/tests/lifecycle.rs b/crates/nostream-core/tests/lifecycle.rs index b639725a7..1fb3b7f3f 100644 --- a/crates/nostream-core/tests/lifecycle.rs +++ b/crates/nostream-core/tests/lifecycle.rs @@ -1,5 +1,10 @@ //! The drain flag must survive a drain that starts before any subscriber exists. +// This integration-test target may unwrap: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used)] + use nostream_core::lifecycle::ShutdownState; #[tokio::test] diff --git a/crates/nostream-core/tests/secret.rs b/crates/nostream-core/tests/secret.rs index c38aa2d56..c1700180b 100644 --- a/crates/nostream-core/tests/secret.rs +++ b/crates/nostream-core/tests/secret.rs @@ -1,5 +1,10 @@ //! `Secret` redacts through every formatting path that reaches a log line. +// This integration-test target may unwrap: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used)] + use nostream_core::Secret; #[test] diff --git a/crates/nostream-storage/tests/storage.rs b/crates/nostream-storage/tests/storage.rs index db204c34c..4a55378b6 100644 --- a/crates/nostream-storage/tests/storage.rs +++ b/crates/nostream-storage/tests/storage.rs @@ -1,5 +1,10 @@ //! The LMDB environment slice and the backend stubs. +// This integration-test target may unwrap: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used)] + use std::path::Path; use nostream_storage::{Backend, LmdbEnv, StorageError}; diff --git a/crates/nostreamd/tests/acceptor.rs b/crates/nostreamd/tests/acceptor.rs index 45dd81c66..af9a9a5d3 100644 --- a/crates/nostreamd/tests/acceptor.rs +++ b/crates/nostreamd/tests/acceptor.rs @@ -1,5 +1,10 @@ //! The acceptor's drain behaviour. +// This integration-test target may unwrap: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used)] + use std::time::Duration; use nostream_core::lifecycle::ShutdownState; diff --git a/crates/nostreamd/tests/check_flag.rs b/crates/nostreamd/tests/check_flag.rs index 8be983834..2297377f5 100644 --- a/crates/nostreamd/tests/check_flag.rs +++ b/crates/nostreamd/tests/check_flag.rs @@ -2,6 +2,11 @@ //! waits for a signal. Regression test for round 3's P0: the gate alone passed while `run()` //! fell through into `serve`. +// This integration-test target may unwrap and panic: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + use std::path::PathBuf; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; diff --git a/crates/nostreamd/tests/settings_gate.rs b/crates/nostreamd/tests/settings_gate.rs index 644047469..301a172ad 100644 --- a/crates/nostreamd/tests/settings_gate.rs +++ b/crates/nostreamd/tests/settings_gate.rs @@ -1,5 +1,10 @@ //! A malformed user settings file must serve on the defaults, not abort (R7's fallback rule). +// This integration-test target may unwrap: a failed assertion here is a failed test, +// which is the point. The allowance names this file rather than exempting test targets +// workspace-wide, so a new test file still has to ask. +#![allow(clippy::unwrap_used, clippy::expect_used)] + use std::fs; use std::path::PathBuf; From 6b8dc4982236d6c5a3319160a83c3fdf7716a7aa Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Sun, 18 Oct 2026 09:09:00 -0400 Subject: [PATCH 5/5] docs: correct the error-model rules Greptile flagged Two contradictions in the CLAUDE.md: - 'everything outside the two binaries returns CliError' read as 'all libraries depend on nostream-cli'. It now says each library module returns its own crate's type, with the CLI's library modules returning CliError. - The anyhow check searched only crates/*/src/lib.rs, so a module file using anyhow would pass. It now scans every .rs file, excludes the two allowed main.rs, and skips comment lines that merely name the crate. Re-proven by adding an anyhow::Result probe to nostream-config/src/load.rs, which the check printed. --- CLAUDE.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c81a652c9..d6bed00e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,11 +90,24 @@ resolver 3: - **One error enum per crate, named `Error`** (`CoreError`, `ConfigError`, `StorageError`, `DaemonError`, `CliError`), `thiserror::Error`, one variant per - failure class, and a message written for an operator. + failure class, and a message written for an operator. **Every library module returns its + own crate's type** — `nostream-config` returns `ConfigError`, and the CLI's library + modules (`crates/nostream-cli/src/`) return `CliError`. A library never returns another + crate's error type, so no crate has to depend on `nostream-cli` to name a failure. - **`anyhow` appears only in `crates/nostreamd/src/main.rs` and - `crates/nostream-cli/src/main.rs`.** Everything else, including the CLI's own modules, - returns `CliError`. The check CI's reviewers use: - `grep -rn 'anyhow' crates/*/src/lib.rs` → empty. + `crates/nostream-cli/src/main.rs`.** The binaries compose, log and exit; they are not + part of any library's contract. The check CI's reviewers use — it searches **every** + source file, not just the `lib.rs` roots, and skips comment lines that merely name the + crate: + + ``` + grep -rn 'anyhow' crates --include=*.rs \ + | grep -vE 'crates/nostreamd/src/main\.rs|crates/nostream-cli/src/main\.rs' \ + | grep -vE '^[^:]+:[0-9]+:\s*(//|///|//!)' + ``` + + → no output. Re-proven 2026-10-18 by adding `fn probe() -> anyhow::Result<()>` to + `crates/nostream-config/src/load.rs`, which the check printed. - **A subsystem that is not implemented returns `Error::NotImplemented { module: "path::to::call" }` — never `todo!()`, `unimplemented!()` or `panic!()`.** The daemon's `main` maps that variant to exit code