Add a documentation field to composites, surfaced in the graph tooltip #125

Closed
opened 2026-06-22 19:21:10 +02:00 by Brummel · 4 comments
Owner

The graph viewer conveys what a node is structurally (type, ports, params) but
not why a composite is composed as it is. A composite carries an authored
design decision — which sub-nodes, wired which way, and to what end — that
exists nowhere in the runnable artifact (the boundary dissolves at compile time,
C23) and nowhere in the rendered graph. An optional doc string on Composite,
authored beside the composite's name and surfaced on hover, captures that
rationale at the tier where it actually lives.

Why composites, not primitives

A primitive's documentation ("what an SMA is") is reference material that
belongs in rustdoc / the generated API docs, and is largely redundant with the
module rustdoc that already exists (e.g. crates/aura-std/src/sma.rs:1-19). The
documentation that exists nowhere else is the compositional "why": "this is a
fast/slow SMA spread that detects trend changes", "this leg is clamped on
purpose". That rationale scales with composition, so the field belongs on
Composite. Primitive-node docs are explicitly out of scope here.

One field covers both "sub-graph" and "experiment"

There is no separate Blueprint type. The module doc states it verbatim
(crates/aura-engine/src/blueprint.rs:7-9):

"the root composite IS the blueprint (there is no separate Blueprint type — a
fully source-bound composite is the runnable root)."

So a single field, doc: Option<String> on Composite, reads on two nesting
levels with no second mechanism:

  • a nested composite (sma_cross, signals, trend) → "why this sub-graph
    is wired this way"
  • the root composite (== the blueprint) → "why this whole experiment is set
    up this way"

Home

  • struct Composite at crates/aura-engine/src/blueprint.rs:141-147 has
    name: String as its first field. The doc field sits beside it.
  • The name is documented (blueprint.rs:152) as "a non-load-bearing render
    symbol (the cluster title for #13); it does not reach the flat graph (the
    boundary dissolves at inline, C23)." A doc string is the same category — the
    prose twin of the name.
  • Authored via a fluent .doc(impl Into<String>) on Composite::new
    (blueprint.rs:154-162) and/or on GraphBuilder::new
    (crates/aura-engine/src/builder.rs:80).

Invariant fit (no ledger amendment; realization note under C9/C23)

  • C4 (4 scalars stream as SoA): a doc string never enters an eval payload;
    it is metadata-beside-the-hot-path, the explicitly-blessed category.
  • C11/C23 (flat graph wired by raw index; names are non-load-bearing symbols
    dropped at lowering): Composite is authoring-only and inlines away entirely —
    it never reaches FlatGraph/Harness. This is the most conservative possible
    home for the field (more so than NodeSchema, which rides in
    FlatGraph.signatures).
  • C9 / C12: surfaced only by the read-only render model into a pre-run
    structure-view tooltip; no write-back, no scene-editing.

Render path

  • Emit an optional "doc" field in composite_def_json
    (crates/aura-engine/src/graph_model.rs:177) for nested composites, and in the
    "root" record (graph_model.rs:264, the {"root":…,"composites":…} shape)
    for the blueprint-level doc.
  • Viewer: the tooltip uses two different lookup maps depending on view state,
    so the doc must be threaded into both:
    • collapsed composite (single box) → addInfo(...)INFO.B[cid]
      (crates/aura-cli/assets/graph-viewer.js:139 calling :109)
    • expanded composite (cluster frame) → INFO.C["clust_"+cid]
      (graph-viewer.js:134)
  • The md() renderer (graph-viewer.js:183) already converts \n, **bold**,
    and `code` for the #tip div — no new tooltip machinery.

Hard gate: JSON escaping (verified)

json_str (crates/aura-engine/src/graph_model.rs:47-58) escapes only " and
\; \n, \t, and control characters pass through raw:

match c {
    '"' => out.push_str("\\\""),
    '\\' => out.push_str("\\\\"),
    _ => out.push(c),          // newline/tab/control passed through unescaped
}

This is safe today because every value it serializes is an identifier. A free-text
doc is the first multi-line value to pass through it — and md() actively wants
\n (it converts \n to <br>), so multi-line docs are the expected case, not
an edge case. Without hardening, the first multi-line doc produces invalid JSON.
This must land in the same change.

