Skip to content

Official-calculator oracles (mode 3): closed-set extension, dated-reading provenance, calculator work list - #64

Open
vahid-ahmadi wants to merge 4 commits into
uk/ukmod-cases-schemafrom
uk/calculator-oracles
Open

Official-calculator oracles (mode 3): closed-set extension, dated-reading provenance, calculator work list#64
vahid-ahmadi wants to merge 4 commits into
uk/ukmod-cases-schemafrom
uk/calculator-oracles

Conversation

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Stacked on #49 (uk/ukmod-cases-schema) — retarget to main when #49 merges; the diff shown here is this lane's own work only.

What's here

  • Oracle vocabulary: five calculator oracle ids join the closed set (govuk_income_tax_estimator, govuk_hicbc_calculator, policy_in_practice_boc, entitledto, turn2us). CALCULATOR_ORACLES marks them as live services: their CaseResult.oracle_version must carry the reading date (YYYY-MM-DD) — enforced at validation — and SCHEMA.md documents the provenance rule (archive every reading; manual or terms-compliant access, one case at a time, never a scrape). oracle_version is now required non-empty for model oracles too.
  • Three new boundary cases (inputs + rationale only, per the battery doctrine): HICBC full clawback at exactly £80,000; the £125,140 double boundary (personal allowance exactly zero, additional rate begins); PSA ordering on £1,500 savings interest (expected extra liability exactly £100). Statutory 2025-26 values verified against gov.uk/LITRG; cases pinned to policy year 2025 accordingly.
  • battery/calculator_set.json + load_calculator_set(): the 10-case starter work list from Official-calculator oracles (mode 3): gov.uk and benefits-calculator household cases as a second UK oracle family #63 — each case assigned to ≥ 2 calculators so a PE-vs-UKMOD disagreement always has an adjudicating third reading; the two-child abolition pair gets three production-calculator readings each. Fail-loud validation: unknown case id, fewer than two oracles, a model oracle in the set, duplicates, and empty notes all raise.

No CaseResult rows and no expected values are committed — readings come from the real calculators later, dated and archived.

Suite: 174 passed / 4 skipped (was 164); ruff format --check clean.

Builds #63.

🤖 Generated with Claude Code

@DTrim99

DTrim99 commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Review — Official-calculator oracles (mode 3)

Enforcement is real code, not just SCHEMA.md prose: CaseResult.__post_init__ (case_diffs.py:283-294) raises when oracle_version is blank and when a CALCULATOR_ORACLES row lacks an ISO date; the closed set is enforced via Oracle(o) / CALCULATOR_ORACLES membership in load_calculator_set. Both paths are tested (test_case_schema.py:393-399, 464-480), and there are no id collisions across the 5 new oracle ids. I verified the three boundary cases against 2025/26 UK rules and they're exact:

  • HICBC £80,000 — 1%/£200 over £60k → 100% clawback at £80k ✓
  • £125,140 — £100k + 2×£12,570 = PA nil = additional-rate threshold ✓
  • PSA — £30k earnings stays basic-rate → £1,000@0% + £500@20% = £100 extra ✓

Should address

Suggestion

  • The provenance rule (SCHEMA.md:190-208, "archive every reading / never scrape / one case at a time") is documentation only — nothing asserts an archive citation exists on the eventual result row. Consider requiring an archive token in annotations for CALCULATOR_ORACLES rows at validation, or the rule stays unenforced.

Reviewed with Claude Code assistance.

@vahid-ahmadi
vahid-ahmadi force-pushed the uk/calculator-oracles branch from ec749c8 to f3e9bb8 Compare August 19, 2026 13:09
@vahid-ahmadi

Copy link
Copy Markdown
Contributor Author

