feat(aura-cli, aura-engine): use-aware unwired introspection; gang-rule prose; LinComb discovery
Harvest sweep, batch 5 of 6. - introspect --unwired resolves use refs through the registry store: the parse+resolve phase is extracted from graph build's path into the shared parse_and_resolve_ops, so a use-bearing document introspects identically to how it builds (fetch, C29 gate, label resolution) — retiring both the hard miss and the 'content id' mislabeling of by-name refs. The engine's subgraph closure contract is unchanged (store-freedom binds the engine, not the CLI caller). - ganging a spliced instance's member path refuses with the rule (OpError::GangOfSplicedInstance) instead of the typo-shaped no-such-param prose; the leaf-typo case keeps its UnknownParam shape under a new sibling pin. - the --node pending note names the follow-up moves on the current surface (build, then introspect --params / --unwired), and the authoring guide gains a worked LinComb op-script example (args-then-bind order, term[i]/weights[i], verified by building it). refs #339 refs #341
This commit is contained in:
@@ -156,16 +156,17 @@ impl OpDoc {
|
||||
}
|
||||
|
||||
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`).
|
||||
/// Infallible, context-free conversion. Neither production caller
|
||||
/// (`composite_from_str`'s / `introspect_unwired`'s shared
|
||||
/// [`parse_and_resolve_ops`] phase, #339 item 4 harvest) reaches the
|
||||
/// `OpDoc::Use` arm below with an unresolved ref: both intercept every
|
||||
/// `Op::Use` op and route it through `resolve_use_op` (registry fetch +
|
||||
/// C29 gate + full-vocabulary resolve) BEFORE ever calling `Op::from` —
|
||||
/// only the remaining, `use`-free op kinds reach this conversion as-is.
|
||||
/// The `OpDoc::Use` arm stays for match exhaustiveness (a bare,
|
||||
/// registry-free conversion, `ref_id` verbatim off the `UseRef`); a
|
||||
/// hypothetical direct caller would still get one, but every op-script
|
||||
/// entry point in this file resolves first.
|
||||
fn from(d: OpDoc) -> Op {
|
||||
match d {
|
||||
OpDoc::Source { role, kind } => Op::Source { role, kind },
|
||||
@@ -255,14 +256,20 @@ fn format_op_error(e: &OpError) -> String {
|
||||
format!("gang: `{node}.{param}` is already ganged")
|
||||
}
|
||||
OpError::GangArity { gang } => format!("gang `{gang}`: needs at least two members"),
|
||||
OpError::GangOfSplicedInstance { node, member } => format!(
|
||||
"gang: {node}.{member} is a spliced instance's member — ganging a used instance's member params is not yet supported"
|
||||
),
|
||||
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.
|
||||
// #317, extended #339 item 4 harvest: BOTH CLI entry points
|
||||
// (`composite_from_str`'s and `introspect_unwired`'s shared
|
||||
// `parse_and_resolve_ops` phase) resolve and fetch every `use` op
|
||||
// before replay/apply, so neither ever constructs an `Op::Use` whose
|
||||
// `ref_id` is anything but an already-resolved content id — this
|
||||
// arm is unreachable from the CLI surface. It stays reachable only
|
||||
// against a bare `GraphSession`/`replay` caller that hands `Op::Use`
|
||||
// a `ref_id` its own `subgraph` closure doesn't resolve (an engine-
|
||||
// level API misuse, not a document-authoring fault).
|
||||
OpError::UnknownSubgraph { ref_id } => {
|
||||
format!("use: no subgraph for content id {ref_id:?}")
|
||||
}
|
||||
@@ -408,13 +415,21 @@ fn resolve_use_op(
|
||||
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). 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> {
|
||||
/// [`parse_and_resolve_ops`]'s success payload: the parsed `Op`s, their
|
||||
/// by-index kind labels (`op N (kind): cause` attribution), and the id -> json
|
||||
/// cache the caller's own `subgraph` closure reads.
|
||||
type ResolvedOps = (Vec<Op>, Vec<String>, std::collections::HashMap<String, String>);
|
||||
|
||||
/// Parse a JSON op-list document into `Op`s, resolving every `use` op through
|
||||
/// the registry (fetch, C29-gate, full-vocabulary resolve) HERE, before either
|
||||
/// caller ever builds a session (#317, extended #339 item 4 harvest): the
|
||||
/// shared first phase of `composite_from_str` (`graph build`'s real path) AND
|
||||
/// `introspect_unwired`'s build-free path, so a `use`-bearing document
|
||||
/// resolves identically under both — introspection no longer treats a
|
||||
/// resolvable ref as a hard miss. A resolution/doc-gate fault is attributed
|
||||
/// exactly like any other per-op construction fault (same `op N (kind):
|
||||
/// cause` shape).
|
||||
fn parse_and_resolve_ops(doc: &str, env: &aura_runner::project::Env) -> Result<ResolvedOps, String> {
|
||||
// #336: deserialize element-by-element off the untyped `Value` array, not in
|
||||
// one `Vec<OpDoc>` shot. For an internally-tagged enum, serde_json's
|
||||
// `deny_unknown_fields` fault fires only once the parser has consumed the
|
||||
@@ -450,6 +465,15 @@ fn composite_from_str(doc: &str, env: &aura_runner::project::Env) -> Result<Comp
|
||||
};
|
||||
ops.push(op);
|
||||
}
|
||||
Ok((ops, labels, cache))
|
||||
}
|
||||
|
||||
/// 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).
|
||||
fn composite_from_str(doc: &str, env: &aura_runner::project::Env) -> Result<Composite, String> {
|
||||
let (ops, labels, cache) = parse_and_resolve_ops(doc, env)?;
|
||||
// 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
|
||||
@@ -527,7 +551,15 @@ pub fn introspect_node(type_id: &str, env: &aura_runner::project::Env) -> Result
|
||||
out.push_str(&format!(" arg {}: {:?} ({})\n", spec.name, spec.kind, spec.kind.hint()));
|
||||
}
|
||||
if builder.is_pending() {
|
||||
out.push_str(" note ports and params form at construction; args are required\n");
|
||||
// #341 item 3 harvest: name the follow-up moves on the CURRENT
|
||||
// surface, not just the fact that ports/params are absent — a
|
||||
// consumer's first port guess is otherwise a coin flip, and only
|
||||
// trial refusals surface the real names.
|
||||
out.push_str(
|
||||
" note ports and params form at construction; args are required — build \
|
||||
the op-script, then `aura graph introspect --params <bp.json>` shows the open knobs and \
|
||||
`--unwired` on a partial document shows the unfilled slots\n",
|
||||
);
|
||||
}
|
||||
for port in &schema.inputs {
|
||||
out.push_str(&format!(" in {}:{:?}\n", port.name, port.kind));
|
||||
@@ -542,17 +574,29 @@ pub fn introspect_node(type_id: &str, env: &aura_runner::project::Env) -> Result
|
||||
}
|
||||
|
||||
/// `aura graph introspect --unwired`: the still-open interior slots of a partial
|
||||
/// op-list document, by-identifier (applies the ops, does NOT finalize).
|
||||
/// op-list document, by-identifier (applies the ops, does NOT finalize). A
|
||||
/// `use` op resolves through the registry via the SAME [`parse_and_resolve_ops`]
|
||||
/// phase `composite_from_str` uses (#339 item 4 harvest: this path used to pass
|
||||
/// `&|_| None` as the subgraph resolver, so a use-bearing document always
|
||||
/// missed with `OpError::UnknownSubgraph` — mislabeling a by-name ref as a
|
||||
/// "content id" in the bargain); the real store resolver threads in cleanly
|
||||
/// because `unwired()` is read straight off the (unfinished) session,
|
||||
/// unaffected by the `use` resolution happening one phase earlier.
|
||||
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 (ops, labels, cache) = parse_and_resolve_ops(doc, env)?;
|
||||
let resolver = |t: &str| env.resolve(t);
|
||||
// #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)))?;
|
||||
let subgraph = |ref_id: &str| {
|
||||
cache.get(ref_id).and_then(|json| blueprint_from_json(json, &|t| env.resolve(t)).ok())
|
||||
};
|
||||
let mut session = GraphSession::new("introspect", &resolver, &subgraph);
|
||||
for (i, op) in ops.into_iter().enumerate() {
|
||||
session.apply(op).map_err(|e| {
|
||||
let cause = format_op_error(&e);
|
||||
match labels.get(i) {
|
||||
Some(kind) => format!("op {i} ({kind}): {cause}"),
|
||||
None => format!("op {i}: {cause}"),
|
||||
}
|
||||
})?;
|
||||
}
|
||||
let mut out = String::new();
|
||||
for (slot, kind) in session.unwired() {
|
||||
|
||||
Reference in New Issue
Block a user