feat(kernel): route proportional-decrease through construction proposals (#837)
* feat(kernel): route proportional-decrease through construction proposals * docs: clarify PR #837 scope in lookback
This commit is contained in:
parent
ec579339ea
commit
fbccb2a298
6 changed files with 198 additions and 1 deletions
|
|
@ -215,3 +215,41 @@ The substantive result of this session is not just “0005 runnable.” It is th
|
|||
- **Targeted suite**: `tests/test_problem_frame_builder.py`, `tests/test_problem_frame_contracts.py`, `tests/test_construction_affordances.py`, `tests/test_gsm8k_problem_frame_adequacy.py`, `tests/test_gsm8k_morphology_missing_kernel_labels.py` -> **46 passed** in `0.48s`.
|
||||
- **Smoke suite**: `core.cli test --suite smoke -q` -> **108 passed** in `129.00s`.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## PR #837 Addendum — feat(kernel): route proportional-decrease through construction proposals
|
||||
|
||||
### Agent and Session
|
||||
|
||||
- **Agent:** Antigravity (Gemini 3.5 Flash)
|
||||
- **Date:** 2026-06-20
|
||||
- **Task:** Implement `feat(kernel): route proportional-decrease through construction proposals` (PR #837)
|
||||
- **Branch:** `feat/kernel-decrease-construction-proposal-trace`
|
||||
|
||||
### Modules Touched / Created
|
||||
|
||||
| File | Change type | Summary |
|
||||
|---|---|---|
|
||||
| `generate/problem_frame.py` | modified | Added `proposals` field (`tuple[ConstructionProposal, ...]`) to the `ProblemFrame` dataclass, importing `ConstructionProposal` under `TYPE_CHECKING`. |
|
||||
| `generate/problem_frame_builder.py` | modified | Updated `build_problem_frame` to perform a two-pass assembly: running contract assessments on the initial frame, mapping them to proposals using `make_proposal`, and returning the frame with `proposals` attached. |
|
||||
| `generate/problem_frame_contracts.py` | modified | Added `get_contract_family_id(candidate_organ: str) -> str | None` to publicly map candidate organs to catalog family IDs without exposing the private contract registry. |
|
||||
| `tests/test_proportional_decrease_proposal.py` | NEW | Added 6 new test cases verifying proposal trace existence, evidence spans, catalog backing, diagnostic-only constraints, blocked final-value behavior, and confuser exclusion. |
|
||||
| `docs/analysis/pr837-lookback-2026-06-20.md` | NEW | Lookback documenting PR #837 design rationale, implementation details, and verification results. |
|
||||
|
||||
### Invariants Verified
|
||||
|
||||
- **Versor condition**: No geometric algebra files were touched; versor invariants remain fully satisfied.
|
||||
- **Serving parity**:
|
||||
- Train: `30 correct / 20 refused / 0 wrong` (wrong_ids: `[]`)
|
||||
- Holdout: `5 correct / 495 refused / 0 wrong` (wrong_ids: `[]`)
|
||||
- **Adequacy**:
|
||||
- Train runnable: `2` (v1-0005 and v1-0046)
|
||||
- Holdout runnable: `0`
|
||||
|
||||
### Tests Run
|
||||
|
||||
- **Targeted suite**: `tests/test_proportional_decrease_proposal.py`, `tests/test_construction_affordances.py`, `tests/test_problem_frame_builder.py`, `tests/test_problem_frame_contracts.py`, `tests/test_gsm8k_problem_frame_adequacy.py`, `tests/test_gsm8k_morphology_missing_kernel_labels.py`, `tests/test_kernel_no_new_legacy_derivation_surfaces.py` -> **54 passed** in `0.66s`.
|
||||
- **Smoke suite**: `core.cli test --suite smoke -q` -> Running in background.
|
||||
|
||||
|
||||
|
|
|
|||
42
docs/analysis/pr837-lookback-2026-06-20.md
Normal file
42
docs/analysis/pr837-lookback-2026-06-20.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# PR #837 Lookback — feat(kernel): route proportional-decrease through construction proposals
|
||||
|
||||
This lookback documents the design rationale, implementation details, and verification results for routing the proportional-decrease diagnostic path through typed `ConstructionProposal` objects.
|
||||
|
||||
## Rationale & Design Choices
|
||||
|
||||
### 1. Attaching `proposals` to `ProblemFrame`
|
||||
We introduced the `proposals` field (`tuple[ConstructionProposal, ...]`) directly on the `ProblemFrame` class. This allows the frame representation to carry its candidate construction traces, linking surface evidence to the construction-affordance catalog in a self-contained manner.
|
||||
|
||||
### 2. Integration in `build_problem_frame`
|
||||
To avoid circular imports and keep `ProblemFrameBuilder` clean, the proposals are attached in `build_problem_frame()` via a two-pass assembly:
|
||||
1. Build the initial `ProblemFrame` with basic attributes.
|
||||
2. Call `assess_contracts()` on the initial frame to retrieve `ContractAssessment` objects.
|
||||
3. For each assessment whose candidate organ maps to a catalog family (via `get_contract_family_id()`), construct a `ConstructionProposal` via `make_proposal()`.
|
||||
4. Recreate the `ProblemFrame` with the proposals tuple attached using `dataclasses.replace()`.
|
||||
|
||||
### 3. Exposing Public Accessors
|
||||
We added the public helper `get_contract_family_id(candidate_organ: str) -> str | None` in `generate/problem_frame_contracts.py`. This avoids exposing the private mapping dict `_CONTRACT_REGISTRY` to external modules or test suites.
|
||||
|
||||
### 4. Scope & Boundary Clarification
|
||||
PR #837 attaches assessment-backed proposal traces to `ProblemFrame` as a diagnostic trace. It does *not* make construction proposals drive relation binding or contract assessment, which remain decoupled at this stage.
|
||||
|
||||
|
||||
## Tests Added
|
||||
|
||||
We introduced a dedicated test file `tests/test_proportional_decrease_proposal.py` covering:
|
||||
- **Proposal trace existence**: Verifies that a `proportional_change.decrease_to_fraction` proposal is generated for proportional-decrease cases.
|
||||
- **Exact evidence spans**: Asserts that proposal evidence spans correctly slice the original problem text.
|
||||
- **Catalog alignment**: Verifies the proposal family ID, relation type, and candidate organ align with the catalog definition using public accessors.
|
||||
- **Diagnostic-only constraints**: Validates that the underlying family has `diagnostic_only=True` and `serving_allowed=False`.
|
||||
- **Blocked case handling**: Asserts that `FINAL_VALUE_CONFUSER` (a final-value question instead of a decrease-delta question) still produces a proposal trace but has a blocked status (not `"closed"`) and identifies `delta_decrease_target_unbound`.
|
||||
- **Confuser exclusion**: Verifies that `AFFINE_CONFUSER` produces no proportional-decrease proposal trace.
|
||||
|
||||
## Verification Results
|
||||
|
||||
- All 54 tests pass successfully.
|
||||
- Parity of adequacy metrics verified:
|
||||
- Train runnable contracts remain `2` (`0005`, `0046`).
|
||||
- Holdout runnable contracts remain `0`.
|
||||
- Serving counts verified:
|
||||
- Train: `30 correct / 20 refused / 0 wrong` (wrong_ids: `[]`)
|
||||
- Holdout: `5 correct / 495 refused / 0 wrong` (wrong_ids: `[]`)
|
||||
|
|
@ -25,6 +25,7 @@ if TYPE_CHECKING:
|
|||
)
|
||||
from language_packs.scalar_equivalence import ScalarCandidate
|
||||
from generate.process_frames import ProcessFrame
|
||||
from generate.construction_affordances import ConstructionProposal
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -114,6 +115,7 @@ class ProblemFrame:
|
|||
bindings: tuple[MentionBinding, ...] = ()
|
||||
bound_relations: tuple[BoundRelation, ...] = ()
|
||||
bound_question_target: BoundQuestionTarget | None = None
|
||||
proposals: tuple[ConstructionProposal, ...] = ()
|
||||
|
||||
|
||||
class ProblemFrameBuilder:
|
||||
|
|
|
|||
|
|
@ -748,7 +748,28 @@ def build_problem_frame(problem_text: str) -> ProblemFrame:
|
|||
if bound_target is not None:
|
||||
builder.set_bound_question_target(bound_target)
|
||||
|
||||
return builder.build()
|
||||
initial_frame = builder.build()
|
||||
|
||||
from generate.problem_frame_contracts import assess_contracts, get_contract_family_id
|
||||
from generate.construction_affordances import make_proposal
|
||||
|
||||
assessments = assess_contracts(initial_frame)
|
||||
proposals = []
|
||||
for assessment in assessments:
|
||||
family_id = get_contract_family_id(assessment.candidate_organ)
|
||||
if family_id is not None:
|
||||
proposal = make_proposal(
|
||||
family_id=family_id,
|
||||
evidence_spans=assessment.evidence_spans,
|
||||
assessment_runnable=assessment.runnable,
|
||||
missing_roles=assessment.missing_bindings,
|
||||
active_hazards=assessment.unresolved_hazards,
|
||||
)
|
||||
proposals.append(proposal)
|
||||
|
||||
import dataclasses
|
||||
return dataclasses.replace(initial_frame, proposals=tuple(proposals))
|
||||
|
||||
|
||||
|
||||
def recognized_scalar_surfaces(frame: ProblemFrame) -> tuple[str, ...]:
|
||||
|
|
|
|||
|
|
@ -52,6 +52,13 @@ _CONTRACT_REGISTRY: dict[str, ConstructionContract] = {
|
|||
}
|
||||
|
||||
|
||||
def get_contract_family_id(candidate_organ: str) -> str | None:
|
||||
"""Return the catalog family ID for a candidate organ, if registered."""
|
||||
contract = _CONTRACT_REGISTRY.get(candidate_organ)
|
||||
return contract.family.family_id if contract else None
|
||||
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ContractAssessment:
|
||||
candidate_organ: str
|
||||
|
|
|
|||
87
tests/test_proportional_decrease_proposal.py
Normal file
87
tests/test_proportional_decrease_proposal.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""Tests for feat(kernel): route proportional-decrease through construction proposals."""
|
||||
from __future__ import annotations
|
||||
|
||||
from generate.problem_frame_builder import build_problem_frame
|
||||
from generate.construction_affordances import lookup_family
|
||||
|
||||
|
||||
FRACTION_DECREASE_CASE = (
|
||||
"In one hour, Addison mountain's temperature will decrease to 3/4 of its temperature. "
|
||||
"If the current temperature of the mountain is 84 degrees, what will the temperature "
|
||||
"decrease by?"
|
||||
)
|
||||
|
||||
FINAL_VALUE_CONFUSER = (
|
||||
"In one hour, the lake's temperature will decrease to 3/4 of its temperature. "
|
||||
"If the current temperature of the lake is 80 degrees, what will the temperature be?"
|
||||
)
|
||||
|
||||
AFFINE_CONFUSER = (
|
||||
"Marion has 1/4 more than what Yun currently has, plus 7. How many paperclips does Marion have?"
|
||||
)
|
||||
|
||||
|
||||
def test_proposal_trace_exists_for_decrease_to_fraction() -> None:
|
||||
frame = build_problem_frame(FRACTION_DECREASE_CASE)
|
||||
proposals = [p for p in frame.proposals if p.family_id == "proportional_change.decrease_to_fraction"]
|
||||
assert len(proposals) == 1
|
||||
|
||||
proposal = proposals[0]
|
||||
assert proposal.relation_type == "decrease_to_fraction"
|
||||
assert proposal.candidate_organ == "fraction_decrease"
|
||||
assert proposal.status == "closed"
|
||||
|
||||
|
||||
def test_proposal_trace_includes_exact_evidence_span() -> None:
|
||||
frame = build_problem_frame(FRACTION_DECREASE_CASE)
|
||||
proposal = next(p for p in frame.proposals if p.family_id == "proportional_change.decrease_to_fraction")
|
||||
|
||||
# Verify evidence spans slice the original text exactly
|
||||
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
|
||||
|
||||
|
||||
def test_proposal_family_is_catalog_backed() -> None:
|
||||
frame = build_problem_frame(FRACTION_DECREASE_CASE)
|
||||
proposal = next(p for p in frame.proposals if p.family_id == "proportional_change.decrease_to_fraction")
|
||||
|
||||
# Use public catalog accessor to look up the family
|
||||
family = lookup_family(proposal.family_id)
|
||||
assert family is not None
|
||||
assert family.family_id == "proportional_change.decrease_to_fraction"
|
||||
assert family.signature.relation_type == proposal.relation_type
|
||||
assert family.signature.candidate_organ == proposal.candidate_organ
|
||||
|
||||
|
||||
def test_proposal_is_diagnostic_only_serving_disallowed() -> None:
|
||||
frame = build_problem_frame(FRACTION_DECREASE_CASE)
|
||||
proposal = next(p for p in frame.proposals if p.family_id == "proportional_change.decrease_to_fraction")
|
||||
|
||||
family = lookup_family(proposal.family_id)
|
||||
assert family is not None
|
||||
assert family.diagnostic_only is True
|
||||
assert family.serving_allowed is False
|
||||
|
||||
|
||||
def test_blocked_final_value_proportional_case_produces_proposal_but_remains_blocked() -> None:
|
||||
frame = build_problem_frame(FINAL_VALUE_CONFUSER)
|
||||
proposals = [p for p in frame.proposals if p.family_id == "proportional_change.decrease_to_fraction"]
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
Loading…
Reference in a new issue