diff --git a/docs/adr/ADR-0228-geometric-search-run-envelope.md b/docs/adr/ADR-0228-geometric-search-run-envelope.md new file mode 100644 index 00000000..fe18d2ef --- /dev/null +++ b/docs/adr/ADR-0228-geometric-search-run-envelope.md @@ -0,0 +1,735 @@ +# 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.