28 KiB
ADR-0232: CandidateAttempt Run-Binding Boundary
Status: Proposed
Date: 2026-06-23
Scope: Kernel diagnostics, immutable run-attempt membership, residual-gated practice loop
Depends on:
- ADR-0225 ContractResidual read model
- ADR-0226 Residual-Gated Practice Loop v1
- ADR-0227 ComputeBudgetPolicy envelope
- ADR-0228 GeometricSearchRun envelope
- ADR-0229 Contract/Proof Replay Adapter boundary
- ADR-0230 SealedPracticeTrace boundary
- ADR-0231 First Candidate Operator boundary
- PR #876 missing-role candidate operator shell
1. Summary
This ADR defines the immutable boundary that binds one externally constructed
CandidateAttempt-compatible candidate to one existing GeometricSearchRun
episode.
The current loop can now construct one candidate operator result:
ContractAssessment
-> ContractResidual
-> SearchGateDecision
-> ComputeBudgetDecision
-> GeometricSearchRun
-> CandidateOperatorResult from missing_role_candidate.v1
-> Contract/Proof Replay Adapter
-> SealedPracticeTrace
The missing joint is not another operator and not mutable insertion into
GeometricSearchRun.candidate_attempts. It is an immutable membership record:
original GeometricSearchRun
+ CandidateOperatorResult
-> CandidateAttemptRunBinding
CandidateAttemptRunBinding is membership evidence only. It proves that a
candidate attempt is structurally admissible as a member of the identified run
episode. It does not prove correctness, run replay, seal a trace, produce an
answer, select a candidate, rank a candidate, mutate a run, or promote any
finding.
This ADR is documentation only. It adds no code, tests, generated reports, search execution, replay execution, sealing behavior, serving behavior, Workbench behavior, or mutation path.
2. Why this exists
ADR-0228 made GeometricSearchRun immutable. ADR-0231 then authorized the
first narrow candidate operator, missing_role_candidate.v1, which emits a
CandidateOperatorResult carrying:
candidate_attempt
candidate_reconstruction
attempt_id
attempt_index
candidate_digest
candidate_reconstruction_digest
geometric_search_run_id
That result is not yet lawfully part of a replayable run episode. The existing
replay adapter shell validates attempts by membership in
GeometricSearchRun.candidate_attempts, while the first candidate operator
correctly does not mutate that tuple. Without a distinct binding boundary, the
next implementation could be tempted to:
mutate GeometricSearchRun.candidate_attempts
smuggle operator output into replay
recompute run IDs after candidate construction
let candidate output masquerade as run evidence
All four collapse immutability and replay identity. The intrinsic space is a relation between two immutable records: the original run envelope and the operator-produced candidate. The lawful action is not mutation; it is a content-addressed membership binding whose corrective stages are still replay and sealing.
3. Architectural directions considered
Option A: external binding record
original GeometricSearchRun
+ CandidateOperatorResult
-> CandidateAttemptRunBinding
The original run remains byte-stable. The binding record carries the evidence
that the candidate attempt belongs to the run episode for downstream replay and
trace sealing. This is selected for v1 because it minimizes mutation risk and
does not reuse or alter run_id semantics.
Option B: derived immutable run snapshot
original GeometricSearchRun
+ CandidateOperatorResult
-> derived GeometricSearchRun(candidate_attempts=(attempt,))
This keeps Python values immutable but creates immediate ambiguity around
whether the derived snapshot inherits the original run_id, receives a new
run_id, or receives a continuation identity. The current replay and sealing
tests already fabricate such snapshots for fixtures, but production semantics
are not authorized. Rejected for this ADR.
Option C: binding record first, future derived snapshot later
The loop may eventually need a derived run snapshot for compatibility, display, or batch replay. That snapshot should consume the binding record and define a new continuation identity under a separate ADR/PR. This ADR chooses only the first half: the external binding record.
4. Decision
The selected record name is:
CandidateAttemptRunBinding
The v1 policy identity is conceptually:
CANDIDATE_ATTEMPT_RUN_BINDING_POLICY_VERSION = "candidate_attempt_run_binding.v1"
The public conceptual outcome is:
CandidateAttemptRunBindingOutcome =
CandidateAttemptRunBinding | CandidateAttemptRunBindingRefusal
The binding layer may decide only:
whether a CandidateOperatorResult can be immutably associated with a specific
GeometricSearchRun episode
The binding layer may not decide:
truth
answerability
proof closure
contract closure
serving
promotion
learning
global uniqueness
candidate ranking
candidate selection
repair
mutation
A successful binding means only:
this candidate attempt is structurally admissible as a member of this run
episode
It does not mean:
the candidate is correct
the candidate should be replayed automatically
the candidate should be sealed automatically
the candidate should be served
the candidate should be promoted
Binding produces a binding_id only. It does not produce a new run_id.
GeometricSearchRun.run_id remains stable and continues to identify the
original run envelope. A future derived immutable run snapshot or continuation
identity requires a separate ADR and implementation authorization.
5. Position in the loop
The dependency direction remains one-way:
ContractAssessment
-> ContractResidual
-> SearchGateDecision
-> ComputeBudgetDecision
-> GeometricSearchRun
-> CandidateOperatorResult
-> CandidateAttemptRunBinding
-> future Replay Adapter compatibility input
-> future SealedPracticeTrace compatibility input
The binding boundary consumes existing records. It does not call upstream producers and does not call downstream replay or sealing:
project_contract_residuals
decide_search_gate
decide_compute_budget
initialize_geometric_search_run
build_missing_role_candidate
build_replay_adapter_input
classify_replay_result
build_practice_trace_input
seal_practice_trace
determine
The binding record is the lawful episode-composition medium. It lets replay and
sealing validate membership later without requiring mutation of
GeometricSearchRun.candidate_attempts.
This ADR does not broaden missing_role_candidate.v1. The operator remains:
one attempt
one typed cue
no source reread
no search
no replay
no sealing
The binding layer consumes an already-produced CandidateOperatorResult. It
does not call or schedule the operator.
6. Input schema
The conceptual input is:
@dataclass(frozen=True, slots=True)
class CandidateAttemptRunBindingInput:
input_digest: str
binding_policy_version: str
original_run_id: str
original_run_policy_version: str
original_run_input_digest: str
operator_result_id: str
operator_policy_version: str
candidate_attempt_id: str
attempt_index: int
candidate_digest: str
candidate_reconstruction_digest: str
operator_set_id: str
operator_set_version: str
budget_id: str
gate_decision_id: str
residual_ids: tuple[str, ...]
problem_frame_digest: str
original_contract_assessment_id: str
schema_versions: tuple[tuple[str, str], ...]
policy_versions: tuple[tuple[str, str], ...]
The input is accompanied by immutable values for:
- the original
GeometricSearchRun; - the
CandidateOperatorResult; - the result's
CandidateAttempt; - the result's
CandidateReconstruction; - identity-bearing
ComputeBudgetDecisionandSearchGateDecisionprojections when needed to verify structural budget and identity chain; and - the residual and original assessment identities required by the run.
Identities are not substitutes for values that must be structurally checked. Values are not substitutes for canonical identity. A future implementation may consume validated projections only when they contain every field needed for the checks in this ADR.
The input may not contain an answer, selected candidate, rank, score, repair command, Workbench action, runtime/session state, mutable container, callable, path, plugin name, model handle, environment value, or filesystem-derived identity.
7. Output schema
The successful conceptual output is:
@dataclass(frozen=True, slots=True)
class CandidateAttemptRunBinding:
binding_id: str
binding_policy_version: str
input_digest: str
original_run_id: str
operator_result_id: str
candidate_attempt_id: str
attempt_index: int
candidate_digest: str
candidate_reconstruction_digest: str
candidate_attempt_ref: str
budget_charge: BudgetCharge
depth: int
step_index: int
run_attempt_membership: str
evidence_spans: tuple[SourceSpan, ...]
reason_codes: tuple[str, ...]
explanation: str
candidate_attempt_ref is a content identity or stable reference for the exact
CandidateAttempt value emitted by the operator result. It must not be a
mutable pointer, object address, path, or serialized object dump.
run_attempt_membership is a closed semantic tag. V1 should use:
structurally_bound
The tag means "the candidate attempt is admissible as a member of this run
episode under candidate_attempt_run_binding.v1." It carries no replay,
truth, answer, selection, serving, promotion, or mutation authority.
The output copies the candidate attempt's structural charge fields:
budget_charge
depth
step_index
Those copied fields are evidence for budget verification, not new budget allocation.
8. Binding refusal schema
The refusal output is:
@dataclass(frozen=True, slots=True)
class CandidateAttemptRunBindingRefusal:
binding_refusal_id: str
binding_policy_version: str
input_digest: str | None
original_run_id: str | None
operator_result_id: str | None
candidate_attempt_id: str | None
reason_codes: tuple[str, ...]
explanation: str
Optional identities are populated only when they can be validated without
guessing. Malformed input may therefore produce a refusal with None
identities. A refusal is never a partial binding.
No binding dataclass may contain any of these public fields:
answer
final_answer
served_output
proof
verdict
promotion
mutation
teaching_update
pack_update
policy_update
identity_update
workbench_state
runtime_effect
confidence
score
rank
priority
selected
selected_candidate
best
best_candidate
serving_allowed
runnable
9. CandidateAttempt membership semantics
The binding layer must fail closed unless all of the following are true:
original_runis aGeometricSearchRun.candidate_operator_resultis aCandidateOperatorResult.candidate_operator_result.geometric_search_run_id == original_run.run_id.candidate_operator_result.candidate_attempt.attempt_id == candidate_operator_result.attempt_id.candidate_operator_result.candidate_attempt.candidate_digest == candidate_operator_result.candidate_digest.candidate_operator_result.candidate_reconstruction.candidate_digest == candidate_operator_result.candidate_digest.candidate_operator_result.candidate_reconstruction.candidate_reconstruction_digest == candidate_operator_result.candidate_reconstruction_digest.candidate_operator_result.candidate_attempt.replay_status == CandidateReplayStatus.REPLAY_PENDING.candidate_operator_result.candidate_attempt.replay_blockers == ().candidate_operator_result.attempt_indexmatchescandidate_operator_result.candidate_attempt.attempt_index.attempt_indexis not already present inoriginal_run.candidate_attempts.attempt_idis not already present inoriginal_run.candidate_attempts.candidate_digestis not already present inoriginal_run.candidate_attemptsunless a later ADR explicitly defines duplicate-candidate semantics.- The candidate
budget_chargefits the remaining run budget. - The candidate
depthfits the remaining run depth. - The candidate
step_indexand step charge fit the remaining run steps. operator_set_idmatchesoriginal_run.operator_set_id.operator_set_versionmatchesoriginal_run.operator_set_version.- The operator result input chain binds to the original run, gate, budget, residual identities, problem-frame digest, and original assessment identity.
- Evidence spans are valid, exact, ordered, and preserved.
schema_versionsandpolicy_versionsare unique sorted name/version pairs.
If the original run already has attempts, membership is deterministic:
existing attempts retain their original order
the new binding is admissible only at its declared attempt_index
gaps are allowed only when the declared index is explicitly supported by the
original run's deterministic attempt policy
duplicates by index or attempt_id are refused
duplicates by candidate_digest are refused in v1
V1 should be stricter than necessary: if an existing attempt set makes ordering
ambiguous, refuse. Do not infer a "next" position from tuple length unless that
position equals the candidate attempt's declared attempt_index and fits every
budget/depth/step rule.
10. Budget and run accounting
The binding layer does not allocate budget. It verifies consumption that was already declared by the candidate attempt.
The future implementation must check:
candidate_attempt.budget_charge.candidates
candidate_attempt.budget_charge.steps
candidate_attempt.depth
candidate_attempt.step_index
against:
original_run.budget_consumed
ComputeBudgetDecision limits already embedded in the run
The run's copied ceilings are load-bearing:
original_run.budget_consumed.max_candidates
original_run.budget_consumed.max_depth
original_run.budget_consumed.max_steps
original_run.budget_consumed.max_parallelism
Required checks:
budget_charge.candidatesis exactly the operator-declared charge and is positive.budget_charge.stepsis exactly the operator-declared charge and is positive.original_run.budget_consumed.candidates_considered + budget_charge.candidates <= original_run.budget_consumed.max_candidates.max(original_run.budget_consumed.depth_reached, candidate_attempt.depth) <= original_run.budget_consumed.max_depth.original_run.budget_consumed.steps_used + budget_charge.steps <= original_run.budget_consumed.max_steps.original_run.budget_consumed.max_parallelism == 1in v1.candidate_attempt.depth >= 0.candidate_attempt.step_index >= 0.
No binding may make budget exhaustion imply correctness. No binding may inflate,
refund, reallocate, or mutate budget. No binding may modify the original
ComputeBudgetDecision or GeometricSearchRun.budget_consumed.
11. Replay adapter implications
The current replay adapter requires an attempt to be present in
GeometricSearchRun.candidate_attempts. That check remains correct for derived
run snapshots, but it cannot consume the first candidate operator result
without either mutation or a compatibility boundary.
This ADR authorizes a later compatibility patch, after the binding record exists, where replay input construction can consume:
original GeometricSearchRun
CandidateAttemptRunBinding
CandidateAttempt
CandidateReconstruction identity
rather than requiring the original run to contain the attempt in
candidate_attempts.
Replay must validate that:
CandidateAttemptRunBinding.original_run_id == GeometricSearchRun.run_id;- the binding's
candidate_attempt_id,attempt_index,candidate_digest, andcandidate_reconstruction_digestmatch the supplied attempt and reconstruction; - the binding's operator result identity matches the candidate operator result if that result is supplied;
- the original run, binding, attempt, and reconstruction reproduce their canonical identities; and
- the binding policy/schema versions are supported.
The replay adapter still may not mutate the original run, mutate the binding, run search, execute candidate operators, select candidates, seal traces, produce answers, or serve output.
This ADR does not modify replay adapter code.
12. Sealed trace implications
A future sealed trace should bind:
original GeometricSearchRun
CandidateOperatorResult
CandidateAttemptRunBinding
ReplayAdapterResult / ReplayAdapterRefusal
A SealedPracticeTrace may reference binding IDs as part of trace_records
and may add a binding identity sequence only after a separately authorized
implementation PR updates the trace schema.
Sealing must treat the binding as membership evidence only. A sealed trace may derive an episode disposition from validated replay records, but not from binding alone. A binding without replay remains pending membership evidence, not candidate refusal, replay closure, answerability, or promotion.
This ADR does not implement sealing behavior.
13. Immutability and run identity
GeometricSearchRun remains immutable. The binding layer must not:
- append to
candidate_attempts; - replace
candidate_attempts; - update
budget_consumed; - update
run_disposition; - update
exhaustion_code; - recompute
run_id; - create a shadow run that reuses the original
run_id; or - alter any upstream gate, budget, residual, assessment, or operator result.
Binding produces:
binding_id only
It does not produce a new run_id. The original run identity remains the
identity of the original exploration envelope. The binding identity is the
identity of the membership relation:
membership(original_run_id, operator_result_id, candidate_attempt_id)
Any future derived immutable run snapshot must be separately authorized and must define whether it receives a continuation ID, snapshot ID, or new run ID.
14. Evidence-span preservation
The binding layer must:
preserve ordered evidence spans
preserve duplicate spans
not synthesize spans
not dedupe spans
not sort spans independently
fail closed on malformed spans
The binding's evidence_spans are copied from the candidate operator result
and must match the candidate attempt and candidate reconstruction spans exactly
when those records expose spans. If multiple upstream records expose evidence
spans, the future implementation must either require exact equality across the
operator result, attempt, and reconstruction, or refuse with an explicit reason
code. V1 should not merge span streams.
Evidence-span order participates in binding identity. Reordering spans changes
binding_id. Duplicate spans remain separate entries.
15. Determinism and canonical identity
All load-bearing IDs use canonical JSON:
json.dumps(
payload,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
The serialized string is encoded as UTF-8 and hashed with SHA-256. The digest is the full lowercase hexadecimal encoding.
Required IDs:
CandidateAttemptRunBindingInput.input_digest
CandidateAttemptRunBinding.binding_id
CandidateAttemptRunBindingRefusal.binding_refusal_id
CandidateAttemptRunBindingInput.input_digest hashes the structural input
fields other than input_digest itself:
binding_policy_version
original_run_id
original_run_policy_version
original_run_input_digest
operator_result_id
operator_policy_version
candidate_attempt_id
attempt_index
candidate_digest
candidate_reconstruction_digest
operator_set_id
operator_set_version
budget_id
gate_decision_id
ordered residual_ids
problem_frame_digest
original_contract_assessment_id
canonical schema_versions
canonical policy_versions
binding_id self-seals the structural binding payload with binding_id
blanked. It includes:
binding_policy_version
input_digest
original_run_id
operator_result_id
candidate_attempt_id
attempt_index
candidate_digest
candidate_reconstruction_digest
candidate_attempt_ref
budget_charge
depth
step_index
run_attempt_membership
ordered evidence_spans
ordered reason_codes
binding_refusal_id self-seals the refusal payload with
binding_refusal_id blanked. It includes:
binding_policy_version
input_digest or null
original_run_id or null
operator_result_id or null
candidate_attempt_id or null
ordered reason_codes
IDs must exclude:
explanation
wall-clock time
timestamps
random values
UUIDs
environment variables
hostname
OS details
CI metadata
file paths
generated prose
memory addresses
thread/process identifiers
filesystem order
hash-map iteration order unless canonicalized
user identity
model identity
machine identity
Changing explanation must not change any binding ID.
16. Authority boundary
CandidateAttemptRunBinding is membership evidence only.
It may decide:
structural admissibility of one operator-produced candidate attempt as a member
of one existing run episode
It has no authority to:
generate candidates
execute operators
execute search
repair source records
run contract replay
run proof replay
seal traces
produce answers
serve output
rank candidates
select candidates
mutate the original run
mutate budgets
mutate gates
mutate residuals
mutate operator results
promote findings
edit packs
edit teaching data
edit policy
edit identity
edit eval reports
write Workbench state
write files/artifacts
No binding result changes durable epistemic standing. Source kind
(operator, binding, search, replay, or practice) grants no promotion
authority.
17. Forbidden imports/calls/effects
The future implementation may import only:
- standard-library immutable value, enum, canonical JSON, and SHA-256 facilities;
SourceSpan;GeometricSearchRun,CandidateAttempt,BudgetCharge, andCandidateReplayStatusas value types;CandidateOperatorResultandCandidateReconstructionas value types;ComputeBudgetDecisionandSearchGateDecisionas value types when needed for identity checks; and- local run-binding immutable value types and static policy constants.
The future implementation must not import or call:
runtime
serving
Workbench
teaching/proposal mutation
pack/policy/identity mutation
eval/report mutation
Vault/recall mutation
filesystem writes
network I/O
subprocess
time/datetime/random/uuid/env/hostname/path identity
dynamic import/plugin discovery
external model/tool invocation
LLM generation
embedding/ANN/cosine/semantic-rank modules
determine answer production
candidate operator execution
contract/proof replay execution
sealed trace writing
answer realization
repair
It also must not call upstream or downstream producer functions:
project_contract_residuals
decide_search_gate
decide_compute_budget
initialize_geometric_search_run
build_missing_role_candidate
build_replay_adapter_input
classify_replay_result
build_practice_trace_input
seal_practice_trace
determine
The binding layer consumes existing records only. No reverse dependency from assessment, residual, gate, budget, search-run, candidate-operator, replay-adapter, or sealed-trace modules into the binding module is authorized unless a later PR explicitly names it.
18. Failure modes and fail-closed behavior
The future implementation must fail closed as follows:
| Failure | Required outcome |
|---|---|
| Malformed binding input | CandidateAttemptRunBindingRefusal |
| Unsupported binding policy | CandidateAttemptRunBindingRefusal |
Candidate result does not bind to run_id |
CandidateAttemptRunBindingRefusal |
| Candidate attempt does not match operator result | CandidateAttemptRunBindingRefusal |
| Candidate reconstruction does not match operator result | CandidateAttemptRunBindingRefusal |
| Attempt index duplicate | CandidateAttemptRunBindingRefusal |
| Attempt ID duplicate | CandidateAttemptRunBindingRefusal |
| Candidate digest duplicate in v1 | CandidateAttemptRunBindingRefusal |
| Budget charge exceeds remaining candidates | CandidateAttemptRunBindingRefusal |
| Budget charge exceeds remaining steps | CandidateAttemptRunBindingRefusal |
| Candidate depth exceeds remaining depth | CandidateAttemptRunBindingRefusal |
| Operator set mismatch | CandidateAttemptRunBindingRefusal |
| Operator result input chain does not bind to run/gate/budget/residual identities | CandidateAttemptRunBindingRefusal |
| Candidate replay status is not pending | CandidateAttemptRunBindingRefusal |
| Replay blockers are present | CandidateAttemptRunBindingRefusal |
| Malformed evidence spans | CandidateAttemptRunBindingRefusal |
| Duplicate or unsorted schema/policy version pairs | CandidateAttemptRunBindingRefusal |
No failure may become:
partial binding
best guess
soft proof
replay closure
sealed trace
answer
rank
priority
serving fallback
promotion
teaching mutation
Unknown == False
An unbindable candidate preserves the original run and original refusal. It does not consume additional budget and does not mutate any input record.
19. Test obligations for future implementation PR
The future implementation PR must add executing tests that meaningfully fail under the prohibited states:
- Public API exports are exact.
- Valid original run plus candidate operator result produces deterministic binding.
- Binding does not mutate original
GeometricSearchRun. - Binding ID is deterministic and explanation-excluded.
- Candidate result/run ID mismatch refuses.
- Candidate attempt/result ID mismatch refuses.
- Candidate attempt candidate digest mismatch refuses.
- Candidate reconstruction candidate digest mismatch refuses.
- Candidate reconstruction digest mismatch refuses.
- Candidate replay status not pending refuses.
- Replay blockers present refuses.
- Duplicate attempt index in original run refuses.
- Duplicate attempt ID in original run refuses.
- Budget charge beyond remaining candidates/steps/depth refuses.
- Operator-set mismatch refuses.
- Evidence spans preserve order and duplicates.
- Malformed evidence spans refuse.
- Binding result contains no answer/proof/verdict/serving/promotion/rank/score fields.
- Binding does not execute candidate operators.
- Binding does not execute replay or sealing.
- Binding does not call upstream producer functions.
- No filesystem/network/time/random/env/UUID/hostname/path identity.
- Static guards block forbidden imports/calls.
- No reverse dependencies from upstream modules unless explicitly intended.
- Focused tests and smoke lane pass.
Additional required controls:
- tests independently recompute input, binding, and refusal hashes rather than calling the implementation's private hash helper;
- static coupling tests parse the binding module and enforce the allowed import/call surface;
- tests prove the original run tuple and budget-consumed values are unchanged before and after binding;
- tests prove a binding alone cannot be consumed as replay closure or a sealed trace; and
- tests prove duplicate candidate digests refuse in v1.
20. Authorized next PR
This ADR authorizes exactly one next implementation PR:
feat(kernel): implement inert run-attempt binding shell
Scope of the next PR:
add run-attempt binding dataclasses/enums/helpers
consume GeometricSearchRun and CandidateOperatorResult
validate identity chain and budget/membership constraints
emit CandidateAttemptRunBinding or CandidateAttemptRunBindingRefusal
produce deterministic binding IDs
tests only for boundary behavior
The next PR explicitly excludes:
candidate operator execution
search execution
run mutation
derived run snapshot
contract/proof replay execution
sealed trace execution
answer production
Workbench
runtime/serving
teaching/proposal/report/eval mutation
pack/policy/identity mutation
promotion
filesystem persistence
artifact writing
new proof engine
No other implementation PR is authorized by this ADR. A replay adapter overload or compatibility patch, sealed-trace schema widening, derived immutable run snapshot, multi-attempt search, candidate ranking/selection, answer production, serving, Workbench display, and promotion each remain separately gated.