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_lexicon_for.cache_clear()
_pack_glosses_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 Narrow organ not a generic affine equation parser, not ``N/M more than`` affine
surfaces (0010-class), not percent-decrease. Promotion requires: 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); - question binds ``decrease`` + ``by`` (delta target, not final-value question);
- exactly one explicit current/base quantity with a unit; - exactly one explicit current/base quantity with a unit;
- hazard refusal (percent, goal language, multiple decrease-to fractions). - hazard refusal (percent, goal language, multiple decrease-to fractions).
@ -20,21 +20,20 @@ Deterministic; sealed module (no ``chat/`` import).
from __future__ import annotations from __future__ import annotations
import re
from typing import Final 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.clauses import segment_clauses
from generate.derivation.extract import extract_quantities from generate.derivation.extract import extract_quantities
from generate.derivation.model import GroundedDerivation, Quantity, Step from generate.derivation.model import GroundedDerivation, Quantity, Step
from generate.derivation.target import _question_clause from generate.derivation.target import _question_clause
from generate.derivation.verify import Resolution, SelfVerification, select_self_verified from generate.derivation.verify import Resolution, SelfVerification, select_self_verified
from generate.problem_frame_contracts import ContractAssessment
from generate.math_roundtrip import _tokens 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( _GOAL_INTENT: Final[frozenset[str]] = frozenset(
{"want", "wants", "wanted", "need", "needs", "hoping", "hopes", "plans", "aims", "goal"} {"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 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: def _has_hazard_surface(problem_text: str, question_clause: str) -> bool:
text_tokens = _tokens(problem_text) text_tokens = _tokens(problem_text)
question_tokens = _tokens(question_clause) question_tokens = _tokens(question_clause)
@ -81,18 +65,15 @@ def _has_hazard_surface(problem_text: str, question_clause: str) -> bool:
return True return True
if _asks_final_value(question_clause): if _asks_final_value(question_clause):
return True return True
fractions = _EXTRA_FRACTION_RE.findall(problem_text) return False
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
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): for clause in segment_clauses(problem_text):
if _DECREASE_TO_FRACTION_RE.search(clause): if source_text in clause:
continue continue
tokens = _tokens(clause) tokens = _tokens(clause)
if "current" not in tokens and "now" not in tokens: 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): for clause in segment_clauses(problem_text):
if clause == question_clause: if clause == question_clause:
continue continue
if _DECREASE_TO_FRACTION_RE.search(clause): if source_text in clause:
continue continue
quantities = [q for q in extract_quantities(clause) if q.unit] quantities = [q for q in extract_quantities(clause) if q.unit]
if len(quantities) == 1: if len(quantities) == 1:
@ -111,33 +92,38 @@ def _current_base_quantity(problem_text: str, question_clause: str) -> Quantity
return None return None
def build_fraction_decrease(problem_text: str) -> GroundedDerivation | None: def build_fraction_decrease(
"""Construct ``base × (1 N/M)`` decrease delta, or ``None``.""" 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) question_clause = _question_clause(problem_text)
if not _asks_decrease_delta(question_clause): if not _asks_decrease_delta(question_clause):
return None return None
if _has_hazard_surface(problem_text, question_clause): if _has_hazard_surface(problem_text, question_clause):
return None return None
fraction = _target_fraction(problem_text) base = _current_base_quantity(problem_text, question_clause, contract)
base = _current_base_quantity(problem_text, question_clause) if base is None:
if fraction is None or base is None:
return None return None
num, den, source_token = fraction D = contract.bindings[0].geometric_payload
delta_factor = 1.0 - (num / den) v = base.value
if delta_factor <= 0: Q = embed_point([v, 0, 0], dtype=np.float64)
return None Q_new = versor_apply(D, Q)
new_value = read_scalar_e1(Q_new)
return GroundedDerivation( return GroundedDerivation(
start=base, start=base,
steps=( steps=(
Step( Step(
op="multiply", op="subtract",
operand=Quantity( operand=Quantity(
value=delta_factor, value=new_value,
unit="", unit=base.unit,
source_token=source_token, source_token=contract.bindings[0].source_span.text,
), ),
cue="decrease", cue="decrease",
), ),
@ -146,34 +132,35 @@ def build_fraction_decrease(problem_text: str) -> GroundedDerivation | None:
def _self_verifies_fraction_decrease( def _self_verifies_fraction_decrease(
derivation: GroundedDerivation, problem_text: str derivation: GroundedDerivation, problem_text: str, contract: ContractAssessment
) -> SelfVerification: ) -> SelfVerification:
from generate.derivation.verify import _base_reasons from generate.derivation.verify import _base_reasons
tokens = _tokens(problem_text) tokens = _tokens(problem_text)
reasons = list(_base_reasons(derivation, tokens)) reasons = list(_base_reasons(derivation, tokens))
fraction = _target_fraction(problem_text) if not contract.bindings:
if fraction is None:
reasons.append("missing decrease-to fraction forecast") reasons.append("missing decrease-to fraction forecast")
else: else:
_, _, token = fraction token = contract.bindings[0].source_span.text
if token not in problem_text.replace(" ", ""): if token not in problem_text:
reasons.append(f"fraction token {token!r} not grounded in 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: if base is None:
reasons.append("missing explicit current/base quantity") reasons.append("missing explicit current/base quantity")
return SelfVerification(verified=not reasons, reasons=tuple(reasons)) 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.""" """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: if derivation is None:
return 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 None
return Resolution( return Resolution(
answer=derivation.answer, 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).""" """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 ( from generate.derivation.fraction_decrease import (
resolve_promotable_fraction_decrease, 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) frame = build_problem_frame(text)
if fraction_decrease_resolution is not None: contracts = assess_geometric_proposals(frame)
return CandidateGraphResult( contract = next((c for c in contracts if c.runnable and c.candidate_organ == "fraction_decrease"), None)
answer=fraction_decrease_resolution.answer,
selected_graph=None, if contract is not None:
refusal_reason=None, fraction_decrease_resolution = resolve_promotable_fraction_decrease(text, contract)
branches_enumerated=1, if fraction_decrease_resolution is not None:
branches_admissible=1, 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). # Gate A2l — equal half-split percent partition (Sprint 8 R6 lift).
from generate.derivation.percent_partition import ( 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.kernel_facts import BoundRelation, GroundedMention, SourceSpan
from generate.problem_frame import ProblemFrame 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 continue
bindings = [] bindings = []
for span in prop.spans: for span in prop.evidence_spans:
signature = resolve_geometric_signature(span.text) signature = resolve_geometric_signature(span.text)
if signature: if signature:
_, geom = signature _, geom = signature
@ -914,7 +915,7 @@ def assess_geometric_proposals(frame: ProblemFrame) -> list[ContractAssessment]:
candidate_organ=candidate_organ, candidate_organ=candidate_organ,
runnable=runnable, runnable=runnable,
explanation="Assessed via geometric algebra.", explanation="Assessed via geometric algebra.",
evidence_spans=tuple(prop.spans) evidence_spans=prop.evidence_spans
) )
assessments.append(assessment) assessments.append(assessment)

View file

@ -23,6 +23,23 @@
"rank": { "rank": {
"type": ["number", "integer"], "type": ["number", "integer"],
"description": "Priority rank among senses for the same lemma. 1 is most primary." "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 "additionalProperties": false