# ADR-0228: Inert GeometricSearchRun Envelope **Status:** Proposed **Date:** 2026-06-22 **Scope:** Kernel diagnostics, bounded candidate exploration envelope, residual-gated practice loop **Depends on:** - ADR-0225 ContractResidual read model - ADR-0226 Residual-Gated Practice Loop v1 - ADR-0227 ComputeBudgetPolicy envelope - PR #868 ComputeBudgetDecision ## 1. Summary `GeometricSearchRun` is an inert diagnostic/practice envelope. It records an ordered sequence of candidate attempts inside a pre-granted deterministic budget. It is functional, bounded, deterministic, replayable, non-serving, non-mutating, and non-authoritative for truth. The envelope consumes exact identities for the problem frame, the refused contract assessment, its residuals, an allowed search-gate decision, an allowed compute-budget decision, a closed operator set, and the governing schema and policy versions. It emits only content-addressed diagnostic evidence: ```text run_id run_policy_version input_digest budget_id gate_decision_id ordered CandidateAttempt records budget_consumed run_disposition explanation ``` It does not emit or imply: ```text answer proof verdict promotion mutation serving_allowed runnable truth score confidence rank priority ``` This ADR defines schemas and boundaries only. It implements no search, operator, repair, replay adapter, trace seal, serving behavior, or mutation path. ## 2. Why this exists ADR-0226 places deterministic candidate exploration after eligibility and budget, but before contract and proof replay. ADR-0227 makes the budget a hard, non-authoritative resource ceiling. A distinct search-run envelope is needed to preserve those boundaries before candidate exploration exists. The intrinsic space is a finite operator graph over immutable, identified evidence. A candidate attempt is a path element in that graph, not a proposed answer. The forward operator is bounded exploration; its required conjugate is later contract and proof replay. Exploration may expose a candidate, but only replay may classify whether the candidate closes the existing obligations. Without an inert envelope, a future implementation could accidentally search without a valid budget, order candidates by non-replayable preference, discard failed attempts, treat exhaustion as correctness, or let search repair source evidence by authority. This ADR makes those states structurally inadmissible. ## 3. Architectural directions considered ### 3.1 Monolithic search controller A controller could gate, allocate, explore, replay, and select in one service. This collapses resource, exploration, and proof authority. A local success condition could silently become an answer-production path. Rejected. ### 3.2 Free-form attempt log An implementation could append arbitrary attempt dictionaries until a limit is reached. This preserves some telemetry, but it cannot make budget overruns, unstable ordering, incomplete identity, or authoritative fields unrepresentable. Rejected. ### 3.3 Immutable run plus typed initialization refusal A discriminated `SearchRunOutcome` separates failed initialization from a valid `GeometricSearchRun`. The valid run carries exact input identity, a closed operator-set identity, ordered attempts, structural budget accounting, and a closed disposition. Replay remains a later authority. Selected. This direction is the smallest boundary that preserves diagnostic usefulness without authorizing exploration implementation. ## 4. Decision The v1 policy identity is conceptually: ```text GEOMETRIC_SEARCH_RUN_POLICY_VERSION = "geometric_search_run.v1" ``` The architecture defines: ```text SearchRunOutcome = SearchRunRefusal | GeometricSearchRun ``` `SearchRunRefusal` represents fail-closed initialization. It may carry the dispositions `not_started`, `blocked_by_budget`, `blocked_by_gate`, or `invalid_input`, plus exact available input identities and typed reason codes. It is not a run, has no candidate attempts, and consumes no budget. `GeometricSearchRun` may be constructed only after every initialization obligation in this ADR succeeds. Once constructed, it is an immutable value. It may record ordered diagnostic attempts, but it cannot search freely, repair by authority, produce an answer, mutate an artifact, or bypass contract/proof replay. The v1 operator set may be empty or placeholder-only. This ADR does not authorize a candidate-producing operator. The only implementation authorized by Section 18 is an inert trace shell that can produce a deterministic zero-attempt terminal run for an empty operator set. ## 5. Envelope position in the loop The dependency and authority direction is one-way: ```text ContractAssessment → ContractResidual → SearchGateDecision → ComputeBudgetDecision → GeometricSearchRun → future Contract/Proof Replay Adapter → future SealedPracticeTrace → future Workbench read-only projection ``` There is no reverse dependency. `GeometricSearchRun` consumes already-produced records; it does not call the assessment, residual projection, gate, or budget producers. Downstream replay or display cannot rewrite the original run. ## 6. Inputs The conceptual input is an immutable `GeometricSearchInput`: ```python @dataclass(frozen=True, slots=True) class GeometricSearchInput: problem_frame_digest: str contract_assessment_id: str residual_ids: tuple[str, ...] gate_decision_id: str gate_policy_version: str gate_input_digest: str budget_id: str budget_policy_version: str operator_set_id: str operator_set_version: str run_policy_version: str schema_version: str ``` The identifiers have load-bearing meaning: - `problem_frame_digest` identifies the exact canonical `ProblemFrame`, including ordered facts, bindings, proposals, targets, hazards, and exact source spans required by the frame schema. A case label or raw filename is not a substitute. - `contract_assessment_id` identifies the exact refused `ContractAssessment`. The assessment must remain immutable and refused. - `residual_ids` is the complete canonical order consumed by the gate. No residual may be added, removed, deduplicated, or reordered by the run. - `gate_decision_id`, `gate_policy_version`, and `gate_input_digest` identify the exact allowed `SearchGateDecision` and its residual context. - `budget_id` and `budget_policy_version` identify the exact allowed `ComputeBudgetDecision` and its structural ceilings. - `operator_set_id` and `operator_set_version` identify an explicit, closed, ordered allowlist. Discovery by import scan, filesystem enumeration, plugin registry, entry point, or environment configuration is forbidden. - `run_policy_version` and `schema_version` make changes to construction, ordering, charging, or serialization explicit. The run input is valid only when all identities are present, full lowercase SHA-256 fields have exactly 64 hexadecimal characters where a digest is required, the residual sequence matches the gate sequence exactly, the gate is `ELIGIBLE`, the budget is `BUDGET_ALLOWED`, and the gate/budget cross-identities and canonical hashes reproduce byte-for-byte. `input_digest` is the SHA-256 digest of the canonical JSON object containing exactly the fields above in their declared sequence values. It identifies the run input, not a semantic truth claim. ## 7. Outputs The conceptual initialized-run schema is: ```python @dataclass(frozen=True, slots=True) class GeometricSearchRun: run_id: str run_policy_version: str schema_version: str problem_frame_digest: str contract_assessment_id: str residual_ids: tuple[str, ...] gate_decision_id: str budget_id: str operator_set_id: str operator_set_version: str input_digest: str candidate_attempts: tuple[CandidateAttempt, ...] budget_consumed: BudgetConsumed run_disposition: RunDisposition exhaustion_code: RunExhaustionCode | None explanation: str ``` `SearchRunOutcome` is the public conceptual result. It is a discriminated union so a blocked or invalid initialization cannot masquerade as a valid empty run. The refusal branch is also immutable and diagnostic-only: ```python @dataclass(frozen=True, slots=True) class SearchRunRefusal: outcome_id: str run_policy_version: str input_digest: str | None gate_decision_id: str | None budget_id: str | None run_disposition: RunDisposition reason_codes: tuple[str, ...] explanation: str ``` `outcome_id` uses the same canonical hashing rules as the run and excludes `explanation`. A refusal never receives a `run_id` because initialization did not produce a run. `RunDisposition` is a closed vocabulary: ```text not_started blocked_by_budget blocked_by_gate invalid_input exhausted_no_candidate candidate_replay_pending candidate_replay_closed candidate_replay_refused ``` The admissible type-state pairs are: | Outcome type | Permitted dispositions | |---|---| | `SearchRunRefusal` | `not_started`, `blocked_by_budget`, `blocked_by_gate`, `invalid_input` | | `GeometricSearchRun` | `exhausted_no_candidate`, `candidate_replay_pending`, `candidate_replay_closed`, `candidate_replay_refused` | `candidate_replay_closed` does not mean the run produced a proof-gated answer. It is reserved for a future immutable projection that records that an independent, authorized replay adapter reported closure. This ADR and the next authorized PR cannot emit it. `RunExhaustionCode` is also closed: ```text operator_set_empty operator_space_depleted max_candidates_reached max_depth_reached max_steps_reached ``` An exhausted run carries exactly one exhaustion code. Non-exhausted dispositions carry `None`. The run schema contains no answer, proof, verdict, promotion, mutation, serving, runnable, truth, score, confidence, rank, or priority field. ## 8. CandidateAttempt record The conceptual attempt schema is: ```python @dataclass(frozen=True, slots=True) class CandidateAttempt: attempt_id: str attempt_index: int parent_attempt_id: str | None operator_id: str operator_version: str input_digest: str candidate_digest: str budget_charge: BudgetCharge depth: int step_index: int replay_status: ReplayStatus replay_blockers: tuple[str, ...] evidence_spans: tuple[SourceSpan, ...] explanation: str ``` `ReplayStatus` is closed: ```text replay_pending replay_closed replay_refused ``` For this ADR and its authorized next PR, replay is placeholder-only. A future candidate-producing implementation must initialize every attempt as `replay_pending` with no invented closure. Only the separately authorized contract/proof replay adapter may report `replay_closed` or `replay_refused`. `BudgetCharge` is a structural record: ```python @dataclass(frozen=True, slots=True) class BudgetCharge: candidates: int # exactly 1 for every CandidateAttempt steps: int # positive deterministic operator/replay applications ``` Each attempt is diagnostic evidence. It is not a proposal artifact, proof, answer, ranking entry, confidence estimate, or mutation command. Its source spans remain in supplied order and preserve duplicates. The run may not widen, merge, synthesize, or deduplicate spans. Each future operator must declare, in the closed operator-set manifest: ```text operator_id operator_version allowed residual kinds input obligations output candidate shape budget charge deterministic ordering key forbidden side effects replay obligations ``` Operator declarations are constraints, not authority. No operator is authorized by this ADR. ## 9. Budget consumption No `GeometricSearchRun` may initialize unless `ComputeBudgetDecision.status == BUDGET_ALLOWED`. Initialization must fail closed when any of the following holds: - the budget is blocked, zero, or unassessable; - `max_candidates == 0` or `max_steps == 0`; - any structural limit is negative; - `max_parallelism != 1` in v1; - `budget_id` does not reproduce from its canonical payload; - the budget's `gate_decision_id` does not equal the supplied gate identity; - the budget's gate policy version or gate input digest does not equal the supplied gate record; - the gate's residual identities do not equal the supplied residual identities; - the gate is not `ELIGIBLE`; - the budget or run policy version is unsupported; or - any required identity is malformed or absent. The conceptual accounting record is: ```python @dataclass(frozen=True, slots=True) class BudgetConsumed: candidates_considered: int max_candidates: int depth_reached: int max_depth: int steps_used: int max_steps: int parallelism_used: int max_parallelism: int exhausted: bool ``` The `max_*` values are copied exactly from the validated budget decision. The consumed values are non-negative and satisfy: ```text candidates_considered == len(candidate_attempts) candidates_considered == sum(attempt.budget_charge.candidates) candidates_considered <= max_candidates depth_reached == 0 when there are no attempts depth_reached == max(attempt.depth) otherwise depth_reached <= max_depth steps_used == sum(attempt.budget_charge.steps) steps_used <= max_steps parallelism_used in {0, 1} parallelism_used <= max_parallelism == 1 ``` Every candidate attempt consumes exactly one candidate unit and a positive, predeclared step charge. Before an attempt is materialized, its complete charge must fit. Partial charging, negative credits, refunds, hidden retries, adaptive budget increases, and “try until solved” are forbidden. `BudgetConsumed.exhausted` is true only when a structural budget ceiling prevents the next otherwise-admissible attempt. An empty or depleted operator space uses `exhausted_no_candidate` with the corresponding operator exhaustion code, but does not falsely claim that budget was consumed. No v1 accounting field depends on wall-clock time. `max_wallclock_ms`, if present on the upstream budget record, is diagnostic metadata and neither authorizes execution nor participates in charging. ## 10. Determinism and identity All load-bearing IDs use canonical JSON serialized with: ```python json.dumps( payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False, ).encode("utf-8") ``` The digest is the full lowercase SHA-256 hexadecimal encoding. NaN, Infinity, floats, implicit object serialization, and locale-dependent values are forbidden in hashed payloads. The identities are layered: - `operator_set_id` hashes the ordered operator manifest and its version. - `input_digest` hashes the exact upstream identities and schema/policy versions described in Section 6. - `candidate_digest` hashes the complete canonical candidate representation. - `attempt_id` hashes `attempt_index`, `parent_attempt_id`, operator identity, `input_digest`, `candidate_digest`, `budget_charge`, `depth`, `step_index`, replay status/blockers, and exact evidence spans. - `run_id` self-seals the complete structural `GeometricSearchRun` payload with `run_id` blanked. It includes ordered attempts, budget consumption, disposition, and exhaustion code. `explanation` is excluded from every ID. IDs must also exclude: ```text wall-clock time timestamps random values UUIDs environment variables hostname OS details CI metadata file paths generated prose memory addresses thread or process identifiers ``` Candidate order is assigned before execution and is deterministic. Permitted ordering inputs are: ```text attempt_index closed operator-set order parent attempt order canonical candidate digest canonical residual/order context ``` Forbidden ordering inputs are: ```text model preference score confidence cosine similarity nearest-neighbor rank wall-clock completion order thread scheduling order filesystem order hash-map iteration order unless canonicalized randomness ``` Duplicate evidence spans are preserved. Re-execution with byte-equivalent inputs, versions, operator manifest, and budget must reproduce every identity, attempt position, charge, disposition, and output byte-for-byte. ## 11. Parallelism semantics V1 is serial. `max_parallelism` must equal `1`; `parallelism_used` is `0` for a zero-attempt run and `1` after the first attempt. A value greater than `1` causes initialization refusal, even if the host could execute concurrently. No parallel candidate execution is authorized in this tranche. A future ADR may define deterministic parallel batches only if it assigns attempt indices, parents, operator order, and complete budget reservations before execution; merges by those canonical positions rather than completion order; preserves the same output as serial execution; and proves that cancellation or scheduling cannot change the trace. That future design is not implemented or implied here. ## 12. Replay boundary Exploration and replay are distinct authority domains. `GeometricSearchRun` may eventually enumerate candidates, but it cannot declare one correct. The run does not invoke `ContractAssessment`, a verifier, a proof engine, or an answer realizer. Only a later, separately authorized contract/proof replay adapter may classify a candidate as closed or refused. It must replay the same organ-specific contract obligations and every applicable proof obligation over the candidate's exact identified evidence. Its report is external evidence; it does not mutate the original run or assessment. No candidate closes by: - ranking; - absence of blockers; - successful generation; - semantic proximity; - budget exhaustion; - operator exhaustion; or - any path lacking `ContractAssessment` replay and applicable proof replay. For #869, `replay_status` is only a placeholder field. There is no replay adapter, replay execution, sealed trace, or answer-production path in this PR. The dispositions `candidate_replay_closed` and `candidate_replay_refused` are reserved until that adapter is separately authorized. ## 13. Exhaustion and refusal semantics Exhaustion is a typed diagnostic outcome. It is never hidden by retry, fallback, selection, or prose. If the operator space is empty, the future inert shell produces a deterministic initialized run with: ```text candidate_attempts == () budget_consumed.candidates_considered == 0 budget_consumed.steps_used == 0 budget_consumed.exhausted == false run_disposition == exhausted_no_candidate exhaustion_code == operator_set_empty ``` If a structural ceiling prevents the next otherwise-admissible attempt, the run records the corresponding budget exhaustion code and `budget_consumed.exhausted == true`. When all available budget or operator space is exhausted without externally reported replay closure: - the original contract refusal remains in force; - every failed or pending attempt remains visible in canonical order; - exhaustion is recorded explicitly; - no answer is produced; and - no state or artifact is mutated. Exhaustion cannot be converted into a best guess, partial answer, soft proof, confidence score, serving fallback, proposal promotion, negative verdict, or `Unknown == False` conclusion. Initialization refusal also preserves the original refusal. It creates no `GeometricSearchRun`, performs no operator work, and consumes no budget. ## 14. Authority boundary `GeometricSearchRun` has no authority to: - assess contracts; - project residuals; - decide search eligibility; - allocate or widen compute budget; - repair source evidence, spans, bindings, relations, targets, or frames; - prove candidate correctness; - produce or select an answer; - mark a case runnable; - serve output; - mutate teaching, packs, policy, identity, Vault, reports, or evals; - write sealed practice traces; - promote findings or epistemic standing; or - change Workbench state. The term “geometric” means bounded deterministic exploration in CORE's structured relational/operator space. It does not mean approximate nearest-neighbor retrieval, cosine similarity ranking, embedding fallback, semantic guessing, unbounded graph search, sampling, LLM scratchpad expansion, best-of-N answer voting, or self-consistency voting. The original `ProblemFrame`, `ContractAssessment`, residuals, gate, and budget are immutable input evidence. A candidate is a separate identified value, never an in-place repair. ## 15. Forbidden imports/calls/effects A future inert implementation may depend only on: - standard-library value, enum, canonical JSON, and SHA-256 facilities; - `SourceSpan` and identity-bearing `ProblemFrame` schema values; - the `ContractAssessment` record type, but not assessment functions; - `ContractResidual` record types, but not projection functions; - `SearchGateDecision` and `SearchGateStatus`, but not gate producers; - `ComputeBudgetDecision` and `ComputeBudgetStatus`, but not budget producers; and - local search-envelope value types and a closed operator-manifest schema. Reverse imports from assessment, residual, gate, or budget modules into a future search-run module are forbidden. The module may not import or call runtime, serving, Workbench, teaching, proposal, pack, Vault, recall, eval, report, calibration, field, algebra, network, filesystem, subprocess, clock, random, UUID, environment, dynamic import, external-model, or plugin-discovery facilities. In particular, no operator may: - mutate any upstream record or shared state; - write, edit, move, delete, seal, or promote artifacts; - call runtime or serving paths; - call external tools or models; - perform filesystem or network I/O; - spawn processes or threads; - read clocks, environment variables, host metadata, or filesystem order; or - use randomness, sampling, approximate recall, or model preference. ## 16. Failure modes and fail-closed behavior | Failure | Required outcome | |---|---| | Gate is blocked, ineligible, or unassessable | `SearchRunRefusal(blocked_by_gate)`; no run | | Budget is blocked, zero, or unassessable | `SearchRunRefusal(blocked_by_budget)`; no run | | Gate/budget identity or digest mismatch | `SearchRunRefusal(invalid_input)`; no run | | Problem-frame, assessment, or residual identity mismatch | `SearchRunRefusal(invalid_input)`; no run | | Unsupported schema, gate, budget, operator-set, or run policy version | `SearchRunRefusal(invalid_input)`; no run | | Negative structural limit | `SearchRunRefusal(invalid_input)`; no run | | `max_parallelism != 1` in v1 | `SearchRunRefusal(invalid_input)`; no run | | Empty operator set | Deterministic zero-attempt `exhausted_no_candidate` run | | Next charge would exceed a hard ceiling | Stop before the attempt; preserve prior attempts; typed exhaustion | | Candidate digest or operator declaration cannot be reproduced | Stop fail-closed; no closure or answer | | Replay is missing | `candidate_replay_pending`; original refusal remains | | Future replay refuses all candidates | `candidate_replay_refused`; original refusal remains | | Future replay reports closure | Record external closure only; the run still emits no answer | No failure may be converted to a default candidate, best effort, silent skip, retry loop, partial charge, or successful disposition. ## 17. Test obligations for future implementation PR The later implementation PR must add executing tests that meaningfully fail under each prohibited state: 1. Public API exports are exact and inert. 2. Initialization refuses blocked, zero, and unassessable budgets. 3. Initialization refuses gate/budget identity and digest mismatches. 4. Unsupported policy and schema versions fail closed. 5. Negative structural limits fail closed. 6. `max_parallelism > 1` fails closed in v1. 7. An empty operator set produces deterministic exhaustion with no attempts and no false budget consumption. 8. Candidate attempts are ordered deterministically. 9. Every candidate attempt consumes exactly one candidate unit and a positive deterministic step charge. 10. Budget exhaustion preserves all prior failed or pending attempts. 11. Run, attempt, and refusal schemas expose no answer, proof, verdict, promotion, serving, runnable, score, confidence, rank, or priority fields. 12. Canonical `input_digest`, `attempt_id`, and `run_id` exclude prose, time, environment, path, host, process, and random values. 13. Duplicate evidence spans and their order are preserved. 14. No runtime, serving, Workbench, teaching, proposal, pack, Vault, eval, or report mutation is reachable. 15. No filesystem, network, clock, random, environment, dynamic import, subprocess, or external-model use is reachable. 16. No reverse dependency exists from budget, gate, residual, or assessment modules into the search-run module. 17. Contract/proof replay remains placeholder-only until the separately authorized replay-adapter PR. Tests must independently recompute canonical hashes rather than calling the implementation's private hash helper. Structural coupling tests must parse the implementation and enforce the allowed dependency surface. The focused tests and the repository smoke lane must pass. ## 18. Authorized next PR This ADR authorizes exactly one next implementation PR: ```text feat(kernel): implement inert GeometricSearchRun trace shell ``` That PR may only: - add inert `GeometricSearchRun`, `SearchRunOutcome`, `SearchRunRefusal`, `CandidateAttempt`, budget-accounting, and closed-enum value types/helpers; - consume an already-produced `ComputeBudgetDecision` and the exact upstream identities; - validate gate/budget identity and canonical digests; - accept an explicit empty or placeholder-only closed operator set; - produce a deterministic empty `exhausted_no_candidate` run for the v1 empty operator set; - record the exact budget ceiling with zero consumption and an empty attempt list; - emit canonical `input_digest` and `run_id`; and - add tests only for envelope behavior and architectural isolation. That PR explicitly excludes: ```text candidate generation operator implementation repair contract/proof replay adapter sealed practice trace Workbench runtime/serving teaching/proposal/report/eval mutation answer production ``` No other implementation PR is authorized by this ADR. Candidate-producing operators, replay, sealing, display, evaluation, serving, and promotion each require their own reviewed authorization.