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:
2026-07-22 00:40:04 +02:00
parent bd0c557f16
commit 8c19260e8d
7 changed files with 170 additions and 0 deletions
@@ -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
);
}