feat(research,campaign): optional selection group on the sweep stage (#210 T1-T2)

The std::sweep selection triple (metric/select/deflate) becomes ONE
optional group, Option<SweepSelection>, serde-flattened so the wire
form keeps the flat slots: every existing document parses to Some and
its canonical bytes — and content id — are unchanged (golden pin
byte-untouched). A half-populated group is refused by the schema-strict
parser (all-or-nothing); a bare {"block":"std::sweep"} is the
selection-free sweep.

Campaign side: preflight permits a selection-free sweep only as the
pipeline's terminal stage (new ExecFault::SelectionFreeSweepNotTerminal,
prose in aura-cli's exhaustive exec_fault_prose); the executor's
selection-free arm appends the family exactly as before but records no
StageSelection and produces no nominee (recording one would fabricate
intent — #210 fork decision 1).

Introspection follows: the sweep block's schema table marks the group
optional; CLI introspection pins updated. New tests: group-or-nothing
refusals, selection-free round-trip + distinct content id, terminal-only
preflight accept/refuse, selection-free execute realization.

refs #210
This commit is contained in:
2026-07-04 17:43:53 +02:00
parent 3fd1cf9593
commit c37abb2a27
6 changed files with 407 additions and 70 deletions
+4
View File
@@ -67,6 +67,10 @@ pub(crate) fn exec_fault_prose(f: &ExecFault) -> String {
ExecFault::DeflatePlateauConflict { stage } => format!(
"process stage {stage}: sweep deflate: true composes only with select \"argmax\""
),
ExecFault::SelectionFreeSweepNotTerminal { stage } => format!(
"process stage {stage}: a sweep without a selection group (metric + \
select) must be the last stage of its process"
),
ExecFault::GeneralizeNeedsInstruments { available } => format!(
"campaign has {available} instrument(s); std::generalize needs at least 2"
),