docs(kernel): add scalar equivalence PR1 handoff

This commit is contained in:
Shay 2026-06-18 17:08:09 -07:00
parent 035a01f44b
commit 51498ca434

View file

@ -0,0 +1,379 @@
# 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:
```text
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
```python
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
```python
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:
```text
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:
```text
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:
```bash
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:
```bash
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:
```text
train_sample: 30 20 0
wrong_ids: []
```
Holdout safety:
```bash
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:
```text
wrong_ids: []
```
If practical:
```bash
uv run python -m core.cli test --suite smoke -q
```
## Commit / PR
Suggested commit:
```text
feat(language-packs): add scalar equivalence facade
```
Suggested PR title:
```text
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.