Both should-address items plus the suggestion addressed in f3e9bb8 (rebased onto the updated #49 branch, a1d57d7):

Item Fix
_contains_iso_date shape-only Every shape-matching chunk is now parsed with date.fromisoformat2026-13-45, 0000-00-00, and 2025-02-30 all fail validation; 2024-02-29 passes. Tests pin all four.
Merge ordering Acknowledged — this stays stacked behind #49 (now also carrying its own review fixes: schema_version, variable_class, the classify→CaseResult seam), and the test-file overlap was resolved additively in the rebase.
Archive rule unenforced (suggestion) Taken: a calculator-oracle row now requires an archive: <path-or-url> annotation citing the archived reading or it raises; model oracles (UKMOD/TAXSIM) are unaffected. SCHEMA.md updated to say the rule is validation, not prose.

Suite: 190 passed / 4 skipped; ruff clean. Force-pushed with lease for the rebase.

@DTrim99

DTrim99 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Re-review — addressed ✅

  • _contains_iso_date now parses each candidate with date.fromisoformat in try/except (I confirmed 2026-13-45 / 0000-00-00 / 2025-02-30 reject and the leap day accepts), with test_impossible_reading_dates_rejected / test_real_reading_date_accepted.
  • Provenance rule is now enforced, not just documented: ARCHIVE_PREFIX + __post_init__ rejects any CALCULATOR_ORACLES row without an archive: annotation, with tests.
  • Boundary cases (HICBC £80k, £125,140, PSA) unchanged and still correct.

Only open item is the merge ordertest_loads_against_battery and the 7 referenced case_ids depend on #49, so this needs to land after #49 (expected for the stack, not a code fix).

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed — findings addressed (real date.fromisoformat parse, archive: token enforcement, boundaries correct). Approving — just land this after #49 per the stack.

@vahid-ahmadi
vahid-ahmadi force-pushed the uk/ukmod-cases-schema branch from a1d57d7 to 8adbc93 Compare August 20, 2026 11:35
@vahid-ahmadi
vahid-ahmadi force-pushed the uk/calculator-oracles branch from f3e9bb8 to 942666e Compare August 20, 2026 11:36
@vahid-ahmadi

Copy link
Copy Markdown
Contributor Author

Rebased onto the updated #49 head (which itself moved onto main post #70#73). One __all__ ordering conflict, resolved as a union. Suite 304 passed / 6 skipped, format clean. Still stacked behind #49.

r and others added 2 commits August 20, 2026 13:53
…d-reading provenance, 10-case calculator work list

- Oracle enum grows five calculator ids (govuk_income_tax_estimator,
  govuk_hicbc_calculator, policy_in_practice_boc, entitledto, turn2us);
  CALCULATOR_ORACLES marks them as live services whose result rows must
  carry the reading date (YYYY-MM-DD) in oracle_version, enforced at
  validation, with the archive-the-reading rule documented in SCHEMA.md.
  oracle_version is now required non-empty for every oracle.
- Three new boundary cases (2025-26 statutory values, verified against
  gov.uk/LITRG): HICBC full clawback at exactly GBP 80,000, the
  zero-allowance/additional-rate double boundary at GBP 125,140, and PSA
  ordering with GBP 1,500 savings interest (expected extra liability
  exactly GBP 100). Inputs + rationale only, per the battery doctrine.
- battery/calculator_set.json + load_calculator_set(): the 10-case
  starter work list, each case into >= 2 calculators so PE-vs-UKMOD
  splits always have an adjudicating third reading; entries validate
  against the battery (unknown case, single oracle, model oracle in the
  set, duplicate, empty notes all raise).

Suite: 174 passed / 4 skipped (was 164); ruff format clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r rows

Review items from the 2026-08-18 pass:
- _contains_iso_date now parses every shape-matching chunk with
  date.fromisoformat, so "2026-13-45" / "0000-00-00" / "2025-02-30"
  fail; "2024-02-29" passes.
- The SCHEMA.md archive rule is validation, not prose: a
  calculator-oracle row must carry an "archive: <path-or-url>"
  annotation citing the archived reading, or it raises. Model oracles
  (UKMOD/TAXSIM) are unaffected.

Also rebased onto uk/ukmod-cases-schema @ a1d57d7 (schema_version,
variable_class, classify->CaseResult seam), resolving the test-file
overlap additively.

Suite: 190 passed, 4 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vahid-ahmadi
vahid-ahmadi force-pushed the uk/calculator-oracles branch from 942666e to e29e523 Compare August 20, 2026 12:54
@vahid-ahmadi

Copy link
Copy Markdown
Contributor Author

Rebased onto the fixed #49 head (e29e523) — it inherited the derived-date drift described on #49, no separate fix needed. Merged against current main locally: 309 passed / 2 skipped, no-drift gates clean. Still stacked behind #49.

@MaxGhenis

Copy link
Copy Markdown
Contributor

Gate round 1 — the closed-enum discipline is right; the seam makes every calculator call raise

Clean: the Oracle enum extension is properly closed (unknown ids raise), the post-#74 virtual merge is conflict-free, and the mirrored lane feeds agree.

  1. Blocking — calculator rows cannot use the required seam. CaseResult.from_classification() can't accept annotations, while calculator rows require an archive annotation — so every calculator call through the factory raises, and the happy-path test sidesteps it via the public constructor, which still carries Mode-3 case schema + UK battery: UKMOD/EUROMOD lane groundwork #49's bypass (optional variable_class, caller-supplied classifications/tolerances never recomputed: 100 vs 200 accepted as match_within_tolerance with tolerance 100).

  2. Blocking — mode 3 still has no persistence. No case tables, writers accept only ExternalScore/PEResult, the exporter reads only those, no build_db step — SCHEMA.md itself defers results/miss-table to future work. Same finding as Mode-3 case schema + UK battery: UKMOD/EUROMOD lane groundwork #49: this family needs its table + writer + export + builder registration before either PR's semantics matter downstream.

  3. Blocking — dated-reading provenance is syntactic. No structured reading date, exact URL/version, archive-byte digest, input-vector digest, or selected policy year; any date substring passes and bare annotations=["archive:"] passes the prefix check; results aren't validated against the battery (unknown case + invented variable accepted). Concrete consequence already present: the work list assigns govuk_income_tax_estimator to 2025 cases, but the live service currently computes only 6 Apr 2026–5 Apr 2027 — a reading date can't substitute for the policy year the calculator actually embeds.

  4. Blocking — benchmark/relationship unassigned. CaseResult carries no benchmark_class/calibration_relationship/baseline/diagnosis, and the relationship registry raises on calculator sources. The defensible assignment is different_model + held_out — GOV.UK describes its tools' outputs as estimates, and explicitly calls Entitledto/Turn2us/Policy-in-Practice "independent" calculators — with per-oracle publisher evidence rather than one umbrella "official" category.

  5. High — the work-list guarantee is thinner than claimed. load_calculator_set() only counts ≥2 oracle ids; no case×variable capability map, no routing through uk_aliases, unversioned work list while SCHEMA_VERSION stays 1 despite the new globally-required oracle_version. The HICBC entries' own descriptions (one calculator reports the charge, the other general liability) show two ids ≠ two comparable readings.

Suggest resolving #49's seam and persistence questions first — this PR inherits both, and its calculator-specific value (dated readings, archive citations) will land cleanly once the substrate exists.

🤖 Generated with Claude Code

Carries the repaired #49 schema, which resolves the first blocker on its
own: from_classification now accepts annotations, so calculator rows can
use the required seam instead of raising, and #49's bypasses (optional
variable_class, caller-supplied classifications and tolerances) are gone.

