21c1621bd0
"Compilat" (German "Kompilat") was a coined noun for the product of the
bootstrap compilation — neither English nor a natural fit. The runtime
artifact already has a code identifier for exactly this thing: the
`FlatGraph` struct (harness.rs). Replace the coinage with that identifier:
- prose mentions -> "flat graph" (mirrors the type, reads plainly)
- definitional anchors -> `FlatGraph` (C11, C23, the running-graph line)
- `render_compilat` -> `render_flat_graph` (historical render symbol;
keeps the `render_blueprint` / `render_flat_graph`
source-vs-product pairing)
- `intra-compilat` -> `intra-graph`
- `from_compilat` (test) -> `from_flat`
"compilation" / "re-compilation" / "bootstrap-as-compilation" (the process,
ordinary English) are deliberately left untouched. Behaviour-preserving:
only comments, design ledger, specs/plans, one test-local variable and its
assert messages change. Full workspace test suite green; clippy clean.
344 lines
17 KiB
Markdown
344 lines
17 KiB
Markdown
# Unify the Blueprint Main-Graph Render Through the Composite Machinery — Design Spec
|
|
|
|
**Date:** 2026-06-08
|
|
**Status:** Draft — awaiting user spec review
|
|
**Authors:** orchestrator + Claude
|
|
|
|
Source: Gitea issue #48 (exhaustive, ratified in-session). This spec produces
|
|
the design that issue names; it does not re-decide it.
|
|
|
|
## Goal
|
|
|
|
`aura graph --macd` must make the strategy **legible**. Today the main graph
|
|
draws bare nodes and bare edges, so what the strategy computes is invisible:
|
|
|
|
```
|
|
[macd] → [Exposure] → [SimBroker] → [Recorder]
|
|
```
|
|
|
|
Two facts the strategy turns on never reach the page: that the `histogram`
|
|
field (not the macd line, not the signal) drives `Exposure`, and that
|
|
`Exposure`'s behaviour is parameterised by `scale`. Both are already present in
|
|
the model (`Edge.from_field`, `LeafFactory::params()`) and both are already
|
|
rendered for a composite's interior in the `where:` view. The main graph is
|
|
poorer only because it runs a **separate, less capable render path**.
|
|
|
|
This cycle removes that asymmetry: the blueprint main graph renders through the
|
|
**same machinery** as a composite interior, because a `Blueprint` is — for
|
|
render purposes — the *root composite*. After this cycle the main graph carries
|
|
leaf params, multi-input slot stubs, and a `from_field` edge label on every
|
|
multi-output producer.
|
|
|
|
**In scope:** unifying the leaf-label and edge-rendering of the main graph with
|
|
the composite-definition render (`leaf_label`, the edge handling, the slot-stub
|
|
machinery, and a new `from_field` edge label that serves **both** views).
|
|
|
|
**Out of scope (deferred, see Architecture §Deferred):** a harness signature
|
|
header line over the main graph. It is orthogonal to legibility and its exact
|
|
shape is unresolved; it is surfaced at the review gate, not baked here.
|
|
|
|
## Architecture
|
|
|
|
### The asymmetry, precisely
|
|
|
|
Two render functions in `crates/aura-cli/src/graph.rs` consume nearly the same
|
|
data but diverge in capability:
|
|
|
|
- `render_blueprint` (the main graph) — `crates/aura-cli/src/graph.rs:36`.
|
|
Pass 1 labels each top-level leaf with the **bare** `factory.label()`
|
|
(`graph.rs:44`); pass 2 pushes each edge as a bare `(from, to)` pair, dropping
|
|
`from_field` and `slot` (`graph.rs:60`).
|
|
- `render_definition` (a composite interior) — `crates/aura-cli/src/graph.rs:295`.
|
|
Labels each leaf with the **enriched** `leaf_label(c, i, factory)`
|
|
(`graph.rs:299`), which folds in aliased param names and ordered input-slot
|
|
stubs, and folds re-exported outputs onto producers via `output_binding`.
|
|
|
|
The enriched path operates only on a composite's `nodes` + `edges`. Those two
|
|
fields are the **same types** on a `Blueprint`:
|
|
|
|
```
|
|
Composite { name, nodes: Vec<BlueprintNode>, edges: Vec<Edge>,
|
|
input_roles: Vec<Role>, params: Vec<ParamAlias>, output: Vec<OutField> }
|
|
Blueprint { nodes: Vec<BlueprintNode>, edges: Vec<Edge>,
|
|
sources: Vec<SourceSpec> }
|
|
```
|
|
|
|
So the enriched machinery applies to a `Blueprint` unchanged on the interior,
|
|
and differs only at the three **borders**. Each border is a small adapter, not
|
|
a new code path:
|
|
|
|
1. **Entries.** `sources` (typed, bound) play the role `input_roles` (named,
|
|
open) play in a composite. Both already render as an entry-marker node —
|
|
`[source:F64]` is the counterpart of `[price]`. The blueprint pass already
|
|
draws source markers (`graph.rs:49-54`); this is unchanged.
|
|
2. **Per-node params.** A composite re-names a leaf's params through its
|
|
`ParamAlias` list. A blueprint has no alias list — a top-level leaf carries
|
|
its own params, whose names the `LeafFactory` declares pre-build
|
|
(`LeafFactory::params() -> &[ParamSpec]`, `crates/aura-core/src/node.rs:85`;
|
|
#43). So the param **source** differs (factory-declared name vs composite
|
|
alias), the rendering **principle** does not (show the leaf's param names).
|
|
The render stays value-empty (names, not values) — consistent with the
|
|
param-generic blueprint (C11) and with the current `[source:F64]` /
|
|
`[Exposure]` golden.
|
|
3. **Outputs / sinks.** A composite re-exports ports (`OutField`, folded onto
|
|
producers by `output_binding`). A blueprint re-exports nothing; its terminal
|
|
nodes are **sinks**. Per the issue's sink sharpening, a sink is not a type —
|
|
no `trait Sink`/enum exists; the only mark is an empty output schema
|
|
(`output: vec![]`, e.g. `Recorder`, `crates/aura-std/src/recorder.rs`). A
|
|
sink is a leaf like any other that happens to have no outgoing edge, run
|
|
through the same `leaf_label`. **No special handling**, and therefore no new
|
|
border logic — this border is "absence of `output_binding`", which the
|
|
blueprint already gets for free by carrying no `OutField`s.
|
|
|
|
### The `from_field` edge label (serves both views)
|
|
|
|
`Edge.from_field` selects which output field a consumer reads. It is dropped at
|
|
`graph.rs:60` (blueprint) and never rendered in `render_definition` either;
|
|
ascii-dag's per-edge label slot is passed `None` at `graph.rs:87`. The strategy
|
|
fact "Exposure reads the histogram" lives exactly here: the macd→Exposure edge
|
|
carries `from_field: 2` (`crates/aura-cli/src/main.rs:254`), and macd's field 2
|
|
is `histogram` (`OutField`, `crates/aura-cli/src/main.rs:226`).
|
|
|
|
**Rule (default, ratify at review):** an edge renders a field label **only when
|
|
its producer has more than one output field** — mirroring the slot-stub
|
|
convention (`fan_in_identifiers` labels a slot only when a node has >1 wired
|
|
input, `graph.rs:184`). For a single-output producer the label is omitted, so
|
|
the existing golden output is unchanged.
|
|
|
|
The field **name** is resolved from the producer:
|
|
|
|
- Producer is a composite → `Composite::output()[from_field].name`
|
|
(`crates/aura-engine/src/blueprint.rs:119`). The headline case: macd is a
|
|
composite, so `from_field: 2` resolves to `histogram`.
|
|
- Producer is a leaf → pre-build field names are not declared (#43, value-empty
|
|
`LeafFactory`), so the label falls back to the field **index** (e.g. `2`). No
|
|
multi-output leaf exists in the current sample/macd, so the fallback is
|
|
latent, not exercised — recorded so the planner handles it deliberately
|
|
rather than panicking.
|
|
|
|
The label is attached through ascii-dag's existing `add_edge(from, to, label)`
|
|
slot (`graph.rs:87`), shared by `render_flat` and therefore by **both** views.
|
|
|
|
**Implementation risk (planner verifies):** whether ascii-dag's `RenderMode::Vertical`
|
|
renders a per-edge label legibly is not yet confirmed in this codebase. If the
|
|
vertical layout cannot place an edge label cleanly, the fallback that preserves
|
|
the same observable — "the selected field is visible at the macd→Exposure
|
|
connection" — is to surface the field name at the consumer side, adjacent to the
|
|
input that reads it (the slot-stub locus the `#…` machinery already owns). The
|
|
acceptance is form-invariant: the field is visible; the exact locus is a
|
|
planner/implementer call against ascii-dag's actual capability.
|
|
|
|
### Deferred: the harness signature header
|
|
|
|
The issue's invariant note establishes that C12 (a harness is ontologically not
|
|
a node) is the **wrong axis** to deny the main graph a signature — ontology and
|
|
render form are independent. A header such as
|
|
`harness(price:f64) {fast, slow, signal, scale} -> {equity, exposure}` is
|
|
therefore *permissible*. It is nonetheless **deferred**: its exact shape is a
|
|
genuine open choice (sources-and-sinks only vs the full param space; whether a
|
|
root graph needs a title at all the way a `where:`-block does), and it is
|
|
**orthogonal** to this cycle's headline — legibility is delivered by the leaf
|
|
params and the edge label without it. Deferring keeps this spec fork-free. The
|
|
choice is surfaced at the review gate; if wanted, it becomes its own cycle.
|
|
|
|
## Concrete code shapes
|
|
|
|
### User-facing: `aura graph --macd` (the acceptance evidence)
|
|
|
|
The program the user runs is unchanged; its output gains information. Current
|
|
main-graph fragment (value-empty, composites opaque):
|
|
|
|
```
|
|
[macd]
|
|
│
|
|
↓
|
|
[Exposure]
|
|
```
|
|
|
|
After this cycle (shapes shown; exact bytes are a golden re-capture, not
|
|
spec-owned):
|
|
|
|
```
|
|
[macd]
|
|
│ histogram ← from_field: 2 resolved via macd's OutField names
|
|
↓
|
|
[Exposure(scale)] ← factory-declared param name, value-empty
|
|
```
|
|
|
|
`SimBroker`, with two wired input slots (exposure, price), would gain slot stubs
|
|
the same way the composite interior already shows them (e.g. `[SimBroker(#…,#…)]`)
|
|
— **deferred to issue #49**: as shipped, `SimBroker` renders bare, because
|
|
`fan_in_identifiers`/`signature_of` is still composite-coupled and threading the
|
|
blueprint case through it was carved out (user-signed). `Recorder` (no params,
|
|
one input, a sink) is unchanged — a bare terminal leaf.
|
|
The headline is legibility: the histogram selection and `Exposure`'s
|
|
parameterisation are now on the page.
|
|
|
|
The `where:` section is unchanged in spirit. A composite interior already shows
|
|
enriched leaves; the only addition reaching it is the same `from_field` edge
|
|
label, which is a no-op for the all-single-output sample/macd interiors.
|
|
|
|
### Implementation shape: one enriched path over the shared interior
|
|
|
|
The change is to make the enriched leaf/edge rendering reachable for the
|
|
blueprint's top-level `nodes` + `edges`, and to teach the edge step the
|
|
`from_field` label. Direction only — the planner owns the exact factoring (a
|
|
shared helper over `(nodes, edges, param-source)`, vs a thin blueprint-side call
|
|
into the existing helpers); this spec does not prescribe the mechanism.
|
|
|
|
Before — blueprint leaf label is bare, edge drops field/slot:
|
|
|
|
```rust
|
|
// graph.rs:43-46 (render_blueprint, pass 1)
|
|
labels.push(match item {
|
|
BlueprintNode::Leaf(factory) => factory.label(), // bare
|
|
BlueprintNode::Composite(c) => c.name().to_string(), // opaque (retained)
|
|
});
|
|
|
|
// graph.rs:59-61 (render_blueprint, pass 2)
|
|
for e in bp.edges() {
|
|
edges.push((item_ids[e.from], item_ids[e.to])); // from_field/slot dropped
|
|
}
|
|
```
|
|
|
|
After — top-level leaf is enriched from its factory params + wired slots; the
|
|
edge carries a field label when the producer is multi-output (composites stay
|
|
opaque, sinks need no special case):
|
|
|
|
```rust
|
|
// pass 1: a top-level leaf renders its factory-declared param names and its
|
|
// wired input-slot stubs — the same enrichment leaf_label already produces for
|
|
// a composite interior, sourced from factory.params() instead of a ParamAlias
|
|
// list. Composites remain opaque (#38). A sink is just a leaf with no out-edge.
|
|
labels.push(match item {
|
|
BlueprintNode::Leaf(factory) => leaf_label_toplevel(bp, idx, factory), // enriched
|
|
BlueprintNode::Composite(c) => c.name().to_string(), // opaque
|
|
});
|
|
|
|
// pass 2: keep the (from, to) pair, add the field label when the producer has
|
|
// >1 output field — name via Composite::output() for a composite producer,
|
|
// index fallback for a (currently absent) multi-output leaf producer.
|
|
let label = field_label(bp, e); // Some("histogram") | None for single-output
|
|
edges.push((item_ids[e.from], item_ids[e.to], label));
|
|
```
|
|
|
|
The edge-label rule, shared by both views through `render_flat` /
|
|
`add_edge` (`graph.rs:81-94`):
|
|
|
|
```rust
|
|
// only-when-ambiguous, mirroring fan_in_identifiers' >1-slot guard:
|
|
// producer single-output -> None (golden unchanged)
|
|
// producer multi-output -> Some(field_name_or_index)
|
|
fn field_label(producer_outputs: usize, from_field: usize, name: Option<&str>) -> Option<String> {
|
|
if producer_outputs <= 1 { return None; }
|
|
Some(name.map(str::to_string).unwrap_or_else(|| from_field.to_string()))
|
|
}
|
|
```
|
|
|
|
## Components
|
|
|
|
- `crates/aura-cli/src/graph.rs` — the only production file changed.
|
|
- `render_blueprint` — pass 1 enriches top-level leaves; pass 2 adds the field
|
|
label. Composite-opaque rendering retained.
|
|
- `render_flat` / the `add_edge` call (`graph.rs:81-94`) — gains an optional
|
|
per-edge label argument; used by `render_blueprint`, `render_definition`,
|
|
and `render_flat_graph` alike (shared, so the label mechanism is single-owned).
|
|
- `leaf_label` — generalised (or paralleled by a top-level variant) to source
|
|
param names from `LeafFactory::params()` when there is no `ParamAlias` list.
|
|
- a `from_field` → name resolver using `Composite::output()` with an
|
|
index fallback.
|
|
- `crates/aura-cli/src/main.rs` — golden tests re-captured (`blueprint_view_golden`,
|
|
the macd render assertions); no production change.
|
|
- No engine change. `Blueprint`/`Composite`/`Edge`/`LeafFactory` accessors all
|
|
already exist; render reads structure + `label()` + `params()`/`output()` only,
|
|
never `eval` (C9).
|
|
|
|
## Data flow
|
|
|
|
Render is a pure read over graph-as-data (C9), unchanged in nature:
|
|
|
|
1. `render_blueprint(bp)` walks `bp.nodes()`: a leaf → enriched label from
|
|
`factory.label()` + `factory.params()` names + wired-slot stubs; a composite
|
|
→ opaque `c.name()`.
|
|
2. Walks `bp.edges()` + `bp.sources()`: each edge → `(from, to, field_label)`,
|
|
the label resolved from the producer's output arity and names; each source →
|
|
marker node + edges to its targets.
|
|
3. `render_flat` builds the ascii-dag graph with the optional labels and renders
|
|
(Plain for golden/pipe, Ansi for TTY).
|
|
4. The `where:` section renders each distinct composite via `render_definition`,
|
|
now also passing field labels through the shared `add_edge` slot.
|
|
|
|
No `eval`, no compile, no ordering dependence — the same determinism the current
|
|
render has (the golden tests are byte-exact, layout is RNG-free).
|
|
|
|
## Error handling
|
|
|
|
- **Malformed `from_field`** (index past the producer's output arity): the
|
|
render is a best-effort debug view, not the validator (the compile path is —
|
|
`graph.rs:135` `signature` already falls back to `?` rather than panicking).
|
|
The field resolver falls back to the index string and never panics.
|
|
- **Multi-output leaf producer** (no pre-build field names, #43): label falls
|
|
back to the field index. Latent in the current corpus; handled, not assumed
|
|
away.
|
|
- **ascii-dag vertical edge-label limitation:** if the label cannot be placed
|
|
legibly in `RenderMode::Vertical`, fall back to the consumer-side field locus
|
|
(Architecture §`from_field` edge label). Observable is preserved.
|
|
|
|
## Testing strategy
|
|
|
|
- **Golden re-capture, value-asserted (not just re-blessed).** `blueprint_view_golden`
|
|
(`crates/aura-cli/src/main.rs:494`) is re-captured; the diff must show the new
|
|
information (a param name on `[Exposure …]`; the `[SimBroker …]` stubs are
|
|
deferred to #49), proving enrichment rather than a blind re-bless.
|
|
- **The headline assertion (macd):** a test on `render_blueprint(&macd_blueprint())`
|
|
asserts the macd→Exposure edge surfaces `histogram` and that `[Exposure(scale)]`
|
|
shows its param — the legibility property the cycle exists to buy. Extends the
|
|
existing `macd_blueprint_renders_a_nested_composite_definition`
|
|
(`crates/aura-cli/src/main.rs:585`).
|
|
- **Single-output producers carry no field label** — asserted on the sma_cross
|
|
sample, where every producer is single-output, so the main-graph edges stay
|
|
label-free (guards the only-when-ambiguous rule and keeps the sample golden
|
|
honest).
|
|
- **`where:` unchanged in spirit:** the composite-definition assertions
|
|
(`[EMA(fast)]`, `[macd := Sub(#Ef,#Es)]`, the typed signature line) still pass
|
|
— the shared edge-label change is a no-op for single-output interiors.
|
|
- **Determinism:** byte-exact goldens remain the determinism guard (layout is
|
|
RNG-free); no new determinism test needed.
|
|
- Full `cargo test --workspace` + `cargo clippy --workspace --all-targets -D warnings`.
|
|
|
|
Note: the project profile declares no `spec_validation` parser, so the
|
|
parse-every-block gate is a no-op this cycle (recorded for the Step-4 trace).
|
|
|
|
## Acceptance criteria
|
|
|
|
Against aura's feature-acceptance criterion (CLAUDE.md):
|
|
|
|
- **The audience reaches for it.** A researcher running `aura graph` on a
|
|
strategy wants to see what the strategy *does*; legibility is the command's
|
|
purpose. The worked `aura graph --macd` output above — histogram on the edge,
|
|
`scale` on `Exposure` — is the empirical evidence.
|
|
- **Improves correctness / removes redundancy.** Collapses two divergent render
|
|
paths into one (less duplication) and stops the render from silently dropping
|
|
`from_field` (more faithful).
|
|
- **Reintroduces no failure class.** Render reads structure + `label()` only,
|
|
never `eval`; determinism (C1), causality (C2), and the SoA hot path (C4) are
|
|
untouched. Composites stay opaque (#38); sinks need no new concept (issue sink
|
|
sharpening).
|
|
|
|
Concretely, the cycle is done when:
|
|
|
|
- [x] `aura graph --macd`'s main graph shows the `histogram` field on the
|
|
macd→Exposure connection and `scale` on the `Exposure` leaf. (Shipped as a
|
|
consumer-side prefix `[histogram → Exposure(scale)]`, not an edge label —
|
|
ascii-dag drops edge labels on collision; see Architecture.)
|
|
- [ ] **Deferred to #49.** `SimBroker`'s two input slots render as stubs in the
|
|
main graph (the same enrichment the composite interior already shows). As
|
|
shipped, `SimBroker` renders bare — the stub machinery stays
|
|
composite-coupled this cycle.
|
|
- [x] Single-output producers carry no field label; the sma_cross sample golden
|
|
shows no spurious labels.
|
|
- [x] The `where:` section's existing assertions still pass (unchanged in spirit).
|
|
- [x] Composites remain opaque in the main graph (#38 retained).
|
|
- [x] `cargo test --workspace` and clippy are green; goldens re-captured with
|
|
visible new information.
|