feat(registry,cli): referential tier requires full open-param coverage; bind refusal speaks the raw namespace
validate == runnable (fieldtest 0107 F7): validate_campaign_refs now
checks the reverse direction — every open param of the resolved
strategy must be bound by some campaign axis — emitting the new
RefFault::ParamNotCovered { strategy, param } with the RAW
param_space() path; ref_fault_prose phrases it ('open param "X" is
bound by no campaign axis'). The executor's every-open-knob-required
rule is thereby mirrored at validate time, and campaign run's
referential gate catches an uncovered knob before any member binds.
The e2e fixture that legitimately under-covered (fast only) gains the
slow.length axis.
Second half, RED-first: the member-bind refusal no longer leaks the
wrapped bind-time path — it names the raw campaign-axis namespace with
the wrapped path as a parenthetical ('open param "threshold" ...
(wrapped path: sma.threshold)').
RED evidence: E0599 on the pinned ParamNotCovered test (tdd-author
handoff), then the flipped bind_axes prose pin observed failing on the
wrapped form.
Gates: workspace 988/0, clippy -D warnings clean.
closes #203
This commit is contained in:
@@ -213,6 +213,11 @@ pub enum RefFault {
|
||||
StrategyUnloadable { id: String, error: String },
|
||||
AxisNotInParamSpace { strategy: String, axis: String },
|
||||
AxisKindMismatch { strategy: String, axis: String },
|
||||
/// An open param of the resolved strategy is bound by no campaign axis —
|
||||
/// the executor's every-open-knob-required rule, mirrored at validate
|
||||
/// time so "valid" means "runnable". Carries the RAW `param_space()`
|
||||
/// path (never the wrapped bind-time path).
|
||||
ParamNotCovered { strategy: String, param: String },
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
@@ -278,6 +283,16 @@ impl Registry {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The reverse direction: every open param must be bound by some
|
||||
// axis (the executor refuses at member-bind otherwise — #203).
|
||||
for spec in &space {
|
||||
if !entry.axes.contains_key(&spec.name) {
|
||||
faults.push(RefFault::ParamNotCovered {
|
||||
strategy: label.clone(),
|
||||
param: spec.name.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(faults)
|
||||
}
|
||||
@@ -1595,6 +1610,82 @@ mod tests {
|
||||
assert_eq!(reg.validate_campaign_refs(&by_identity, &resolve).expect("io ok"), Vec::new());
|
||||
}
|
||||
|
||||
/// Property: the referential tier requires FULL open-param coverage — every
|
||||
/// open param of a referenced strategy must be bound by a campaign axis, so
|
||||
/// a document `validate` blesses is one `run` can actually bind (the
|
||||
/// glossary's "every open knob required, no default" rule, mirrored at
|
||||
/// validate time — C11). A campaign whose axes omit an open param must yield
|
||||
/// a `ParamNotCovered` fault carrying the RAW param path (the `param_space()`
|
||||
/// namespace the axes and `--params` share), while a fully-covering campaign
|
||||
/// stays fault-free (coverage must not over-reject a covered knob).
|
||||
#[test]
|
||||
fn referential_tier_requires_full_open_param_coverage() {
|
||||
use aura_engine::{blueprint_to_json, Composite};
|
||||
|
||||
let reg = Registry::open(temp_family_dir("coverage_tier"));
|
||||
let resolve = |t: &str| aura_std::std_vocabulary(t);
|
||||
|
||||
// A fixture composite with TWO open params, from two zero-arg `Bias`
|
||||
// nodes so `std_vocabulary` can load it on round-trip (the sibling test
|
||||
// documents why LinComb composites cannot). Their raw param paths are
|
||||
// `b1.scale` and `b2.scale`.
|
||||
let composite = Composite::new(
|
||||
"fixture",
|
||||
vec![
|
||||
aura_std::Bias::builder().named("b1").into(),
|
||||
aura_std::Bias::builder().named("b2").into(),
|
||||
],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
);
|
||||
let space = composite.param_space();
|
||||
assert_eq!(space.len(), 2, "fixture exposes two open params");
|
||||
let covered = space[0].name.clone(); // e.g. "b1.scale"
|
||||
let uncovered = space[1].name.clone(); // e.g. "b2.scale"
|
||||
|
||||
let blueprint_json = blueprint_to_json(&composite).expect("serializes");
|
||||
let bp_id = aura_research::content_id_of(&blueprint_json);
|
||||
reg.put_blueprint(&bp_id, &blueprint_json).expect("seed blueprint");
|
||||
let process = r#"{"format_version":1,"kind":"process","name":"p","pipeline":[{"block":"std::generalize","metric":"sqn"}]}"#;
|
||||
let proc_id = reg.put_process(process).expect("seed process");
|
||||
|
||||
// A campaign whose axes cover BOTH open params (kind-correct F64 values).
|
||||
let full_text = format!(
|
||||
concat!(
|
||||
r#"{{"format_version":1,"kind":"campaign","name":"c","#,
|
||||
r#""data":{{"instruments":["GER40"],"windows":[{{"from_ms":1,"to_ms":2}}]}},"#,
|
||||
r#""strategies":[{{"ref":{{"content_id":"{bp}"}},"#,
|
||||
r#""axes":{{"{a}":{{"kind":"F64","values":[2.0]}},"{b}":{{"kind":"F64","values":[3.0]}}}}}}],"#,
|
||||
r#""process":{{"ref":{{"content_id":"{proc}"}}}},"#,
|
||||
r#""seed":1,"presentation":{{"persist_taps":[],"emit":["family_table"]}}}}"#
|
||||
),
|
||||
bp = bp_id,
|
||||
a = covered,
|
||||
b = uncovered,
|
||||
proc = proc_id,
|
||||
);
|
||||
let full = aura_research::parse_campaign(&full_text).expect("campaign parses");
|
||||
assert_eq!(
|
||||
reg.validate_campaign_refs(&full, &resolve).expect("io ok"),
|
||||
Vec::new(),
|
||||
"a fully-covering campaign is fault-free (coverage must not over-reject)",
|
||||
);
|
||||
|
||||
// Drop the axis for `uncovered`: now one open param is bound by no axis.
|
||||
let mut partial = full.clone();
|
||||
partial.strategies[0].axes.remove(&uncovered).expect("axis was present");
|
||||
let faults = reg.validate_campaign_refs(&partial, &resolve).expect("io ok");
|
||||
assert!(
|
||||
faults.iter().any(|f| matches!(
|
||||
f,
|
||||
RefFault::ParamNotCovered { param, .. } if param == &uncovered
|
||||
)),
|
||||
"an open param bound by no axis must fault at validate time, naming the \
|
||||
RAW param path (not the wrapped bind-time path); got {faults:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_r_metric_accepts_r_and_refuses_pip() {
|
||||
assert!(check_r_metric("expectancy_r").is_ok());
|
||||
|
||||
Reference in New Issue
Block a user