3. Dated-reading provenance is structured, not syntactic. A calculator
   row now carries a CalculatorReading: the reading date as a real
   calendar date, the exact https page, the tax year the SERVICE
   computed, the archive location, and sha256 digests of both the
   archived bytes and the canonical input vector. The oracle_version
   date must be the reading's own; a bare "archive:" annotation is
   rejected and the citation must name the reading's own archive_ref; a
   reading may not postdate its row; and a model oracle may not carry a
   reading at all. CALCULATOR_POLICY_YEARS declares what each service
   actually computes — gov.uk's income-tax estimator is 2026-27 only —
   and both the work list and every row are checked against it, which is
   the concrete consequence the review named. The three cases pinned to
   2025 for verified 2025-26 thresholds move to 2026 (those thresholds
   are frozen at the same nominal values), so the assignment is honest
   rather than merely permitted. validate_results() checks a run against
   its battery: an unknown case_id and an invented variable were both
   accepted before.

4. Benchmark class and calibration relationship are assigned per oracle
   with publisher evidence, and land on the row. All seven oracles are
   different_model + held_out: GOV.UK describes its own tools' outputs as
   estimates and explicitly calls Entitledto, Turn2us and Policy in
   Practice "independent" calculators, so none is an authority PE is
   fitted to. A caller cannot relabel a calculator as authoritative.

