Make composites data-authorable — a named, registered subgraph referencable like a node #317

Closed
opened 2026-07-23 12:52:54 +02:00 by claude · 4 comments
Collaborator

Composites are Rust-builder-only today: a composite "runs as a composite today but is not yet campaign-reachable" (crates/aura-research/src/lib.rs:518). A data-plane author cannot name, register, or reference a multi-node pattern — every use repeats the wiring inside each blueprint, while a native Rust node is built once and referenced everywhere by name.

Field evidence (2026-07-22 external field test, triage in #314): the external agent built four-node patterns as native Rust nodes instead — e.g. its Agree combinator (emit a when a and b share a sign, else 0) is fully expressible with the shipped vocabulary:

Agree(a,b) = Select(Gt(Mul(Sign(a), Sign(b)), 0), a, 0)

— and after its first aura nodes new it never returned to graph authoring: the early workspaces were graph-heavy (nine blueprint documents in one of them), the later ones Rust-first. With no data-plane abstraction mechanism, the structural incentive runs against the design's intent that only genuinely new logic escalates to a Rust block (domain invariant 10, CLAUDE.md); composition itself gets pulled into Rust because Rust is where reuse lives.

Design-goal context (user decision, 2026-07-23): native-node authoring is to remain the escalation path for genuinely new logic, not the most convenient path for composition.

Open fork — deliberately undecided: the shape of data-authorable composites. A plausible form is a registered subgraph document in the content-addressed store, referenced by id/name like a vocabulary member — mirroring the authoring-level composite that already inlines at bootstrap (domain invariant 11, CLAUDE.md) — but this is the same data-authorability boundary #310 (the Named fold subscription: data selector vs Rust-only) negotiates for folds, one size larger. Direction not yet decided.

Related: #316 (self-description principle — a composite entering the vocabulary needs a meaning entry like any other member), #315 (vocabulary introspection surfaces).

Composites are Rust-builder-only today: a composite "runs as a composite today but is not yet campaign-reachable" (`crates/aura-research/src/lib.rs:518`). A data-plane author cannot name, register, or reference a multi-node pattern — every use repeats the wiring inside each blueprint, while a native Rust node is built once and referenced everywhere by name. Field evidence (2026-07-22 external field test, triage in #314): the external agent built four-node patterns as native Rust nodes instead — e.g. its `Agree` combinator (emit `a` when `a` and `b` share a sign, else 0) is fully expressible with the shipped vocabulary: ``` Agree(a,b) = Select(Gt(Mul(Sign(a), Sign(b)), 0), a, 0) ``` — and after its first `aura nodes new` it never returned to graph authoring: the early workspaces were graph-heavy (nine blueprint documents in one of them), the later ones Rust-first. With no data-plane abstraction mechanism, the structural incentive runs against the design's intent that only genuinely new logic escalates to a Rust block (domain invariant 10, `CLAUDE.md`); composition itself gets pulled into Rust because Rust is where reuse lives. Design-goal context (user decision, 2026-07-23): native-node authoring is to remain the escalation path for genuinely new logic, not the most convenient path for composition. Open fork — deliberately undecided: the shape of data-authorable composites. A plausible form is a registered subgraph document in the content-addressed store, referenced by id/name like a vocabulary member — mirroring the authoring-level composite that already inlines at bootstrap (domain invariant 11, `CLAUDE.md`) — but this is the same data-authorability boundary #310 (the `Named` fold subscription: data selector vs Rust-only) negotiates for folds, one size larger. Direction not yet decided. Related: #316 (self-description principle — a composite entering the vocabulary needs a meaning entry like any other member), #315 (vocabulary introspection surfaces).
claude added the idea label 2026-07-23 12:52:54 +02:00
claude added this to the Data-authorability boundary — folds, composites, construction args milestone 2026-07-23 13:42:57 +02:00
claude self-assigned this 2026-07-24 17:28:13 +02:00
Author
Collaborator

Design reconciliation (specify)

Spec: composites-data-authorable. The issue leaves the shape "deliberately
undecided"; this records its resolution before the spec is written. All
decisions below are derived orchestrator calls (recorded per the /boss
discipline) except where marked as user provenance; veto window open.

  • Goal (user provenance). Native-node authoring stays the escalation path
    for genuinely new logic, not the most convenient path for composition —
    user decision 2026-07-23, recorded in the issue body. Everything below
    serves that goal.

  • Fork: shape → a registered subgraph referenced from the op-script and
    spliced at construction time as a nested composite
    . A new construction op
    fetches a registered blueprint from the content-addressed store and adds it
    to the building graph as a BlueprintNode::Composite under an instance
    name; the emitted canonical blueprint contains the subgraph inline.
    Basis: derived — invariant 11 (a composite is an authoring-level node that
    inlines at bootstrap); the serialized format already nests composites
    recursively (green pins: nested_composite_round_trips,
    serialized_blueprint_runs_bit_identical_to_rust_built); blueprints stay
    self-contained values, so the load path, C1, C18/reproduce, and the
    format version are all untouched. Rejected: (a) membership in the
    compiled-in vocabulary roster — structurally impossible (the op resolver
    returns PrimitiveBuilder, NodeSchema.doc is &'static str, the roster
    macro is zero-arg-only) and it would blur the C24 logic/content boundary;
    (b) a load-time ref node kind in the format — makes blueprints
    non-self-contained, forces a Tier-2 format bump, and drags the store into
    reproduction.

  • Fork: reference formcontent id (canonical, exact; unique prefix
    accepted per the #302 precedent) AND a registry-level name label
    . graph register --name <label> records an append-only name→content-id entry
    (sidecar; re-registering under the same label points it at the new id);
    the op's ref takes { "content_id": … } or { "name": … }, and graph build echoes every resolved ref's content id so an author can pin.
    Basis: derived, corrected by the spec-skeptic pass — the initial
    derivation cut name resolution out on a C24-marketplace reading; the
    skeptic showed C24 §"Enforcement shift" explicitly permits store-layer
    discovery above the engine (a World/C21-layer charter point), and the
    issue's own framing ("name, register, reference", "by id/name", the #319
    LLM-agent audience) is not met by reuse-through-64-hex-hash. The label is
    deliberately decoupled from the composite's internal name field, so the
    #328 axis-namespace and #331 blueprint-name questions stay untouched.

  • Fork: op shape{"op":"use","ref":{…},"name":<instance>?,"bind":{…}?}.
    Instance name defaults to the stored composite's name; the splice renames
    the composite to the instance name (names are identity-blind debug
    symbols). The instance's open input roles address as <instance>.<role>,
    its exposed outputs as <instance>.<field>; open params surface
    path-qualified <instance>.<node>.<param> (existing nested-composite
    prefix discipline), sweepable and gangable; bind binds inner params
    path-qualified at splice time. Basis: derived — every element reuses an
    existing mechanism (add's name/bind, the collect_params prefixing, gang).

  • Fork: engine/store boundary → the engine stays store-free: the
    construction session gains a second injected resolver
    (Fn(&ref) -> Option<Composite>-shaped), the CLI supplies the closure
    that hits the registry. Basis: derived — the exact injection pattern the
    node vocabulary already uses (C16/invariant 9).

  • Fork: C29 at the new seam → the use-op gates the fetched composite's
    docs at build time (fail fast, refusal names the ref, the doc-less nested
    entry, and the rule). A pre-C29 doc-less store entry stays readable and
    runnable (no retroactive invalidation); it just cannot enter new
    compositions until re-registered with docs. Basis: derived — the skeptic's
    grounding pass surfaced the alternative as a trap (splice succeeds, the
    composed blueprint later refuses at put_blueprint's recursive gate with
    a deep nested path); C29 wants the gate at the entry seam, and the use-op
    is the entry seam of store content into a new graph.

  • Scope cuts. (a) RiskRegime::Fixed (the lib.rs:518 "not yet
    campaign-reachable" comment) is a closed-enum variant question, split to
    #338. (b) The #328 sweep-axis namespace question is untouched — use
    instances follow the existing prefix discipline. (c) No engine-level name
    resolution — naming lives in the CLI/store layer only. (d) A minimal
    discovery rendering (list registered labels + id prefix + root doc line)
    is IN scope — an unfindable name fails "referencable like a node" for the
    #319 audience; its exact CLI home is a spec detail.

## Design reconciliation (specify) Spec: composites-data-authorable. The issue leaves the shape "deliberately undecided"; this records its resolution before the spec is written. All decisions below are derived orchestrator calls (recorded per the /boss discipline) except where marked as user provenance; veto window open. - **Goal (user provenance).** Native-node authoring stays the escalation path for genuinely new logic, not the most convenient path for composition — user decision 2026-07-23, recorded in the issue body. Everything below serves that goal. - **Fork: shape** → a **registered subgraph referenced from the op-script and spliced at construction time as a nested composite**. A new construction op fetches a registered blueprint from the content-addressed store and adds it to the building graph as a `BlueprintNode::Composite` under an instance name; the emitted canonical blueprint contains the subgraph inline. Basis: derived — invariant 11 (a composite is an authoring-level node that inlines at bootstrap); the serialized format already nests composites recursively (green pins: `nested_composite_round_trips`, `serialized_blueprint_runs_bit_identical_to_rust_built`); blueprints stay self-contained values, so the load path, C1, C18/reproduce, and the format version are all untouched. Rejected: (a) membership in the compiled-in vocabulary roster — structurally impossible (the op resolver returns `PrimitiveBuilder`, `NodeSchema.doc` is `&'static str`, the roster macro is zero-arg-only) and it would blur the C24 logic/content boundary; (b) a load-time ref node kind in the format — makes blueprints non-self-contained, forces a Tier-2 format bump, and drags the store into reproduction. - **Fork: reference form** → **content id (canonical, exact; unique prefix accepted per the #302 precedent) AND a registry-level name label**. `graph register --name <label>` records an append-only name→content-id entry (sidecar; re-registering under the same label points it at the new id); the op's ref takes `{ "content_id": … }` or `{ "name": … }`, and `graph build` echoes every resolved ref's content id so an author can pin. Basis: derived, **corrected by the spec-skeptic pass** — the initial derivation cut name resolution out on a C24-marketplace reading; the skeptic showed C24 §"Enforcement shift" explicitly permits store-layer discovery *above* the engine (a World/C21-layer charter point), and the issue's own framing ("name, register, reference", "by id/name", the #319 LLM-agent audience) is not met by reuse-through-64-hex-hash. The label is deliberately decoupled from the composite's internal `name` field, so the #328 axis-namespace and #331 blueprint-name questions stay untouched. - **Fork: op shape** → `{"op":"use","ref":{…},"name":<instance>?,"bind":{…}?}`. Instance name defaults to the stored composite's name; the splice renames the composite to the instance name (names are identity-blind debug symbols). The instance's open input roles address as `<instance>.<role>`, its exposed outputs as `<instance>.<field>`; open params surface path-qualified `<instance>.<node>.<param>` (existing nested-composite prefix discipline), sweepable and gangable; `bind` binds inner params path-qualified at splice time. Basis: derived — every element reuses an existing mechanism (add's name/bind, the collect_params prefixing, gang). - **Fork: engine/store boundary** → the engine stays store-free: the construction session gains a second injected resolver (`Fn(&ref) -> Option<Composite>`-shaped), the CLI supplies the closure that hits the registry. Basis: derived — the exact injection pattern the node vocabulary already uses (C16/invariant 9). - **Fork: C29 at the new seam** → the use-op gates the fetched composite's docs at build time (fail fast, refusal names the ref, the doc-less nested entry, and the rule). A pre-C29 doc-less store entry stays readable and runnable (no retroactive invalidation); it just cannot enter *new* compositions until re-registered with docs. Basis: derived — the skeptic's grounding pass surfaced the alternative as a trap (splice succeeds, the composed blueprint later refuses at `put_blueprint`'s recursive gate with a deep nested path); C29 wants the gate at the entry seam, and the use-op is the entry seam of store content into a new graph. - **Scope cuts.** (a) `RiskRegime::Fixed` (the lib.rs:518 "not yet campaign-reachable" comment) is a closed-enum variant question, split to #338. (b) The #328 sweep-axis namespace question is untouched — use instances follow the existing prefix discipline. (c) No engine-level name resolution — naming lives in the CLI/store layer only. (d) A minimal discovery rendering (list registered labels + id prefix + root doc line) is IN scope — an unfindable name fails "referencable like a node" for the #319 audience; its exact CLI home is a spec detail.
Author
Collaborator

Derived decision (mid-cycle, implementation evidence): open patterns at finish()

Batch-1 implementation surfaced a blocker for the spec's worked example:
GraphSession::finish() shares check_root_roles_bound with compile, so an
op-script declaring open input roles — the very form of a reusable
pattern — cannot build at all today (no shipped fixture uses the input
op; it was reachable only from the Rust builder, which defers holistic
checks to compile).

Decision (derived): finish() drops the root-role gate; compile()/
bootstrap keep it unchanged. Basis: the gate's own doc comment grounds it
in runnability ("only a fully source-bound composite is runnable"), which
compile/bootstrap still enforce; in the op path an open role only exists by
explicit {"op":"input"} declaration (a forgotten source faults at
feed as an unknown role), so the eager-accident rationale does not apply;
and #317's core deliverable is precisely that open patterns become
buildable, registerable data. Consequences: the finish-refuses-unbound-role
pin (construction.rs ~:1002) flips to assert the new split (finish OK,
compile refuses); C24's "Construction gates" paragraph moves root-role
boundness out of the holistic finalize list; running an open blueprint
standalone refuses at bootstrap exactly as before. Spec amended
accordingly; grounding-check re-dispatched on the amended bytes.

## Derived decision (mid-cycle, implementation evidence): open patterns at `finish()` Batch-1 implementation surfaced a blocker for the spec's worked example: `GraphSession::finish()` shares `check_root_roles_bound` with compile, so an op-script declaring open `input` roles — the very form of a reusable pattern — cannot build at all today (no shipped fixture uses the `input` op; it was reachable only from the Rust builder, which defers holistic checks to compile). **Decision (derived):** `finish()` drops the root-role gate; `compile()`/ bootstrap keep it unchanged. Basis: the gate's own doc comment grounds it in *runnability* ("only a fully source-bound composite is runnable"), which compile/bootstrap still enforce; in the op path an open role only exists by explicit `{"op":"input"}` declaration (a forgotten `source` faults at `feed` as an unknown role), so the eager-accident rationale does not apply; and #317's core deliverable is precisely that open patterns become buildable, registerable data. Consequences: the finish-refuses-unbound-role pin (construction.rs ~:1002) flips to assert the new split (finish OK, compile refuses); C24's "Construction gates" paragraph moves root-role boundness out of the holistic finalize list; running an open blueprint standalone refuses at bootstrap exactly as before. Spec amended accordingly; grounding-check re-dispatched on the amended bytes.
Author
Collaborator

Minute correction (audit close)

Comment 4621's op-shape decision described a spliced instance's open params
as "sweepable and gangable". Shipped reality: sweepable, not gangable
the gang op refuses a composite instance's member path (a gang fuses a
primitive's raw param slot; instance params are nested/path-qualified). The
refusal is now test-pinned (gang_of_a_spliced_instance_member_path_refuses)
and every prose surface (C24, authoring guide, spec) states it as such.
Instance-param ganging — together with the gang-knob-bind pairing, the
open-run refusal rendering, and --unwired on use-bearing documents — is
bundled as the collective residue issue #339.

## Minute correction (audit close) Comment 4621's op-shape decision described a spliced instance's open params as "sweepable and gangable". Shipped reality: **sweepable, not gangable** — the `gang` op refuses a composite instance's member path (a gang fuses a primitive's raw param slot; instance params are nested/path-qualified). The refusal is now test-pinned (`gang_of_a_spliced_instance_member_path_refuses`) and every prose surface (C24, authoring guide, spec) states it as such. Instance-param ganging — together with the gang-knob-bind pairing, the open-run refusal rendering, and `--unwired` on use-bearing documents — is bundled as the collective residue issue #339.
Author
Collaborator

Cycle close (fieldtest routing + one ratification)

Source-blind fieldtest (4 examples, corpus fieldtests/composites-use/):
the register→label→discover→use loop, all ref forms + refusals, and sweep
axes through a spliced instance all hold (3 working). Routed:

  • B1 → ratified, docs corrected (fieldtest had it bug→debug; overridden
    with rationale): an open pattern whose input role names an archive
    column runs standalone — the harness binds roles by name, which is
    C26's design, deterministic and useful. The defect was this cycle's own
    ratification sentence ("running standalone refuses"), not the code; the
    authoring guide now states the real contract (docs commit 26b3d68). C24's
    and the glossary's wording were already accurate.
  • S1 → ledger/guide tightened in the same commit (the run path's actual
    refusal trio: bias/tap gate, free knobs, role-column — the bootstrap
    unbound-role gate is a compile-level backstop a consumer rarely meets).
  • F1 → #339 item 1 evidence (gang-on-instance refusal should name the
    unsupported rule).
  • S2 → fixed (README's op enumeration was still nine; now ten).
  • S3 → ratified (the use-seam C29 gate is a backstop — register already
    gates both input forms; one line in C24).
  • Orthogonal pre-existing observation → #340 (aura run --params).

Cycle shipped as 7 commits on worktree-issue-317-composites
(7392075..26b3d68, base 05e9a00): registry labels, engine Op::Use + open
patterns, CLI surface (closes #317 on merge), ledger/glossary/guide, audit
close, fieldtest corpus, docs truth pass. All gates green: 3× grounding-check
(1 BLOCK fixed → 2× PASS), independent diff review (changes_requested →
approved after repair), architect (drift resolved: #339, minute correction
4629, pin test), bench all fingerprints OK, workspace suite + clippy clean.

## Cycle close (fieldtest routing + one ratification) Source-blind fieldtest (4 examples, corpus `fieldtests/composites-use/`): the register→label→discover→use loop, all ref forms + refusals, and sweep axes through a spliced instance all hold (3 working). Routed: - **B1 → ratified, docs corrected** (fieldtest had it bug→debug; overridden with rationale): an open pattern whose `input` role names an archive column **runs standalone** — the harness binds roles by name, which is C26's design, deterministic and useful. The defect was this cycle's own ratification sentence ("running standalone refuses"), not the code; the authoring guide now states the real contract (docs commit 26b3d68). C24's and the glossary's wording were already accurate. - **S1 → ledger/guide tightened** in the same commit (the run path's actual refusal trio: bias/tap gate, free knobs, role-column — the bootstrap unbound-role gate is a compile-level backstop a consumer rarely meets). - **F1 → #339 item 1 evidence** (gang-on-instance refusal should name the unsupported rule). - **S2 → fixed** (README's op enumeration was still nine; now ten). - **S3 → ratified** (the use-seam C29 gate is a backstop — register already gates both input forms; one line in C24). - Orthogonal pre-existing observation → **#340** (`aura run --params`). Cycle shipped as 7 commits on `worktree-issue-317-composites` (7392075..26b3d68, base 05e9a00): registry labels, engine Op::Use + open patterns, CLI surface (closes #317 on merge), ledger/glossary/guide, audit close, fieldtest corpus, docs truth pass. All gates green: 3× grounding-check (1 BLOCK fixed → 2× PASS), independent diff review (changes_requested → approved after repair), architect (drift resolved: #339, minute correction 4629, pin test), bench all fingerprints OK, workspace suite + clippy clean.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#317