audit(0090): cycle + C24 milestone close — fieldtest green; ledger + rm ephemeral spec/plan

Closes the cycle-0090 (#156) work and delivers the "Topology-as-data:
blueprint serialization & loader (C24)" milestone.

Cycle-0090 audit (architect drift review, range 7ad7f58..HEAD): drift-clean
on substance. Tier-1 forward-tolerance is genuinely serde-default
(deny_unknown_fields = 0 in every production struct), pinned at envelope +
primitive across both the in-crate and public-seam tests; Tier-2 refusals
real and green; no struct/signature/runtime change; the invariant-5
acyclicity lockstep (construction gate <-> bootstrap Kahn sort) undisturbed;
invariant-9 resolver stays injected, no registry. The one medium item the
architect flagged — the C24 Remaining block carried no deferral signal for
#158/#159 — is fixed in this commit's ledger update.

Milestone fieldtest (fieldtests/milestone-topology-as-data/, the close-gate
evidence): GREEN, 0 behavioural bugs. A downstream consumer ran the full
author -> serialize -> load -> construct -> introspect -> reproduce story from
the public surface alone — round-trip bit-exact on both authoring surfaces
(Rust builder + op-script CLI), build-free introspection, Tier-1/Tier-2
forward-compat by name, fail-fast diagnostics by identifier. Verified the
headline claims myself (the two Rust binaries + the CLI build/diff + the
fail-fast exits), not just the agent's report.

Findings triaged to the forward queue (none blocking): op-script grammar
undocumented + a stale cycle-0088 example (#163); canonical trailing-newline
divergence + Composite Debug/PartialEq ergonomics (#164); CLI discoverability
of build/introspect + no `graph run` (commented on #159); the newline as a
content-addressing prerequisite (commented on #158).

#158 (content-address topology in the manifest) and #159 (retire the
hard-wired harnesses) were moved OUT of this milestone: both are premature
until the World generates runs from blueprint-data (commit still fully
identifies every hard-wired topology), so they belong to the
project-as-crate / World cycle. The C24 ledger Status records the delivery
and the deferral.

Ephemeral cycle-0090 spec + plan removed per the lifecycle convention.
The Gitea milestone container is closed on user ratification (this run
tees up the close, does not click it).
This commit is contained in:
2026-06-30 14:43:28 +02:00
parent cdd2da6337
commit 3f75d59598
24 changed files with 1091 additions and 402 deletions
+450
View File
@@ -0,0 +1,450 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "aura-analysis"
version = "0.1.0"
dependencies = [
"aura-core",
"serde",
]
[[package]]
name = "aura-core"
version = "0.1.0"
dependencies = [
"serde",
]
[[package]]
name = "aura-engine"
version = "0.1.0"
dependencies = [
"aura-analysis",
"aura-core",
"serde",
"serde_json",
]
[[package]]
name = "aura-std"
version = "0.1.0"
dependencies = [
"aura-core",
"chrono",
"chrono-tz",
]
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cc"
version = "1.2.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
dependencies = [
"iana-time-zone",
"num-traits",
"windows-link",
]
[[package]]
name = "chrono-tz"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"
dependencies = [
"chrono",
"phf",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "js-sys"
version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
dependencies = [
"cfg-if",
"futures-util",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "log"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "ms-topology-as-data-fieldtest"
version = "0.0.0"
dependencies = [
"aura-core",
"aura-engine",
"aura-std",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "phf"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_shared"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "siphasher"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wasm-bindgen"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
dependencies = [
"unicode-ident",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
@@ -0,0 +1,33 @@
# Milestone fieldtest consumer crate — Topology-as-data (C24): blueprint
# serialization & loader + the construction service.
#
# A standalone downstream project (the shape a C16 research project takes):
# path-deps on the engine crates only. It drives the C24 surface — GraphBuilder
# authoring, blueprint_to_json / blueprint_from_json, std_vocabulary resolution,
# and the run/round-trip path — from the PUBLIC interface only. No crates/*/src
# was read; the API was discovered from the design ledger (docs/design/INDEX.md,
# C24), `cargo doc` doc-comments, the CLI's own `aura graph build/introspect`
# output, and the public re-exports.
#
# Empty [workspace] table: this fixture crate is its OWN workspace root, so it
# is not pulled into the aura engine workspace.
[workspace]
[package]
name = "ms-topology-as-data-fieldtest"
edition = "2024"
version = "0.0.0"
publish = false
[[bin]]
name = "mt_1_roundtrip"
path = "mt_1_roundtrip.rs"
[[bin]]
name = "mt_4_forward_compat"
path = "mt_4_forward_compat.rs"
[dependencies]
aura-core = { path = "../../crates/aura-core" }
aura-std = { path = "../../crates/aura-std" }
aura-engine = { path = "../../crates/aura-engine" }
@@ -0,0 +1,210 @@
# 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 |
@@ -0,0 +1,123 @@
// Milestone fieldtest — Topology-as-data (C24), Example 1.
//
// The headline round-trip the milestone promises: a researcher authors a small
// SMA-cross bias signal with the Rust GraphBuilder, serializes the topology to
// canonical JSON (blueprint_to_json), loads it back through the std vocabulary
// resolver (blueprint_from_json + aura_std::std_vocabulary), and proves
// (a) the canonical JSON round-trips byte-identically (serialize -> load ->
// re-serialize == original), and
// (b) the RELOADED blueprint runs BIT-IDENTICALLY to its Rust-built twin
// (C1: the recorded bias trace matches value-for-value).
//
// Discovery: the GraphBuilder / blueprint_to_json / blueprint_from_json surface
// was learned from `cargo doc` doc-comments + the C24 ledger entry + `aura graph
// introspect --node <T>` (which gives each node's port and bind shapes). No
// crates/*/src was read.
//
// Note on the run path: a recording sink (Recorder) is deliberately OUTSIDE the
// serializable round-trippable set (C24 ledger: it captures an mpsc::Sender —
// runtime identity, not param-generic data). So the *serializable* unit is the
// pure signal sub-composite (price source -> SMAs -> Sub -> Bias -> expose
// bias); to OBSERVE a run we nest that sub-composite under a tiny throwaway
// harness wrapper that adds a Recorder. The same wrapper is applied to both the
// original and the reloaded sub-composite, so the comparison is apples-to-apples.
use std::sync::mpsc;
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{
blueprint_from_json, blueprint_to_json, BlueprintNode, Composite, Edge, GraphBuilder, OutField,
Role, Target, VecSource,
};
use aura_std::{std_vocabulary, Bias, Recorder, Sma, Sub};
/// Author the serializable signal sub-composite via the Rust builder API.
/// SMA(2) fast, SMA(4) slow, Sub = fast - slow, Bias over the spread, exposing
/// the single `bias` field. The two SMA lengths are baked (`.bind`); Bias.scale
/// is left as a tunable param (mirrors the canonical op-script form).
fn author_signal() -> Composite {
let mut gb = GraphBuilder::new("graph");
let price = gb.source_role("price", ScalarKind::F64);
let fast = gb.add(Sma::builder().named("fast").bind("length", Scalar::I64(2)));
let slow = gb.add(Sma::builder().named("slow").bind("length", Scalar::I64(4)));
gb.feed(price, [fast.input("series"), slow.input("series")]);
let sub = gb.add(Sub::builder().named("sub"));
gb.connect(fast.output("value"), sub.input("lhs"));
gb.connect(slow.output("value"), sub.input("rhs"));
let bias = gb.add(Bias::builder().named("bias"));
gb.connect(sub.output("value"), bias.input("signal"));
gb.expose(bias.output("bias"), "bias");
gb.build().expect("signal composite should build")
}
/// Run a signal sub-composite by nesting it under a Recorder harness and
/// recording the exposed `bias` each fired cycle. Returns (ts, bias) rows.
fn run_signal(signal: Composite, prices: &[f64]) -> Vec<(i64, f64)> {
let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
let harness = Composite::new(
"harness",
vec![
BlueprintNode::Composite(signal),
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx).into(),
],
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }], // bias -> recorder col 0
vec![Role {
name: "price".into(),
targets: vec![Target { node: 0, slot: 0 }],
source: Some(ScalarKind::F64),
}],
vec![] as Vec<OutField>,
);
let stream: Vec<(Timestamp, Scalar)> = prices
.iter()
.enumerate()
.map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::F64(p)))
.collect();
// bias.scale is the one tunable param left in the space; bind it at bootstrap.
let mut h = harness
.bootstrap_with_params(vec![Scalar::F64(1.0)])
.expect("harness should bootstrap");
h.run(vec![Box::new(VecSource::new(stream))]);
drop(h); // release the recorder's tx clone
rx.iter()
.map(|(ts, row)| match row[0] {
Scalar::F64(x) => (ts.0, x),
other => panic!("expected f64 bias, got {other:?}"),
})
.collect()
}
fn main() {
let prices: Vec<f64> = vec![
1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07,
];
// --- (a) canonical JSON round-trip ---------------------------------------
let orig = author_signal();
let json1 = blueprint_to_json(&orig).expect("serialize original");
std::fs::write("mt_1_rust_blueprint.json", &json1).expect("persist artifact");
let loaded = blueprint_from_json(&json1, &std_vocabulary).expect("load from json");
let json2 = blueprint_to_json(&loaded).expect("re-serialize loaded");
assert_eq!(
json1, json2,
"canonical JSON must round-trip byte-identically (serialize->load->re-serialize)"
);
println!("(a) canonical JSON round-trip: IDENTICAL ({} bytes)", json1.len());
// --- (b) run identity: reloaded twin runs bit-identical to the original ---
let orig_run = run_signal(author_signal(), &prices);
let loaded_run = run_signal(loaded, &prices);
assert_eq!(
orig_run, loaded_run,
"C1: the reloaded blueprint must run bit-identically to its Rust-built twin"
);
println!(
"(b) run identity: {} recorded bias rows, value-for-value IDENTICAL",
orig_run.len()
);
for (ts, b) in &loaded_run {
println!(" ts={ts:>14} bias={b:+.6}");
}
println!("PASS: author -> serialize -> load -> reproduce (C1 round-trip).");
}
@@ -0,0 +1 @@
{"format_version":1,"blueprint":{"name":"graph","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","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":"graph","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","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", "bind": {"length": {"I64": 2}}},
{"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,43 @@
## --node SMA
SMA
in series:F64
out value:F64
param length:I64 (bind {"I64": <v>})
[exit=0]
## --node Sub
Sub
in lhs:F64
in rhs:F64
out value:F64
[exit=0]
## --node Bias
Bias
in signal:F64
out bias:F64
param scale:F64 (bind {"F64": <v>})
[exit=0]
## --node EMA
EMA
in series:F64
out value:F64
param length:I64 (bind {"I64": <v>})
[exit=0]
## --node RollingMax
RollingMax
in series:F64
out value:F64
param length:I64 (bind {"I64": <v>})
[exit=0]
## --node LinComb
aura: unknown node type "LinComb"
[exit=2]
## --node Recorder
aura: unknown node type "Recorder"
[exit=2]
@@ -0,0 +1,8 @@
[
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 2}}},
{"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"}
]
@@ -0,0 +1,2 @@
sub.rhs:F64
[exit=0]
@@ -0,0 +1,22 @@
Add
And
Bias
CarryCost
ConstantCost
Delay
EMA
EqConst
FixedStop
Gt
Latch
LongOnly
Mul
PositionManagement
Resample
RollingMax
RollingMin
Sizer
SMA
Sqrt
Sub
VolSlippageCost
@@ -0,0 +1,152 @@
// Milestone fieldtest — Topology-as-data (C24), Example 4.
//
// The forward-compatibility discipline the milestone promises (#156 / C24
// two-tier), exercised from the loader's seat, plus cross-surface interop:
//
// 0. Cross-surface interop: the blueprint emitted by the `aura graph build`
// CLI (mt_2_blueprint.cli.json, trailing newline and all) loads cleanly via
// the library blueprint_from_json and runs — proving CLI-authored topology
// is library-consumable.
// 1. Tier-1 (additive-optional tolerance): a future writer adds an UNKNOWN
// optional field — at the envelope level AND inside a node — and an older
// reader must (a) load it, (b) drop it on re-serialize back to the SAME
// canonical form, and (c) run BIT-IDENTICALLY to the clean blueprint.
// 2. Tier-2 (must-understand refusal): a too-new format_version and an
// out-of-vocabulary node type must each fail CLEANLY BY NAME
// (LoadError::UnsupportedVersion / UnknownNodeType) — never a panic, never
// a silently-wrong graph.
//
// Public interface only: blueprint_to_json / blueprint_from_json / LoadError /
// std_vocabulary, learned from `cargo doc` + the C24 ledger. No crates/*/src.
use std::sync::mpsc;
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{
blueprint_from_json, blueprint_to_json, BlueprintNode, Composite, Edge, GraphBuilder, LoadError,
OutField, Role, Target, VecSource,
};
use aura_std::{std_vocabulary, Bias, Recorder, Sma, Sub};
fn author_signal() -> Composite {
let mut gb = GraphBuilder::new("graph");
let price = gb.source_role("price", ScalarKind::F64);
let fast = gb.add(Sma::builder().named("fast").bind("length", Scalar::I64(2)));
let slow = gb.add(Sma::builder().named("slow").bind("length", Scalar::I64(4)));
gb.feed(price, [fast.input("series"), slow.input("series")]);
let sub = gb.add(Sub::builder().named("sub"));
gb.connect(fast.output("value"), sub.input("lhs"));
gb.connect(slow.output("value"), sub.input("rhs"));
let bias = gb.add(Bias::builder().named("bias"));
gb.connect(sub.output("value"), bias.input("signal"));
gb.expose(bias.output("bias"), "bias");
gb.build().expect("signal composite should build")
}
fn run_signal(signal: Composite) -> Vec<(i64, f64)> {
let prices: [f64; 12] = [
1.00, 1.01, 1.02, 1.03, 1.05, 1.08, 1.06, 1.04, 1.02, 1.01, 1.03, 1.07,
];
let (tx, rx) = mpsc::channel::<(Timestamp, Vec<Scalar>)>();
let harness = Composite::new(
"harness",
vec![
BlueprintNode::Composite(signal),
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx).into(),
],
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
vec![Role {
name: "price".into(),
targets: vec![Target { node: 0, slot: 0 }],
source: Some(ScalarKind::F64),
}],
vec![] as Vec<OutField>,
);
let stream: Vec<(Timestamp, Scalar)> = prices
.iter()
.enumerate()
.map(|(i, &p)| (Timestamp(60_000_000_000 * i as i64), Scalar::F64(p)))
.collect();
let mut h = harness
.bootstrap_with_params(vec![Scalar::F64(1.0)])
.expect("bootstrap");
h.run(vec![Box::new(VecSource::new(stream))]);
drop(h);
rx.iter()
.map(|(ts, row)| match row[0] {
Scalar::F64(x) => (ts.0, x),
other => panic!("expected f64 bias, got {other:?}"),
})
.collect()
}
fn main() {
let canonical = blueprint_to_json(&author_signal()).expect("serialize");
let baseline_run = run_signal(author_signal());
// --- 0. cross-surface interop: load the CLI `graph build` output ----------
match std::fs::read_to_string("mt_2_blueprint.cli.json") {
Ok(cli_json) => {
let from_cli = blueprint_from_json(&cli_json, &std_vocabulary)
.expect("CLI-emitted blueprint must load via the library loader");
assert_eq!(
run_signal(from_cli),
baseline_run,
"CLI-authored topology must run identically to the Rust-authored twin"
);
let trailing_nl = cli_json.ends_with('\n');
println!(
"0. cross-surface interop: CLI blueprint loaded + ran identically \
(loader tolerated trailing newline = {trailing_nl})"
);
}
Err(_) => println!("0. (skipped: run mt_2 CLI build first to produce mt_2_blueprint.cli.json)"),
}
// --- 1. Tier-1: unknown optional fields are tolerated ---------------------
// envelope-level unknown key
let env_unknown = canonical.replacen('{', r#"{"x_future_envelope":{"any":[1,2]},"#, 1);
// node-level unknown key (inject inside the first primitive object)
let node_unknown = canonical.replacen(
r#"{"type":"SMA","name":"fast""#,
r#"{"x_future_node_flag":true,"type":"SMA","name":"fast""#,
1,
);
for (label, mutated) in [("envelope", &env_unknown), ("node", &node_unknown)] {
let loaded = blueprint_from_json(mutated, &std_vocabulary)
.unwrap_or_else(|e| panic!("Tier-1 {label} unknown field must be tolerated, got {e:?}"));
let reser = blueprint_to_json(&loaded).expect("re-serialize");
assert_eq!(
reser, canonical,
"Tier-1 {label}: re-serialized form must drop the unknown field and match canonical"
);
assert_eq!(
run_signal(loaded),
baseline_run,
"Tier-1 {label}: must run bit-identically to the clean blueprint"
);
println!("1. Tier-1 {label} unknown field: tolerated, dropped on re-emit, ran identical.");
}
// --- 2. Tier-2: must-understand refusals, by name, no panic ---------------
// (Composite has no Debug impl, so we cannot {:?} the Ok arm — match explicitly.)
let too_new = canonical.replacen(r#""format_version":1"#, r#""format_version":999"#, 1);
match blueprint_from_json(&too_new, &std_vocabulary) {
Err(e @ LoadError::UnsupportedVersion { .. }) => {
println!("2a. too-new format_version (999): refused -> {e:?}")
}
Err(other) => panic!("2a. expected UnsupportedVersion, got {other:?}"),
Ok(_) => panic!("2a. expected refusal, but a graph was silently loaded"),
}
let bad_type = canonical.replacen(r#""type":"Sub""#, r#""type":"FOO""#, 1);
match blueprint_from_json(&bad_type, &std_vocabulary) {
Err(e @ LoadError::UnknownNodeType(..)) => {
println!("2b. out-of-vocabulary node type (FOO): refused -> {e:?}")
}
Err(other) => panic!("2b. expected UnknownNodeType, got {other:?}"),
Ok(_) => panic!("2b. expected refusal, but a graph was silently loaded"),
}
println!("PASS: Tier-1 tolerance + Tier-2 by-name refusal (no panic, no silent wrong graph).");
}
@@ -0,0 +1 @@
aura: op 1 (add): unknown node type "FOO"
@@ -0,0 +1 @@
2
@@ -0,0 +1,6 @@
[
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "FOO", "name": "mystery"},
{"op": "feed", "role": "price", "into": ["mystery.series"]},
{"op": "expose", "from": "mystery.value", "as": "out"}
]
@@ -0,0 +1 @@
aura: op 5 (connect): connecting b.value -> a.rhs would close a cycle
@@ -0,0 +1 @@
2
@@ -0,0 +1,9 @@
[
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "Sub", "name": "a"},
{"op": "add", "type": "Sub", "name": "b"},
{"op": "feed", "role": "price", "into": ["a.lhs", "b.rhs"]},
{"op": "connect", "from": "a.value", "to": "b.lhs"},
{"op": "connect", "from": "b.value", "to": "a.rhs"},
{"op": "expose", "from": "a.value", "as": "out"}
]
@@ -0,0 +1 @@
aura: finalize: slot sub.rhs is unconnected
@@ -0,0 +1 @@
2
@@ -0,0 +1,11 @@
[
{"op": "source", "role": "price", "kind": "F64"},
{"op": "add", "type": "SMA", "name": "fast", "bind": {"length": {"I64": 2}}},
{"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": "add", "type": "Bias", "name": "bias"},
{"op": "connect", "from": "sub.value", "to": "bias.signal"},
{"op": "expose", "from": "bias.bias", "as": "bias"}
]