Acceptance

  • doc: Option<String> on Composite + a .doc() builder on
    Composite::new / GraphBuilder
  • "doc" emitted in composite_def_json (nested) and the "root" record
    (blueprint-level)
  • doc appended in both viewer states — INFO.B (collapsed) and INFO.C
    (expanded)
  • json_str hardened to escape \n, \t, and U+0000U+001F (\u00XX)
  • golden model test (graph_model.rs:497), tests/fixtures/sample-model.json,
    and the viewer_*.mjs assertions updated for the new optional field

Out of scope

  • Primitive-node documentation (belongs in rustdoc / generated API docs).
  • Port and edge documentation (ports already have an INFO.P slot; edges have no
    model identity — separate, deferrable decisions).

Open questions

  • Root/blueprint-level display slot: nested composites have a ready home
    (INFO.B/INFO.C), but the root doc emits into the "root" record and needs a
    display surface (a root frame or a graph header). Small, not a blocker.
  • Authoring scope (root-only vs also nested) is authoring discretion, not a design
    fork — the same field works at every nesting level. A reusable composite shipped
    in a project's nodes/ (C9) wants its own doc, so it is not root-restricted.
  • Per-instance composite doc: composites are referenced once by type name today
    (no instance renaming, unlike a primitive's .named()), so the doc is
    type-level. If composite instancing is added later, an instance-level overlay is
    a separate concern.

Related

  • #28 (expose rendering for a consumer's OWN graph, not just the built-in
    sample) gates whether project-authored composite docs are viewable at all.
    Today aura graph renders only the built-in sample — which does contain
    composites (the golden model at graph_model.rs:497 shows sma_cross under
    "composites"), so engine-side composite docs are viewable immediately; a
    project's own composite docs depend on #28.
