From 3f75d595984eb8cdcf9cbd850947be2084235b73 Mon Sep 17 00:00:00 2001 From: Brummel Date: Tue, 30 Jun 2026 14:43:28 +0200 Subject: [PATCH] =?UTF-8?q?audit(0090):=20cycle=20+=20C24=20milestone=20cl?= =?UTF-8?q?ose=20=E2=80=94=20fieldtest=20green;=20ledger=20+=20rm=20epheme?= =?UTF-8?q?ral=20spec/plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- docs/design/INDEX.md | 5 +- docs/plans/0090-blueprint-forward-compat.md | 195 -------- docs/specs/0090-blueprint-forward-compat.md | 204 -------- .../milestone-topology-as-data/Cargo.lock | 450 ++++++++++++++++++ .../milestone-topology-as-data/Cargo.toml | 33 ++ .../milestone-topology-as-data/FINDINGS.md | 210 ++++++++ .../mt_1_roundtrip.rs | 123 +++++ .../mt_1_rust_blueprint.json | 1 + .../mt_2_blueprint.cli.json | 1 + .../mt_2_sma_cross.ops.json | 12 + .../milestone-topology-as-data/mt_3_node.out | 43 ++ .../mt_3_partial.ops.json | 8 + .../mt_3_unwired.out | 2 + .../mt_3_vocabulary.out | 22 + .../mt_4_forward_compat.rs | 152 ++++++ .../mt_5a_unknown_type.err | 1 + .../mt_5a_unknown_type.exit | 1 + .../mt_5a_unknown_type.ops.json | 6 + .../mt_5b_cycle.err | 1 + .../mt_5b_cycle.exit | 1 + .../mt_5b_cycle.ops.json | 9 + .../mt_5c_unconnected.err | 1 + .../mt_5c_unconnected.exit | 1 + .../mt_5c_unconnected.ops.json | 11 + 24 files changed, 1091 insertions(+), 402 deletions(-) delete mode 100644 docs/plans/0090-blueprint-forward-compat.md delete mode 100644 docs/specs/0090-blueprint-forward-compat.md create mode 100644 fieldtests/milestone-topology-as-data/Cargo.lock create mode 100644 fieldtests/milestone-topology-as-data/Cargo.toml create mode 100644 fieldtests/milestone-topology-as-data/FINDINGS.md create mode 100644 fieldtests/milestone-topology-as-data/mt_1_roundtrip.rs create mode 100644 fieldtests/milestone-topology-as-data/mt_1_rust_blueprint.json create mode 100644 fieldtests/milestone-topology-as-data/mt_2_blueprint.cli.json create mode 100644 fieldtests/milestone-topology-as-data/mt_2_sma_cross.ops.json create mode 100644 fieldtests/milestone-topology-as-data/mt_3_node.out create mode 100644 fieldtests/milestone-topology-as-data/mt_3_partial.ops.json create mode 100644 fieldtests/milestone-topology-as-data/mt_3_unwired.out create mode 100644 fieldtests/milestone-topology-as-data/mt_3_vocabulary.out create mode 100644 fieldtests/milestone-topology-as-data/mt_4_forward_compat.rs create mode 100644 fieldtests/milestone-topology-as-data/mt_5a_unknown_type.err create mode 100644 fieldtests/milestone-topology-as-data/mt_5a_unknown_type.exit create mode 100644 fieldtests/milestone-topology-as-data/mt_5a_unknown_type.ops.json create mode 100644 fieldtests/milestone-topology-as-data/mt_5b_cycle.err create mode 100644 fieldtests/milestone-topology-as-data/mt_5b_cycle.exit create mode 100644 fieldtests/milestone-topology-as-data/mt_5b_cycle.ops.json create mode 100644 fieldtests/milestone-topology-as-data/mt_5c_unconnected.err create mode 100644 fieldtests/milestone-topology-as-data/mt_5c_unconnected.exit create mode 100644 fieldtests/milestone-topology-as-data/mt_5c_unconnected.ops.json diff --git a/docs/design/INDEX.md b/docs/design/INDEX.md index e831614..331f243 100644 --- a/docs/design/INDEX.md +++ b/docs/design/INDEX.md @@ -1829,9 +1829,8 @@ node type, edge semantics, or structural-axis kind) bumps `format_version` so an old reader refuses cleanly (`LoadError::UnsupportedVersion` / `UnknownNodeType`, already green). The per-section required-flag scheme is deferred (no current Tier-2 section to validate it; recorded on #156). -**Remaining** -(each its own milestone issue): -content-addressed topology identity in the manifest (#158, C18); retiring the pre-C24 +**Milestone delivered — 2026-06-30, cycle 0090 — the serializable format + loader + construction service.** A green end-to-end milestone fieldtest (`fieldtests/milestone-topology-as-data/`) proves the full author → serialize → load → construct → introspect → reproduce story from the public surface alone, 0 behavioural bugs; the round-trippable format (#155), the construction service (#157), and the forward-compat two-tier discipline (#156) all shipped. Polish filed forward: op-script grammar docs + a stale example (#163), the canonical trailing-newline / `Composite` value ergonomics (#164), CLI discoverability of `build`/`introspect` (#159). +**Deferred to the World / project-as-crate cycle** (moved out of this milestone — they address a problem that only arises once the World *generates* runs from blueprint-data, which does not exist yet: `commit` still fully identifies every hard-wired topology): content-addressed topology identity in the manifest (#158, C18) — premature until a run is built *from* a serializable blueprint; retiring the pre-C24 hard-wired `aura-cli` harnesses (`HarnessKind`, `run_stage1_r`, `*_sweep_family`) once the project-as-crate layer lands (#159, paired with #157's data-authoring surface). **Out of the first cut's round-trippable set** (deliberate; fails clean as `UnknownNodeType`, never a silent wrong graph): recording diff --git a/docs/plans/0090-blueprint-forward-compat.md b/docs/plans/0090-blueprint-forward-compat.md deleted file mode 100644 index 1fab5d3..0000000 --- a/docs/plans/0090-blueprint-forward-compat.md +++ /dev/null @@ -1,195 +0,0 @@ -# Blueprint format forward-compatibility: the two-tier discipline — Implementation Plan - -> **Parent spec:** `docs/specs/0090-blueprint-forward-compat.md` -> -> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` skill to run -> this plan. Steps use `- [ ]` checkboxes for tracking. - -**Goal:** Pin the blueprint format's Tier-1 additive forward-tolerance with one -characterization test and codify the two-tier discipline in the `LoadError` doc -and the C24 ledger entry (#156). - -**Architecture:** No struct, signature, or runtime behaviour changes. Tier-1 -tolerance is already true (serde ignores unknown fields — no `deny_unknown_fields` -anywhere; omit-defaults via `skip_serializing_if`); this iteration proves it and -documents the discipline. Both Tier-2 refusals already ship green -(`LoadError::UnsupportedVersion` / `UnknownNodeType`). - -**Tech Stack:** `crates/aura-engine/src/blueprint_serde.rs` (test + doc), -`docs/design/INDEX.md` (C24 ledger). - ---- - -**Files this plan creates or modifies:** - -- Test: `crates/aura-engine/src/blueprint_serde.rs:171-283` — append the Tier-1 - forward-tolerance characterization test to the `#[cfg(test)] mod tests` block. -- Modify: `crates/aura-engine/src/blueprint_serde.rs:110-112` — reword the - `LoadError::UnsupportedVersion` doc comment to state the settled two-tier - discipline. -- Modify: `docs/design/INDEX.md:1822-1827` — add one codification sentence to the - C24 Status block and drop #156 from the Remaining list (keep #158/#159). - -All three changes are one cohesive, independently-reviewable unit → a single task. - ---- - -### Task 1: Tier-1 forward-tolerance pin + two-tier codification - -**Files:** -- Test: `crates/aura-engine/src/blueprint_serde.rs:171-283` -- Modify: `crates/aura-engine/src/blueprint_serde.rs:110-112` -- Modify: `docs/design/INDEX.md:1822-1827` - -- [ ] **Step 1: Append the Tier-1 forward-tolerance test** - -In `crates/aura-engine/src/blueprint_serde.rs`, inside the `#[cfg(test)] mod tests` -block, insert the following test immediately **after** the closing `}` of -`unsupported_version_fails_named` (currently line 282) and **before** the module's -closing `}` (currently line 283). All referenced helpers are in scope: -`run_recording` (:188), `crate::test_fixtures::sink_free_sma_cross_signal` (used at -:212/:225/:237/:258), the `&|t| aura_std::std_vocabulary(t)` closure form (used at -:214/:227/:250/:273/:280), `blueprint_to_json`/`blueprint_from_json` (via `use -super::*;` at :173). - -```rust - #[test] - fn unknown_optional_field_is_tolerated_byte_identically() { - // Forward-compat (C24 / #156, Tier-1): a future writer that has additively - // extended the format emits optional keys the CURRENT loader does not know. - // The loader must silently ignore them and reconstruct the IDENTICAL graph — - // never refuse, never let an unknown key leak into the built blueprint. serde - // ignores unknown fields by default (no deny_unknown_fields). This pins the - // additive property the canonical-golden + omit-defaults discipline claims but - // no existing test exercises. - let canonical = - blueprint_to_json(&crate::test_fixtures::sink_free_sma_cross_signal()).expect("serializes"); - - // Inject unknown optional keys at two additive shapes a new writer might emit: - // a doc-level section ("metadata") and a per-primitive key ("annotations"). - let with_unknown = canonical - .replacen( - r#"{"format_version":1,"#, - r#"{"format_version":1,"metadata":{"author":"future"},"#, - 1, - ) - .replacen(r#"{"type":"Sub"}"#, r#"{"type":"Sub","annotations":["future"]}"#, 1); - assert_ne!(with_unknown, canonical, "probe must actually inject unknown keys"); - - // tolerated, not refused - let loaded = blueprint_from_json(&with_unknown, &|t| aura_std::std_vocabulary(t)) - .expect("an unknown optional field is tolerated, not refused"); - - // the unknown keys did not leak into the graph: re-serialization equals the - // canonical form WITHOUT them (byte-identical reconstruction). - assert_eq!(blueprint_to_json(&loaded).expect("re-serializes"), canonical); - - // and the loaded graph runs bit-identically to the plain canonical one (C1). - let plain = blueprint_from_json(&canonical, &|t| aura_std::std_vocabulary(t)).expect("loads"); - assert_eq!( - run_recording(loaded), - run_recording(plain), - "run diverged under an unknown optional field", - ); - } -``` - -- [ ] **Step 2: Run the new test to verify it passes (characterization pin)** - -This test pins ALREADY-EXISTING behaviour, so it goes GREEN on first run (it is a -forward-tolerance characterization pin, NOT a RED test). - -Run: `cargo test -p aura-engine --lib unknown_optional_field_is_tolerated_byte_identically` -Expected: PASS — `test result: ok. 1 passed` (the filter matches exactly this one -new test by name). - -- [ ] **Step 3: Reword the `LoadError::UnsupportedVersion` doc comment** - -In `crates/aura-engine/src/blueprint_serde.rs`, replace the exact current doc -comment (lines 110-112, verbatim): - -```rust - /// `format_version` the loader does not understand. The full must-understand - /// horizon (multiple versions, unknown-section policy) is #156; #155 supports - /// exactly one version and refuses any other, cleanly. -``` - -with: - -```rust - /// `format_version` the loader does not understand — the **Tier-2 - /// (must-understand)** refusal. The format extends under a two-tier discipline - /// (#156): a **Tier-1** additive-optional change (a new optional field / - /// section) does NOT bump the version — an older reader tolerates it (serde - /// ignores unknown fields; see `unknown_optional_field_is_tolerated_byte_identically`) - /// and, by C1, a new optional field defaults to prior behaviour. A **Tier-2** - /// load-bearing change (a new node type, edge semantics, or structural-axis - /// kind) MUST bump `BLUEPRINT_FORMAT_VERSION`, so an old reader refuses here - /// rather than silently building a different graph (C1 / C18). The - /// per-section required-flag scheme is deferred until a real sub-version Tier-2 - /// addition needs finer granularity than a version bump. - UnsupportedVersion { found: u32, supported: u32 }, -``` - -(The replacement includes the unchanged `UnsupportedVersion { ... }` variant line -as the trailing anchor so the edit lands at the right spot. The -`BLUEPRINT_FORMAT_VERSION` doc at :15-16 is left unchanged — it already states the -rule.) - -- [ ] **Step 4: Add the C24 codification sentence to the ledger Status block** - -In `docs/design/INDEX.md`, the C24 Status block currently ends (line 1822) with -`...the no-second-validator lockstep preserved), only translating their result.` -immediately followed by `**Remaining**` on line 1823. Replace the exact current -text: - -``` -no-second-validator lockstep preserved), only translating their result. -**Remaining** -``` - -with: - -``` -no-second-validator lockstep preserved), only translating their result. -**Cycle 0090 (#156) codifies the forward-compat two-tier discipline.** Tier-1 -(additive-optional) is serde-default silent-ignore with no `format_version` bump -(a new optional field defaults to prior behaviour, C1) — now proven by -`unknown_optional_field_is_tolerated_byte_identically` (an unknown optional key -loads byte-identically and runs bit-identically). Tier-2 (must-understand: a new -node type, edge semantics, or structural-axis kind) bumps `format_version` so an -old reader refuses cleanly (`LoadError::UnsupportedVersion` / `UnknownNodeType`, -already green). The per-section required-flag scheme is deferred (no current -Tier-2 section to validate it; recorded on #156). -**Remaining** -``` - -- [ ] **Step 5: Drop #156 from the C24 Remaining list** - -In `docs/design/INDEX.md`, the Remaining block (line 1824) currently reads: - -``` -(each its own milestone issue): the forward-compat *must-understand* gate (#156); -``` - -Replace that exact line with: - -``` -(each its own milestone issue): -``` - -This leaves the #158 / #159 entries on lines 1825-1827 intact. Do NOT touch the -other four `#156` occurrences (`blueprint_serde.rs:15-16`, `blueprint_serde.rs:111` -in the reworded doc, `aura-std/src/vocabulary.rs:18`, `INDEX.md:1831`) — they are -semantically distinct and stay. - -- [ ] **Step 6: Full workspace gate** - -Run: `cargo test --workspace` -Expected: PASS — all suites green, including the new -`unknown_optional_field_is_tolerated_byte_identically`. No regressions. - -- [ ] **Step 7: Lint gate** - -Run: `cargo clippy --workspace --all-targets -- -D warnings` -Expected: clean (no warnings). diff --git a/docs/specs/0090-blueprint-forward-compat.md b/docs/specs/0090-blueprint-forward-compat.md deleted file mode 100644 index 079afae..0000000 --- a/docs/specs/0090-blueprint-forward-compat.md +++ /dev/null @@ -1,204 +0,0 @@ -# Blueprint format forward-compatibility: the two-tier discipline — Design Spec - -**Date:** 2026-06-30 -**Status:** Draft — awaiting user spec review -**Authors:** orchestrator + Claude - -## Goal - -Close #156: guarantee the blueprint data format (C24) extends **additively** — -later additions land without breaking existing serializations — under a single, -codified two-tier discipline: - -- **Tier-1 (additive-optional, ignore / default-safe).** A new optional field or - section is tolerated by an older reader: serde ignores unknown fields (no - `deny_unknown_fields` anywhere — verified `rg = 0`), `skip_serializing_if` - omits absent options, and **no `format_version` bump** occurs. Constraint (C1): - a new optional field's default equals prior behaviour, so an old blueprint - loaded by a new engine reproduces the **same** graph. -- **Tier-2 (load-bearing, must-understand).** A new node type, edge semantics, or - structural-axis kind **must bump `format_version`**, so an old reader refuses - cleanly (`LoadError::UnsupportedVersion`) rather than silently building a - different graph. An out-of-vocabulary node likewise refuses - (`LoadError::UnknownNodeType`). - -Most of #156 already shipped with #155 (cycle 0087). This cycle is a small -**verification-plus-codification** iteration: it adds the **one** missing proof — -forward-tolerance of an unknown optional field on the current loader — and writes -the discipline durably into the `LoadError` doc and the C24 ledger entry. It is -**not** a new mechanism. - -### What #155 already delivers (verified, green — the grounding evidence) - -- The `format_version` envelope: `BLUEPRINT_FORMAT_VERSION = 1`, `BlueprintDoc { - format_version, blueprint }` (`blueprint_serde.rs:17-24`). -- Canonical omit-defaults form: `#[serde(default, skip_serializing_if = ...)]` on - `CompositeData::{edges,input_roles,output}` and `PrimitiveData::{name,bound}`. - Pinned by `signal_serializes_to_canonical_golden`. -- **Both Tier-2 refusals at the load path**, each with a green test: - - `LoadError::UnsupportedVersion { found, supported }` — - `blueprint_serde.rs:162-167`, test `unsupported_version_fails_named` - (`format_version: 2` → refused). - - `LoadError::UnknownNodeType(String)` — `blueprint_serde.rs:128`, test - `unknown_node_type_fails_named` (`"NoSuchNode"` → refused). - -There is **no CLI `load-a-blueprint-JSON` command** today; the version-refusal has -no CLI surface, so the engine-level typed `LoadError` **is** the Tier-2 contract. -The CLI *build* path (`aura graph build`) already exit-codes an unknown node type -via `OpError` (cycle 0088). This cycle adds no CLI surface. - -## Architecture - -The forward-compat guarantee is a **property of the existing serde projection**, -not a new component. Tier-1 tolerance is already true (serde's default -unknown-field handling + the omit-defaults discipline); it is simply **unproven**. -The deliverable is therefore: - -1. one **executable proof** (the Tier-1 forward-tolerance test), and -2. a **documentation codification** turning the currently-deferred - "must-understand horizon … is #156" note into the settled two-tier rule. - -No struct, signature, or runtime behaviour changes. The derived design fork (the -per-section required-flag / PNG critical-vs-ancillary mechanism) is **not built** -— recorded on #156 with its rationale: the version envelope already gives the -must-understand refusal at version granularity, there is no current Tier-2 section -to validate a finer scheme against, and C1 holds either way. - -## Concrete code shapes - -### The deliverable: the Tier-1 forward-tolerance test (new — the additive property as executable evidence) - -Added to the `blueprint_serde.rs` test module (it already has `run_recording`, -`sink_free_sma_cross_signal`, and `std_vocabulary` in scope): - -```rust -#[test] -fn unknown_optional_field_is_tolerated_byte_identically() { - // Forward-compat (C24 / #156, Tier-1): a future writer that has additively - // extended the format emits optional keys the CURRENT loader does not know. - // The loader must silently ignore them and reconstruct the IDENTICAL graph — - // never refuse, never let an unknown key leak into the built blueprint. serde - // ignores unknown fields by default (no deny_unknown_fields). This pins the - // additive property the canonical-golden + omit-defaults discipline claims but - // no existing test exercises. - let canonical = - blueprint_to_json(&crate::test_fixtures::sink_free_sma_cross_signal()).expect("serializes"); - - // Inject unknown optional keys at two additive shapes a new writer might emit: - // a doc-level section ("metadata") and a per-primitive key ("annotations"). - let with_unknown = canonical - .replacen( - r#"{"format_version":1,"#, - r#"{"format_version":1,"metadata":{"author":"future"},"#, - 1, - ) - .replacen(r#"{"type":"Sub"}"#, r#"{"type":"Sub","annotations":["future"]}"#, 1); - assert_ne!(with_unknown, canonical, "probe must actually inject unknown keys"); - - // tolerated, not refused - let loaded = blueprint_from_json(&with_unknown, &|t| aura_std::std_vocabulary(t)) - .expect("an unknown optional field is tolerated, not refused"); - - // the unknown keys did not leak into the graph: re-serialization equals the - // canonical form WITHOUT them (byte-identical reconstruction). - assert_eq!(blueprint_to_json(&loaded).expect("re-serializes"), canonical); - - // and the loaded graph runs bit-identically to the plain canonical one (C1). - let plain = blueprint_from_json(&canonical, &|t| aura_std::std_vocabulary(t)).expect("loads"); - assert_eq!( - run_recording(loaded), - run_recording(plain), - "run diverged under an unknown optional field", - ); -} -``` - -### The codification: `LoadError::UnsupportedVersion` doc (before → after) - -Before (`blueprint_serde.rs:110-113`): - -```rust - /// `format_version` the loader does not understand. The full must-understand - /// horizon (multiple versions, unknown-section policy) is #156; #155 supports - /// exactly one version and refuses any other, cleanly. - UnsupportedVersion { found: u32, supported: u32 }, -``` - -After (the discipline is now settled, not deferred): - -```rust - /// `format_version` the loader does not understand — the **Tier-2 - /// (must-understand)** refusal. The format extends under a two-tier discipline - /// (#156): a **Tier-1** additive-optional change (a new optional field / - /// section) does NOT bump the version — an older reader tolerates it (serde - /// ignores unknown fields; see `unknown_optional_field_is_tolerated_byte_identically`) - /// and, by C1, a new optional field defaults to prior behaviour. A **Tier-2** - /// load-bearing change (a new node type, edge semantics, or structural-axis - /// kind) MUST bump `BLUEPRINT_FORMAT_VERSION`, so an old reader refuses here - /// rather than silently building a different graph (C1 / C18). The - /// per-section required-flag scheme is deferred until a real sub-version Tier-2 - /// addition needs finer granularity than a version bump. - UnsupportedVersion { found: u32, supported: u32 }, -``` - -(The `BLUEPRINT_FORMAT_VERSION` doc at `:15-17` already states "Bumped only by a -load-bearing (Tier-2) change; additive optional fields do not bump it (#156)" and -needs no change — it is consistent with the settled discipline.) - -### The codification: C24 ledger Status block (before → after, shape only) - -The C24 entry's `**Remaining**` list (`INDEX.md:1823-1827`) names "the -forward-compat *must-understand* gate (#156)". This cycle moves #156 out of -Remaining into a shipped note stating the two-tier discipline and the proof test. -Exact prose is the planner's; the shape: drop #156 from Remaining, add one -sentence to Status that the forward-compat discipline is codified (Tier-1 -serde-default tolerance proven; Tier-2 = version/vocabulary refusal, already -green) — leaving #158 and #159 in Remaining. - -## Components - -- `crates/aura-engine/src/blueprint_serde.rs` — add one test - (`unknown_optional_field_is_tolerated_byte_identically`); reword the - `LoadError::UnsupportedVersion` doc comment. No code/behaviour change. -- `docs/design/INDEX.md` — C24 Status/Remaining update (codification). - -## Data flow - -Unchanged. `blueprint_from_json` → `serde_json::from_str::` (ignores -unknown fields) → version check → `reconstruct` (resolves each `type_id` through -the injected vocabulary). The test exercises exactly this path with an -unknown-key-bearing input. - -## Error handling - -Unchanged and already complete: `LoadError::{Json, UnsupportedVersion, -UnknownNodeType}` — typed, named, never a panic, never a silent wrong graph. The -two Tier-2 arms are the must-understand refusals; the new test asserts the Tier-1 -path does **not** error. - -## Testing strategy - -- **New (Tier-1, the gap):** `unknown_optional_field_is_tolerated_byte_identically` - — an unknown optional key at doc and primitive level is ignored; the - reconstructed graph re-serializes byte-identically to the canonical form and - runs bit-identically (C1). -- **Existing (Tier-2, cited as grounding evidence, unchanged):** - `unsupported_version_fails_named`, `unknown_node_type_fails_named`. -- Full workspace gate: `cargo test --workspace`; lint `cargo clippy --workspace - --all-targets -- -D warnings`. - -## Acceptance criteria - -- [ ] **(a) Tier-1 additive forward-tolerance** — a blueprint JSON carrying an - unknown optional field/section loads on the current loader, the unknown key does - not leak into the graph (byte-identical re-serialization), and the run is - bit-identical. Pinned by the new test. -- [ ] **(b) Tier-2 must-understand refusal** — a too-new `format_version` and an - out-of-vocabulary node each yield a clean, named `LoadError` (never a panic, - never a silent graph change). Already green (`unsupported_version_fails_named`, - `unknown_node_type_fails_named`); unchanged by this cycle. -- [ ] The two-tier discipline is codified in the `LoadError` doc and the C24 - ledger entry; the deferred per-section required-flag mechanism is recorded as - out-of-scope on #156. -- [ ] `cargo test --workspace` green; `cargo clippy --workspace --all-targets -- -D - warnings` clean. diff --git a/fieldtests/milestone-topology-as-data/Cargo.lock b/fieldtests/milestone-topology-as-data/Cargo.lock new file mode 100644 index 0000000..7f4ef0b --- /dev/null +++ b/fieldtests/milestone-topology-as-data/Cargo.lock @@ -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" diff --git a/fieldtests/milestone-topology-as-data/Cargo.toml b/fieldtests/milestone-topology-as-data/Cargo.toml new file mode 100644 index 0000000..fbf6e58 --- /dev/null +++ b/fieldtests/milestone-topology-as-data/Cargo.toml @@ -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" } diff --git a/fieldtests/milestone-topology-as-data/FINDINGS.md b/fieldtests/milestone-topology-as-data/FINDINGS.md new file mode 100644 index 0000000..b7b9cbc --- /dev/null +++ b/fieldtests/milestone-topology-as-data/FINDINGS.md @@ -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": })`); `--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 ` (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`, + 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 | diff --git a/fieldtests/milestone-topology-as-data/mt_1_roundtrip.rs b/fieldtests/milestone-topology-as-data/mt_1_roundtrip.rs new file mode 100644 index 0000000..b304788 --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_1_roundtrip.rs @@ -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 ` (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)>(); + 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, + ); + 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 = 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)."); +} diff --git a/fieldtests/milestone-topology-as-data/mt_1_rust_blueprint.json b/fieldtests/milestone-topology-as-data/mt_1_rust_blueprint.json new file mode 100644 index 0000000..45b7b58 --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_1_rust_blueprint.json @@ -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"}]}} \ No newline at end of file diff --git a/fieldtests/milestone-topology-as-data/mt_2_blueprint.cli.json b/fieldtests/milestone-topology-as-data/mt_2_blueprint.cli.json new file mode 100644 index 0000000..a8c9d74 --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_2_blueprint.cli.json @@ -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"}]}} diff --git a/fieldtests/milestone-topology-as-data/mt_2_sma_cross.ops.json b/fieldtests/milestone-topology-as-data/mt_2_sma_cross.ops.json new file mode 100644 index 0000000..53e7cac --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_2_sma_cross.ops.json @@ -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"} +] diff --git a/fieldtests/milestone-topology-as-data/mt_3_node.out b/fieldtests/milestone-topology-as-data/mt_3_node.out new file mode 100644 index 0000000..455b271 --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_3_node.out @@ -0,0 +1,43 @@ +## --node SMA +SMA + in series:F64 + out value:F64 + param length:I64 (bind {"I64": }) +[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": }) +[exit=0] + +## --node EMA +EMA + in series:F64 + out value:F64 + param length:I64 (bind {"I64": }) +[exit=0] + +## --node RollingMax +RollingMax + in series:F64 + out value:F64 + param length:I64 (bind {"I64": }) +[exit=0] + +## --node LinComb +aura: unknown node type "LinComb" +[exit=2] + +## --node Recorder +aura: unknown node type "Recorder" +[exit=2] + diff --git a/fieldtests/milestone-topology-as-data/mt_3_partial.ops.json b/fieldtests/milestone-topology-as-data/mt_3_partial.ops.json new file mode 100644 index 0000000..e306b8a --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_3_partial.ops.json @@ -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"} +] diff --git a/fieldtests/milestone-topology-as-data/mt_3_unwired.out b/fieldtests/milestone-topology-as-data/mt_3_unwired.out new file mode 100644 index 0000000..fd0c48d --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_3_unwired.out @@ -0,0 +1,2 @@ +sub.rhs:F64 +[exit=0] diff --git a/fieldtests/milestone-topology-as-data/mt_3_vocabulary.out b/fieldtests/milestone-topology-as-data/mt_3_vocabulary.out new file mode 100644 index 0000000..c63d6ae --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_3_vocabulary.out @@ -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 diff --git a/fieldtests/milestone-topology-as-data/mt_4_forward_compat.rs b/fieldtests/milestone-topology-as-data/mt_4_forward_compat.rs new file mode 100644 index 0000000..9dc6244 --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_4_forward_compat.rs @@ -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)>(); + 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, + ); + 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)."); +} diff --git a/fieldtests/milestone-topology-as-data/mt_5a_unknown_type.err b/fieldtests/milestone-topology-as-data/mt_5a_unknown_type.err new file mode 100644 index 0000000..29008cb --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_5a_unknown_type.err @@ -0,0 +1 @@ +aura: op 1 (add): unknown node type "FOO" diff --git a/fieldtests/milestone-topology-as-data/mt_5a_unknown_type.exit b/fieldtests/milestone-topology-as-data/mt_5a_unknown_type.exit new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_5a_unknown_type.exit @@ -0,0 +1 @@ +2 diff --git a/fieldtests/milestone-topology-as-data/mt_5a_unknown_type.ops.json b/fieldtests/milestone-topology-as-data/mt_5a_unknown_type.ops.json new file mode 100644 index 0000000..36e3305 --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_5a_unknown_type.ops.json @@ -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"} +] diff --git a/fieldtests/milestone-topology-as-data/mt_5b_cycle.err b/fieldtests/milestone-topology-as-data/mt_5b_cycle.err new file mode 100644 index 0000000..6bb6aee --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_5b_cycle.err @@ -0,0 +1 @@ +aura: op 5 (connect): connecting b.value -> a.rhs would close a cycle diff --git a/fieldtests/milestone-topology-as-data/mt_5b_cycle.exit b/fieldtests/milestone-topology-as-data/mt_5b_cycle.exit new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_5b_cycle.exit @@ -0,0 +1 @@ +2 diff --git a/fieldtests/milestone-topology-as-data/mt_5b_cycle.ops.json b/fieldtests/milestone-topology-as-data/mt_5b_cycle.ops.json new file mode 100644 index 0000000..4e58e82 --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_5b_cycle.ops.json @@ -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"} +] diff --git a/fieldtests/milestone-topology-as-data/mt_5c_unconnected.err b/fieldtests/milestone-topology-as-data/mt_5c_unconnected.err new file mode 100644 index 0000000..8dbda67 --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_5c_unconnected.err @@ -0,0 +1 @@ +aura: finalize: slot sub.rhs is unconnected diff --git a/fieldtests/milestone-topology-as-data/mt_5c_unconnected.exit b/fieldtests/milestone-topology-as-data/mt_5c_unconnected.exit new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_5c_unconnected.exit @@ -0,0 +1 @@ +2 diff --git a/fieldtests/milestone-topology-as-data/mt_5c_unconnected.ops.json b/fieldtests/milestone-topology-as-data/mt_5c_unconnected.ops.json new file mode 100644 index 0000000..3d24a9c --- /dev/null +++ b/fieldtests/milestone-topology-as-data/mt_5c_unconnected.ops.json @@ -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"} +]