From fc78db75005bf69d18be2e966eeba7fb27834587 Mon Sep 17 00:00:00 2001 From: Shay Date: Mon, 22 Jun 2026 19:50:20 -0700 Subject: [PATCH] docs(kernel): define candidate-attempt run binding boundary --- ...-candidate-attempt-run-binding-boundary.md | 952 ++++++++++++++++++ 1 file changed, 952 insertions(+) create mode 100644 docs/adr/ADR-0232-candidate-attempt-run-binding-boundary.md diff --git a/docs/adr/ADR-0232-candidate-attempt-run-binding-boundary.md b/docs/adr/ADR-0232-candidate-attempt-run-binding-boundary.md new file mode 100644 index 00000000..60dd5ffd --- /dev/null +++ b/docs/adr/ADR-0232-candidate-attempt-run-binding-boundary.md @@ -0,0 +1,952 @@ +# 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.