fieldtest: tap-subscribers — 5 examples, 0 bugs / 2 friction / 3 spec-gap
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)
This commit is contained in:
@@ -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"}
|
||||||
|
]
|
||||||
@@ -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"}
|
||||||
|
]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
target/
|
||||||
|
Cargo.lock
|
||||||
@@ -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"
|
||||||
@@ -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 -- <blueprint.json>
|
||||||
|
|
||||||
|
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)");
|
||||||
|
}
|
||||||
@@ -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 <blueprint.json>
|
||||||
|
//! Run from a scratch dir; the fold rows land in ./runs/traces/<name>/.
|
||||||
|
|
||||||
|
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 <blueprint.json>");
|
||||||
|
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/<name>/.
|
||||||
|
println!("fold run complete (signal=mean, price=count)");
|
||||||
|
if let Some(r) = report.metrics.r {
|
||||||
|
println!("report expectancy_r = {}", r.expectancy_r);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <blueprint.json>
|
||||||
|
|
||||||
|
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<i64>,
|
||||||
|
last_ts: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let path = std::env::args().nth(1).expect("usage: tap_3_live <blueprint.json>");
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user