feat(aura-cli): use op on the data plane — register --name, resolution echo, discovery
The op-script gains its tenth verb end-to-end: OpDoc::Use carries a tagged ref
({"content_id": id-or-unique-prefix} | {"name": label}); DTO conversion resolves
it (label sidecar / #302 prefix semantics / verbatim id), fetches, deserializes
eagerly (a vocab/parse failure fails fast naming the id prefix — review
finding, no swallowed .ok()), re-walks the C29 doc gate over the fetched
composite (a doc-less store entry cannot enter a NEW composition; existing
reads stay valid — no retroactive invalidation), echoes every resolution as
the existing benign marker (aura: note: use "<instance>": <ref> -> <id>,
stderr; stdout stays clean payload), and hands the session a pure cached
lookup closure.
graph register <file> --name <label> records the label and echoes repoints
(label "x" -> <new> (was <old>)); graph introspect --registered lists
label / 12-char id prefix / root doc line — the discovery surface for what
use can reference. Refusals are by-identifier, name their rule, and enumerate
the label set (empty store reads as prose); all op-list content faults are
runtime exit 1 per the pinned #175 attribution, argv misuse stays exit 2.
Ten new/extended e2e tests cover label round-trip + repoint, echo discipline,
unknown-label enumeration, the C29 refusal shape, end-to-end sweep axes
through a spliced instance (graph.<instance>.<node>.<param>), the open-pattern
build flow, and the standalone-run bootstrap refusal (asserts the existing
Debug-form fault; an index->name rendering at the run boundary is recorded
residue, not silently claimed). OP_REFERENCE and its help pin move 9 -> 10.
Empirical note for the record: aura run's bias arm re-roots the loaded signal
via wrap_r, so the standalone-run refusal for an open pattern is exercised on
the bare-tap measurement path; the bias path surfaces openness as a role-
binding refusal instead (pre-existing shape, untouched).
closes #317
This commit is contained in:
@@ -9,16 +9,18 @@ use std::path::Path;
|
||||
|
||||
use aura_engine::{
|
||||
blueprint_from_json, blueprint_identity_json, blueprint_to_json, replay, BindOpError,
|
||||
CompileError, Composite, GraphSession, LoadError, Op, OpError, Scalar, ScalarKind,
|
||||
BlueprintDoc, CompileError, Composite, GraphSession, LoadError, Op, OpError, Scalar, ScalarKind,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::research_docs::resolve_id_prefix;
|
||||
|
||||
// `std_vocabulary` is now only reached through `aura_runner::project::Env::resolve` in
|
||||
// production code; tests still exercise it directly.
|
||||
#[cfg(test)]
|
||||
use aura_vocabulary::std_vocabulary;
|
||||
|
||||
/// The op-list reference `aura graph build --help` appends (#323): the nine
|
||||
/// The op-list reference `aura graph build --help` appends (#323): the ten
|
||||
/// op kinds with their fields and one worked element each. Lives beside
|
||||
/// [`OpDoc`] so a new op variant is one screen away from its help line.
|
||||
pub const OP_REFERENCE: &str = r#"Op-list reference (stdin: a JSON array of op objects, applied in order):
|
||||
@@ -40,6 +42,9 @@ pub const OP_REFERENCE: &str = r#"Op-list reference (stdin: a JSON array of op o
|
||||
fuse two or more sibling params into one public knob
|
||||
{"op":"doc","text":"..."}
|
||||
declare the composite's one-line meaning (C29)
|
||||
{"op":"use","ref":{"name":"agree"},"name":"gate","bind":{"sma.length":{"I64":9}}}
|
||||
splice a registered blueprint (by "content_id" or "name") under an
|
||||
instance name ("bind" path-qualifies the spliced instance's params)
|
||||
|
||||
Node types and their ports: aura graph introspect --vocabulary | --node <T>"#;
|
||||
|
||||
@@ -82,26 +87,66 @@ enum OpDoc {
|
||||
/// Declare the composite's one-line meaning (C29, #316) — the op-script
|
||||
/// twin of the builder's `.doc(...)`.
|
||||
Doc { text: String },
|
||||
/// Splice a registered blueprint into the building graph as a nested
|
||||
/// composite (#317) — the DTO the engine's `Op::Use` never sees
|
||||
/// directly: the CLI resolves `ref` (a store content id, a unique
|
||||
/// content-id prefix, or a registry name label) to the full content id
|
||||
/// at conversion time, before the engine ever runs.
|
||||
Use {
|
||||
#[serde(rename = "ref")]
|
||||
r#ref: UseRef,
|
||||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
#[serde(default)]
|
||||
bind: BTreeMap<String, Scalar>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A `use` op's reference (#317) — exactly one of a store content id (full
|
||||
/// or a unique prefix, #302 semantics) or a registry name label
|
||||
/// (`graph register --name`, latest-wins).
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
enum UseRef {
|
||||
#[serde(rename = "content_id")]
|
||||
ContentId(String),
|
||||
#[serde(rename = "name")]
|
||||
Name(String),
|
||||
}
|
||||
|
||||
impl OpDoc {
|
||||
/// The op-kind label for the `op N (kind): cause` message.
|
||||
fn kind_label(&self) -> &'static str {
|
||||
/// The op-kind label for the `op N (kind): cause` message. A `use` op
|
||||
/// carries its instance name (when the author gave one) so a `use`
|
||||
/// fault reads `use "gate"`, not a bare, undifferentiated `use` — the
|
||||
/// only kind whose label is not a fixed string (#317).
|
||||
fn kind_label(&self) -> String {
|
||||
match self {
|
||||
OpDoc::Source { .. } => "source",
|
||||
OpDoc::Input { .. } => "input",
|
||||
OpDoc::Add { .. } => "add",
|
||||
OpDoc::Feed { .. } => "feed",
|
||||
OpDoc::Connect { .. } => "connect",
|
||||
OpDoc::Expose { .. } => "expose",
|
||||
OpDoc::Tap { .. } => "tap",
|
||||
OpDoc::Gang { .. } => "gang",
|
||||
OpDoc::Doc { .. } => "doc",
|
||||
OpDoc::Source { .. } => "source".to_string(),
|
||||
OpDoc::Input { .. } => "input".to_string(),
|
||||
OpDoc::Add { .. } => "add".to_string(),
|
||||
OpDoc::Feed { .. } => "feed".to_string(),
|
||||
OpDoc::Connect { .. } => "connect".to_string(),
|
||||
OpDoc::Expose { .. } => "expose".to_string(),
|
||||
OpDoc::Tap { .. } => "tap".to_string(),
|
||||
OpDoc::Gang { .. } => "gang".to_string(),
|
||||
OpDoc::Doc { .. } => "doc".to_string(),
|
||||
OpDoc::Use { name: Some(n), .. } => format!("use {n:?}"),
|
||||
OpDoc::Use { name: None, .. } => "use".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OpDoc> for Op {
|
||||
/// Infallible, context-free conversion — used directly only by
|
||||
/// build-free introspection paths (`introspect --unwired`, #317's
|
||||
/// spec: "Build-free introspection paths pass a `|_| None` closure"),
|
||||
/// which never resolve a `use` ref through the registry. A bare
|
||||
/// `OpDoc::Use` therefore maps its `UseRef` payload verbatim into
|
||||
/// `ref_id` (unresolved) — that session's `subgraph` closure is always
|
||||
/// `&|_| None`, so any `use` op there faults `UnknownSubgraph`
|
||||
/// regardless of the exact `ref_id` text; `graph build`'s real path
|
||||
/// (`composite_from_str`) never reaches this arm — it resolves and
|
||||
/// replaces each `Op::Use` before conversion (see `resolve_use_op`).
|
||||
fn from(d: OpDoc) -> Op {
|
||||
match d {
|
||||
OpDoc::Source { role, kind } => Op::Source { role, kind },
|
||||
@@ -115,6 +160,14 @@ impl From<OpDoc> for Op {
|
||||
OpDoc::Tap { from, as_name } => Op::Tap { from, as_name },
|
||||
OpDoc::Gang { as_name, into } => Op::Gang { as_name, into },
|
||||
OpDoc::Doc { text } => Op::Doc { text },
|
||||
OpDoc::Use { r#ref, name, bind } => Op::Use {
|
||||
ref_id: match r#ref {
|
||||
UseRef::ContentId(id) => id,
|
||||
UseRef::Name(name) => name,
|
||||
},
|
||||
name,
|
||||
bind: bind.into_iter().collect(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,10 +200,12 @@ fn format_op_error(e: &OpError) -> String {
|
||||
BindOpError::KindMismatch { param, expected, got } => {
|
||||
format!("param {node}.{param} expects {expected:?} but got {got:?}")
|
||||
}
|
||||
BindOpError::AlreadyGanged { param, gang } => {
|
||||
format!("cannot bind {node}.{param} — member of gang {gang:?}")
|
||||
}
|
||||
},
|
||||
OpError::UnconnectedPort { node, slot } => format!("slot {node}.{slot} is unconnected"),
|
||||
OpError::RoleKindMismatch { role } => format!("input role {role} fans into slots of differing kinds"),
|
||||
OpError::UnboundRootRole { role } => format!("root input role {role} is unbound"),
|
||||
OpError::GangKindMismatch { member, expected, got } => {
|
||||
format!("gang: member `{member}` is {got:?}, expected {expected:?}")
|
||||
}
|
||||
@@ -160,18 +215,174 @@ fn format_op_error(e: &OpError) -> String {
|
||||
OpError::GangArity { gang } => format!("gang `{gang}`: needs at least two members"),
|
||||
OpError::Incomplete(ce) => format!("{ce:?}"),
|
||||
OpError::DuplicateDoc => "a doc op may appear at most once".to_string(),
|
||||
// #317: `graph build`'s real path (`composite_from_str`) resolves and
|
||||
// fetches every `use` op before replay, so this never fires there —
|
||||
// but `introspect --unwired` stays build-free/subgraph-free by spec
|
||||
// (`&|_| None`, see `From<OpDoc> for Op`), so a `use` op in a partial
|
||||
// document reaches this arm through THAT path, by-identifier on the
|
||||
// (unresolved) `ref_id` text.
|
||||
OpError::UnknownSubgraph { ref_id } => {
|
||||
format!("use: no subgraph for content id {ref_id:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve one `use` op's [`UseRef`] to the full store content id (#317):
|
||||
/// verbatim if it already IS a 64-hex content id, else a unique content-id
|
||||
/// prefix (#302 semantics, reusing [`resolve_id_prefix`]) or a registry name
|
||||
/// label (latest-wins, [`aura_registry::Registry::resolve_blueprint_label`]).
|
||||
/// Returns the full id plus the `<label-or-prefix> -> <full id>` text the
|
||||
/// resolution echo shows — the bare cause on a miss (unknown label / unknown
|
||||
/// or ambiguous prefix), for the caller to attribute by op index.
|
||||
fn resolve_use_ref_id(r: &UseRef, registry: &aura_registry::Registry) -> Result<(String, String), String> {
|
||||
match r {
|
||||
UseRef::Name(label) => {
|
||||
let id = registry.resolve_blueprint_label(label).ok_or_else(|| {
|
||||
let labels = registry.blueprint_labels();
|
||||
if labels.is_empty() {
|
||||
format!("no registered blueprint labeled {label:?} — no labels registered")
|
||||
} else {
|
||||
let names = labels.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>().join(", ");
|
||||
format!("no registered blueprint labeled {label:?} — registered labels: {names}")
|
||||
}
|
||||
})?;
|
||||
Ok((id.clone(), format!("{label} -> {id}")))
|
||||
}
|
||||
UseRef::ContentId(raw) => {
|
||||
if aura_runner::axes::is_content_id(raw) {
|
||||
return Ok((raw.clone(), format!("{raw} -> {raw}")));
|
||||
}
|
||||
let candidates = registry.list_blueprint_ids().map_err(|e| e.to_string())?;
|
||||
match resolve_id_prefix(raw, &candidates)? {
|
||||
Some(full) => {
|
||||
let shown = format!("{raw} -> {full}");
|
||||
Ok((full, shown))
|
||||
}
|
||||
None => Err(format!("no registered blueprint matches content id {raw:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The 12-char content-id prefix convention `graph introspect --registered`
|
||||
/// shows (#317), reused in the use-seam C29 refusal so both surfaces name a
|
||||
/// blueprint the same shortened way.
|
||||
fn id_prefix12(id: &str) -> &str {
|
||||
&id[..id.len().min(12)]
|
||||
}
|
||||
|
||||
/// The use-seam C29 doc-gate refusal cause (#317): re-run
|
||||
/// [`aura_registry::gate_composite_docs`] — the SAME walk `graph register`
|
||||
/// gates the write path with — over a FETCHED composite, before it ever
|
||||
/// reaches the session; names the fetched blueprint's id-prefix and the
|
||||
/// failing (root or nested — from the consumer's view, everything fetched is
|
||||
/// "nested") composite by identifier, mirroring `research_docs.rs`'s
|
||||
/// `BadDescription` phrasing for the same two `DocGateFault` kinds.
|
||||
fn use_doc_gate_cause(full_id: &str, e: &aura_registry::RegistryError) -> String {
|
||||
let aura_registry::RegistryError::UndescribedComposite { name, fault } = e else {
|
||||
// Defensive: `gate_composite_docs` only ever constructs
|
||||
// `UndescribedComposite`; every other `RegistryError` variant is
|
||||
// unreachable from this call, but the match stays total rather than
|
||||
// panicking on a surprise variant.
|
||||
return format!("registered blueprint {} fails the description gate", id_prefix12(full_id));
|
||||
};
|
||||
let rule = match fault {
|
||||
aura_core::DocGateFault::Empty => format!(
|
||||
"nested composite {name:?} has no description — re-register it with a \"doc\" op (C29)"
|
||||
),
|
||||
aura_core::DocGateFault::RestatesName => {
|
||||
format!("nested composite {name:?} has a doc that merely restates its name (C29)")
|
||||
}
|
||||
};
|
||||
format!("registered blueprint {} fails the description gate: {rule}", id_prefix12(full_id))
|
||||
}
|
||||
|
||||
/// Resolve, fetch, C29-gate, and echo one `use` op (#317) — the CLI-side
|
||||
/// half of the store/engine split (the engine never resolves a label, a
|
||||
/// prefix, or the registry itself). On success, caches the fetched
|
||||
/// CANONICAL JSON (not the parsed `Composite` — `Composite` is not `Clone`,
|
||||
/// mirroring the engine's own `use_fixture`-reload test pattern) under the
|
||||
/// full content id, so the `subgraph` closure handed to `replay` is a pure,
|
||||
/// registry-free lookup that re-parses on every call. Returns the bare
|
||||
/// cause on any refusal; the caller attributes it by op index.
|
||||
fn resolve_use_op(
|
||||
r#ref: UseRef,
|
||||
name: Option<String>,
|
||||
bind: BTreeMap<String, Scalar>,
|
||||
env: &aura_runner::project::Env,
|
||||
cache: &mut std::collections::HashMap<String, String>,
|
||||
) -> Result<Op, String> {
|
||||
let registry = env.registry();
|
||||
let (full_id, shown) = resolve_use_ref_id(&r#ref, ®istry)?;
|
||||
let json = registry
|
||||
.get_blueprint(&full_id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| format!("no registered blueprint {full_id}"))?;
|
||||
let doc: BlueprintDoc = serde_json::from_str(&json)
|
||||
.map_err(|e| format!("registered blueprint {full_id} is not a valid blueprint: {e}"))?;
|
||||
// C29 at the use seam: gate the DATA form (no vocabulary needed) before
|
||||
// the composite ever reaches the session.
|
||||
if let Err(e) = aura_registry::gate_composite_docs(&doc.blueprint) {
|
||||
return Err(use_doc_gate_cause(&full_id, &e));
|
||||
}
|
||||
// Resolve the fetched envelope through the FULL vocabulary here, at DTO
|
||||
// conversion (review finding, #317 follow-up): deferring this to the
|
||||
// `subgraph` closure's lazy re-parse let a deserialize/vocab-resolution
|
||||
// failure fold into `.ok() -> None`, which `replay` then misreports as
|
||||
// `UnknownSubgraph` — a fetched-but-unloadable blueprint is a runtime
|
||||
// content fault (exit 1), not a missing reference. `blueprint_load_prose`
|
||||
// + `unresolved_namespace_hint` are the same house-style pair
|
||||
// `blueprint_slot_prose`/`composite_from_any` already use over a
|
||||
// `LoadError`.
|
||||
if let Err(e) = blueprint_from_json(&json, &|t| env.resolve(t)) {
|
||||
let mut msg = blueprint_load_prose(&e);
|
||||
if let Some(hint) = unresolved_namespace_hint(&e, env) {
|
||||
msg.push_str(" — ");
|
||||
msg.push_str(&hint);
|
||||
}
|
||||
return Err(format!("registered blueprint {} is invalid: {msg}", id_prefix12(&full_id)));
|
||||
}
|
||||
let instance = name.clone().unwrap_or_else(|| doc.blueprint.name.clone());
|
||||
// The resolution echo (C14 benign class): stderr only, so stdout stays
|
||||
// clean payload (#164's convention, extended to this new note).
|
||||
crate::diag::note!("use {instance:?}: {shown}");
|
||||
cache.insert(full_id.clone(), json);
|
||||
Ok(Op::Use { ref_id: full_id, name, bind: bind.into_iter().collect() })
|
||||
}
|
||||
|
||||
/// Parse a JSON op-list document and replay it through the env's vocabulary into
|
||||
/// a built `Composite` — or a `op N (kind): cause` message (a per-op fault,
|
||||
/// attributed by the retained op-kind list) / `finalize: cause` (a holistic fault
|
||||
/// past the last op).
|
||||
/// past the last op). Every `use` op resolves through the registry HERE, before
|
||||
/// replay (#317): a resolution/doc-gate fault is attributed exactly like any
|
||||
/// other per-op construction fault (same `op N (kind): cause` shape, exit 1).
|
||||
fn composite_from_str(doc: &str, env: &aura_runner::project::Env) -> Result<Composite, String> {
|
||||
let docs: Vec<OpDoc> = serde_json::from_str(doc).map_err(|e| format!("invalid op-list: {e}"))?;
|
||||
let labels: Vec<&'static str> = docs.iter().map(OpDoc::kind_label).collect();
|
||||
let ops: Vec<Op> = docs.into_iter().map(Op::from).collect();
|
||||
replay("graph", ops, &|t| env.resolve(t)).map_err(|(idx, err)| {
|
||||
let labels: Vec<String> = docs.iter().map(OpDoc::kind_label).collect();
|
||||
let mut cache: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
||||
let mut ops: Vec<Op> = Vec::with_capacity(docs.len());
|
||||
for (idx, d) in docs.into_iter().enumerate() {
|
||||
let op = match d {
|
||||
OpDoc::Use { r#ref, name, bind } => match resolve_use_op(r#ref, name, bind, env, &mut cache) {
|
||||
Ok(op) => op,
|
||||
Err(cause) => return Err(format!("op {idx} ({}): {cause}", labels[idx])),
|
||||
},
|
||||
other => Op::from(other),
|
||||
};
|
||||
ops.push(op);
|
||||
}
|
||||
// The injected `subgraph` lookup (#317): a pure, registry-free map read —
|
||||
// every `use` op's blueprint was already fetched, gated, AND resolved
|
||||
// (`resolve_use_op`'s eager `blueprint_from_json` check) above; this
|
||||
// closure only re-parses the cached bytes (`Composite` is not `Clone`).
|
||||
// The `.ok()` is defensive, not a fallible path in practice: re-parsing
|
||||
// the SAME cached JSON through the SAME pure resolver cannot fail here
|
||||
// once it has already succeeded once above (review finding, #317
|
||||
// follow-up — the failure case now surfaces eagerly, at DTO conversion).
|
||||
let subgraph = |ref_id: &str| {
|
||||
cache.get(ref_id).and_then(|json| blueprint_from_json(json, &|t| env.resolve(t)).ok())
|
||||
};
|
||||
replay("graph", ops, &|t| env.resolve(t), &subgraph).map_err(|(idx, err)| {
|
||||
let mut cause = format_op_error(&err);
|
||||
// #244: the op-script path reports an unresolvable namespaced type as
|
||||
// `OpError::UnknownNodeType`, a different error family than the
|
||||
@@ -245,7 +456,11 @@ pub fn introspect_node(type_id: &str, env: &aura_runner::project::Env) -> Result
|
||||
pub fn introspect_unwired(doc: &str, env: &aura_runner::project::Env) -> Result<String, String> {
|
||||
let docs: Vec<OpDoc> = serde_json::from_str(doc).map_err(|e| format!("invalid op-list: {e}"))?;
|
||||
let resolver = |t: &str| env.resolve(t);
|
||||
let mut session = GraphSession::new("introspect", &resolver);
|
||||
// #317: build-free introspection stays subgraph-free by design (spec:
|
||||
// "Build-free introspection paths pass a `|_| None` closure") — a `use`
|
||||
// op here always misses (`OpError::UnknownSubgraph`, `From<OpDoc>`'s own
|
||||
// doc comment), never a registry read.
|
||||
let mut session = GraphSession::new("introspect", &resolver, &|_: &str| None);
|
||||
for (i, d) in docs.into_iter().enumerate() {
|
||||
session.apply(Op::from(d)).map_err(|e| format!("op {i}: {}", format_op_error(&e)))?;
|
||||
}
|
||||
@@ -256,6 +471,33 @@ pub fn introspect_unwired(doc: &str, env: &aura_runner::project::Env) -> Result<
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `aura graph introspect --registered` (#317): one row per registry label —
|
||||
/// `<label> <12-char id prefix> <root doc line>`, latest-wins (the label
|
||||
/// sidecar's own resolution rule) — the discovery surface the worked
|
||||
/// acceptance program's last step reads. An empty store is `no labels
|
||||
/// registered` on stdout, exit 0 (a listing, not a fault); dangling label
|
||||
/// rows (content id no longer resolves) are already skipped by
|
||||
/// `blueprint_labels()`.
|
||||
fn introspect_registered(env: &aura_runner::project::Env) -> Result<String, String> {
|
||||
let registry = env.registry();
|
||||
let labels = registry.blueprint_labels();
|
||||
if labels.is_empty() {
|
||||
return Ok("no labels registered\n".to_string());
|
||||
}
|
||||
let mut out = String::new();
|
||||
for (name, id) in labels {
|
||||
let json = registry
|
||||
.get_blueprint(&id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| format!("label {name:?} names {id}, which is not in the store"))?;
|
||||
let doc: BlueprintDoc = serde_json::from_str(&json)
|
||||
.map_err(|e| format!("registered blueprint {id} is not a valid blueprint: {e}"))?;
|
||||
let root_doc = doc.blueprint.doc.as_deref().unwrap_or("");
|
||||
out.push_str(&format!("{name} {} {root_doc}\n", id_prefix12(&id)));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `aura graph introspect`: dispatch the read-only queries. Exactly one of
|
||||
/// `--vocabulary` / `--node <T>` / `--unwired` / `--params <FILE|ID>` / the id
|
||||
/// group must be set; zero or more than one is the usage error (exit 2). The id
|
||||
@@ -266,11 +508,12 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
|
||||
+ cmd.node.is_some() as usize
|
||||
+ cmd.unwired as usize
|
||||
+ cmd.folds as usize
|
||||
+ cmd.registered as usize
|
||||
+ cmd.params.is_some() as usize
|
||||
+ (cmd.content_id.is_some() || cmd.identity_id) as usize;
|
||||
if count != 1 {
|
||||
eprintln!(
|
||||
"aura: Usage: aura graph introspect --vocabulary | --node <T> | --unwired | --folds | --params <FILE|ID> | --content-id [FILE] | --identity-id (the two id flags may be combined)"
|
||||
"aura: Usage: aura graph introspect --vocabulary | --node <T> | --unwired | --folds | --registered | --params <FILE|ID> | --content-id [FILE] | --identity-id (the two id flags may be combined)"
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
@@ -294,6 +537,17 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd, env: &aura_runner::project
|
||||
for (label, doc) in aura_runner::FoldRegistry::core().roster() {
|
||||
println!("{label:<7} — {doc}");
|
||||
}
|
||||
} else if cmd.registered {
|
||||
// The label sidecar's discovery surface (#317): one row per
|
||||
// registered label — nothing to build, so a store-only read, exit 0
|
||||
// even on an empty store (a listing, not a fault).
|
||||
match introspect_registered(env) {
|
||||
Ok(s) => print!("{s}"),
|
||||
Err(m) => {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
} else if let Some(type_id) = cmd.node.as_deref() {
|
||||
match introspect_node(type_id, env) {
|
||||
Ok(s) => print!("{s}"),
|
||||
@@ -580,13 +834,20 @@ fn params_lines(target: &str, env: &aura_runner::project::Env) -> Result<String,
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `aura graph register <blueprint.json>` (#196): parse the blueprint through
|
||||
/// the project vocabulary, canonicalize, content-address, and store — the
|
||||
/// `process register` pattern, printing the store path so the trail is
|
||||
/// followable. Prose to stderr + exit 1 on any refusal.
|
||||
pub fn register_cmd(file: &Path, env: &aura_runner::project::Env) {
|
||||
match register_blueprint(file, env) {
|
||||
Ok(line) => println!("{line}"),
|
||||
/// `aura graph register <blueprint.json> [--name <label>]` (#196, `--name`
|
||||
/// #317): parse the blueprint through the project vocabulary, canonicalize,
|
||||
/// content-address, and store — the `process register` pattern, printing
|
||||
/// the store path so the trail is followable. With `--name`, additionally
|
||||
/// labels the stored content id (`aura_registry::Registry::put_blueprint_label`),
|
||||
/// echoing the repoint when the label already pointed elsewhere. Prose to
|
||||
/// stderr + exit 1 on any refusal.
|
||||
pub fn register_cmd(file: &Path, name: Option<&str>, env: &aura_runner::project::Env) {
|
||||
match register_blueprint(file, name, env) {
|
||||
Ok(lines) => {
|
||||
for line in lines {
|
||||
println!("{line}");
|
||||
}
|
||||
}
|
||||
Err(m) => {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(1);
|
||||
@@ -594,7 +855,11 @@ pub fn register_cmd(file: &Path, env: &aura_runner::project::Env) {
|
||||
}
|
||||
}
|
||||
|
||||
fn register_blueprint(file: &Path, env: &aura_runner::project::Env) -> Result<String, String> {
|
||||
fn register_blueprint(
|
||||
file: &Path,
|
||||
name: Option<&str>,
|
||||
env: &aura_runner::project::Env,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let text = std::fs::read_to_string(file)
|
||||
.map_err(|e| format!("cannot read {}: {e}", file.display()))?;
|
||||
// Shape-discriminated like file-mode --content-id (#202): either shape
|
||||
@@ -604,10 +869,21 @@ fn register_blueprint(file: &Path, env: &aura_runner::project::Env) -> Result<St
|
||||
let id = crate::content_id(&canonical);
|
||||
let registry = env.registry();
|
||||
registry.put_blueprint(&id, &canonical).map_err(|e| e.to_string())?;
|
||||
Ok(format!(
|
||||
let mut lines = vec![format!(
|
||||
"registered blueprint {id} ({})",
|
||||
registry.blueprint_path(&id).display()
|
||||
))
|
||||
)];
|
||||
if let Some(label) = name {
|
||||
// Resolve BEFORE writing, so the repoint echo compares against the
|
||||
// pre-write target (#317).
|
||||
let previous = registry.resolve_blueprint_label(label);
|
||||
registry.put_blueprint_label(label, &id).map_err(|e| e.to_string())?;
|
||||
lines.push(match previous {
|
||||
Some(old) if old != id => format!("label {label:?} -> {id} (was {old})"),
|
||||
_ => format!("label {label:?} -> {id}"),
|
||||
});
|
||||
}
|
||||
Ok(lines)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -636,7 +912,8 @@ mod tests {
|
||||
assert_eq!(docs[1].kind_label(), "add");
|
||||
let ops: Vec<Op> = docs.into_iter().map(Op::from).collect();
|
||||
// both SMA lengths are bound, so the only open param is bias.scale (f64).
|
||||
let composite = replay("graph", ops, &std_vocabulary).expect("replay resolves");
|
||||
let composite =
|
||||
replay("graph", ops, &std_vocabulary, &|_: &str| None).expect("replay resolves");
|
||||
composite.compile_with_params(&[Scalar::f64(0.5)]).expect("compiles");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user