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:
@@ -147,8 +147,14 @@ fn bind_axes(
|
||||
match slot {
|
||||
Some(v) => point.push(v.cell()),
|
||||
None => {
|
||||
// Speak the RAW campaign-axis namespace (the doc's own): the
|
||||
// wrapped space prefixes the signal's params with one node
|
||||
// segment, so the raw path is everything after it (#203).
|
||||
let raw =
|
||||
spec.name.split_once('.').map(|(_, rest)| rest).unwrap_or(&spec.name);
|
||||
return Err(MemberFault::Bind(format!(
|
||||
"open param \"{}\" of strategy {strategy_id} is bound by no campaign axis",
|
||||
"open param \"{raw}\" of strategy {strategy_id} is bound by no \
|
||||
campaign axis (wrapped path: {})",
|
||||
spec.name
|
||||
)));
|
||||
}
|
||||
@@ -491,6 +497,11 @@ mod tests {
|
||||
let params = vec![("length".to_string(), Scalar::i64(14))];
|
||||
let err = bind_axes(&space, "strat", ¶ms).unwrap_err();
|
||||
let MemberFault::Bind(m) = err else { panic!("expected Bind, got {err:?}") };
|
||||
assert!(m.contains("threshold") && m.contains("bound by no campaign axis"));
|
||||
// The refusal speaks the RAW campaign-axis namespace (what the doc
|
||||
// must say), with the wrapped bind-time path as a parenthetical (#203).
|
||||
assert!(
|
||||
m.contains("open param \"threshold\"") && m.contains("(wrapped path: sma.threshold)"),
|
||||
"raw-namespace prose expected: {m}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +263,9 @@ pub(crate) fn ref_fault_prose(f: &RefFault) -> String {
|
||||
RefFault::AxisKindMismatch { strategy, axis } => {
|
||||
format!("strategy {strategy}: axis \"{axis}\" declares a kind that is not the param's kind")
|
||||
}
|
||||
RefFault::ParamNotCovered { strategy, param } => {
|
||||
format!("strategy {strategy}: open param \"{param}\" is bound by no campaign axis")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user