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
This commit is contained in:
2026-07-04 04:27:52 +02:00
parent 179c2f8bf0
commit 01925e0619
2 changed files with 47 additions and 2 deletions
+2 -2
View File
@@ -39,7 +39,7 @@ pub struct DocIntrospectCmd {
/// Describe one block's typed slots
#[arg(long, value_name = "ID")]
pub block: Option<String>,
/// 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<PathBuf>,
/// 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}"),
}
+45
View File
@@ -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::<Vec<_>>().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/`