Make composites data-authorable — a named, registered subgraph referencable like a node #317
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
Agreecombinator (emitawhenaandbshare a sign, else 0) is fully expressible with the shipped vocabulary:— and after its first
aura nodes newit 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 (theNamedfold 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).
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::Compositeunder an instancename; 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 stayself-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.docis&'static str, the rostermacro 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": … }, andgraph buildechoes 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
namefield, 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 surfacepath-qualified
<instance>.<node>.<param>(existing nested-compositeprefix discipline), sweepable and gangable;
bindbinds inner paramspath-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 closurethat 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 witha 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 yetcampaign-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.
Derived decision (mid-cycle, implementation evidence): open patterns at
finish()Batch-1 implementation surfaced a blocker for the spec's worked example:
GraphSession::finish()sharescheck_root_roles_boundwith compile, so anop-script declaring open
inputroles — the very form of a reusablepattern — cannot build at all today (no shipped fixture uses the
inputop; 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 forgottensourcefaults atfeedas 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.
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
gangop refuses a composite instance's member path (a gang fuses aprimitive'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
--unwiredon use-bearing documents — isbundled as the collective residue issue #339.
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:
with rationale): an open pattern whose
inputrole names an archivecolumn 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'sand the glossary's wording were already accurate.
refusal trio: bias/tap gate, free knobs, role-column — the bootstrap
unbound-role gate is a compile-level backstop a consumer rarely meets).
unsupported rule).
gates both input forms; one line in C24).
aura run --params).Cycle shipped as 7 commits on
worktree-issue-317-composites(7392075..26b3d68, base
05e9a00): registry labels, engine Op::Use + openpatterns, 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.