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>
156 lines
9.5 KiB
Markdown
156 lines
9.5 KiB
Markdown
# Fieldtest — cycle-0006 (sink recording) — 2026-06-04
|
|
|
|
**Status:** Draft — awaiting orchestrator triage
|
|
**Author:** fieldtester (dispatched by fieldtest skill)
|
|
|
|
## Scope
|
|
|
|
Cycle 0006 settled "a sink is a role, not a type": recording is done by an
|
|
ordinary node in its `eval`, pushing a record to a destination it holds as a
|
|
field (channel / buffer / chart handle) — an out-of-graph side effect. Three
|
|
substrate changes shipped: (1) `Ctx::now() -> Timestamp` (a `Copy` accessor so a
|
|
recording node can causally stamp each record with the cycle timestamp); (2) the
|
|
old single `observe: usize` recording affordance was removed — `Harness::bootstrap`
|
|
dropped its 4th parameter and `Harness::run` returns `()` (retention is owned by
|
|
whoever holds the recording node's destination's read end); (3) the run loop is
|
|
otherwise an unchanged topological router. No `Sink` type, no `SinkHandler` trait,
|
|
no engine sink registry. The fieldtest exercised this from a standalone downstream
|
|
consumer crate (`fieldtests/cycle-0006-substrate/`) that `path`-depends on the
|
|
engine crates and uses only the public surface (rustdoc + specs + ledger).
|
|
|
|
## Examples
|
|
|
|
### fieldtests/cycle-0006-substrate/c0006_1_custom_node_now.rs — custom recording node via the Node contract
|
|
- Authors a `Recorder` node from scratch: `schema()` declares one f64 input +
|
|
`output: vec![]` (pure consumer); `eval` reads the typed window, calls
|
|
`ctx.now()`, sends `(now, value)` out of graph, returns `None`. Wired as
|
|
`source -> SMA(2) -> Recorder`.
|
|
- Fits the "author a custom node incl. `Ctx::now()`" axis.
|
|
- Outcome: built, ran, matched expected `[(20,11.0),(30,13.0),(40,15.0)]` on first try.
|
|
|
|
### fieldtests/cycle-0006-substrate/c0006_2_fanout_dag.rs — multi-stage fan-out / fan-in DAG, bootstrapped + run
|
|
- `source -> {SMA(2), SMA(4)} -> Sub(fast-slow) -> Recorder`. Uses the post-0006
|
|
3-arg `bootstrap` (no `observe`) and `run() -> ()`; retention via the channel.
|
|
- Fits the "fan-out/fan-in DAG" and "bootstrap arity / run returns ()" axes.
|
|
- Outcome: built, ran, matched `[(4,2.0),(5,2.0),(6,2.0)]` on first try.
|
|
|
|
### fieldtests/cycle-0006-substrate/c0006_3_multi_sink_record.rs — many interior streams, one run (headline)
|
|
- Two independent `Recorder` nodes tap SMA(2) and SMA(4) in one harness, draining
|
|
to two channels into `Vec<(Timestamp, Vec<Scalar>)>` (the spec's stated drain
|
|
shape). Asserts both streams correct AND of different length (sparse, timestamped,
|
|
no shared cycle index).
|
|
- Fits the "record multiple distinct interior streams of one run" axis (the cycle
|
|
headline; impossible before 0006).
|
|
- Outcome: built, ran, both streams matched (4 vs 2 records) on first try.
|
|
|
|
### fieldtests/cycle-0006-substrate/c0006_4_determinism.rs — byte-identical recorded output across two runs
|
|
- Two fresh harnesses, identical input through `{SMA(2),SMA(4)} -> Sub -> Recorder`;
|
|
compares recorded `f64::to_bits()` and timestamps for true byte-identity.
|
|
- Fits the "verify determinism (C1)" axis.
|
|
- Outcome: built, ran, byte-identical on first try.
|
|
|
|
### fieldtests/cycle-0006-substrate/c0006_5_reject_mismatch.rs — mis-wired recording edge rejected
|
|
- An i64 producer field bound into a recorder's f64 slot; asserts bootstrap returns
|
|
`BootstrapError::KindMismatch { producer: I64, consumer: F64 }` before any data
|
|
flows. Correct behaviour here is rejection.
|
|
- Fits the recording axis from the failure side (recording adds no new wiring hole).
|
|
- Outcome: built, ran, rejected with the documented error on first try.
|
|
|
|
## Findings
|
|
|
|
### [working] Custom recording node + `Ctx::now()` is natural and correct
|
|
- Examples: c0006_1, c0006_3, c0006_5.
|
|
- What happened: implementing `Node` for a user type, declaring `output: vec![]`,
|
|
reading `ctx.f64_in(0)`, calling `ctx.now()`, performing an mpsc side effect, and
|
|
returning `None` all worked verbatim from the rustdoc signatures and the 0006
|
|
spec's worked example. `Ctx::now()` returned the present cycle's timestamp on every
|
|
fired cycle; recorded streams were sparse + timestamped exactly as documented.
|
|
- Why working: the new surface was reached for, used as designed, and correct on the
|
|
first run with no surprises.
|
|
- Recommended action: carry-on.
|
|
|
|
### [working] The cycle headline — many interior streams from one run
|
|
- Example: c0006_3.
|
|
- What happened: two recorders tapped two different-rate indicators in one harness;
|
|
drained streams were individually correct and independently lengthed (4 vs 2). The
|
|
thing the cycle exists to enable (more than one recorded stream per run) works.
|
|
- Why working: this is the acceptance evidence the spec promised, reproduced
|
|
empirically by a downstream consumer.
|
|
- Recommended action: carry-on.
|
|
|
|
### [working] Post-0006 `Harness` API (3-arg bootstrap, `run() -> ()`) + determinism + rejection
|
|
- Examples: c0006_2 (3-arg bootstrap, `run` returns `()`), c0006_4 (byte-identical
|
|
re-run), c0006_5 (`KindMismatch` rejection).
|
|
- What happened: the shrunk surface is exactly as the rustdoc/ledger state; a
|
|
consumer holding the channel's read end owns retention with no engine help.
|
|
Determinism held bit-exactly; a mis-wired recorder edge was rejected at bootstrap.
|
|
- Why working: the substrate shrink introduced no regression and no forbidden
|
|
failure class (C1/C7 hold from the consumer's vantage).
|
|
- Recommended action: carry-on.
|
|
|
|
### [friction] Standalone consumer crate fights the workspace resolver
|
|
- Example: all (the fixture crate's `Cargo.toml`).
|
|
- What happened, verbatim:
|
|
`error: current package believes it's in a workspace when it's not: ... this may
|
|
be fixable by adding ... Alternatively, to keep it out of the workspace, add ...
|
|
an empty [workspace] table to the package's manifest.`
|
|
- Resolution taken: added an empty `[workspace]` table to the fixture manifest (the
|
|
keep-it-out option Cargo itself lists; no edit to the engine root manifest). After
|
|
that, all five binaries built and ran.
|
|
- Why friction: a real downstream research project (C16: "a project is always a Rust
|
|
crate" depending on aura) created *inside* or beside this repo hits the same wall.
|
|
The task completed, but only after a non-obvious one-liner. This is the
|
|
resolver-fight the carrier explicitly asked be recorded.
|
|
- Recommended action: plan — note in onboarding/docs (or the future `aura new`
|
|
scaffolder, an open ledger thread) that a project crate needs its own `[workspace]`
|
|
root when nested under the engine repo.
|
|
|
|
### [friction] Recorder boilerplate is hand-rewritten per author
|
|
- Examples: all five (each redeclares a near-identical `Recorder`: a `tx` field, a
|
|
`schema` with `output: vec![]`, an `eval` that reads inputs by kind, sends, returns
|
|
`None`).
|
|
- What happened: the cycle deliberately ships no `Recorder`/sink library type (C9 —
|
|
no speculative surface; the spec calls `Recorder` "this cycle's test-local
|
|
fixture, not a shipped type"). So every consumer writes the channel-sink dance,
|
|
including the per-kind `match` to pull the newest value of each input.
|
|
- Why friction: it is redundant across authors and the 0006 spec implies a real
|
|
author writes "their ChartSink, their RegistrySink" each from scratch. The cycle's
|
|
scope intentionally excludes a library sink, so this is expected friction, not a
|
|
bug — but it is the natural candidate for the next tidy.
|
|
- Recommended action: plan — consider an `aura-std` channel/buffer recording block
|
|
(or a small `Recorder` helper) once a second consumer confirms the shape, so the
|
|
boilerplate is written once. Not urgent; C9 correctly kept it out of this cycle.
|
|
|
|
### [spec_gap] No public statement of the canonical sink declaration / `Some`-with-empty-output
|
|
- Examples: c0006_1, c0006_3, c0006_5 (all declare `output: vec![]`).
|
|
- What happened: I needed the canonical way to declare a no-output (pure consumer)
|
|
node. The public surface (rustdoc for `NodeSchema`/`Node`; the 0006 spec) shows
|
|
`output: vec![]` only implicitly inside the worked example's prose ("a node with
|
|
`output: vec![]`"); neither the rustdoc nor the ledger C8 entry states outright
|
|
that an empty `output` vec is *the* sink declaration, nor what the engine does if a
|
|
node with `output: vec![]` nonetheless returns `Some(row)` (ignored? debug-asserts
|
|
on a width mismatch? UB?). I picked `output: vec![]` + return `None` — it matched
|
|
the worked example and ran correctly — but the "what if a sink returns `Some`"
|
|
corner is unconstrained by the public surface, and I did not probe it (probing the
|
|
engine's reaction would require reading implementation source, which ends the
|
|
test).
|
|
- Why spec_gap: the design surface is silent on a corner a downstream author can hit
|
|
by mistake; the natural reading worked but another (a sink that records *and*
|
|
forwards via `output: vec![]` + `Some`) is left undefined publicly.
|
|
- Recommended action: tighten the design ledger / rustdoc — state explicitly that
|
|
`output: vec![]` is the pure-consumer declaration and define the contract for a
|
|
`Some` return paired with an empty `output` (reject at bootstrap, debug-assert, or
|
|
documented-ignore). The C8 "both" case is documented for a node with a real output
|
|
port; the empty-output-plus-`Some` mistake is not.
|
|
|
|
## Recommendation summary
|
|
|
|
| Finding | Action |
|
|
|---|---|
|
|
| [working] custom node + `Ctx::now()` | carry-on |
|
|
| [working] many interior streams, one run (headline) | carry-on |
|
|
| [working] 3-arg bootstrap / `run()->()` / determinism / rejection | carry-on |
|
|
| [friction] consumer crate fights workspace resolver | plan |
|
|
| [friction] recorder boilerplate rewritten per author | plan |
|
|
| [spec_gap] canonical sink declaration / `Some`-with-empty-output undefined | tighten the design ledger |
|