core/generate/structure_mapping/pipeline.py
Shay e5643454d9 feat(trackb): S1–S4 symbolic SME, selector, pure-S1 coverage gain
Increment 2 for ADR-0252 Track B. Extends Increment 1's structure-mapping
slice with pure-family canonicals S2–S4, overlapping-waves selector, and a
structure-mapping-owned pure-S1 text extract that recovers four real
holdout cases the serving reader misses (0148, 0228, 0234, 0441).

Bar results (command-backed via scripts/measure_trackb_inc2.py):
- generalization ratio S1 = 9.0 (9 holdout cases / 1 template)
- coverage gain organ 5 → trackb 9, wrong=0
- selector routes S1/S2/S3; refuses empty; surface≠structure

Off-serving: no organ retirement, serving reader untouched. S2–S4 holdout
ratios are 0 (parser frontier). S3/S4 emit refuses at multi-register scope
without weakening the three-gate wrong=0 path.

[Verification]: Smoke suite passed locally (~132s, 176 passed);
trackb unit tests 35 passed; measure_trackb_inc2 all modes green.
2026-07-19 19:49:07 -07:00

83 lines
2.6 KiB
Python

"""End-to-end off-serving structure-mapping pipeline.
``text → (serving parse | SM-owned extract) → role graph → selector → solve``
Serving reader is tried first; SM-owned extractors run only on refuse, and
never feed the live organ path. Gold labels never enter.
"""
from __future__ import annotations
from dataclasses import dataclass
from generate.math_candidate_graph import parse_and_solve
from generate.math_problem_graph import MathProblemGraph
from generate.structure_mapping.convert import graph_to_role_graph
from generate.structure_mapping.solve import SolveOutcome, try_structure_map_and_solve
from generate.structure_mapping.text_extract import extract_pure_s1
@dataclass(frozen=True, slots=True)
class PipelineTrace:
"""Diagnostics for measurement scripts (not a gold label)."""
organ_parsed: bool
organ_answer: float | None
organ_refusal: str | None
extract_used: bool
extract_pattern: str | None
graph_source: str # "organ" | "sm_extract" | "none"
def run_structure_mapping_pipeline(
problem: str,
*,
families: tuple[str, ...] = ("S1", "S2", "S3", "S4"),
allow_sm_extract: bool = True,
) -> tuple[SolveOutcome, PipelineTrace, MathProblemGraph | None]:
"""Full off-serving path for one problem text.
Returns ``(outcome, trace, graph_used)``.
"""
organ = parse_and_solve(problem)
graph: MathProblemGraph | None = organ.selected_graph
extract_used = False
extract_pattern: str | None = None
graph_source = "organ" if graph is not None else "none"
if graph is None and allow_sm_extract:
ext = extract_pure_s1(problem)
if ext.graph is not None:
graph = ext.graph
extract_used = True
extract_pattern = ext.pattern_id
graph_source = "sm_extract"
trace = PipelineTrace(
organ_parsed=organ.selected_graph is not None,
organ_answer=organ.answer,
organ_refusal=organ.refusal_reason,
extract_used=extract_used,
extract_pattern=extract_pattern,
graph_source=graph_source,
)
if graph is None:
return (
SolveOutcome(
emitted=False,
answer=None,
refusal_reason="parser_frontier:no_graph",
binding=None,
derivation=None,
multi_register_certified=False,
classical_verified=False,
),
trace,
None,
)
# Optional: convert for hash; solve path re-converts.
_ = graph_to_role_graph(graph)
out = try_structure_map_and_solve(graph, families=families)
return out, trace, graph