core/generate/derivation/state/change.py
Shay a719f1b58c
feat(adr-0184): extract semantic-state helper seam S1 (#490)
* feat(adr-0184): add semantic-state helper package

* feat(adr-0184): add referent binding helpers

* feat(adr-0184): add change cue helpers

* refactor(adr-0184): use semantic-state helpers in accumulation

* test(adr-0184): cover semantic-state helper guards
2026-05-30 08:36:02 -07:00

110 lines
2.9 KiB
Python

"""ADR-0184 S1 — conservative change-cue helpers.
These helpers are behavior-equivalent extractions from
:mod:`generate.derivation.accumulate`. They classify only the closed gain/loss
cue set already used by GB-3b.1 and return ``None`` when polarity is absent or
ambiguous. Cue hits propose semantic change frames; they never commit answers.
"""
from __future__ import annotations
from typing import Final
from generate.math_roundtrip import _tokens
# Closed change-cue lexeme sets (ADR-0165: lexemes, not grammar templates; refined
# by the CP ledger, not asserted complete). Sorted use keeps cue selection stable.
GAIN_VERBS: Final[frozenset[str]] = frozenset(
{
"buys",
"bought",
"gets",
"got",
"finds",
"found",
"picks",
"picked",
"earns",
"earned",
"receives",
"received",
"collects",
"collected",
"wins",
"won",
"makes",
"made",
"gains",
"gained",
"adds",
"added",
}
)
LOSS_VERBS: Final[frozenset[str]] = frozenset(
{
"loses",
"lost",
"spends",
"spent",
"uses",
"used",
"eats",
"ate",
"sells",
"sold",
"donates",
"donated",
"drops",
"dropped",
"removes",
"removed",
"breaks",
"broke",
}
)
def classify_change_polarity(clause: str) -> int | None:
"""Return ``+1`` for gain, ``-1`` for loss, or ``None`` to refuse.
Ordering is behavior-equivalent with the prior accumulation helper:
* ``more`` present -> gain;
* else an unambiguous loss verb -> loss;
* else ``gives``/``gave`` with ``to``/``away`` -> loss;
* else an unambiguous gain verb -> gain;
* else refuse by returning ``None``.
"""
tokens = set(_tokens(clause))
if "more" in tokens:
return +1
loss = bool(LOSS_VERBS & tokens)
gain = bool(GAIN_VERBS & tokens)
gives = "gives" in tokens or "gave" in tokens
directional = "to" in tokens or "away" in tokens
if loss and not gain:
return -1
if gives and directional and not gain and not loss:
return -1
if gain and not loss:
return +1
return None
def select_change_cue(clause: str, polarity: int) -> str:
"""Return a grounded cue lexeme present in ``clause`` for ``polarity``.
The returned cue is consumed by the existing derivation verifier's cue-grounding
clause. This function assumes ``polarity`` was produced by
:func:`classify_change_polarity` for the same clause.
"""
tokens = set(_tokens(clause))
if "more" in tokens and polarity > 0:
return "more"
verbs = GAIN_VERBS if polarity > 0 else LOSS_VERBS
present = sorted(verbs & tokens)
if present:
return present[0]
return "gives" # the only remaining licensed loss path (gives … to/away)