The graph viewer conveys what a node is structurally (type, ports, params) but not *why* a composite is composed as it is. A composite carries an authored design decision — which sub-nodes, wired which way, and to what end — that exists nowhere in the runnable artifact (the boundary dissolves at compile time, C23) and nowhere in the rendered graph. An optional `doc` string on `Composite`, authored beside the composite's name and surfaced on hover, captures that rationale at the tier where it actually lives. ## Why composites, not primitives A primitive's documentation ("what an SMA is") is reference material that belongs in rustdoc / the generated API docs, and is largely redundant with the module rustdoc that already exists (e.g. `crates/aura-std/src/sma.rs:1-19`). The documentation that exists *nowhere else* is the compositional "why": "this is a fast/slow SMA spread that detects trend changes", "this leg is clamped on purpose". That rationale scales with composition, so the field belongs on `Composite`. Primitive-node docs are explicitly out of scope here. ## One field covers both "sub-graph" and "experiment" There is no separate `Blueprint` type. The module doc states it verbatim (`crates/aura-engine/src/blueprint.rs:7-9`): > "the root composite IS the blueprint (there is no separate `Blueprint` type — a > fully source-bound composite is the runnable root)." So a single field, `doc: Option<String>` on `Composite`, reads on two nesting levels with no second mechanism: - a **nested** composite (`sma_cross`, `signals`, `trend`) → "why this sub-graph is wired this way" - the **root** composite (== the blueprint) → "why this whole experiment is set up this way" ## Home - `struct Composite` at `crates/aura-engine/src/blueprint.rs:141-147` has `name: String` as its first field. The `doc` field sits beside it. - The `name` is documented (`blueprint.rs:152`) as "a non-load-bearing render symbol (the cluster title for #13); it does not reach the flat graph (the boundary dissolves at inline, C23)." A `doc` string is the same category — the prose twin of the name. - Authored via a fluent `.doc(impl Into<String>)` on `Composite::new` (`blueprint.rs:154-162`) and/or on `GraphBuilder::new` (`crates/aura-engine/src/builder.rs:80`). ## Invariant fit (no ledger amendment; realization note under C9/C23) - **C4** (4 scalars stream as SoA): a doc string never enters an `eval` payload; it is metadata-beside-the-hot-path, the explicitly-blessed category. - **C11/C23** (flat graph wired by raw index; names are non-load-bearing symbols dropped at lowering): `Composite` is authoring-only and inlines away entirely — it never reaches `FlatGraph`/`Harness`. This is the most conservative possible home for the field (more so than `NodeSchema`, which rides in `FlatGraph.signatures`). - **C9 / C12**: surfaced only by the read-only render model into a pre-run structure-view tooltip; no write-back, no scene-editing. ## Render path - Emit an optional `"doc"` field in `composite_def_json` (`crates/aura-engine/src/graph_model.rs:177`) for nested composites, and in the `"root"` record (`graph_model.rs:264`, the `{"root":…,"composites":…}` shape) for the blueprint-level doc. - Viewer: the tooltip uses **two different lookup maps** depending on view state, so the doc must be threaded into both: - **collapsed** composite (single box) → `addInfo(...)` → `INFO.B[cid]` (`crates/aura-cli/assets/graph-viewer.js:139` calling `:109`) - **expanded** composite (cluster frame) → `INFO.C["clust_"+cid]` (`graph-viewer.js:134`) - The `md()` renderer (`graph-viewer.js:183`) already converts `\n`, `**bold**`, and `` `code` `` for the `#tip` div — no new tooltip machinery. ## Hard gate: JSON escaping (verified) `json_str` (`crates/aura-engine/src/graph_model.rs:47-58`) escapes only `"` and `\`; `\n`, `\t`, and control characters pass through raw: ```rust match c { '"' => out.push_str("\\\""), '\\' => out.push_str("\\\\"), _ => out.push(c), // newline/tab/control passed through unescaped } ``` This is safe today because every value it serializes is an identifier. A free-text doc is the first multi-line value to pass through it — and `md()` actively wants `\n` (it converts `\n` to `<br>`), so multi-line docs are the expected case, not an edge case. Without hardening, the first multi-line doc produces invalid JSON. This must land in the same change. ## Acceptance - [ ] `doc: Option<String>` on `Composite` + a `.doc()` builder on `Composite::new` / `GraphBuilder` - [ ] `"doc"` emitted in `composite_def_json` (nested) and the `"root"` record (blueprint-level) - [ ] doc appended in both viewer states — `INFO.B` (collapsed) and `INFO.C` (expanded) - [ ] `json_str` hardened to escape `\n`, `\t`, and `U+0000`–`U+001F` (`\u00XX`) - [ ] golden model test (`graph_model.rs:497`), `tests/fixtures/sample-model.json`, and the `viewer_*.mjs` assertions updated for the new optional field ## Out of scope - Primitive-node documentation (belongs in rustdoc / generated API docs). - Port and edge documentation (ports already have an `INFO.P` slot; edges have no model identity — separate, deferrable decisions). ## Open questions - Root/blueprint-level display slot: nested composites have a ready home (`INFO.B`/`INFO.C`), but the root doc emits into the `"root"` record and needs a display surface (a root frame or a graph header). Small, not a blocker. - Authoring scope (root-only vs also nested) is authoring discretion, not a design fork — the same field works at every nesting level. A reusable composite shipped in a project's `nodes/` (C9) wants its own doc, so it is not root-restricted. - Per-instance composite doc: composites are referenced once by type name today (no instance renaming, unlike a primitive's `.named()`), so the doc is type-level. If composite instancing is added later, an instance-level overlay is a separate concern. ## Related - `#28` (expose rendering for a consumer's OWN graph, not just the built-in sample) gates whether project-authored composite docs are viewable at all. Today `aura graph` renders only the built-in sample — which does contain composites (the golden model at `graph_model.rs:497` shows `sma_cross` under `"composites"`), so engine-side composite docs are viewable immediately; a project's own composite docs depend on `#28`.
Brummel added the feature label 2026-06-22 19:21:10 +02:00
Author
Owner

