feat(kernel): route percent-partition through proposal-first seam (#842)

This commit is contained in:
Shay 2026-06-20 16:00:35 -07:00 committed by GitHub
parent 955fec3bc3
commit 8e89562f6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 309 additions and 6 deletions

View file

@ -490,3 +490,122 @@ PR #840 makes the family-spec gate structurally inspectable; it authorizes nothi
- pre-edit-sweep: completed manually across imports/call sites in `generate/`, `tests/`, `evals/`, and `calibration/`
- claim-proposal-guardian: not needed; no claim or proposal mutation path touched
- Other: none
---
## PR #842 Addendum — feat(kernel): route percent-partition 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 `partition.percent_partition` from process-frame-triggered assessment dispatch to the ADR-0223/0224 proposal-first construction seam, without changing serving or derivation organs
- **Branch:** `feat/kernel-percent-partition-proposal-first`
- **Base:** `origin/main` at `955fec3b` (merged PR #841)
### Smoke Suite + Bootstrap Status
```text
uv run python -m core.cli test --suite smoke -q
108 passed, 1 warning in 128.03s (0:02:08)
```
The warning reports an existing `engine_state` checkpoint revision mismatch (`4bab1638` versus `955fec3b`); no engine-state or runtime file was modified. `core-bootstrap` was unavailable as a skill, so the `GPT55.md` checklist was completed manually: `GPT55.md`, `AGENTS.md`, `docs/runtime_contracts.md`, and the current handoff were read; baseline state and branch ancestry were inspected; the pre-edit import/call-site/test sweep was completed.
### Modules Touched
| File | Change type | Summary |
|---|---|---|
| `generate/problem_frame_builder.py` | modified | Added a narrow pre-assessment percent-partition proposal factory requiring an existing partition/consumption process cue plus explicit `N% of` evidence; attaches the proposal before mention/relation binding and contract assessment. |
| `generate/problem_frame_contracts.py` | modified | Replaced percent-partition process-frame-name dispatch with catalog-family proposal-gated dispatch; left `assess_percent_partition()` structurally unchanged. |
| `tests/test_percent_partition_proposal.py` | new | Proves proposal posture/obligations/order, exact evidence spans, legacy-adapter exclusion, assessment authority, no-proposal dispatch refusal, and percent-change confuser refusal. |
| `HANDOFF-gpt55-2026-06-20.md` | modified | Added this PR #842 continuity record. |
### Invariants Verified
- **Field closure:** Preserved by scope and smoke. No algebra, field, propagation, normalization, vault, or runtime field-state path was touched; no field operator was added.
- **Proposal/grounding/contract duality:** `propose_construction()` creates only `status="proposed"`; exact bound relations remain grounding evidence; `assess_percent_partition()` remains the only runnable/refused authority.
- **Diagnostic-only / serving forbidden:** Catalog-derived proposal retains `diagnostic_only=True` and `serving_allowed=False`; serving train/holdout results are byte-equivalent in outcome counts with `wrong_ids == []`.
- **No legacy adapter for migrated families:** Focused regression makes `make_proposal()` fail if percent partition reaches it; the supported path remains green.
- **No broad parser or state-change runtime:** Proposal recognition is limited to the existing partition/consumption process cue plus explicit `N% of` surface evidence. The percent-change confuser can be proposed as a hypothesis but is contract-refused.
- **Exact recall / epistemic / identity:** No recall, CGA ranking, ANN/cosine, teaching mutation, pack, policy, identity, or epistemic transition path was touched.
- **Diagnostic readiness:** Train runnable count remains `2`; holdout runnable count remains `0`.
### Subagent / Arena Reconciliation
- Number of subagents spawned: 0
- Each subagent independently verified versor closure: N/A
- Reconciliation: N/A
### Tests Run
```bash
# exit 0 — 54 passed
uv run python -m pytest -q \
tests/test_construction_affordances.py \
tests/test_problem_frame_builder.py \
tests/test_problem_frame_contracts.py \
tests/test_proportional_decrease_proposal.py \
tests/test_percent_partition_proposal.py \
tests/test_kernel_no_new_legacy_derivation_surfaces.py
# exit 0 — 108 passed, one pre-existing state-revision warning
uv run python -m core.cli test --suite smoke -q
# exit 0
uv run ruff check \
generate/problem_frame_builder.py \
generate/problem_frame_contracts.py \
tests/test_percent_partition_proposal.py
# 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 — train serving parity
# {'correct': 30, 'wrong': 0, 'refused': 20}; wrong_ids == []
# exit 0 — holdout serving parity
# {'correct': 5, 'wrong': 0, 'refused': 495}; wrong_ids == []
# exit 0 — diagnostic readiness
# train contract_runnable_count == 2
# holdout contract_runnable_count == 0
# exit 0
git diff --check
```
### Open Tasks / Next Session Entry Point
1. Review the four changed files, commit intentionally, push `feat/kernel-percent-partition-proposal-first`, and open PR #842 using the brief's required body sections.
2. Leave `make_proposal()` cleanup for a separate small follow-up; both catalog families now bypass it, but deletion was intentionally not mixed into this migration.
### Known Hazards / Do Not Touch
- Do not broaden `_PERCENT_OF_PROPOSAL_RE` into general percentage or state-change recognition. Proposal closeness is intentionally narrower than contract authority.
- Do not weaken `assess_percent_partition()` obligations to recover diagnostic counts; its whole/part/scale topology and forward-target checks remain load-bearing.
- Do not connect this diagnostic proposal to serving, derivation organs, `math_candidate_graph`, foundational-family implementation, pack mutation, or runtime admission.
### Architectural Decision
Percent partition now follows the same ordering proven by PR #841: `surface/process cue → proposed ConstructionProposal → bound percent/subgroup relations → ContractAssessment`. Process-frame similarity authorizes only a hypothesis; exact span bindings ground it; the organ-specific contract determines closure or refusal.
### What Must Not Be Forgotten
Both construction-affordance catalog families are now proposal-first, but proposals remain diagnostic-only and serving-disallowed. `ContractAssessment`, not `ConstructionProposal`, is the executable readiness authority.
**Prompt Library Reference**: See `docs/core-rd-base-prompts.md` (especially sections #7 Standing Loop Axiom Check, #8 PR Merge-Readiness Audit, and #9 Grok Build Implementation Session). Use them as standing prefixes.
### 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/`, and `calibration/`
- claim-proposal-guardian: unavailable; proposal status and serving posture verified through catalog/focused tests
- Other: none

View file

@ -321,6 +321,10 @@ _DECREASE_TO_FRACTION_RE = re.compile(
r"(?P<transition>decrease\s+to)\s+(?P<fraction>\d+\s*/\s*\d+)\s+of",
re.IGNORECASE,
)
_PERCENT_OF_PROPOSAL_RE = re.compile(
r"\b\d+(?:\.\d+)?\s*%\s+of\b",
re.IGNORECASE,
)
_DECREASE_STATE_RE = re.compile(
r"(?P<state>[A-Za-z][A-Za-z'-]*)\s+will\s+decrease\s+to",
re.IGNORECASE,
@ -359,6 +363,30 @@ def _proportional_decrease_proposals(text: str) -> tuple[ConstructionProposal, .
)
def _percent_partition_proposals(
text: str,
frames: tuple[ProcessFrame, ...],
) -> tuple[ConstructionProposal, ...]:
"""Propose percent partition from a process cue plus explicit percent-of."""
frame_names = {frame.name for frame in frames}
if not frame_names & {"partition", "consumption"}:
return ()
evidence_spans = tuple(
SourceSpan(text[match.start():match.end()], match.start(), match.end())
for match in _PERCENT_OF_PROPOSAL_RE.finditer(text)
)
if not evidence_spans:
return ()
return (
propose_construction(
"partition.percent_partition",
evidence_spans,
),
)
def _extract_mentions(
text: str,
quantities: tuple[GroundedScalar, ...],
@ -753,11 +781,13 @@ 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.
# ADR-0223/0224: surface/process evidence proposes catalog constructions
# before role binding and ContractAssessment. Proposals remain diagnostic
# hypotheses; bound relations ground and organ contracts determine.
for proposal in _proportional_decrease_proposals(problem_text):
builder.add_proposal(proposal)
for proposal in _percent_partition_proposals(problem_text, frames):
builder.add_proposal(proposal)
mentions = _extract_mentions(problem_text, tuple(grounded_quantities), units)
bindings = _extract_bindings(problem_text, mentions)

View file

@ -299,8 +299,9 @@ def assess_contracts(frame: ProblemFrame) -> tuple[ContractAssessment, ...]:
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``.
2. ``percent_partition`` triggered by its proposal-first catalog family
in ``frame.proposals``. Routes to ``assess_percent_partition``, which
still determines closure from bound frame evidence.
Registry key: ``_CONTRACT_REGISTRY["percent_partition"]``.
3. ``container_packing`` / ``labor_rate`` inline skeleton assessments;
not yet in the catalog registry (added to registry when obligations are
@ -318,7 +319,7 @@ def assess_contracts(frame: ProblemFrame) -> tuple[ContractAssessment, ...]:
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"}:
if _PERCENT_PARTITION_FAMILY.family_id in proposed_family_ids:
# Catalog: _CONTRACT_REGISTRY["percent_partition"]
results.append(assess_percent_partition(frame))

View file

@ -0,0 +1,153 @@
"""Percent-partition proposal-first construction seam tests."""
from __future__ import annotations
import dataclasses
import generate.construction_affordances as construction_affordances
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_contracts import assess_contracts
PERCENT_PARTITION_CASE = (
"A school has 100 students. Half of the students are girls, the other half are boys. "
"20% of the girls have dogs at home and 10% of the boys have dogs at home. "
"How many students own dogs?"
)
ONE_SUBGROUP_CONFUSER = (
"There are 100 students. Half are girls. 30% of the girls own pets. "
"How many students own pets?"
)
PERCENT_CHANGE_CONFUSER = "A store spent 20% of its budget. How many dollars remain?"
def _percent_partition_proposal(frame):
return next(
proposal
for proposal in frame.proposals
if proposal.family_id == "partition.percent_partition"
)
def _percent_partition_assessment(frame):
return next(
assessment
for assessment in assess_contracts(frame)
if assessment.candidate_organ == "percent_partition"
)
def test_supported_case_has_diagnostic_catalog_proposal() -> None:
proposal = _percent_partition_proposal(build_problem_frame(PERCENT_PARTITION_CASE))
assert proposal.status == "proposed"
assert proposal.diagnostic_only is True
assert proposal.serving_allowed is False
assert {role.role for role in proposal.role_obligations if role.required} == {
"whole",
"part",
"scale",
}
assert tuple(span.text for span in proposal.evidence_spans) == (
"20% of",
"10% of",
)
assert all(
PERCENT_PARTITION_CASE[span.start : span.end] == span.text
for span in proposal.evidence_spans
)
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):
proposal = original_propose(*args, **kwargs)
if proposal.family_id == "partition.percent_partition":
events.append("proposal")
return proposal
def observe_assessment(frame):
events.append("assessment")
assert _percent_partition_proposal(frame).status == "proposed"
assert frame.bound_relations
return original_assess(frame)
monkeypatch.setattr(problem_frame_builder, "propose_construction", observe_proposal)
monkeypatch.setattr(problem_frame_contracts, "assess_contracts", observe_assessment)
build_problem_frame(PERCENT_PARTITION_CASE)
assert events == ["proposal", "assessment"]
def test_migrated_family_does_not_use_legacy_assessment_adapter(monkeypatch) -> None:
original_make_proposal = construction_affordances.make_proposal
def reject_migrated_family(*args, **kwargs):
family_id = kwargs.get("family_id", args[0] if args else None)
if family_id == "partition.percent_partition":
raise AssertionError("migrated family reached legacy make_proposal adapter")
return original_make_proposal(*args, **kwargs)
monkeypatch.setattr(
construction_affordances,
"make_proposal",
reject_migrated_family,
)
assert (
_percent_partition_proposal(build_problem_frame(PERCENT_PARTITION_CASE)).status
== "proposed"
)
def test_contract_assessment_remains_runnable_authority() -> None:
frame = build_problem_frame(PERCENT_PARTITION_CASE)
assert _percent_partition_proposal(frame).status == "proposed"
assert _percent_partition_assessment(frame).runnable is True
def test_positive_proposal_does_not_close_missing_subgroup_bindings() -> None:
frame = build_problem_frame(ONE_SUBGROUP_CONFUSER)
assessment = _percent_partition_assessment(frame)
assert _percent_partition_proposal(frame).status == "proposed"
assert assessment.runnable is False
assert "partition_subgroups_not_distinct" in assessment.missing_bindings
assert "percent_subgroup_links_incomplete" in assessment.missing_bindings
def test_percent_partition_dispatch_requires_proposal_first_trace() -> None:
frame = build_problem_frame(PERCENT_PARTITION_CASE)
relation_only_frame = dataclasses.replace(frame, proposals=())
assert {candidate.name for candidate in relation_only_frame.process_frames} & {
"partition",
"consumption",
}
assert any(
relation.relation_type in {"subgroup_partition", "percent_of"}
for relation in relation_only_frame.bound_relations
)
assert not any(
assessment.candidate_organ == "percent_partition"
for assessment in assess_contracts(relation_only_frame)
)
def test_percent_change_confuser_is_proposed_but_not_runnable() -> None:
frame = build_problem_frame(PERCENT_CHANGE_CONFUSER)
assessment = _percent_partition_assessment(frame)
assert _percent_partition_proposal(frame).status == "proposed"
assert assessment.runnable is False
assert "grounded_partition_subgroup" in assessment.missing_bindings
assert "percent_change_vs_percent_of" in assessment.unresolved_hazards