Lane 3: Geometric evaluation for fraction_decrease
This commit is contained in:
parent
2a17d5b6f3
commit
8383b852d0
5 changed files with 177 additions and 67 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue