feat(kernel): route percent-partition through proposal-first seam
This commit is contained in:
parent
955fec3bc3
commit
2c3b2ed315
4 changed files with 309 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
153
tests/test_percent_partition_proposal.py
Normal file
153
tests/test_percent_partition_proposal.py
Normal 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
|
||||
Loading…
Reference in a new issue