Graph viewer: render bound params (e.g. weights[2]=0.5) instead of dropping them from the node signature #63

Closed
opened 2026-06-13 21:51:11 +02:00 by Brummel · 1 comment
Owner

Symptom (observable)

The aura graph viewer renders the cycle-0036 sample's blend node as

blend: LinComb[weights[0], weights[1]]

but that node is a LinComb(3) with three input ports (term[0..2]), whose
third weight was bind-bound to a constant (weights[2] = 0.5). So the rendered
signature claims arity 2 while the box shows 3 input ports, and the binding — the
fixed 0.5 — is invisible. The signature misrepresents the node.

The bound param correctly drops out of the sweepable surface (that is what
bind is for). The defect is that it also vanishes from the rendered signature,
which should still show all slots and mark the bound one as a fixed constant.

Root cause (where the information is lost)

PrimitiveBuilder::bind (crates/aura-core/src/node.rs:149) does two things:

  1. self.schema.params.remove(pos) — removes the param from the schema, and
  2. seals the value inside the rebuilt build closure (full.insert(pos, value)).

prim_record (crates/aura-engine/src/graph_model.rs:72-89) emits only
b.schema().params, so a bound param leaves no trace in the graph model: not
its name, not its kind, not its value. The model serializer never builds the
node, so the value captured in the closure is unreachable to it. Net: for the
blend the model carries ins = 3 ports but params = 2, and no record of the
binding. The viewer's cellLabel (crates/aura-cli/assets/graph-viewer.js,
the sig built from o.params) therefore can only show 2 weights.

This is a data gap, not a label bug — the fix must carry the bound info into the
model first.

Conceptual fix

Separate the two concerns bind currently conflates:

  • Tunable surface (param_space, the sweep axes, C12/C19): bind removes
    the slot — correct, keep it.
  • Node signature (the render): bind should annotate, not erase. Show all
    slots; mark the bound one with its fixed value.

This is consistent with the value-empty blueprint convention: a free param
renders as a name (its value is deferred to sweep/bootstrap — e.g. SMA[length]),
while a bound param has its value fixed at blueprint time, so it renders as
name=value. Showing the bound value is showing a structural fact, not a tuning
value.

Proposed fix — structurally identical to cycle 0035

Cycle 0035 (commit 0ae8320, feat(aura): surface explicit node instance name in the graph viewer) threaded a conditional "name" field through
engine -> model -> viewer + goldens + a render guard. This is the same shape:

  1. aura-core (node.rs): bind additionally records a visible
    bound: Vec<(String /*slot*/, ScalarKind, Scalar /*value*/)> (alongside the
    existing closure capture), plus a bound_params() accessor — the twin of
    instance_name() (node.rs:120). The closure mechanics stay unchanged.
  2. aura-engine (graph_model.rs prim_record): emit a conditional "bound"
    field only when non-empty, e.g.
    "bound":[["weights[2]","f64","0.5"]]. Deterministic (C14): the Scalar
    value formats to a canonical string (mirror the existing kind_str /
    named_kind helpers; pick a fixed f64/i64/bool/timestamp rendering).
  3. graph-viewer.js: adaptNodes carries bound; cellLabel merges the free
    params and the bound params back into slot order and renders a bound slot as
    weights[2]=0.5, visually dimmed (reuse the dim-font pattern the 0035 :
    separator already uses).
  4. Goldens + tests: re-capture crates/aura-cli/tests/fixtures/sample-model.json
    (the blend node gains a "bound" field); add a prim_record unit test (bound
    field present only when bound) and a headless viewer render guard (bound slot
    renders name=value, dimmed; free slots unchanged).

C23 is unaffected: bind still resolves the name to a fixed position and the
compilat is unchanged; this only adds a render/debug-symbol surface (like the
instance name), dropped at lowering.

The one open design decision — settle BEFORE implementing

The render notation. Recommended (keeps the existing TYPE[...] convention from
SMA[length]):

LinComb[weights[0], weights[1], weights[2]=0.5]      <- all 3 slots; bound dimmed

Alternatives considered:

  • LinComb(A, B, C=0.5) — switches the bracket to (); that is a global
    notation change for every node's render, a bigger decision than this issue.
  • LinComb[weights[0], weights[1], 0.5] — positional; terser but drops the slot
    name on the bound entry.

Decide the bracket style and the bound annotation (with the user) before writing
the spec. Everything else above is settled (the engine+viewer mechanism mirrors
0035).

Grounding (currently-green tests that ratify the mechanics this builds on)

  • bind mechanics: bind_removes_slot_and_reconstructs_positionally,
    bind_kind_mismatch_panics, bind_unknown_slot_panics (crates/aura-core/src/node.rs).
  • LinComb arity + named slots: chained_bind_reconstructs_positional_vector,
    input_slots_are_named_term_index (crates/aura-std/src/lincomb.rs).
  • Conditional-model-field precedent (the pattern to copy):
    prim_record_emits_name_for_explicit_only (crates/aura-engine/src/graph_model.rs)
    • the viewer_name_prefix guard (crates/aura-cli/tests/).
  • First rendered bound param (the thing that surfaced this): the blend node in
    signals() (crates/aura-cli/src/main.rs), cycle 0036.

Meta

  • depends on: nothing — bind already works; this adds a model+render
    representation for it.
  • surfaced by: cycle 0036 (#62), the first rendered bound param.
  • related: the 0035 instance-name cycle (the pattern this copies); #37
    (interactive blueprint navigation / viewer).
  • entry path: the symptom is a render defect, but the fix is a render-surface
    extension with one design decision (the notation). Settle the notation first
    (a short design call), then it is a 0035-shaped, test-specifiable engine+viewer
    change — not a blind RED-first debug.
## Symptom (observable) The `aura graph` viewer renders the cycle-0036 sample's `blend` node as ``` blend: LinComb[weights[0], weights[1]] ``` but that node is a `LinComb(3)` with **three input ports** (`term[0..2]`), whose third weight was `bind`-bound to a constant (`weights[2] = 0.5`). So the rendered signature claims arity 2 while the box shows 3 input ports, and the binding — the fixed `0.5` — is **invisible**. The signature misrepresents the node. The bound param correctly drops out of the *sweepable* surface (that is what `bind` is for). The defect is that it also vanishes from the *rendered signature*, which should still show all slots and mark the bound one as a fixed constant. ## Root cause (where the information is lost) `PrimitiveBuilder::bind` (`crates/aura-core/src/node.rs:149`) does two things: 1. `self.schema.params.remove(pos)` — removes the param from the schema, **and** 2. seals the value inside the rebuilt `build` closure (`full.insert(pos, value)`). `prim_record` (`crates/aura-engine/src/graph_model.rs:72-89`) emits only `b.schema().params`, so a bound param leaves **no trace** in the graph model: not its name, not its kind, not its value. The model serializer never *builds* the node, so the value captured in the closure is unreachable to it. Net: for the blend the model carries `ins` = 3 ports but `params` = 2, and no record of the binding. The viewer's `cellLabel` (`crates/aura-cli/assets/graph-viewer.js`, the `sig` built from `o.params`) therefore can only show 2 weights. This is a data gap, not a label bug — the fix must carry the bound info into the model first. ## Conceptual fix Separate the two concerns `bind` currently conflates: - **Tunable surface** (`param_space`, the sweep axes, C12/C19): `bind` *removes* the slot — correct, keep it. - **Node signature** (the render): `bind` should *annotate*, not erase. Show all slots; mark the bound one with its fixed value. This is consistent with the value-empty blueprint convention: a *free* param renders as a name (its value is deferred to sweep/bootstrap — e.g. `SMA[length]`), while a *bound* param has its value fixed at blueprint time, so it renders as `name=value`. Showing the bound value is showing a structural fact, not a tuning value. ## Proposed fix — structurally identical to cycle 0035 Cycle 0035 (commit 0ae8320, `feat(aura): surface explicit node instance name in the graph viewer`) threaded a conditional `"name"` field through engine -> model -> viewer + goldens + a render guard. This is the same shape: 1. **aura-core** (`node.rs`): `bind` additionally records a visible `bound: Vec<(String /*slot*/, ScalarKind, Scalar /*value*/)>` (alongside the existing closure capture), plus a `bound_params()` accessor — the twin of `instance_name()` (`node.rs:120`). The closure mechanics stay unchanged. 2. **aura-engine** (`graph_model.rs` `prim_record`): emit a conditional `"bound"` field only when non-empty, e.g. `"bound":[["weights[2]","f64","0.5"]]`. Deterministic (C14): the `Scalar` value formats to a canonical string (mirror the existing `kind_str` / `named_kind` helpers; pick a fixed f64/i64/bool/timestamp rendering). 3. **graph-viewer.js**: `adaptNodes` carries `bound`; `cellLabel` merges the free params and the bound params back into slot order and renders a bound slot as `weights[2]=0.5`, visually dimmed (reuse the dim-font pattern the 0035 `: ` separator already uses). 4. **Goldens + tests**: re-capture `crates/aura-cli/tests/fixtures/sample-model.json` (the blend node gains a `"bound"` field); add a `prim_record` unit test (bound field present only when bound) and a headless viewer render guard (bound slot renders `name=value`, dimmed; free slots unchanged). C23 is unaffected: `bind` still resolves the name to a fixed position and the compilat is unchanged; this only adds a render/debug-symbol surface (like the instance name), dropped at lowering. ## The one open design decision — settle BEFORE implementing The render notation. Recommended (keeps the existing `TYPE[...]` convention from `SMA[length]`): ``` LinComb[weights[0], weights[1], weights[2]=0.5] <- all 3 slots; bound dimmed ``` Alternatives considered: - `LinComb(A, B, C=0.5)` — switches the bracket to `()`; that is a *global* notation change for every node's render, a bigger decision than this issue. - `LinComb[weights[0], weights[1], 0.5]` — positional; terser but drops the slot name on the bound entry. Decide the bracket style and the bound annotation (with the user) before writing the spec. Everything else above is settled (the engine+viewer mechanism mirrors 0035). ## Grounding (currently-green tests that ratify the mechanics this builds on) - `bind` mechanics: `bind_removes_slot_and_reconstructs_positionally`, `bind_kind_mismatch_panics`, `bind_unknown_slot_panics` (`crates/aura-core/src/node.rs`). - `LinComb` arity + named slots: `chained_bind_reconstructs_positional_vector`, `input_slots_are_named_term_index` (`crates/aura-std/src/lincomb.rs`). - Conditional-model-field precedent (the pattern to copy): `prim_record_emits_name_for_explicit_only` (`crates/aura-engine/src/graph_model.rs`) + the `viewer_name_prefix` guard (`crates/aura-cli/tests/`). - First rendered bound param (the thing that surfaced this): the `blend` node in `signals()` (`crates/aura-cli/src/main.rs`), cycle 0036. ## Meta - **depends on:** nothing — `bind` already works; this adds a model+render representation for it. - **surfaced by:** cycle 0036 (#62), the first rendered bound param. - **related:** the 0035 instance-name cycle (the pattern this copies); #37 (interactive blueprint navigation / viewer). - **entry path:** the symptom is a render defect, but the fix is a render-surface *extension* with one design decision (the notation). Settle the notation first (a short design call), then it is a 0035-shaped, test-specifiable engine+viewer change — not a blind RED-first debug.
Brummel added the feature label 2026-06-13 21:51:11 +02:00
Author
Owner

Design reconciliation (specify, in-context entry)

Spec: 0037-bound-param-in-graph-model. The fork below is still listed open on
this issue ("The one open design decision — settle BEFORE implementing"); it was
resolved in the in-context design discussion of this /boss session.

  • Fork: render notation for a bind-bound param slot → Option A: keep the
    square-bracket TYPE[...] convention (as SMA[length]) and render the bound
    slot as name=value (e.g. weights[2]=0.5), visually dimmed (reuse the 0035
    : instance-name separator's muted font). The two alternatives the issue
    listed were both rejected: positional value-only (...,0.5) drops the slot
    name; parens (LinComb(...)) is a global notation change beyond this issue.
    Provenance: the user selected this option directly in the /boss #63 session
    on 2026-06-13, in response to the notation decision the issue flagged for the
    user ("Decide the bracket style and the bound annotation (with the user)
    before implementing").
## Design reconciliation (specify, in-context entry) Spec: 0037-bound-param-in-graph-model. The fork below is still listed open on this issue ("The one open design decision — settle BEFORE implementing"); it was resolved in the in-context design discussion of this `/boss` session. - **Fork: render notation for a bind-bound param slot** → Option A: keep the square-bracket `TYPE[...]` convention (as `SMA[length]`) and render the bound slot as `name=value` (e.g. `weights[2]=0.5`), visually dimmed (reuse the 0035 `:` instance-name separator's muted font). The two alternatives the issue listed were both rejected: positional value-only (`...,0.5`) drops the slot name; parens (`LinComb(...)`) is a global notation change beyond this issue. Provenance: the user selected this option directly in the `/boss #63` session on 2026-06-13, in response to the notation decision the issue flagged for the user ("Decide the bracket style and the bound annotation (with the user) before implementing").
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#63