refactor(kernel): fence legacy assessment-backed proposal adapter (#844)
* test(kernel): reject migrated proposal adapter paths * refactor(kernel): fence legacy assessment-backed proposal adapter
This commit is contained in:
parent
2802562da8
commit
a266324eea
2 changed files with 88 additions and 98 deletions
|
|
@ -385,6 +385,11 @@ _BY_RELATION_TYPE: dict[str, ConstructionFamily] = {
|
|||
for family in _CATALOG.values()
|
||||
}
|
||||
|
||||
_PROPOSAL_FIRST_FAMILIES: frozenset[str] = frozenset({
|
||||
"proportional_change.decrease_to_fraction",
|
||||
"partition.percent_partition",
|
||||
})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public accessors
|
||||
|
|
@ -456,10 +461,9 @@ def make_proposal(
|
|||
) -> ConstructionProposal:
|
||||
"""Map assessment evidence onto a proposal for legacy catalog paths.
|
||||
|
||||
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.
|
||||
Migrated proposal-first families must enter through
|
||||
:func:`propose_construction`. This adapter remains only for explicitly
|
||||
unmigrated catalog paths that still synthesize proposals from assessments.
|
||||
|
||||
Args:
|
||||
family_id: Catalog family identifier.
|
||||
|
|
@ -478,7 +482,12 @@ def make_proposal(
|
|||
|
||||
Raises:
|
||||
KeyError: If *family_id* is not registered in the catalog.
|
||||
ValueError: If *family_id* has already migrated to the proposal-first seam.
|
||||
"""
|
||||
if family_id in _PROPOSAL_FIRST_FAMILIES:
|
||||
raise ValueError(
|
||||
f"{family_id} is proposal-first; use propose_construction before assessment"
|
||||
)
|
||||
proposal = propose_construction(family_id, evidence_spans)
|
||||
|
||||
if assessment_runnable:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from generate.construction_affordances import (
|
||||
ConstructionProposal,
|
||||
all_diagnostic_families,
|
||||
lookup_family,
|
||||
lookup_by_organ,
|
||||
lookup_by_relation_type,
|
||||
lookup_family,
|
||||
make_proposal,
|
||||
propose_construction,
|
||||
ConstructionProposal,
|
||||
)
|
||||
from generate.kernel_facts import SourceSpan
|
||||
|
||||
|
|
@ -22,7 +25,6 @@ def test_catalog_entries_are_diagnostic_only_and_serving_forbidden() -> None:
|
|||
def test_catalog_ordering_is_deterministic() -> None:
|
||||
families = all_diagnostic_families()
|
||||
ids = [f.family_id for f in families]
|
||||
# Check that they are sorted
|
||||
assert ids == sorted(ids)
|
||||
assert ids == [
|
||||
"partition.percent_partition",
|
||||
|
|
@ -31,114 +33,94 @@ def test_catalog_ordering_is_deterministic() -> None:
|
|||
|
||||
|
||||
def test_lookups_correctness() -> None:
|
||||
# 1. lookup_family
|
||||
f1 = lookup_family("proportional_change.decrease_to_fraction")
|
||||
assert f1 is not None
|
||||
assert f1.family_id == "proportional_change.decrease_to_fraction"
|
||||
fraction_family = lookup_family("proportional_change.decrease_to_fraction")
|
||||
assert fraction_family is not None
|
||||
assert lookup_by_organ("fraction_decrease") is fraction_family
|
||||
assert lookup_by_relation_type("decrease_to_fraction") is fraction_family
|
||||
|
||||
f2 = lookup_family("partition.percent_partition")
|
||||
assert f2 is not None
|
||||
assert f2.family_id == "partition.percent_partition"
|
||||
partition_family = lookup_family("partition.percent_partition")
|
||||
assert partition_family is not None
|
||||
assert lookup_by_organ("percent_partition") is partition_family
|
||||
assert lookup_by_relation_type("percent_of") is partition_family
|
||||
|
||||
assert lookup_family("invalid_family_id") is None
|
||||
|
||||
# 2. lookup_by_organ
|
||||
assert lookup_by_organ("fraction_decrease") is f1
|
||||
assert lookup_by_organ("percent_partition") is f2
|
||||
assert lookup_by_organ("invalid_organ") is None
|
||||
|
||||
# 3. lookup_by_relation_type
|
||||
assert lookup_by_relation_type("decrease_to_fraction") is f1
|
||||
assert lookup_by_relation_type("percent_of") is f2
|
||||
assert lookup_by_relation_type("invalid_relation") is None
|
||||
|
||||
|
||||
def test_make_proposal_validation_and_status() -> None:
|
||||
span = SourceSpan("test text", 0, 9)
|
||||
@pytest.mark.parametrize(
|
||||
("family_id", "relation_type", "candidate_organ", "required_roles"),
|
||||
(
|
||||
(
|
||||
"proportional_change.decrease_to_fraction",
|
||||
"decrease_to_fraction",
|
||||
"fraction_decrease",
|
||||
{"base_quantity", "scale", "state_entity", "transition"},
|
||||
),
|
||||
(
|
||||
"partition.percent_partition",
|
||||
"percent_of",
|
||||
"percent_partition",
|
||||
{"whole", "part", "scale"},
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_propose_construction_is_preassessment_and_catalog_backed(
|
||||
family_id: str,
|
||||
relation_type: str,
|
||||
candidate_organ: str,
|
||||
required_roles: set[str],
|
||||
) -> None:
|
||||
span = SourceSpan("surface cue", 0, 11)
|
||||
|
||||
# 1. Closed status (runnable = True)
|
||||
prop_closed = make_proposal(
|
||||
family_id="proportional_change.decrease_to_fraction",
|
||||
evidence_spans=(span,),
|
||||
assessment_runnable=True,
|
||||
missing_roles=(),
|
||||
active_hazards=(),
|
||||
)
|
||||
assert prop_closed.family_id == "proportional_change.decrease_to_fraction"
|
||||
assert prop_closed.relation_type == "decrease_to_fraction"
|
||||
assert prop_closed.candidate_organ == "fraction_decrease"
|
||||
assert prop_closed.evidence_spans == (span,)
|
||||
assert prop_closed.status == "closed"
|
||||
assert prop_closed.missing_roles == ()
|
||||
assert prop_closed.active_hazards == ()
|
||||
|
||||
# 2. Refused status (active hazards present)
|
||||
prop_refused = make_proposal(
|
||||
family_id="proportional_change.decrease_to_fraction",
|
||||
evidence_spans=(span,),
|
||||
assessment_runnable=False,
|
||||
missing_roles=(),
|
||||
active_hazards=("unbound_base_quantity",),
|
||||
)
|
||||
assert prop_refused.status == "refused"
|
||||
assert prop_refused.active_hazards == ("unbound_base_quantity",)
|
||||
|
||||
# 3. Partial status (missing roles present, no active hazards)
|
||||
prop_partial = make_proposal(
|
||||
family_id="proportional_change.decrease_to_fraction",
|
||||
evidence_spans=(span,),
|
||||
assessment_runnable=False,
|
||||
missing_roles=("base_quantity_unbound",),
|
||||
active_hazards=(),
|
||||
)
|
||||
assert prop_partial.status == "partial"
|
||||
assert prop_partial.missing_roles == ("base_quantity_unbound",)
|
||||
|
||||
# 4. Proposed status (not runnable, no missing roles, no active hazards)
|
||||
prop_proposed = make_proposal(
|
||||
family_id="proportional_change.decrease_to_fraction",
|
||||
evidence_spans=(span,),
|
||||
assessment_runnable=False,
|
||||
missing_roles=(),
|
||||
active_hazards=(),
|
||||
)
|
||||
assert prop_proposed.status == "proposed"
|
||||
|
||||
# 5. Invalid family_id raises KeyError
|
||||
with pytest.raises(KeyError):
|
||||
make_proposal(
|
||||
family_id="invalid_family_id",
|
||||
evidence_spans=(span,),
|
||||
assessment_runnable=False,
|
||||
missing_roles=(),
|
||||
active_hazards=(),
|
||||
)
|
||||
|
||||
|
||||
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,),
|
||||
)
|
||||
proposal = propose_construction(family_id, (span,))
|
||||
|
||||
assert proposal.family_id == family_id
|
||||
assert proposal.relation_type == relation_type
|
||||
assert proposal.candidate_organ == candidate_organ
|
||||
assert proposal.evidence_spans == (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",
|
||||
}
|
||||
assert {role.role for role in proposal.role_obligations if role.required} == required_roles
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"family_id",
|
||||
(
|
||||
"proportional_change.decrease_to_fraction",
|
||||
"partition.percent_partition",
|
||||
),
|
||||
)
|
||||
def test_make_proposal_rejects_migrated_proposal_first_families(
|
||||
family_id: str,
|
||||
) -> None:
|
||||
span = SourceSpan("surface cue", 0, 11)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=rf"{family_id} is proposal-first; use propose_construction before assessment",
|
||||
):
|
||||
make_proposal(
|
||||
family_id=family_id,
|
||||
evidence_spans=(span,),
|
||||
assessment_runnable=True,
|
||||
missing_roles=(),
|
||||
active_hazards=(),
|
||||
)
|
||||
|
||||
|
||||
def test_make_proposal_still_rejects_unknown_family_ids() -> None:
|
||||
span = SourceSpan("surface cue", 0, 11)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
propose_construction("invalid_family_id", (span,))
|
||||
|
||||
|
||||
def test_construction_proposal_status_validation() -> None:
|
||||
span = SourceSpan("test text", 0, 9)
|
||||
# Valid status doesn't raise
|
||||
ConstructionProposal(
|
||||
family_id="proportional_change.decrease_to_fraction",
|
||||
relation_type="decrease_to_fraction",
|
||||
|
|
@ -149,7 +131,6 @@ def test_construction_proposal_status_validation() -> None:
|
|||
active_hazards=(),
|
||||
)
|
||||
|
||||
# Invalid status raises ValueError
|
||||
with pytest.raises(ValueError, match="status must be one of"):
|
||||
ConstructionProposal(
|
||||
family_id="proportional_change.decrease_to_fraction",
|
||||
|
|
|
|||
Loading…
Reference in a new issue