Adds `teaching/math_claim_signature.py` with `lexical_claim_signature()`:
sha256 hex of a normalised lexical token, collapsing two refusal cases on
the same surface token into one teaching-corpus candidate.
Normalisation pipeline (documented in module, breaking-change surface):
1. Lowercase surface
2. Strip string.punctuation from both ends (!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)
3. Extract token from refusal_detail via r"no primitive or lexicon match for '([^']+)'"
4. Fallback: use stripped-lowercase surface if regex doesn't match
5. Canonical: "lexical:" + extracted_token
6. sha256 hex of UTF-8 bytes → 64-char lowercase hex
Also adds `teaching/math_contemplation.py` (W2-A adapter included as
union-merge; W2-A worktree was not yet dispatched):
- `audit_to_evidence()`: AuditRow iterable → MathReaderRefusalEvidence tuple
- `audit_problem_to_evidence()`: convenience wrapper for tests and W3-A
- Lexical evidence: claim_signature filled; evidence_hash recomputed to include it
- Non-lexical sub_types: claim_signature stays "" (deferred per ADR-0167 §Q1)
Real-data result on audit_brief_11.json:
- 14 distinct lexical tokens → 14 distinct signatures (no false collisions)
- No duplicate tokens in the 50-case sample; dedup logic verified deterministic
Wave 2, parallel with W2-C/D; depends on W1-A branch.
wrong=0 verified by passing regression suite.
104 lines
3.4 KiB
Python
104 lines
3.4 KiB
Python
"""ADR-0167 W2-A / W2-B — Audit-to-evidence adapter + lexical claim signature.
|
|
|
|
W2-A deliverable: :func:`audit_to_evidence` and
|
|
:func:`audit_problem_to_evidence` convert :class:`AuditRow` sequences into
|
|
typed :class:`MathReaderRefusalEvidence` teaching-corridor records.
|
|
|
|
W2-B deliverable: For ``sub_type == "lexical"`` evidence, ``claim_signature``
|
|
is computed via :func:`lexical_claim_signature` and the ``evidence_hash`` is
|
|
recomputed to incorporate the signature. All other sub_types leave
|
|
``claim_signature == ""``.
|
|
|
|
Pure module. No filesystem writes, no network calls, no global mutation.
|
|
Deterministic: same inputs → byte-identical output across all reruns.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Iterable
|
|
|
|
from generate.comprehension.audit import AuditRow, audit_problem
|
|
from teaching.math_claim_signature import lexical_claim_signature
|
|
from teaching.math_evidence import (
|
|
MathReaderRefusalEvidence,
|
|
SUB_TYPE_FOR_OPERATOR,
|
|
from_audit_row,
|
|
)
|
|
|
|
|
|
def audit_to_evidence(
|
|
audit_rows: Iterable[AuditRow],
|
|
) -> tuple[MathReaderRefusalEvidence, ...]:
|
|
"""Convert audit rows into typed teaching-corridor evidence records.
|
|
|
|
Pure function. Deterministic. No filesystem, no network, no mutation.
|
|
Sub-type assignment from :data:`teaching.math_evidence.SUB_TYPE_FOR_OPERATOR`.
|
|
Skips rows with ``missing_operator=None`` (no sub_type → no candidate).
|
|
|
|
For ``sub_type == "lexical"``, :func:`lexical_claim_signature` fills the
|
|
``claim_signature`` field and the ``evidence_hash`` incorporates it.
|
|
For all other sub_types, ``claim_signature`` remains ``""`` (deferred).
|
|
|
|
Input order is preserved.
|
|
|
|
Parameters
|
|
----------
|
|
audit_rows:
|
|
Iterable of :class:`AuditRow` instances (e.g. from :func:`audit_problem`).
|
|
|
|
Returns
|
|
-------
|
|
tuple[MathReaderRefusalEvidence, ...]
|
|
One record per row whose ``missing_operator`` maps to a known sub_type.
|
|
"""
|
|
results: list[MathReaderRefusalEvidence] = []
|
|
for row in audit_rows:
|
|
if row.missing_operator is None:
|
|
continue
|
|
sub_type = SUB_TYPE_FOR_OPERATOR.get(row.missing_operator)
|
|
if sub_type is None:
|
|
continue
|
|
if sub_type == "lexical":
|
|
sig = lexical_claim_signature(
|
|
surface=row.token_text,
|
|
refusal_detail=row.refusal_detail,
|
|
)
|
|
else:
|
|
sig = ""
|
|
evidence = from_audit_row(row, sub_type, claim_signature=sig)
|
|
results.append(evidence)
|
|
return tuple(results)
|
|
|
|
|
|
def audit_problem_to_evidence(
|
|
problem_text: str,
|
|
*,
|
|
case_id: str,
|
|
) -> tuple[MathReaderRefusalEvidence, ...]:
|
|
"""Run the reader over *problem_text* and return evidence records.
|
|
|
|
Convenience wrapper that calls :func:`audit_problem` and pipes the
|
|
resulting :class:`AuditRow` list through :func:`audit_to_evidence`.
|
|
Useful for tests and downstream pipeline work (W3-A).
|
|
|
|
Parameters
|
|
----------
|
|
problem_text:
|
|
Raw GSM8K-style problem string.
|
|
case_id:
|
|
Identifier attached to every :class:`AuditRow` (e.g. ``"probe"``).
|
|
|
|
Returns
|
|
-------
|
|
tuple[MathReaderRefusalEvidence, ...]
|
|
Evidence records for any refusals encountered. Empty tuple on full
|
|
admission or if the text produced no sentences.
|
|
"""
|
|
_result, rows = audit_problem(problem_text, case_id=case_id)
|
|
return audit_to_evidence(rows)
|
|
|
|
|
|
__all__ = [
|
|
"audit_to_evidence",
|
|
"audit_problem_to_evidence",
|
|
]
|