From 01925e061938d74a28d3727617fb6e62c1a9c50e Mon Sep 17 00:00:00 2001 From: Brummel Date: Sat, 4 Jul 2026 04:27:52 +0200 Subject: [PATCH] feat(cli): envelope-probe discoverability + kind-key refusal prose (#193 residue) The --unwired help line names the whole-envelope starting move (start from a bare {}), closing the 0106-F1 discoverability gap the narrowed issue kept open; the wrong-kind refusal names the exact key a blind author must fix ('the "kind" key must be "process"') instead of describing the mismatch abstractly. The third original item (block-ids vs field names in --vocabulary) is sufficiently served by the envelope probe per the issue's narrowing comment. RED-first (tdd-author handoff): both pins observed failing on the old help text / prose. Gates: workspace 1043/0, clippy -D warnings clean. closes #193 --- crates/aura-cli/src/research_docs.rs | 4 +-- crates/aura-cli/tests/research_docs.rs | 45 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/crates/aura-cli/src/research_docs.rs b/crates/aura-cli/src/research_docs.rs index edc5022..e06a757 100644 --- a/crates/aura-cli/src/research_docs.rs +++ b/crates/aura-cli/src/research_docs.rs @@ -39,7 +39,7 @@ pub struct DocIntrospectCmd { /// Describe one block's typed slots #[arg(long, value_name = "ID")] pub block: Option, - /// List the open slots of a (partial) document + /// List the open slots of a (partial) document — start from a bare {} to see the whole envelope #[arg(long, value_name = "FILE")] pub unwired: Option, /// Print the content id of a valid document @@ -112,7 +112,7 @@ pub(crate) fn doc_error_prose(what: &str, e: &DocError) -> String { DocKind::Process => "process", DocKind::Campaign => "campaign", }; - format!("{what}: wrong document kind (expected \"{want}\")") + format!("{what}: the \"kind\" key must be \"{want}\"") } DocError::Malformed(msg) => format!("{what}: {msg}"), } diff --git a/crates/aura-cli/tests/research_docs.rs b/crates/aura-cli/tests/research_docs.rs index 2fe63c2..9dbd553 100644 --- a/crates/aura-cli/tests/research_docs.rs +++ b/crates/aura-cli/tests/research_docs.rs @@ -648,6 +648,51 @@ fn campaign_introspect_block_presentation_names_the_tap_vocabulary() { assert!(out.contains("persist_taps"), "stdout/stderr: {out}"); } +/// #193 (narrowed scope, cycle-0107 F3): the whole document envelope IS +/// derivable — `introspect --unwired` over a bare `{}` enumerates every +/// top-level field (seed, data, strategies, process.ref, presentation) as an +/// open slot — but nothing pointed the author there, so the 0106 round +/// iterated blind `validate` errors instead of starting from the skeleton. +/// The shared `--unwired` help line now carries the skeleton hint naming that +/// starting move, making it discoverable from `--help` alone (the process and +/// campaign families share `DocIntrospectCmd`, so pinning one covers both). +/// Whitespace is normalized before matching so clap's help-column wrapping +/// (terminal-width dependent) can never split the pinned phrase. +#[test] +fn introspect_help_hints_the_bare_envelope_probe() { + let (out, code) = run_code(&["campaign", "introspect", "--help"]); + assert_eq!(code, Some(0), "clap --help exits 0: {out}"); + let flat = out.split_whitespace().collect::>().join(" "); + assert!( + flat.contains("start from a bare {}"), + "the --unwired help line must point at the bare-{{}} envelope probe: {out}" + ); +} + +/// #193 (narrowed scope, cycle-0106 F1): a document whose `kind` key carries +/// the wrong value — the blind-author trap of pointing `process validate` at +/// a campaign-kind file — is refused with prose that NAMES the `kind` key the +/// author must fix ("the \"kind\" key must be \"process\""), instead of the +/// opaque "wrong document kind" that never told them WHICH field to change. +/// Autonomous: a minimal campaign-kind envelope written inline, refused at the +/// envelope check before any deserialization (exit 1), Debug-free. +#[test] +fn wrong_kind_refusal_names_the_kind_key() { + let dir = temp_cwd("wrong-kind-names-key"); + write_doc( + &dir, + "mislabeled.process.json", + r#"{ "format_version": 1, "kind": "campaign", "name": "x", "pipeline": [] }"#, + ); + let (out, code) = run_code_in(&dir, &["process", "validate", "mislabeled.process.json"]); + assert_eq!(code, Some(1), "stdout/stderr: {out}"); + assert!( + out.contains("the \"kind\" key must be \"process\""), + "the refusal must name the kind key the author must fix: {out}" + ); + assert!(!out.contains("WrongKind"), "Debug leak: {out}"); +} + /// Register must be a gate, not a passthrough: an invalid document is /// refused with prose (exit 1) and — the property that matters for a /// content-addressed store — no file is ever written under `runs/campaigns/`