fix(aura-research): process.ref hint states the tagged { content_id } shape

campaign introspect (--block std::process_ref and --unwired) described
the slot as "content id of a process document" — a plausible bare-string
reading the validator rejects; the required {"content_id":"..."} tagging
appeared in no introspection output (binary-only M1 fieldtest finding).

The SlotKind::ContentRef label and the open-slot hint now lead with the
tagged { content_id } shape. Deliberately narrower than the issue's
suggestion of mirroring std::strategy's { content_id } | { identity_id }
form: validate_campaign refuses an identity_id process ref
(DocFault::ProcessRefMustBeContentId) and a pinned test forbids
advertising it, so the hint states the one accepted key only (C29
honesty; correction minuted on the issue). RED-first: the extended
process_ref_describe_advertises_content_id_only assertion failed against
the old label before the fix.

closes #329
This commit is contained in:
2026-07-24 16:02:45 +02:00
parent 7943b123ae
commit 73ad87b08a
2 changed files with 17 additions and 7 deletions
+2 -2
View File
@@ -996,7 +996,7 @@ fn campaign_introspect_unwired_reports_the_spec_example_slots() {
write_doc(&dir, "draft.campaign.json", draft); write_doc(&dir, "draft.campaign.json", draft);
let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "draft.campaign.json"]); let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "draft.campaign.json"]);
assert_eq!(code, Some(0)); assert_eq!(code, Some(0));
assert!(out.contains("open slot: process.ref (required, content id of a process document)")); assert!(out.contains("open slot: process.ref (required, { content_id }: content id of a process document)"));
assert!(out.contains("open slot: strategies[0].axes.slow (axis declared empty — a P1 axis is a non-empty finite set)")); assert!(out.contains("open slot: strategies[0].axes.slow (axis declared empty — a P1 axis is a non-empty finite set)"));
} }
@@ -1022,7 +1022,7 @@ fn campaign_introspect_unwired_reports_placeholder_refs() {
"strategy `ref: null` placeholder not enumerated: {out}" "strategy `ref: null` placeholder not enumerated: {out}"
); );
assert!( assert!(
out.contains("open slot: process.ref (required, content id of a process document)"), out.contains("open slot: process.ref (required, { content_id }: content id of a process document)"),
"process `ref: {{}}` placeholder not enumerated: {out}" "process `ref: {{}}` placeholder not enumerated: {out}"
); );
} }
+15 -5
View File
@@ -1102,7 +1102,7 @@ pub fn slot_kind_label(kind: SlotKind) -> &'static str {
SlotKind::Strings => "list of strings", SlotKind::Strings => "list of strings",
SlotKind::Windows => "list of { from_ms, to_ms }", SlotKind::Windows => "list of { from_ms, to_ms }",
SlotKind::Ref => "one of { content_id } | { identity_id }", SlotKind::Ref => "one of { content_id } | { identity_id }",
SlotKind::ContentRef => "content id of a process document", SlotKind::ContentRef => "{ content_id }: content id of a process document",
SlotKind::Axes => "map: param name -> { kind, values }", SlotKind::Axes => "map: param name -> { kind, values }",
SlotKind::EmitKinds => "list of: family_table | selection_report", SlotKind::EmitKinds => "list of: family_table | selection_report",
SlotKind::TapKinds => "list of: equity | exposure | r_equity | net_r_equity", SlotKind::TapKinds => "list of: equity | exposure | r_equity | net_r_equity",
@@ -1301,8 +1301,10 @@ pub fn open_slots_campaign(text: &str) -> Result<Vec<OpenSlot>, DocError> {
} }
if ref_slot_is_open(v.get("process").and_then(|p| p.get("ref"))) { if ref_slot_is_open(v.get("process").and_then(|p| p.get("ref"))) {
// deliberately narrower than the strategy-ref hint: validate_campaign // deliberately narrower than the strategy-ref hint: validate_campaign
// refuses an identity_id process ref, so the guide must not offer it // refuses an identity_id process ref, so the guide must not offer it.
slots.push(open("process.ref", "required, content id of a process document")); // The tagged { content_id } shape is stated explicitly (#329) so the
// hint cannot be misread as accepting a bare id string.
slots.push(open("process.ref", "required, { content_id }: content id of a process document"));
} }
if v.get("seed").and_then(|s| s.as_u64()).is_none() { if v.get("seed").and_then(|s| s.as_u64()).is_none() {
slots.push(open("seed", "required, non-negative integer")); slots.push(open("seed", "required, non-negative integer"));
@@ -2374,6 +2376,14 @@ mod tests {
label.contains("content"), label.contains("content"),
"process_ref describe must still name the content-id form: {label}" "process_ref describe must still name the content-id form: {label}"
); );
// #329: the label must state the TAGGED shape ({ content_id: ... }) the
// parser actually requires, not read like a bare id string — mirroring
// the `{ content_id } | { identity_id }` bracket notation std::strategy
// already uses, narrowed to the one key process.ref accepts.
assert!(
label.contains("{ content_id }"),
"process_ref describe must state the tagged {{ content_id }} shape, not a bare id: {label}"
);
let strategy = describe_block("std::strategy").expect("strategy describable"); let strategy = describe_block("std::strategy").expect("strategy describable");
let strat_ref = let strat_ref =
@@ -2430,7 +2440,7 @@ mod tests {
"presentation": { "persist_taps": [], "emit": [] } }"#; "presentation": { "persist_taps": [], "emit": [] } }"#;
let slots = open_slots_campaign(draft).unwrap(); let slots = open_slots_campaign(draft).unwrap();
assert!(slots.iter().any(|s| s.path == "process.ref" assert!(slots.iter().any(|s| s.path == "process.ref"
&& s.hint == "required, content id of a process document")); && s.hint == "required, { content_id }: content id of a process document"));
assert!(slots.iter().any(|s| s.path == "strategies[0].axes.slow" assert!(slots.iter().any(|s| s.path == "strategies[0].axes.slow"
&& s.hint == "axis declared empty — a P1 axis is a non-empty finite set")); && s.hint == "axis declared empty — a P1 axis is a non-empty finite set"));
@@ -2466,7 +2476,7 @@ mod tests {
// ...and the `{}` process ref reports the narrower content-id-only hint. // ...and the `{}` process ref reports the narrower content-id-only hint.
assert!( assert!(
slots.iter().any(|s| s.path == "process.ref" slots.iter().any(|s| s.path == "process.ref"
&& s.hint == "required, content id of a process document"), && s.hint == "required, { content_id }: content id of a process document"),
"process `ref: {{}}` placeholder not reported as an open slot: {slots:?}" "process `ref: {{}}` placeholder not reported as an open slot: {slots:?}"
); );
} }