Merge pull request #883 from AssetOverflow/codex/quantity-entity-binding-candidate-operator
This commit is contained in:
commit
3fc080d3e2
6 changed files with 1055 additions and 8 deletions
|
|
@ -18,7 +18,7 @@ from generate.kernel_facts import SourceSpan
|
||||||
from generate.search_gate import SearchGateDecision, SearchGateStatus
|
from generate.search_gate import SearchGateDecision, SearchGateStatus
|
||||||
|
|
||||||
CANDIDATE_OPERATOR_POLICY_VERSION = "candidate_operator.v1"
|
CANDIDATE_OPERATOR_POLICY_VERSION = "candidate_operator.v1"
|
||||||
CANDIDATE_OPERATOR_SET_VERSION = "candidate_operators.v1"
|
CANDIDATE_OPERATOR_SET_VERSION = "candidate_operators.v2"
|
||||||
|
|
||||||
MISSING_ROLE_CANDIDATE_OPERATOR_NAME = "missing_role_candidate"
|
MISSING_ROLE_CANDIDATE_OPERATOR_NAME = "missing_role_candidate"
|
||||||
MISSING_ROLE_CANDIDATE_OPERATOR_VERSION = "missing_role_candidate.v1"
|
MISSING_ROLE_CANDIDATE_OPERATOR_VERSION = "missing_role_candidate.v1"
|
||||||
|
|
@ -27,6 +27,12 @@ MISSING_ROLE_RESIDUAL_KIND = "missing_role"
|
||||||
MISSING_ROLE_RESIDUAL_CODE = "direction_unbound"
|
MISSING_ROLE_RESIDUAL_CODE = "direction_unbound"
|
||||||
MISSING_ROLE_CANDIDATE_ORGAN = "unary_delta_transition"
|
MISSING_ROLE_CANDIDATE_ORGAN = "unary_delta_transition"
|
||||||
|
|
||||||
|
QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME = "quantity_entity_binding_candidate"
|
||||||
|
QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_VERSION = "quantity_entity_binding_candidate.v1"
|
||||||
|
QUANTITY_ENTITY_BINDING_RESIDUAL_KIND = "missing_relation"
|
||||||
|
QUANTITY_ENTITY_BINDING_RESIDUAL_CODE = "local_binding_relation_unbound"
|
||||||
|
QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN = "quantity_entity_binding"
|
||||||
|
|
||||||
_OPERATOR_FAMILY = "residual_missing_role"
|
_OPERATOR_FAMILY = "residual_missing_role"
|
||||||
_SUPPORTED_DIRECTIONS = frozenset({"increase", "decrease"})
|
_SUPPORTED_DIRECTIONS = frozenset({"increase", "decrease"})
|
||||||
_FIXED_BUDGET_CHARGE = BudgetCharge(candidates=1, steps=1)
|
_FIXED_BUDGET_CHARGE = BudgetCharge(candidates=1, steps=1)
|
||||||
|
|
@ -75,6 +81,13 @@ class CandidateOperatorRefusalReason(str, Enum):
|
||||||
MALFORMED_EVIDENCE_SPAN = "malformed_evidence_span"
|
MALFORMED_EVIDENCE_SPAN = "malformed_evidence_span"
|
||||||
INVALID_OPERATOR_INPUT = "invalid_operator_input"
|
INVALID_OPERATOR_INPUT = "invalid_operator_input"
|
||||||
UNSUPPORTED_SCHEMA_VERSION = "unsupported_schema_version"
|
UNSUPPORTED_SCHEMA_VERSION = "unsupported_schema_version"
|
||||||
|
INVALID_QUANTITY_ENTITY_CUE_TYPE = "invalid_quantity_entity_cue_type"
|
||||||
|
QUANTITY_ENTITY_CUE_COUNT_MISMATCH = "quantity_entity_cue_count_mismatch"
|
||||||
|
EMPTY_QUANTITY_MENTION_ID = "empty_quantity_mention_id"
|
||||||
|
EMPTY_ENTITY_MENTION_ID = "empty_entity_mention_id"
|
||||||
|
EMPTY_QUANTITY_KIND = "empty_quantity_kind"
|
||||||
|
EMPTY_UNIT_MENTION_ID = "empty_unit_mention_id"
|
||||||
|
MISSING_EVIDENCE_SPANS = "missing_evidence_spans"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|
@ -111,12 +124,38 @@ MISSING_ROLE_OPERATOR_POLICY = CandidateOperatorPolicy(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
QUANTITY_ENTITY_BINDING_OPERATOR_POLICY = CandidateOperatorPolicy(
|
||||||
|
operator_policy_version=CANDIDATE_OPERATOR_POLICY_VERSION,
|
||||||
|
operator_family="residual_missing_relation",
|
||||||
|
operator_name=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME,
|
||||||
|
operator_version=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_VERSION,
|
||||||
|
allowed_residual_kinds=(QUANTITY_ENTITY_BINDING_RESIDUAL_KIND,),
|
||||||
|
allowed_residual_codes=(QUANTITY_ENTITY_BINDING_RESIDUAL_CODE,),
|
||||||
|
allowed_candidate_organs=(QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN,),
|
||||||
|
max_attempts_per_run=1,
|
||||||
|
budget_charge=_FIXED_BUDGET_CHARGE,
|
||||||
|
depth=_OPERATOR_DEPTH,
|
||||||
|
max_parallelism=_OPERATOR_MAX_PARALLELISM,
|
||||||
|
determinism_requirements=_DETERMINISM_REQUIREMENTS,
|
||||||
|
forbidden_authority_paths=_FORBIDDEN_AUTHORITY_PATHS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class GroundedUnaryDeltaCue:
|
class GroundedUnaryDeltaCue:
|
||||||
direction: str
|
direction: str
|
||||||
evidence_spans: tuple[SourceSpan, ...]
|
evidence_spans: tuple[SourceSpan, ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class GroundedQuantityEntityCue:
|
||||||
|
quantity_mention_id: str
|
||||||
|
entity_mention_id: str
|
||||||
|
quantity_kind: str
|
||||||
|
evidence_spans: tuple[SourceSpan, ...]
|
||||||
|
unit_mention_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class CandidateOperatorInput:
|
class CandidateOperatorInput:
|
||||||
input_digest: str
|
input_digest: str
|
||||||
|
|
@ -300,7 +339,28 @@ def _operator_set_table_payload() -> dict[str, object]:
|
||||||
},
|
},
|
||||||
"depth": MISSING_ROLE_OPERATOR_POLICY.depth,
|
"depth": MISSING_ROLE_OPERATOR_POLICY.depth,
|
||||||
"max_parallelism": MISSING_ROLE_OPERATOR_POLICY.max_parallelism,
|
"max_parallelism": MISSING_ROLE_OPERATOR_POLICY.max_parallelism,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"operator_family": QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.operator_family,
|
||||||
|
"operator_name": QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.operator_name,
|
||||||
|
"operator_version": QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.operator_version,
|
||||||
|
"allowed_residual_kinds": list(
|
||||||
|
QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.allowed_residual_kinds
|
||||||
|
),
|
||||||
|
"allowed_residual_codes": list(
|
||||||
|
QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.allowed_residual_codes
|
||||||
|
),
|
||||||
|
"allowed_candidate_organs": list(
|
||||||
|
QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.allowed_candidate_organs
|
||||||
|
),
|
||||||
|
"max_attempts_per_run": QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.max_attempts_per_run,
|
||||||
|
"budget_charge": {
|
||||||
|
"candidates": QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.budget_charge.candidates,
|
||||||
|
"steps": QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.budget_charge.steps,
|
||||||
|
},
|
||||||
|
"depth": QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.depth,
|
||||||
|
"max_parallelism": QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.max_parallelism,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"schema_versions": [],
|
"schema_versions": [],
|
||||||
"policy_versions": [],
|
"policy_versions": [],
|
||||||
|
|
@ -323,6 +383,22 @@ def _candidate_payload(direction: str) -> tuple[tuple[str, str], ...]:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _quantity_entity_binding_candidate_payload(
|
||||||
|
cue: GroundedQuantityEntityCue,
|
||||||
|
) -> tuple[tuple[str, str], ...]:
|
||||||
|
return (
|
||||||
|
("binding_type", "quantity_entity"),
|
||||||
|
("candidate_organ", QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN),
|
||||||
|
("entity_mention_id", cue.entity_mention_id),
|
||||||
|
("kind", "mention_binding"),
|
||||||
|
("quantity_kind", cue.quantity_kind),
|
||||||
|
("quantity_mention_id", cue.quantity_mention_id),
|
||||||
|
("relation_type", "quantity_entity"),
|
||||||
|
("source", "GroundedQuantityEntityCue"),
|
||||||
|
("unit_mention_id", cue.unit_mention_id or ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _candidate_payload_dict(
|
def _candidate_payload_dict(
|
||||||
payload: tuple[tuple[str, str], ...],
|
payload: tuple[tuple[str, str], ...],
|
||||||
) -> list[list[str]]:
|
) -> list[list[str]]:
|
||||||
|
|
@ -925,6 +1001,400 @@ def build_missing_role_candidate(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_quantity_entity_binding_candidate(
|
||||||
|
*,
|
||||||
|
residual: object,
|
||||||
|
search_gate: SearchGateDecision,
|
||||||
|
compute_budget: ComputeBudgetDecision,
|
||||||
|
run: GeometricSearchRun,
|
||||||
|
problem_frame_digest: str,
|
||||||
|
original_contract_assessment_id: str,
|
||||||
|
grounded_quantity_entity_cues: tuple[GroundedQuantityEntityCue, ...],
|
||||||
|
attempt_index: int = 0,
|
||||||
|
schema_versions: tuple[tuple[str, str], ...] = (),
|
||||||
|
policy_versions: tuple[tuple[str, str], ...] = (),
|
||||||
|
explanation: str = "",
|
||||||
|
) -> CandidateOperatorOutcome:
|
||||||
|
"""Construct one quantity-entity binding candidate or return a typed refusal."""
|
||||||
|
|
||||||
|
operator_set_id = candidate_operator_set_id()
|
||||||
|
reasons: list[str] = []
|
||||||
|
|
||||||
|
residual_id = _safe_getattr(residual, "residual_id")
|
||||||
|
residual_kind = _enum_value(_safe_getattr(residual, "residual_kind"))
|
||||||
|
residual_code = _safe_getattr(residual, "residual_code")
|
||||||
|
candidate_organ = _safe_getattr(residual, "candidate_organ")
|
||||||
|
residual_spans = _safe_getattr(residual, "evidence_spans")
|
||||||
|
|
||||||
|
gate_id = _safe_getattr(search_gate, "decision_id")
|
||||||
|
gate_status = _safe_getattr(search_gate, "status")
|
||||||
|
gate_reason = _safe_getattr(search_gate, "reason_code")
|
||||||
|
gate_residual_ids = _safe_getattr(search_gate, "residual_ids")
|
||||||
|
gate_candidate_organ = _safe_getattr(search_gate, "candidate_organ")
|
||||||
|
|
||||||
|
budget_id = _safe_getattr(compute_budget, "budget_id")
|
||||||
|
budget_status = _safe_getattr(compute_budget, "status")
|
||||||
|
budget_reason = _safe_getattr(compute_budget, "reason_code")
|
||||||
|
budget_gate_id = _safe_getattr(compute_budget, "gate_decision_id")
|
||||||
|
max_candidates = _safe_getattr(compute_budget, "max_candidates")
|
||||||
|
max_depth = _safe_getattr(compute_budget, "max_depth")
|
||||||
|
max_steps = _safe_getattr(compute_budget, "max_steps")
|
||||||
|
max_parallelism = _safe_getattr(compute_budget, "max_parallelism")
|
||||||
|
|
||||||
|
run_id = _safe_getattr(run, "run_id")
|
||||||
|
run_gate_id = _safe_getattr(run, "gate_decision_id")
|
||||||
|
run_budget_id = _safe_getattr(run, "budget_id")
|
||||||
|
run_operator_set_id = _safe_getattr(run, "operator_set_id")
|
||||||
|
run_operator_set_version = _safe_getattr(run, "operator_set_version")
|
||||||
|
run_problem_frame_digest = _safe_getattr(run, "problem_frame_digest")
|
||||||
|
run_assessment_id = _safe_getattr(run, "contract_assessment_id")
|
||||||
|
run_residual_ids = _safe_getattr(run, "residual_ids")
|
||||||
|
|
||||||
|
if not _valid_residual_record(residual):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
if not isinstance(search_gate, SearchGateDecision):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
if not isinstance(compute_budget, ComputeBudgetDecision):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
if not isinstance(run, GeometricSearchRun):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
|
||||||
|
if not _nonempty_text(residual_id):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
if residual_kind != QUANTITY_ENTITY_BINDING_RESIDUAL_KIND:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.UNSUPPORTED_RESIDUAL_KIND.value)
|
||||||
|
if residual_code != QUANTITY_ENTITY_BINDING_RESIDUAL_CODE:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.UNSUPPORTED_RESIDUAL_CODE.value)
|
||||||
|
if candidate_organ != QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.UNSUPPORTED_CANDIDATE_ORGAN.value)
|
||||||
|
if not _valid_spans(residual_spans):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.MALFORMED_EVIDENCE_SPAN.value)
|
||||||
|
|
||||||
|
if gate_status is not SearchGateStatus.ELIGIBLE:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INELIGIBLE_SEARCH_GATE.value)
|
||||||
|
if gate_reason != "eligible_missing_relation":
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INELIGIBLE_SEARCH_GATE.value)
|
||||||
|
if gate_candidate_organ != QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.UNSUPPORTED_CANDIDATE_ORGAN.value)
|
||||||
|
|
||||||
|
if budget_status is not ComputeBudgetStatus.BUDGET_ALLOWED:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.NON_ALLOWED_BUDGET.value)
|
||||||
|
if budget_reason != "budget_allowed_missing_relation":
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.NON_ALLOWED_BUDGET.value)
|
||||||
|
|
||||||
|
if not _nonempty_text(budget_gate_id) or not _nonempty_text(gate_id):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.GATE_BUDGET_MISMATCH.value)
|
||||||
|
elif budget_gate_id != gate_id:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.GATE_BUDGET_MISMATCH.value)
|
||||||
|
|
||||||
|
if not _nonempty_text(run_gate_id) or not _nonempty_text(gate_id):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.RUN_GATE_MISMATCH.value)
|
||||||
|
elif run_gate_id != gate_id:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.RUN_GATE_MISMATCH.value)
|
||||||
|
|
||||||
|
if not _nonempty_text(run_budget_id) or not _nonempty_text(budget_id):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.RUN_BUDGET_MISMATCH.value)
|
||||||
|
elif run_budget_id != budget_id:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.RUN_BUDGET_MISMATCH.value)
|
||||||
|
|
||||||
|
if run_operator_set_id != operator_set_id:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.OPERATOR_SET_MISMATCH.value)
|
||||||
|
if run_operator_set_version != CANDIDATE_OPERATOR_SET_VERSION:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.OPERATOR_SET_MISMATCH.value)
|
||||||
|
|
||||||
|
if type(attempt_index) is not int or attempt_index < 0:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
elif attempt_index != 0:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.ATTEMPT_INDEX_EXCEEDS_BUDGET.value)
|
||||||
|
|
||||||
|
if type(max_candidates) is not int:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
elif max_candidates < 1:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.BUDGET_CHARGE_EXCEEDS_BUDGET.value)
|
||||||
|
|
||||||
|
if attempt_index >= QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.max_attempts_per_run:
|
||||||
|
reasons.append(
|
||||||
|
CandidateOperatorRefusalReason.ATTEMPT_INDEX_EXCEEDS_OPERATOR_POLICY.value
|
||||||
|
)
|
||||||
|
|
||||||
|
if type(max_depth) is not int or type(max_steps) is not int:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
else:
|
||||||
|
charge = QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.budget_charge
|
||||||
|
if charge.candidates > max_candidates:
|
||||||
|
reasons.append(
|
||||||
|
CandidateOperatorRefusalReason.BUDGET_CHARGE_EXCEEDS_BUDGET.value
|
||||||
|
)
|
||||||
|
if charge.steps > max_steps:
|
||||||
|
reasons.append(
|
||||||
|
CandidateOperatorRefusalReason.BUDGET_CHARGE_EXCEEDS_BUDGET.value
|
||||||
|
)
|
||||||
|
if QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.depth > max_depth:
|
||||||
|
reasons.append(
|
||||||
|
CandidateOperatorRefusalReason.BUDGET_CHARGE_EXCEEDS_BUDGET.value
|
||||||
|
)
|
||||||
|
|
||||||
|
if type(max_parallelism) is not int or max_parallelism != 1:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.NON_SERIAL_BUDGET.value)
|
||||||
|
|
||||||
|
if not _valid_version_pairs(schema_versions):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.UNSUPPORTED_SCHEMA_VERSION.value)
|
||||||
|
if not _valid_version_pairs(policy_versions):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.UNSUPPORTED_SCHEMA_VERSION.value)
|
||||||
|
|
||||||
|
if not _sha256_text(problem_frame_digest):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
if not _nonempty_text(original_contract_assessment_id):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
if not _sha256_text(run_id):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
|
||||||
|
if (
|
||||||
|
isinstance(residual_id, str)
|
||||||
|
and isinstance(gate_residual_ids, tuple)
|
||||||
|
and residual_id not in gate_residual_ids
|
||||||
|
):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
if (
|
||||||
|
isinstance(residual_id, str)
|
||||||
|
and isinstance(run_residual_ids, tuple)
|
||||||
|
and residual_id not in run_residual_ids
|
||||||
|
):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
if (
|
||||||
|
isinstance(problem_frame_digest, str)
|
||||||
|
and isinstance(run_problem_frame_digest, str)
|
||||||
|
and problem_frame_digest != run_problem_frame_digest
|
||||||
|
):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
if (
|
||||||
|
isinstance(original_contract_assessment_id, str)
|
||||||
|
and isinstance(run_assessment_id, str)
|
||||||
|
and original_contract_assessment_id != run_assessment_id
|
||||||
|
):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value)
|
||||||
|
|
||||||
|
if not isinstance(grounded_quantity_entity_cues, tuple):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_QUANTITY_ENTITY_CUE_TYPE.value)
|
||||||
|
elif len(grounded_quantity_entity_cues) == 0:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.MISSING_TYPED_CUE.value)
|
||||||
|
elif len(grounded_quantity_entity_cues) > 1:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.QUANTITY_ENTITY_CUE_COUNT_MISMATCH.value)
|
||||||
|
|
||||||
|
cue: GroundedQuantityEntityCue | None = None
|
||||||
|
if isinstance(grounded_quantity_entity_cues, tuple) and len(grounded_quantity_entity_cues) == 1:
|
||||||
|
cue = grounded_quantity_entity_cues[0]
|
||||||
|
if not isinstance(cue, GroundedQuantityEntityCue):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.INVALID_QUANTITY_ENTITY_CUE_TYPE.value)
|
||||||
|
else:
|
||||||
|
if not _nonempty_text(cue.quantity_mention_id):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.EMPTY_QUANTITY_MENTION_ID.value)
|
||||||
|
if not _nonempty_text(cue.entity_mention_id):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.EMPTY_ENTITY_MENTION_ID.value)
|
||||||
|
if not _nonempty_text(cue.quantity_kind):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.EMPTY_QUANTITY_KIND.value)
|
||||||
|
if cue.unit_mention_id is not None and not _nonempty_text(cue.unit_mention_id):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.EMPTY_UNIT_MENTION_ID.value)
|
||||||
|
|
||||||
|
if not _valid_spans(cue.evidence_spans):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.MALFORMED_EVIDENCE_SPAN.value)
|
||||||
|
elif not cue.evidence_spans:
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.MISSING_EVIDENCE_SPANS.value)
|
||||||
|
elif isinstance(residual_spans, tuple) and not _spans_grounded_in_residual(
|
||||||
|
cue.evidence_spans, residual_spans
|
||||||
|
):
|
||||||
|
reasons.append(CandidateOperatorRefusalReason.MALFORMED_EVIDENCE_SPAN.value)
|
||||||
|
|
||||||
|
input_digest: str | None = None
|
||||||
|
if (
|
||||||
|
not reasons
|
||||||
|
and isinstance(residual_id, str)
|
||||||
|
and isinstance(run_id, str)
|
||||||
|
and isinstance(gate_id, str)
|
||||||
|
and isinstance(budget_id, str)
|
||||||
|
and type(attempt_index) is int
|
||||||
|
):
|
||||||
|
input_digest = _canonical_digest(
|
||||||
|
_input_digest_payload(
|
||||||
|
operator_policy_version=CANDIDATE_OPERATOR_POLICY_VERSION,
|
||||||
|
operator_name=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME,
|
||||||
|
operator_version=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_VERSION,
|
||||||
|
problem_frame_digest=problem_frame_digest,
|
||||||
|
original_contract_assessment_id=original_contract_assessment_id,
|
||||||
|
residual_id=residual_id,
|
||||||
|
residual_kind=QUANTITY_ENTITY_BINDING_RESIDUAL_KIND,
|
||||||
|
residual_code=QUANTITY_ENTITY_BINDING_RESIDUAL_CODE,
|
||||||
|
candidate_organ=QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN,
|
||||||
|
search_gate_decision_id=gate_id,
|
||||||
|
compute_budget_id=budget_id,
|
||||||
|
geometric_search_run_id=run_id,
|
||||||
|
operator_set_id=operator_set_id,
|
||||||
|
operator_set_version=CANDIDATE_OPERATOR_SET_VERSION,
|
||||||
|
attempt_index=attempt_index,
|
||||||
|
schema_versions=schema_versions,
|
||||||
|
policy_versions=policy_versions,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if reasons:
|
||||||
|
operator_refusal_id = _canonical_digest(
|
||||||
|
_operator_refusal_id_payload(
|
||||||
|
operator_policy_version=CANDIDATE_OPERATOR_POLICY_VERSION,
|
||||||
|
input_digest=input_digest,
|
||||||
|
geometric_search_run_id=_text_or_none(run_id),
|
||||||
|
residual_id=_text_or_none(residual_id),
|
||||||
|
operator_name=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME,
|
||||||
|
reason_codes=tuple(dict.fromkeys(reasons)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return CandidateOperatorRefusal(
|
||||||
|
operator_refusal_id=operator_refusal_id,
|
||||||
|
operator_policy_version=CANDIDATE_OPERATOR_POLICY_VERSION,
|
||||||
|
input_digest=input_digest,
|
||||||
|
geometric_search_run_id=_text_or_none(run_id),
|
||||||
|
residual_id=_text_or_none(residual_id),
|
||||||
|
operator_name=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME,
|
||||||
|
reason_codes=tuple(dict.fromkeys(reasons)),
|
||||||
|
explanation="Candidate operator refused: " + ", ".join(dict.fromkeys(reasons)) + ".",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert cue is not None
|
||||||
|
assert input_digest is not None
|
||||||
|
assert isinstance(residual_id, str)
|
||||||
|
assert isinstance(run_id, str)
|
||||||
|
|
||||||
|
evidence_spans = cue.evidence_spans
|
||||||
|
payload = _quantity_entity_binding_candidate_payload(cue)
|
||||||
|
candidate_digest = _canonical_digest(
|
||||||
|
_candidate_digest_payload(
|
||||||
|
problem_frame_digest=problem_frame_digest,
|
||||||
|
candidate_organ=QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN,
|
||||||
|
candidate_payload=payload,
|
||||||
|
evidence_spans=evidence_spans,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
attempt_id = _canonical_digest(
|
||||||
|
_attempt_id_payload(
|
||||||
|
attempt_index=attempt_index,
|
||||||
|
parent_attempt_id=None,
|
||||||
|
operator_id=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME,
|
||||||
|
operator_version=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_VERSION,
|
||||||
|
input_digest=input_digest,
|
||||||
|
candidate_digest=candidate_digest,
|
||||||
|
budget_charge=_FIXED_BUDGET_CHARGE,
|
||||||
|
depth=_OPERATOR_DEPTH,
|
||||||
|
step_index=attempt_index,
|
||||||
|
replay_status=CandidateReplayStatus.REPLAY_PENDING,
|
||||||
|
replay_blockers=(),
|
||||||
|
evidence_spans=evidence_spans,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
candidate_reconstruction_digest = _canonical_digest(
|
||||||
|
_reconstruction_digest_payload(
|
||||||
|
geometric_search_run_id=run_id,
|
||||||
|
attempt_id=attempt_id,
|
||||||
|
attempt_index=attempt_index,
|
||||||
|
operator_set_id=operator_set_id,
|
||||||
|
operator_set_version=CANDIDATE_OPERATOR_SET_VERSION,
|
||||||
|
operator_name=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME,
|
||||||
|
operator_version=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_VERSION,
|
||||||
|
residual_id=residual_id,
|
||||||
|
residual_kind=QUANTITY_ENTITY_BINDING_RESIDUAL_KIND,
|
||||||
|
residual_code=QUANTITY_ENTITY_BINDING_RESIDUAL_CODE,
|
||||||
|
candidate_digest=candidate_digest,
|
||||||
|
evidence_spans=evidence_spans,
|
||||||
|
schema_versions=schema_versions,
|
||||||
|
policy_versions=policy_versions,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
operator_provenance = (
|
||||||
|
("operator_family", QUANTITY_ENTITY_BINDING_OPERATOR_POLICY.operator_family),
|
||||||
|
("operator_name", QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME),
|
||||||
|
("operator_version", QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_VERSION),
|
||||||
|
("operator_set_id", operator_set_id),
|
||||||
|
("operator_set_version", CANDIDATE_OPERATOR_SET_VERSION),
|
||||||
|
)
|
||||||
|
|
||||||
|
candidate_reconstruction = CandidateReconstruction(
|
||||||
|
candidate_digest=candidate_digest,
|
||||||
|
candidate_reconstruction_digest=candidate_reconstruction_digest,
|
||||||
|
candidate_organ=QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN,
|
||||||
|
candidate_payload=payload,
|
||||||
|
evidence_spans=evidence_spans,
|
||||||
|
operator_name=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME,
|
||||||
|
operator_version=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_VERSION,
|
||||||
|
operator_provenance=operator_provenance,
|
||||||
|
source_residual_id=residual_id,
|
||||||
|
problem_frame_digest=problem_frame_digest,
|
||||||
|
original_contract_assessment_id=original_contract_assessment_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
candidate_attempt = CandidateAttempt(
|
||||||
|
attempt_id=attempt_id,
|
||||||
|
attempt_index=attempt_index,
|
||||||
|
parent_attempt_id=None,
|
||||||
|
operator_id=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME,
|
||||||
|
operator_version=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_VERSION,
|
||||||
|
input_digest=input_digest,
|
||||||
|
candidate_digest=candidate_digest,
|
||||||
|
budget_charge=_FIXED_BUDGET_CHARGE,
|
||||||
|
depth=_OPERATOR_DEPTH,
|
||||||
|
step_index=attempt_index,
|
||||||
|
replay_status=CandidateReplayStatus.REPLAY_PENDING,
|
||||||
|
replay_blockers=(),
|
||||||
|
evidence_spans=evidence_spans,
|
||||||
|
explanation="",
|
||||||
|
)
|
||||||
|
|
||||||
|
reason_codes: tuple[str, ...] = ()
|
||||||
|
operator_result_id = _canonical_digest(
|
||||||
|
_operator_result_id_payload(
|
||||||
|
operator_policy_version=CANDIDATE_OPERATOR_POLICY_VERSION,
|
||||||
|
input_digest=input_digest,
|
||||||
|
geometric_search_run_id=run_id,
|
||||||
|
attempt_id=attempt_id,
|
||||||
|
attempt_index=attempt_index,
|
||||||
|
candidate_digest=candidate_digest,
|
||||||
|
candidate_reconstruction_digest=candidate_reconstruction_digest,
|
||||||
|
candidate_organ=QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN,
|
||||||
|
operator_name=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME,
|
||||||
|
operator_version=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_VERSION,
|
||||||
|
reason_codes=reason_codes,
|
||||||
|
evidence_spans=evidence_spans,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
safe_explanation = explanation or (
|
||||||
|
"Quantity-entity binding candidate constructed for residual "
|
||||||
|
+ residual_id
|
||||||
|
+ " linking "
|
||||||
|
+ cue.quantity_mention_id
|
||||||
|
+ " and "
|
||||||
|
+ cue.entity_mention_id
|
||||||
|
+ "."
|
||||||
|
)
|
||||||
|
return CandidateOperatorResult(
|
||||||
|
operator_result_id=operator_result_id,
|
||||||
|
operator_policy_version=CANDIDATE_OPERATOR_POLICY_VERSION,
|
||||||
|
input_digest=input_digest,
|
||||||
|
geometric_search_run_id=run_id,
|
||||||
|
attempt_id=attempt_id,
|
||||||
|
attempt_index=attempt_index,
|
||||||
|
candidate_digest=candidate_digest,
|
||||||
|
candidate_reconstruction_digest=candidate_reconstruction_digest,
|
||||||
|
candidate_organ=QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN,
|
||||||
|
operator_name=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME,
|
||||||
|
operator_version=QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_VERSION,
|
||||||
|
candidate_attempt=candidate_attempt,
|
||||||
|
candidate_reconstruction=candidate_reconstruction,
|
||||||
|
reason_codes=reason_codes,
|
||||||
|
evidence_spans=evidence_spans,
|
||||||
|
explanation=safe_explanation,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CANDIDATE_OPERATOR_POLICY_VERSION",
|
"CANDIDATE_OPERATOR_POLICY_VERSION",
|
||||||
"CANDIDATE_OPERATOR_SET_VERSION",
|
"CANDIDATE_OPERATOR_SET_VERSION",
|
||||||
|
|
@ -933,9 +1403,15 @@ __all__ = [
|
||||||
"MISSING_ROLE_RESIDUAL_KIND",
|
"MISSING_ROLE_RESIDUAL_KIND",
|
||||||
"MISSING_ROLE_RESIDUAL_CODE",
|
"MISSING_ROLE_RESIDUAL_CODE",
|
||||||
"MISSING_ROLE_CANDIDATE_ORGAN",
|
"MISSING_ROLE_CANDIDATE_ORGAN",
|
||||||
|
"QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME",
|
||||||
|
"QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_VERSION",
|
||||||
|
"QUANTITY_ENTITY_BINDING_RESIDUAL_KIND",
|
||||||
|
"QUANTITY_ENTITY_BINDING_RESIDUAL_CODE",
|
||||||
|
"QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN",
|
||||||
"CandidateOperatorRefusalReason",
|
"CandidateOperatorRefusalReason",
|
||||||
"CandidateOperatorPolicy",
|
"CandidateOperatorPolicy",
|
||||||
"GroundedUnaryDeltaCue",
|
"GroundedUnaryDeltaCue",
|
||||||
|
"GroundedQuantityEntityCue",
|
||||||
"CandidateOperatorInput",
|
"CandidateOperatorInput",
|
||||||
"CandidateReconstruction",
|
"CandidateReconstruction",
|
||||||
"CandidateOperatorResult",
|
"CandidateOperatorResult",
|
||||||
|
|
@ -943,4 +1419,5 @@ __all__ = [
|
||||||
"CandidateOperatorOutcome",
|
"CandidateOperatorOutcome",
|
||||||
"candidate_operator_set_id",
|
"candidate_operator_set_id",
|
||||||
"build_missing_role_candidate",
|
"build_missing_role_candidate",
|
||||||
]
|
"build_quantity_entity_binding_candidate",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,19 @@ def _expected_operator_set_id() -> str:
|
||||||
"budget_charge": {"candidates": 1, "steps": 1},
|
"budget_charge": {"candidates": 1, "steps": 1},
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
"max_parallelism": 1,
|
"max_parallelism": 1,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"operator_family": "residual_missing_relation",
|
||||||
|
"operator_name": "quantity_entity_binding_candidate",
|
||||||
|
"operator_version": "quantity_entity_binding_candidate.v1",
|
||||||
|
"allowed_residual_kinds": ["missing_relation"],
|
||||||
|
"allowed_residual_codes": ["local_binding_relation_unbound"],
|
||||||
|
"allowed_candidate_organs": ["quantity_entity_binding"],
|
||||||
|
"max_attempts_per_run": 1,
|
||||||
|
"budget_charge": {"candidates": 1, "steps": 1},
|
||||||
|
"depth": 1,
|
||||||
|
"max_parallelism": 1,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
"schema_versions": [],
|
"schema_versions": [],
|
||||||
"policy_versions": [],
|
"policy_versions": [],
|
||||||
|
|
@ -412,7 +424,7 @@ def test_public_api_exports_are_exact() -> None:
|
||||||
import generate.candidate_operator as candidate_operator
|
import generate.candidate_operator as candidate_operator
|
||||||
|
|
||||||
assert CANDIDATE_OPERATOR_POLICY_VERSION == "candidate_operator.v1"
|
assert CANDIDATE_OPERATOR_POLICY_VERSION == "candidate_operator.v1"
|
||||||
assert CANDIDATE_OPERATOR_SET_VERSION == "candidate_operators.v1"
|
assert CANDIDATE_OPERATOR_SET_VERSION == "candidate_operators.v2"
|
||||||
assert MISSING_ROLE_CANDIDATE_OPERATOR_NAME == "missing_role_candidate"
|
assert MISSING_ROLE_CANDIDATE_OPERATOR_NAME == "missing_role_candidate"
|
||||||
assert MISSING_ROLE_CANDIDATE_OPERATOR_VERSION == "missing_role_candidate.v1"
|
assert MISSING_ROLE_CANDIDATE_OPERATOR_VERSION == "missing_role_candidate.v1"
|
||||||
assert MISSING_ROLE_RESIDUAL_KIND == "missing_role"
|
assert MISSING_ROLE_RESIDUAL_KIND == "missing_role"
|
||||||
|
|
@ -426,9 +438,15 @@ def test_public_api_exports_are_exact() -> None:
|
||||||
"MISSING_ROLE_RESIDUAL_KIND",
|
"MISSING_ROLE_RESIDUAL_KIND",
|
||||||
"MISSING_ROLE_RESIDUAL_CODE",
|
"MISSING_ROLE_RESIDUAL_CODE",
|
||||||
"MISSING_ROLE_CANDIDATE_ORGAN",
|
"MISSING_ROLE_CANDIDATE_ORGAN",
|
||||||
|
"QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME",
|
||||||
|
"QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_VERSION",
|
||||||
|
"QUANTITY_ENTITY_BINDING_RESIDUAL_KIND",
|
||||||
|
"QUANTITY_ENTITY_BINDING_RESIDUAL_CODE",
|
||||||
|
"QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN",
|
||||||
"CandidateOperatorRefusalReason",
|
"CandidateOperatorRefusalReason",
|
||||||
"CandidateOperatorPolicy",
|
"CandidateOperatorPolicy",
|
||||||
"GroundedUnaryDeltaCue",
|
"GroundedUnaryDeltaCue",
|
||||||
|
"GroundedQuantityEntityCue",
|
||||||
"CandidateOperatorInput",
|
"CandidateOperatorInput",
|
||||||
"CandidateReconstruction",
|
"CandidateReconstruction",
|
||||||
"CandidateOperatorResult",
|
"CandidateOperatorResult",
|
||||||
|
|
@ -436,6 +454,7 @@ def test_public_api_exports_are_exact() -> None:
|
||||||
"CandidateOperatorOutcome",
|
"CandidateOperatorOutcome",
|
||||||
"candidate_operator_set_id",
|
"candidate_operator_set_id",
|
||||||
"build_missing_role_candidate",
|
"build_missing_role_candidate",
|
||||||
|
"build_quantity_entity_binding_candidate",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
551
tests/test_candidate_operator_quantity_entity_binding.py
Normal file
551
tests/test_candidate_operator_quantity_entity_binding.py
Normal file
|
|
@ -0,0 +1,551 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from generate.candidate_operator import (
|
||||||
|
CANDIDATE_OPERATOR_SET_VERSION,
|
||||||
|
QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME,
|
||||||
|
QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_VERSION,
|
||||||
|
QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN,
|
||||||
|
QUANTITY_ENTITY_BINDING_RESIDUAL_CODE,
|
||||||
|
CandidateOperatorRefusal,
|
||||||
|
CandidateOperatorRefusalReason,
|
||||||
|
CandidateOperatorResult,
|
||||||
|
GroundedQuantityEntityCue,
|
||||||
|
build_quantity_entity_binding_candidate,
|
||||||
|
candidate_operator_set_id,
|
||||||
|
)
|
||||||
|
from generate.compute_budget import ComputeBudgetDecision, ComputeBudgetStatus
|
||||||
|
from generate.contract_residuals import ContractResidual, ResidualKind, ResidualSourceAxis
|
||||||
|
from generate.geometric_search_run import (
|
||||||
|
BudgetCharge,
|
||||||
|
CandidateReplayStatus,
|
||||||
|
GeometricSearchRun,
|
||||||
|
initialize_geometric_search_run,
|
||||||
|
)
|
||||||
|
from generate.kernel_facts import SourceSpan
|
||||||
|
from generate.search_gate import SearchGateDecision, SearchGateStatus
|
||||||
|
from generate.run_attempt_binding import bind_candidate_attempt_to_run
|
||||||
|
from generate.replay_adapter import build_replay_adapter_input_from_binding, ReplayAdapterRefusal
|
||||||
|
|
||||||
|
|
||||||
|
def _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 _span(text: str = "5 apples", start: int = 10, end: int = 18) -> SourceSpan:
|
||||||
|
return SourceSpan(text=text, start=start, end=end, sentence_index=0)
|
||||||
|
|
||||||
|
|
||||||
|
def _residual(
|
||||||
|
*,
|
||||||
|
residual_id: str | None = None,
|
||||||
|
evidence_spans: tuple[SourceSpan, ...] | None = None,
|
||||||
|
residual_kind: ResidualKind = ResidualKind.MISSING_RELATION,
|
||||||
|
residual_code: str = QUANTITY_ENTITY_BINDING_RESIDUAL_CODE,
|
||||||
|
candidate_organ: str = QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN,
|
||||||
|
) -> ContractResidual:
|
||||||
|
spans = evidence_spans if evidence_spans is not None else (_span(),)
|
||||||
|
if residual_id is None:
|
||||||
|
payload = {
|
||||||
|
"candidate_organ": candidate_organ,
|
||||||
|
"residual_kind": residual_kind.value,
|
||||||
|
"residual_code": residual_code,
|
||||||
|
"evidence_spans": [_span_payload(span) for span in spans],
|
||||||
|
}
|
||||||
|
residual_id = _digest(payload)
|
||||||
|
return ContractResidual(
|
||||||
|
residual_id=residual_id,
|
||||||
|
candidate_organ=candidate_organ,
|
||||||
|
family_id="state_change.quantity_entity",
|
||||||
|
residual_kind=residual_kind,
|
||||||
|
residual_code=residual_code,
|
||||||
|
source_axis=ResidualSourceAxis.ROLE,
|
||||||
|
evidence_spans=spans,
|
||||||
|
explanation="residual prose",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _gate(
|
||||||
|
residual: ContractResidual,
|
||||||
|
*,
|
||||||
|
status: SearchGateStatus = SearchGateStatus.ELIGIBLE,
|
||||||
|
reason_code: str = "eligible_missing_relation",
|
||||||
|
input_digest: str = "a" * 64,
|
||||||
|
) -> SearchGateDecision:
|
||||||
|
payload = {
|
||||||
|
"policy_version": "search_gate.v1",
|
||||||
|
"input_digest": input_digest,
|
||||||
|
"residual_ids": [residual.residual_id],
|
||||||
|
"candidate_organ": residual.candidate_organ,
|
||||||
|
"status": status.value,
|
||||||
|
"reason_code": reason_code,
|
||||||
|
"evidence_spans": [_span_payload(span) for span in residual.evidence_spans],
|
||||||
|
}
|
||||||
|
return SearchGateDecision(
|
||||||
|
decision_id=_digest(payload),
|
||||||
|
policy_version="search_gate.v1",
|
||||||
|
input_digest=input_digest,
|
||||||
|
residual_ids=(residual.residual_id,),
|
||||||
|
candidate_organ=residual.candidate_organ,
|
||||||
|
status=status,
|
||||||
|
reason_code=reason_code,
|
||||||
|
evidence_spans=residual.evidence_spans,
|
||||||
|
explanation="gate prose",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _budget(
|
||||||
|
gate: SearchGateDecision,
|
||||||
|
*,
|
||||||
|
status: ComputeBudgetStatus = ComputeBudgetStatus.BUDGET_ALLOWED,
|
||||||
|
reason_code: str = "budget_allowed_missing_relation",
|
||||||
|
max_candidates: int = 5,
|
||||||
|
max_depth: int = 2,
|
||||||
|
max_steps: int = 10,
|
||||||
|
max_parallelism: int = 1,
|
||||||
|
) -> ComputeBudgetDecision:
|
||||||
|
payload = {
|
||||||
|
"policy_version": "compute_budget.v1",
|
||||||
|
"gate_decision_id": gate.decision_id,
|
||||||
|
"gate_policy_version": gate.policy_version,
|
||||||
|
"gate_input_digest": gate.input_digest,
|
||||||
|
"status": status.value,
|
||||||
|
"reason_code": reason_code,
|
||||||
|
"max_candidates": max_candidates,
|
||||||
|
"max_depth": max_depth,
|
||||||
|
"max_steps": max_steps,
|
||||||
|
"max_parallelism": max_parallelism,
|
||||||
|
"evidence_spans": [_span_payload(span) for span in gate.evidence_spans],
|
||||||
|
}
|
||||||
|
return ComputeBudgetDecision(
|
||||||
|
budget_id=_digest(payload),
|
||||||
|
policy_version="compute_budget.v1",
|
||||||
|
gate_decision_id=gate.decision_id,
|
||||||
|
gate_policy_version=gate.policy_version,
|
||||||
|
gate_input_digest=gate.input_digest,
|
||||||
|
status=status,
|
||||||
|
reason_code=reason_code,
|
||||||
|
max_candidates=max_candidates,
|
||||||
|
max_depth=max_depth,
|
||||||
|
max_steps=max_steps,
|
||||||
|
max_wallclock_ms=None,
|
||||||
|
max_parallelism=max_parallelism,
|
||||||
|
evidence_spans=gate.evidence_spans,
|
||||||
|
explanation="budget prose",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(
|
||||||
|
*,
|
||||||
|
residual: ContractResidual,
|
||||||
|
gate: SearchGateDecision,
|
||||||
|
budget: ComputeBudgetDecision,
|
||||||
|
problem_frame_digest: str = "f" * 64,
|
||||||
|
assessment_id: str = "assessment-a",
|
||||||
|
) -> GeometricSearchRun:
|
||||||
|
outcome = initialize_geometric_search_run(
|
||||||
|
problem_frame_digest=problem_frame_digest,
|
||||||
|
contract_assessment_id=assessment_id,
|
||||||
|
residual_ids=(residual.residual_id,),
|
||||||
|
gate_decision=gate,
|
||||||
|
compute_budget=budget,
|
||||||
|
operator_set_id=candidate_operator_set_id(),
|
||||||
|
operator_set_version=CANDIDATE_OPERATOR_SET_VERSION,
|
||||||
|
)
|
||||||
|
assert isinstance(outcome, GeometricSearchRun)
|
||||||
|
return outcome
|
||||||
|
|
||||||
|
|
||||||
|
def _cue(
|
||||||
|
*,
|
||||||
|
quantity_mention_id: str = "q1",
|
||||||
|
entity_mention_id: str = "e1",
|
||||||
|
quantity_kind: str = "count",
|
||||||
|
evidence_spans: tuple[SourceSpan, ...] | None = None,
|
||||||
|
unit_mention_id: str | None = None,
|
||||||
|
) -> GroundedQuantityEntityCue:
|
||||||
|
return GroundedQuantityEntityCue(
|
||||||
|
quantity_mention_id=quantity_mention_id,
|
||||||
|
entity_mention_id=entity_mention_id,
|
||||||
|
quantity_kind=quantity_kind,
|
||||||
|
evidence_spans=evidence_spans if evidence_spans is not None else (_span(),),
|
||||||
|
unit_mention_id=unit_mention_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _chain() -> tuple[
|
||||||
|
ContractResidual,
|
||||||
|
SearchGateDecision,
|
||||||
|
ComputeBudgetDecision,
|
||||||
|
GeometricSearchRun,
|
||||||
|
GroundedQuantityEntityCue,
|
||||||
|
]:
|
||||||
|
residual = _residual()
|
||||||
|
gate = _gate(residual)
|
||||||
|
budget = _budget(gate)
|
||||||
|
run = _run(residual=residual, gate=gate, budget=budget)
|
||||||
|
cue = _cue(evidence_spans=residual.evidence_spans)
|
||||||
|
return residual, gate, budget, run, cue
|
||||||
|
|
||||||
|
|
||||||
|
def _build(**changes: object) -> CandidateOperatorResult | CandidateOperatorRefusal:
|
||||||
|
residual, gate, budget, run, cue = _chain()
|
||||||
|
values: dict[str, object] = {
|
||||||
|
"residual": residual,
|
||||||
|
"search_gate": gate,
|
||||||
|
"compute_budget": budget,
|
||||||
|
"run": run,
|
||||||
|
"problem_frame_digest": run.problem_frame_digest,
|
||||||
|
"original_contract_assessment_id": run.contract_assessment_id,
|
||||||
|
"grounded_quantity_entity_cues": (cue,),
|
||||||
|
}
|
||||||
|
values.update(changes)
|
||||||
|
return build_quantity_entity_binding_candidate(**values) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_chain_produces_candidate_operator_result() -> None:
|
||||||
|
outcome = _build()
|
||||||
|
assert isinstance(outcome, CandidateOperatorResult)
|
||||||
|
assert outcome.operator_name == QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_NAME
|
||||||
|
assert outcome.operator_version == QUANTITY_ENTITY_BINDING_CANDIDATE_OPERATOR_VERSION
|
||||||
|
assert outcome.candidate_organ == QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN
|
||||||
|
assert outcome.attempt_index == 0
|
||||||
|
assert outcome.candidate_attempt.budget_charge == BudgetCharge(candidates=1, steps=1)
|
||||||
|
assert outcome.candidate_attempt.depth == 1
|
||||||
|
assert outcome.candidate_attempt.replay_status == CandidateReplayStatus.REPLAY_PENDING
|
||||||
|
assert outcome.candidate_attempt.replay_blockers == ()
|
||||||
|
assert outcome.reason_codes == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_candidate_payload_matches_schema() -> None:
|
||||||
|
outcome = _build()
|
||||||
|
assert isinstance(outcome, CandidateOperatorResult)
|
||||||
|
payload_dict = dict(outcome.candidate_reconstruction.candidate_payload)
|
||||||
|
assert payload_dict == {
|
||||||
|
"binding_type": "quantity_entity",
|
||||||
|
"candidate_organ": QUANTITY_ENTITY_BINDING_CANDIDATE_ORGAN,
|
||||||
|
"entity_mention_id": "e1",
|
||||||
|
"kind": "mention_binding",
|
||||||
|
"quantity_kind": "count",
|
||||||
|
"quantity_mention_id": "q1",
|
||||||
|
"relation_type": "quantity_entity",
|
||||||
|
"source": "GroundedQuantityEntityCue",
|
||||||
|
"unit_mention_id": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_deterministic_result_id() -> None:
|
||||||
|
first = _build()
|
||||||
|
second = _build()
|
||||||
|
assert isinstance(first, CandidateOperatorResult)
|
||||||
|
assert isinstance(second, CandidateOperatorResult)
|
||||||
|
assert first.operator_result_id == second.operator_result_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_deterministic_attempt_id() -> None:
|
||||||
|
first = _build()
|
||||||
|
second = _build()
|
||||||
|
assert isinstance(first, CandidateOperatorResult)
|
||||||
|
assert isinstance(second, CandidateOperatorResult)
|
||||||
|
assert first.attempt_id == second.attempt_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_deterministic_candidate_digest() -> None:
|
||||||
|
first = _build()
|
||||||
|
second = _build()
|
||||||
|
assert isinstance(first, CandidateOperatorResult)
|
||||||
|
assert isinstance(second, CandidateOperatorResult)
|
||||||
|
assert first.candidate_digest == second.candidate_digest
|
||||||
|
|
||||||
|
|
||||||
|
def test_explanation_does_not_affect_canonical_ids() -> None:
|
||||||
|
first = _build(explanation="hello")
|
||||||
|
second = _build(explanation="world")
|
||||||
|
assert isinstance(first, CandidateOperatorResult)
|
||||||
|
assert isinstance(second, CandidateOperatorResult)
|
||||||
|
assert first.operator_result_id == second.operator_result_id
|
||||||
|
assert first.attempt_id == second.attempt_id
|
||||||
|
assert first.candidate_digest == second.candidate_digest
|
||||||
|
|
||||||
|
|
||||||
|
def test_evidence_span_order_preserved() -> None:
|
||||||
|
spans = (_span("A"), _span("B"), _span("C"))
|
||||||
|
residual = _residual(evidence_spans=spans)
|
||||||
|
gate = _gate(residual)
|
||||||
|
budget = _budget(gate)
|
||||||
|
run = _run(residual=residual, gate=gate, budget=budget)
|
||||||
|
cue = _cue(evidence_spans=spans)
|
||||||
|
outcome = _build(residual=residual, search_gate=gate, compute_budget=budget, run=run, grounded_quantity_entity_cues=(cue,))
|
||||||
|
assert isinstance(outcome, CandidateOperatorResult)
|
||||||
|
assert outcome.evidence_spans == spans
|
||||||
|
assert outcome.candidate_attempt.evidence_spans == spans
|
||||||
|
|
||||||
|
|
||||||
|
def test_duplicate_evidence_spans_preserved() -> None:
|
||||||
|
spans = (_span("A"), _span("A"))
|
||||||
|
residual = _residual(evidence_spans=spans)
|
||||||
|
gate = _gate(residual)
|
||||||
|
budget = _budget(gate)
|
||||||
|
run = _run(residual=residual, gate=gate, budget=budget)
|
||||||
|
cue = _cue(evidence_spans=spans)
|
||||||
|
outcome = _build(residual=residual, search_gate=gate, compute_budget=budget, run=run, grounded_quantity_entity_cues=(cue,))
|
||||||
|
assert isinstance(outcome, CandidateOperatorResult)
|
||||||
|
assert outcome.evidence_spans == spans
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_candidate_attempts_unchanged() -> None:
|
||||||
|
residual, gate, budget, run, cue = _chain()
|
||||||
|
run_attempts_before = tuple(run.candidate_attempts)
|
||||||
|
_build(residual=residual, search_gate=gate, compute_budget=budget, run=run, grounded_quantity_entity_cues=(cue,))
|
||||||
|
assert run.candidate_attempts == run_attempts_before
|
||||||
|
|
||||||
|
|
||||||
|
def test_spine_compatibility() -> None:
|
||||||
|
residual, gate, budget, run, cue = _chain()
|
||||||
|
outcome = build_quantity_entity_binding_candidate(
|
||||||
|
residual=residual,
|
||||||
|
search_gate=gate,
|
||||||
|
compute_budget=budget,
|
||||||
|
run=run,
|
||||||
|
problem_frame_digest=run.problem_frame_digest,
|
||||||
|
original_contract_assessment_id=run.contract_assessment_id,
|
||||||
|
grounded_quantity_entity_cues=(cue,),
|
||||||
|
)
|
||||||
|
assert isinstance(outcome, CandidateOperatorResult)
|
||||||
|
bound_attempt = bind_candidate_attempt_to_run(original_run=run, candidate_operator_result=outcome)
|
||||||
|
adapter_input = build_replay_adapter_input_from_binding(
|
||||||
|
run=run,
|
||||||
|
binding=bound_attempt,
|
||||||
|
candidate_operator_result=outcome,
|
||||||
|
)
|
||||||
|
assert isinstance(adapter_input, ReplayAdapterRefusal)
|
||||||
|
assert any("contract_replay_unavailable" in rc or "operator_replay_unavailable" in rc or "unsupported_candidate_organ" in rc for rc in adapter_input.reason_codes)
|
||||||
|
|
||||||
|
|
||||||
|
# Refusal tests
|
||||||
|
def test_wrong_residual_kind() -> None:
|
||||||
|
residual = _residual(residual_kind=ResidualKind.MISSING_ROLE)
|
||||||
|
gate = _gate(residual, reason_code="eligible_missing_relation")
|
||||||
|
budget = _budget(gate, reason_code="budget_allowed_missing_relation")
|
||||||
|
run = _run(residual=residual, gate=gate, budget=budget)
|
||||||
|
outcome = build_quantity_entity_binding_candidate(
|
||||||
|
residual=residual,
|
||||||
|
search_gate=gate,
|
||||||
|
compute_budget=budget,
|
||||||
|
run=run,
|
||||||
|
problem_frame_digest=run.problem_frame_digest,
|
||||||
|
original_contract_assessment_id=run.contract_assessment_id,
|
||||||
|
grounded_quantity_entity_cues=(_cue(evidence_spans=residual.evidence_spans),),
|
||||||
|
)
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.UNSUPPORTED_RESIDUAL_KIND.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrong_residual_code() -> None:
|
||||||
|
residual = _residual(residual_code="some_other_code")
|
||||||
|
gate = _gate(residual)
|
||||||
|
budget = _budget(gate)
|
||||||
|
run = _run(residual=residual, gate=gate, budget=budget)
|
||||||
|
outcome = build_quantity_entity_binding_candidate(
|
||||||
|
residual=residual,
|
||||||
|
search_gate=gate,
|
||||||
|
compute_budget=budget,
|
||||||
|
run=run,
|
||||||
|
problem_frame_digest=run.problem_frame_digest,
|
||||||
|
original_contract_assessment_id=run.contract_assessment_id,
|
||||||
|
grounded_quantity_entity_cues=(_cue(evidence_spans=residual.evidence_spans),),
|
||||||
|
)
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.UNSUPPORTED_RESIDUAL_CODE.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_unsupported_candidate_organ() -> None:
|
||||||
|
residual = _residual(candidate_organ="some_other_organ")
|
||||||
|
gate = _gate(residual)
|
||||||
|
budget = _budget(gate)
|
||||||
|
run = _run(residual=residual, gate=gate, budget=budget)
|
||||||
|
outcome = build_quantity_entity_binding_candidate(
|
||||||
|
residual=residual,
|
||||||
|
search_gate=gate,
|
||||||
|
compute_budget=budget,
|
||||||
|
run=run,
|
||||||
|
problem_frame_digest=run.problem_frame_digest,
|
||||||
|
original_contract_assessment_id=run.contract_assessment_id,
|
||||||
|
grounded_quantity_entity_cues=(_cue(evidence_spans=residual.evidence_spans),),
|
||||||
|
)
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.UNSUPPORTED_CANDIDATE_ORGAN.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_evidence_spans() -> None:
|
||||||
|
cue = GroundedQuantityEntityCue(
|
||||||
|
quantity_mention_id="q1",
|
||||||
|
entity_mention_id="e1",
|
||||||
|
quantity_kind="count",
|
||||||
|
evidence_spans=(),
|
||||||
|
)
|
||||||
|
outcome = _build(grounded_quantity_entity_cues=(cue,))
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.MISSING_EVIDENCE_SPANS.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_malformed_evidence_spans() -> None:
|
||||||
|
bad_span = SimpleNamespace(text="bad", start=-1, end=3, sentence_index=0)
|
||||||
|
cue = GroundedQuantityEntityCue(
|
||||||
|
quantity_mention_id="q1",
|
||||||
|
entity_mention_id="e1",
|
||||||
|
quantity_kind="count",
|
||||||
|
evidence_spans=(bad_span,), # type: ignore[arg-type]
|
||||||
|
)
|
||||||
|
outcome = _build(grounded_quantity_entity_cues=(cue,))
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.MALFORMED_EVIDENCE_SPAN.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_ungrounded_quantity_cue() -> None:
|
||||||
|
outcome = _build(grounded_quantity_entity_cues=("not a cue",))
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.INVALID_QUANTITY_ENTITY_CUE_TYPE.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_zero_cues() -> None:
|
||||||
|
outcome = _build(grounded_quantity_entity_cues=())
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.MISSING_TYPED_CUE.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_cues() -> None:
|
||||||
|
cue1 = _cue()
|
||||||
|
cue2 = _cue()
|
||||||
|
outcome = _build(grounded_quantity_entity_cues=(cue1, cue2))
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.QUANTITY_ENTITY_CUE_COUNT_MISMATCH.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_quantity_mention_id() -> None:
|
||||||
|
outcome = _build(grounded_quantity_entity_cues=(_cue(quantity_mention_id=""),))
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.EMPTY_QUANTITY_MENTION_ID.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_entity_mention_id() -> None:
|
||||||
|
outcome = _build(grounded_quantity_entity_cues=(_cue(entity_mention_id=""),))
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.EMPTY_ENTITY_MENTION_ID.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_quantity_kind() -> None:
|
||||||
|
outcome = _build(grounded_quantity_entity_cues=(_cue(quantity_kind=""),))
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.EMPTY_QUANTITY_KIND.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_unit_mention_id() -> None:
|
||||||
|
outcome = _build(grounded_quantity_entity_cues=(_cue(unit_mention_id=""),))
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.EMPTY_UNIT_MENTION_ID.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_change_surface_backdoor() -> None:
|
||||||
|
residual = _residual(candidate_organ="unary_delta_transition")
|
||||||
|
gate = _gate(residual)
|
||||||
|
budget = _budget(gate)
|
||||||
|
run = _run(residual=residual, gate=gate, budget=budget)
|
||||||
|
outcome = build_quantity_entity_binding_candidate(
|
||||||
|
residual=residual,
|
||||||
|
search_gate=gate,
|
||||||
|
compute_budget=budget,
|
||||||
|
run=run,
|
||||||
|
problem_frame_digest=run.problem_frame_digest,
|
||||||
|
original_contract_assessment_id=run.contract_assessment_id,
|
||||||
|
grounded_quantity_entity_cues=(_cue(evidence_spans=residual.evidence_spans),),
|
||||||
|
)
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.UNSUPPORTED_CANDIDATE_ORGAN.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_attempt_index_not_zero() -> None:
|
||||||
|
outcome = _build(attempt_index=1)
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.ATTEMPT_INDEX_EXCEEDS_BUDGET.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_residual_id_not_in_run() -> None:
|
||||||
|
residual = _residual()
|
||||||
|
gate = _gate(residual)
|
||||||
|
budget = _budget(gate)
|
||||||
|
run = _run(residual=residual, gate=gate, budget=budget)
|
||||||
|
other_residual = _residual(residual_id="other", evidence_spans=residual.evidence_spans)
|
||||||
|
outcome = build_quantity_entity_binding_candidate(
|
||||||
|
residual=other_residual,
|
||||||
|
search_gate=gate,
|
||||||
|
compute_budget=budget,
|
||||||
|
run=run,
|
||||||
|
problem_frame_digest=run.problem_frame_digest,
|
||||||
|
original_contract_assessment_id=run.contract_assessment_id,
|
||||||
|
grounded_quantity_entity_cues=(_cue(evidence_spans=residual.evidence_spans),),
|
||||||
|
)
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_problem_frame_mismatch() -> None:
|
||||||
|
outcome = _build(problem_frame_digest="wrong" * 8)
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_contract_assessment_mismatch() -> None:
|
||||||
|
outcome = _build(original_contract_assessment_id="wrong_assessment")
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.INVALID_OPERATOR_INPUT.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_gate_not_eligible() -> None:
|
||||||
|
residual, valid_gate, valid_budget, run, cue = _chain()
|
||||||
|
gate = _gate(residual, status=SearchGateStatus.INELIGIBLE)
|
||||||
|
outcome = build_quantity_entity_binding_candidate(
|
||||||
|
residual=residual,
|
||||||
|
search_gate=gate,
|
||||||
|
compute_budget=valid_budget,
|
||||||
|
run=run,
|
||||||
|
problem_frame_digest=run.problem_frame_digest,
|
||||||
|
original_contract_assessment_id=run.contract_assessment_id,
|
||||||
|
grounded_quantity_entity_cues=(_cue(evidence_spans=residual.evidence_spans),),
|
||||||
|
)
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.INELIGIBLE_SEARCH_GATE.value in outcome.reason_codes
|
||||||
|
|
||||||
|
|
||||||
|
def test_compute_budget_not_allowed() -> None:
|
||||||
|
residual, gate, valid_budget, run, cue = _chain()
|
||||||
|
budget = _budget(gate, status=ComputeBudgetStatus.BUDGET_BLOCKED)
|
||||||
|
outcome = build_quantity_entity_binding_candidate(
|
||||||
|
residual=residual,
|
||||||
|
search_gate=gate,
|
||||||
|
compute_budget=budget,
|
||||||
|
run=run,
|
||||||
|
problem_frame_digest=run.problem_frame_digest,
|
||||||
|
original_contract_assessment_id=run.contract_assessment_id,
|
||||||
|
grounded_quantity_entity_cues=(_cue(evidence_spans=residual.evidence_spans),),
|
||||||
|
)
|
||||||
|
assert isinstance(outcome, CandidateOperatorRefusal)
|
||||||
|
assert CandidateOperatorRefusalReason.NON_ALLOWED_BUDGET.value in outcome.reason_codes
|
||||||
|
|
@ -135,7 +135,7 @@ def _chain() -> tuple[GeometricSearchRun, object, CandidateAttemptRunBinding]:
|
||||||
gate_decision=gate,
|
gate_decision=gate,
|
||||||
compute_budget=budget,
|
compute_budget=budget,
|
||||||
operator_set_id=candidate_operator_set_id(),
|
operator_set_id=candidate_operator_set_id(),
|
||||||
operator_set_version="candidate_operators.v1",
|
operator_set_version="candidate_operators.v2",
|
||||||
)
|
)
|
||||||
assert isinstance(run, GeometricSearchRun)
|
assert isinstance(run, GeometricSearchRun)
|
||||||
result = build_missing_role_candidate(
|
result = build_missing_role_candidate(
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ def _chain() -> tuple[ContractResidual, GeometricSearchRun, object]:
|
||||||
gate_decision=gate,
|
gate_decision=gate,
|
||||||
compute_budget=budget,
|
compute_budget=budget,
|
||||||
operator_set_id=candidate_operator_set_id(),
|
operator_set_id=candidate_operator_set_id(),
|
||||||
operator_set_version="candidate_operators.v1",
|
operator_set_version="candidate_operators.v2",
|
||||||
)
|
)
|
||||||
assert isinstance(run, GeometricSearchRun)
|
assert isinstance(run, GeometricSearchRun)
|
||||||
result = build_missing_role_candidate(
|
result = build_missing_role_candidate(
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ def _chain() -> tuple[
|
||||||
gate_decision=gate,
|
gate_decision=gate,
|
||||||
compute_budget=budget,
|
compute_budget=budget,
|
||||||
operator_set_id=candidate_operator_set_id(),
|
operator_set_id=candidate_operator_set_id(),
|
||||||
operator_set_version="candidate_operators.v1",
|
operator_set_version="candidate_operators.v2",
|
||||||
)
|
)
|
||||||
assert isinstance(run, GeometricSearchRun)
|
assert isinstance(run, GeometricSearchRun)
|
||||||
result = build_missing_role_candidate(
|
result = build_missing_role_candidate(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue