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:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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(®_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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user