Also: the two-child multiple-birth case leaves the calculator work list.
After #49 it is evaluated in the registered pre_ab2025 world, and a
production calculator computes current law only — now enforced, so the
abolished case is the only member of that family a calculator can answer.

2. Mode-3 persistence is still absent and is deliberately not guessed at
   here: it is the same table/writer/export/builder design #49 defers to
   the pairing Max offered, and the epistemic columns added here are
   written to travel on the row so they are ready for it.

Suite 340 passed / 4 skipped, ruff format clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
vahid-ahmadi pushed a commit that referenced this pull request Aug 21, 2026
1. The classify -> CaseResult seam is enforced, not conventional.
   __post_init__ now RECOMPUTES classify() and compares. A stored
   classification is valid only if it equals the classifier's, or if it
   adjudicates a row the classifier left `unclassified` — drawn from
   ADJUDICATABLE and carrying a writeup. Every round-2 probe now raises:
   100 vs 100 as unclassified/pe_gap, a numeric mismatch as
   policy_scope_mismatch with nothing said, and a boolean 1 vs 0 as
   match_within_tolerance with a caller-supplied tolerance of 100.
   pe_gap and policy_scope_mismatch are two-sided: mechanical on a null
   side, adjudicated (and explained) on a numeric-vs-numeric row —
   previously only oracle_difference and rounding demanded a writeup.
   Tolerances now come from DEFAULT_TOLERANCES alone; from_classification
   no longer takes a table, and a row carrying a different tolerance is
   rejected. It DOES now take annotations, which is also what made every
   #64 calculator call raise.

2. variable_class is required (the validator cannot check a row without
   it, and the shipped valid-row test omitted it) and schema_version is
   required rather than defaulting to the current contract. Blank engine
   or oracle versions are rejected.

4. The two-child-limit family isolates one mechanism per pair. Three
   2026 cases: `binding` (three SEPARATE births, so no exception can
   stand in for the limit) and `multiple-birth` (identical household,
   one date of birth apart) both in the registered `pre_ab2025` world,
   and `abolished` — byte-identical household, same year, same rates —
   under current law. binding/abolished attributes the abolition;
   binding/multiple-birth attributes the exception. The old pair used
   twins on both sides, so an engine that wrongly kept the limit but
   rightly applied the exception still paid three elements and passed,
   and its year-on-year delta moved ages, rates and policy year at once.
   Cases can now reference a registered baseline world, which is the
   instrument that made the same-year pair possible.

5. Identity closure reaches the edges. Regions are a closed per-country
   registry (region="MARS" raises), expected_focus is closed per country
   and may not repeat, NaN and infinity are rejected everywhere an
   amount is read, a boolean-class comparison may only hold 0 or 1, the
   battery's `schema` path must name a contract this repo defines, and a
   case baseline must already be registered in baselines.py. UK
   private-rent cases must now pin a BRMA — LHA is set per Broad Rental
   Market Area, so "the rent clears the cap in most Yorkshire BRMAs" was
   not a pinned world — and a BRMA outside its own region raises.

6. sync_lane_feed's `updated` is documented as the caller-supplied
   LITERAL it is, with the constraint that every caller in a build must
   pass the same constant, so the next caller does not reintroduce the
   drift.

Deliberately NOT in this commit: finding 3, the case/result table,
writer, exporter and build_db step, plus the epistemic columns and the
status/diagnosis split that ride on them. That is the piece with
repo-wide surface that Max offered to pair on, and guessing at it
unilaterally is how it ends up re-litigated. SCHEMA.md and the module
docstring now state the open shape explicitly rather than leaving it
implied.

Suite 306 passed / 4 skipped, ruff format clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
@vahid-ahmadi

Copy link
Copy Markdown
Contributor Author

