8688a60ded
The single-file ledger had grown to 2968 lines / ~42k tokens, mixing current design law with accreted history: 59 cycle-stamped realization blocks, 18 [HISTORY] passages, 22 supersession markers, and the C10 / C22 / C24 reframe sagas layered several supersessions deep. A code-grounding audit (31 agents, adversarially verified) confirmed 11 defects stated as current truth: stale crate homes from the C28 #288 roster split (cost nodes, PositionManagement, PositionEvent, Session), the renamed InputSpec->PortSpec, the pre-#241 project model in C16 and the open-threads section, a stale HarnessKind retirement deferral in C24, and three C28-internal inconsistencies. New shape, per the ailang precedent: - INDEX.md stays the sole addressable entry point: foundation, external components, a C-id-keyed contract map (one line per contract), and only the genuinely open architectural threads. - contracts/cNN-<slug>.md carries each contract's current truth only: Guarantee / Forbids / Why with ratified refinements integrated, plus a code-anchored Current state. All confirmed defects are fixed here; crate anchors were re-verified against the tree. - contracts/cNN-<slug>.history.md (18 sidecars) and INDEX.history.md preserve every superseded block verbatim, stamps and issue refs intact, under a frozen-record banner. Nothing was deleted: superseded design intent remains an addressable working-tree artifact, off the per-cycle audit walk. - Ledger discipline is now stated in INDEX.md: live files are edited in place at cycle close, superseded text moves verbatim to the sidecar, and a supersession marker in a live file is itself an audit finding. Every contract file was verified against its old text by an independent zero-loss pass (statement-by-statement) plus a code-accuracy spot check; C-ids and contract titles are unchanged, so existing C-id citations in code, tests, and issues resolve as before.
301 lines
20 KiB
Markdown
301 lines
20 KiB
Markdown
# C24 — The blueprint is a serializable, World-owned data value (the topology data format)
|
||
|
||
**Guarantee.** A **blueprint** — the param-generic, named graph-as-data a Rust
|
||
builder produces (C9/C19) — is a **first-class serializable data value** with a
|
||
stable, versioned format, and the engine has a **load path** (data → blueprint →
|
||
`FlatGraph`, C23), not only the Rust-builder construction path. Topology is
|
||
therefore a **value the World owns** (C21): constructed, **generated**, mutated,
|
||
**structurally searched**, serialized for **reproduction** (C18), and (optionally)
|
||
rendered or edited by a visual face (C22). `model_to_json` (C9) is the **existing
|
||
reverse half** (blueprint → data, for render); this contract closes the loop. The
|
||
format carries **topology + structural axes + the param-space**, referencing nodes
|
||
by their **compiled-in type identity** — the closed, typed node vocabulary of the
|
||
std set plus the project `cdylib` (C8/C16) — and carries **no node logic**
|
||
(computation is compiled Rust, C17). It is **non-Turing-complete by construction**
|
||
(a static DAG, no control flow), so it structurally cannot become a second RustAst.
|
||
|
||
**Forbids.** Putting **node logic / computation** in the format (the RustAst trap —
|
||
logic is Rust, C17); a **Turing-complete or control-flow-bearing** blueprint format
|
||
(it is static data; the *generator* that emits it may be Rust, C20, but the artifact
|
||
is not a language); a **by-name node marketplace / registry** (the vocabulary is the
|
||
compiled-in closed set referenced by type identity — the format is not a distribution
|
||
mechanism, C9/C16); treating the **visual face (C22) as the motivation or the
|
||
authoring brain** (the format is required by C21/C18 at the CLI level, independent of
|
||
any UI); a **generated / searched run whose topology is not recoverable from its
|
||
manifest** (C18 reproduction); baking topology as **engine source** compiled into the
|
||
binary. The hard-wired `aura-cli` harnesses (`HarnessKind`, `run_r_sma`, `r_sma_graph`,
|
||
`sample_blueprint_with_sinks`, the built-in `*_sweep_family`) were pre-C24 scaffolding,
|
||
a C16 tension, and are now **retired** (#159/#220): topology reaches the engine only as
|
||
data, resolved by the injected vocabulary. The `wrap_r` R-scaffolding wrap
|
||
(`aura-runner`) **survives** — it is the r-sma wrap every `run` / `sweep` / `mc` /
|
||
`walkforward` path applies to the loaded signal at run (supplied at run, not serialized);
|
||
its full R-scaffolding retirement stays deferred (#159). Harness input roles bind archive
|
||
columns **by name**, not by a single baked-in price weld (C26).
|
||
|
||
**Why.** The World (C21) is the product, and it cannot orchestrate, **structurally
|
||
search**, or **reproduce** *families that vary topology* while topology stays opaque
|
||
Rust source baked into the engine. The **game-engine principle** (C16's engine / game
|
||
split) makes it concrete: the engine is compiled native systems; the "game" is
|
||
**content** — and topology *is* content, a data value the engine owns, serializes,
|
||
instantiates, and mutates, exactly as a game engine owns its scene / prefab /
|
||
blueprint graphs. The #109 fork was never "*who types the wiring*" — a small LLM
|
||
cannot reliably *apply* a DSL and a strong one does not need one, so a
|
||
manifest-as-authoring-surface dies on both horns (C17). It is "**is topology a
|
||
compile-time *source* artifact or a runtime, serializable, World-owned *value*?**",
|
||
and determinism (C1) + reproduction (C18) + structural search (the open search-policy
|
||
thread) force the **value**. The engine already treats topology as a runtime value
|
||
(C9 graph-as-data, C19 "cheap graph re-compilation, not a code recompile", C23 the
|
||
flat graph as the optimisation target); C24 only adds that the value **serializes out
|
||
of Rust and loads back in**.
|
||
|
||
## Current state
|
||
|
||
The **principle** is settled — the #109 resolution, ratified in an in-context design
|
||
discussion. The format, its loader, the construction service, and the family
|
||
orchestration all ship. The acceptance criterion is green: a serialized blueprint
|
||
runs **bit-identical** (C1) to its Rust-built twin.
|
||
|
||
### The format and load path
|
||
|
||
A `Composite` serializes to a **canonical, versioned** data value — a
|
||
`format_version` envelope (`BlueprintDoc`), omit-defaults JSON — and **loads back**
|
||
(data → blueprint → `FlatGraph`) via `blueprint_to_json` / `blueprint_from_json`
|
||
(`aura-engine::blueprint_serde`). Nodes are referenced by **compiled-in type
|
||
identity** through an **injected resolver** whose concrete closed `match` over the
|
||
std vocabulary lives outside the engine (`aura-vocabulary::std_vocabulary`), so the
|
||
engine stays domain-free, with no node registry (invariant 9). An unknown type fails
|
||
clean as `LoadError::UnknownNodeType` — the data-plane face of invariant 9, never a
|
||
silent wrong graph. `model_to_json` (C9) remains the render half; this closes the
|
||
loop for the round-trippable vocabulary.
|
||
|
||
**Out of the round-trippable set** (deliberate; fails clean as `UnknownNodeType`):
|
||
recording sinks (they capture an `mpsc::Sender` — runtime identity, not param-generic
|
||
data, C19) and construction-arg builders (`LinComb` / `CostSum` / `SimBroker` /
|
||
`Session` — structural-axis args, a C20 concern), additively addable later (#156).
|
||
|
||
### Runs and families are built FROM blueprint-data
|
||
|
||
`aura run <blueprint.json>` loads a serialized **signal** blueprint and emits a
|
||
`RunReport` **bit-identical** (C1) to its Rust-built twin; the run scaffolding (sinks
|
||
/ broker / data) is supplied **at run**, not serialized. Beyond a single run, the
|
||
World constructs and orchestrates **families** of harnesses from topology-data:
|
||
|
||
- `aura sweep <blueprint.json> --axis <name>=<csv>` → `FamilyKind::Sweep`. A sweep
|
||
needs an **open** blueprint (a fully-bound one has an empty `param_space`).
|
||
- `aura mc <blueprint.json> --seeds N` → `FamilyKind::MonteCarlo`, each seed a
|
||
distinct synthetic price walk drawn disjoint-parallel through the engine
|
||
`monte_carlo` seam (invariant 1). MC binds no axis, so it needs a **closed**
|
||
blueprint (the sweep's distinction inverted); an open one returns a named `Err`,
|
||
rendered exit-2 at the builder boundary — no hidden exit in the pure builder.
|
||
- `aura walkforward <blueprint.json> --axis <name>=<csv> [--select
|
||
argmax|plateau:mean|plateau:worst]` → `FamilyKind::WalkForward`: re-optimizes the
|
||
loaded blueprint's params over the `--axis` grid on each 24/12/12 IS window,
|
||
selects the winner by `sqn_normalized`, runs it out-of-sample. Reduce-mode members
|
||
are R-measured (`oos_r` the meaningful summary; stitched pip-equity empty, C10).
|
||
|
||
The family builders live in `aura-runner::family` (`blueprint_sweep_family` /
|
||
`blueprint_mc_family` / `blueprint_walkforward_family`). Every member manifest carries
|
||
the **shared** `topology_hash` (one signal topology, only params vary; `member_key`
|
||
distinguishes members), and each family stores its blueprint(s) content-addressed so
|
||
`aura reproduce` re-derives every member bit-identically (`aura-runner::reproduce`,
|
||
C18). The synthetic-walk DGP is the MC machinery, not trader-grade statistics; a
|
||
real-data block-bootstrap — and retiring the `synthetic_walk_sources` `len:60`↔warm-up
|
||
coupling — rides #172.
|
||
|
||
### Reproduction identity
|
||
|
||
The `RunManifest` carries a **`topology_hash`**: SHA256 of the canonical
|
||
`blueprint_to_json` (see *Canonical form*), the #158 reproducibility anchor, a Tier-1
|
||
optional field (#156). The hash and its helper live research-side (`aura-cli` +
|
||
`sha2`), off the frozen engine (invariant 8). The hash covers the **signal** only —
|
||
the fixed scaffolding is identified by `commit`; a content-id distinguishing
|
||
structural-axis / whole-harness variants is a #158/#166 concern once scaffolding is
|
||
itself blueprint-data. The **identity id** (#171) is an additive sibling:
|
||
`blueprint_identity_json` (`aura-engine::blueprint_serde`) + `graph introspect
|
||
--identity-id`. The byte-exact content id keeps the store/reproduce roles; the
|
||
identity id is introspection-only until a dedup consumer exists.
|
||
|
||
### Axis discovery
|
||
|
||
`aura sweep <blueprint.json> --list-axes` lists a loaded blueprint's open sweepable
|
||
knobs (one `<name>:<kind>` per line, `param_space()` order) and exits; the printed
|
||
names are exactly what `--axis` binds. Every listed name is **mandatory** on `sweep` /
|
||
`walkforward` — the blueprint must be fully bound before it runs — so a subset grid is
|
||
refused with the missing knob named (`BindError::MissingKnob`, `aura-engine`) and
|
||
there is no default; pin a knob you do not want to vary with a single-value axis. A
|
||
single `blueprint_axis_probe` (`aura-runner`) single-sources the wrapped probe for the
|
||
sweep terminal, the MC closed-check, and the listing, so **listed == swept by
|
||
construction** (and stays so across the harness retirement — the listing tracks
|
||
whatever the sweep actually resolves). The names are prefixed by the current wrapping
|
||
(`sma_signal.fast.length`, the nested-composite prefix), which is why discovery lives
|
||
on the sweep verb (it owns the wrapping), not `graph introspect`. `--trace` on
|
||
`sweep` / `walkforward` writes per-member traces on the real-data campaign path
|
||
(depth-2 fan-out, chartable by the printed family handle, #224); the synthetic path
|
||
still refuses (`run` / `mc` refuse `--trace` outright). The live trace-writer is the
|
||
campaign `presentation.persist_taps` (`persist_campaign_traces`, `aura-runner::runner`).
|
||
|
||
### The construction service (op-script)
|
||
|
||
`aura graph build` / `introspect` over a JSON op-list (#157) is the introspectable,
|
||
replayable, by-identifier construction surface; it **emits** the serializable
|
||
blueprint. The engine `GraphSession` / `replay` (`aura-engine::construction`) build a
|
||
runnable blueprint through validated ops. Construction gates split into **eager**
|
||
per-op checks — name resolution, edge kind-match (`edge_kind_check`), the double-wire
|
||
arm, param bind, and acyclicity (a `connect` that would close a cycle is rejected
|
||
eagerly at the closing op, `GraphSession::closes_cycle`, #161) — and **holistic**
|
||
finalize checks — wiring totality (`validate_wiring`), param-namespace injectivity
|
||
(`check_param_namespace_injective`), root-role boundness (`check_root_roles_bound`).
|
||
Both cadences call the **same extracted predicates** — no second validator. The
|
||
holistic faults read **by-identifier** (#162): `finish()` translates the
|
||
index-carrying `CompileError`s (`UnconnectedPort` / `RoleKindMismatch` /
|
||
`UnboundRootRole`) into by-identifier `OpError` variants while still *calling* the
|
||
unchanged holistic gates. Build-free introspection answers over the closed vocabulary:
|
||
`graph introspect --vocabulary | --node <T> | --unwired`. The engine `Op` stays
|
||
serde-free; the wire DTO (`OpDoc`) is CLI-side (`aura-cli::graph_construct`).
|
||
Acceptance: a graph built purely through the ops compiles identical (C1) to its
|
||
Rust-built twin, an invalid op is rejected at the op naming the cause, introspection
|
||
answers without a build.
|
||
|
||
**Invariant-5 lockstep.** The eager acyclicity gate is a *second* home of invariant 5
|
||
alongside the bootstrap Kahn sort (`harness.rs`, `BootstrapError::Cycle`); the two
|
||
must co-evolve when the explicit delay/register node — invariant 5's sole legal
|
||
feedback — lands, or a valid delay-feedback graph the bootstrap accepts would be
|
||
rejected at construction.
|
||
|
||
### Op-script grammar
|
||
|
||
The op-script is a JSON **array of ops**, each object internally tagged by `"op"`,
|
||
replayed in order; nodes are referenced **by identifier**, ports as dotted
|
||
`<identifier>.<port>`. The eight verbs:
|
||
|
||
- `source` — `{"op":"source","role":<str>,"kind":<ScalarKind>}` — declare a root
|
||
source role producing a base column of `kind`.
|
||
- `input` — `{"op":"input","role":<str>}` — declare a root input role (kind inferred
|
||
from the slots it feeds).
|
||
- `add` — `{"op":"add","type":<TypeId>,"name":<str>?,"bind":{<param>:<Scalar>}?}` —
|
||
add a node of compiled-in type identity `type`; **`name` is its identifier**
|
||
(mirrors the builder's `.named(...)`; defaults to the lowercased type label, so
|
||
two unnamed nodes of one type collide); `bind` sets params.
|
||
- `feed` — `{"op":"feed","role":<str>,"into":[<port>,…]}` — fan a root role into
|
||
interior input slots.
|
||
- `connect` — `{"op":"connect","from":<port>,"to":<port>}` — wire an interior output
|
||
field to an input slot; a `connect` closing a cycle is rejected eagerly
|
||
(invariant 5).
|
||
- `expose` — `{"op":"expose","from":<port>,"as":<str>}` — promote an interior output
|
||
field to a boundary output, aliased by `as` (a real alias, not a naming; contrast
|
||
`add`'s `name`) — one of the two verbs that keep `as` (with `tap`).
|
||
- `tap` — `{"op":"tap","from":<port>,"as":<str>}` — declare a measurement **tap** on
|
||
an interior output field under `as` (#284) — the output-side twin of `expose`: a
|
||
recorded observation point (a `Composite.taps` entry, C27), not a boundary output.
|
||
Name-addressed like every other op (no raw index); tap names are their own
|
||
namespace (a duplicate refuses). The `finish` gate threads op-declared taps into
|
||
the built `Composite` (`.with_taps`); a single `aura run` records each, a sweep
|
||
leaves them inert.
|
||
- `gang` — `{"op":"gang","as":"channel_length","into":["channel_hi.length","channel_lo.length"]}`
|
||
— fuse two or more sibling params into ONE public knob: the member addresses
|
||
leave the sweepable param space and `as` replaces them; the bound or swept
|
||
value fans out to every member at bootstrap. Members must share one scalar
|
||
kind and stay open (un-bound).
|
||
|
||
Value forms are the serialized representations: a `Scalar` bind value is the typed tag
|
||
form `{"I64":2}` / `{"F64":0.5}` / `{"Bool":true}` / `{"Timestamp":<i64>}`, and `kind`
|
||
is the capitalized `ScalarKind` (`"F64"`). Worked example:
|
||
`fieldtests/cycle-0088-construction-op-script/c0088_1_sma_crossover.json` (an
|
||
SMA-crossover bias).
|
||
|
||
### Canonical form (#164)
|
||
|
||
The canonical blueprint artifact is exactly the bytes `blueprint_to_json` returns — a
|
||
JSON value with **no trailing newline** (`serde_json::to_string`, not framed with a
|
||
display `println!`), so the CLI and library emit paths are **byte-identical** — a
|
||
prerequisite for content-addressed topology identity (#158, a hash over the canonical
|
||
form). The canonical JSON is also the blueprint's **equality / identity surface**:
|
||
`Composite` (`aura-engine::blueprint`) and `PrimitiveBuilder` (`aura-core::node`)
|
||
deliberately carry **no in-memory `PartialEq` / `Debug`** — the recipe holds a
|
||
`build: Box<dyn Fn>` closure (non-comparable, non-printable), so equality could only be
|
||
defined over the serialized *data*, of which `blueprint_to_json` is already the single
|
||
source; a second in-memory notion would be a drift hazard against the very form #158
|
||
content-addresses. The loader stays lenient (a trailing newline on input still parses,
|
||
Tier-1 robustness).
|
||
|
||
### Forward-compat: the two-tier discipline (#156)
|
||
|
||
Tier-1 (additive-optional) is serde-default silent-ignore with **no** `format_version`
|
||
bump — a new optional field defaults to prior behaviour (C1). 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`).
|
||
**Pre-ship dormancy (#61):** until the first external ship there are no out-of-repo
|
||
readers — reader and writer change atomically in one commit — so the Tier-2 bump
|
||
discipline is dormant and structurally-semantic additions (the `gangs` section) land as
|
||
additive-optional fields of v1; the first ship consciously freezes v1, gangs included,
|
||
and activates the bump discipline. The per-section required-flag scheme is deferred (no
|
||
current Tier-2 section to validate it; recorded on #156).
|
||
|
||
### Enforcement shift — invariant 9 on the data plane
|
||
|
||
Lifting topology onto the data plane relocates *where* the engine/project boundary
|
||
(invariant 9, no bespoke node registry/marketplace) is enforced, without changing what
|
||
it forbids. **Pre-C24** a blueprint referencing another project's node was a **compile
|
||
error** — topology was in-process Rust, so cross-boundary node resolution was
|
||
*structurally impossible*, compiler-guaranteed for free. **Post-C24** that same
|
||
reference is a **type-id string in a serialized blueprint**, refused only by the
|
||
injected resolver's closed set (`UnknownNodeType`). The anti-NIH rationale survives
|
||
intact (node *logic* stays cargo+Gitea-only; the format distributes
|
||
**topology-as-content**, never logic — C17), and `std_vocabulary` stays a compiled-in
|
||
dispatch table, not a registry — but its enforcement **degrades from
|
||
compiler-guaranteed to resolver-seam discipline**. Two consequences:
|
||
|
||
1. **#160** (the guard that the closed vocabulary stays honest) is no longer droppable
|
||
hygiene — it guards the data-plane face of the boundary (its own drift direction
|
||
still fails *safe*, `UnknownNodeType`). The `std_vocabulary_roster!` macro
|
||
(`aura-vocabulary`) expands the resolver `match` and the enumerable
|
||
`std_vocabulary_types()` list from **one roster**, closing the resolver-vs-list
|
||
drift mode by construction; the residual (a new zero-arg node never rostered at all)
|
||
stays fail-safe, guarded by the one-line maintainer surface plus the count pins (the
|
||
in-crate shape test and aura-cli's cross-boundary `--vocabulary` count e2e). The
|
||
per-project **merged resolver** (project ∪ std, project first) lives in
|
||
`aura-runner::project`.
|
||
2. The **injected per-project resolver** (C16) is the genuinely new, *deferred*
|
||
invariant-9 surface: nothing structurally stops a project resolver from accepting
|
||
arbitrary type-ids, or a project from layering a blueprint *marketplace* (store +
|
||
type-discovery API) **on top of** the format — one layer *above* the engine
|
||
contract. Invariant 9 holds **at the engine**; the project/ecosystem boundary (what
|
||
an injected resolver may resolve, where closed-set discipline draws the line) is a
|
||
**World/C21-layer charter point**, not an engine fix.
|
||
|
||
Invariant 8 (frozen) is likewise reframed by C24/C21: from "topology baked at build
|
||
time" to "the World may *generate* topology at runtime (research plane), the chosen
|
||
blueprint is frozen into the deploy artifact, and any run is deterministic once
|
||
instantiated" (C1).
|
||
|
||
### Adjacent / deferred
|
||
|
||
The **project-as-crate load boundary** has landed (`Aura.toml` discovery + `cdylib`
|
||
loading + merged project ∪ std vocabulary; the `aura new` scaffolder emits a buildable
|
||
project whose blueprint runs through the merged vocabulary) — see C13/C16. **Canonical
|
||
project shape (#181):** the `aura new` templates (`scaffold.rs`) are the canonical
|
||
authoring shape and evolve with the engine; the `demo-project` fixture is an
|
||
intentionally frozen known-good twin. The two are deliberately **not** lockstep-guarded
|
||
— no consumer requires them to match, each is e2e-guarded on its fitness for purpose
|
||
(build → descriptor load → charter check → deterministic run; blueprint *wiring* content
|
||
is pinned in neither, stated honestly), and an equality guard would convert every
|
||
deliberate template improvement into forced churn of the frozen fixture (the same
|
||
cross-purpose coupling that rules out regenerating the fixture from the scaffolder).
|
||
What remains open is the **composable-orchestration** thread (#109): topology-as-data is
|
||
the substrate it stands on.
|
||
|
||
## See also
|
||
|
||
- [C9](c09-fractal-composition.md) — graph-as-data; `model_to_json` render half
|
||
- [C16](c16-engine-project-split.md) — engine / project split; the injected resolver
|
||
- [C17](c17-authoring-surface.md) — logic is Rust; the closed-vocabulary-data-artifact clarification
|
||
- [C18](c18-registry.md) — reproduction, the run registry, content-addressed store
|
||
- [C19](c19-bootstrap.md) — bootstrap: blueprint → frozen instance
|
||
- [C21](c21-world.md) — the World owns and orchestrates topology
|
||
- [C23](c23-graph-compilation.md) — bootstrap-as-compilation to `FlatGraph`; the optimisation target
|
||
- [C26](c26-input-binding.md) — role names bind archive columns
|
||
- [C27](c27-declared-taps.md) — declared taps as named measurement points
|
||
- [C1](c01-determinism.md) — determinism / bit-identity; [C10](c10-bias-r-cost.md), [C13](c13-hot-reload-frozen-deploy.md), [C20](c20-strategy-harness.md), [C22](c22-playground-traces.md), [C25](c25-role-model.md), [C28](c28-stratification.md)
|
||
|
||
> History: [c24-blueprint-data.history.md](c24-blueprint-data.history.md)
|