Make the haskell simulation buildable
Home /
Input Output /
ouroboros-leios
May 11, 8-9 AM (0)
May 11, 9-10 AM (1)
May 11, 10-11 AM (2)
May 11, 11-12 PM (1)
May 11, 12-1 PM (0)
May 11, 1-2 PM (0)
May 11, 2-3 PM (1)
May 11, 3-4 PM (2)
May 11, 4-5 PM (0)
May 11, 5-6 PM (0)
May 11, 6-7 PM (0)
May 11, 7-8 PM (0)
May 11, 8-9 PM (1)
May 11, 9-10 PM (1)
May 11, 10-11 PM (0)
May 11, 11-12 AM (0)
May 12, 12-1 AM (0)
May 12, 1-2 AM (0)
May 12, 2-3 AM (0)
May 12, 3-4 AM (1)
May 12, 4-5 AM (0)
May 12, 5-6 AM (0)
May 12, 6-7 AM (0)
May 12, 7-8 AM (0)
May 12, 8-9 AM (1)
May 12, 9-10 AM (0)
May 12, 10-11 AM (0)
May 12, 11-12 PM (0)
May 12, 12-1 PM (0)
May 12, 1-2 PM (1)
May 12, 2-3 PM (0)
May 12, 3-4 PM (0)
May 12, 4-5 PM (0)
May 12, 5-6 PM (0)
May 12, 6-7 PM (0)
May 12, 7-8 PM (0)
May 12, 8-9 PM (0)
May 12, 9-10 PM (0)
May 12, 10-11 PM (0)
May 12, 11-12 AM (0)
May 13, 12-1 AM (0)
May 13, 1-2 AM (0)
May 13, 2-3 AM (0)
May 13, 3-4 AM (0)
May 13, 4-5 AM (0)
May 13, 5-6 AM (0)
May 13, 6-7 AM (0)
May 13, 7-8 AM (0)
May 13, 8-9 AM (0)
May 13, 9-10 AM (1)
May 13, 10-11 AM (2)
May 13, 11-12 PM (0)
May 13, 12-1 PM (1)
May 13, 1-2 PM (1)
May 13, 2-3 PM (0)
May 13, 3-4 PM (2)
May 13, 4-5 PM (1)
May 13, 5-6 PM (0)
May 13, 6-7 PM (0)
May 13, 7-8 PM (0)
May 13, 8-9 PM (0)
May 13, 9-10 PM (0)
May 13, 10-11 PM (0)
May 13, 11-12 AM (0)
May 14, 12-1 AM (0)
May 14, 1-2 AM (0)
May 14, 2-3 AM (0)
May 14, 3-4 AM (0)
May 14, 4-5 AM (0)
May 14, 5-6 AM (0)
May 14, 6-7 AM (0)
May 14, 7-8 AM (0)
May 14, 8-9 AM (0)
May 14, 9-10 AM (0)
May 14, 10-11 AM (0)
May 14, 11-12 PM (0)
May 14, 12-1 PM (0)
May 14, 1-2 PM (0)
May 14, 2-3 PM (1)
May 14, 3-4 PM (0)
May 14, 4-5 PM (0)
May 14, 5-6 PM (0)
May 14, 6-7 PM (0)
May 14, 7-8 PM (1)
May 14, 8-9 PM (1)
May 14, 9-10 PM (1)
May 14, 10-11 PM (0)
May 14, 11-12 AM (0)
May 15, 12-1 AM (0)
May 15, 1-2 AM (0)
May 15, 2-3 AM (0)
May 15, 3-4 AM (0)
May 15, 4-5 AM (0)
May 15, 5-6 AM (0)
May 15, 6-7 AM (0)
May 15, 7-8 AM (0)
May 15, 8-9 AM (0)
May 15, 9-10 AM (0)
May 15, 10-11 AM (0)
May 15, 11-12 PM (0)
May 15, 12-1 PM (1)
May 15, 1-2 PM (0)
May 15, 2-3 PM (1)
May 15, 3-4 PM (0)
May 15, 4-5 PM (0)
May 15, 5-6 PM (0)
May 15, 6-7 PM (0)
May 15, 7-8 PM (0)
May 15, 8-9 PM (0)
May 15, 9-10 PM (0)
May 15, 10-11 PM (0)
May 15, 11-12 AM (0)
May 16, 12-1 AM (0)
May 16, 1-2 AM (0)
May 16, 2-3 AM (0)
May 16, 3-4 AM (0)
May 16, 4-5 AM (0)
May 16, 5-6 AM (0)
May 16, 6-7 AM (0)
May 16, 7-8 AM (0)
May 16, 8-9 AM (0)
May 16, 9-10 AM (0)
May 16, 10-11 AM (0)
May 16, 11-12 PM (0)
May 16, 12-1 PM (0)
May 16, 1-2 PM (0)
May 16, 2-3 PM (0)
May 16, 3-4 PM (0)
May 16, 4-5 PM (0)
May 16, 5-6 PM (0)
May 16, 6-7 PM (0)
May 16, 7-8 PM (0)
May 16, 8-9 PM (0)
May 16, 9-10 PM (0)
May 16, 10-11 PM (0)
May 16, 11-12 AM (0)
May 17, 12-1 AM (0)
May 17, 1-2 AM (0)
May 17, 2-3 AM (0)
May 17, 3-4 AM (0)
May 17, 4-5 AM (0)
May 17, 5-6 AM (0)
May 17, 6-7 AM (0)
May 17, 7-8 AM (0)
May 17, 8-9 AM (0)
May 17, 9-10 AM (0)
May 17, 10-11 AM (0)
May 17, 11-12 PM (0)
May 17, 12-1 PM (0)
May 17, 1-2 PM (0)
May 17, 2-3 PM (0)
May 17, 3-4 PM (0)
May 17, 4-5 PM (0)
May 17, 5-6 PM (0)
May 17, 6-7 PM (0)
May 17, 7-8 PM (0)
May 17, 8-9 PM (0)
May 17, 9-10 PM (0)
May 17, 10-11 PM (0)
May 17, 11-12 AM (0)
May 18, 12-1 AM (0)
May 18, 1-2 AM (0)
May 18, 2-3 AM (0)
May 18, 3-4 AM (0)
May 18, 4-5 AM (0)
May 18, 5-6 AM (0)
May 18, 6-7 AM (0)
May 18, 7-8 AM (0)
May 18, 8-9 AM (0)
26 commits this week
May 11, 2026
-
May 18, 2026
LinearLeiosTuner.html: add Mempool refill chart
LinearLeiosTuner.html: add Mempool refill chart
LinearLeiosTuner.html: address caveat about imperfect chain growth
LinearLeiosTuner.html: major pivot in format
LinearLeiosTuner.html: emphasize the trade-off of increased network usage
con-rs: enforce CIP-0164 PV/NPV pool partition
CIP-0164 partitions pools by stake-ordering into persistent (indices
[1, n_1]) and non-persistent candidates ([i*, |P|]) — disjoint by
construction. A pool emits exactly one vote per election: PV xor
NPV, never both. Spec text in post-cip/weighted-fait-accompli.md
(§ Persistent seats / § Non-persistent candidates).
The current implementation violated the partition on both sides:
- `decide_vote` (leios.rs) ran the NPV trial unconditionally; a
persistent-seated voter could emit both a PV and an NPV signature
for the same EB.
- `weight_for` (elections.rs) accepted both and credited them
independently in `voter_weights`, since `record_vote` dedups by
`(voter_id, tag)` — so dual-emitters were double-counted toward
quorum.
Now:
- `decide_vote` returns NPV signature only when `persistent_seats == 0`.
- `weight_for(tag=1, ..)` returns 0 if the voter is in the
persistent committee. Defence-in-depth: a malformed dual-vote
arriving over the wire is silently dropped instead of inflating
weight.
Empirical at NA,0.350 / wfa-ls / 750n / -s 500 (con-rs):
bundles: 2093 PV-only + 207 NPV-only + 1593 dual (pre)
= 3686 PV-only + 207 NPV-only + 0 dual (post)
total weight: 11748 → 9648 (−18%, the prior over-count was the
dual NPV weight from PV-seated voters)
uncertified: 6/26 → 9/26 (cert rate degrades because the previous
quorum-passing weights were inflated by
the spec violation; the new number is
the honest baseline)
The mean per-EB weight drops 452 → 371, crossing below the configured
`vote-threshold: 450` on three more EBs. Recovering cert rate from
this regime is a committee-size / threshold tuning question, not a
correctness fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-rs: plumb NPV through con-rs WfaLs adapter
Renames the two committee-size config fields to match con-rs's
internal naming and the CIP-0164 terminology:
persistent-vote-generation-probability → persistent-voters
non-persistent-vote-generation-probability → non-persistent-voters
Old YAML keys keep working via `#[serde(alias = ...)]` so the 70+
existing experiment configs round-trip unchanged. The "probability"
framing was always misleading — these have always been expected
committee sizes (defaults 400 / 100, not 0.4 / 0.1).
SimConfiguration grows `persistent_voters` / `non_persistent_voters`
alongside the existing `vote_probability` (= their sum). Linear's
VRF lottery keeps consuming the sum unchanged. The con-rs adapter
now wires both into `CommitteeSelection::WfaLs { persistent_voters,
non_persistent_voters }` — previously NPV was hard-zeroed because
the sim collapsed both into one number.
Empirical at NA,0.350 / wfa-ls / 750n / -s 500:
bundles: 2093 PV-only + 207 NPV-only + 1593 both
total: 3893 bundles, 11748 votes, mean 3.018/bundle
uncertified: 6/26, endorsements: 9
NPV-only bundles previously were 0 across every sim run; the 207
new ones are voters without a PV seat who won the per-EB Bernoulli
trial against their stake share — exactly the CIP-0164 contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: EveryoneVotes covers every registered node
Drops the `stake > 0` filter from `build_committee`'s EveryoneVotes arm. EveryoneVotes is an extreme-test selection mode — the point is to drive the maximum possible voter set, including relay-only (zero-stake) nodes, so that a sim configured with this committee has the full network as its committee. Without the filter, the everyone committee aligns with linear_leios's `CommitteeSelectionAlgorithm::Everyone` (every node returns `vrf_wins = 1`) and with the script-level threshold formula `ceil(TOTAL_NODES * 0.75)`. Empirical confirmation at NA,0.350 / everyone / 750n / -s 500 / con-rs single-shot: - before filter lift: 26/26 uncertified, 4203 bundles (committee was 216 staking pools; threshold 563 unreachable). - after: 7/26 uncertified, 14434 bundles, mean 627 σ 208 — within noise of linear-with-tx-references's 6/26, 14296 bundles, mean 549 σ 291. WfaLs and StakeCentile retain stake-weighted semantics; only the test-only EveryoneVotes mode changes. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: retry_vote_in_window knob covers predicate + lottery retries
Adds `VotingConfig.retry_vote_in_window` (default `true`). The flag gates both in-window retry paths uniformly: - predicate failure (`WrongEB` / `LateRBHeader` / `MissingTX`): a later slot in the window may see chain-tip / mempool state that flips the predicate to success; - lottery loss (no PV seat and no NPV win): the NPV trial re-runs with a fresh per-slot VRF input, so a non-seated voter has up to `vote_window` independent chances to win. `LateEB` stays permanent regardless — `eb_seen_slot` is fixed. `true` matches the CIP-0164 reading that the voting window is the licence to vote at any in-window moment. `false` gives one decision per (voter, EB) and one NPV trial — useful for adversarial sims and for like-for-like comparison against linear_leios.rs. Surfaced as `retry-vote-in-window` in sim-rs's RawParameters. net-rs hardcodes `true` (production stays on CIP semantics). Empirical: at NA,0.350 / wfa-ls / 750n / -s 1500 the flag doesn't move the aggregate (this workload's WfaLs committee is pure PV; the NPV path is dormant). The real divergence vs linear_leios turned out to be that linear's WfaLs is a per-EB VRF lottery rather than the CIP-0164 stake-weighted Hare-quota seat allocation con-rs uses — covered separately by item 1 in the diff plan. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-rs: gate con-rs EB validation on local tx availability
Mirrors `LinearWithTxReferences`'s CIP-0164 receiver gate: an EB can only be validated once every referenced tx body is in the local mempool. Two side maps thread the dependency: - `eb_pending_txs: EBId → BTreeSet<TxId>` — per-EB still-missing set - `missing_tx_index: TxId → BTreeSet<EBId>` — reverse index for the tx-admit release path `finish_validating_eb_header` now computes `missing` against `mempool.has_tx` (union of free pool + EB-pinned bodies) and either schedules `EBBlockValidated` immediately or stashes the EB pending in both maps. `acknowledge_tx_for_pending_ebs` walks the reverse index after every successful `admit_validated` and releases EBs whose set reaches empty. Chain-prune sweeps both maps in lockstep with the existing `self.ebs` retention. Empirical note: NA,0.350 / wfa-ls / 750n / -s 1500 produces bit-identical results with and without the gate (88/15/33/42084 either way), because tx diffusion is fast enough in this sim that every receiver already has the bodies in mempool before the EB arrives. The gate matters in regimes where Message::Tx lags Message::EB (cluster-scale net-rs, withheld-tx attacks, or once the upcoming sim message split surfaces a real manifest-only intermediate state). It's the protocol-correct behaviour either way — without it, an EB could in principle validate and open voting before any voter has the bodies, an outcome the CIP-0164 MissingTX predicate is supposed to prevent at validation time, not just at decide-vote time. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: own bitmap helpers and missing-eb-tx logic
Moves the sparse BTreeMap<u16, u64> bitmap helpers (from_indices / select_all / contains / iter_indices) into con_rs::bitmap. The encoded form already crosses the con-rs trait boundary (EbTxsFetchPolicy::pick, on_eb_txs_offered), so its encoders belong on the same side. net-core's leios_fetch::bitmap stays as a thin re-export to preserve callers. Adds LeiosState::missing_eb_tx_bitmap(eb_hash, &MempoolState) → the wire-encoded sparse bitmap of manifest indices whose bodies aren't yet locally available. Empty bitmap when the manifest is unknown or every referenced tx is held — both deliberately suppress further fetches (a "select all" reply against a peer that doesn't have the bodies either triggers the retry storm we hit on the broadcast-n=3 experiment). Net-rs's bitmap_for_missing_txs now delegates to con-rs. LeiosConsensus is the only consumer with both the LeiosState and mempool handles, so the wrapper exposes a borrow via Mempool::as_inner. Dead Mempool::current_tx_ids method removed. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: NoFetch policy + per-class fetch-policy YAML knob
Adds a NoFetch policy implementing BlockFetchPolicy, EbFetchPolicy,
EbTxsFetchPolicy, and VoteFetchPolicy. Every pick returns empty so
the matching offer never fans out into a fetch effect.
Surfaces it as a config in both consumers:
net-rs: FetchPolicyKind::NoFetch added alongside LowestRtt /
Broadcast, routed through all four into_*_policy helpers.
sim-rs: new top-level `fetch-policy` YAML stanza mirroring net-rs's
FetchPolicyConfig shape (block / eb / eb-txs / votes), each
class independently set to lowest-rtt | broadcast | no-fetch.
Default `lowest-rtt` everywhere preserves LeiosState::new
behaviour.
Wired into the sim adapter via set_eb_policy / set_eb_txs_policy /
set_vote_policy on LeiosState and set_fetch_policy on PraosState.
Empirical note: at NA,0.350 / wfa-ls / 750n / -s 1500, switching the
eb-txs class to NoFetch produces results bit-identical to LowestRtt.
The sim adapter sends EBs via `Message::EB(eb)` with the full
LinearEndorserBlock.txs Arcs in-band; the receiver never enters a
manifest-without-bodies state, so on_eb_txs_offered is never called.
Modelling separate EB-tx fetch in the simulator needs an adapter
change to split EB transmission into manifest + body fetch.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
con-rs: cap EB body at eb_body_max_bytes on overflow
`BodyPath::decide` now takes an `eb_body_max_bytes` cap. When the mempool overflows the RB body cap, the EB manifest is truncated at this byte limit (FIFO-ordered, at-least-one guarantee preserved) and the remainder stays in the mempool for the next RB. `MempoolState::produce_eb` takes a matching `count` so the drain commits exactly the manifest's prefix instead of every free tx. Sim-rs's adapter passes `eb_referenced_txs_max_size_bytes`; net-rs gains an `eb_body_max_bytes` config field (default 16 MB). At NA,0.350 / wfa-ls / 750n / -s 1500: lifts con-rs cert rate from 11% to 83%, beats `linear-with-tx-references` (53%) on the same config. Pre-cap EBs grew monotonically with mempool ingest because overflow spilled the whole pool into one EB; capped EBs now sit at ~5k-10k txs in the regime where the cap binds. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Add improved DeltaQ analysis for Linear Leios EB diffusion
Fixes three bugs in the prior Haskell analysis and adds a parametric sweep over EB closure size (S_EB_tx) to determine practical limits. Adds a sensitivity study to analyze effects of mempool fragmentation: 1) 1-hop vs. multi hop closure fetch: a. Full-blended (12MB) worst case b. Maximum feasible closure size when 1-hop fails. 2) TxCache miss rate sensitivity study for 1-hop missing tx's closure fetch Key corrections: - RB structure: certRB and txRB paths are mutually exclusive (probabilistic choice weighted by p_cert), not forced in parallel as in the prior model - Scale mixture → fixed-N CLT: EB closure validator processes all N txs, not a random subset; prior model underestimated reapply time by 2× - apply vs. reapply: cache-miss txs (π₁=1/6) require full applyTx (0.507 ms/tx), not just reapplyTx (0.070 ms/tx); effective cost μ_eff = 0.143 ms/tx - Adds two TCP congestion control algorithms to simulate transfer times from cold start and with steady state max thruput given a set loss parameter. Lists assumptions, caveats, and items which deserve further study.
Start work on a deltaq backend that reports progress
Narrow haskell.nix source set to relevant directories
Replace `src = ./..` in nix/project.nix's leios-hs-sources derivation with a lib.fileset.toSource that includes only cabal.project, the Haskell package directories, data/ (test fixtures referenced by the patchPhase), and leios-trace-verifier/conformance-traces/. Edits to unrelated parts of the repo (ui/, docs/, demo/, etc.) no longer invalidate the source hash, so haskell.nix's plan-to-nix-pkgs IFD doesn't re-run on every nix develop after a non-Haskell change. After this change, re-entering the dev shell after an edit in ui/ takes ~2s instead of triggering the full plan-to-nix-pkgs rebuild. Co-Authored-By: Claude Opus 4.7 <[email protected]>
Split the monolithic cabal.project into per-cluster Haskell projects
Each Haskell cluster (simulation, leios-trace-hs, leios-trace-verifier, trace-processor, leios-deltaq, betti0) now owns its own cabal.project and its own haskell.nix project scoped to just its subtree, exposed lazily under legacyPackages.<cluster>. The previous setup snapshot the whole repo as haskell.nix source, so any edit invalidated every cluster's plan and IFD ran on every nix develop; now eval inputs are bounded per cluster, dev shells are plain mkShell decoupled from haskell.nix (~10s entry), and clusters can drift their pins independently. Cross-cluster source deps (simulation, leios-trace-verifier both consume leios-trace-hs) use lib.fileset.toSource + cabalProjectFileName so the existing ../leios-trace-hs paths resolve naturally. iogx is dropped; flake-parts drives the flake directly with haskell-nix and CHaP as top-level inputs. CI now scopes to the simulation cluster. Co-Authored-By: Claude Opus 4.7 <[email protected]>
Add new brainstorming idea, ProtoEBs
sim-rs: tx-aging index for con-rs adapter
The chain-state prune landed in a781d1548 left `tx_arcs` and `announced_or_known` growing forever because they carried no slot label. At NA,0.200 with 7.5 tx/s × 1500 slots that's >11k unbounded entries plus the unmatched `tx_id → Arc<Transaction>` references they pin, dominating the late-run memory growth that still pushed RSS to 11 GB at slot 373. `tx_seen_slot: BTreeMap<TxId, u64>` records the first-observe slot on every entry path (`handle_new_tx`, `Message::AnnounceTx`, `Message::Tx`). `prune_chain_state` then ages the tx side-tables on a second clock (sim's `linear-tx-max-age-slots`, default 100) while preserving anything still in `MempoolState::current_tx_ids` — mirrors `linear_leios::prune_old_txs`. Determinism check passes (`cmp` clean on simple.yaml/300 slots). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-rs: prune chain-state in con-rs adapter every 100 slots
Without this, every chain-state side-table (`rbs`, `ebs`, `eb_announcers`, `eb_hash_to_id`, `vote_bundles`, `votes_by_eb`, `noted_no_vote`) accumulates one entry per (slot, producer/voter) forever. At 750 nodes × 1500 slots that's millions of entries and a ~6× wall-clock slowdown vs `linear_leios` at the same throughput (linear: slot 300 in 4 min; con-rs: slot 376 in 30 min). `prune_chain_state` drops anything older than 5× the pipeline window — well beyond cert assembly, vote serving, and `LeiosState`'s own slot-aging cutoff. `tx_arcs` and `announced_or_known` are excluded for now because they don't carry a slot label; that's a follow-on once a `(tx_id → seen_slot)` index lands. Determinism check still passes (cmp clean on simple.yaml/300 slots). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
sim-rs: dedup NoVote telemetry in con-rs adapter
con-rs deliberately re-fires `LeiosEffect::NoVote` once per slot of the voting window for transient reasons (WrongEB / MissingTX / LateRBHeader) — `mark_voted` is suppressed so the election keeps re-checking as the chain tip catches up. At 750 nodes × O(10) EBs in flight that's thousands of duplicate telemetry events per slot which dominates the event-monitor queue and slows W2.6 runs to a crawl. The cleanup memory (`project_con_rs_cleanups`) flags this as a known con-rs telemetry-noise item. Collapse it adapter-side until con-rs grows a per-(eb, reason) emission gate: a `noted_no_vote` BTreeSet keyed by `(eb_hash, NoVoteReason)` records the first emission and suppresses subsequent duplicates. `apply_leios_effects` switches from `&self` to `&mut self` to carry the set. Determinism check still passes (`cmp` clean across two runs, 18490 events each on simple.yaml/300 slots). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026w18: add user-facing README, note unbounded mempool in CLAUDE.md
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
net-rs: per-traffic-class fetch policy config
`con-rs::fetch` already exposes four independently-swappable policy
traits (block / EB / EB-txs / votes) with omnibus stock impls
(`LowestRttFirst`, `BroadcastN`). Until now the wrapper plugged the
defaults — `LowestRttFirst` for every class — and never touched them,
so the lever we have on EB-tx fan-out was unreachable from config.
Add a `fetch_policy` section to `NodeConfig`:
[fetch_policy.eb_txs]
kind = "broadcast" # or "lowest_rtt"
n = 3 # omit `n` => broadcast to every candidate
Defaults preserve the historical single-peer RTT-min behaviour for every
class. Each class is set independently so research configs can fan out
just one traffic class. `n: Option<usize>` translates `None` into
`BroadcastN::all()` at the boundary.
Wiring: `PraosConsensus::set_block_policy`,
`LeiosConsensus::{set_eb,set_eb_txs,set_vote}_policy` mirror
`set_rtt`; `Consensus::new` accepts the parsed `FetchPolicyConfig` and
applies it after constructing the two state machines.
Also drops in a 100n/d10 cluster sample
(`net-cluster/configs/cluster-100n-d10-NA0.200.toml`) matching the
collapse-baseline topology and sim-rs's NA,0.200 throughput target.
Drive-by: two stale tests
(`bitmap_for_offered_txs_falls_back_to_select_all_when_manifest_unknown`,
`bitmap_is_empty_when_mempool_already_has_every_tx`) hung on
`rx.recv().await` because they asserted the pre-1c279c709 fallback
behaviour where a missing manifest still produced a fetch command. The
empty-bitmap short-circuit means no fetch is emitted in those scenarios;
renamed both tests + new `no_fetch_cmd_within` helper that asserts the
new no-emit behaviour with a 50ms negative window.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Create a schema that is expected by the visualizer
This uses the same typescript -> json schema workflow and also sanity checks the prepared simulator traces in the repository.