refactor(cli): factor the campaign run outcome out of the presenter (#210 T1)

`run_campaign_returning` runs the resolve->execute body and returns a
`CampaignRun` bundle (outcome + campaign/strategies/server); `present_campaign`
consumes it for the stdout tail; `run_campaign_by_id` becomes their composition.
Behaviour-preserving — the existing campaign + sweep-sugar e2e stay green
(research_docs 54/0 incl. the real-data e2e, sweep dissolution e2e green).
Exposes the outcome so the forthcoming generalize sugar can read it without the
presentation tail.

refs #210
This commit is contained in:
2026-07-06 17:04:56 +02:00
parent fd9a7322a0
commit 9ddea840dd
+44 -1
View File
@@ -345,6 +345,19 @@ pub(crate) fn run_campaign(target: &str, env: &Env) -> Result<(), String> {
run_campaign_by_id(&campaign_id, env, RunPresentation::Full)
}
/// An executed campaign plus the context its presenter and the dissolved-verb
/// sugar consume: `outcome` (records + per-cell realizations) and the
/// `campaign` / `strategies` / `server` the emit + trace-persistence tail need.
/// Returned by [`run_campaign_returning`] so a caller can read the outcome
/// without the stdout tail (a forthcoming dissolved-verb sugar path — the
/// generalize translator's own runner lands in a later task).
pub(crate) struct CampaignRun {
pub outcome: aura_campaign::CampaignOutcome,
pub campaign: CampaignDoc,
pub strategies: Vec<(String, String)>,
pub server: Arc<data_server::DataServer>,
}
/// The one campaign executor path from a resolved content id onward: fetch
/// the stored canonical bytes by id (so file addressing and id addressing
/// produce the same realization by construction) and re-run the intrinsic
@@ -359,6 +372,22 @@ pub(crate) fn run_campaign_by_id(
env: &Env,
presentation: RunPresentation,
) -> Result<(), String> {
let run = run_campaign_returning(campaign_id, env)?;
present_campaign(run, presentation, env)
}
/// The one campaign executor path from a resolved content id up to (not
/// including) presentation: fetch the stored canonical bytes by id (so file
/// addressing and id addressing produce the same realization by construction),
/// re-run the intrinsic tier, resolve strategies, execute — returning the
/// outcome bundled with the context the presenter needs. Shared by
/// `run_campaign_by_id` (which then presents) and a forthcoming dissolved-verb
/// sugar path that reads the outcome and self-prints (lands in a later task,
/// alongside the generalize translator's own runner).
pub(crate) fn run_campaign_returning(
campaign_id: &str,
env: &Env,
) -> Result<CampaignRun, String> {
let registry = env.registry();
let campaign_text = registry
.get_campaign(campaign_id)
@@ -438,6 +467,20 @@ pub(crate) fn run_campaign_by_id(
)
.map_err(|f| exec_fault_prose(&f))?;
Ok(CampaignRun { outcome, campaign, strategies, server: runner.server })
}
/// Emit an executed campaign's results per `presentation`: the zero-survivor
/// stderr notes, the emit-gated family/selection lines, the always-on record
/// line (Full only), then trace persistence. Split out of `run_campaign_by_id`
/// so the dissolved-verb sugar can consume the outcome without this stdout tail.
fn present_campaign(
run: CampaignRun,
presentation: RunPresentation,
env: &Env,
) -> Result<(), String> {
let CampaignRun { outcome, campaign, strategies, server } = run;
// Zero-survivor stderr notes (exit stays 0 — a null result is a valid
// research result, #198 decision 8). Addressed by the fields the record
// already carries (strategy/instrument/window_ms), not by re-deriving a
@@ -516,7 +559,7 @@ pub(crate) fn run_campaign_by_id(
&outcome,
&campaign,
&strategies,
&runner.server,
&server,
env,
)?;
}