0bf15cb069
First fieldtest of the project. A standalone downstream-consumer crate
(fieldtests/cycle-0006-substrate/) path-depends on the engine crates and
exercises the post-0006 substrate from the public interface only (rustdoc +
specs + ledger, never crates/ source):
1 custom recording node via the Node contract + Ctx::now()
2 fan-out/fan-in DAG, 3-arg bootstrap, run() -> ()
3 two interior streams recorded from one run (the cycle headline)
4 byte-identical recorded output across two runs (C1 determinism)
5 mis-wired recording edge rejected (KindMismatch) at bootstrap
Findings: 3 working (carry-on), 2 friction, 1 spec_gap.
- friction: a nested standalone consumer crate fights the workspace
resolver (needs an empty [workspace] table — Cargo's own hint).
- friction: recorder boilerplate is hand-rewritten per author (C9
deliberately ships no library sink).
- spec_gap: the public surface never states output: vec![] is THE sink
declaration, nor defines a Some return paired with empty output.
Spec at docs/specs/fieldtest-0006-substrate.md feeds the 0007 plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
2.8 KiB
Rust
93 lines
2.8 KiB
Rust
//! Fieldtest c0006 #5 — a mis-wired recording edge is rejected at bootstrap.
|
|
//!
|
|
//! Supports the "record interior streams" axis from the failure side: a recording
|
|
//! node declares typed input slots like any consumer, so an edge whose producer
|
|
//! field kind mismatches the slot kind is rejected at bootstrap (the existing
|
|
//! per-field 0005 check; recording adds no new hole). Correct behaviour here is
|
|
//! REJECTION — this fixture asserts the rejection happens with the documented
|
|
//! error, before any data flows. Mirrors the 0006 spec's must-fail fixture.
|
|
|
|
use std::sync::mpsc;
|
|
|
|
use aura_core::{Ctx, FieldSpec, Firing, InputSpec, Node, NodeSchema, Scalar, ScalarKind, Timestamp};
|
|
use aura_engine::{BootstrapError, Edge, Harness, SourceSpec, Target};
|
|
|
|
/// A producer that emits one i64 field.
|
|
struct I64Source {
|
|
out: [Scalar; 1],
|
|
}
|
|
impl Node for I64Source {
|
|
fn schema(&self) -> NodeSchema {
|
|
NodeSchema {
|
|
inputs: vec![InputSpec {
|
|
kind: ScalarKind::I64,
|
|
lookback: 1,
|
|
firing: Firing::Any,
|
|
}],
|
|
output: vec![FieldSpec { name: "v", kind: ScalarKind::I64 }],
|
|
}
|
|
}
|
|
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
|
let w = ctx.i64_in(0);
|
|
if w.is_empty() {
|
|
return None;
|
|
}
|
|
self.out[0] = Scalar::I64(w[0]);
|
|
Some(&self.out)
|
|
}
|
|
}
|
|
|
|
/// A recorder declaring an f64 input slot.
|
|
struct Recorder {
|
|
tx: mpsc::Sender<(Timestamp, f64)>,
|
|
}
|
|
impl Node for Recorder {
|
|
fn schema(&self) -> NodeSchema {
|
|
NodeSchema {
|
|
inputs: vec![InputSpec {
|
|
kind: ScalarKind::F64,
|
|
lookback: 1,
|
|
firing: Firing::Any,
|
|
}],
|
|
output: vec![],
|
|
}
|
|
}
|
|
fn eval(&mut self, ctx: Ctx<'_>) -> Option<&[Scalar]> {
|
|
let w = ctx.f64_in(0);
|
|
if w.is_empty() {
|
|
return None;
|
|
}
|
|
let _ = self.tx.send((ctx.now(), w[0]));
|
|
None
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let (tx, _rx) = mpsc::channel();
|
|
|
|
// Bind the i64 producer field into the f64 recorder slot -> must be rejected.
|
|
let err = Harness::bootstrap(
|
|
vec![
|
|
Box::new(I64Source { out: [Scalar::I64(0)] }),
|
|
Box::new(Recorder { tx }),
|
|
],
|
|
vec![SourceSpec {
|
|
kind: ScalarKind::I64,
|
|
targets: vec![Target { node: 0, slot: 0 }],
|
|
}],
|
|
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
|
|
)
|
|
.unwrap_err();
|
|
|
|
println!("err = {err:?}");
|
|
assert_eq!(
|
|
err,
|
|
BootstrapError::KindMismatch {
|
|
producer: ScalarKind::I64,
|
|
consumer: ScalarKind::F64,
|
|
},
|
|
"kind-mismatched recorder edge must be rejected at bootstrap"
|
|
);
|
|
println!("c0006_5 OK: mis-wired recording edge rejected with KindMismatch");
|
|
}
|