Skip to content

feat(campaign): Campaign declares steering intent (arc A: A0 + A1)#424

Open
xmap wants to merge 3 commits into
mainfrom
campaign-steering-intent
Open

feat(campaign): Campaign declares steering intent (arc A: A0 + A1)#424
xmap wants to merge 3 commits into
mainfrom
campaign-steering-intent

Conversation

@xmap

@xmap xmap commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Summary

Arc A, first two slices: promote the Campaign aggregate from a passive run-id
envelope toward the owner of across-Run steering INTENT, so a future across-Run
steerer can derive the next Run from what-good-means + where-to-look declared on
the Campaign. Grounded by two deep-research passes (the within/across two-level
split is sound; selection is optimizer-derived; Campaign owns the INTENT, the
Decision log owns the per-step selection).

  • A0 (d46143ffa3): relocate the optimizer- and BC-agnostic steering INTENT
    value objects (SteeringObjective / SteeringSpace / SteeringAxis /
    SteeringObjectiveKind / SteeringPoint) to cora.shared.steering, re-exported
    from cora.operation.ports.decide_port so every Operation importer stays
    stable. Required because cora.campaign cannot import cora.operation.ports
    (tach). Mirrors the earlier DecisionConfidenceSource -> cora.shared move.
    Mechanical, behavior-preserving.

  • A1 (d9705e9e0d): Campaign declares its steering intent. Two nullable state
    fields (steering_objective / steering_space, default None, additive no-
    migration), a new CampaignSteeringDeclared event with symmetric serde, and a
    declare_campaign_steering slice (REST POST /campaigns/{id}/declare-steering
    -> 204; MCP declare_campaign_steering). PUT semantics; guards status in
    {Planned, Active}, non-empty space, and a Satisfy objective carrying its target.

Two load-bearing design rules keep the future GenerationStrategy extraction clean:
(1) Slim Aggregate -- only declarative INTENT on Campaign, NEVER per-step
selection/optimizer state (that lives in the Decision log); (2) fold-symmetry
-- the evolver threads both new fields through all seven non-genesis arms so a
later lifecycle/membership transition never resets declared steering (guarded by a
fold-preservation test).

Deferred (next): A2 across-Run selection seam

A2 (build SteeringEvidence from a Campaign's completed-Run results + advise +
record a Decision) is deferred: its READ PATH is undesigned and would invent two
conventions on a guess -- the point a Run ran at lives in free-form
effective_parameters (not axis-keyed), and the objective scalar lives in the
Run's observation logbook (read by channel_name). Per the layer's "design WITH the
real consumer, not speculatively" stance, A2's read path will be designed WITH
gpCAM (or a concrete steered-Run-records-its-point convention).

Test plan

  • A1 event round-trip + legacy-fold; evolver fold-preservation across
    Start/Hold/Resume/Close/Abandon/RunAdded/RunRemoved; decider guards (status,
    empty space, Satisfy-without-target); decider PBT; REST + MCP contract tests
  • A0: decide-layer + steering tests unchanged; tach clean
  • openapi snapshot regenerated for the new route
  • Full fast tier + new contract tests: 39042 passed, 0 failed
  • ruff + pyright + tach clean; gate-reviewed (naming-r3 OK; adversarial SAFE)

🤖 Generated with Claude Code

xmap and others added 3 commits June 29, 2026 11:39
The Campaign aggregate (arc A) needs to declare a campaign's steering INTENT
(steering_objective / steering_space) so an across-Run steerer can derive the next
Run, but tach forbids cora.campaign from importing cora.operation.ports where
those VOs live (cora.campaign depends_on = {infrastructure, shared,
campaign.aggregates, run.aggregates}).

Move the optimizer- and BC-agnostic INTENT value objects (SteeringObjective,
SteeringSpace, SteeringAxis, SteeringObjectiveKind, SteeringPoint) to a new
cora.shared.steering home (an allowed campaign dependency), re-exported from
cora.operation.ports.decide_port so every existing Operation importer stays
stable. Mirrors the DecisionConfidenceSource -> cora.shared.decision_signals
relocation done for the same tach reason. The ADVICE side of the seam
(SteeringAdvice / Verdict / Evidence / Observation / Budget, the Decide*Error
families, the DecidePort Protocol, objective_is_satisfied) stays in decide_port:
it depends on the Operation BC's own Measurement / ArtifactRef / ActuationKind and
only Operation consumes it.

Mechanical, behavior-preserving: the VOs are frozen dataclasses with no Operation
dependency. tach clean; full fast tier 38928 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Arc A slice A1: promote the Campaign aggregate from a passive run-id envelope
toward the owner of across-Run steering INTENT. An operator (or a future steerer)
declares what good means (a SteeringObjective) and where to look (a SteeringSpace)
on the Campaign; a later across-Run selection seam (A2) reads that intent to
derive the next Run.

Additive, no migration:
- Two nullable state fields on Campaign: steering_objective / steering_space
  (default None), imported from cora.shared.steering (A0 relocation; tach forbids
  campaign -> operation.ports). Legacy CampaignRegistered streams fold to None.
- New CampaignSteeringDeclared event (objective + space) with symmetric serde
  (enum .value <-> kind, choices tuple <-> list, nullable axis bounds via .get();
  bad-enum ValueError wrapped via extra=(ValueError,)).
- declare_campaign_steering slice (command/handler/decider/route/tool): PUT
  semantics (re-declare overwrites), guards status in {Planned, Active} else
  CampaignCannotDeclareSteeringError (409), validates non-empty space + a
  Satisfy objective carrying its target else InvalidCampaignSteeringError (400).
  REST POST /campaigns/{id}/declare-steering (204); MCP declare_campaign_steering.

Two load-bearing design rules honored (keep the future GenerationStrategy
extraction clean): (1) Slim Aggregate -- only declarative INTENT lands on
Campaign, NEVER per-step selection/optimizer state (that lives in the Decision
log); (2) fold-symmetry -- the evolver threads both new fields through ALL seven
non-genesis arms so a later Start/Hold/Resume/Close/Abandon/RunAdded/RunRemoved
never resets declared steering (guarded by a fold-preservation test).

Gate-reviewed: naming-r3 OK; adversarial SAFE (fold-symmetry + legacy-fold both
correct + tested); a phase tag the build introduced was reworded. Full fast tier
+ both new contract tests: 39042 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  apps/api/src/cora/campaign
  routes.py
  tools.py
  apps/api/src/cora/campaign/aggregates/campaign
  events.py
  evolver.py
  state.py
  apps/api/src/cora/campaign/features/declare_campaign_steering
  __init__.py
  command.py
  decider.py
  handler.py
  route.py
  tool.py
  apps/api/src/cora/operation/ports
  decide_port.py
  apps/api/src/cora/shared
  steering.py
Project Total  

This report was generated by python-coverage-comment-action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant