Lane 3: Geometric evaluation for fraction_decrease

This commit is contained in:
Shay 2026-07-04 15:11:50 -07:00
parent 2a17d5b6f3
commit 8383b852d0
5 changed files with 177 additions and 67 deletions

View file

@ -251,3 +251,99 @@ def clear_resolver_cache() -> None:
"""
_pack_lexicon_for.cache_clear()
_pack_glosses_for.cache_clear()
_pack_lemma_to_entry_id.cache_clear()
_pack_senses_for.cache_clear()
@lru_cache(maxsize=16)
def _pack_lemma_to_entry_id(pack_id: str) -> dict[str, str]:
"""Return ``{lemma_lower: entry_id}`` from the pack's lexicon.jsonl."""
lexicon_path = _PACK_ROOT / pack_id / "lexicon.jsonl"
if not lexicon_path.exists():
return {}
out: dict[str, str] = {}
for line in lexicon_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if not isinstance(entry, dict):
continue
lemma = entry.get("lemma") or entry.get("surface")
if not lemma or not isinstance(lemma, str):
continue
entry_id = entry.get("entry_id")
if isinstance(entry_id, str) and entry_id.strip():
out[lemma.lower()] = entry_id.strip()
return out
@lru_cache(maxsize=16)
def _pack_senses_for(pack_id: str) -> dict[str, dict]:
"""Return ``{lemma_id: geometric_signature}`` from the pack's optional
``senses.jsonl`` file. Also maps ``lemma`` as a fallback key.
"""
path = _PACK_ROOT / pack_id / "senses.jsonl"
if not path.exists():
return {}
out: dict[str, dict] = {}
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if not isinstance(entry, dict):
continue
geometric_signature = entry.get("geometric_signature")
if not isinstance(geometric_signature, dict):
continue
lemma_id = entry.get("lemma_id")
if isinstance(lemma_id, str) and lemma_id.strip():
out[lemma_id.strip()] = geometric_signature
# Optional fallback for direct lemma matching if lemma is present
lemma = entry.get("lemma")
if isinstance(lemma, str) and lemma.strip():
out[lemma.strip().lower()] = geometric_signature
return out
def resolve_geometric_signature(
lemma: str,
pack_ids: tuple[str, ...] = DEFAULT_RESOLVABLE_PACK_IDS,
) -> tuple[str, dict] | None:
"""Return ``(pack_id, geometric_signature)`` for the first pack in
*pack_ids* that BOTH (a) ratifies *lemma* in its ``lexicon.jsonl``
AND (b) carries a geometric_signature for it in ``senses.jsonl``.
"""
if not lemma or not isinstance(lemma, str):
return None
key = lemma.strip().lower()
if not key:
return None
for pack_id in pack_ids:
lex = _pack_lexicon_for(pack_id)
if key not in lex:
continue
lemma_ids = _pack_lemma_to_entry_id(pack_id)
entry_id = lemma_ids.get(key)
senses = _pack_senses_for(pack_id)
if entry_id and entry_id in senses:
return (pack_id, senses[entry_id])
if key in senses:
return (pack_id, senses[key])
return None

View file

