fieldtest: bound-override cycle — 4 examples, 6 findings (4 working, 1 friction, 1 bug)

Per-cycle fieldtest over the #246 surface, consumer-perspective, public
interface only: the scaffold quickstart end to end (aura new -> run ->
--list-axes -> override sweep), --list-axes on a partially-open
blueprint, an override family reproduced bit-identically (3/3 members),
and the error surface (wrong axis name, kind-mismatched value — both
exit 2). Corpus under fieldtests/cycle-246-bound-override/.

Dispositions: the one bug (F6) is fixed in this commit — authoring-guide
§1 still asserted a bound param 'never shows up in --list-axes' and 'no
aura sweep --axis can reopen it', contradicting the shipped #246
semantics and the C12 amendment; the three-states paragraph now reads
open = must-bind, bound = overridable default. The friction finding (F5,
KindMismatch/MissingKnob printed as raw Debug structs beside polished
prose) is filed as #247.

refs #246
This commit is contained in:
2026-07-13 04:50:36 +02:00
parent 9d4e4c7897
commit d7874e2569
9 changed files with 128 additions and 7 deletions
+9 -7
View File
@@ -259,14 +259,16 @@ purpose — three open campaign axes, see §3. This is the milestone fieldtest
corpus's own example, verified below; byte-identical to the on-disk corpus's own example, verified below; byte-identical to the on-disk
`fieldtests/milestone-research-artifacts/mra_1_strategy_smacross.json`.) `fieldtests/milestone-research-artifacts/mra_1_strategy_smacross.json`.)
A `bind` in an `add` op pins that param to a fixed value and **removes it A `bind` in an `add` op pins that param to a value and removes it from the
from the sweepable param space**: a bound param is closed — it never shows **open** param space (`--params`): a bound param is a **default** (#246) — a
up in `--params`/`--list-axes`, and no campaign axis and no `aura sweep run uses it as-is, while any campaign axis or `aura sweep --axis` naming it
--axis` can reopen it. `bind` is for a constant the strategy never varies; re-opens it for that family and binds it per cell. `--list-axes` lists it
leave a param unbound, as all three are here, to keep it open for sweeping. after the open knobs as `<name>:<KIND> default=<value>`. `bind` is for a
value the strategy carries by default; leave a param unbound, as all three
are here, to make binding it mandatory for every sweep.
A param therefore has three states: **open** (a sweepable axis), **bound** A param therefore has three states: **open** (an axis every sweep MUST bind),
(closed by a `bind` — gone from the axis namespace), and **ganged** (open, but **bound** (a default any axis MAY override, #246), and **ganged** (open, but
fused with its siblings under ONE public knob declared by a `gang` op; the fused with its siblings under ONE public knob declared by a `gang` op; the
member addresses are unbindable and only the gang's own single-segment name — member addresses are unbindable and only the gang's own single-segment name —
wrapped like any knob, e.g. `graph.channel_length` — appears in `--list-axes`). wrapped like any knob, e.g. `graph.channel_length` — appears in `--list-axes`).
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"graph","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
@@ -0,0 +1,12 @@
[
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "SMA", "name": "fast"},
{"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"},
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]
@@ -0,0 +1,85 @@
Field test — cycle-246-bound-override — verbatim transcript
Binary: target/debug/aura (built from HEAD 9d4e4c7, `cargo build -p aura-cli`)
All runs synthetic (no --real). Project = the scaffolded signal-lab/.
=====================================================================
EXAMPLE 1 — quickstart end to end (axis 1), exactly as scaffolded CLAUDE.md
=====================================================================
$ aura new signal-lab
created project "signal-lab" (data-only; attach native nodes later with `aura nodes new`)
# scaffold ships ONE blueprint: signal-lab/blueprints/signal.json (all params bound)
# CLAUDE.md quickstart advertises, verbatim:
# Run: aura run blueprints/signal.json
# Sweep: aura sweep blueprints/signal.json --axis signal_lab_signal.fast.length=2,4,8
# (aura sweep <bp> --list-axes lists open + bound-overridable axes)
$ aura run blueprints/signal.json # [exit 0]
{"manifest":{...,"params":[],...},"metrics":{"total_pips":0.3418...,"r":{"expectancy_r":1.271...,"n_trades":3,...}}}
$ aura sweep blueprints/signal.json --list-axes # [exit 0]
signal_lab_signal.fast.length:I64 default=2
signal_lab_signal.slow.length:I64 default=4
signal_lab_signal.bias.scale:F64 default=0.5
$ aura sweep blueprints/signal.json --axis signal_lab_signal.fast.length=2,4,8 # [exit 0]
{"family_id":"sweep-0","report":{"manifest":{"params":[["signal_lab_signal.fast.length",{"I64":2}],["stop_length",{"I64":3}],["stop_k",{"F64":2.0}]],...}}}
{"family_id":"sweep-0","report":{"manifest":{"params":[["signal_lab_signal.fast.length",{"I64":4}],...]}}}
{"family_id":"sweep-0","report":{"manifest":{"params":[["signal_lab_signal.fast.length",{"I64":8}],...]}}}
# bound fast.length overridden per member; slow.length + bias.scale stay at their bound defaults.
=====================================================================
EXAMPLE 2 — --list-axes on a partially-open blueprint (axis 2)
=====================================================================
# op-script bo_2_partial_open_opscript.json binds slow.length=4, leaves fast.length + bias.scale open.
$ aura graph build < bo_2_partial_open_opscript.json > bo_2_partial_open.json # [exit 0] (name = "graph")
$ aura graph introspect --params bo_2_partial_open.json # [exit 0] (raw namespace, OPEN only)
fast.length:I64
bias.scale:F64
$ aura sweep bo_2_partial_open.json --list-axes # [exit 0]
graph.fast.length:I64 # open knobs first, NO default=
graph.bias.scale:F64
graph.slow.length:I64 default=4 # bound param last, WITH default=
# bound is optional (uses default); open is still required:
$ aura sweep blueprints/partial.json --axis graph.fast.length=2,4 --axis graph.bias.scale=0.5 --name bo2d # [exit 0]
# 2 members run; slow.length omitted -> stays at default 4 (absent from manifest params).
$ aura sweep blueprints/partial.json --axis graph.fast.length=2,4 # [exit 2]
aura: MissingKnob("graph.bias.scale") # <-- raw Debug enum print (see finding F5)
=====================================================================
EXAMPLE 3 — override sweep + reproduce (axis 3)
=====================================================================
$ aura sweep blueprints/signal.json --axis signal_lab_signal.fast.length=2,4,8 --name bo3-override # [exit 0]
$ aura runs families # [exit 0]
{"family_id":"sweep-0","kind":"Sweep","members":3}
{"family_id":"bo2d-0","kind":"Sweep","members":2}
{"family_id":"bo3-override-0","kind":"Sweep","members":3}
$ aura reproduce bo3-override-0 # [exit 0] (help says "content id"; the name works too)
bo3-override-0 member signal_lab_signal.fast.length=2, stop_length=3, stop_k=2 reproduced: bit-identical
bo3-override-0 member signal_lab_signal.fast.length=4, stop_length=3, stop_k=2 reproduced: bit-identical
bo3-override-0 member signal_lab_signal.fast.length=8, stop_length=3, stop_k=2 reproduced: bit-identical
reproduced 3/3 members bit-identically
=====================================================================
EXAMPLE 4 — error surface (axis 4)
=====================================================================
$ aura sweep blueprints/signal.json --axis signal_lab_signal.nonexistent=1,2 # [exit 2] CLEAN
aura: axis signal_lab_signal.nonexistent: names no param of this blueprint (open or bound) — see `aura sweep <bp> --list-axes`
$ aura sweep blueprints/signal.json --axis totally.bogus=1 # [exit 2] CLEAN (retired "nothing to sweep" gone)
aura: axis totally.bogus: names no param of this blueprint (open or bound) — see `aura sweep <bp> --list-axes`
$ aura sweep blueprints/signal.json --axis signal_lab_signal.fast.length=1.5,2.5 # [exit 2] RAW DEBUG (finding F5)
aura: KindMismatch { knob: "signal_lab_signal.fast.length", expected: I64, got: F64 }
# run/mc still refuse open blueprints:
$ aura run blueprints/partial.json # [exit 2]
aura: run requires a closed blueprint (no free parameters); 2 free knob(s) — bind them or use `aura sweep --axis`
$ aura mc blueprints/partial.json # [exit 2] (generic usage error, not the open-param prose)
$ aura run blueprints/signal.json # [exit 0] (all bound = closed)
@@ -0,0 +1 @@
/runs
@@ -0,0 +1,4 @@
# Static project context only (C17); paths only.
[paths]
runs = "runs"
# data = "/path/to/archive" # the recorded-data root; defaults to the built-in path
@@ -0,0 +1,14 @@
# signal-lab — an aura research project (data-only)
This directory is an aura project: blueprints + research documents over the
std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
- Run: `aura run blueprints/signal.json` (the starter is closed — all
params bound; bound values are defaults — any `--axis` may override them
(#246))
- Sweep: `aura sweep blueprints/signal.json --axis signal_lab_signal.fast.length=2,4,8`
(`aura sweep <bp> --list-axes` lists the open + bound-overridable axes)
- Native nodes: when the project needs its first project-specific node,
`aura nodes new <name>` scaffolds a node crate beside this project and
attaches it via `[nodes]` in `Aura.toml` (build it with `cargo build`).
- Topology is data (`blueprints/*.json`); results land in `runs/`.
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"graph","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub","name":"sub"}},{"primitive":{"type":"Bias","name":"bias"}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"signal_lab_signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}