GB-1 — first slice of the comprehension-guided composer (ADR-0178). Reads the problem one clause at a time and derives each clause's LOCAL contribution; GB-2 combines them across clauses. generate/derivation/clauses.py: - segment_clauses(text): sentence-level orthographic split (ADR-0165; not grammar). - clause_local_results(text) -> tuple[ClauseResult]: per clause, 0 quantities = context (hold), 1 = leaf (its value), >=2 = bounded local search (reuses MS-3 search_chain). Refuse-preferring: ambiguous multi-quantity clause -> unresolved hold, not guessed. Locality is the guidance that bounds the search + steers grouping. 9 GB-1 tests (segmentation, leaf/context/local-product, ambiguous-holds, determinism, per-clause structure of a multi-sentence problem). Full derivation surface 86/86; ruff clean; smoke 67. Sealed; not wired into serving (ClauseResults ready for GB-2 sequential combination).
74 lines
2.9 KiB
Python
74 lines
2.9 KiB
Python
"""ADR-0178 GB-1 — clause segmentation + clause-local sub-derivation.
|
|
|
|
The first slice of the comprehension-guided composer: read the problem one clause
|
|
at a time and derive each clause's *local* contribution, before GB-2 combines them
|
|
across clauses. The text's clause structure is the guidance that keeps the search
|
|
bounded and steers grouping (quantities in a clause tend to combine locally).
|
|
|
|
Segmentation is lexeme/orthographic (ADR-0165) — sentence-level via terminal
|
|
punctuation, not a grammar template. Each clause's local sub-derivation reuses the
|
|
MS-3 :func:`search_chain` (a small bounded search over that clause's few
|
|
quantities); a single-quantity clause is a leaf; a zero-quantity clause is context.
|
|
Refuse-preferring: a clause whose local op is ambiguous resolves to nothing (a
|
|
hold), it is not guessed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from typing import Final
|
|
|
|
from generate.derivation.extract import extract_quantities
|
|
from generate.derivation.model import Quantity
|
|
from generate.derivation.multistep import search_chain
|
|
|
|
_SENTENCE_SPLIT: Final[re.Pattern[str]] = re.compile(r"(?<=[.?!])\s+")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ClauseResult:
|
|
"""A clause and its locally-derived contribution.
|
|
|
|
``value`` is the clause's local sub-result (``None`` = unresolved/hold —
|
|
context clause, or an ambiguous multi-quantity clause the local search
|
|
refused). ``resolved`` is ``True`` iff a local value was derived.
|
|
"""
|
|
|
|
text: str
|
|
quantities: tuple[Quantity, ...]
|
|
value: float | None
|
|
unit: str | None
|
|
resolved: bool
|
|
|
|
|
|
def segment_clauses(problem_text: str) -> tuple[str, ...]:
|
|
"""Split a problem into clauses (sentence-level, orthographic). Deterministic."""
|
|
return tuple(s.strip() for s in _SENTENCE_SPLIT.split(problem_text) if s.strip())
|
|
|
|
|
|
def clause_local_results(problem_text: str) -> tuple[ClauseResult, ...]:
|
|
"""Derive each clause's local contribution (GB-1). Deterministic.
|
|
|
|
- 0 quantities -> context clause (unresolved).
|
|
- 1 quantity -> leaf (its value).
|
|
- >= 2 -> bounded local search (:func:`search_chain`); resolves to a
|
|
local value or holds (unresolved) on ambiguity.
|
|
"""
|
|
out: list[ClauseResult] = []
|
|
for clause in segment_clauses(problem_text):
|
|
quantities = extract_quantities(clause)
|
|
if len(quantities) == 0:
|
|
out.append(ClauseResult(clause, (), None, None, False))
|
|
elif len(quantities) == 1:
|
|
q = quantities[0]
|
|
out.append(ClauseResult(clause, quantities, q.value, q.unit, True))
|
|
else:
|
|
res = search_chain(clause)
|
|
if res is None:
|
|
out.append(ClauseResult(clause, quantities, None, None, False))
|
|
else:
|
|
out.append(
|
|
ClauseResult(clause, quantities, res.answer, res.answer_unit, True)
|
|
)
|
|
return tuple(out)
|