feat(sim): rutster-sim crate skeleton + Scenario/ScenarioStep types (slice-4½ S1)

The critical-path foundation for the benchmark + simulation harness.
Scenario is a TOML-deserializable scripted-caller data type; ScenarioStep
covers speak_loud / speak_quiet / pause / await_reply / end. Determinism is
the point -- reproducible thresholds in CI (ADR-0010). All other sim
modules land as stubs here + fill in across S2-S7. Threshold consts
(BARGE_IN_KILL_TIME_P99_MS = 80.0, MOUTH_TO_EAR_P99_MS = 700.0,
TICK_LAG_MAX_MS = 10.0, TICK_OVERRUN_PCT_MAX = 1.0, SWEEP_CONCURRENCIES =
[1,10,50]) land now per the plan's S1 step 2 note (used by S5/S6/S7 wiring).

Adds toml = 0.8 to workspace.dependencies (the first consumer; spec §1.1
claim of pre-existing membership was inaccurate).

Task S1 of slice-4½ -- everything else depends on this landing.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
This commit is contained in:
2026-07-05 02:55:32 -04:00
parent f8a8a2c21f
commit dc5a605535
11 changed files with 560 additions and 4 deletions

97
Cargo.lock generated
View File

@@ -374,7 +374,7 @@ checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
dependencies = [
"cfg-if",
"crossbeam-utils",
"hashbrown",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
@@ -485,6 +485,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
@@ -630,6 +636,12 @@ version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "http"
version = "1.4.2"
@@ -720,6 +732,16 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
]
[[package]]
name = "inout"
version = "0.1.4"
@@ -1243,6 +1265,20 @@ dependencies = [
"tracing",
]
[[package]]
name = "rutster-sim"
version = "0.0.0"
dependencies = [
"rutster",
"rutster-media",
"serde",
"thiserror 1.0.69",
"tokio",
"toml",
"tracing",
"url",
]
[[package]]
name = "rutster-spend"
version = "0.0.0"
@@ -1374,6 +1410,15 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -1689,6 +1734,47 @@ dependencies = [
"tungstenite",
]
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tower"
version = "0.5.3"
@@ -1945,6 +2031,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"

View File

@@ -6,13 +6,14 @@
resolver = "2"
members = [
"crates/rutster",
"crates/rutster-brain-realtime",
"crates/rutster-call-model",
"crates/rutster-media",
"crates/rutster-trunk",
"crates/rutster-sim",
"crates/rutster-spend",
"crates/rutster-tap",
"crates/rutster-tap-echo",
"crates/rutster-brain-realtime",
"crates/rutster-spend",
"crates/rutster-trunk",
]
[workspace.package]
@@ -59,3 +60,7 @@ async-trait = "0.1"
# client sends before WS upgrade; `openai_client::openai_headers` produces
# the header pairs the caller stuffs into it.
http = "1"
# toml 0.8: TOML deserialization for the slice-4½ sim-crate's Scenario files
# (crates/rutster-sim/scenarios/*.toml). The first consumer of `toml` in
# the workspace; declared here so future members share the version pin.
toml = "0.8"

View File

@@ -0,0 +1,30 @@
# crates/rutster-sim/Cargo.toml — the self-hostable benchmark + simulation
# harness crate (ADR-0010 spearhead step 4½). Default-off `sim-bench`
# feature gates the CI-regressed threshold sweep so the routine
# `cargo test --all` gate stays fast.
[package]
name = "rutster-sim"
version = "0.0.0"
license.workspace = true
edition.workspace = true
repository.workspace = true
description = "Self-hostable benchmark + simulation harness (ADR-0010 spearhead step 4½)."
[dependencies]
rutster-media = { path = "../rutster-media" }
rutster = { path = "../rutster" }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync", "time"] }
serde = { workspace = true, features = ["derive"] }
toml = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
[features]
default = []
# The CI-regressed threshold sweep. Default OFF so `cargo test --all` (the
# routine gate) stays fast. A separate CI job runs
# `cargo test --all --features=sim-bench -- --test-threads=1` per spec §5.4 +
# §6.5. A latency regression fails the build the same way a broken test
# does (ADR-0010).
sim-bench = []

View File

@@ -0,0 +1,11 @@
//! # concurrency — `ConcurrencyRunner`: N concurrent `SimCall`s + sweep-report
//! aggregation
//!
//! **Stub — lands in S5.**
//!
//! See `docs/superpowers/specs/2026-07-05-slice-4-half-benchmark-sim-design.md`
//! §3.4 + §4.2 + §2.4 for the design + `docs/superpowers/plans/2026-07-05-slice-4-half-benchmark-sim.md`
//! Task S5 for the implementation. Spawns N concurrent `SimCall`s
//! (N ∈ `SWEEP_CONCURRENCIES = [1, 10, 50]`) against the same in-process
//! `MockRealtimeBrain`, aggregates per-call latencies into the
//! `PerConcurrencyReport` rows of the `SweepReport`.

View File

@@ -0,0 +1,8 @@
//! # latency — post-hoc p50/p99 latency computation from `Capture` events
//!
//! **Stub — lands in S3.**
//!
//! See `docs/superpowers/specs/2026-07-05-slice-4-half-benchmark-sim-design.md`
//! §3.3 for the design + `docs/superpowers/plans/2026-07-05-slice-4-half-benchmark-sim.md`
//! Task S3 for the implementation. Consumes the `Capture` stream from a
//! `SimAudioPipe` and computes barge-in kill-time + mouth-to-ear p50/p99.

View File

@@ -0,0 +1,65 @@
//! # rutster-sim — the self-hostable benchmark + simulation harness
//!
//! **Status:** spearhead step 4½ (ADR-0010). The wedge's measurement surface.
//!
//! This crate drives synthetic callers through the SAME media-leg path real
//! callers use, measures p50/p99 mouth-to-ear latency + barge-in kill-time
//! against slice-4's ≤60 ms kill budget, and runs the same measurements at
//! 1 / 10 / 50 concurrent calls. A separate CI job
//! (`cargo test --all --features=sim-bench`) asserts thresholds per commit;
//! a latency regression fails the build (ADR-0010).
//!
//! # Why this crate exists (the FOB differentiator)
//!
//! Slice-4 ships a reflex loop + a synthetic e2e test. SIM-BENCH is the
//! artifact that turns arithmetic latency claims into CI-regressed
//! measurement. See
//! [`docs/superpowers/specs/2026-07-05-slice-4-half-benchmark-sim-design.md`]
//! for the design.
//!
//! # Why a separate crate (not in-tree tests)
//!
//! The harness is hot-path-adjacent + differentiating (ADR-0008 FOB) — it
//! earns cratehood the same way `rutster-tap` did. The dep direction is
//! clean: `rutster-sim` → `rutster-media` + `rutster`. The harness
//! consumes types; it doesn't ride on the binary's internal plumbing.
//!
//! # Module map (lands across S1-S7)
//!
//! - [`scenario`] (S1) — `Scenario` + `ScenarioStep` TOML-deserializable
//! scripted-caller data types. Determinism is the point.
//! - [`sim_audio_pipe`] (S2) — `SimAudioPipe: AudioPipe` test-double that
//! IS the caller; captures both clocks (`Instant::now()` at onset +
//! receipt). The measurement boundary (spec §2.2).
//! - [`latency`] (S3) — `LatencyProbe`: post-hoc p50/p99 kill + mouth-to-ear
//! computation from the `Capture` event stream.
//! - [`runner`] (S4) — `SimCall` + `ScenarioRunner`: drives one synthetic
//! caller end-to-end against the FOB reflex loop standalone in tokio
//! (no `MediaThread` extension per S4 standalone-path conclusion).
//! - [`concurrency`] (S5) — `ConcurrencyRunner`: N concurrent `SimCall`s
//! against the same MockRealtimeBrain; aggregates per-call latencies
//! into a `SweepReport`.
//! - [`tick_lag`] (S6) — `TickLagGauge`: the ADR-0010 doctrine-drift
//! detector. Surfaces `tick_overruns` / `last_tick_micros` in the
//! `SweepReport`.
//! - [`thresholds`] (S7) — Threshold consts + the `#[cfg(feature =
//! "sim-bench")] #[tokio::test]` assertion tests. A latency regression
//! fails the build.
// All modules declared upfront so the lib.rs is stable across task
// commits; each module file grows from a `//! stub` header to its full
// impl as its task lands. Only the `pub use` re-exports for landed
// modules are present — they grow as each task's symbols become available.
pub mod concurrency;
pub mod latency;
pub mod runner;
pub mod scenario;
pub mod sim_audio_pipe;
pub mod thresholds;
pub mod tick_lag;
pub use scenario::{Scenario, ScenarioError, ScenarioStep};
pub use thresholds::{
BARGE_IN_KILL_TIME_P99_MS, MOUTH_TO_EAR_P99_MS, SWEEP_CONCURRENCIES, TICK_LAG_MAX_MS,
TICK_OVERRUN_PCT_MAX,
};

