feat(kernel): route proportional-decrease through proposal-first seam
This commit is contained in:
parent
168c9f26ac
commit
dccc211039
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
|
## PR #836 Addendum — feat(kernel): introduce construction-affordance catalog skeleton
|
||||||
|
|
||||||
### Agent and Session
|
### Agent and Session
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ Design doctrine (from ADR-0223):
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, replace
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -160,9 +160,9 @@ class ConstructionProposal:
|
||||||
→ hazards
|
→ hazards
|
||||||
→ status
|
→ status
|
||||||
|
|
||||||
A proposal is created by ``make_proposal()`` from ProblemFrame evidence.
|
A proposal is created from exact surface evidence before contract
|
||||||
It does not affect serving. Its ``status`` reflects whether the
|
assessment. It does not affect serving. Its initial ``status`` is
|
||||||
corresponding contract assessment declared the frame runnable.
|
``"proposed"``; assessment remains a separate, downstream judgment.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
family_id: Catalog family identifier, e.g.
|
family_id: Catalog family identifier, e.g.
|
||||||
|
|
@ -175,6 +175,10 @@ class ConstructionProposal:
|
||||||
assessment was runnable.
|
assessment was runnable.
|
||||||
missing_roles: Role obligation names that were absent at assessment time.
|
missing_roles: Role obligation names that were absent at assessment time.
|
||||||
active_hazards: Hazard categories that were unresolved 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
|
family_id: str
|
||||||
|
|
@ -184,6 +188,9 @@ class ConstructionProposal:
|
||||||
status: str
|
status: str
|
||||||
missing_roles: tuple[str, ...]
|
missing_roles: tuple[str, ...]
|
||||||
active_hazards: tuple[str, ...]
|
active_hazards: tuple[str, ...]
|
||||||
|
role_obligations: tuple[RoleObligation, ...] = ()
|
||||||
|
diagnostic_only: bool = True
|
||||||
|
serving_allowed: bool = False
|
||||||
|
|
||||||
_VALID_STATUSES: frozenset[str] = frozenset({
|
_VALID_STATUSES: frozenset[str] = frozenset({
|
||||||
"proposed", "partial", "closed", "refused"
|
"proposed", "partial", "closed", "refused"
|
||||||
|
|
@ -195,6 +202,10 @@ class ConstructionProposal:
|
||||||
f"ConstructionProposal.status must be one of "
|
f"ConstructionProposal.status must be one of "
|
||||||
f"{sorted(self._VALID_STATUSES)}, got {self.status!r}"
|
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))
|
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(
|
def make_proposal(
|
||||||
family_id: str,
|
family_id: str,
|
||||||
evidence_spans: tuple[SourceSpan, ...],
|
evidence_spans: tuple[SourceSpan, ...],
|
||||||
|
|
@ -412,11 +454,12 @@ def make_proposal(
|
||||||
missing_roles: tuple[str, ...],
|
missing_roles: tuple[str, ...],
|
||||||
active_hazards: tuple[str, ...],
|
active_hazards: tuple[str, ...],
|
||||||
) -> ConstructionProposal:
|
) -> 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
|
Proportional decrease no longer uses this assessment-backed adapter; it
|
||||||
``assess_contracts()``; this function only maps that evidence to the
|
enters through :func:`propose_construction`. The adapter remains for
|
||||||
standard proposal trace shape.
|
catalog families that have not yet received an authorized proposal-first
|
||||||
|
migration.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
family_id: Catalog family identifier.
|
family_id: Catalog family identifier.
|
||||||
|
|
@ -436,7 +479,7 @@ def make_proposal(
|
||||||
Raises:
|
Raises:
|
||||||
KeyError: If *family_id* is not registered in the catalog.
|
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:
|
if assessment_runnable:
|
||||||
status = "closed"
|
status = "closed"
|
||||||
|
|
@ -447,11 +490,8 @@ def make_proposal(
|
||||||
else:
|
else:
|
||||||
status = "proposed"
|
status = "proposed"
|
||||||
|
|
||||||
return ConstructionProposal(
|
return replace(
|
||||||
family_id=family_id,
|
proposal,
|
||||||
relation_type=family.signature.relation_type,
|
|
||||||
candidate_organ=family.signature.candidate_organ,
|
|
||||||
evidence_spans=evidence_spans,
|
|
||||||
status=status,
|
status=status,
|
||||||
missing_roles=missing_roles,
|
missing_roles=missing_roles,
|
||||||
active_hazards=active_hazards,
|
active_hazards=active_hazards,
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,7 @@ class ProblemFrameBuilder:
|
||||||
self._bindings: list[MentionBinding] = []
|
self._bindings: list[MentionBinding] = []
|
||||||
self._bound_relations: list[BoundRelation] = []
|
self._bound_relations: list[BoundRelation] = []
|
||||||
self._bound_question_target: BoundQuestionTarget | None = None
|
self._bound_question_target: BoundQuestionTarget | None = None
|
||||||
|
self._proposals: list[ConstructionProposal] = []
|
||||||
|
|
||||||
def add_quantity(self, scalar: GroundedScalar) -> None:
|
def add_quantity(self, scalar: GroundedScalar) -> None:
|
||||||
"""Add a GroundedScalar to the frame, collecting hazards and provenance."""
|
"""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:
|
def set_bound_question_target(self, target: BoundQuestionTarget) -> None:
|
||||||
self._bound_question_target = target
|
self._bound_question_target = target
|
||||||
|
|
||||||
|
def add_proposal(self, proposal: ConstructionProposal) -> None:
|
||||||
|
self._proposals.append(proposal)
|
||||||
|
|
||||||
def build(self) -> ProblemFrame:
|
def build(self) -> ProblemFrame:
|
||||||
"""Produce the immutable ProblemFrame."""
|
"""Produce the immutable ProblemFrame."""
|
||||||
return ProblemFrame(
|
return ProblemFrame(
|
||||||
|
|
@ -216,4 +220,5 @@ class ProblemFrameBuilder:
|
||||||
bindings=tuple(self._bindings),
|
bindings=tuple(self._bindings),
|
||||||
bound_relations=tuple(self._bound_relations),
|
bound_relations=tuple(self._bound_relations),
|
||||||
bound_question_target=self._bound_question_target,
|
bound_question_target=self._bound_question_target,
|
||||||
|
proposals=tuple(self._proposals),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,11 @@ Non-goals:
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
import re
|
import re
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
|
|
||||||
|
from generate.construction_affordances import ConstructionProposal, propose_construction
|
||||||
from generate.kernel_facts import (
|
from generate.kernel_facts import (
|
||||||
BoundRelation,
|
BoundRelation,
|
||||||
BoundRole,
|
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(
|
def _extract_mentions(
|
||||||
text: str,
|
text: str,
|
||||||
quantities: tuple[GroundedScalar, ...],
|
quantities: tuple[GroundedScalar, ...],
|
||||||
|
|
@ -732,6 +753,12 @@ def build_problem_frame(problem_text: str) -> ProblemFrame:
|
||||||
if question_target is not None:
|
if question_target is not None:
|
||||||
builder.set_question_target(question_target)
|
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)
|
mentions = _extract_mentions(problem_text, tuple(grounded_quantities), units)
|
||||||
bindings = _extract_bindings(problem_text, mentions)
|
bindings = _extract_bindings(problem_text, mentions)
|
||||||
for mention in mentions:
|
for mention in mentions:
|
||||||
|
|
@ -754,10 +781,11 @@ def build_problem_frame(problem_text: str) -> ProblemFrame:
|
||||||
from generate.construction_affordances import make_proposal
|
from generate.construction_affordances import make_proposal
|
||||||
|
|
||||||
assessments = assess_contracts(initial_frame)
|
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:
|
for assessment in assessments:
|
||||||
family_id = get_contract_family_id(assessment.candidate_organ)
|
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(
|
proposal = make_proposal(
|
||||||
family_id=family_id,
|
family_id=family_id,
|
||||||
evidence_spans=assessment.evidence_spans,
|
evidence_spans=assessment.evidence_spans,
|
||||||
|
|
@ -766,8 +794,8 @@ def build_problem_frame(problem_text: str) -> ProblemFrame:
|
||||||
active_hazards=assessment.unresolved_hazards,
|
active_hazards=assessment.unresolved_hazards,
|
||||||
)
|
)
|
||||||
proposals.append(proposal)
|
proposals.append(proposal)
|
||||||
|
proposed_family_ids.add(family_id)
|
||||||
|
|
||||||
import dataclasses
|
|
||||||
return dataclasses.replace(initial_frame, proposals=tuple(proposals))
|
return dataclasses.replace(initial_frame, proposals=tuple(proposals))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ from generate.construction_affordances import (
|
||||||
ConstructionContract,
|
ConstructionContract,
|
||||||
_DECREASE_TO_FRACTION_FAMILY,
|
_DECREASE_TO_FRACTION_FAMILY,
|
||||||
_PERCENT_PARTITION_FAMILY,
|
_PERCENT_PARTITION_FAMILY,
|
||||||
make_proposal,
|
|
||||||
)
|
)
|
||||||
from generate.kernel_facts import BoundRelation, SourceSpan
|
from generate.kernel_facts import BoundRelation, SourceSpan
|
||||||
from generate.problem_frame import ProblemFrame
|
from generate.problem_frame import ProblemFrame
|
||||||
|
|
@ -296,8 +295,9 @@ def assess_contracts(frame: ProblemFrame) -> tuple[ContractAssessment, ...]:
|
||||||
"""Return deterministic diagnostic assessments; never admits serving.
|
"""Return deterministic diagnostic assessments; never admits serving.
|
||||||
|
|
||||||
Dispatch order:
|
Dispatch order:
|
||||||
1. ``decrease_to_fraction`` — triggered by relation type presence in
|
1. ``decrease_to_fraction`` — triggered by its proposal-first catalog
|
||||||
``frame.bound_relations``. Routes to ``assess_fraction_decrease``.
|
family in ``frame.proposals``. Routes to ``assess_fraction_decrease``,
|
||||||
|
which still determines closure from bound frame evidence.
|
||||||
Registry key: ``_CONTRACT_REGISTRY["fraction_decrease"]``.
|
Registry key: ``_CONTRACT_REGISTRY["fraction_decrease"]``.
|
||||||
2. ``percent_partition`` — triggered by process-frame names ``partition``
|
2. ``percent_partition`` — triggered by process-frame names ``partition``
|
||||||
or ``consumption``. Routes to ``assess_percent_partition``.
|
or ``consumption``. Routes to ``assess_percent_partition``.
|
||||||
|
|
@ -314,7 +314,8 @@ def assess_contracts(frame: ProblemFrame) -> tuple[ContractAssessment, ...]:
|
||||||
results: list[ContractAssessment] = []
|
results: list[ContractAssessment] = []
|
||||||
|
|
||||||
# Registry-backed diagnostic families
|
# 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"]
|
# Catalog: _CONTRACT_REGISTRY["fraction_decrease"]
|
||||||
results.append(assess_fraction_decrease(frame))
|
results.append(assess_fraction_decrease(frame))
|
||||||
if frame_names & {"partition", "consumption"}:
|
if frame_names & {"partition", "consumption"}:
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ from generate.construction_affordances import (
|
||||||
lookup_by_organ,
|
lookup_by_organ,
|
||||||
lookup_by_relation_type,
|
lookup_by_relation_type,
|
||||||
make_proposal,
|
make_proposal,
|
||||||
|
propose_construction,
|
||||||
ConstructionProposal,
|
ConstructionProposal,
|
||||||
_CATALOG,
|
|
||||||
)
|
)
|
||||||
from generate.kernel_facts import SourceSpan
|
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:
|
def test_construction_proposal_status_validation() -> None:
|
||||||
span = SourceSpan("test text", 0, 9)
|
span = SourceSpan("test text", 0, 9)
|
||||||
# Valid status doesn't raise
|
# Valid status doesn't raise
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
"""Tests for feat(kernel): route proportional-decrease through construction proposals."""
|
"""Tests for feat(kernel): route proportional-decrease through construction proposals."""
|
||||||
from __future__ import annotations
|
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.problem_frame_builder import build_problem_frame
|
||||||
from generate.construction_affordances import lookup_family
|
from generate.construction_affordances import lookup_family
|
||||||
|
from generate.problem_frame_contracts import assess_contracts
|
||||||
|
|
||||||
|
|
||||||
FRACTION_DECREASE_CASE = (
|
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?"
|
"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:
|
def test_proposal_trace_exists_for_decrease_to_fraction() -> None:
|
||||||
frame = build_problem_frame(FRACTION_DECREASE_CASE)
|
frame = build_problem_frame(FRACTION_DECREASE_CASE)
|
||||||
|
|
@ -29,7 +71,15 @@ def test_proposal_trace_exists_for_decrease_to_fraction() -> None:
|
||||||
proposal = proposals[0]
|
proposal = proposals[0]
|
||||||
assert proposal.relation_type == "decrease_to_fraction"
|
assert proposal.relation_type == "decrease_to_fraction"
|
||||||
assert proposal.candidate_organ == "fraction_decrease"
|
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:
|
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:
|
for span in proposal.evidence_spans:
|
||||||
assert FRACTION_DECREASE_CASE[span.start:span.end] == span.text
|
assert FRACTION_DECREASE_CASE[span.start:span.end] == span.text
|
||||||
|
|
||||||
# Verify key tokens/spans are present in the evidence
|
assert len(proposal.evidence_spans) == 1
|
||||||
evidence_texts = {span.text for span in proposal.evidence_spans}
|
assert proposal.evidence_spans[0].text == "decrease to 3/4 of"
|
||||||
assert "decrease to" in evidence_texts
|
|
||||||
assert "3/4" in evidence_texts
|
|
||||||
assert "84" in evidence_texts
|
|
||||||
assert "temperature" in evidence_texts
|
|
||||||
|
|
||||||
|
|
||||||
def test_proposal_family_is_catalog_backed() -> None:
|
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 is not None
|
||||||
assert family.diagnostic_only is True
|
assert family.diagnostic_only is True
|
||||||
assert family.serving_allowed is False
|
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:
|
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
|
assert len(proposals) == 1
|
||||||
|
|
||||||
proposal = proposals[0]
|
proposal = proposals[0]
|
||||||
# The final value question target is not a delta/decrease target, so contract is not runnable
|
assessment = next(
|
||||||
assert proposal.status != "closed"
|
item for item in assess_contracts(frame)
|
||||||
assert "delta_decrease_target_unbound" in proposal.missing_roles
|
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:
|
def test_no_proposal_trace_for_affine_more_than_confuser() -> None:
|
||||||
frame = build_problem_frame(AFFINE_CONFUSER)
|
frame = build_problem_frame(AFFINE_CONFUSER)
|
||||||
proposals = [p for p in frame.proposals if p.family_id == "proportional_change.decrease_to_fraction"]
|
proposals = [p for p in frame.proposals if p.family_id == "proportional_change.decrease_to_fraction"]
|
||||||
assert len(proposals) == 0
|
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