# Fieldtest — milestone: Topology-as-data (C24) — 2026-06-30

**Status:** Draft — awaiting orchestrator triage
**Author:** fieldtester (dispatched by fieldtest skill)

## Scope

The milestone-close gate for **C24 — the blueprint is a serializable,
World-owned data value**. The milestone promises a downstream researcher can run
the whole topology-as-data story end to end: author a param-generic graph with
the Rust builder, **serialize** it to a canonical versioned data value,
**load** it back (data → blueprint → runnable), **construct** the same topology
declaratively through the CLI op-script (`aura graph build`), **introspect** the
closed node vocabulary without a build (`aura graph introspect`), and
**reproduce** a run bit-identically (C1). Spanning cycles 0087 (#155 serde
first-cut), 0088 (#157 construction service), 0089 (#161/#162 diagnostics +
acyclicity), 0090 (#156 forward-compat two-tier).

Run entirely from the public interface: the C24 ledger entry, `cargo doc`
doc-comments, `aura graph introspect`'s own output, and the existing op-script
fixture *shape*. No `crates/*/src` was read. Artefact under test:
`target/debug/aura` + the engine path-dep crates, both rebuilt from HEAD
(`cargo build --workspace`) before the run.

## Examples

All fixtures under `fieldtests/milestone-topology-as-data/`.

### mt_1_roundtrip.rs — Rust round-trip + run identity (tasks 1)
- Authors an SMA(2)/SMA(4)-cross bias signal with `GraphBuilder`
  (`source_role` → `Sma::builder().named().bind()` → `Sub` → `Bias` → `expose`),
  serializes with `blueprint_to_json`, loads with `blueprint_from_json` +
  `aura_std::std_vocabulary`, re-serializes, and runs both the original and the
  reloaded twin through an identical Recorder harness.
- Fits scope: the headline C24 acceptance (serialize → load → reproduce).
- Outcome: **built, ran, matched.** (a) canonical JSON round-trips
  byte-identically (647 bytes, serialize→load→re-serialize equal). (b) 9
  recorded bias rows value-for-value identical between original and reload (C1).

### mt_2_sma_cross.ops.json + mt_2_blueprint.cli.json — declarative construction + cross-surface identity (task 2)
- The same topology as a CLI op-script; `aura graph build < ops.json` emits the
  #155 blueprint. Compared against `mt_1_rust_blueprint.json` (the library
  emit).
- Fits scope: C1 identity across authoring surfaces (Rust builder vs op-script).
- Outcome: **built, ran, matched (modulo one byte).** The two blueprints are
  **character-for-character identical** except the CLI appends a trailing `\n`
  (648 vs 647 bytes). See spec_gap below.

### mt_3_*.out + mt_3_partial.ops.json — build-free introspection (task 3)
- `introspect --vocabulary` (22 closed types), `--node SMA|Sub|Bias|EMA|RollingMax`
  (ports, kinds, bind form), and `--unwired` on a partial op-list.
- Fits scope: discover the vocabulary / a node's ports / unwired slots without a
  build.
- Outcome: **ran, matched.** `--unwired` on a partial doc reports `sub.rhs:F64`
  (the one open slot, by identifier, no build). `--node LinComb`/`Recorder`
  correctly refuse (`unknown node type`, exit 2) — they are outside the
  round-trippable set, as the ledger states.

### mt_4_forward_compat.rs — forward-compat Tier-1/Tier-2 + cross-surface interop (tasks 4)
- Loads the **CLI-emitted** blueprint via the **library** loader (cross-surface
  interop); injects unknown optional fields at envelope and node level (Tier-1);
  bumps `format_version` to 999 and swaps a node type to `FOO` (Tier-2).
- Fits scope: the #156 two-tier discipline from the loader's seat.
- Outcome: **built, ran, matched.** CLI blueprint loaded + ran identically
  (trailing newline tolerated). Tier-1 unknown fields tolerated, dropped on
  re-emit (back to canonical), ran bit-identical. Tier-2 refused **by name** —
  `UnsupportedVersion { found: 999, supported: 1 }` and `UnknownNodeType("FOO")`
  — no panic, no silent wrong graph.

### mt_5{a,b,c}_*.ops.json + .err/.exit — fail-fast CLI diagnostics (task 5)
- Three malformed op-lists fed to `aura graph build`: unknown node type, a
  connect that closes a cycle, an unconnected input slot.
- Fits scope: invalid construction must fail fast, by identifier, non-zero exit.
- Outcome: **ran, matched.** Each reported by identifier with empty stdout and
  exit 2 (verbatim below).

## Findings

### [working] The C24 round-trip is real and bit-exact, both surfaces
- Examples: mt_1, mt_2, mt_4.
- What happened: a builder-authored signal serialized → loaded → re-serialized
  byte-identically (647 bytes) and the reload ran value-for-value identical to
  its twin (9 bias rows). The CLI op-script emitted a blueprint whose **content
  is character-identical** to the library emit, and that CLI blueprint loaded
  through the library loader and ran identically.
- Why working: the milestone's central promise (author → serialize → load →
  reproduce, identical across the Rust and CLI authoring surfaces) holds on
  first try from the public surface alone.
- Recommended action: carry-on. These are the C24 acceptance pins, empirically
  confirmed downstream.

### [working] Build-free introspection answers the three discovery questions
- Examples: mt_3.
- What happened: `--vocabulary` listed the closed 22-type set; `--node`
  surfaced ports/kinds **and** the exact bind form (`param length:I64 (bind
  {"I64": <v>})`); `--unwired` named the single open slot `sub.rhs:F64` with no
  build. Out-of-vocabulary `--node LinComb`/`Recorder` refused cleanly (exit 2).
- Why working: a consumer can discover the entire authorable surface (what nodes
  exist, each node's ports, what a partial doc still needs) without compiling
  anything — exactly the #157 introspection promise.
- Recommended action: carry-on.

### [working] Forward-compat + fail-fast diagnostics are by-name / by-identifier
- Examples: mt_4, mt_5.
- What happened: Tier-1 unknown fields tolerated and dropped on re-emit; Tier-2
  refused by name with values (`found: 999, supported: 1`; `"FOO"`). The three
  build failures each named the cause at the offending site:
  `op 1 (add): unknown node type "FOO"`;
  `op 5 (connect): connecting b.value -> a.rhs would close a cycle`;
  `finalize: slot sub.rhs is unconnected` — all exit 2, all with empty stdout
  (no half-emitted blueprint).
- Why working: every refusal is actionable and points at an identifier, never a
  raw index or a panic — the #161/#162 polish landed and holds downstream.
- Recommended action: carry-on.

### [friction] The construction surface is undiscoverable from the CLI's own help
- Examples: observed probing `aura --help`, `aura graph --help`, `aura graph`.
- What happened: top-level usage lists only `aura graph` (no `build` /
  `introspect`). `aura graph --help` reprints the same top-level usage. `aura
  graph` with no subcommand silently streams a ~1.4 MB HTML viewer to stdout and
  exits 0. The only way I learned `build`/`introspect` existed was the C24
  ledger; the only way I learned the introspect flags was triggering its usage
  line by passing a bad arg.
- Why friction: the task completed, but a consumer with only the binary cannot
  find the milestone's headline CLI surface. (Known/filed as #159.)
- Recommended action: plan — add a `graph` subcommand help listing
  `build | introspect` and their flags; make bare `aura graph` print usage
  rather than dump HTML to a TTY.

### [friction] The op-script JSON schema is documented nowhere durable; the only example is stale
- Examples: mt_2 (and the failed build of `fieldtests/cycle-0088-…/c0088_1`).
- What happened: no durable public doc (ledger, glossary, README) states the
  op-list grammar — the verbs (`source`/`add`/`feed`/`connect`/`expose`) or
  their keys. `aura graph build --help` does not exist (empty input just prints a
  JSON parse error). The only worked example is the cycle-0088 fixture corpus,
  which still uses the **retired `as` key** for `add`; replaying it verbatim
  fails with `op 2 (add): duplicate identifier "sma"` because `add`'s naming key
  was renamed `as → name` (commit `feat: construction add names a node via name`).
  A consumer copying the visible example hits this immediately.
- Why friction: I had to reconstruct the correct schema (`"name"` not `"as"` for
  `add`; `"as"` retained only for `expose`) by cross-reading the commit log — a
  downstream consumer reading docs + the example corpus would be misled.
- Recommended action: plan / docwriter — document the op-script grammar in a
  durable doc (or `aura graph build --help`), and either refresh or clearly
  date-stamp the cycle-0088 corpus as pre-rename.

### [friction] Closing the loop to a *run* drops out of the data surface into hand-wired Rust
- Examples: mt_1, mt_4 (the Recorder wrapper).
- What happened: the serializable unit is the sink-less signal composite (a
  Recorder captures an `mpsc::Sender`, deliberately outside the round-trippable
  set). To *observe* a reloaded blueprint actually running, I had to drop to
  Rust, hand-author a wrapper `Composite` by raw index, attach a `Recorder`, and
  bootstrap. There is no `aura graph run <blueprint.json>` (cf. #28): the CLI can
  author and emit topology but cannot run an emitted blueprint or show its
  trace.
- Why friction: the milestone's "data → blueprint → **runnable**" is
  library-complete but, from the CLI alone, dead-ends at the emitted JSON; the
  reproduce step requires writing Rust and re-introducing a non-serializable
  sink. (Intended per the ledger, but a real seam a consumer feels.)
- Recommended action: plan — track under the #28/#159 build→consume edge (a CLI
  consumer that loads a blueprint, attaches a sink/tap, and runs).

### [spec_gap] "Canonical" form: CLI `graph build` appends a trailing newline the library emit omits
- Examples: mt_2 (`mt_2_blueprint.cli.json` 648 B vs `mt_1_rust_blueprint.json`
  647 B; last bytes `5d 7d 7d 0a` vs `7d 5d 7d 7d`).
- What happened: both paths emit the documented "canonical, versioned" blueprint,
  byte-identical in content, but the CLI frames it with a trailing `\n` and the
  library `blueprint_to_json` does not. The ledger calls the format canonical and
  lists **content-addressed topology identity in the manifest (#158)** as
  remaining — yet a content hash of the same blueprint differs by which surface
  emitted it.
- Why spec_gap: the ledger does not constrain whether the canonical artifact
  includes a trailing newline; I picked the reading "the JSON content is
  canonical, the CLI's newline is incidental framing," but the reading "the CLI's
  stdout *is* the canonical on-disk artifact" is equally plausible, and the two
  disagree by one byte — load-bearing once #158 content-addresses topology.
- Repro: `diff <(aura graph build < mt_2_sma_cross.ops.json)
  mt_1_rust_blueprint.json` → differs only by the final newline.
- Recommended action: ratify / tighten the ledger — state whether canonical
  includes a trailing newline and make the two emit paths byte-identical before
  #158 lands.

### [spec_gap] `Composite` (the loaded value) has no `Debug` / `PartialEq`
- Examples: mt_4 (a `match` had to avoid `{:?}`-ing the `Ok(Composite)` arm; a
  direct `assert_eq!` on two loaded composites is impossible).
- What happened: `blueprint_from_json` returns `Result<Composite, LoadError>`,
  but `Composite` derives neither `Debug` nor `PartialEq`, so `unwrap_err()`,
  `{:?}`, and composite-level equality assertions don't compile. A consumer must
  route every identity/round-trip assertion through `blueprint_to_json` (string
  compare), which is what I did.
- Why spec_gap: the ledger frames the blueprint as a first-class data **value**
  the World "owns, stores, mutates, structurally searches" — value-like
  ergonomics (compare, debug-print) are implied but unspecified, and the type as
  shipped supports neither.
- Recommended action: ratify / tighten — decide whether the public `Composite`
  value should carry `Debug`/`PartialEq` (string-via-JSON is the current
  contract); document the intended equality surface.

## Recommendation summary

| Finding | Class | Action |
|---|---|---|
| C24 round-trip bit-exact, both surfaces | working | carry-on |
| Build-free introspection (vocab/node/unwired) | working | carry-on |
| By-name / by-identifier forward-compat + fail-fast | working | carry-on |
| Construction surface undiscoverable from CLI help | friction | plan (#159) |
| Op-script schema undocumented; example stale on `as`→`name` | friction | plan / docwriter |
| Run-the-loop requires hand-wired Rust sink (no `graph run`) | friction | plan (#28/#159) |
| Trailing-newline divergence vs "canonical" / #158 | spec_gap | ratify / tighten |
| `Composite` value lacks `Debug`/`PartialEq` | spec_gap | ratify / tighten |