Addressed in 79c9d4b, which carries the repaired #49.

  1. The seam. Resolved upstream: from_classification now accepts annotations, so calculator rows can use the required seam instead of raising, and Mode-3 case schema + UK battery: UKMOD/EUROMOD lane groundwork #49's bypasses (optional variable_class, caller-supplied classifications and tolerances) are gone.

  2. Dated-reading provenance. Agreed it was syntactic. A calculator row now carries a CalculatorReading: the reading date as a real calendar date, the exact https page, the tax year the SERVICE computed, the archive location, and sha256 digests of both the archived bytes and the canonical input vector. The oracle_version date must be the reading's own; a bare "archive:" is rejected and the citation must name the reading's own archive_ref; a reading may not postdate its row; a model oracle may not carry a reading at all. CALCULATOR_POLICY_YEARS declares what each service actually computes — the gov.uk estimator is 2026-27 only — and both the work list and every row are checked against it. That gate caught the concrete instance you named, so the three cases pinned to 2025 for verified 2025-26 thresholds move to 2026 (those thresholds are frozen at the same nominal values), making the assignment honest rather than merely permitted. validate_results() checks a run against its battery: an unknown case_id and an invented variable were both accepted before.

  3. Benchmark and relationship. ORACLE_BENCHMARK assigns both per oracle with the publisher evidence, and both land on the row. All seven are different_model + held_out on the evidence you pointed at — GOV.UK describes its own tools' outputs as estimates and explicitly calls Entitledto, Turn2us and Policy in Practice "independent" calculators. A caller cannot relabel a calculator as authoritative.

Also, falling out of #49: the multiple-birth case leaves the calculator work list. It is now evaluated in the registered pre_ab2025 world, and a production calculator computes current law only — now enforced, so the abolished case is the only member of that family a calculator can answer.

  1. Persistence is still absent and deliberately not guessed at here: it is the same table/writer/export/builder design Mode-3 case schema + UK battery: UKMOD/EUROMOD lane groundwork #49 defers to the pairing you offered, and the epistemic columns added here are written to travel on the row so they are ready for it.

Suite 340 passed / 4 skipped, ruff format clean.

Same integration finding as uk/ukmod-cases-schema, which this branch
stacks on: uk/calculator-oracles forked before #74, so it had no
scorecard_db/build_db.py and still tracked the committed database. Its
CI was the PRE-#74 workflow, so the determinism check and the no-drift
gate had never run here either.

Now under the current gates: builds from scratch, two builds agree on
content_hash, clean tree afterwards, suite 359 passed (up from 321).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
@vahid-ahmadi

Copy link
Copy Markdown
Contributor Author

Integration review finding — this branch had never met the post-#74 gates.

It forked before #74 (the database leaving git), so it carried no scorecard_db/build_db.py and still tracked data/scorecard.db as a committed binary. CI config is per-branch, so this PR was running the pre-#74 workflow: the determinism check and the no-drift gate had never executed against it at all. The green tick was a weaker check than it looked — worth knowing before reading the approval as covering them.

Current main is merged in now. The branch builds from scratch, two builds agree on content_hash, the tree is clean afterwards, and the suite goes from 321 to 359 passed because main's own tests come with it. No conflicts — main's deletion of the committed database wins over an untouched file on this side, so nothing resurrects.

Nothing in the branch's own code changed.

vahid-ahmadi pushed a commit that referenced this pull request Aug 25, 2026
GitHub runs a pull_request workflow from the PULL REQUEST'S OWN branch,
not from the base. So a branch that forked before a gate was added keeps
running the workflow WITHOUT it — and its green tick looks identical to
a full one while meaning strictly less.

That is not hypothetical. #74 made the database a derived artifact and
added a determinism check and a no-drift gate. Branches forked before it
kept the pre-#74 workflow, so neither gate had ever run against them,
and they were reviewed and approved on the understanding that both had.
An audit of every open PR found four in that state; two were mine (#49,
#64, since fixed) and two are still open.

gate-freshness.yml runs from the BASE via pull_request_target, so a
stale head cannot skip it: the check is defined by main and applies to
every PR regardless of what its own .github looks like.

The gate set is DERIVED FROM THE BASE rather than hardcoded — it reads
main's ci.yml and requires every determinism/no-drift line it finds to
be present in the head's — so a gate added later is enforced on every
open PR without anyone remembering to update the guard. It also refuses
a branch that still tracks data/scorecard.db, whose build cannot have
been from-scratch.

Security: pull_request_target runs in the base repo's context, so this
job NEVER checks out or executes pull-request code. It reads git
metadata only and holds contents:read. A test asserts that, and asserts
the guard cannot quietly become a pull_request trigger.

Verified by running the exact logic against all 16 open PRs: 14 pass and
exactly the two known-stale branches fail, with the reasons named.

Suite 270 passed, ruff format clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants