# Fieldtest — cycle 0088 (construction op-script service, #157) — 2026-06-29

**Status:** Triaged 2026-06-29 — filed to the forward queue (see Triage outcome)
**Author:** fieldtester (dispatched by fieldtest skill); triaged by orchestrator

> **Corpus refreshed 2026-06-30 (#163)** to replay against the current binary.
> Two drifts had accumulated since this run recorded the fixtures: `add`'s
> naming key was renamed `as` -> `name` (commit `3c4b667`; `expose` keeps `as`),
> and the dataflow-cycle bug below was fixed (`1652042`, closes #161). The `.json`
> inputs now use `name` for `add`; `c0088_3m_two_cycle` is a rejection fixture
> (`c0088_3m.err` carries the cycle-rejection message; the gap-era cyclic-blueprint
> golden was removed). The findings below are preserved as written at fieldtest time.

## Scope
Cycle 0088 shipped the **construction op-script service** (#157, C24): two CLI
subcommands over a declarative JSON op-list. `aura graph build` reads an op-list
from stdin, replays it op-by-op through the `aura-std` node vocabulary, and on
success prints a `format_version:1` #155 blueprint to stdout; on the first failing
op it prints `op N (kind): cause` (or `finalize: cause`) to stderr and exits
non-zero. `aura graph introspect --vocabulary | --node <T> | --unwired` answers,
build-free: the node-type roster, one type's input ports / output fields / param
paths, and (reading a partial op-list) the still-open interior slots. This field
test exercised the surface purely as a downstream consumer authoring topology as
data — public interface only (the C24/C17/C19/C23 ledger + the CLI's own output);
no implementation source read. Binary under test: freshly built `target/debug/aura`
(`cargo build --workspace`, nothing to recompile at HEAD).

## Examples
### fieldtests/cycle-0088-construction-op-script/c0088_1_sma_crossover.json — discover-then-author an SMA-crossover bias
- Used `introspect --vocabulary` (22 types) then `introspect --node SMA|Sub|Bias`
  to learn ports/kinds, then hand-wrote a 10-op script: one `price` source → two
  SMAs (lengths 2/4) → `Sub` → `Bias`, expose `bias.bias`.
- Fits scope: exercises `source`, `add`+`bind`, `feed` (fan-out into two slots),
  `connect`, `expose` — the full op vocabulary in one realistic signal graph.
- Outcome: **built, ran, matched expected.** Emitted a `format_version:1` blueprint
  with the expected 4 nodes / 3 edges / 1 input_role / 1 output
  (`c0088_1_blueprint.out.json`). First try, no diagnostics.

### fieldtests/cycle-0088-construction-op-script/c0088_2_partial.json (+ _completed.json) — author with the --unwired helper
- Wrote a deliberately partial doc (left `sub.rhs` and `bias` unconnected), ran
  `introspect --unwired`, finished wiring per its report, built.
- Fits scope: the cycle's headline build-free authoring aid.
- Outcome: **worked.** `--unwired` reported `sub.rhs:F64` / `bias.signal:F64`
  by-identifier with kinds; the completed doc built to a #155 blueprint;
  `--unwired` on the completed doc returned empty (exit 0) — a reliable
  "am I done?" check.

### fieldtests/cycle-0088-construction-op-script/c0088_3*.json — error ergonomics (15 fixtures)
- Provoked the realistic hand-author mistakes: bad node type, wrong-kind connect,
  wrong output field, unknown node id, double-wired slot, duplicate id, bad bind
  kind, bad expose field, unknown role, bad feed port, left-open slot, dataflow
  cycle (self-loop + explicit 2-cycle), unfed role, no-expose, naive/untyped bind,
  lowercase kind.
- Fits scope: judges whether `op N (kind): cause` tells you *where* and *what*.
- Outcome: **eager per-op messages are excellent** (by-identifier, right op index);
  three message-quality friction points and one acceptance gap surfaced (below).

### Round-trip sense-check (Task 4)
- Piped `c0088_1_blueprint.out.json` into `aura graph`; compared to bare `aura graph`.
- Outcome: byte-**identical** — `aura graph` ignores stdin and renders a hard-wired
  sample. No CLI subcommand consumes an emitted #155 blueprint (see G2).

## Findings

### [working] Discover-then-author round-trips cleanly through the CLI
- Examples: c0088_1, all `introspect` runs.
- `introspect --vocabulary` lists 22 node types; `introspect --node SMA` prints
  `in series:F64 / out value:F64 / param length:I64` — the exact dotted-port names
  (`fast.series`, `fast.value`) and capitalized kinds the op-script consumes. An
  author can go vocabulary → ports → op-script → `format_version:1` blueprint with
  no source reading. This is the core promise of C24's data-authoring surface and
  it holds.
- Recommended action: carry-on.

### [working] `--unwired` is a precise, by-identifier authoring aid
- Examples: c0088_2_partial / _completed.
- Reports open interior slots as `node.port:Kind` (`sub.rhs:F64`,
  `bias.signal:F64`) and returns empty on a fully-wired doc. Exactly the
  finish-the-wiring loop a hand-author wants; the kinds double as a connect hint.
- Recommended action: carry-on.

### [working] Eager per-op diagnostics name the op and the identifier
- Examples: c0088_3a–h, 3j, 3k.
- Verbatim wins: `op 1 (add): unknown node type "Smaa"`; `op 4 (connect): kind
  mismatch g.value -> fast.series (producer Bool, consumer F64)`; `op 4 (connect):
  node "fast" has no output field "output"`; `op 4 (connect): unknown node
  identifier "fst"`; `op 6 (connect): slot sub.lhs already wired`; `op 2 (add):
  duplicate identifier "fast"`; `op 2 (feed): unknown role "pryce"`; `op 2 (feed):
  node "fast" has no input port "serie"`. Every one names the op index and speaks
  by-identifier — you can fix without counting. Lowercase `kind:"f64"` even
  self-documents: `unknown variant 'f64', expected one of 'I64','F64','Bool',
  'Timestamp'`.
- Recommended action: carry-on.

### [bug] `graph build` accepts a dataflow cycle and reports success
- Examples: c0088_3i_unfed_role (self-loop), c0088_3m_two_cycle.
- `aura graph build < c0088_3m_two_cycle.json` → **exit 0**, emits a blueprint
  whose edges are `{from:0,to:1,slot:0},{from:1,to:0,slot:0}` — a combinational
  2-cycle. A self-loop (`fast.value → fast.series`, edge `{from:0,to:0}`) is
  likewise accepted. C9 / domain-invariant 5: "The graph is a DAG; the only
  feedback path is an explicit delay/state node." The construction service's gate
  set (kind, interior-slot totality, param-namespace injectivity, root-role
  boundness) omits acyclicity, so it green-lights a non-runnable blueprint and
  calls it a success. Caveat (cadence, unverifiable as a CLI consumer): acyclicity
  may be intended as a bootstrap-time (compile-to-`FlatGraph`) gate — but the CLI
  exposes no compile/run path for an emitted blueprint (see G2), so a CLI consumer
  gets no signal at all that the graph is not a DAG.
- Repro: `aura graph build < fieldtests/cycle-0088-construction-op-script/c0088_3m_two_cycle.json; echo $?` → `0`.
- Recommended action: debug.

### [friction] The holistic `finalize` error speaks raw indices, breaking the by-identifier surface
- Examples: c0088_2_partial.
- `aura graph build < c0088_2_partial.json` → `aura: finalize: UnconnectedPort
  { node: 2, slot: 1 }`. The whole op-script is authored by-identifier, and the
  sibling `introspect --unwired` reports the *same* open slot as `sub.rhs:F64` —
  so the identifier mapping demonstrably exists. The build finalize path drops to
  raw `node`/`slot` indices (and Debug-struct form), forcing the author to count
  nodes (0=fast,1=slow,2=sub) and slots to decode it.
- Why friction: task completed (the error is non-zero and correct) but the surface
  forced an index-counting dance the by-identifier design implies should be gone.
- Recommended action: plan — route finalize faults through the same by-identifier
  formatter `--unwired` already uses.

### [friction] Bind-kind-mismatch leaks the Debug struct, inconsistent with the connect message
- Examples: c0088_3g_bad_bind_kind.
- `op 1 (add): bad param on fast: KindMismatch { param: "length", expected: I64,
  got: F64 }` — the `KindMismatch { … }` is raw `#[derive(Debug)]`, whereas the
  parallel connect mismatch is hand-phrased prose (`producer Bool, consumer F64`).
  Two kind-mismatch errors, two registers.
- Recommended action: plan — phrase the bind mismatch like the connect one.

### [friction] The typed-Scalar bind form is not discoverable from introspection
- Examples: c0088_3n_naive_bind.
- `introspect --node SMA` shows `param length:I64` but never that a bind *value*
  must be wrapped `{"I64": 2}`. The natural first guess `"bind":{"length":2}`
  fails with a generic serde error: `invalid op-list: invalid type: integer 2,
  expected string or map at line 4 column 2` — correct location, no domain hint
  about the `{"I64": 2}` shape. (Capitalized `kind` *is* discoverable — both from
  `introspect --node` output and the self-documenting lowercase-kind error.)
- Recommended action: plan/doc — show the bind-value form in `introspect --node`
  (e.g. `param length:I64  (bind {"I64": <v>})`) or hint it in the serde error.

### [spec_gap] The construction surface is undiscoverable from the CLI
- Examples: bare `aura`, `aura --help`, `aura graph build --help`.
- Neither `aura` (no args, exit 2), `aura --help`, nor any public doc (glossary,
  project-layout) mentions `aura graph build` or `aura graph introspect`. The
  top-level usage string lists only the bare `aura graph` (HTML-render) arm. The
  subcommands appear *only* in the C24 ledger Status prose. `aura graph build
  --help` and `aura graph introspect --help` both print the giant top-level usage
  at exit 0 (the `--help` token is swallowed); there is no per-subcommand help and
  no statement anywhere of the op-list document shape. A genuine new user has no
  in-CLI route to learn the surface exists or how a document is shaped.
- Why spec_gap: the surface shipped without a discovery path; the ledger is silent
  on whether the CLI usage should advertise it. I read the surface only because the
  carrier named the subcommands.
- Recommended action: tighten the design ledger / plan — add `graph build` /
  `graph introspect` to the usage string and a `build --help` documenting the
  op-list shape (the op kinds, the typed-Scalar bind, capitalized kind).

### [spec_gap] No CLI consumer for the emitted #155 blueprint — the round-trip dead-ends
- Examples: Task 4 (pipe c0088_1_blueprint into `aura graph`).
- `graph build` emits a blueprint to stdout, but no subcommand reads one back:
  `aura graph` ignores stdin and renders a hard-wired sample (verified
  byte-identical with and without a piped blueprint), and no `aura run` /
  `graph render` accepts a blueprint file. C24 promises topology is a value
  "constructed, generated, mutated, serialized for reproduction, and (optionally)
  rendered"; the load path is realised only at the library level
  (`blueprint_from_json`, cycle 0087). A pure-CLI consumer can author and build
  topology-as-data but cannot run, render, or reload-validate it through the CLI —
  which is also why the cycle bug (above) is undiscoverable. Equally plausible
  reading: `build` is intentionally an emit-only producer feeding a library/World
  consumer — hence spec_gap, not bug.
- Recommended action: ratify the emit-only scope, or planner — add a CLI edge that
  consumes a #155 blueprint (render/run/validate). C24 Status already tracks #159
  (retire hard-wired harnesses); note the missing build→consume edge there.

### [spec_gap] The validity floor of `graph build` is unstated
- Examples: c0088_3i_unfed_role, c0088_3l_no_expose.
- Beyond the cycle (bug above), build accepts at exit 0: an unfed `source` role
  (emitted as `"targets":[]` — a dead role kept in the blueprint) and a graph with
  no `expose`/output at all (no `output` key emitted). The ledger names the gates
  build *does* enforce (kind, interior-slot totality, injectivity, root-role
  boundness) but not the minimal well-formedness it *guarantees* — so a consumer
  can't tell whether a dead role / output-less graph is intended-valid or a
  silently-accepted slip.
- Recommended action: ratify (state the validity floor — what `build` does/does not
  promise) or tighten.

## Recommendation summary
| Finding | Class | Action |
|---|---|---|
| Discover-then-author round-trips cleanly | working | carry-on |
| `--unwired` precise by-identifier aid | working | carry-on |
| Eager per-op diagnostics name op + identifier | working | carry-on |
| `graph build` accepts a dataflow cycle | bug | debug |
| `finalize` error speaks raw indices | friction | plan |
| Bind-kind-mismatch leaks Debug struct | friction | plan |
| Typed-Scalar bind form not discoverable | friction | plan / doc |
| Construction surface undiscoverable from CLI | spec_gap | tighten ledger / plan |
| No CLI consumer for emitted blueprint | spec_gap | ratify / plan |
| Validity floor of `graph build` unstated | spec_gap | ratify / tighten |

## Triage outcome (orchestrator, 2026-06-29)

The cycle bug was reproduced directly before filing. Findings routed to the forward queue:

| Finding | Disposition |
|---|---|
| `graph build` accepts a dataflow cycle (+ validity-floor: unfed role, no-`expose`) | filed **#161** (`bug`) — reject cyclic graphs, DAG invariant 5; validity floor decided alongside. RED-first via debug. |
| `finalize` raw indices + bind-mismatch Debug leak + typed-Scalar bind undiscoverable | filed **#162** (polish) — three message-quality defects on the by-identifier surface. |
| Construction surface undiscoverable from the CLI | commented on **#159** (CLI usage/help rework) — advertise the subcommands + per-subcommand `--help`. |
| No CLI consumer for the emitted #155 blueprint (round-trip dead-end) | commented on **#28** (render a consumer's OWN graph) — the build→consume CLI edge. |
| Three `working` findings | carry-on — the happy path, `--unwired`, and eager per-op diagnostics hold. |
