Files
Aura/docs/specs/fieldtest-0009-run-metrics.md
T
Brummel a982b96ecc fieldtest: cycle-0009 — 4 examples, 6 findings
First fieldtest of the run-metrics + manifest report surface. A standalone
downstream-consumer crate (fieldtests/cycle-0009-run-metrics/) path-depends on
the engine crates and exercises the post-0009 surface from the public interface
only (rustdoc + ledger + glossary + project layout, never crates/*/src).

Primary axis empirically met: the north-star "a run emits metrics + manifest"
move is reachable from rustdoc alone — drain two recording sinks -> f64_field ->
summarize -> RunManifest -> to_json, metrics matching the hand model on the first
run, deterministic across reruns.

Findings: 0 bugs, 1 friction, 1 spec_gap, 4 working.
  - working x4: north-star reachable from rustdoc; SimBroker firing/slot docs (a
    resolved 0007 gap) now carry the example; summarize metric definitions exact
    on six degenerate inputs (incl. negative-curve drawdown + flat-as-sign-0);
    f64_field panics precise and well-located.
  - spec_gap: to_json's JSON key names + {manifest,metrics} nesting are not on
    the public surface — a consumer parsing the JSON (C18 registry, the deferred
    aura run printer) cannot author against it from rustdoc alone.
  - friction: to_json renders whole-valued f64 without a decimal point (3.0 ->
    "3"), so one f64 field appears as integer or decimal token within one schema.

Both doc-level findings are the same doc pass and matter mainly for the deferred
aura run (#8) and the C18 registry that will parse this JSON. Spec feeds the next
plan as reference.

refs #6
2026-06-04 19:06:13 +02:00

174 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Fieldtest — cycle-0009 (run metrics + manifest) — 2026-06-04
**Status:** Draft — awaiting orchestrator triage
**Author:** fieldtester (dispatched by fieldtest skill)
## Scope
Cycle 0009 (Walking-skeleton milestone, issue #6) shipped a pure-additive
`report` module in `aura-engine`, re-exported at the crate root:
`summarize(equity, exposure) -> RunMetrics` (fields `total_pips`,
`max_drawdown`, `exposure_sign_flips`); `f64_field(rows, field)` bridging a
recording sink's `Vec<Scalar>` rows to `summarize`; `RunManifest`
(`commit`, `params`, `window`, `seed`, `broker`); `RunReport { manifest,
metrics }` with `to_json() -> String` (the structured C14 face). The intended
downstream move: after `Harness::run`, drain recording sinks, project f64
columns with `f64_field`, `summarize` the pip-equity + exposure streams, pair
with a caller-built `RunManifest`, render `to_json`. The `aura run` CLI that
prints this is deferred to #8 and out of scope. Worked from the public interface
only (rustdoc via `cargo doc`, `docs/design/INDEX.md`, `docs/glossary.md`,
project layout, public re-exports); never `crates/*/src`.
Build/run: workspace built clean from HEAD (`cargo build --workspace`); each
example run via `cargo run --manifest-path
fieldtests/cycle-0009-run-metrics/Cargo.toml --bin <name>`, so HEAD source is
always what compiled and ran (a standalone non-workspace consumer crate
path-depending on the three engine crates, exactly as a C16 project would).
## Examples
### fieldtests/cycle-0009-run-metrics/c0009_1_run_to_report.rs — north-star run-to-report
- **What it does.** Bootstraps the full SMA(2)/SMA(4) → Sub → Exposure(4) →
SimBroker harness with TWO recording sinks (equity tapped on the broker,
exposure tapped on the Exposure node), runs a 7-tick rising-price stream,
drains both sinks, `f64_field`-projects each, `summarize`s, builds a
`RunManifest`, and renders `to_json`.
- **Why it fits.** This is the carrier's primary axis verbatim — the end-to-end
research-loop move the whole module exists to enable, reaching every new
surface element once.
- **Outcome.** Built, ran, matched the hand model exactly: equity
`[0,0,0,0,1,2,3]``total_pips=3.0`, `max_drawdown=0.0`; exposure all `+0.5`
`exposure_sign_flips=0`. `to_json` emitted a single well-formed object.
### fieldtests/cycle-0009-run-metrics/c0009_2_compare_two_runs.rs — comparison + determinism
- **What it does.** Runs the same harness twice at `Exposure` scale=4 (asserts
bit-identical metrics AND bit-identical `to_json` across the two runs), then a
third run at scale=2, and compares `total_pips`.
- **Why it fits.** The carrier's second axis: determinism (C1) plus a
smallest-slice tuning-sweep comparison (C12/C21) read off `RunMetrics`.
- **Outcome.** Built, ran, matched expected: scale=4 deterministic
(`total_pips=3`), scale=2 = `total_pips=6` (exactly double — exposure
saturates 0.5→1.0), both drawdown-free, no sign flips.
### fieldtests/cycle-0009-run-metrics/c0009_3_degenerate_streams.rs — degenerate summarize semantics
- **What it does.** Calls `summarize` with six hand-built `&[(Timestamp,f64)]`
argument literals: empty, monotonic-up, dip-and-recover, long/short/flat
sign-flips, same-sign varying magnitude, and an all-negative curve.
- **Why it fits.** The carrier's third axis: verify the documented metric
definitions from docs alone. The slices are the public function's direct
argument type — calling the function with literals, not authoring an
intermediate representation.
- **Outcome.** Built, ran, every documented definition held exactly: empty→
zeros; last-value `total_pips` (incl. negative); running-peak drawdown (incl.
on a negative curve, peak 1 → trough 5 = 4); sign-flips counting flat (sign
0) as distinct from long/short (3 flips), magnitude changes not counted.
### fieldtests/cycle-0009-run-metrics/c0009_4_f64field_and_json.rs — f64_field bridge + JSON shape
- **What it does.** Projects a non-zero column from a multi-column recorded row;
confirms (via `catch_unwind`) the documented panics on a non-f64 field and an
out-of-range field index; inspects the exact `to_json` byte shape under a
realistic FX `pip_size=0.0001`, fractional/negative metrics, and ns-scale
timestamps.
- **Why it fits.** Exercises the `f64_field` "checked at wiring" contract and
the `to_json` round-trippability claim — the corners of the north-star
surface a generic sink-drainer would hit.
- **Outcome.** Built, ran, matched: column-pick correct; both panics fired with
precise messages; JSON kept ns timestamps as bare integers (no sci-notation),
params in insertion order as a nested object.
## Findings
### [working] North-star run-to-report is reachable from rustdoc alone
- **Example(s):** c0009_1, c0009_2.
- **What happened.** The full drain → `f64_field``summarize`
`RunManifest``to_json` chain was authored entirely from the crate-root
rustdoc (`fn.summarize`, `fn.f64_field`, the three structs, `to_json`) plus
the prior fieldtest corpus for the harness wiring. Metrics matched the hand
model on the first run; `to_json` produced a clean object.
- **Why working.** The cycle's headline move is reachable and correct without
reading `src` — the module rustdoc names the post-run drain-and-fold workflow
explicitly, and the field docs are precise enough to predict every value.
- **Recommended action:** carry-on.
### [working] SimBroker firing/warm-up/slot-order docs (a resolved 0007 spec_gap) now carry the example
- **Example(s):** c0009_1.
- **What happened.** Predicting the equity sink's `[0,0,0,0,1,2,3]` shape
required knowing slot 0 = exposure / slot 1 = price, both `Firing::Any`, and
the "leading 0.0 rows, one row per price cycle" warm-up. All three are now on
`struct.SimBroker` rustdoc ("Input slots" / "Firing and warm-up"). The 0007
fieldtest had to recover these from the ledger + commit body and recorded them
as a spec_gap; they are now on the surface.
- **Why working.** A prior gap is closed and directly enabled this cycle's
north-star example to be authored from rustdoc.
- **Recommended action:** carry-on.
### [working] summarize metric definitions hold exactly on all degenerate inputs
- **Example(s):** c0009_3.
- **What happened.** All six hand-built cases matched the rustdoc field
definitions to the bit, including the two subtle ones: drawdown measured from
the running peak even when the whole curve is negative, and `exposure_sign_flips`
treating flat (0.0 → sign 0) as distinct from long/short.
- **Why working.** The `RunMetrics` field docs are unambiguous and empirically
exact; the "flat is sign 0" and "running-peak" subtleties are spelled out and
behave as written.
- **Recommended action:** carry-on.
### [working] f64_field panics are precise and well-located
- **Example(s):** c0009_4.
- **What happened.** `f64_field` on an `I64` field panicked with
`f64_field: field 0 is not an f64 scalar: I64(7)`; on an out-of-range index
with `f64_field: row has no field 5 (row width 2)`. Both match the documented
"checked at wiring" panic contract and name the exact offending field/width.
- **Why working.** The diagnostic is actionable and matches the rustdoc promise;
a downstream consumer mis-wiring a sink column gets a precise message.
- **Recommended action:** carry-on.
### [spec_gap] to_json key names / nesting are not on the public surface
- **Example(s):** c0009_1, c0009_4.
- **What happened.** `RunReport::to_json` rustdoc promises "field order is
fixed", "params renders as a JSON object in insertion order", "f64 uses the
round-trippable `{}` shortest form" — but it does **not** state the actual JSON
key names or the `{manifest:{...},metrics:{...}}` nesting. The observed shape
is `{"manifest":{"commit":..,"params":{..},"window":[from,to],"seed":..,
"broker":..},"metrics":{"total_pips":..,"max_drawdown":..,"exposure_sign_flips":..}}`.
I had to read the runtime output to learn the schema; a consumer parsing the
JSON (the C18 registry, the deferred `aura run` printer) cannot author against
it from rustdoc alone and would couple to an undocumented contract.
- **Why spec_gap.** The reading I guessed (keys = struct field names, `window`
as a 2-array, `params` as an object) was the natural one and proved correct,
but it is unconstrained by the public surface; another reading (e.g. `window`
as `{from,to}`, or top-level flattening) was equally plausible. For a
"machine-readable" (C14) face this schema is part of the contract.
- **Recommended action:** tighten the design ledger / rustdoc — add a documented
JSON schema example to `to_json` rustdoc (a one-line sample object suffices),
or ratify the field-name-mirroring as the stated contract.
### [friction] to_json renders whole-valued f64 without a decimal point (3.0 → `3`)
- **Example(s):** c0009_1, c0009_2.
- **What happened.** `total_pips: 3.0_f64` serializes as `"total_pips":3` (and
`4.0` param → `4`), whereas `-12.5`/`2.5` keep the point. This is the
documented round-trippable `{}` shortest form and is valid JSON (a number is a
number), so it is not a bug. But a downstream consumer string-matching or
schema-typing the field could trip: the same logical f64 field appears
sometimes as an integer token, sometimes as a decimal token, within one
schema. My example had to substring-match `"3"` rather than `"3.0"`.
- **Why friction.** The task completed, but the mixed integer/decimal rendering
of a single f64 field is a small surprise for a consumer expecting stable
per-field token shape; it interacts with the spec_gap above (no documented
schema to set the expectation).
- **Recommended action:** plan (tidy) — either document explicitly in the
`to_json` rustdoc that f64 fields may render without a fractional part, or (if
a stable decimal shape is wanted for the registry) normalize. Low urgency;
fold into the same doc pass as the spec_gap.
## Recommendation summary
| Finding | Class | Action |
|---|---|---|
| North-star reachable from rustdoc | working | carry-on |
| SimBroker firing/slot docs now carry the example | working | carry-on |
| summarize definitions exact on degenerate inputs | working | carry-on |
| f64_field panics precise + well-located | working | carry-on |
| to_json key names / nesting not on public surface | spec_gap | tighten the design ledger / rustdoc (or ratify) |
| to_json renders whole f64 as integer token | friction | plan (tidy doc pass) |