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:
2026-07-11 04:35:01 +02:00
parent 1d14ebc1cd
commit 43b1c7ff5d
9 changed files with 323 additions and 13 deletions
+2
View File
@@ -475,6 +475,7 @@ mod tests {
data: DataSection {
instruments: vec!["EURUSD".to_string()],
windows: vec![Window { from_ms: 0, to_ms: 10_000 }],
bindings: BTreeMap::new(),
},
risk: vec![],
strategies: vec![StrategyEntry {
@@ -901,6 +902,7 @@ mod wf_tests {
data: DataSection {
instruments: vec!["SYNTH".to_string()],
windows: vec![Window { from_ms: window.0, to_ms: window.1 }],
bindings: BTreeMap::new(),
},
risk: vec![],
strategies: vec![StrategyEntry {
+1
View File
@@ -149,6 +149,7 @@ fn campaign(instruments: &[&str]) -> CampaignDoc {
data: DataSection {
instruments: instruments.iter().map(|s| s.to_string()).collect(),
windows: vec![Window { from_ms: 1_000, to_ms: 9_000 }],
bindings: BTreeMap::new(),
},
risk: vec![],
strategies: vec![StrategyEntry {