Build an introspectable, fail-fast construction service that emits blueprints #157
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?
C24/C17 (docs/design/INDEX.md): node logic stays Rust; topology is data the World or an LLM constructs. The Rust builders — GraphBuilder, PrimitiveBuilder (crates/aura-engine/src/blueprint.rs, crates/aura-core/src/node.rs) — deliver a correct graph at compile time via per-op kind matching, check_ports_connected (wiring totality), and check_param_namespace_injective (blueprint.rs). A data-level author (a small LLM especially) should not hand-write the format and satisfy those invariants blind — the failure mode that sank the RustAst DSL was unaided authoring of a many-invariant artifact, not the syntax.
Goal: a construction service that delivers the builder's compile-time guarantees on the data level, mirroring them:
It is a thin service over the already-correct Rust machinery, so it introduces no new language and no second validator (C24 forbids a Turing-complete or applied-DSL format).
Open fork (unresolved — its own design step, does not block #155): the surface — an MCP / tool API an LLM drives vs. stateful
aura graph ...subcommands over a session file. The format and the gates are surface-agnostic.Acceptance (validated property):
Depends on the serialization + loader issue #155.
Construction-service surface — fork resolved toward a declarative, replayable op-script over the CLI
The open fork in the issue body ("an MCP / tool API an LLM drives vs. stateful
aura graph …subcommands over a session file") resolves toward a declarative, replayable by-identifier op-script run through the CLI — not an MCP server, and not hand-authoring the serialized format from #155.What the surface is
The authoring surface is a list of by-identifier
GraphBuilderacts (add/connect/feed/expose, with named ports) as a document. Illustrative shape (syntax is aspecify/plannerconcern, not fixed here):aura graph build < scriptrebuilds theGraphBuilderstate inside one process, replays op by op, and stops at the first that does not resolve, naming it. The document is the artifact; each run replays it from scratch. This is the same compile path the Rust builder already takes (GraphBuilder::build→compile_with_params→FlatGraph), so a graph built through the ops runs identically to its Rust-built twin (acceptance criterion 1 below; C1 determinism — docs/design/INDEX.md).Why this shape, not the alternatives
Not hand-authoring the #155 format. The serialized blueprint (
crates/aura-engine/src/blueprint_serde.rs) wires by raw index (Edge { from, to, slot, from_field }) because it is the machine side of the two-level split (named blueprint source lowered to an index-wiredFlatGraph— C23, docs/design/INDEX.md). It is the compiled output, not an authoring surface; hand-editing it is a category error. The issue body's "should not hand-write the format and satisfy those invariants blind" means precisely this. Authoring is by-identifier (fast.out, never{from:0}); the service resolves names → indices exactly asGraphBuilder::builddoes today.Not a held session / MCP server (for now). A document replayed from scratch keeps the surface deterministic and serializable: the script is a World-owned topology value (C24, docs/design/INDEX.md), reproducible (C1), with no held process state that can diverge from the history that produced it. A RAM-session / MCP server is the one construction shape that can let result drift from history unless disciplined back to document-replay — it builds a process to enforce a model the file gives for free. MCP is deferred as consumer-strength-dependent: a held interactive dialog (op → "which slots are open?" → adaptively pick the next) only helps a weak author that must be probed step by step; a strong author (the real near-term consumer) plans the sequence and corrects against stop-at-first-error over the document.
The actual work: split the gates eager + holistic
Claim (from builder recon, to be ratified by the
specifygrounding-check): the three structural gates run today at finalize (compile_with_params), not per op —check_param_namespace_injective,validate_wiring(index-range + edge kind-match), andcheck_ports_connected(wiring totality), incrates/aura-engine/src/blueprint.rs. The op-script model requires splitting them:add→ the type is in the injected vocabulary;connect→ producer-field kind == consumer-port kind, and the consumer slot is not already wired.Plus build-free introspection (acceptance criterion 3 below): a read-only query over the vocabulary (node types, their ports + kinds, param paths — already declared pre-build on
PrimitiveBuilder's schema, C8) and over a partial script (the still-unwired slots). The vocabulary is the injected closed set (aura_std::std_vocabulary); no node registry (invariant 9, CLAUDE.md).This core (eager-split gates + introspection) is surface-agnostic — the CLI op-script is a thin shell over it, and an MCP adapter, if ever wanted, is additive over the same core.
Acceptance mapping (issue body)
GraphBuilder→compile_with_params, the existing path.Scope & sequencing
aura-cliharnesses (sample_blueprint,run_stage1_r, theHarnessKindset) are exactly the topology-as-source that C24 forbids and #159 retires. So this issue is valuable today against the existingaura-stdvocabulary — it is not gated on the project-as-crate / cdylib layer — and pairs with #159 (this issue supplies the data-authoring surface; #159 removes the source-topology). TheGraphBuildersurvives as an emitter of #155 data inside a topology generator (a Rust meta-program is fine — C20 — its output is data), never as the home of a concrete harness.d5602ec). The format extensions (#156) — construction-arg builders (LinComb/CostSum/SimBroker/Session) and recording sinks — are out of the round-trippable set today and additive later.Next step: enter
specifyfrom this comment — the design is settled here, with no open load-bearing fork remaining.Spec 0088 — derived design decisions (specify)
The construction-service surface fork was resolved earlier in this thread
(the "op-script over the CLI" comment). Writing the spec (
docs/specs/0088)surfaces four lower-level forks beneath it. All four are derived
orchestrator decisions (rationale given), not user decisions — recorded here
for after-the-fact audit and veto.
Fork: one validator or two → reuse the engine's existing gates; introduce
no second validator. The per-op (eager) checks and the end-of-document
(holistic) checks call the same extracted predicates at two cadences.
Basis: derived — the issue body forbids "a second validator" (C24);
duplicating the kind / coverage logic would be exactly that.
Fork: per-op fallibility on
GraphBuildervs. a new surface → a newper-op-fallible construction surface in
aura-enginethat reusesGraphBuilder's name-resolution (crates/aura-engine/src/builder.rs) and theshared predicates, leaving
GraphBuilder's infallible-accumulate-then-build()contract (builder.rs:1-9,:67-68) intact. Basis: derived — theop-script's stop-at-first-failing-op is a per-op-fallible discipline, whereas
GraphBuilder's contract is infallible ops + one falliblebuild().Overloading one type with both contracts is worse structural fit than a
sibling surface over shared internals.
Fork: the eager / holistic boundary → the "every interior input slot
covered exactly once" invariant (
check_ports_connected,crates/aura-engine/src/blueprint.rs:688) splits along its own two failurearms: the ≤1 half (a second cover =
DoubleWiredPort) is eagerlydecidable at the
connect/feedop; the ≥1 half (zero cover =UnconnectedPort) is only decidable once the document ends, so it staysholistic. Edge kind-match (
validate_wiring,blueprint.rs:638-649) iseager; param-namespace injectivity (
check_param_namespace_injective,blueprint.rs:566) is holistic. Basis: derived — a mid-document graph islegitimately incomplete, so totality cannot fire per-op; the split is forced
by what is decidable when.
Fork: document encoding → the canonical core encoding is a JSON
op-list (serde-native, mirroring the #155 blueprint format), demonstrated as
the worked example; a line-oriented human text front-end (the
add SMA(length=2) as fastshape illustrated earlier in this thread) is adeferred ergonomic sugar, not core. Basis: derived — the three acceptance
criteria are encoding-agnostic; JSON-through-serde adds no hand-written
parser / lexer (the smallest "no new language" surface) and syntax was
explicitly delegated to specify/planner ("syntax is a specify/planner concern,
not fixed here"). The replay /
< scriptdocument semantics are identicaleither way.
Consequence (not a fork): build-free vocabulary introspection (acceptance 3)
needs the closed
std_vocabularymatch (crates/aura-std/src/vocabulary.rs:30)paired with an enumerable companion list of its type identities — a
fn(&str) -> Option<…>resolver alone cannot be enumerated. The same iterablesurface would also give #160's guard test something to iterate, though #160
stays a separate item.
The spec auto-signs under
/bosson agrounding-checkPASS; a reply vetoesany of the above.
Iteration 2 (§C CLI) — wire-format decision (derived)
Iteration 1 (the §A+§B engine core) shipped:
aura_engine::{replay, GraphSession, Op, OpError}+ thestd_vocabulary_typesintrospection companion (commitsea1ca32,27ac4dc; all green). Iteration 2 is the CLI shell —aura graph build/aura graph introspectover a JSON op-list. Writing its plan(
docs/plans, cycle 0088) surfaces one load-bearing fork; derived (rationalebelow), recorded for audit/veto.
Fork: how the JSON op-list deserializes into the engine
Op→ a CLI-sideserde DTO (
#[serde(tag = "op", rename_all = "lowercase")]+ field renamestype→type_id,as→as_name) that maps into the engineOp, which staysserde-free. Basis: derived — keeping the wire shape out of the engine
preserves the engine as a pure data type (the wire format is a CLI concern, the
engine owns no second representation), and the document's hand-friendly keys
(
op/type/as) differ from the engine field names anyway.Two sub-decisions inside it:
Scalarform —"bind": {"length": {"I64": 2}},not a bare
2. Basis: derived — it deserializes directly viaScalar'sexisting #155 externally-tagged serde shape (no schema-aware kind-inference
decoder, no second vocabulary lookup), and a kind that disagrees with the
node's declared param is caught for free by the engine's
try_bindas a namedper-op fault.
ScalarKinduses its native capitalized form —"kind": "F64". Basis:derived — it is the single representation
Scalar/#155/theblueprint_to_jsonbuild output already use, so input and output stay consistent and no bespoke
case-mapping is introduced. (The spec's lowercase
"f64"illustration wasexplicitly non-binding: "syntax is a specify/planner concern.")
Two smaller derived calls the plan records but not worth their own fork here: the
op N (kind): causelabel is recovered by the CLI retaining the parsed oplist and indexing it by
replay's returned error index (no engine change, noOp: Clone); the per-variant error phrasing is a CLI format helper (theengine error types stay
Display-free, matchingCompileError/BootstrapError/BindOpErrortoday).aura graph buildemits the #155 blueprint JSON for a valid document; an invalidop prints
op N (kind): causeto stderr and exits non-zero;introspect(
--vocabulary/--node <T>/--unwired) answers build-free. The bare["graph"]HTML-render arm (sample_blueprint, #159) is left intact. A replyvetoes.