@ -10,7 +10,7 @@ the **decrease by** delta (not the final value).
Narrow organ not a generic affine equation parser, not ``N/M more than`` affine
surfaces (0010-class), not percent-decrease. Promotion requires:
- exactly one ``decrease to N/M of`` forecast (no ``more than`` fraction affine);
- exactly one ``decrease to N/M of forecast`` (no ``more than`` fraction affine);
- question binds ``decrease`` + ``by`` (delta target, not final-value question);
- exactly one explicit current/base quantity with a unit;
- hazard refusal (percent, goal language, multiple decrease-to fractions).
@ -20,21 +20,20 @@ Deterministic; sealed module (no ``chat/`` import).
from __future__ import annotations
import re
from typing import Final
import numpy as np
from algebra.cga import embed_point, read_scalar_e1
from algebra.versor import versor_apply
from generate.derivation.clauses import segment_clauses
from generate.derivation.extract import extract_quantities
from generate.derivation.model import GroundedDerivation, Quantity, Step
from generate.derivation.target import _question_clause
from generate.derivation.verify import Resolution, SelfVerification, select_self_verified
from generate.problem_frame_contracts import ContractAssessment
from generate.math_roundtrip import _tokens
_DECREASE_TO_FRACTION_RE: Final[re.Pattern[str]] = re.compile(
r"decrease\s+to\s+(\d+)\s*/\s*(\d+)\s+of",
re.IGNORECASE,
)
_EXTRA_FRACTION_RE: Final[re.Pattern[str]] = re.compile(r"\d+\s*/\s*\d+")
_GOAL_INTENT: Final[frozenset[str]] = frozenset(
{"want", "wants", "wanted", "need", "needs", "hoping", "hopes", "plans", "aims", "goal"}
)
@ -55,21 +54,6 @@ def _asks_final_value(question_clause: str) -> bool:
return bool(_FINAL_VALUE_CUES & tokens) or "temperature" in tokens
def _target_fraction(problem_text: str) -> tuple[int, int, str] | None:
matches = list(_DECREASE_TO_FRACTION_RE.finditer(problem_text))
if len(matches) != 1:
return None
num_s, den_s = matches[0].group(1), matches[0].group(2)
try:
num = int(num_s)
den = int(den_s)
except ValueError:
return None
if num <= 0 or den <= 0 or num >= den:
return None
return num, den, f"{num_s}/{den_s}"
def _has_hazard_surface(problem_text: str, question_clause: str) -> bool:
text_tokens = _tokens(problem_text)
question_tokens = _tokens(question_clause)
@ -81,18 +65,15 @@ def _has_hazard_surface(problem_text: str, question_clause: str) -> bool:
return True
if _asks_final_value(question_clause):
return True
fractions = _EXTRA_FRACTION_RE.findall(problem_text)
target = _target_fraction(problem_text)
if target is None:
return True
_, _, token = target
extra = [f for f in fractions if f.replace(" ", "") != token.replace(" ", "")]
return len(extra) > 0
return False
def _current_base_quantity(problem_text: str, question_clause: str) -> Quantity | None:
def _current_base_quantity(
problem_text: str, question_clause: str, contract: ContractAssessment
) -> Quantity | None:
source_text = contract.bindings[0].source_span.text
for clause in segment_clauses(problem_text):
if _DECREASE_TO_FRACTION_RE.search(clause):
if source_text in clause:
continue
tokens = _tokens(clause)
if "current" not in tokens and "now" not in tokens:
@ -103,7 +84,7 @@ def _current_base_quantity(problem_text: str, question_clause: str) -> Quantity
for clause in segment_clauses(problem_text):
if clause == question_clause:
continue
if _DECREASE_TO_FRACTION_RE.search(clause):
if source_text in clause:
continue
quantities = [q for q in extract_quantities(clause) if q.unit]
if len(quantities) == 1:
@ -111,33 +92,38 @@ def _current_base_quantity(problem_text: str, question_clause: str) -> Quantity
return None
def build_fraction_decrease(problem_text: str) -> GroundedDerivation | None:
"""Construct ``base × (1 N/M)`` decrease delta, or ``None``."""
def build_fraction_decrease(
problem_text: str, contract: ContractAssessment
) -> GroundedDerivation | None:
"""Construct mathematical decrease delta, or ``None``."""
if not contract.is_runnable or not contract.bindings:
return None
question_clause = _question_clause(problem_text)
if not _asks_decrease_delta(question_clause):
return None
if _has_hazard_surface(problem_text, question_clause):
return None
fraction = _target_fraction(problem_text)
base = _current_base_quantity(problem_text, question_clause)
if fraction is None or base is None:
base = _current_base_quantity(problem_text, question_clause, contract)
if base is None:
return None
num, den, source_token = fraction
delta_factor = 1.0 - (num / den)
if delta_factor <= 0:
return None
D = contract.bindings[0].geometric_payload
v = base.value
Q = embed_point([v, 0, 0], dtype=np.float64)
Q_new = versor_apply(D, Q)
new_value = read_scalar_e1(Q_new)
return GroundedDerivation(
start=base,
steps=(
Step(
op="multiply",
op="subtract",
operand=Quantity(
value=delta_factor,
unit="",
source_token=source_token,
value=new_value,
unit=base.unit,
source_token=contract.bindings[0].source_span.text,
),
cue="decrease",
),
@ -146,34 +132,35 @@ def build_fraction_decrease(problem_text: str) -> GroundedDerivation | None:
def _self_verifies_fraction_decrease(
derivation: GroundedDerivation, problem_text: str
derivation: GroundedDerivation, problem_text: str, contract: ContractAssessment
) -> SelfVerification:
from generate.derivation.verify import _base_reasons
tokens = _tokens(problem_text)
reasons = list(_base_reasons(derivation, tokens))
fraction = _target_fraction(problem_text)
if fraction is None:
if not contract.bindings:
reasons.append("missing decrease-to fraction forecast")
else:
_, _, token = fraction
if token not in problem_text.replace(" ", ""):
token = contract.bindings[0].source_span.text
if token not in problem_text:
reasons.append(f"fraction token {token!r} not grounded in text")
base = _current_base_quantity(problem_text, _question_clause(problem_text))
base = _current_base_quantity(problem_text, _question_clause(problem_text), contract)
if base is None:
reasons.append("missing explicit current/base quantity")
return SelfVerification(verified=not reasons, reasons=tuple(reasons))
def compose_fraction_decrease(problem_text: str) -> Resolution | None:
def compose_fraction_decrease(
problem_text: str, contract: ContractAssessment
) -> Resolution | None:
"""Gate the typed fraction-decrease chain through self-verification."""
derivation = build_fraction_decrease(problem_text)
derivation = build_fraction_decrease(problem_text, contract)
if derivation is None:
return None
if not _self_verifies_fraction_decrease(derivation, problem_text).verified:
if not _self_verifies_fraction_decrease(derivation, problem_text, contract).verified:
return None
return Resolution(
answer=derivation.answer,
@ -182,6 +169,8 @@ def compose_fraction_decrease(problem_text: str) -> Resolution | None:
)
def resolve_promotable_fraction_decrease(problem_text: str) -> Resolution | None:
def resolve_promotable_fraction_decrease(
problem_text: str, contract: ContractAssessment
) -> Resolution | None:
"""Serving promotion bridge (Gate A2k)."""
return compose_fraction_decrease(problem_text)
return compose_fraction_decrease(problem_text, contract)

View file

@ -793,16 +793,23 @@ def parse_and_solve(text: str, *, sealed: bool = False) -> CandidateGraphResult:
from generate.derivation.fraction_decrease import (
resolve_promotable_fraction_decrease,
)
from generate.problem_frame_builder import build_problem_frame
from generate.problem_frame_contracts import assess_geometric_proposals
fraction_decrease_resolution = resolve_promotable_fraction_decrease(text)
if fraction_decrease_resolution is not None:
return CandidateGraphResult(
answer=fraction_decrease_resolution.answer,
selected_graph=None,
refusal_reason=None,
branches_enumerated=1,
branches_admissible=1,
)
frame = build_problem_frame(text)
contracts = assess_geometric_proposals(frame)
contract = next((c for c in contracts if c.runnable and c.candidate_organ == "fraction_decrease"), None)
if contract is not None:
fraction_decrease_resolution = resolve_promotable_fraction_decrease(text, contract)
if fraction_decrease_resolution is not None:
return CandidateGraphResult(
answer=fraction_decrease_resolution.answer,
selected_graph=None,
refusal_reason=None,
branches_enumerated=1,
branches_admissible=1,
)
# Gate A2l — equal half-split percent partition (Sprint 8 R6 lift).
from generate.derivation.percent_partition import (

View file

@ -25,6 +25,7 @@ from generate.construction_affordances import (
)
from generate.kernel_facts import BoundRelation, GroundedMention, SourceSpan
from generate.problem_frame import ProblemFrame
from chat.pack_resolver import resolve_geometric_signature
# ---------------------------------------------------------------------------
@ -888,7 +889,7 @@ def assess_geometric_proposals(frame: ProblemFrame) -> list[ContractAssessment]:
continue
bindings = []
for span in prop.spans:
for span in prop.evidence_spans:
signature = resolve_geometric_signature(span.text)
if signature:
_, geom = signature
@ -914,7 +915,7 @@ def assess_geometric_proposals(frame: ProblemFrame) -> list[ContractAssessment]:
candidate_organ=candidate_organ,
runnable=runnable,
explanation="Assessed via geometric algebra.",
evidence_spans=tuple(prop.spans)
evidence_spans=prop.evidence_spans
)
assessments.append(assessment)

View file

@ -23,6 +23,23 @@
"rank": {
"type": ["number", "integer"],
"description": "Priority rank among senses for the same lemma. 1 is most primary."
},
"geometric_signature": {
"type": "object",
"properties": {
"operator_type": {
"enum": ["static_blade", "parameterized_dilation", "parameterized_translation"]
},
"blade_values": {
"type": "array",
"items": { "type": "number" }
},
"generator_binding": {
"type": "string"
}
},
"required": ["operator_type", "blade_values"],
"additionalProperties": false
}
},
"additionalProperties": false