core/docs/analysis/scalar-equivalence-facade-pr1-brief-2026-06-18.md

9.7 KiB

ScalarEquivalence Facade PR-1 Handoff (2026-06-18)

This handoff is the execution brief for the first runtime slice after the Kernel Knowledge Layer PR-0 (#828).

It is intentionally narrow: implement a ScalarEquivalence facade over the existing ADR-0128 en_numerics_v1 pack. Do not change serving behavior in this PR.

Current anchor

  • PR #827 is merged.
  • PR #828 is merged.
  • Current expected train_sample: 30 correct / 20 refused / 0 wrong.
  • Current observed holdout_dev: 5 correct / 495 refused / 0 wrong.

Existing repo foothold

The implementation should reuse existing pack machinery rather than create a duplicate scalar pack.

language_packs/numerics_loader.py already exposes:

  • lookup_cardinal(token)
  • lookup_ordinal(token)
  • lookup_fraction(token)
  • lookup_quantifier(token)
  • lookup_multiplier(token)
  • lookup_comparison_anchor(token)
  • lookup_comparison_anchors(token)
  • match_number_format(token)
  • parse_compound_cardinal(text)

Important existing behavior:

  • lookup_fraction() supports article-bound forms such as a half and a quarter.
  • lookup_fraction() supports compound forms such as two-thirds, three quarters, and one half through _parse_compound_fraction().
  • match_number_format() supports ratified exact-format parsing for decimals, slash fractions, mixed numbers, percentages, signed integers, and thousands-separated integers.
  • The existing ADR-0128 numeric-format tests deliberately refuse ambiguous/malformed formats such as spaced slash fractions (1 / 2) and percent forms without the exact ratified shape.

The facade should respect those invariants unless a later ADR explicitly extends ADR-0128.

Mission

Create:

language_packs/scalar_equivalence.py

The facade should expose source-grounded scalar candidates with canonical rational values and ambiguity hazards.

It must not solve problems, bind bases, choose operations, or admit serving answers.

Required public type

from dataclasses import dataclass
from fractions import Fraction
from typing import Literal

@dataclass(frozen=True)
class ScalarLexeme:
    canonical: Fraction
    source_surface: str
    source_span: tuple[int, int]
    surface_kind: Literal[
        "digit_fraction",
        "decimal",
        "percent",
        "word_fraction",
        "unicode_fraction",
    ]
    provenance: Literal["problem_text"]
    hazards: tuple[str, ...]

Required public API

def extract_scalar_lexemes(text: str) -> tuple[ScalarLexeme, ...]: ...

def canonicalize_scalar_surface(surface: str) -> Fraction | None: ...

def hazards_for_scalar_surface(
    surface: str,
    *,
    context: str | None = None,
) -> tuple[str, ...]: ...

Optional helpers are allowed only if they remain facade-local and are directly tested.

Hard non-goals

Do not touch:

  • generate/math_candidate_graph.py
  • any generate/derivation/* organ
  • evals/gsm8k_math/**/report.json
  • sealed artifacts
  • train or holdout data
  • scoring logic

Do not implement:

  • a new duplicate scalar data pack
  • a broad parser
  • a generic fraction solver
  • serving integration
  • case-id logic
  • hardcoded benchmark answers
  • direct final-answer extraction

Canonical values for v1

At minimum:

  • 1/2
  • 1/3
  • 2/3
  • 1/4
  • 3/4
  • 1/10
  • 1/100

Represent canonical values as fractions.Fraction, not float.

Required surface families

Word fractions

Examples:

  • half
  • one half
  • one-half
  • third
  • one third
  • two thirds
  • quarter
  • one quarter
  • three quarters

Use lookup_fraction() where possible.

Digit fractions

Examples:

  • 1/2
  • 3/4

Use match_number_format() where possible.

Do not silently broaden ADR-0128. If 1 / 2 is unsupported by match_number_format(), either leave it unsupported in PR-1 or document why a later ADR should extend the numerics pack.

Decimals

Examples:

  • 0.5
  • 0.25
  • 0.75

Use match_number_format() where possible.

Do not silently add .5 if ADR-0128 currently refuses it. If .5 is desired, document it as a future numerics-pack extension unless the PR intentionally includes a reviewed ADR-0128 extension.

Percentages

Examples:

  • 50%
  • 25%
  • 75%
  • 100%

Use match_number_format() where possible.

For phrase forms such as 50 percent, implement conservatively and test exact span/provenance. Do not let 50 percentage points become an unhazarded scalar-of-base.

Unicode fractions

Examples:

  • ½
  • ¼
  • ¾

Use lookup_fraction() where possible.

Hazard policy

The facade may emit scalar candidates with hazards, but it must not emit ambiguous surfaces as silently safe.

Required hazard labels:

  • unbound_base_quantity
  • half_duration
  • quarter_coin
  • quarter_calendar_period
  • quarter_school_term
  • third_ordinal
  • ordinal_context
  • currency_context
  • temporal_context
  • percent_change_vs_percent_of
  • multiple_scalar_ambiguity

Examples:

  • half by itself should carry unbound_base_quantity unless a caller-supplied context explicitly narrows it.
  • half an hour should carry temporal_context and half_duration, or the facade may refuse to emit it as a usable scalar candidate.
  • third place should carry ordinal_context and third_ordinal, or be refused as scalar.
  • quarter dollar should carry currency_context and quarter_coin, or be refused as scalar.
  • 50 percentage points should carry percent_change_vs_percent_of, or be refused as scalar-of-base.

A downstream derivation organ or future ProblemFrame must still license the base quantity, actor, object, unit, relation, and question target.

Provenance policy

Every emitted ScalarLexeme must have:

  • exact source_surface
  • exact source_span
  • provenance == "problem_text"
  • no derived or synthesized values

A scalar value produced by arithmetic is not a ScalarLexeme and must not be represented as problem-text provenance.

Determinism requirements

  • Extraction order must be deterministic by source span.
  • Duplicate/overlapping scalar candidates must be handled deterministically.
  • If two candidates occupy the same span, prefer the more specific surface family only if the rule is explicitly tested.
  • If ambiguous, refuse or emit hazards; do not guess.

Tests

Create:

tests/test_language_packs_scalar_equivalence.py

Required tests:

  1. half maps to Fraction(1, 2) with problem_text provenance.
  2. one half maps to Fraction(1, 2) with exact span.
  3. one-half maps to Fraction(1, 2) with exact span if supported by lookup_fraction(); otherwise document unsupported.
  4. 1/2 maps to Fraction(1, 2).
  5. 3/4 maps to Fraction(3, 4).
  6. 0.5 maps to Fraction(1, 2).
  7. 0.25 maps to Fraction(1, 4).
  8. 50% maps to Fraction(1, 2).
  9. 50 percent maps to Fraction(1, 2) if phrase support is implemented.
  10. three quarters maps to Fraction(3, 4).
  11. Unicode ½ maps to Fraction(1, 2) if supported.
  12. source_surface preserves exact text.
  13. source_span slices the original text to exactly source_surface.
  14. extraction order is by source span.
  15. multiple scalar surfaces emit multiple deterministic lexemes.
  16. third place is hazardous or refused; it must not be silently safe.
  17. quarter dollar is hazardous or refused; it must not be silently safe.
  18. half an hour is hazardous or refused; it must not be silently safe.
  19. 50 percentage points is hazardous or refused; it must not be silently safe.
  20. unsupported forms from ADR-0128 remain unsupported unless explicitly extended.

Do not weaken existing ADR-0128 tests.

Documentation

Create:

docs/analysis/scalar-equivalence-facade-pr1-2026-06-18.md

Required sections:

  1. Purpose
  2. Relationship to #828
  3. Relationship to ADR-0128/en_numerics_v1
  4. Why this is a facade, not a new pack
  5. Supported canonical scalars
  6. Supported surfaces
  7. Explicit unsupported surfaces
  8. Hazard policy
  9. Provenance policy
  10. Determinism policy
  11. Non-goals
  12. Tests
  13. Next PR recommendation

Validation

Run:

git diff --check origin/main...HEAD
pytest tests/test_language_packs_scalar_equivalence.py -q
pytest tests/test_adr_0128_numeric_formats.py -q
pytest tests/test_math_candidate_graph_xhigh_sprint13_lift.py -q
pytest tests/test_math_candidate_graph_sprint12_singleton_contract_lift.py -q
pytest tests/test_math_candidate_graph_sprint11_cluster_contract_lift.py -q

Confirm no score change:

uv run python - <<'PY'
from evals.gsm8k_math.train_sample.v1.runner import _CASES_PATH, _load_cases, build_report

r = build_report(_load_cases(_CASES_PATH))
c = r["counts"]
print("train_sample:", c["correct"], c["refused"], c["wrong"])
print("wrong_ids:", sorted(x["case_id"] for x in r["per_case"] if x["verdict"] == "wrong"))
PY

Expected:

train_sample: 30 20 0
wrong_ids: []

Holdout safety:

uv run python - <<'PY'
from evals.gsm8k_math.holdout_dev.v1.runner import build_report

r = build_report()
c = r["counts"]
print("holdout_dev:", c, "n=", r["n"])
print("wrong_ids:", [x["case_id"] for x in r["per_case"] if x["verdict"] == "wrong"])
PY

Expected:

wrong_ids: []

If practical:

uv run python -m core.cli test --suite smoke -q

Commit / PR

Suggested commit:

feat(language-packs): add scalar equivalence facade

Suggested PR title:

feat(language-packs): add scalar equivalence facade

PR body must include:

  • Implements ScalarEquivalence facade over ADR-0128/en_numerics_v1.
  • No serving integration.
  • No generate/math_candidate_graph.py change.
  • No A2 organ changes.
  • No report.json change.
  • No sealed artifact change.
  • Preserves train_sample 30/20/0.
  • Preserves holdout_dev wrong=0.
  • Includes provenance and ambiguity hazards.
  • Validation outputs.

Do not merge without review.