Skip to content

Let a compute step's parameters carry a capture or steering reference - #790

Merged
xmap merged 6 commits into
mainfrom
worktree-compute-step-refs
Sep 9, 2026
Merged

Let a compute step's parameters carry a capture or steering reference#790
xmap merged 6 commits into
mainfrom
worktree-compute-step-refs

Conversation

@xmap

@xmap xmap commented Sep 9, 2026

Copy link
Copy Markdown
Owner

What

A steering brain could already whisper a new value to a motor: a SetpointStep's value accepts a CaptureRef or a SteeringRef, and the per-conduct captures bus resolves it at execute time. It could not whisper one to a reconstruction. A ComputeStep's parameters took literals only, so an autonomous loop could move the stage but never retune the job it fed.

Nothing new was needed to close that. The captures bus already carries both kinds of reference and the seed-the-captures keystone already deposits a brain's advice into it. The value simply had to survive every place a step is written down and read back.

The five places a reference has to survive

site why it matters
recipe/aggregates/recipe/body.py The Recipe's own wire, which runs before the Operation BC. Left unfixed, a ref round-trips out of Postgres as a bare {"__capture__": "x"} dict: silent corruption, no error.
operation/_recipe_expansion/_expand.py The determinism hash. canonical_json_bytes takes no default=, so an unserialized ref object crashes the hash rather than quietly differing.
conductor.py (pinned-step payload) ResolvedStepsRecorded is what a resume replays against, so a ref that cannot round-trip here breaks resume, not execution.
conductor.py (_run_compute) The actual resolution, against the same bus a SetpointStep reads. Loud failure on an unseeded name, matching the setpoint path.
conductor.py (provenance) The recorded payload now carries parameter_refs when any parameter was a reference, so the record says what was asked for as well as what it resolved to.

A latent bug fixed on the way

validate_capture_refs used if/elif over the step kinds, so a RecipeComputeStep that both declared a capture_name and consumed a forward reference had its consume check swallowed by the declare branch. Declaring and consuming are now checked independently. This was reachable before this PR; the widened type just makes it easy to hit.

Two documentation commits

The Operation module docstring enumerated the runtime Step union as three arms in four places, having gone stale twice as arms landed. It now points at STEP_KIND_VALUES and the fitness test that pins them, so it cannot go stale a third time.

The module doc now also states where deciding happens, which was written down nowhere. Deciding sits at two altitudes: a CheckStep evaluates a criterion and gates the walk, which is the deterministic rule brain BrainKind.RULE names, while the brain behind DecidePort stays outside the step list because its verdict can complete the whole Procedure and it reads the full cross-pass history that no step sees.

Verification

  • 40,330 passed, 648 skipped across tests/architecture, tests/unit/operation, tests/unit/recipe
  • 8 new unit tests for the resolution path, cloned from the existing compute-output harness
  • A new arity fitness case round-trips a compute step with parameter refs through both serializers, so a future arm cannot land with one wired and the other not
  • make docs-build strict, clean
  • Rebased onto current main after the deployments-site collapse; docs build re-verified against the new nav

🤖 Generated with Claude Code

xmap and others added 6 commits September 9, 2026 09:39
RecipeComputeStep.parameters copied verbatim through the Recipe
aggregate's own event-payload wire, unlike its sibling RecipeActionStep
which already resolves BindingRef per-value. Left as-is, a CaptureRef or
SteeringRef written into a compute parameter would round-trip back out
of Postgres as a bare {"__capture__": ...} dict instead of the sentinel
object: silent corruption, and upstream of anything Operation BC does.

Also fixes a latent bug in validate_capture_refs: folding a compute
step's own capture_name declaration and its parameters' reference check
into one if/elif let a step that both declares and forward-references
skip its own consume check. Declare and consume now run independently,
consume first, mirroring validate_output_refs's existing ordering.

First slice of letting a compute step (e.g. a tomography reconstruction)
read a measured value or a steering brain's advice into its own
parameters, the way a setpoint already can.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s literal

steps_to_wire's ComputeStep arm copied parameters verbatim, which would
crash canonical_json_bytes the moment one carried a CaptureRef or
SteeringRef (no default= to fall back on). Adds a per-value wire
encoder mirroring the existing input_uris/OutputRef element encoder and
the SetpointStep.value sentinel shape, so a ref-bearing parameter
hashes deterministically instead of crashing register_procedure_from_recipe.

