# 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: ```text 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: ```text 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: ```text 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: ```text 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 ```text 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 ```text 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: ```text CandidateAttemptRunBinding ``` The v1 policy identity is conceptually: ```text CANDIDATE_ATTEMPT_RUN_BINDING_POLICY_VERSION = "candidate_attempt_run_binding.v1" ``` The public conceptual outcome is: ```text CandidateAttemptRunBindingOutcome = CandidateAttemptRunBinding | CandidateAttemptRunBindingRefusal ``` The binding layer may decide only: ```text whether a CandidateOperatorResult can be immutably associated with a specific GeometricSearchRun episode ``` The binding layer may not decide: ```text truth answerability proof closure contract closure serving promotion learning global uniqueness candidate ranking candidate selection repair mutation ``` A successful binding means only: ```text this candidate attempt is structurally admissible as a member of this run episode ``` It does not mean: ```text 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: ```text 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: ```text 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: ```text 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: ```python @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 `ComputeBudgetDecision` and `SearchGateDecision` projections 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: ```python @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: ```text 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: ```text 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: ```python @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: ```text 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_run` is a `GeometricSearchRun`. - `candidate_operator_result` is a `CandidateOperatorResult`. - `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_index` matches `candidate_operator_result.candidate_attempt.attempt_index`. - `attempt_index` is not already present in `original_run.candidate_attempts`. - `attempt_id` is not already present in `original_run.candidate_attempts`. - `candidate_digest` is not already present in `original_run.candidate_attempts` unless a later ADR explicitly defines duplicate-candidate semantics. - The candidate `budget_charge` fits the remaining run budget. - The candidate `depth` fits the remaining run depth. - The candidate `step_index` and step charge fit the remaining run steps. - `operator_set_id` matches `original_run.operator_set_id`. - `operator_set_version` matches `original_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_versions` and `policy_versions` are unique sorted name/version pairs. If the original run already has attempts, membership is deterministic: ```text 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: ```text candidate_attempt.budget_charge.candidates candidate_attempt.budget_charge.steps candidate_attempt.depth candidate_attempt.step_index ``` against: ```text original_run.budget_consumed ComputeBudgetDecision limits already embedded in the run ``` The run's copied ceilings are load-bearing: ```text 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.candidates` is exactly the operator-declared charge and is positive. - `budget_charge.steps` is 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 == 1` in 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: ```text 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`, and `candidate_reconstruction_digest` match 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: ```text 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: ```text 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: ```text 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: ```text 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: ```python 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: ```text CandidateAttemptRunBindingInput.input_digest CandidateAttemptRunBinding.binding_id CandidateAttemptRunBindingRefusal.binding_refusal_id ``` `CandidateAttemptRunBindingInput.input_digest` hashes the structural input fields other than `input_digest` itself: ```text 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: ```text 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: ```text 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: ```text 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: ```text structural admissibility of one operator-produced candidate attempt as a member of one existing run episode ``` It has no authority to: ```text 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`, and `CandidateReplayStatus` as value types; - `CandidateOperatorResult` and `CandidateReconstruction` as value types; - `ComputeBudgetDecision` and `SearchGateDecision` as 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: ```text 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: ```text 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: ```text 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: 1. Public API exports are exact. 2. Valid original run plus candidate operator result produces deterministic binding. 3. Binding does not mutate original `GeometricSearchRun`. 4. Binding ID is deterministic and explanation-excluded. 5. Candidate result/run ID mismatch refuses. 6. Candidate attempt/result ID mismatch refuses. 7. Candidate attempt candidate digest mismatch refuses. 8. Candidate reconstruction candidate digest mismatch refuses. 9. Candidate reconstruction digest mismatch refuses. 10. Candidate replay status not pending refuses. 11. Replay blockers present refuses. 12. Duplicate attempt index in original run refuses. 13. Duplicate attempt ID in original run refuses. 14. Budget charge beyond remaining candidates/steps/depth refuses. 15. Operator-set mismatch refuses. 16. Evidence spans preserve order and duplicates. 17. Malformed evidence spans refuse. 18. Binding result contains no answer/proof/verdict/serving/promotion/rank/score fields. 19. Binding does not execute candidate operators. 20. Binding does not execute replay or sealing. 21. Binding does not call upstream producer functions. 22. No filesystem/network/time/random/env/UUID/hostname/path identity. 23. Static guards block forbidden imports/calls. 24. No reverse dependencies from upstream modules unless explicitly intended. 25. 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: ```text feat(kernel): implement inert run-attempt binding shell ``` Scope of the next PR: ```text 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: ```text 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.