Triage 2026-07-09 (tree at 68317ec) — premise intact, acceptance list needs two additions from the moved tree:

  • Unchanged: Composite has no doc field (crates/aura-engine/src/blueprint.rs:142-148), no .doc() in builder.rs, json_str still escapes only quote and backslash (crates/aura-cli/src/graph_model.rs:47-59), and both tooltip maps exist as described in graph-viewer.js.
  • New since filing: a Composite is also produced by the construction op-script (#157) and round-trips through blueprint_serde, including the identity-canonical form of #171. A doc field therefore must (a) survive the blueprint_serde roundtrip to display at all in the default aura graph path, and (b) get a one-sentence decision on its treatment in the identity-canonical hash (presumably stripped, like other non-load-bearing debug symbols). Optionally the construction Op vocabulary grows a doc-carrying surface.
  • #28 (rendering a consumer's own graph) still gates project-authored composite docs being visible at all.
Triage 2026-07-09 (tree at 68317ec) — premise intact, acceptance list needs two additions from the moved tree: - Unchanged: Composite has no doc field (crates/aura-engine/src/blueprint.rs:142-148), no .doc() in builder.rs, json_str still escapes only quote and backslash (crates/aura-cli/src/graph_model.rs:47-59), and both tooltip maps exist as described in graph-viewer.js. - New since filing: a Composite is also produced by the construction op-script (#157) and round-trips through blueprint_serde, including the identity-canonical form of #171. A doc field therefore must (a) survive the blueprint_serde roundtrip to display at all in the default `aura graph` path, and (b) get a one-sentence decision on its treatment in the identity-canonical hash (presumably stripped, like other non-load-bearing debug symbols). Optionally the construction Op vocabulary grows a doc-carrying surface. - #28 (rendering a consumer's own graph) still gates project-authored composite docs being visible at all.
Author
Owner

Design reconciliation (specify)

Spec: composite-doc-tooltip. The forks below are listed open (or newly arose from the 2026-07-09 triage) on this issue; this records their resolution (2026-07-11, autonomous run).

  • Fork: identity-canonical treatment of doc → stripped in the identity projection, exactly like the composite name it sits beside.
    Basis: derived — strip_debug_symbols (crates/aura-engine/src/blueprint_serde.rs:143) already blanks every non-load-bearing debug symbol (composite name, instance names, bound-param names, role names, output names, gang names); a doc string is the prose twin of the name and inherits its category.
  • Fork: construction-Op doc-carrying surface → out of scope this cycle.
    Basis: derived — the Op vocabulary change is additive and separable; an Op-built composite simply carries no doc until that vocabulary grows a slot, while the serde roundtrip (the display path) already tolerates the absent field.
  • Fork: root/blueprint-level display slot → one additional muted header line on the graph page (the existing <header>/.sub chrome, populated only when the root doc is present).
    Basis: derived — the page already owns a header with breadcrumb + hint line; a doc line there is the smallest surface that shows the root doc without inventing a new chrome region or overloading a hover map that has no root target.
  • Fork: serde format-version treatmentdoc is a Tier-1 additive-optional field; NO BLUEPRINT_FORMAT_VERSION bump.
    Basis: derived — the loader's documented two-tier discipline (blueprint_serde.rs LoadError, #156) states an additive-optional field does not bump the version; an older reader tolerates it (pinned by unknown_optional_field_is_tolerated_byte_identically) and the field defaults to prior behaviour.

The json_str escaping hardening (escape \n, \t, U+0000–U+001F) is not a fork — the issue body's verified gate stands (re-verified at 2532e8f: crates/aura-engine/src/graph_model.rs:47-59 still escapes only quote and backslash) and lands in the same change.

## Design reconciliation (specify) Spec: composite-doc-tooltip. The forks below are listed open (or newly arose from the 2026-07-09 triage) on this issue; this records their resolution (2026-07-11, autonomous run). - **Fork: identity-canonical treatment of `doc`** → stripped in the identity projection, exactly like the composite `name` it sits beside. Basis: derived — `strip_debug_symbols` (crates/aura-engine/src/blueprint_serde.rs:143) already blanks every non-load-bearing debug symbol (composite name, instance names, bound-param names, role names, output names, gang names); a doc string is the prose twin of the name and inherits its category. - **Fork: construction-Op doc-carrying surface** → out of scope this cycle. Basis: derived — the Op vocabulary change is additive and separable; an Op-built composite simply carries no doc until that vocabulary grows a slot, while the serde roundtrip (the display path) already tolerates the absent field. - **Fork: root/blueprint-level display slot** → one additional muted header line on the graph page (the existing `<header>`/`.sub` chrome, populated only when the root doc is present). Basis: derived — the page already owns a header with breadcrumb + hint line; a doc line there is the smallest surface that shows the root doc without inventing a new chrome region or overloading a hover map that has no root target. - **Fork: serde format-version treatment** → `doc` is a Tier-1 additive-optional field; NO `BLUEPRINT_FORMAT_VERSION` bump. Basis: derived — the loader's documented two-tier discipline (blueprint_serde.rs `LoadError`, #156) states an additive-optional field does not bump the version; an older reader tolerates it (pinned by `unknown_optional_field_is_tolerated_byte_identically`) and the field defaults to prior behaviour. The json_str escaping hardening (escape `\n`, `\t`, U+0000–U+001F) is not a fork — the issue body's verified gate stands (re-verified at 2532e8f: crates/aura-engine/src/graph_model.rs:47-59 still escapes only quote and backslash) and lands in the same change.
Author
Owner

Autonomous spec sign-off (2026-07-11): the spec for this issue's capability — an optional authored doc on Composite, surfaced in the graph tooltip (both view states) and as a root header line, with serde round-trip, identity-strip, and json_str hardening — was signed by a grounding-check PASS (all seven load-bearing current-behaviour assumptions ratified by named, currently-green tests), not by a human. The previously unpinned viewer-tooltip layer was pinned first (viewer_tooltip.mjs, commit at the pin's refs on this issue) after an initial BLOCK named it. Design-fork resolutions: [the reconciliation comment above in this thread] — restated: identity-strip like name; Tier-1 no-version-bump; root display = muted header line; construction-Op surface out of scope.

Autonomous spec sign-off (2026-07-11): the spec for this issue's capability — an optional authored `doc` on `Composite`, surfaced in the graph tooltip (both view states) and as a root header line, with serde round-trip, identity-strip, and `json_str` hardening — was signed by a `grounding-check` PASS (all seven load-bearing current-behaviour assumptions ratified by named, currently-green tests), not by a human. The previously unpinned viewer-tooltip layer was pinned first (`viewer_tooltip.mjs`, commit at the pin's `refs` on this issue) after an initial BLOCK named it. Design-fork resolutions: [the reconciliation comment above in this thread] — restated: identity-strip like `name`; Tier-1 no-version-bump; root display = muted header line; construction-Op surface out of scope.
Author
Owner

Implementation complete (2026-07-11, autonomous run; commits pending push): Composite carries the optional authored docwith_doc/doc() plus a GraphBuilder::doc knob; persisted as a Tier-1 additive-optional serde field (no format-version bump, absent-field documents byte-unchanged); blanked in the identity projection like the name while staying canonical-byte-bearing; emitted as an optional trailing "doc" fragment in both model scopes with json_str hardened for multi-line free text; shown in both composite view states (collapsed tooltip via INFO.B, expanded cluster frame via INFO.C) and as a muted root header line (#rootdoc). e2e drives aura graph over root-doc'd and nested-doc'd blueprint files; the doc moves the content id but never the identity id.

Accepted boundaries, per the reconciliation in this thread: the construction op-script vocabulary has no doc-carrying surface (marked at GraphSession::finish); the root header's DOM population line sits in the browser-only block, the same untested boundary as the breadcrumb (the headless pins cover normalizeModel(...).root.doc and the empty header slot).

Implementation complete (2026-07-11, autonomous run; commits pending push): `Composite` carries the optional authored `doc` — `with_doc`/`doc()` plus a `GraphBuilder::doc` knob; persisted as a Tier-1 additive-optional serde field (no format-version bump, absent-field documents byte-unchanged); blanked in the identity projection like the name while staying canonical-byte-bearing; emitted as an optional trailing `"doc"` fragment in both model scopes with `json_str` hardened for multi-line free text; shown in both composite view states (collapsed tooltip via INFO.B, expanded cluster frame via INFO.C) and as a muted root header line (`#rootdoc`). e2e drives `aura graph` over root-doc'd and nested-doc'd blueprint files; the doc moves the content id but never the identity id. Accepted boundaries, per the reconciliation in this thread: the construction op-script vocabulary has no doc-carrying surface (marked at `GraphSession::finish`); the root header's DOM population line sits in the browser-only block, the same untested boundary as the breadcrumb (the headless pins cover `normalizeModel(...).root.doc` and the empty header slot).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#125