expand() itself needed no functional change: a shallow dict copy
already preserves a CaptureRef/SteeringRef object unchanged, since
resolve_value (which only ever substitutes a BindingRef) was never
called on parameters. Only the stale comment claiming parameters were
purely literal needed correcting.

Second slice of letting a compute step read a measured value or a
steering brain's advice into its own parameters.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d too

step_to_payload/_step_from_payload is a second, independent encode/decode
site from _recipe_expansion._expand's determinism hash: it is what
ResolvedStepsRecorded persists for resume replay. Left unfixed after the
prior two slices, a pinned compute step carrying a CaptureRef/SteeringRef
parameter would crash canonical_json_bytes at conduct time (this path
runs on every conduct, not just recipe-driven ones) even though the
hash serializer already handled it.

Adds the same per-value encode/decode pair here, kept as an independent
copy rather than importing _expand's, matching this file's own existing
precedent for _input_uri_to_wire/_input_uri_from_wire (duplicated
between the two modules already, not shared, despite both living under
the single cora.operation tach boundary).

Extends the architecture fitness test that exists specifically to catch
a new field with no serializer arm, so the feature ships without a hole
in its own safety net.

Third slice; ComputeStep.parameters can now carry a ref through every
encoding path. Runtime resolution against the per-conduct captures dict
is the next slice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…eters

The final slice: _run_compute now resolves every CaptureRef/SteeringRef
value in ComputeStep.parameters against the per-conduct captures dict,
immediately after the existing input_uris/OutputRef resolve and before
building the JobSpec. An unresolved capture or unseeded steering axis
loud-fails with a recorded entry, no in-flight marker, nothing
submitted, parity with the OutputRef case and with _run_setpoint's own
CaptureRef/SteeringRef handling.

Reuses _ERROR_UNRESOLVED_CAPTURE for both ref kinds rather than adding a
new error class, matching _run_setpoint's own precedent (it already
uses this label for its SteeringRef arm, not a separate one).
Provenance rides a new parameter_refs payload key, emitted only when a
parameter actually carries a ref, so a literal-only step's recorded
entries stay byte-identical to before this feature.

Also fixes the pre-existing OutputRef-unresolved failure body, which
copied parameters via a raw dict() even though parameters may still
hold an unresolved ref at that point (that failure returns before the
new parameter resolution runs); it now goes through the same wire
encoder.

This closes the loop the first three slices set up: a tomography
reconstruction (or any compute job) can now read a value an earlier
step measured, or a coordinate a steering brain just advised, the same
way a setpoint already could. Compute stops being read-only in the
conduct loop.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Operation docstring still enumerated the runtime Step union as
Setpoint | Action | Check in four places, two step kinds out of date
since capture and compute landed. Replaced the enumerations with a
pointer to STEP_KIND_VALUES and the fitness test that pins the arm set,
rather than a fresh hand-list that goes stale the next time an arm
lands.

The module doc gains the split the five kinds already have but that no
single page stated: capture and check observe, setpoint and action act,
compute does both, and deciding is not a step kind at all because the
brain runs between passes rather than inside one. That last part is the
load-bearing half. Reading the kinds as though they ought to partition
into observe, think and act compresses two altitudes into one, which is
the reading the prose now heads off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The paragraph shipped a day ago said deciding is deliberately not a step
kind. A CheckStep evaluates a criterion and gates the walk on the result,
and BrainKind names RULE as a real brain, so a deterministic brain already
runs inside the step list. The accurate statement is that deciding happens
at two altitudes: the cheap deterministic kind fits in a step, and the loop
brain does not, because its verdict can complete the whole Procedure and it
reads across every pass rather than one.

The reasons the loop brain sits outside the list were written down nowhere,
so a reader had to reconstruct them from the DecidePort docstring and the
conductor's decide pseudo-kind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  apps/api/src/cora/operation
  conductor.py
  apps/api/src/cora/operation/_recipe_expansion
  _expand.py
  apps/api/src/cora/recipe/aggregates/recipe
  body.py
Project Total  

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

@xmap
xmap merged commit 4cb3d3a into main Sep 9, 2026
19 checks passed
@xmap
xmap deleted the worktree-compute-step-refs branch September 9, 2026 15:03
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