256ec7320c
`dispatch_generalize` synthesizes the bare `sma_signal` blueprint, resolves the
window (explicit --from/--to, else the first symbol's full window), and calls
the new `run_generalize_sugar`; the inline `run_generalize` is deleted. The
sugar builds the selection-bearing campaign doc, runs it through
`run_campaign_returning`, reprints the byte-identical `{"generalize":…}` +
`{"family_id":…}` lines, and persists the CrossInstrument family.
The exact-grade anchor stays green through the path shift — proving the two stop
mechanisms (former grid axis, now the risk-regime seam -> StopRule::Vol) yield
identical R. The family pin relaxes to "exactly one CrossInstrument family"
since the campaign executor now also persists a per-instrument Sweep family per
cell (the durable audit trail; #210 Q4). Two new e2e pin the window fallback and
the family member attribution order.
Deviations from the plan (necessary, verified): cross_instrument_members returns
Vec<aura_engine::RunReport> (aura_registry::RunReport is not pub); a shared
validate_before_register helper is factored out of run_sweep_sugar (DRY); the
run_generalize doc-comment and the now-unused `generalization` import removed.
refs #210
530 lines
22 KiB
Rust
530 lines
22 KiB
Rust
//! Verb sugar: the dissolved orchestration verbs' argv → generated campaign
|
|
//! documents (docs/specs/sweep-dissolution.md; #210 fork decisions).
|
|
//!
|
|
//! A dissolved verb invocation is translated into a selection-free process
|
|
//! document plus a campaign document expressing exactly the invocation's
|
|
//! intent, both auto-registered content-addressed (every ad-hoc invocation
|
|
//! becomes durable, diffable, reproducible intent), then run through the one
|
|
//! campaign executor in sugar presentation mode (member lines only).
|
|
|
|
use std::collections::BTreeMap;
|
|
|
|
use aura_core::{Scalar, ScalarKind};
|
|
use aura_research::{
|
|
campaign_to_json, content_id_of, process_to_json, Axis, CampaignDoc, DataSection, DocKind,
|
|
DocRef, Presentation, ProcessDoc, ProcessRef, RiskRegime, SelectRule, StageBlock,
|
|
StrategyEntry, SweepSelection, Window, FORMAT_VERSION,
|
|
};
|
|
|
|
/// The two generated documents of one dissolved-verb invocation.
|
|
#[derive(Debug)]
|
|
pub(crate) struct GeneratedSweep {
|
|
pub process: ProcessDoc,
|
|
pub campaign: CampaignDoc,
|
|
}
|
|
|
|
/// Map one verb axis (`--axis name=csv`, already scalar-parsed) to a
|
|
/// document `Axis`: the kind is the first value's kind; a mixed-kind CSV is
|
|
/// refused (the document axis declares its kind ONCE — #189).
|
|
fn axis_from_values(name: &str, values: &[Scalar]) -> Result<Axis, String> {
|
|
let kind = match values.first() {
|
|
Some(Scalar::I64(_)) => ScalarKind::I64,
|
|
Some(Scalar::F64(_)) => ScalarKind::F64,
|
|
Some(Scalar::Bool(_)) => ScalarKind::Bool,
|
|
Some(Scalar::Timestamp(_)) => ScalarKind::Timestamp,
|
|
None => return Err(format!("axis {name}: empty value list")),
|
|
};
|
|
let homogeneous = values.iter().all(|v| {
|
|
matches!(
|
|
(v, kind),
|
|
(Scalar::I64(_), ScalarKind::I64)
|
|
| (Scalar::F64(_), ScalarKind::F64)
|
|
| (Scalar::Bool(_), ScalarKind::Bool)
|
|
| (Scalar::Timestamp(_), ScalarKind::Timestamp)
|
|
)
|
|
});
|
|
if !homogeneous {
|
|
return Err(format!(
|
|
"axis {name}: mixed value kinds in one axis (an axis declares its kind once)"
|
|
));
|
|
}
|
|
Ok(Axis { kind, values: values.to_vec() })
|
|
}
|
|
|
|
/// Translate a real-data blueprint sweep invocation into its two generated
|
|
/// documents. `blueprint_canonical` is the canonical blueprint JSON already
|
|
/// stored by topology hash; its content id is the strategy ref.
|
|
pub(crate) fn translate_sweep(
|
|
axes: &[(String, Vec<Scalar>)],
|
|
name: &str,
|
|
symbol: &str,
|
|
from_ms: i64,
|
|
to_ms: i64,
|
|
blueprint_canonical: &str,
|
|
) -> Result<GeneratedSweep, String> {
|
|
let process = ProcessDoc {
|
|
format_version: FORMAT_VERSION,
|
|
kind: DocKind::Process,
|
|
name: "sweep".to_string(),
|
|
description: None,
|
|
pipeline: vec![StageBlock::Sweep { selection: None }],
|
|
};
|
|
let mut doc_axes: BTreeMap<String, Axis> = BTreeMap::new();
|
|
for (axis_name, values) in axes {
|
|
doc_axes.insert(axis_name.clone(), axis_from_values(axis_name, values)?);
|
|
}
|
|
let campaign = CampaignDoc {
|
|
format_version: FORMAT_VERSION,
|
|
kind: DocKind::Campaign,
|
|
name: name.to_string(),
|
|
description: None,
|
|
data: DataSection {
|
|
instruments: vec![symbol.to_string()],
|
|
windows: vec![Window { from_ms, to_ms }],
|
|
},
|
|
strategies: vec![StrategyEntry {
|
|
r#ref: DocRef::ContentId(content_id_of(blueprint_canonical)),
|
|
axes: doc_axes,
|
|
}],
|
|
process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) },
|
|
// No stop regime is bound for a dissolved single-sweep member; the
|
|
// R-path stop is a wrap_r constant baked outside the signal axes
|
|
// (absent risk == empty risk, per the established parity).
|
|
risk: vec![],
|
|
// No stage of a selection-free single-sweep pipeline consumes the
|
|
// seed; a fixed zero keeps generated bytes deterministic so identical
|
|
// invocations dedupe onto identical content ids.
|
|
seed: 0,
|
|
presentation: Presentation {
|
|
persist_taps: vec![],
|
|
emit: vec!["family_table".to_string()],
|
|
},
|
|
};
|
|
Ok(GeneratedSweep { process, campaign })
|
|
}
|
|
|
|
/// Auto-register both generated documents (content-addressed, idempotent).
|
|
/// Returns `(process_id, campaign_id)`.
|
|
pub(crate) fn register_generated(
|
|
reg: &aura_registry::Registry,
|
|
generated: &GeneratedSweep,
|
|
) -> Result<(String, String), String> {
|
|
let process_id =
|
|
reg.put_process(&process_to_json(&generated.process)).map_err(|e| e.to_string())?;
|
|
let campaign_id =
|
|
reg.put_campaign(&campaign_to_json(&generated.campaign)).map_err(|e| e.to_string())?;
|
|
Ok((process_id, campaign_id))
|
|
}
|
|
|
|
/// The two generated documents of one dissolved `generalize` invocation.
|
|
#[derive(Debug)]
|
|
pub(crate) struct GeneratedGeneralize {
|
|
pub process: ProcessDoc,
|
|
pub campaign: CampaignDoc,
|
|
}
|
|
|
|
/// Translate one `aura generalize` invocation into its two generated documents:
|
|
/// a **selection-bearing** process (`[std::sweep(argmax), std::generalize]` —
|
|
/// generalize needs a nominee, so the sweep stage is selection-bearing, unlike
|
|
/// the selection-free single-sweep translator) and a campaign running the one
|
|
/// fixed candidate (single-value raw axes) across all instruments under a single
|
|
/// risk regime that carries the stop. `blueprint_canonical` is the bare
|
|
/// `sma_signal` blueprint already stored by topology hash; its content id is the
|
|
/// strategy ref.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(crate) fn translate_generalize(
|
|
fast: i64,
|
|
slow: i64,
|
|
stop_length: i64,
|
|
stop_k: f64,
|
|
metric: &str,
|
|
name: &str,
|
|
symbols: &[String],
|
|
from_ms: i64,
|
|
to_ms: i64,
|
|
blueprint_canonical: &str,
|
|
) -> Result<GeneratedGeneralize, String> {
|
|
let process = ProcessDoc {
|
|
format_version: FORMAT_VERSION,
|
|
kind: DocKind::Process,
|
|
name: "generalize".to_string(),
|
|
description: None,
|
|
pipeline: vec![
|
|
StageBlock::Sweep {
|
|
selection: Some(SweepSelection {
|
|
metric: metric.to_string(),
|
|
select: SelectRule::Argmax,
|
|
deflate: false,
|
|
}),
|
|
},
|
|
StageBlock::Generalize { metric: metric.to_string() },
|
|
],
|
|
};
|
|
let mut doc_axes: BTreeMap<String, Axis> = BTreeMap::new();
|
|
doc_axes.insert(
|
|
"fast.length".to_string(),
|
|
axis_from_values("fast.length", &[Scalar::i64(fast)])?,
|
|
);
|
|
doc_axes.insert(
|
|
"slow.length".to_string(),
|
|
axis_from_values("slow.length", &[Scalar::i64(slow)])?,
|
|
);
|
|
let campaign = CampaignDoc {
|
|
format_version: FORMAT_VERSION,
|
|
kind: DocKind::Campaign,
|
|
name: name.to_string(),
|
|
description: None,
|
|
data: DataSection {
|
|
instruments: symbols.to_vec(),
|
|
windows: vec![Window { from_ms, to_ms }],
|
|
},
|
|
strategies: vec![StrategyEntry {
|
|
r#ref: DocRef::ContentId(content_id_of(blueprint_canonical)),
|
|
axes: doc_axes,
|
|
}],
|
|
process: ProcessRef { r#ref: DocRef::ContentId(content_id_of(&process_to_json(&process))) },
|
|
// The stop is a single structural risk regime (the just-shipped axis);
|
|
// the member runner maps it back to StopRule::Vol at run time.
|
|
risk: vec![RiskRegime::Vol { length: stop_length, k: stop_k }],
|
|
seed: 0,
|
|
presentation: Presentation {
|
|
persist_taps: vec![],
|
|
emit: vec!["family_table".to_string()],
|
|
},
|
|
};
|
|
Ok(GeneratedGeneralize { process, campaign })
|
|
}
|
|
|
|
/// Auto-register both generated generalize documents (content-addressed,
|
|
/// idempotent). Returns `(process_id, campaign_id)`.
|
|
pub(crate) fn register_generated_g(
|
|
reg: &aura_registry::Registry,
|
|
generated: &GeneratedGeneralize,
|
|
) -> Result<(String, String), String> {
|
|
let process_id =
|
|
reg.put_process(&process_to_json(&generated.process)).map_err(|e| e.to_string())?;
|
|
let campaign_id =
|
|
reg.put_campaign(&campaign_to_json(&generated.campaign)).map_err(|e| e.to_string())?;
|
|
Ok((process_id, campaign_id))
|
|
}
|
|
|
|
/// Validate a generated `(process, campaign)` pair BEFORE anything is
|
|
/// registered (#210 c0110 fieldtest, "store litter" finding): a referential
|
|
/// refusal must never leave a dead generated document in the content-
|
|
/// addressed store. Defensive doc-shape checks first (generated docs should
|
|
/// always pass this — the translators build them by construction), then the
|
|
/// executable-shape preflight, then the axis-binding check against the
|
|
/// blueprint's own (wrapped) open param space via the pure `bind_axes` seam
|
|
/// (campaign_run.rs) — the referential shape the dispatch-boundary name
|
|
/// preflight (main.rs) does not catch: a subset of axes that leaves an open
|
|
/// param unbound (probe P3). `probe_params` is any one grid value per axis
|
|
/// (checks NAME coverage/uniqueness, never the bound value). Shared by both
|
|
/// dissolved-verb sugars: one seam enforcing the store-litter invariant,
|
|
/// rather than two drift-prone copies.
|
|
fn validate_before_register(
|
|
process: &ProcessDoc,
|
|
campaign: &CampaignDoc,
|
|
blueprint_canonical: &str,
|
|
probe_params: &[(String, Scalar)],
|
|
env: &crate::project::Env,
|
|
) -> Result<(), String> {
|
|
let doc_faults = aura_research::validate_campaign(campaign);
|
|
if !doc_faults.is_empty() {
|
|
return Err(crate::research_docs::fault_block(
|
|
"generated campaign document invalid:",
|
|
doc_faults.iter().map(crate::research_docs::doc_fault_prose).collect(),
|
|
));
|
|
}
|
|
let process_faults = aura_research::validate_process(process);
|
|
if !process_faults.is_empty() {
|
|
return Err(crate::research_docs::fault_block(
|
|
"generated process document invalid:",
|
|
process_faults.iter().map(crate::research_docs::doc_fault_prose).collect(),
|
|
));
|
|
}
|
|
aura_campaign::preflight(process, campaign).map_err(|f| crate::campaign_run::exec_fault_prose(&f))?;
|
|
|
|
let space = crate::blueprint_axis_probe(blueprint_canonical, env).param_space();
|
|
let strategy_id = content_id_of(blueprint_canonical);
|
|
crate::campaign_run::bind_axes(&space, &strategy_id, probe_params)
|
|
.map(|_| ())
|
|
.map_err(|f| crate::campaign_run::exec_fault_prose(&aura_campaign::ExecFault::Member(f)))
|
|
}
|
|
|
|
/// Run one dissolved sweep invocation end-to-end: register the generated
|
|
/// documents, then execute through the one campaign path in sugar mode.
|
|
pub(crate) fn run_sweep_sugar(
|
|
axes: &[(String, Vec<Scalar>)],
|
|
name: &str,
|
|
symbol: &str,
|
|
from_ms: i64,
|
|
to_ms: i64,
|
|
blueprint_canonical: &str,
|
|
env: &crate::project::Env,
|
|
) -> Result<(), String> {
|
|
let generated = translate_sweep(axes, name, symbol, from_ms, to_ms, blueprint_canonical)?;
|
|
|
|
let probe_params: Vec<(String, Scalar)> = axes
|
|
.iter()
|
|
.filter_map(|(n, vals)| vals.first().map(|v| (n.clone(), *v)))
|
|
.collect();
|
|
validate_before_register(&generated.process, &generated.campaign, blueprint_canonical, &probe_params, env)?;
|
|
|
|
let reg = env.registry();
|
|
let (_process_id, campaign_id) = register_generated(®, &generated)?;
|
|
crate::campaign_run::run_campaign_by_id(&campaign_id, env, crate::campaign_run::RunPresentation::MemberLinesOnly)
|
|
}
|
|
|
|
/// Build the `CrossInstrument` family members from an executed generalize
|
|
/// campaign: each cell's nominee `RunReport`, in cell (instrument doc) order —
|
|
/// the same order + shape the inline `run_generalize` built its `members` in,
|
|
/// each already carrying `manifest.instrument`. Debug-asserts one member per
|
|
/// cell: generalize's pipeline has no gate stage, so every cell nominates —
|
|
/// a cell silently missing its nominee here would be a campaign-executor
|
|
/// regression, not a legitimate generalize outcome, and the filter_map below
|
|
/// must not swallow it unnoticed.
|
|
fn cross_instrument_members(
|
|
outcome: &aura_campaign::CampaignOutcome,
|
|
) -> Vec<aura_engine::RunReport> {
|
|
let members: Vec<aura_engine::RunReport> = outcome
|
|
.cells
|
|
.iter()
|
|
.filter_map(|c| c.nominee.as_ref().map(|(_, report)| report.clone()))
|
|
.collect();
|
|
debug_assert_eq!(
|
|
members.len(),
|
|
outcome.cells.len(),
|
|
"generalize has no gate stage — every cell must nominate"
|
|
);
|
|
members
|
|
}
|
|
|
|
/// Run one dissolved `generalize` invocation end-to-end: register the generated
|
|
/// documents, run through the one campaign path, then reprint today's exact
|
|
/// `{"generalize":{…}}` + `{"family_id":…}` lines from the recorded outcome and
|
|
/// persist the `CrossInstrument` family. The stdout is byte-identical to the
|
|
/// inline path (the committed exact-grade anchor is the gate).
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(crate) fn run_generalize_sugar(
|
|
fast: i64,
|
|
slow: i64,
|
|
stop_length: i64,
|
|
stop_k: f64,
|
|
metric: &str,
|
|
name: &str,
|
|
symbols: &[String],
|
|
from_ms: i64,
|
|
to_ms: i64,
|
|
blueprint_canonical: &str,
|
|
env: &crate::project::Env,
|
|
) -> Result<(), String> {
|
|
let generated = translate_generalize(
|
|
fast, slow, stop_length, stop_k, metric, name, symbols, from_ms, to_ms, blueprint_canonical,
|
|
)?;
|
|
|
|
let probe_params: Vec<(String, Scalar)> = vec![
|
|
("fast.length".to_string(), Scalar::i64(fast)),
|
|
("slow.length".to_string(), Scalar::i64(slow)),
|
|
];
|
|
validate_before_register(&generated.process, &generated.campaign, blueprint_canonical, &probe_params, env)?;
|
|
|
|
let reg = env.registry();
|
|
let (_process_id, campaign_id) = register_generated_g(®, &generated)?;
|
|
let run = crate::campaign_run::run_campaign_returning(&campaign_id, env)?;
|
|
|
|
// Reprint the verb's aggregate line from the recorded cross-instrument
|
|
// grade — byte-identical to the inline `generalize_json(&agg)`.
|
|
let cg = run
|
|
.outcome
|
|
.record
|
|
.generalizations
|
|
.iter()
|
|
.find_map(|g| g.generalization.as_ref())
|
|
.ok_or("generalize produced no cross-instrument grade")?;
|
|
println!("{}", crate::generalize_json(cg));
|
|
|
|
let members = cross_instrument_members(&run.outcome);
|
|
let family_id = reg
|
|
.append_family(name, aura_registry::FamilyKind::CrossInstrument, &members)
|
|
.map_err(|e| e.to_string())?;
|
|
println!("{{\"family_id\":\"{family_id}\"}}");
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn axes() -> Vec<(String, Vec<Scalar>)> {
|
|
vec![
|
|
("fast.length".to_string(), vec![Scalar::I64(2), Scalar::I64(4)]),
|
|
("slow.length".to_string(), vec![Scalar::I64(8), Scalar::I64(16)]),
|
|
]
|
|
}
|
|
|
|
#[test]
|
|
fn translate_sweep_is_deterministic_and_names_flow() {
|
|
let a = translate_sweep(&axes(), "probe", "GER40", 100, 200, "{\"bp\":1}").unwrap();
|
|
let b = translate_sweep(&axes(), "probe", "GER40", 100, 200, "{\"bp\":1}").unwrap();
|
|
assert_eq!(
|
|
content_id_of(&campaign_to_json(&a.campaign)),
|
|
content_id_of(&campaign_to_json(&b.campaign)),
|
|
"identical invocations must generate identical campaign ids"
|
|
);
|
|
assert_eq!(a.campaign.name, "probe");
|
|
assert_eq!(a.campaign.seed, 0);
|
|
assert_eq!(a.process.name, "sweep");
|
|
assert_eq!(a.process.pipeline, vec![StageBlock::Sweep { selection: None }]);
|
|
assert_eq!(a.campaign.data.instruments, vec!["GER40".to_string()]);
|
|
assert_eq!(a.campaign.data.windows, vec![Window { from_ms: 100, to_ms: 200 }]);
|
|
assert_eq!(a.campaign.presentation.emit, vec!["family_table".to_string()]);
|
|
assert!(a.campaign.presentation.persist_taps.is_empty());
|
|
// the process ref is the content id of the generated process bytes
|
|
assert_eq!(
|
|
a.campaign.process.r#ref,
|
|
DocRef::ContentId(content_id_of(&process_to_json(&a.process)))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn translate_sweep_refuses_a_mixed_kind_axis() {
|
|
let mixed = vec![("k".to_string(), vec![Scalar::I64(1), Scalar::F64(2.5)])];
|
|
let err = translate_sweep(&mixed, "n", "GER40", 1, 2, "{}").unwrap_err();
|
|
assert!(err.contains("mixed value kinds"), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn generated_campaign_validates_intrinsically_and_preflights() {
|
|
let g = translate_sweep(&axes(), "probe", "GER40", 100, 200, "{\"bp\":1}").unwrap();
|
|
assert!(aura_research::validate_campaign(&g.campaign).is_empty());
|
|
assert!(aura_research::validate_process(&g.process).is_empty());
|
|
assert!(aura_campaign::preflight(&g.process, &g.campaign).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn translate_generalize_is_deterministic_and_carries_the_regime() {
|
|
let a = translate_generalize(
|
|
3, 12, 14, 2.0, "expectancy_r", "generalize",
|
|
&["GER40".to_string(), "USDJPY".to_string()], 100, 200, "{\"bp\":1}",
|
|
)
|
|
.unwrap();
|
|
let b = translate_generalize(
|
|
3, 12, 14, 2.0, "expectancy_r", "generalize",
|
|
&["GER40".to_string(), "USDJPY".to_string()], 100, 200, "{\"bp\":1}",
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
content_id_of(&campaign_to_json(&a.campaign)),
|
|
content_id_of(&campaign_to_json(&b.campaign)),
|
|
"identical invocations must generate identical campaign ids"
|
|
);
|
|
// A selection-bearing pipeline: sweep(argmax) then generalize.
|
|
assert_eq!(
|
|
a.process.pipeline,
|
|
vec![
|
|
StageBlock::Sweep {
|
|
selection: Some(SweepSelection {
|
|
metric: "expectancy_r".to_string(),
|
|
select: SelectRule::Argmax,
|
|
deflate: false,
|
|
})
|
|
},
|
|
StageBlock::Generalize { metric: "expectancy_r".to_string() },
|
|
]
|
|
);
|
|
// The stop rides a single risk regime (the structural axis).
|
|
assert_eq!(a.campaign.risk, vec![RiskRegime::Vol { length: 14, k: 2.0 }]);
|
|
// All instruments under one campaign; the candidate as single-value axes.
|
|
assert_eq!(
|
|
a.campaign.data.instruments,
|
|
vec!["GER40".to_string(), "USDJPY".to_string()]
|
|
);
|
|
assert_eq!(a.campaign.strategies.len(), 1);
|
|
assert!(a.campaign.strategies[0].axes.contains_key("fast.length"));
|
|
assert!(a.campaign.strategies[0].axes.contains_key("slow.length"));
|
|
assert_eq!(a.process.name, "generalize");
|
|
assert_eq!(a.campaign.name, "generalize");
|
|
// Intrinsically valid + preflights (like the sweep sibling).
|
|
assert!(aura_research::validate_campaign(&a.campaign).is_empty());
|
|
assert!(aura_research::validate_process(&a.process).is_empty());
|
|
assert!(aura_campaign::preflight(&a.process, &a.campaign).is_ok());
|
|
}
|
|
|
|
/// Content-addressing correctness for the risk-regime axis (#210 T1-T2):
|
|
/// two invocations that differ ONLY in the protective stop (`stop_length`/
|
|
/// `stop_k`) must generate campaigns with DIFFERENT content ids. The stop
|
|
/// is now carried through `CampaignDoc.risk` rather than as an opaque
|
|
/// runtime constant, so a hashing/serialization bug that dropped or
|
|
/// normalized-away the regime would silently collide two distinct
|
|
/// invocations onto one cached campaign — the wrong grade would be served
|
|
/// for one of them without any error. This pins that a regime change is
|
|
/// visible in the generated bytes, both against a same-length,
|
|
/// different-k pair and a different-length, same-k pair.
|
|
#[test]
|
|
fn translate_generalize_diverging_regimes_do_not_collide_content_ids() {
|
|
let base = translate_generalize(
|
|
3, 12, 14, 2.0, "expectancy_r", "generalize",
|
|
&["GER40".to_string(), "USDJPY".to_string()], 100, 200, "{\"bp\":1}",
|
|
)
|
|
.unwrap();
|
|
let different_k = translate_generalize(
|
|
3, 12, 14, 3.0, "expectancy_r", "generalize",
|
|
&["GER40".to_string(), "USDJPY".to_string()], 100, 200, "{\"bp\":1}",
|
|
)
|
|
.unwrap();
|
|
let different_length = translate_generalize(
|
|
3, 12, 20, 2.0, "expectancy_r", "generalize",
|
|
&["GER40".to_string(), "USDJPY".to_string()], 100, 200, "{\"bp\":1}",
|
|
)
|
|
.unwrap();
|
|
let base_id = content_id_of(&campaign_to_json(&base.campaign));
|
|
let k_id = content_id_of(&campaign_to_json(&different_k.campaign));
|
|
let length_id = content_id_of(&campaign_to_json(&different_length.campaign));
|
|
assert_ne!(base_id, k_id, "a different stop_k must not collide onto the same campaign id");
|
|
assert_ne!(
|
|
base_id, length_id,
|
|
"a different stop_length must not collide onto the same campaign id"
|
|
);
|
|
assert_ne!(k_id, length_id, "the two diverging regimes must not collide with each other");
|
|
}
|
|
|
|
/// The translator's output is not merely intrinsically valid in isolation
|
|
/// (preflight/validate never touch a store) — `register_generated_g` must
|
|
/// actually persist documents the campaign can resolve through a real
|
|
/// `Registry`: the campaign's `process.ref` content id must fetch back the
|
|
/// EXACT process the translator produced, and the returned `campaign_id`
|
|
/// must fetch back the exact campaign. A drift between what gets hashed
|
|
/// into the ref and what gets stored (e.g. a canonicalization mismatch)
|
|
/// would pass every static check here yet leave the campaign permanently
|
|
/// unresolvable at real run time — this is the property that would catch
|
|
/// that silently, before the dispatch rewire ever exercises the path live.
|
|
#[test]
|
|
fn register_generated_g_stores_documents_the_campaign_can_actually_resolve() {
|
|
let dir = std::env::temp_dir().join(format!(
|
|
"aura-verb-sugar-generalize-registry-test-{}",
|
|
std::process::id()
|
|
));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let reg = aura_registry::Registry::open(dir.join("runs.jsonl"));
|
|
|
|
let generated = translate_generalize(
|
|
3, 12, 14, 2.0, "expectancy_r", "generalize",
|
|
&["GER40".to_string(), "USDJPY".to_string()], 100, 200, "{\"bp\":1}",
|
|
)
|
|
.unwrap();
|
|
let (process_id, campaign_id) = register_generated_g(®, &generated).unwrap();
|
|
|
|
let stored_campaign = reg.get_campaign(&campaign_id).unwrap().expect("campaign stored");
|
|
assert_eq!(stored_campaign, campaign_to_json(&generated.campaign));
|
|
|
|
let DocRef::ContentId(ref_id) = &generated.campaign.process.r#ref else {
|
|
panic!("generalize's process ref is a content id");
|
|
};
|
|
assert_eq!(ref_id, &process_id, "the stored process id must be the campaign's own ref");
|
|
let stored_process = reg.get_process(ref_id).unwrap().expect("process stored");
|
|
assert_eq!(stored_process, process_to_json(&generated.process));
|
|
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
}
|