feat(kernel): route proportional-decrease through proposal-first seam (#841)
This commit is contained in:
parent
168c9f26ac
commit
955fec3bc3
7 changed files with 323 additions and 32 deletions
|
|
@ -180,6 +180,121 @@ The substantive result of this session is not just “0005 runnable.” It is th
|
|||
|
||||
---
|
||||
|
||||
## PR #841 Addendum — feat(kernel): route proportional-decrease through proposal-first seam
|
||||
|
||||
### Agent and Session
|
||||
|
||||
- **Agent:** gpt55
|
||||
- **Date:** 2026-06-20
|
||||
- **Reasoning effort used:** high
|
||||
- **Grok Build mode used:** Headless
|
||||
- **Session entry point:** Migrate only `proportional_change.decrease_to_fraction` from assessment-backed proposal synthesis to the ADR-0223/0224 proposal-first construction seam without changing serving or derivation.
|
||||
- **Branch:** `feat/kernel-proportional-decrease-proposal-first`
|
||||
- **Base:** `origin/main` at `168c9f26ac3deb8037d4eb7bca77c33dc9d5ff62`
|
||||
|
||||
### Smoke Suite + Bootstrap Status
|
||||
|
||||
```text
|
||||
uv run python -m core.cli test --suite smoke -q
|
||||
108 passed in 123.41s (baseline)
|
||||
108 passed in 123.05s (post-edit)
|
||||
```
|
||||
|
||||
The GPT55.md startup checklist was completed manually. The Codex worktree was clean and `HEAD == origin/main` before edits. `/Users/kaizenpro/Projects/core` was not modified, stashed, reset, cleaned, or reconciled.
|
||||
|
||||
### Modules Touched
|
||||
|
||||
| File | Change type | Summary |
|
||||
|---|---|---|
|
||||
| `generate/construction_affordances.py` | modified | Added a pre-assessment catalog proposal factory; proposals now carry immutable role obligations and enforce diagnostic-only / serving-disallowed posture. Retained the assessment-backed adapter only for unmigrated catalog paths. |
|
||||
| `generate/problem_frame.py` | modified | Let `ProblemFrameBuilder` accumulate proposals before relation binding. |
|
||||
| `generate/problem_frame_builder.py` | modified | Produces the proportional-decrease proposal from the exact existing surface chunk before mention/relation binding and assessment; preserves assessment-backed traces for other families. |
|
||||
| `generate/problem_frame_contracts.py` | modified | Routes fraction-decrease assessment from proposal metadata read-only while leaving `assess_fraction_decrease` obligations unchanged. |
|
||||
| `tests/test_construction_affordances.py` | modified | Covers the pre-assessment factory, catalog role obligations, and serving posture. |
|
||||
| `tests/test_proportional_decrease_proposal.py` | modified | Proves proposal-before-assessment order, exact span grounding, proposal-driven dispatch, unchanged contract refusal, and confuser blocking. |
|
||||
| `HANDOFF-gpt55-2026-06-20.md` | modified | Added this PR #841 continuity record. |
|
||||
|
||||
### Invariants Verified
|
||||
|
||||
- **Field closure:** Preserved by construction. No algebra, field, propagation, normalization, vault, or runtime field-state code changed.
|
||||
- **Exact recall:** Preserved. No recall path, cosine, ANN, HNSW, or stochastic retrieval was added.
|
||||
- **Proposal/binding/contract separation:** The proposal begins as `status="proposed"`; only `ContractAssessment` determines runnable/refused state from bound relations and question-target evidence.
|
||||
- **Catalog and serving gates:** Proposal role obligations and posture come from the existing catalog; `diagnostic_only=True` and `serving_allowed=False` are enforced on the proposal type.
|
||||
- **Controlled migration:** Only proportional decrease is proposal-first. Percent partition retains its existing assessment-backed trace; no foundational family gained implementation authorization.
|
||||
- **No universal IR / no live-CGA claim:** The patch reuses `ConstructionProposal`, `ProblemFrame`, `BoundRelation`, and `ContractAssessment`; it adds no new carrier and makes no substrate-retrieval claim.
|
||||
- **Serving parity:** Train remained `30 correct / 20 refused / 0 wrong`; holdout remained `5 correct / 495 refused / 0 wrong`; both wrong-id lists were empty.
|
||||
- **Diagnostic parity:** Train remained 2 runnable contracts (`0005`, `0046`); holdout remained 0 runnable contracts.
|
||||
|
||||
### Subagent / Arena Reconciliation
|
||||
|
||||
- Number of subagents spawned: 0
|
||||
- Each subagent independently verified versor closure: N/A
|
||||
- Reconciliation: N/A
|
||||
|
||||
### Tests Run
|
||||
|
||||
```bash
|
||||
# exit 0 — 55 passed
|
||||
uv run python -m pytest -q \
|
||||
tests/test_construction_affordances.py \
|
||||
tests/test_foundational_families.py \
|
||||
tests/test_problem_frame_builder.py \
|
||||
tests/test_problem_frame_contracts.py \
|
||||
tests/test_proportional_decrease_proposal.py \
|
||||
tests/test_kernel_no_new_legacy_derivation_surfaces.py
|
||||
|
||||
# exit 0 — 108 passed
|
||||
uv run python -m core.cli test --suite smoke -q
|
||||
|
||||
# exit 0 — serving parity
|
||||
# train: {'correct': 30, 'wrong': 0, 'refused': 20}; wrong_ids: []
|
||||
# holdout: {'correct': 5, 'wrong': 0, 'refused': 495}; wrong_ids: []
|
||||
|
||||
# exit 0 — diagnostic parity
|
||||
# train contract_runnable_count: 2; ids: 0005, 0046
|
||||
# holdout contract_runnable_count: 0
|
||||
|
||||
# exit 0
|
||||
uv run python -m compileall -q \
|
||||
generate/construction_affordances.py \
|
||||
generate/problem_frame.py \
|
||||
generate/problem_frame_builder.py \
|
||||
generate/problem_frame_contracts.py
|
||||
|
||||
# exit 0
|
||||
git diff --check
|
||||
```
|
||||
|
||||
### Open Tasks / Next Session Entry Point
|
||||
|
||||
1. Review the six implementation/test files and publish PR #841 with the brief's required body sections.
|
||||
2. Keep percent partition assessment-backed until a separately authorized proposal-first migration.
|
||||
|
||||
### Known Hazards / Do Not Touch
|
||||
|
||||
- Do not map `ConstructionProposal.status` to fraction assessment output again; that recreates the assessment-first inversion.
|
||||
- Do not generalize the reused `decrease to <fraction> of` recognizer into generic state change, acquisition, or transaction behavior.
|
||||
- Do not connect foundational-family registry metadata to runtime authorization or serving.
|
||||
- Do not edit derivation, candidate graph, eval cases, reports, packs, policy, identity, recall, field, or algebra for this slice.
|
||||
|
||||
### Architectural Decision
|
||||
|
||||
The proposal is the construction hypothesis, not a cached verdict. It carries the catalog's obligations and exact motivating chunk. `BoundRelation` grounds those obligations, and the existing `ContractAssessment` remains the corrective/conjugate stage that closes or refuses the hypothesis. Other families are deliberately left on the legacy assessment-backed adapter to keep this migration singular.
|
||||
|
||||
### What Must Not Be Forgotten
|
||||
|
||||
PR #841 proves the ordering `surface chunk → proposed construction → span-grounded relation → ContractAssessment` for proportional decrease only. A positive proposal is not serving authorization, is not a runnable verdict, and is not evidence that general CGA/substrate retrieval is live.
|
||||
|
||||
### Skills Used
|
||||
|
||||
- core-bootstrap: unavailable; checklist completed manually
|
||||
- versor-coherence-guardian: unavailable; closure preserved by scope and smoke validation
|
||||
- pre-edit-sweep: completed manually across imports, callers, tests, evals, calibration, and scripts
|
||||
- claim-proposal-guardian: unavailable; proposal posture and assessment separation verified directly
|
||||
- Other: none
|
||||
|
||||
---
|
||||
|
||||
## PR #836 Addendum — feat(kernel): introduce construction-affordance catalog skeleton
|
||||
|
||||
### Agent and Session
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ Design doctrine (from ADR-0223):
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -160,9 +160,9 @@ class ConstructionProposal:
|
|||
→ hazards
|
||||
→ status
|
||||
|
||||
A proposal is created by ``make_proposal()`` from ProblemFrame evidence.
|
||||
It does not affect serving. Its ``status`` reflects whether the
|
||||
corresponding contract assessment declared the frame runnable.
|
||||
A proposal is created from exact surface evidence before contract
|
||||
assessment. It does not affect serving. Its initial ``status`` is
|
||||
``"proposed"``; assessment remains a separate, downstream judgment.
|
||||
|
||||
Args:
|
||||
family_id: Catalog family identifier, e.g.
|
||||
|
|
@ -175,6 +175,10 @@ class ConstructionProposal:
|
|||
assessment was runnable.
|
||||
missing_roles: Role obligation names that were absent at assessment time.
|
||||
active_hazards: Hazard categories that were unresolved at assessment time.
|
||||
role_obligations: Catalog-declared roles that downstream binding and
|
||||
assessment must ground or explicitly refuse.
|
||||
diagnostic_only: True for every proposal in the current catalog.
|
||||
serving_allowed: False for every proposal in the current catalog.
|
||||
"""
|
||||
|
||||
family_id: str
|
||||
|
|
@ -184,6 +188,9 @@ class ConstructionProposal:
|
|||
status: str
|
||||
missing_roles: tuple[str, ...]
|
||||
active_hazards: tuple[str, ...]
|
||||
role_obligations: tuple[RoleObligation, ...] = ()
|
||||
diagnostic_only: bool = True
|
||||
serving_allowed: bool = False
|
||||
|
||||
_VALID_STATUSES: frozenset[str] = frozenset({
|
||||
"proposed", "partial", "closed", "refused"
|
||||
|
|
@ -195,6 +202,10 @@ class ConstructionProposal:
|
|||
f"ConstructionProposal.status must be one of "
|
||||
f"{sorted(self._VALID_STATUSES)}, got {self.status!r}"
|
||||
)
|
||||
if not self.diagnostic_only or self.serving_allowed:
|
||||
raise ValueError(
|
||||
"ConstructionProposal must remain diagnostic-only and serving-disallowed"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -405,6 +416,37 @@ def all_diagnostic_families() -> tuple[ConstructionFamily, ...]:
|
|||
return tuple(_CATALOG[key] for key in sorted(_CATALOG))
|
||||
|
||||
|
||||
def propose_construction(
|
||||
family_id: str,
|
||||
evidence_spans: tuple[SourceSpan, ...],
|
||||
) -> ConstructionProposal:
|
||||
"""Create a catalog-backed proposal from pre-assessment surface evidence.
|
||||
|
||||
This factory deliberately has no assessment inputs. It records the
|
||||
construction hypothesis and its catalog obligations; bound relations and
|
||||
``ContractAssessment`` remain responsible for grounding and determination.
|
||||
|
||||
Raises:
|
||||
KeyError: If *family_id* is not registered in the catalog.
|
||||
"""
|
||||
family = _CATALOG[family_id]
|
||||
return ConstructionProposal(
|
||||
family_id=family.family_id,
|
||||
relation_type=family.signature.relation_type,
|
||||
candidate_organ=family.signature.candidate_organ,
|
||||
evidence_spans=evidence_spans,
|
||||
status="proposed",
|
||||
missing_roles=(),
|
||||
active_hazards=(),
|
||||
role_obligations=(
|
||||
*family.signature.required_roles,
|
||||
*family.signature.optional_roles,
|
||||
),
|
||||
diagnostic_only=family.diagnostic_only,
|
||||
serving_allowed=family.serving_allowed,
|
||||
)
|
||||
|
||||
|
||||
def make_proposal(
|
||||
family_id: str,
|
||||
evidence_spans: tuple[SourceSpan, ...],
|
||||
|
|
@ -412,11 +454,12 @@ def make_proposal(
|
|||
missing_roles: tuple[str, ...],
|
||||
active_hazards: tuple[str, ...],
|
||||
) -> ConstructionProposal:
|
||||
"""Create a lightweight diagnostic-only ConstructionProposal from frame evidence.
|
||||
"""Map assessment evidence onto a proposal for legacy catalog paths.
|
||||
|
||||
This is a thin factory. The caller supplies evidence already gathered by
|
||||
``assess_contracts()``; this function only maps that evidence to the
|
||||
standard proposal trace shape.
|
||||
Proportional decrease no longer uses this assessment-backed adapter; it
|
||||
enters through :func:`propose_construction`. The adapter remains for
|
||||
catalog families that have not yet received an authorized proposal-first
|
||||
migration.
|
||||
|
||||
Args:
|
||||
family_id: Catalog family identifier.
|
||||
|
|
@ -436,7 +479,7 @@ def make_proposal(
|
|||
Raises:
|
||||
KeyError: If *family_id* is not registered in the catalog.
|
||||
"""
|
||||
family = _CATALOG[family_id] # raises KeyError if absent — intended
|
||||
proposal = propose_construction(family_id, evidence_spans)
|
||||
|
||||
if assessment_runnable:
|
||||
status = "closed"
|
||||
|
|
@ -447,11 +490,8 @@ def make_proposal(
|
|||
else:
|
||||
status = "proposed"
|
||||
|
||||
return ConstructionProposal(
|
||||
family_id=family_id,
|
||||
relation_type=family.signature.relation_type,
|
||||
candidate_organ=family.signature.candidate_organ,
|
||||
evidence_spans=evidence_spans,
|
||||
return replace(
|
||||
proposal,
|
||||
status=status,
|
||||
missing_roles=missing_roles,
|
||||
active_hazards=active_hazards,
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ class ProblemFrameBuilder:
|
|||
self._bindings: list[MentionBinding] = []
|
||||
self._bound_relations: list[BoundRelation] = []
|
||||
self._bound_question_target: BoundQuestionTarget | None = None
|
||||
self._proposals: list[ConstructionProposal] = []
|
||||
|
||||
def add_quantity(self, scalar: GroundedScalar) -> None:
|
||||
"""Add a GroundedScalar to the frame, collecting hazards and provenance."""
|
||||
|
|
@ -199,6 +200,9 @@ class ProblemFrameBuilder:
|
|||
def set_bound_question_target(self, target: BoundQuestionTarget) -> None:
|
||||
self._bound_question_target = target
|
||||
|
||||
def add_proposal(self, proposal: ConstructionProposal) -> None:
|
||||
self._proposals.append(proposal)
|
||||
|
||||
def build(self) -> ProblemFrame:
|
||||
"""Produce the immutable ProblemFrame."""
|
||||
return ProblemFrame(
|
||||
|
|
@ -216,4 +220,5 @@ class ProblemFrameBuilder:
|
|||
bindings=tuple(self._bindings),
|
||||
bound_relations=tuple(self._bound_relations),
|
||||
bound_question_target=self._bound_question_target,
|
||||
proposals=tuple(self._proposals),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,9 +12,11 @@ Non-goals:
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import re
|
||||
from fractions import Fraction
|
||||
|
||||
from generate.construction_affordances import ConstructionProposal, propose_construction
|
||||
from generate.kernel_facts import (
|
||||
BoundRelation,
|
||||
BoundRole,
|
||||
|
|
@ -338,6 +340,25 @@ _TRANSFER_RE = re.compile(
|
|||
)
|
||||
|
||||
|
||||
def _proportional_decrease_proposals(text: str) -> tuple[ConstructionProposal, ...]:
|
||||
"""Propose the one authorized proposal-first construction from its chunk."""
|
||||
matches = tuple(_DECREASE_TO_FRACTION_RE.finditer(text))
|
||||
if len(matches) != 1:
|
||||
return ()
|
||||
match = matches[0]
|
||||
evidence = SourceSpan(
|
||||
text[match.start():match.end()],
|
||||
match.start(),
|
||||
match.end(),
|
||||
)
|
||||
return (
|
||||
propose_construction(
|
||||
"proportional_change.decrease_to_fraction",
|
||||
(evidence,),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _extract_mentions(
|
||||
text: str,
|
||||
quantities: tuple[GroundedScalar, ...],
|
||||
|
|
@ -732,6 +753,12 @@ def build_problem_frame(problem_text: str) -> ProblemFrame:
|
|||
if question_target is not None:
|
||||
builder.set_question_target(question_target)
|
||||
|
||||
# ADR-0223/0224 proving slice: the surface chunk proposes the catalog
|
||||
# construction before role binding and ContractAssessment. Only
|
||||
# proportional decrease is authorized for this controlled migration.
|
||||
for proposal in _proportional_decrease_proposals(problem_text):
|
||||
builder.add_proposal(proposal)
|
||||
|
||||
mentions = _extract_mentions(problem_text, tuple(grounded_quantities), units)
|
||||
bindings = _extract_bindings(problem_text, mentions)
|
||||
for mention in mentions:
|
||||
|
|
@ -754,10 +781,11 @@ def build_problem_frame(problem_text: str) -> ProblemFrame:
|
|||
from generate.construction_affordances import make_proposal
|
||||
|
||||
assessments = assess_contracts(initial_frame)
|
||||
proposals = []
|
||||
proposals = list(initial_frame.proposals)
|
||||
proposed_family_ids = {proposal.family_id for proposal in proposals}
|
||||
for assessment in assessments:
|
||||
family_id = get_contract_family_id(assessment.candidate_organ)
|
||||
if family_id is not None:
|
||||
if family_id is not None and family_id not in proposed_family_ids:
|
||||
proposal = make_proposal(
|
||||
family_id=family_id,
|
||||
evidence_spans=assessment.evidence_spans,
|
||||
|
|
@ -766,8 +794,8 @@ def build_problem_frame(problem_text: str) -> ProblemFrame:
|
|||
active_hazards=assessment.unresolved_hazards,
|
||||
)
|
||||
proposals.append(proposal)
|
||||
proposed_family_ids.add(family_id)
|
||||
|
||||
import dataclasses
|
||||
return dataclasses.replace(initial_frame, proposals=tuple(proposals))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ from generate.construction_affordances import (
|
|||
ConstructionContract,
|
||||
_DECREASE_TO_FRACTION_FAMILY,
|
||||
_PERCENT_PARTITION_FAMILY,
|
||||
make_proposal,
|
||||
)
|
||||
from generate.kernel_facts import BoundRelation, SourceSpan
|
||||
from generate.problem_frame import ProblemFrame
|
||||
|
|
@ -296,8 +295,9 @@ def assess_contracts(frame: ProblemFrame) -> tuple[ContractAssessment, ...]:
|
|||
"""Return deterministic diagnostic assessments; never admits serving.
|
||||
|
||||
Dispatch order:
|
||||
1. ``decrease_to_fraction`` — triggered by relation type presence in
|
||||
``frame.bound_relations``. Routes to ``assess_fraction_decrease``.
|
||||
1. ``decrease_to_fraction`` — triggered by its proposal-first catalog
|
||||
family in ``frame.proposals``. Routes to ``assess_fraction_decrease``,
|
||||
which still determines closure from bound frame evidence.
|
||||
Registry key: ``_CONTRACT_REGISTRY["fraction_decrease"]``.
|
||||
2. ``percent_partition`` — triggered by process-frame names ``partition``
|
||||
or ``consumption``. Routes to ``assess_percent_partition``.
|
||||
|
|
@ -314,7 +314,8 @@ def assess_contracts(frame: ProblemFrame) -> tuple[ContractAssessment, ...]:
|
|||
results: list[ContractAssessment] = []
|
||||
|
||||
# Registry-backed diagnostic families
|
||||
if any(relation.relation_type == "decrease_to_fraction" for relation in frame.bound_relations):
|
||||
proposed_family_ids = {proposal.family_id for proposal in frame.proposals}
|
||||
if _DECREASE_TO_FRACTION_FAMILY.family_id in proposed_family_ids:
|
||||
# Catalog: _CONTRACT_REGISTRY["fraction_decrease"]
|
||||
results.append(assess_fraction_decrease(frame))
|
||||
if frame_names & {"partition", "consumption"}:
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ from generate.construction_affordances import (
|
|||
lookup_by_organ,
|
||||
lookup_by_relation_type,
|
||||
make_proposal,
|
||||
propose_construction,
|
||||
ConstructionProposal,
|
||||
_CATALOG,
|
||||
)
|
||||
from generate.kernel_facts import SourceSpan
|
||||
|
||||
|
|
@ -115,6 +115,27 @@ def test_make_proposal_validation_and_status() -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_propose_construction_is_preassessment_and_carries_catalog_obligations() -> None:
|
||||
span = SourceSpan("decrease to 3/4 of", 0, 18)
|
||||
|
||||
proposal = propose_construction(
|
||||
"proportional_change.decrease_to_fraction",
|
||||
(span,),
|
||||
)
|
||||
|
||||
assert proposal.status == "proposed"
|
||||
assert proposal.missing_roles == ()
|
||||
assert proposal.active_hazards == ()
|
||||
assert proposal.diagnostic_only is True
|
||||
assert proposal.serving_allowed is False
|
||||
assert {role.role for role in proposal.role_obligations if role.required} == {
|
||||
"base_quantity",
|
||||
"scale",
|
||||
"state_entity",
|
||||
"transition",
|
||||
}
|
||||
|
||||
|
||||
def test_construction_proposal_status_validation() -> None:
|
||||
span = SourceSpan("test text", 0, 9)
|
||||
# Valid status doesn't raise
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
"""Tests for feat(kernel): route proportional-decrease through construction proposals."""
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
|
||||
import generate.problem_frame_builder as problem_frame_builder
|
||||
import generate.problem_frame_contracts as problem_frame_contracts
|
||||
from generate.problem_frame_builder import build_problem_frame
|
||||
from generate.construction_affordances import lookup_family
|
||||
from generate.problem_frame_contracts import assess_contracts
|
||||
|
||||
|
||||
FRACTION_DECREASE_CASE = (
|
||||
|
|
@ -20,6 +25,43 @@ AFFINE_CONFUSER = (
|
|||
"Marion has 1/4 more than what Yun currently has, plus 7. How many paperclips does Marion have?"
|
||||
)
|
||||
|
||||
MULTIPLE_FRACTION_CONFUSER = (
|
||||
"The reactor's temperature will decrease to 3/4 of its temperature and later decrease "
|
||||
"to 1/2 of its temperature. If the current temperature is 80 degrees, what will the "
|
||||
"temperature decrease by?"
|
||||
)
|
||||
|
||||
|
||||
def test_proposal_precedes_role_binding_and_contract_assessment(monkeypatch) -> None:
|
||||
events: list[str] = []
|
||||
original_propose = problem_frame_builder.propose_construction
|
||||
original_assess = problem_frame_contracts.assess_contracts
|
||||
|
||||
def observe_proposal(*args, **kwargs):
|
||||
events.append("proposal")
|
||||
return original_propose(*args, **kwargs)
|
||||
|
||||
def observe_assessment(frame):
|
||||
events.append("assessment")
|
||||
proposal = next(
|
||||
item
|
||||
for item in frame.proposals
|
||||
if item.family_id == "proportional_change.decrease_to_fraction"
|
||||
)
|
||||
assert proposal.status == "proposed"
|
||||
return original_assess(frame)
|
||||
|
||||
monkeypatch.setattr(problem_frame_builder, "propose_construction", observe_proposal)
|
||||
monkeypatch.setattr(problem_frame_contracts, "assess_contracts", observe_assessment)
|
||||
|
||||
frame = build_problem_frame(FRACTION_DECREASE_CASE)
|
||||
|
||||
assert events == ["proposal", "assessment"]
|
||||
assert any(
|
||||
relation.relation_type == "decrease_to_fraction"
|
||||
for relation in frame.bound_relations
|
||||
)
|
||||
|
||||
|
||||
def test_proposal_trace_exists_for_decrease_to_fraction() -> None:
|
||||
frame = build_problem_frame(FRACTION_DECREASE_CASE)
|
||||
|
|
@ -29,7 +71,15 @@ def test_proposal_trace_exists_for_decrease_to_fraction() -> None:
|
|||
proposal = proposals[0]
|
||||
assert proposal.relation_type == "decrease_to_fraction"
|
||||
assert proposal.candidate_organ == "fraction_decrease"
|
||||
assert proposal.status == "closed"
|
||||
assert proposal.status == "proposed"
|
||||
assert proposal.missing_roles == ()
|
||||
assert proposal.active_hazards == ()
|
||||
assert {role.role for role in proposal.role_obligations if role.required} == {
|
||||
"base_quantity",
|
||||
"scale",
|
||||
"state_entity",
|
||||
"transition",
|
||||
}
|
||||
|
||||
|
||||
def test_proposal_trace_includes_exact_evidence_span() -> None:
|
||||
|
|
@ -40,12 +90,8 @@ def test_proposal_trace_includes_exact_evidence_span() -> None:
|
|||
for span in proposal.evidence_spans:
|
||||
assert FRACTION_DECREASE_CASE[span.start:span.end] == span.text
|
||||
|
||||
# Verify key tokens/spans are present in the evidence
|
||||
evidence_texts = {span.text for span in proposal.evidence_spans}
|
||||
assert "decrease to" in evidence_texts
|
||||
assert "3/4" in evidence_texts
|
||||
assert "84" in evidence_texts
|
||||
assert "temperature" in evidence_texts
|
||||
assert len(proposal.evidence_spans) == 1
|
||||
assert proposal.evidence_spans[0].text == "decrease to 3/4 of"
|
||||
|
||||
|
||||
def test_proposal_family_is_catalog_backed() -> None:
|
||||
|
|
@ -68,6 +114,8 @@ def test_proposal_is_diagnostic_only_serving_disallowed() -> None:
|
|||
assert family is not None
|
||||
assert family.diagnostic_only is True
|
||||
assert family.serving_allowed is False
|
||||
assert proposal.diagnostic_only is True
|
||||
assert proposal.serving_allowed is False
|
||||
|
||||
|
||||
def test_blocked_final_value_proportional_case_produces_proposal_but_remains_blocked() -> None:
|
||||
|
|
@ -76,12 +124,45 @@ def test_blocked_final_value_proportional_case_produces_proposal_but_remains_blo
|
|||
assert len(proposals) == 1
|
||||
|
||||
proposal = proposals[0]
|
||||
# The final value question target is not a delta/decrease target, so contract is not runnable
|
||||
assert proposal.status != "closed"
|
||||
assert "delta_decrease_target_unbound" in proposal.missing_roles
|
||||
assessment = next(
|
||||
item for item in assess_contracts(frame)
|
||||
if item.candidate_organ == "fraction_decrease"
|
||||
)
|
||||
|
||||
# The proposal remains a hypothesis; ContractAssessment alone determines closure.
|
||||
assert proposal.status == "proposed"
|
||||
assert not assessment.runnable
|
||||
assert "delta_decrease_target_unbound" in assessment.missing_bindings
|
||||
|
||||
|
||||
def test_no_proposal_trace_for_affine_more_than_confuser() -> None:
|
||||
frame = build_problem_frame(AFFINE_CONFUSER)
|
||||
proposals = [p for p in frame.proposals if p.family_id == "proportional_change.decrease_to_fraction"]
|
||||
assert len(proposals) == 0
|
||||
|
||||
|
||||
def test_multiple_fraction_confuser_is_not_proposed_or_assessed() -> None:
|
||||
frame = build_problem_frame(MULTIPLE_FRACTION_CONFUSER)
|
||||
|
||||
assert not any(
|
||||
proposal.family_id == "proportional_change.decrease_to_fraction"
|
||||
for proposal in frame.proposals
|
||||
)
|
||||
assert not any(
|
||||
assessment.candidate_organ == "fraction_decrease"
|
||||
for assessment in assess_contracts(frame)
|
||||
)
|
||||
|
||||
|
||||
def test_fraction_contract_dispatch_requires_the_proposal_first_trace() -> None:
|
||||
frame = build_problem_frame(FRACTION_DECREASE_CASE)
|
||||
relation_only_frame = dataclasses.replace(frame, proposals=())
|
||||
|
||||
assert any(
|
||||
relation.relation_type == "decrease_to_fraction"
|
||||
for relation in relation_only_frame.bound_relations
|
||||
)
|
||||
assert not any(
|
||||
assessment.candidate_organ == "fraction_decrease"
|
||||
for assessment in assess_contracts(relation_only_frame)
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue