From 8c19260e8d7580921b55d66f9ec3b97ba8683cfc Mon Sep 17 00:00:00 2001 From: claude Date: Wed, 22 Jul 2026 00:40:04 +0200 Subject: [PATCH] =?UTF-8?q?fieldtest:=20tap-subscribers=20=E2=80=94=205=20?= =?UTF-8?q?examples,=200=20bugs=20/=202=20friction=20/=203=20spec-gap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cycle-close fieldtest for #283 + #77 (tap subscribers), exercising the new tap surface as a downstream consumer over the public interface only (design ledger, glossary, cargo doc, example corpus — never the crate sources). Five examples, one per axis, all built and run against worktree HEAD bd0c557: - tap_1_record_spread.ops.json — record → runs/traces/graph/spread.json - tap_4_measure_ic.ops.json — two-tap run → aura measure ic (clean IC) - tap_consumer/src/tap_2_fold.rs — Named{mean}/Named{count} via run_signal_r - tap_consumer/src/tap_3_live.rs — Live(closure); bit-identical to fold + record - tap_consumer/examples/refusal_probe.rs — unknown-label refusal (exit 1, no write) The consumer crate is a detached workspace (own empty [workspace], path-deps on aura-core/-engine/-runner), exactly as a real World program; it is not a workspace member and does not build under `cargo build --workspace`. Verified here with a fresh `cargo build --bins --examples` (Finished, 0 errors). No merge-blocking defect: record/fold/live/measure over the shared pair are correct and deterministic. Follow-ups filed at cycle close: - #309 (feature) — a plain `aura run` echoes no recorded trace handle (F1+SG1) - #310 (idea) — Named fold has no data/CLI reach; ratify Rust-only vs selector (SG2) - #311 (bug) — same-name re-run orphans still-chartable tap files (SG3, pre-existing) - #297 (comment) — library refusal = process::exit, not Result (F2, already tracked) --- .../tap_1_record_spread.ops.json | 13 +++++ .../tap_4_measure_ic.ops.json | 15 +++++ .../tap_consumer/.gitignore | 2 + .../tap_consumer/Cargo.toml | 24 ++++++++ .../tap_consumer/examples/refusal_probe.rs | 21 +++++++ .../tap_consumer/src/tap_2_fold.rs | 39 +++++++++++++ .../tap_consumer/src/tap_3_live.rs | 56 +++++++++++++++++++ 7 files changed, 170 insertions(+) create mode 100644 fieldtests/cycle-283-tap-subscribers/tap_1_record_spread.ops.json create mode 100644 fieldtests/cycle-283-tap-subscribers/tap_4_measure_ic.ops.json create mode 100644 fieldtests/cycle-283-tap-subscribers/tap_consumer/.gitignore create mode 100644 fieldtests/cycle-283-tap-subscribers/tap_consumer/Cargo.toml create mode 100644 fieldtests/cycle-283-tap-subscribers/tap_consumer/examples/refusal_probe.rs create mode 100644 fieldtests/cycle-283-tap-subscribers/tap_consumer/src/tap_2_fold.rs create mode 100644 fieldtests/cycle-283-tap-subscribers/tap_consumer/src/tap_3_live.rs diff --git a/fieldtests/cycle-283-tap-subscribers/tap_1_record_spread.ops.json b/fieldtests/cycle-283-tap-subscribers/tap_1_record_spread.ops.json new file mode 100644 index 0000000..c06636f --- /dev/null +++ b/fieldtests/cycle-283-tap-subscribers/tap_1_record_spread.ops.json @@ -0,0 +1,13 @@ +[ + {"op": "source", "role": "price", "kind": "F64"}, + {"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 2}}}, + {"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 4}}}, + {"op": "feed", "role": "price", "into": ["fast.series", "slow.series"]}, + {"op": "add", "type": "Sub", "name": "sub"}, + {"op": "connect", "from": "fast.value", "to": "sub.lhs"}, + {"op": "connect", "from": "slow.value", "to": "sub.rhs"}, + {"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}}, + {"op": "connect", "from": "sub.value", "to": "bias.signal"}, + {"op": "tap", "from": "sub.value", "as": "spread"}, + {"op": "expose", "from": "bias.bias", "as": "bias"} +] diff --git a/fieldtests/cycle-283-tap-subscribers/tap_4_measure_ic.ops.json b/fieldtests/cycle-283-tap-subscribers/tap_4_measure_ic.ops.json new file mode 100644 index 0000000..e426076 --- /dev/null +++ b/fieldtests/cycle-283-tap-subscribers/tap_4_measure_ic.ops.json @@ -0,0 +1,15 @@ +[ + {"op": "source", "role": "price", "kind": "F64"}, + {"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 2}}}, + {"op": "add", "type": "SMA", "name": "slow", "bind": {"length": {"I64": 4}}}, + {"op": "add", "type": "SMA", "name": "px", "bind": {"length": {"I64": 1}}}, + {"op": "feed", "role": "price", "into": ["fast.series", "slow.series", "px.series"]}, + {"op": "add", "type": "Sub", "name": "sub"}, + {"op": "connect", "from": "fast.value", "to": "sub.lhs"}, + {"op": "connect", "from": "slow.value", "to": "sub.rhs"}, + {"op": "add", "type": "Bias", "name": "bias", "bind": {"scale": {"F64": 0.5}}}, + {"op": "connect", "from": "sub.value", "to": "bias.signal"}, + {"op": "tap", "from": "sub.value", "as": "signal"}, + {"op": "tap", "from": "px.value", "as": "price"}, + {"op": "expose", "from": "bias.bias", "as": "bias"} +] diff --git a/fieldtests/cycle-283-tap-subscribers/tap_consumer/.gitignore b/fieldtests/cycle-283-tap-subscribers/tap_consumer/.gitignore new file mode 100644 index 0000000..2c96eb1 --- /dev/null +++ b/fieldtests/cycle-283-tap-subscribers/tap_consumer/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock diff --git a/fieldtests/cycle-283-tap-subscribers/tap_consumer/Cargo.toml b/fieldtests/cycle-283-tap-subscribers/tap_consumer/Cargo.toml new file mode 100644 index 0000000..100940c --- /dev/null +++ b/fieldtests/cycle-283-tap-subscribers/tap_consumer/Cargo.toml @@ -0,0 +1,24 @@ +# A downstream World-program consumer of aura-runner's public tap-plan API. +# Detached from the aura workspace via an empty [workspace] table, exactly as a +# real research project's node crate is (docs/project-layout.md). It reaches the +# fold and live tap subscriptions — the two surfaces neither CLI verb exposes +# (both pass a record-all plan) — through run_signal_r + TapPlan. +[package] +name = "tap-consumer" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +aura-core = { path = "../../../crates/aura-core" } +aura-engine = { path = "../../../crates/aura-engine" } +aura-runner = { path = "../../../crates/aura-runner" } + +[[bin]] +name = "tap_2_fold" +path = "src/tap_2_fold.rs" + +[[bin]] +name = "tap_3_live" +path = "src/tap_3_live.rs" diff --git a/fieldtests/cycle-283-tap-subscribers/tap_consumer/examples/refusal_probe.rs b/fieldtests/cycle-283-tap-subscribers/tap_consumer/examples/refusal_probe.rs new file mode 100644 index 0000000..acf32b1 --- /dev/null +++ b/fieldtests/cycle-283-tap-subscribers/tap_consumer/examples/refusal_probe.rs @@ -0,0 +1,21 @@ +//! Ergonomics probe for the fold axis: what does a downstream *library* +//! consumer get when it subscribes an unknown fold label? A `Result` it can +//! handle, or a whole-process termination? (C28 tracks the ~24 process-exit +//! refusal sites as deferred #297.) +//! +//! Usage: cargo run --example refusal_probe -- + +use aura_engine::blueprint_from_json; +use aura_runner::member::{run_signal_r, RunData}; +use aura_runner::project::Env; +use aura_runner::{TapPlan, TapSubscription}; + +fn main() { + let json = std::fs::read_to_string(std::env::args().nth(1).unwrap()).unwrap(); + let env = Env::std(); + let signal = blueprint_from_json(&json, &|id| env.resolve(id)).unwrap(); + let mut plan = TapPlan::empty(); + plan.subscribe("signal", TapSubscription::named("median")); // not in the core roster + let _ = run_signal_r(signal, &[], RunData::Synthetic, 0, &env, plan); + println!("RETURNED NORMALLY (no refusal surfaced to the caller)"); +} diff --git a/fieldtests/cycle-283-tap-subscribers/tap_consumer/src/tap_2_fold.rs b/fieldtests/cycle-283-tap-subscribers/tap_consumer/src/tap_2_fold.rs new file mode 100644 index 0000000..0062a92 --- /dev/null +++ b/fieldtests/cycle-283-tap-subscribers/tap_consumer/src/tap_2_fold.rs @@ -0,0 +1,39 @@ +//! Axis 2 — fold subscription. A downstream consumer subscribes named core +//! folds (`mean`, `count`) against the declared taps of a blueprint and runs it +//! through the R scaffolding. Neither CLI verb can do this (both pass a +//! record-all plan), so this is the only reachable path to a fold. +//! +//! Usage: tap_2_fold +//! Run from a scratch dir; the fold rows land in ./runs/traces//. + +use aura_engine::blueprint_from_json; +use aura_runner::member::{run_signal_r, RunData}; +use aura_runner::project::Env; +use aura_runner::{TapPlan, TapSubscription}; + +fn main() { + let path = std::env::args().nth(1).expect("usage: tap_2_fold "); + let json = std::fs::read_to_string(&path).expect("read blueprint"); + + // Env::std() is the no-project context: every accessor collapses to the + // literal defaults (trace store rooted at ./runs). Its resolver doubles as + // the vocabulary resolver blueprint_from_json needs. + let env = Env::std(); + let signal = + blueprint_from_json(&json, &|id| env.resolve(id)).expect("parse blueprint"); + + // Build a per-tap plan: mean of the signal, count of the price. empty() = + // no default, so any tap left unsubscribed stays inert (C27). + let mut plan = TapPlan::empty(); + plan.subscribe("signal", TapSubscription::named("mean")); + plan.subscribe("price", TapSubscription::named("count")); + + let report = run_signal_r(signal, &[], RunData::Synthetic, 0, &env, plan); + + // The fold rows are persisted through the trace store, not returned in the + // report — so a consumer reads them back from ./runs/traces//. + println!("fold run complete (signal=mean, price=count)"); + if let Some(r) = report.metrics.r { + println!("report expectancy_r = {}", r.expectancy_r); + } +} diff --git a/fieldtests/cycle-283-tap-subscribers/tap_consumer/src/tap_3_live.rs b/fieldtests/cycle-283-tap-subscribers/tap_consumer/src/tap_3_live.rs new file mode 100644 index 0000000..79e0c73 --- /dev/null +++ b/fieldtests/cycle-283-tap-subscribers/tap_consumer/src/tap_3_live.rs @@ -0,0 +1,56 @@ +//! Axis 3 — live observation. A downstream consumer attaches an inline closure +//! to a declared tap and observes every (Timestamp, Cell) on the sim thread, +//! accumulating into shared state it reads after the run. No disk, no CLI path. +//! +//! Usage: tap_3_live + +use std::sync::{Arc, Mutex}; + +use aura_engine::blueprint_from_json; +use aura_runner::member::{run_signal_r, RunData}; +use aura_runner::project::Env; +use aura_runner::{TapPlan, TapSubscription}; + +#[derive(Default)] +struct Seen { + n: u64, + sum: f64, + first_ts: Option, + last_ts: i64, +} + +fn main() { + let path = std::env::args().nth(1).expect("usage: tap_3_live "); + let json = std::fs::read_to_string(&path).expect("read blueprint"); + + let env = Env::std(); + let signal = + blueprint_from_json(&json, &|id| env.resolve(id)).expect("parse blueprint"); + + let seen = Arc::new(Mutex::new(Seen::default())); + let sink = Arc::clone(&seen); + + let mut plan = TapPlan::empty(); + plan.subscribe( + "signal", + TapSubscription::live(move |ts, cell| { + let mut s = sink.lock().unwrap(); + s.n += 1; + s.sum += cell.f64(); + s.first_ts.get_or_insert(ts.0); + s.last_ts = ts.0; + }), + ); + + let _report = run_signal_r(signal, &[], RunData::Synthetic, 0, &env, plan); + + let s = seen.lock().unwrap(); + let mean = if s.n > 0 { s.sum / s.n as f64 } else { 0.0 }; + println!( + "live saw {} cycles, ts {}..={}, mean(signal)={}", + s.n, + s.first_ts.unwrap_or(-1), + s.last_ts, + mean + ); +}