View File

@@ -0,0 +1,12 @@
//! # runner — `SimCall` + `ScenarioRunner`: drive one synthetic caller
//! end-to-end
//!
//! **Stub — lands in S4.**
//!
//! See `docs/superpowers/specs/2026-07-05-slice-4-half-benchmark-sim-design.md`
//! §3.4 + §4.1 for the design + `docs/superpowers/plans/2026-07-05-slice-4-half-benchmark-sim.md`
//! Task S4 for the implementation. The SimCall wires itself standalone in
//! tokio (no `MediaCmd::RegisterSim` — per the plan's S4 standalone-path
//! conclusion, the File Structure table is stale on this point; S4
//! supersedes). Composes slice-4's `Reflex<TapAudioPipe>` +
//! `LocalVadReflex` stack directly against an in-process `MockRealtimeBrain`.

View File

@@ -0,0 +1,250 @@
//! # Scenario — the scripted-caller data type
//!
//! See `docs/superpowers/specs/2026-07-05-slice-4-half-benchmark-sim-design.md`
//! §3.1.
//!
//! A `Scenario` is a sequence of `ScenarioStep`s read from a TOML file under
//! `crates/rutster-sim/scenarios/*.toml`. Deterministic by construction —
//! the entire point is reproducible thresholds in CI (LLM-driven callers
//! land in a post-spearhead refinement tier; see §1.2).
//!
//! # Why TOML (not YAML, not RON)
//!
//! `serde` + `toml` is already a workspace member. TOML keeps the scenario
//! file readable as a one-shot script (a sequence of named steps + numbers);
//! YAML would invite flow-mapping complexity this format doesn't need.
//!
//! # Why `#[serde(tag = "kind")]` (internally-tagged enum)
//!
//! Each step in the scenario TOML is one TOML table:
//!
//! ```toml
//! [[steps]]
//! kind = "speak_loud"
//! frames = 20
//! ```
//!
//! `serde`'s internally-tagged enum representation (`tag = "kind"`) reads the
//! `kind` key to dispatch to the matching enum variant. This is the idiomatic
//! shape for "list of named, differently-shaped records" in TOML — the
//! alternative (externally-tagged) would require a redundant table layer
//! (`[[steps]] variant = { speak_loud = { frames = 20 } }`) that hurts
//! readability for no benefit. See <https://serde.rs enum-representations>.
//!
//! `rename_all = "snake_case"` maps the Rust `SpeakLoud` variant to the
//! TOML `speak_loud` tag — matches the convention used in slice-4's
//! `AdvisoryEvent` enum (the precedent this file follows).
use std::path::Path;
/// The scripted-caller scenario. Read from a TOML file. Deterministic.
///
/// # Example
///
/// ```toml
/// name = "loud-barge"
///
/// [[steps]]
/// kind = "speak_loud"
/// frames = 20
///
/// [[steps]]
/// kind = "await_reply"
/// frames = 0
///
/// [[steps]]
/// kind = "end"
/// ```
///
/// The `SimAudioPipe::new(scenario, ..)` constructor consumes the
/// `steps` vector front-to-back during `on_pcm_frame` (the sink path —
/// the caller "speaks") and `next_pcm_frame` (the source path — the
/// caller "hears" brain replies, advancing `AwaitReply` steps).
#[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq)]
pub struct Scenario {
/// Human-readable identifier; surfaces in CI failure messages
/// ("scenario loud-barge failed: p99 kill-time 84ms > 80ms").
pub name: String,
/// Time-ordered sequence of caller actions. The `SimAudioPipe`
/// consumes them in order during `on_pcm_frame` (for speak/pause
/// steps) and `next_pcm_frame` (for `AwaitReply` barriers).
pub steps: Vec<ScenarioStep>,
}
/// One axis of caller behavior. A scenario is a time-ordered sequence
/// of these. The `SimAudioPipe` consumes them in order during
/// `on_pcm_frame`.
///
/// # Why an enum (not a struct with a `kind` field)
///
/// The steps have *different fields* (`SpeakLoud { frames }` vs `End`
/// has none). A struct-with-kind-field would require `Option<u32>` for
/// every variant-irrelevant field — losing type safety for no ergonomic
/// gain. The enum approach makes the variant's payload explicit at the
/// type level; `serde`'s internally-tagged representation keeps the TOML
/// shape flat + readable.
#[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ScenarioStep {
/// Send N loud PCM frames (sample value 1000, well above
/// `VAD_RMS_THRESHOLD = 500.0`). Triggers the local VAD via
/// slice-4's `LocalVadReflex::on_pcm_frame` — the primary barge-in
/// path (slice-4 §5.1). The `SimAudioPipe`'s sink path emits one
/// loud `PcmFrame` per `on_pcm_frame` call while this step is
/// active; on step entry it captures `Capture::CallerLoudOnset`.
SpeakLoud { frames: u32 },
/// Send N zero frames (sample value 0, well below
/// `VAD_RMS_THRESHOLD`). Drives the mock-brain advisory path
/// (slice-4 §5.2 secondary path): `MockRealtimeBrain` sees
/// "no caller audio for M frames" + emits an advisory →
/// `Reflex::muted = true`. The wedge cares about LOUD barge
/// measurement; quiet onsets are unscored (no `Capture`).
SpeakQuiet { frames: u32 },
/// Insert N zero frames before the next step (silence pacing).
/// Used by the `sustained-call.toml` scenario (5 minutes of talk
/// with 3 barges) to space barge cycles apart.
Pause { frames: u32 },
/// Wait until the harness receives M "ear" frames before advancing.
/// Barrier semantics: brain's reply must arrive before the next
/// caller action. The `SimAudioPipe`'s source path
/// (`next_pcm_frame`) decrements this counter for each `Some(frame)`
/// returned; on reaching zero, advances.
AwaitReply { frames: u32 },
/// End the scenario. The `SimAudioPipe`'s `next_pcm_frame` returns
/// `None` thereafter; the `SimCall` (S4) detects end-of-scenario +
/// terminates its tick loop.
End,
}
/// Errors surfaced during scenario loading. Cold-path; OK to be
/// `thiserror`-derived (the hot path goes through
/// `SimAudioPipe::on_pcm_frame` which never reads files).
///
/// `#[from]` on the variants auto-implements `From<io::Error>` and
/// `From<toml::de::Error>` so `?`-propagation Just Works in
/// `Scenario::load`.
#[derive(Debug, thiserror::Error)]
pub enum ScenarioError {
#[error("scenario file read failed: {0}")]
Io(#[from] std::io::Error),
#[error("scenario TOML parse failed: {0}")]
Parse(#[from] toml::de::Error),
}
impl Scenario {
/// Load a scenario from a TOML file. Cold-path.
///
/// Wraps `std::fs::read_to_string` + `toml::from_str` behind the
/// `ScenarioError` enum so callers can `?`-propagate both failure
/// modes uniformly. The `path: impl AsRef<Path>` bound follows
/// the std-library convention: it accepts `&str`, `String`,
/// `PathBuf`, `&Path` — matching how scenarios are loaded from
/// CLI args or test fixtures.
pub fn load(path: impl AsRef<Path>) -> Result<Self, ScenarioError> {
let raw = std::fs::read_to_string(path)?;
Self::from_toml(&raw)
}
/// Parse a scenario from an in-memory TOML string.
///
/// Split out from `load` so unit tests can construct scenarios
/// without touching the filesystem (filesystem-isolated unit
/// tests are the std pattern in this codebase — see slice-4's
/// `reflex.rs` tests).
pub fn from_toml(s: &str) -> Result<Self, ScenarioError> {
Ok(toml::from_str(s)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scenario_parses_minimal_end_only() {
// The trivial scenario: just one `End` step. Exercises the
// internally-tagged enum's bare-variant shape (`kind = "end"`
// with no payload fields).
let toml = r#"
name = "trivial"
[[steps]]
kind = "end"
"#;
let s = Scenario::from_toml(toml).expect("parse");
assert_eq!(s.name, "trivial");
assert_eq!(s.steps, vec![ScenarioStep::End]);
}
#[test]
fn scenario_parses_loud_barge_shape() {
// The canonical loud-barge scenario from spec §5.3. Verifies
// the three-step shape (speak_loud → await_reply → end) parses
// to the expected variant sequence with payload fields intact.
let toml = r#"
name = "loud-barge"
[[steps]]
kind = "speak_loud"
frames = 20
[[steps]]
kind = "await_reply"
frames = 0
[[steps]]
kind = "end"
"#;
let s = Scenario::from_toml(toml).expect("parse");
assert_eq!(s.name, "loud-barge");
assert_eq!(
s.steps,
vec![
ScenarioStep::SpeakLoud { frames: 20 },
ScenarioStep::AwaitReply { frames: 0 },
ScenarioStep::End,
]
);
}
#[test]
fn scenario_parses_sustained_call_shape() {
// The sustained-call scenario (spec §5.3 entry #3) alternates
// speak_loud + speak_quiet. Verifies both payload-bearing
// variants parse correctly in sequence.
let toml = r#"
name = "sustained"
[[steps]]
kind = "speak_loud"
frames = 10
[[steps]]
kind = "speak_quiet"
frames = 10
[[steps]]
kind = "speak_loud"
frames = 10
[[steps]]
kind = "end"
"#;
let s = Scenario::from_toml(toml).expect("parse");
assert_eq!(s.steps.len(), 4);
assert!(matches!(s.steps[0], ScenarioStep::SpeakLoud { frames: 10 }));
assert!(matches!(
s.steps[1],
ScenarioStep::SpeakQuiet { frames: 10 }
));
assert!(matches!(s.steps[2], ScenarioStep::SpeakLoud { frames: 10 }));
assert!(matches!(s.steps[3], ScenarioStep::End));
}
#[test]
fn scenario_unknown_kind_errors() {
// An unknown `kind` tag (typo, future-extension tag, etc.)
// must surface as a `Parse` error rather than silently
// defaulting. This is the contract `serde`'s internally-tagged
// enum provides: unknown tags fail the deserialize rather
// than producing a `None`-ish default.
let toml = r#"
name = "bad"
[[steps]]
kind = "ship_a_real_caller"
"#;
assert!(Scenario::from_toml(toml).is_err());
}
}

View File

@@ -0,0 +1,9 @@
//! # sim_audio_pipe — the test-double `AudioPipe` that simulates a caller
//!
//! **Stub — lands in S2.**
//!
//! See `docs/superpowers/specs/2026-07-05-slice-4-half-benchmark-sim-design.md`
//! §3.2 for the design + `docs/superpowers/plans/2026-07-05-slice-4-half-benchmark-sim.md`
//! Task S2 for the implementation. Drives a `Scenario` on `on_pcm_frame`
//! (sink: caller speaks); captures `Instant::now()` at every meaningful
//! event for the `LatencyProbe`.

View File

@@ -0,0 +1,58 @@
//! # thresholds — CI-regressed latency thresholds + sim-bench assertion tests
//!
//! See `docs/superpowers/specs/2026-07-05-slice-4-half-benchmark-sim-design.md`
//! §5.1 + §5.5.
//!
//! The threshold consts land here at S1 (per the plan's S1 step 2 note:
//! "the consts as immediate module-level `pub const` items per spec §5.1 —
//! they're used by S5/S6/S7 wiring"). The `#[cfg(feature = "sim-bench")]
//! #[tokio::test]` assertion tests land at S7.
//!
//! # Why these numbers
//!
//! See spec §5.1 for the budget-vs-assertion-slack reasoning. Each const
//! is paired with a doc-comment explaining the budget it enforces + the
//! slack rationale (so a future maintainer who needs to bump one knows
//! *why* the current value is what it is, not just *what* it is).
/// Slice-4 spec §5.1 + §7 done-criteria #8: kill-time budget is
/// ≤60 ms (3 debounce frames × 20 ms tick + 1 tick to drain + apply).
/// Observer slack to make CI deterministic-but-not-flaky on a slow
/// runner: effective CI assertion ≤80 ms (60 ms budget + 20 ms slack).
///
/// A regression here is the red X ADR-0010 demands — the wedge's
/// "local real-time reflexes that don't need the brain" claim is
/// arithmetic until this assertion fires on every PR.
pub const BARGE_IN_KILL_TIME_P99_MS: f64 = 80.0;
/// Slice-1 + slice-3 mouth-to-ear budget: 200 ms (slice-1 notification
/// budget) + 250 ms mock brain round-trip + 100 ms playout buffer.
/// CI assertion ceiling: 700 ms (allowance for CI runner variance
/// against the dev machine — the mock brain is deterministic but the
/// harness adds observer cost; the dev machine usually lands ~600 ms).
pub const MOUTH_TO_EAR_P99_MS: f64 = 700.0;
/// Slice-5/seams tick-lag gauge: the meta-tick must stay under 10 ms
/// (the loop's nominal period). At 1 call: ≤2 ms expected. At 50
/// calls: ≤10 ms expected. Tick overruns (count of ticks exceeding
/// 10 ms) at p50 across the sweep: ≤1% of total ticks per
/// `TICK_OVERRUN_PCT_MAX`.
///
/// If a concurrency sweep shows `tick_overrun_pct > 1.0` at 50 calls,
/// **the FOB reflex loop's single-thread debt is real and the
/// dedicated-threadpool-shard graduation (slice-4 §1.2 deferral #2)
/// gets its data-driven case.** That finding is the slice's
/// load-bearing output regardless of whether the latency thresholds
/// pass — the doctrine-drift detector worked.
pub const TICK_LAG_MAX_MS: f64 = 10.0;
pub const TICK_OVERRUN_PCT_MAX: f64 = 1.0;
/// Concurrency-sweep sample sizes per spec §2.4: 1 isolates the
/// baseline (cold-path latency with zero concurrency pressure —
/// slice-4's §5.1 ≤60 ms kill budget asserted here); 10 is the
/// warm working set (~peak spearhead-scale); 50 is the saturation
/// point (ADR-0010's "single-poll-task head-of-line-blocking debt"
/// lives here). We do NOT test 100/500/5000 — that's fleet-scale
/// (rung 3). 50 is the upper edge of the spearhead's "one binary,
/// one city" claim.
pub const SWEEP_CONCURRENCIES: &[usize] = &[1, 10, 50];

View File

@@ -0,0 +1,13 @@
//! # tick_lag — `TickLagGauge`: the ADR-0010 doctrine-drift detector
//!
//! **Stub — lands in S6.**
//!
//! See `docs/superpowers/specs/2026-07-05-slice-4-half-benchmark-sim-design.md`
//! §3.6 + §6.4 for the design + `docs/superpowers/plans/2026-07-05-slice-4-half-benchmark-sim.md`
//! Task S6 for the implementation. Surfaces `MediaStats.{tick_overruns,
//! last_tick_micros}` as primary readouts in the `SweepReport`. The
//! concurrency sweep turns the gauge from a counter into a decision
//! artifact: "does single-thread poll loop breach budget at realistic
//! concurrency?" gets answered with data, not vibes. If yes, the
//! dedicated threadpool-shard graduation (slice-4 deferral #2) gets
//! scheduled on evidence.