feat(research,registry,cli): campaign data.bindings — the 6b rebind seam (#231 task 5)
DataSection gains an additive bindings block (role -> column; serde default + skip-if-empty, so binding-less documents keep their content ids — pinned). Validation is two-tier along the existing line: the intrinsic tier checks binding VALUES against the column vocabulary (DocFault::UnknownBindingColumn, alias-annotated display register); the resolver tier checks binding KEYS against the loaded strategies' input_roles() (RefFault::BindingRoleUnknown — a key must name a role of at least one campaign strategy). A cross-surface pin keeps the doc-tier vocabulary and the CLI binding module from drifting. CliMemberRunner threads the campaign overrides into resolve_binding on BOTH halves (column opening and wrap), keeping the C1 drift alarm comparing like-with-like; the verb sugar passes no bindings (name defaults rule). Proof: an archive-gated campaign e2e rebinds price -> open and yields different realized metrics from the close-bound run; refusal prose exact-pinned at both tiers. Verified: full workspace suite green, clippy -D warnings clean; independent quality review, all findings repaired. refs #231
This commit is contained in:
@@ -6,10 +6,10 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use aura_research::{
|
||||
campaign_to_json, campaign_vocabulary, describe_block, open_slots_campaign,
|
||||
open_slots_process, parse_campaign, parse_process, process_to_json, process_vocabulary,
|
||||
slot_kind_label, tap_vocabulary, validate_campaign, validate_process, CampaignDoc, DocError,
|
||||
DocFault, DocKind, ProcessDoc, StageBlock,
|
||||
binding_column_vocabulary_display, campaign_to_json, campaign_vocabulary, describe_block,
|
||||
open_slots_campaign, open_slots_process, parse_campaign, parse_process, process_to_json,
|
||||
process_vocabulary, slot_kind_label, tap_vocabulary, validate_campaign, validate_process,
|
||||
CampaignDoc, DocError, DocFault, DocKind, ProcessDoc, StageBlock,
|
||||
};
|
||||
use aura_registry::RefFault;
|
||||
|
||||
@@ -154,6 +154,10 @@ pub(crate) fn doc_fault_prose(f: &DocFault) -> String {
|
||||
"presentation.persist_taps[{index}]: unknown tap \"{tap}\" (taps: {})",
|
||||
tap_vocabulary().join(" | ")
|
||||
),
|
||||
DocFault::UnknownBindingColumn { role, column } => format!(
|
||||
"data.bindings.{role}: \"{column}\" names no archive column (columns: {})",
|
||||
binding_column_vocabulary_display()
|
||||
),
|
||||
DocFault::ProcessRefMustBeContentId => {
|
||||
"process.ref: a process is referenced by content id in this version".into()
|
||||
}
|
||||
@@ -331,6 +335,10 @@ pub(crate) fn ref_fault_prose(f: &RefFault) -> String {
|
||||
RefFault::ParamNotCovered { strategy, param } => {
|
||||
format!("strategy {strategy}: open param \"{param}\" is bound by no campaign axis")
|
||||
}
|
||||
RefFault::BindingRoleUnknown { role, roles } => format!(
|
||||
"data.bindings: key \"{role}\" names no input role of any campaign strategy (roles: {})",
|
||||
roles.join(", ")
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,6 +545,22 @@ fn introspect_campaign(cmd: &DocIntrospectCmd) -> Result<(), String> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Exact prose pin for the binding-key referential refusal (#231): the
|
||||
/// message is path-addressed, names the offending key, and lists the
|
||||
/// strategies' actual roles.
|
||||
#[test]
|
||||
fn binding_role_unknown_prose_names_key_and_roles() {
|
||||
let msg = ref_fault_prose(&RefFault::BindingRoleUnknown {
|
||||
role: "sentiment".into(),
|
||||
roles: vec!["price".into(), "high".into()],
|
||||
});
|
||||
assert_eq!(
|
||||
msg,
|
||||
"data.bindings: key \"sentiment\" names no input role of any campaign \
|
||||
strategy (roles: price, high)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ref_fault_prose_is_debug_free() {
|
||||
let cases = [
|
||||
|
||||
Reference in New Issue
Block a user