feat(cli): one bare content-id display form; CLI targets tolerate the content: prefix (0106 F3 / 0107 F6)

The copyable token is now the valid ref form everywhere: all three
register lines print 'registered <family> <id> (<path>)' and both doc
introspect --content-id modes print the bare 64-hex id (converging on
graph introspect's existing form). CLI FILE-or-id targets (campaign
run, graph introspect --params) strip a leading 'content:' before
resolving — a CLI arg is ephemeral input, leniency costs nothing. Doc
ref FIELDS stay bare-only (two accepted spellings would make
semantically identical docs hash apart — canonical-form byte
stability outranks input leniency); instead the referential not-found
prose appends 'refs use the bare 64-hex id — drop the content: prefix'
when the offending ref carries it.

RED-first (tdd-author handoff): eight same-seam assertions across the
three surfaces observed failing on the prefixed forms, then the five
display sites, two target ladders, and the conditional hint landed.

Gates: workspace 993/0, clippy -D warnings clean.

closes #194
This commit is contained in:
2026-07-03 23:01:20 +02:00
parent c3d62f2ce3
commit 63d2657850
5 changed files with 151 additions and 27 deletions
+4 -1
View File
@@ -264,7 +264,10 @@ pub(crate) fn run_campaign(target: &str, env: &Env) -> Result<(), String> {
// Target resolution: a readable file is register-then-run sugar; a bare
// 64-hex token addresses the store directly; anything else refuses naming
// both readings.
// both readings. A CLI arg is ephemeral input, so a leading `content:`
// display prefix is tolerated here (#194) — doc ref FIELDS stay bare-only
// (canonical-form byte stability).
let target = target.strip_prefix("content:").filter(|t| is_content_id(t)).unwrap_or(target);
let campaign_id = if Path::new(target).is_file() {
let doc = parse_valid_campaign(&PathBuf::from(target))?;
registry
+7 -1
View File
@@ -347,6 +347,12 @@ fn composite_from_any(text: &str, env: &crate::project::Env) -> Result<Composite
/// `campaign run`'s target resolution uses, so the two FILE-or-id surfaces
/// cannot drift apart on what counts as a store address.
fn resolve_blueprint_text(target: &str, env: &crate::project::Env) -> Result<String, String> {
// A CLI arg tolerates the `content:` display prefix (#194); doc ref
// fields stay bare-only.
let target = target
.strip_prefix("content:")
.filter(|t| crate::campaign_run::is_content_id(t))
.unwrap_or(target);
let path = Path::new(target);
if path.is_file() {
return std::fs::read_to_string(path)
@@ -405,7 +411,7 @@ fn register_blueprint(file: &Path, env: &crate::project::Env) -> Result<String,
let registry = env.registry();
registry.put_blueprint(&id, &canonical).map_err(|e| e.to_string())?;
Ok(format!(
"registered blueprint content:{id} ({})",
"registered blueprint {id} ({})",
registry.blueprint_path(&id).display()
))
}
+37 -6
View File
@@ -179,7 +179,7 @@ fn register_process(file: &PathBuf, env: &Env) -> Result<(), String> {
let registry = env.registry();
let id = registry.put_process(&canonical).map_err(|e| e.to_string())?;
println!(
"registered process content:{id} ({})",
"registered process {id} ({})",
registry.process_path(&id).display()
);
Ok(())
@@ -205,7 +205,7 @@ fn introspect_process(cmd: &DocIntrospectCmd) -> Result<(), String> {
if let Some(file) = &cmd.content_id {
let doc = parse_valid_process(file)
.map_err(|m| format!("an invalid document has no canonical form — {m}"))?;
println!("content:{}", aura_research::content_id_of(&process_to_json(&doc)));
println!("{}", aura_research::content_id_of(&process_to_json(&doc)));
return Ok(());
}
unreachable!("guard_one_mode enforces one mode")
@@ -249,10 +249,25 @@ pub enum CampaignSub {
Run { target: String },
}
/// The one authoring trap the referential tier can name outright: a ref that
/// carries the `content:` DISPLAY prefix (doc ref fields are bare-only —
/// canonical-form byte stability, #194).
fn prefix_hint(id: &str) -> &'static str {
if id.starts_with("content:") {
" (refs use the bare 64-hex id — drop the 'content:' prefix)"
} else {
""
}
}
pub(crate) fn ref_fault_prose(f: &RefFault) -> String {
match f {
RefFault::ProcessNotFound(id) => format!("process {id} not found in the project store"),
RefFault::StrategyNotFound(id) => format!("strategy {id} not found in the blueprint store"),
RefFault::ProcessNotFound(id) => {
format!("process {id} not found in the project store{}", prefix_hint(id))
}
RefFault::StrategyNotFound(id) => {
format!("strategy {id} not found in the blueprint store{}", prefix_hint(id))
}
RefFault::IdentityUnmatched(id) => format!("identity id {id} matches no stored blueprint"),
RefFault::StrategyUnloadable { id, error } => {
format!("strategy {id} cannot be loaded: {error}")
@@ -343,7 +358,7 @@ fn register_campaign(file: &PathBuf, env: &Env) -> Result<(), String> {
let registry = env.registry();
let id = registry.put_campaign(&canonical).map_err(|e| e.to_string())?;
println!(
"registered campaign content:{id} ({})",
"registered campaign {id} ({})",
registry.campaign_path(&id).display()
);
Ok(())
@@ -369,7 +384,7 @@ fn introspect_campaign(cmd: &DocIntrospectCmd) -> Result<(), String> {
if let Some(file) = &cmd.content_id {
let doc = parse_valid_campaign(file)
.map_err(|m| format!("an invalid document has no canonical form — {m}"))?;
println!("content:{}", aura_research::content_id_of(&campaign_to_json(&doc)));
println!("{}", aura_research::content_id_of(&campaign_to_json(&doc)));
return Ok(());
}
unreachable!("guard_one_mode enforces one mode")
@@ -412,6 +427,22 @@ mod tests {
}
}
#[test]
/// #194 (the prefix trap): a doc ref that carries the display `content:`
/// prefix (a copy-paste from register/introspect output) is bare-only in
/// canonical documents — the referential refusal names the prefix as the
/// cause instead of failing mute. A bare id keeps the plain refusal, so the
/// hint is conditional, not always-on.
fn ref_fault_prose_hints_when_a_ref_carries_the_content_prefix() {
let hash = "a".repeat(64);
let proc = ref_fault_prose(&RefFault::ProcessNotFound(format!("content:{hash}")));
let strat = ref_fault_prose(&RefFault::StrategyNotFound(format!("content:{hash}")));
assert!(proc.contains("drop the 'content:' prefix"), "process ref hint missing: {proc}");
assert!(strat.contains("drop the 'content:' prefix"), "strategy ref hint missing: {strat}");
let bare = ref_fault_prose(&RefFault::ProcessNotFound(hash));
assert!(!bare.contains("drop the 'content:' prefix"), "bare id must not carry the hint: {bare}");
}
#[test]
fn zero_walk_forward_length_prose_is_path_addressed_and_debug_free() {
let prose = doc_fault_prose(&DocFault::ZeroWalkForwardLength { stage: 2, field: "step_ms" });
+25 -3
View File
@@ -407,16 +407,19 @@ fn fixture(name: &str) -> String {
format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"))
}
/// The id extracted from a `registered blueprint content:{id} ({path})` line.
/// The id extracted from a `registered blueprint {id} ({path})` line.
/// Prefix-tolerant across the #194 convention change (the display form dropped
/// the glued `content:` prefix): the id is bare either way.
fn registered_id(stdout: &str) -> String {
stdout
.lines()
.find(|l| l.starts_with("registered blueprint content:"))
.find(|l| l.starts_with("registered blueprint "))
.expect("register line")
.trim_start_matches("registered blueprint content:")
.trim_start_matches("registered blueprint ")
.split(' ')
.next()
.expect("id")
.trim_start_matches("content:")
.to_string()
}
@@ -428,6 +431,8 @@ fn graph_register_stores_and_prints_content_id() {
let dir = temp_cwd("register");
let (stdout, stderr, code) = run_in(&dir, &["graph", "register", &fixture("sma_signal.json")]);
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
// #194: the register verb prints the bare id — `registered blueprint <id> (<path>)`.
assert!(!stdout.contains("content:"), "register prints the bare id, no content: prefix: {stdout}");
let id = registered_id(&stdout);
assert_eq!(id.len(), 64, "a 64-hex content id: {id:?}");
assert!(id.chars().all(|c| c.is_ascii_hexdigit()), "hex only: {id:?}");
@@ -514,6 +519,23 @@ fn graph_params_by_content_id_matches_params_by_file() {
assert_eq!(by_id_out, "fast.length:I64\nslow.length:I64\n", "the raw open params, in order");
}
/// #194 (the prefix trap): a `--params <ID>` target may carry the display
/// `content:` prefix copied verbatim from register/introspect output — the CLI
/// strips it and resolves the bare store address, printing exactly the same raw
/// axis namespace the bare id (and the file) print.
#[test]
fn graph_params_tolerates_content_prefix_on_target() {
let dir = temp_cwd("params-content-prefix");
let bp = fixture("sma_signal_open.json");
let (reg_out, reg_err, reg_code) = run_in(&dir, &["graph", "register", &bp]);
assert_eq!(reg_code, Some(0), "register: {reg_out} {reg_err}");
let id = registered_id(&reg_out);
let (pfx_out, pfx_err, pfx_code) =
run_in(&dir, &["graph", "introspect", "--params", &format!("content:{id}")]);
assert_eq!(pfx_code, Some(0), "content:-prefixed --params must resolve: stdout {pfx_out} stderr {pfx_err}");
assert_eq!(pfx_out, "fast.length:I64\nslow.length:I64\n", "same raw axis namespace as the bare id");
}
/// Property (#196, negative): `--params <ID>` with a well-shaped 64-hex id
/// that was never registered refuses with a store-miss message, not a panic
/// or a silent empty namespace — the registry lookup branch of
+78 -16
View File
@@ -152,8 +152,12 @@ fn process_introspect_vocabulary_block_and_content_id() {
write_doc(&dir, "p.process.json", PROCESS_DOC);
let (out, code) = run_code_in(&dir, &["process", "introspect", "--content-id", "p.process.json"]);
assert_eq!(code, Some(0), "stdout/stderr: {out}");
let line = out.lines().find(|l| l.starts_with("content:")).expect("id line");
assert_eq!(line.len(), "content:".len() + 64);
// #194: --content-id prints the bare 64-hex id (matching `graph introspect
// --content-id` and the store filename), no glued `content:` display prefix.
let line = out.lines().last().expect("id line").trim();
assert_eq!(line.len(), 64, "bare 64-hex id, no content: prefix: {out}");
assert!(line.chars().all(|c| c.is_ascii_hexdigit()), "bare hex id: {out}");
assert!(!out.contains("content:"), "--content-id must not glue the content: prefix: {out}");
}
#[test]
@@ -187,9 +191,11 @@ fn process_register_stores_content_addressed_under_runs_root() {
write_doc(&dir, "p.process.json", PROCESS_DOC);
let (out, code) = run_code_in(&dir, &["process", "register", "p.process.json"]);
assert_eq!(code, Some(0), "stdout/stderr: {out}");
let line = out.lines().find(|l| l.starts_with("registered process content:")).expect("line");
// #194: the register verb prints the bare id — `registered process <id> (<path>)`.
let line = out.lines().find(|l| l.starts_with("registered process ")).expect("line");
assert!(!line.contains("content:"), "register line prints the bare id, no content: prefix: {line}");
let id = line
.trim_start_matches("registered process content:")
.trim_start_matches("registered process ")
.split(' ')
.next()
.expect("id");
@@ -308,12 +314,13 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() {
assert_eq!(proc_code, Some(0), "seed process register failed: {proc_out}");
let proc_id = proc_out
.lines()
.find(|l| l.starts_with("registered process content:"))
.find(|l| l.starts_with("registered process "))
.expect("register line")
.trim_start_matches("registered process content:")
.trim_start_matches("registered process ")
.split(' ')
.next()
.expect("id")
.trim_start_matches("content:")
.to_string();
// "fast.length": the axis name is the RAW composite's `param_space` name.
@@ -424,8 +431,11 @@ fn campaign_introspect_content_id_prints_hash() {
write_doc(&dir, "c.campaign.json", CAMPAIGN_DOC);
let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--content-id", "c.campaign.json"]);
assert_eq!(code, Some(0), "stdout/stderr: {out}");
let line = out.lines().find(|l| l.starts_with("content:")).expect("id line");
assert_eq!(line.len(), "content:".len() + 64);
// #194: --content-id prints the bare 64-hex id, no glued `content:` prefix.
let line = out.lines().last().expect("id line").trim();
assert_eq!(line.len(), 64, "bare 64-hex id, no content: prefix: {out}");
assert!(line.chars().all(|c| c.is_ascii_hexdigit()), "bare hex id: {out}");
assert!(!out.contains("content:"), "--content-id must not glue the content: prefix: {out}");
}
#[test]
@@ -434,9 +444,11 @@ fn campaign_register_stores_content_addressed_under_runs_root() {
write_doc(&dir, "c.campaign.json", CAMPAIGN_DOC);
let (out, code) = run_code_in(&dir, &["campaign", "register", "c.campaign.json"]);
assert_eq!(code, Some(0), "stdout/stderr: {out}");
let line = out.lines().find(|l| l.starts_with("registered campaign content:")).expect("line");
// #194: the register verb prints the bare id — `registered campaign <id> (<path>)`.
let line = out.lines().find(|l| l.starts_with("registered campaign ")).expect("line");
assert!(!line.contains("content:"), "register line prints the bare id, no content: prefix: {line}");
let id = line
.trim_start_matches("registered campaign content:")
.trim_start_matches("registered campaign ")
.split(' ')
.next()
.expect("id");
@@ -527,12 +539,13 @@ fn register_process_doc(dir: &Path, file: &str, doc: &str) -> String {
let (out, code) = run_code_in(dir, &["process", "register", file]);
assert_eq!(code, Some(0), "process register failed: {out}");
out.lines()
.find(|l| l.starts_with("registered process content:"))
.find(|l| l.starts_with("registered process "))
.expect("register line")
.trim_start_matches("registered process content:")
.trim_start_matches("registered process ")
.split(' ')
.next()
.expect("id")
.trim_start_matches("content:")
.to_string()
}
@@ -821,12 +834,13 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() {
assert_eq!(reg_code, Some(0), "register failed: {reg_out}");
let id = reg_out
.lines()
.find(|l| l.starts_with("registered campaign content:"))
.find(|l| l.starts_with("registered campaign "))
.expect("register line")
.trim_start_matches("registered campaign content:")
.trim_start_matches("registered campaign ")
.split(' ')
.next()
.expect("id")
.trim_start_matches("content:")
.to_string();
let (id_out, id_code) = run_code_in(dir, &["campaign", "run", &id]);
assert_eq!(id_code, Some(1), "stdout/stderr: {id_out}");
@@ -836,6 +850,53 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() {
assert_eq!(file_line, id_line, "file- and id-addressed runs must refuse identically");
}
/// #194 (the prefix trap): a `campaign run` target may carry the display
/// `content:` prefix copied verbatim from the tool's own register/introspect
/// output — the CLI strips it and resolves the bare store address. Pinned
/// observably: running the SAME registered campaign by bare id and by its
/// `content:`-prefixed form must refuse identically (proof the prefix is
/// stripped, not read as a second, unresolvable address). Both refuse at the
/// data-free v1 preflight (MC process), so no real data is needed.
#[test]
fn campaign_run_tolerates_content_prefix_on_target() {
let _fixture = project_lock();
let dir = built_project();
let runs_dir = dir.join("runs");
std::fs::remove_dir_all(&runs_dir).ok();
let _cleanup = ScratchGuard(vec![
ScratchPath::Dir(runs_dir.clone()),
ScratchPath::File(dir.join("prefix.process.json")),
ScratchPath::File(dir.join("prefix.campaign.json")),
]);
let bp_id = seed_blueprint(dir, "campaign-run-prefix-seed");
let proc_id = register_process_doc(dir, "prefix.process.json", MC_PROCESS_DOC);
write_doc(dir, "prefix.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
let (reg_out, reg_code) = run_code_in(dir, &["campaign", "register", "prefix.campaign.json"]);
assert_eq!(reg_code, Some(0), "register failed: {reg_out}");
let id = reg_out
.lines()
.find(|l| l.starts_with("registered campaign "))
.expect("register line")
.trim_start_matches("registered campaign ")
.split(' ')
.next()
.expect("id")
.trim_start_matches("content:")
.to_string();
let (bare_out, _) = run_code_in(dir, &["campaign", "run", &id]);
let (pfx_out, _) = run_code_in(dir, &["campaign", "run", &format!("content:{id}")]);
let pfx_line = pfx_out.lines().find(|l| l.starts_with("aura: ")).expect("prefixed refusal line");
assert!(
!pfx_line.contains("neither a readable .json file nor a 64-hex content id"),
"a content:-prefixed target must resolve as the bare id, not refuse as an unknown target: {pfx_line}"
);
let bare_line = bare_out.lines().find(|l| l.starts_with("aura: ")).expect("bare refusal line");
assert_eq!(bare_line, pfx_line, "bare-id and content:-prefixed runs must resolve identically");
}
/// Property (#196 on-ramp x #198 executor): a blueprint registered through
/// `aura graph register` — NOT the pre-#196 `aura sweep` side-effect
/// `seed_blueprint` relies on elsewhere in this file — is a fully valid
@@ -863,12 +924,13 @@ fn campaign_run_accepts_a_graph_register_seeded_strategy() {
assert_eq!(reg_code, Some(0), "graph register failed: {reg_out}");
let bp_id = reg_out
.lines()
.find(|l| l.starts_with("registered blueprint content:"))
.find(|l| l.starts_with("registered blueprint "))
.expect("register line")
.trim_start_matches("registered blueprint content:")
.trim_start_matches("registered blueprint ")
.split(' ')
.next()
.expect("id")
.trim_start_matches("content:")
.to_string();
let proc_id = register_process_doc(dir, "onramp.process.json", SWEEP_ONLY_PROCESS_DOC);