feat(aura-cli, aura-runner): exec routing-seam refusals + by-name unbound-role prose
Harvest sweep, batch 2 of 6.
- exec's file-target classification is now a four-way shape peek
(campaign / blueprint / op-script / wrong-kind): a valid-JSON op-script
array refuses at the routing seam with a build-first pointer ('aura
graph build') instead of the blueprint leg's false 'not valid JSON'
claim, and a kind-bearing non-campaign document names the found kind
plus exec's two executable classes. Exit-code classes unchanged
(op-script usage-class 2, wrong-kind runtime-class 1).
- the bare-tap measurement leg renders CompileError::UnboundRootRole by
the signal's own root-role NAME (captured pre-consumption), retiring
the raw Debug-form leak; every other variant keeps the total fallback.
refs #342
refs #339
This commit is contained in:
+105
-31
@@ -663,13 +663,17 @@ fn parse_override_tokens(tokens: &[String]) -> Vec<(String, Scalar)> {
|
||||
// The declarative argument grammar. clap owns argv tokenizing, scoped `--help`,
|
||||
// `--version`, `--flag=value`, `--`, and long-option abbreviation; the `dispatch_*`
|
||||
// handlers below convert each `*Cmd` into the argument shapes the existing execution
|
||||
// fns accept. `exec` (#319) is the one document-executing verb: it dispatches on
|
||||
// `is_blueprint_file` layered with `classify_exec_document_file` (the "kind"-field
|
||||
// peek) — a first-positional naming an existing `.json` file that parses as JSON
|
||||
// and carries no top-level "kind" selects the loaded-blueprint leg; a file
|
||||
// carrying "kind", or a target that is not a readable `.json` file at all (a
|
||||
// fns accept. `exec` (#319, extended #342) is the one document-executing verb: it
|
||||
// dispatches on `is_blueprint_file` layered with `classify_exec_document_file`'s
|
||||
// shape peek (top-level array vs. object, then the "kind" field) — a
|
||||
// first-positional naming an existing `.json` file that parses as JSON and carries
|
||||
// no top-level "kind" selects the loaded-blueprint leg; a file carrying
|
||||
// `"kind":"campaign"`, or a target that is not a readable `.json` file at all (a
|
||||
// registered content id), selects the campaign leg; a file that does not parse as
|
||||
// JSON at all refuses neutrally before either leg's own validation runs.
|
||||
// JSON at all refuses neutrally before either leg's own validation runs; a
|
||||
// top-level JSON array (an op-script) or a `"kind"` naming anything other than
|
||||
// "campaign" (e.g. a process document) is likewise refused right at this seam,
|
||||
// with exec's own vocabulary, before either leg ever sees it.
|
||||
// `--override`/`--tap` tokens are lexed by `parse_override_tokens`/
|
||||
// `tap_plan_from_args` above, shared across both legs where applicable. Usage
|
||||
// errors exit 2; runtime refusals exit 1; a campaign that completes with ≥1
|
||||
@@ -967,27 +971,63 @@ fn is_blueprint_file(arg: &Option<String>) -> Option<&str> {
|
||||
.filter(|a| a.ends_with(".json") && std::path::Path::new(a).is_file())
|
||||
}
|
||||
|
||||
/// `exec`'s FILE-target shape, discriminated once at the routing seam
|
||||
/// (#342 items 3-4, extending #319's review Minor-3): a document exec
|
||||
/// cannot execute at all is refused HERE, with exec's own vocabulary,
|
||||
/// before either leg's own document-shape validation ever runs — never by
|
||||
/// falling through to a leg whose own refusal prose assumes a shape this
|
||||
/// document never had.
|
||||
enum ExecFileShape {
|
||||
/// A research-document envelope carrying top-level `"kind":"campaign"`
|
||||
/// — the campaign leg.
|
||||
Campaign,
|
||||
/// No top-level `"kind"` at all (a `{"format_version":N,"blueprint":{…}}`
|
||||
/// envelope) — the blueprint leg.
|
||||
Blueprint,
|
||||
/// A top-level JSON array: a construction op-script (#157) — not an
|
||||
/// envelope of either executable class, however valid its JSON is.
|
||||
OpScript,
|
||||
/// A top-level `"kind"` naming something other than `"campaign"` (e.g.
|
||||
/// a process document) — a real, kind-bearing document, just not one
|
||||
/// of exec's two executable classes. Carries the found kind value.
|
||||
WrongKind(String),
|
||||
}
|
||||
|
||||
/// `exec`'s FILE-target leg discriminator, layered on top of
|
||||
/// `is_blueprint_file`: a research-document envelope (process/campaign)
|
||||
/// carries a required top-level `"kind"` field; a blueprint envelope
|
||||
/// (`{"format_version":N,"blueprint":{…}}`) carries none. `is_blueprint_file`
|
||||
/// alone only tells "an existing `.json` file" from a bare harness-kind/id
|
||||
/// token — it cannot tell a campaign document ending in `.json` from a
|
||||
/// blueprint file, both being ordinary readable `.json` files — so `exec`'s
|
||||
/// routing peeks this one cheap key before choosing a leg.
|
||||
/// (`{"format_version":N,"blueprint":{…}}`) carries none; a construction
|
||||
/// op-script (#157) is a top-level JSON array, not an object at all.
|
||||
/// `is_blueprint_file` alone only tells "an existing `.json` file" from a
|
||||
/// bare harness-kind/id token — it cannot tell these shapes apart, all
|
||||
/// being ordinary readable `.json` files — so `exec`'s routing peeks the
|
||||
/// top-level shape and the `"kind"` key before choosing a leg.
|
||||
///
|
||||
/// `Ok(true)`/`Ok(false)` classifies a file that parses as JSON at all
|
||||
/// (campaign vs. not); `Err` means the file does NOT parse as JSON at all —
|
||||
/// review Minor-3: a file this malformed has no leg to misattribute the
|
||||
/// fault to (neither leg's own "document is not valid JSON" prose is the
|
||||
/// right cause when the shape was never determined), so the caller refuses
|
||||
/// neutrally instead of silently falling through to the blueprint leg.
|
||||
fn classify_exec_document_file(path: &str) -> Result<bool, String> {
|
||||
/// `Ok(_)` classifies a file that parses as JSON at all; `Err` means the
|
||||
/// file does NOT parse as JSON at all — review Minor-3: a file this
|
||||
/// malformed has no leg to misattribute the fault to (neither leg's own
|
||||
/// "document is not valid JSON" prose is the right cause when the shape was
|
||||
/// never determined), so the caller refuses neutrally instead of silently
|
||||
/// falling through to the blueprint leg.
|
||||
fn classify_exec_document_file(path: &str) -> Result<ExecFileShape, String> {
|
||||
let text =
|
||||
std::fs::read_to_string(path).map_err(|e| format!("target file is not readable: {e}"))?;
|
||||
let v: serde_json::Value = serde_json::from_str(&text)
|
||||
.map_err(|e| format!("target file is not valid JSON: {e}"))?;
|
||||
Ok(v.get("kind").is_some())
|
||||
if v.is_array() {
|
||||
return Ok(ExecFileShape::OpScript);
|
||||
}
|
||||
match v.get("kind") {
|
||||
None => Ok(ExecFileShape::Blueprint),
|
||||
Some(k) => {
|
||||
let found = k.as_str().map(str::to_string).unwrap_or_else(|| k.to_string());
|
||||
if found == "campaign" {
|
||||
Ok(ExecFileShape::Campaign)
|
||||
} else {
|
||||
Ok(ExecFileShape::WrongKind(found))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Terminate per a campaign-path result (#272): a String error is a refusal
|
||||
@@ -1032,26 +1072,60 @@ fn tap_plan_from_args(args: &[String]) -> Result<TapPlan, String> {
|
||||
|
||||
/// The one executor verb (#319): a campaign document (file or content id)
|
||||
/// or a signal blueprint (single run). Flag discipline is per-leg. A
|
||||
/// `.json` file target is CLASSIFIED once, up front (review Minor-3): a file
|
||||
/// that does not parse as JSON at all refuses neutrally here, before either
|
||||
/// leg's own document-shape validation ever runs — so a malformed campaign
|
||||
/// document is never misattributed to the blueprint leg's "blueprint
|
||||
/// document is not valid JSON" prose.
|
||||
/// `.json` file target is CLASSIFIED once, up front (review Minor-3, #342
|
||||
/// items 3-4): a file that does not parse as JSON at all refuses neutrally
|
||||
/// here, before either leg's own document-shape validation ever runs — so a
|
||||
/// malformed campaign document is never misattributed to the blueprint
|
||||
/// leg's "blueprint document is not valid JSON" prose; an op-script or a
|
||||
/// kind-bearing non-campaign document is likewise refused right here, with
|
||||
/// exec's own vocabulary, rather than falling through to a leg whose own
|
||||
/// validation would misattribute or under-explain the cause.
|
||||
fn dispatch_exec(a: ExecCmd, env: &aura_runner::project::Env) {
|
||||
let target = Some(a.target.clone());
|
||||
let file_path = is_blueprint_file(&target);
|
||||
let is_campaign_file = match file_path.map(classify_exec_document_file) {
|
||||
Some(Ok(is_campaign)) => is_campaign,
|
||||
let shape = match file_path.map(classify_exec_document_file) {
|
||||
Some(Ok(shape)) => Some(shape),
|
||||
Some(Err(msg)) => {
|
||||
eprintln!("aura: exec: {}: {msg}", file_path.expect("Some by the match arm above"));
|
||||
std::process::exit(2);
|
||||
}
|
||||
None => false,
|
||||
None => None,
|
||||
};
|
||||
let blueprint_path = file_path.filter(|_| !is_campaign_file);
|
||||
match blueprint_path {
|
||||
Some(path) => exec_blueprint_leg(path, &a.r#override, &a.tap, env),
|
||||
None => {
|
||||
match (file_path, shape) {
|
||||
// #342 item 3: a valid-JSON op-script array is not a document exec
|
||||
// can execute at all — point at the build step that turns it into
|
||||
// one, instead of letting it fall through to the blueprint leg's
|
||||
// "not valid JSON" refusal (false: it IS valid JSON, just the wrong
|
||||
// shape). Same exit-code class as that refusal (usage-class, 2).
|
||||
(Some(path), Some(ExecFileShape::OpScript)) => {
|
||||
eprintln!(
|
||||
"aura: exec: {path}: this document is an op-script (a JSON array of \
|
||||
construction ops), not something exec can run directly — build it into a blueprint first \
|
||||
with `aura graph build < {path}`, then exec the result"
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
// #342 item 4: a kind-bearing document that is not a campaign (e.g.
|
||||
// a process document) names the found kind and exec's two
|
||||
// executable classes, instead of the campaign leg's generic
|
||||
// "kind" key must be "campaign" (which never says WHAT was found or
|
||||
// what exec accepts at all). Same exit-code class as today's
|
||||
// fall-through refusal (runtime-class, 1 — mirrors
|
||||
// `exit_on_campaign_result`'s `Err` arm).
|
||||
(Some(path), Some(ExecFileShape::WrongKind(found))) => {
|
||||
eprintln!(
|
||||
"aura: exec: {path}: this document's kind is \"{found}\", but exec executes \
|
||||
only a campaign document or a fully-bound blueprint"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
(Some(path), Some(ExecFileShape::Blueprint)) => {
|
||||
exec_blueprint_leg(path, &a.r#override, &a.tap, env)
|
||||
}
|
||||
// A campaign-kind file, or a non-file target (a bare 64-hex content
|
||||
// id): both run the campaign leg, which re-resolves the target
|
||||
// itself (file-vs-id, then its own full parse+validate).
|
||||
_ => {
|
||||
if !a.tap.is_empty() {
|
||||
eprintln!(
|
||||
"aura: exec: --tap applies only to a blueprint target; a campaign \
|
||||
|
||||
Reference in New Issue
Block a user