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.
77 lines
2.7 KiB
Python
77 lines
2.7 KiB
Python
"""ADR-0167 W2-B — Deterministic claim-signature normalisation for LexicalClaim.
|
|
|
|
Produces a stable sha256 hex that collapses two GSM8K cases refusing on the
|
|
same lexical token into one teaching-corpus candidate. Only ``sub_type ==
|
|
"lexical"`` evidence gets a non-empty signature in this PR; all other sub_types
|
|
are deferred to a follow-up ADR.
|
|
|
|
Normalisation pipeline (applied in order, documented here as a breaking-change
|
|
surface — modifying these rules changes all existing signatures):
|
|
|
|
1. Lowercase the ``surface`` string.
|
|
2. Strip leading and trailing characters that are members of the fixed
|
|
punctuation set ``string.punctuation``:
|
|
``!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~``
|
|
This set is frozen; expansions are breaking changes.
|
|
3. Extract the unknown-token from ``refusal_detail`` via the canonical
|
|
regex r"no primitive or lexicon match for '([^']+)'"
|
|
4. If the regex does not match (e.g. fraction/percentage detail format),
|
|
use the stripped-lowercase ``surface`` from step 2 as the token.
|
|
5. Canonical string: ``"lexical:" + extracted_token``
|
|
6. sha256 hex of canonical string encoded as UTF-8.
|
|
|
|
Returns a 64-character lowercase hex string. Pure function. Deterministic
|
|
across machines (sha256 of well-defined UTF-8 bytes).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import re
|
|
import string
|
|
|
|
# Frozen punctuation set — documented as breaking-change surface.
|
|
_PUNCT_SET: str = string.punctuation # !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
|
|
|
|
# Canonical regex matching lexicon-entry refusal details.
|
|
_LEXICON_DETAIL_RE: re.Pattern[str] = re.compile(
|
|
r"no primitive or lexicon match for '([^']+)'"
|
|
)
|
|
|
|
|
|
def lexical_claim_signature(
|
|
*,
|
|
surface: str,
|
|
refusal_detail: str,
|
|
) -> str:
|
|
"""Deterministic sha256 hex of a normalised lexical claim.
|
|
|
|
See module docstring for the full normalisation pipeline.
|
|
|
|
Parameters
|
|
----------
|
|
surface:
|
|
The raw token surface form (e.g. ``"crayons,"``).
|
|
refusal_detail:
|
|
The ``AuditRow.refusal_detail`` string verbatim.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
64-character lowercase hex.
|
|
"""
|
|
# Step 1 — lowercase
|
|
lowered = surface.lower()
|
|
# Step 2 — strip punctuation set members from both ends
|
|
stripped = lowered.strip(_PUNCT_SET)
|
|
# Step 3 — attempt canonical extraction from refusal_detail
|
|
match = _LEXICON_DETAIL_RE.search(refusal_detail)
|
|
# Step 4 — fallback to stripped surface if regex doesn't match
|
|
token = match.group(1) if match else stripped
|
|
# Step 5 — canonical string
|
|
canonical = "lexical:" + token
|
|
# Step 6 — sha256 hex
|
|
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
|
|
|
|
__all__ = ["lexical_claim_signature"]
|