core/generate/run_attempt_binding.py

620 lines
24 KiB
Python

"""Inert CandidateAttempt run-binding shell per ADR-0232."""
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from enum import Enum, unique
from generate.candidate_operator import CandidateOperatorResult
from generate.geometric_search_run import (
BudgetCharge,
CandidateAttempt,
CandidateReplayStatus,
GeometricSearchRun,
)
from generate.kernel_facts import SourceSpan
CANDIDATE_ATTEMPT_RUN_BINDING_POLICY_VERSION = "candidate_attempt_run_binding.v1"
@unique
class RunAttemptBindingRefusalReason(str, Enum):
INVALID_BINDING_INPUT = "invalid_binding_input"
UNSUPPORTED_BINDING_POLICY = "unsupported_binding_policy"
RUN_RESULT_MISMATCH = "run_result_mismatch"
ATTEMPT_RESULT_MISMATCH = "attempt_result_mismatch"
RECONSTRUCTION_RESULT_MISMATCH = "reconstruction_result_mismatch"
REPLAY_STATUS_NOT_PENDING = "replay_status_not_pending"
REPLAY_BLOCKERS_PRESENT = "replay_blockers_present"
DUPLICATE_ATTEMPT_INDEX = "duplicate_attempt_index"
DUPLICATE_ATTEMPT_ID = "duplicate_attempt_id"
DUPLICATE_CANDIDATE_DIGEST = "duplicate_candidate_digest"
BUDGET_CHARGE_EXCEEDS_REMAINING = "budget_charge_exceeds_remaining"
OPERATOR_SET_MISMATCH = "operator_set_mismatch"
RUN_IDENTITY_MISMATCH = "run_identity_mismatch"
MALFORMED_EVIDENCE_SPAN = "malformed_evidence_span"
UNSUPPORTED_SCHEMA_VERSION = "unsupported_schema_version"
@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], ...]
@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
@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
CandidateAttemptRunBindingOutcome = (
CandidateAttemptRunBinding | CandidateAttemptRunBindingRefusal
)
def _canonical_digest(payload: dict[str, object]) -> str:
encoded = json.dumps(
payload,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def _span_payload(span: SourceSpan) -> dict[str, object]:
return {
"text": span.text,
"start": span.start,
"end": span.end,
"sentence_index": span.sentence_index,
}
def _budget_charge_payload(charge: BudgetCharge) -> dict[str, int]:
return {"candidates": charge.candidates, "steps": charge.steps}
def _candidate_attempt_payload(attempt: CandidateAttempt) -> dict[str, object]:
return {
"attempt_id": attempt.attempt_id,
"attempt_index": attempt.attempt_index,
"parent_attempt_id": attempt.parent_attempt_id,
"operator_id": attempt.operator_id,
"operator_version": attempt.operator_version,
"input_digest": attempt.input_digest,
"candidate_digest": attempt.candidate_digest,
"budget_charge": _budget_charge_payload(attempt.budget_charge),
"depth": attempt.depth,
"step_index": attempt.step_index,
"replay_status": attempt.replay_status.value,
"replay_blockers": list(attempt.replay_blockers),
"evidence_spans": [_span_payload(span) for span in attempt.evidence_spans],
}
def _version_pairs_payload(versions: tuple[tuple[str, str], ...]) -> list[list[str]]:
return [[name, version] for name, version in versions]
def _valid_version_pairs(value: object) -> bool:
if not isinstance(value, tuple):
return False
names: list[str] = []
for entry in value:
if not isinstance(entry, tuple) or len(entry) != 2:
return False
name, version = entry
if not isinstance(name, str) or not name:
return False
if not isinstance(version, str) or not version:
return False
names.append(name)
return len(names) == len(set(names)) and names == sorted(names)
def _valid_span(span: object) -> bool:
return (
isinstance(span, SourceSpan)
and isinstance(span.text, str)
and type(span.start) is int
and type(span.end) is int
and span.start >= 0
and span.end >= span.start
and (span.sentence_index is None or type(span.sentence_index) is int)
)
def _valid_spans(spans: object) -> bool:
return isinstance(spans, tuple) and all(_valid_span(span) for span in spans)
def _binding_input_payload(
*,
original_run: GeometricSearchRun,
candidate_operator_result: CandidateOperatorResult,
schema_versions: tuple[tuple[str, str], ...],
policy_versions: tuple[tuple[str, str], ...],
) -> dict[str, object]:
attempt = candidate_operator_result.candidate_attempt
reconstruction = candidate_operator_result.candidate_reconstruction
return {
"binding_policy_version": CANDIDATE_ATTEMPT_RUN_BINDING_POLICY_VERSION,
"original_run_id": original_run.run_id,
"original_run_policy_version": original_run.run_policy_version,
"original_run_input_digest": original_run.input_digest,
"operator_result_id": candidate_operator_result.operator_result_id,
"operator_policy_version": candidate_operator_result.operator_policy_version,
"candidate_attempt_id": attempt.attempt_id,
"attempt_index": attempt.attempt_index,
"candidate_digest": attempt.candidate_digest,
"candidate_reconstruction_digest": reconstruction.candidate_reconstruction_digest,
"operator_set_id": original_run.operator_set_id,
"operator_set_version": original_run.operator_set_version,
"budget_id": original_run.budget_id,
"gate_decision_id": original_run.gate_decision_id,
"residual_ids": list(original_run.residual_ids),
"problem_frame_digest": original_run.problem_frame_digest,
"original_contract_assessment_id": original_run.contract_assessment_id,
"schema_versions": _version_pairs_payload(schema_versions),
"policy_versions": _version_pairs_payload(policy_versions),
}
def _refusal_id_payload(
*,
input_digest: str | None,
original_run_id: str | None,
operator_result_id: str | None,
candidate_attempt_id: str | None,
reason_codes: tuple[str, ...],
) -> dict[str, object]:
return {
"binding_refusal_id": "",
"binding_policy_version": CANDIDATE_ATTEMPT_RUN_BINDING_POLICY_VERSION,
"input_digest": input_digest,
"original_run_id": original_run_id,
"operator_result_id": operator_result_id,
"candidate_attempt_id": candidate_attempt_id,
"reason_codes": list(reason_codes),
}
def _refusal(
*,
reason: RunAttemptBindingRefusalReason,
input_digest: str | None = None,
original_run_id: str | None = None,
operator_result_id: str | None = None,
candidate_attempt_id: str | None = None,
) -> CandidateAttemptRunBindingRefusal:
reason_codes = (reason.value,)
binding_refusal_id = _canonical_digest(
_refusal_id_payload(
input_digest=input_digest,
original_run_id=original_run_id,
operator_result_id=operator_result_id,
candidate_attempt_id=candidate_attempt_id,
reason_codes=reason_codes,
)
)
return CandidateAttemptRunBindingRefusal(
binding_refusal_id=binding_refusal_id,
binding_policy_version=CANDIDATE_ATTEMPT_RUN_BINDING_POLICY_VERSION,
input_digest=input_digest,
original_run_id=original_run_id,
operator_result_id=operator_result_id,
candidate_attempt_id=candidate_attempt_id,
reason_codes=reason_codes,
explanation="CandidateAttempt run binding refused: " + reason.value + ".",
)
def _binding_id_payload(
*,
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,
evidence_spans: tuple[SourceSpan, ...],
) -> dict[str, object]:
return {
"binding_id": "",
"binding_policy_version": CANDIDATE_ATTEMPT_RUN_BINDING_POLICY_VERSION,
"input_digest": input_digest,
"original_run_id": original_run_id,
"operator_result_id": operator_result_id,
"candidate_attempt_id": candidate_attempt_id,
"attempt_index": attempt_index,
"candidate_digest": candidate_digest,
"candidate_reconstruction_digest": candidate_reconstruction_digest,
"candidate_attempt_ref": candidate_attempt_ref,
"budget_charge": _budget_charge_payload(budget_charge),
"depth": depth,
"step_index": step_index,
"run_attempt_membership": "structurally_bound",
"evidence_spans": [_span_payload(span) for span in evidence_spans],
"reason_codes": [],
}
def _operator_provenance_has(
provenance: tuple[tuple[str, str], ...], key: str, value: str
) -> bool:
return (key, value) in provenance
def bind_candidate_attempt_to_run(
*,
original_run: GeometricSearchRun,
candidate_operator_result: CandidateOperatorResult,
schema_versions: tuple[tuple[str, str], ...] = (),
policy_versions: tuple[tuple[str, str], ...] = (),
explanation: str = "",
) -> CandidateAttemptRunBindingOutcome:
"""Bind an existing candidate operator result to one immutable run episode."""
if not isinstance(original_run, GeometricSearchRun):
return _refusal(reason=RunAttemptBindingRefusalReason.INVALID_BINDING_INPUT)
if not isinstance(candidate_operator_result, CandidateOperatorResult):
return _refusal(
reason=RunAttemptBindingRefusalReason.INVALID_BINDING_INPUT,
original_run_id=original_run.run_id,
)
attempt = candidate_operator_result.candidate_attempt
reconstruction = candidate_operator_result.candidate_reconstruction
if not isinstance(attempt, CandidateAttempt):
return _refusal(
reason=RunAttemptBindingRefusalReason.INVALID_BINDING_INPUT,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
)
if not _valid_version_pairs(schema_versions) or not _valid_version_pairs(
policy_versions
):
return _refusal(
reason=RunAttemptBindingRefusalReason.UNSUPPORTED_SCHEMA_VERSION,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if candidate_operator_result.geometric_search_run_id != original_run.run_id:
return _refusal(
reason=RunAttemptBindingRefusalReason.RUN_RESULT_MISMATCH,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if candidate_operator_result.attempt_id != attempt.attempt_id:
return _refusal(
reason=RunAttemptBindingRefusalReason.ATTEMPT_RESULT_MISMATCH,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if candidate_operator_result.attempt_index != attempt.attempt_index:
return _refusal(
reason=RunAttemptBindingRefusalReason.ATTEMPT_RESULT_MISMATCH,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if candidate_operator_result.candidate_digest != attempt.candidate_digest:
return _refusal(
reason=RunAttemptBindingRefusalReason.ATTEMPT_RESULT_MISMATCH,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if candidate_operator_result.input_digest != attempt.input_digest:
return _refusal(
reason=RunAttemptBindingRefusalReason.ATTEMPT_RESULT_MISMATCH,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if candidate_operator_result.operator_name != attempt.operator_id:
return _refusal(
reason=RunAttemptBindingRefusalReason.ATTEMPT_RESULT_MISMATCH,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if candidate_operator_result.operator_version != attempt.operator_version:
return _refusal(
reason=RunAttemptBindingRefusalReason.ATTEMPT_RESULT_MISMATCH,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if reconstruction.candidate_digest != candidate_operator_result.candidate_digest:
return _refusal(
reason=RunAttemptBindingRefusalReason.RECONSTRUCTION_RESULT_MISMATCH,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if (
reconstruction.candidate_reconstruction_digest
!= candidate_operator_result.candidate_reconstruction_digest
):
return _refusal(
reason=RunAttemptBindingRefusalReason.RECONSTRUCTION_RESULT_MISMATCH,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if attempt.replay_status is not CandidateReplayStatus.REPLAY_PENDING:
return _refusal(
reason=RunAttemptBindingRefusalReason.REPLAY_STATUS_NOT_PENDING,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if attempt.replay_blockers != ():
return _refusal(
reason=RunAttemptBindingRefusalReason.REPLAY_BLOCKERS_PRESENT,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if reconstruction.source_residual_id not in original_run.residual_ids:
return _refusal(
reason=RunAttemptBindingRefusalReason.RUN_IDENTITY_MISMATCH,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if reconstruction.problem_frame_digest != original_run.problem_frame_digest:
return _refusal(
reason=RunAttemptBindingRefusalReason.RUN_IDENTITY_MISMATCH,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if (
reconstruction.original_contract_assessment_id
!= original_run.contract_assessment_id
):
return _refusal(
reason=RunAttemptBindingRefusalReason.RUN_IDENTITY_MISMATCH,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if not _operator_provenance_has(
reconstruction.operator_provenance,
"operator_set_id",
original_run.operator_set_id,
) or not _operator_provenance_has(
reconstruction.operator_provenance,
"operator_set_version",
original_run.operator_set_version,
):
return _refusal(
reason=RunAttemptBindingRefusalReason.OPERATOR_SET_MISMATCH,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if not _valid_spans(attempt.evidence_spans):
return _refusal(
reason=RunAttemptBindingRefusalReason.MALFORMED_EVIDENCE_SPAN,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if candidate_operator_result.evidence_spans != attempt.evidence_spans:
return _refusal(
reason=RunAttemptBindingRefusalReason.MALFORMED_EVIDENCE_SPAN,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if reconstruction.evidence_spans != attempt.evidence_spans:
return _refusal(
reason=RunAttemptBindingRefusalReason.MALFORMED_EVIDENCE_SPAN,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
for existing in original_run.candidate_attempts:
if existing.attempt_index == attempt.attempt_index:
return _refusal(
reason=RunAttemptBindingRefusalReason.DUPLICATE_ATTEMPT_INDEX,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if existing.attempt_id == attempt.attempt_id:
return _refusal(
reason=RunAttemptBindingRefusalReason.DUPLICATE_ATTEMPT_ID,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if existing.candidate_digest == attempt.candidate_digest:
return _refusal(
reason=RunAttemptBindingRefusalReason.DUPLICATE_CANDIDATE_DIGEST,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
consumed = original_run.budget_consumed
charge = attempt.budget_charge
if charge.candidates < 0 or charge.steps < 0 or attempt.depth < 0:
return _refusal(
reason=RunAttemptBindingRefusalReason.BUDGET_CHARGE_EXCEEDS_REMAINING,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if attempt.step_index < 0:
return _refusal(
reason=RunAttemptBindingRefusalReason.BUDGET_CHARGE_EXCEEDS_REMAINING,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if consumed.candidates_considered + charge.candidates > consumed.max_candidates:
return _refusal(
reason=RunAttemptBindingRefusalReason.BUDGET_CHARGE_EXCEEDS_REMAINING,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if consumed.steps_used + charge.steps > consumed.max_steps:
return _refusal(
reason=RunAttemptBindingRefusalReason.BUDGET_CHARGE_EXCEEDS_REMAINING,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if max(consumed.depth_reached, attempt.depth) > consumed.max_depth:
return _refusal(
reason=RunAttemptBindingRefusalReason.BUDGET_CHARGE_EXCEEDS_REMAINING,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
if consumed.parallelism_used > consumed.max_parallelism:
return _refusal(
reason=RunAttemptBindingRefusalReason.BUDGET_CHARGE_EXCEEDS_REMAINING,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
)
input_payload = _binding_input_payload(
original_run=original_run,
candidate_operator_result=candidate_operator_result,
schema_versions=schema_versions,
policy_versions=policy_versions,
)
input_digest = _canonical_digest(input_payload)
binding_input = CandidateAttemptRunBindingInput(
input_digest=input_digest,
residual_ids=original_run.residual_ids,
schema_versions=schema_versions,
policy_versions=policy_versions,
**{
key: value
for key, value in input_payload.items()
if key not in {"residual_ids", "schema_versions", "policy_versions"}
},
)
candidate_attempt_ref = _canonical_digest(_candidate_attempt_payload(attempt))
binding_id = _canonical_digest(
_binding_id_payload(
input_digest=binding_input.input_digest,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
attempt_index=attempt.attempt_index,
candidate_digest=attempt.candidate_digest,
candidate_reconstruction_digest=reconstruction.candidate_reconstruction_digest,
candidate_attempt_ref=candidate_attempt_ref,
budget_charge=charge,
depth=attempt.depth,
step_index=attempt.step_index,
evidence_spans=attempt.evidence_spans,
)
)
return CandidateAttemptRunBinding(
binding_id=binding_id,
binding_policy_version=CANDIDATE_ATTEMPT_RUN_BINDING_POLICY_VERSION,
input_digest=binding_input.input_digest,
original_run_id=original_run.run_id,
operator_result_id=candidate_operator_result.operator_result_id,
candidate_attempt_id=attempt.attempt_id,
attempt_index=attempt.attempt_index,
candidate_digest=attempt.candidate_digest,
candidate_reconstruction_digest=reconstruction.candidate_reconstruction_digest,
candidate_attempt_ref=candidate_attempt_ref,
budget_charge=charge,
depth=attempt.depth,
step_index=attempt.step_index,
run_attempt_membership="structurally_bound",
evidence_spans=attempt.evidence_spans,
reason_codes=(),
explanation=explanation
or "CandidateAttempt structurally bound to GeometricSearchRun.",
)
__all__ = [
"CANDIDATE_ATTEMPT_RUN_BINDING_POLICY_VERSION",
"RunAttemptBindingRefusalReason",
"CandidateAttemptRunBindingInput",
"CandidateAttemptRunBinding",
"CandidateAttemptRunBindingRefusal",
"CandidateAttemptRunBindingOutcome",
"bind_candidate_attempt_to_run",
]