Repository navigation
Engine: semantic cache key, one rounding, history tie labels, model-read pension types, job isolation - #10
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ead pension types, job isolation
Deferred items from the 2026-09-30 adversarial review, batched so the cache is invalidated once.
1. Job cache key hashes what each engine file computes (its AST without comments or
docstrings; pyproject.toml parsed), so prose-only edits keep every cached job. The
results file's provenance still records raw file hashes, so it goes stale on any edit.
2. One rounding helper, rules.round_rate (numpy's), for every statutory input and rate:
the rules, rate_sources, the additional pension's September CPI, the Pension Credit
guarantee and its check. engine.model_triple_lock_rate keeps Python's round() on
purpose: it reproduces policyengine-uk's add_triple_lock (differential test).
The Hypothesis "half-grid" generator produced quarter-grid values and never hit a tie;
it now draws true half-grid and on-grid values.
3. rate_sources labels ties as triple_lock_history does (rules.triple_lock_source):
floor whenever neither input exceeds 2.5%, CPI when the inputs tie; the plan's "cpi"
only above 2.5%. Differential test on the 2011-2026 published inputs.
4. engine.py docstring: the Pension Credit guarantee follows May-July earnings, not CPI.
5. held_pension_type_records/people are counted from sim.calculate("state_pension_type")
after pinning, in every year, and every run (path, history, coverage) fails if any
person's type differs from the held one.
6. run_jobs reports failures of jobs still running at fail-fast shutdown; jobs run in
their own session and are killed as a group on Ctrl-C/SIGTERM/SIGHUP; a job whose
build is SIGKILLed stops itself (watch_parent); slot_lock logs the holder and times
out instead of blocking silently. Household examples use the same helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… numbers unchanged) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
b4607a2 to
adac499
Compare
…antee precision, signal-test robustness - rules.py, config.py: the rounding is rules.round_rate's; policyengine-uk's own triple lock takes 3 dp with Python's round(), which differs only on exact half-grid values. - model_horizon.py: the Pension Credit guarantee is set from May-July earnings by every run, not built on the extended series (it is still checked). - households.run_examples passes the spec's rate_decimals to pension_credit_levels, as run_path does (every spec uses 3 today, so no example changes). - test_jobs: the stopped-build test resets SIGINT and SIGHUP in the build, so it does not depend on how the test runner was started, and kills the build if the test fails. Prose-only edits to engine files: engine_semantics() is unchanged, so every cached job stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t under load - held_pension_types' docstring (and a test comment) said policyengine-uk's enum encoder silently maps an unknown type name to BASIC. The executing review found the pinned policyengine-core raises instead, so the claim goes; the docstring now says what the check catches (a type the model ignored, recomputed or reordered; a length mismatch; an unknown type). - test_jobs: the sleeper jobs watch their parent as real jobs do, startup waits up to 120 s (a loaded host took over 20 s), and the stopped-build test kills every recorded sleeper's process group if it fails, so a failure leaves nothing running. Prose and tests only: engine_semantics() is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s unchanged) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review of adac499, with the later commits up to f1751cb checked too (docstring wording and test robustness; the findings below still apply there). Verdict: approve with nits. Nothing blocks, and the six changes do what the description says. I reproduced the results diff against On 3.13/macOS the full suite gives 175 passed, 1 failed. The failure is Should fix
Nits Checked and fine
|
Program reviewBase repository: PolicyEngine/uk-triple-lock Source Documents
CriticalNone. Should AddressA1 — Apply the fiscal rounding rule in the policy-gap backtests (OPEN)Location: A2 — A job that exits can leave a same-group descendant beyond cleanup (SHOULD ADDRESS) (OPEN)Location: Trigger: A model job starts a subprocess in its process group that closes inherited stdout/stderr, then the direct job exits normally or with a failure. Expected: The new session/child registry mechanism should stop that descendant before releasing the worker directory, consistent with Impact and limits: If a PolicyEngine loader or other job code leaves such a subprocess, it can continue working in the worker directory after the lock is released; a later build can acquire that directory while it remains active. The in-repo job bodies do not directly launch such a descendant, and I did not establish one in a real PolicyEngine run, so this is a confirmed helper contract gap with contingent production impact, not a demonstrated wrong estimate or critical failure. The existing tests cover killing descendants while the direct child is still registered, but not after its exit. The SIGKILL parent watcher checks the original PPID of a still-running direct worker ( SuggestionsNone. Evidence Gaps
Notes
Validation SummaryAt exact head f1751cb, GitHub Dashboard lint/test/build, Pipeline tests and committed-output checks, and Vercel: SUCCESS. Read-only checks: AST import closure/parse PASS, exact-function process-group reproduction confirms escaped descendant, base/head JSON comparison confirms no pre-existing numeric impact differences, annual pension-type vectors present in committed results, March 2018 EFO gap arithmetic 0.0652% unrounded versus 0.0973% with fiscal 3dp rounding. Local Pytest and full population rerun NOT RUN; five cached source files passed SHA-256 integrity. TimingNot measured. Review SeverityCOMMENT. Open findings: 0 critical, 2 should address, 0 suggestions. |
|
@juaristi22 thanks for the review. Both points are fixed in #16, with tests:
Neither changes a published number; A1 only moves the backtest diagnostics. Since |
The six items deferred from the 2026-09-30 adversarial review of #6, batched into one engine change so the job cache is invalidated once and the results are rebuilt once. Rebased onto
main(after #6, #11 and #12).What changes
engine.job_keynow usesengine_semantics(): each engine file's syntax tree (ast.dump, no line numbers) with comments, docstrings and other bare string statements removed, andpyproject.tomlparsed. An edit to prose alone keeps every cached job; any change to code, names or values reruns them. The results file's provenance still records raw file hashes (engine_hashes,source_hashes), sotest_not_stalestill fails on any edit, and the rebuild after a prose edit only reassembles from the cache.households.keyuses the same semantics.rules.round_rate(numpy's) is the only rounding of statutory inputs and rates: in the rules,rate_sources, the additional pension's September CPI (september_cpi), the Pension Credit guarantee (guarantee_growth,pension_credit_levels) and the guarantee check inrun_path. Python'sround()disagrees with it on 84 of the 151 half-grid values from -3% to 12% (for exampleround(0.0355, 3)is 0.035 andnp.roundgives 0.036). Oneround()stays on purpose:engine.model_triple_lock_ratereproduces policyengine-uk'sadd_triple_lockto check the model's own triple lock, so it rounds as upstream does. A differential test runs the upstream builder on half-grid inputs.tests/test_rules.pydrew(k + 0.5) / 2000. Those are quarter-grid values, so it never produced a tie. It now draws true half-grid values(k + 0.5) / 1000, plus values on the 0.1-point grid so inputs tie with each other and with the floor.rules.triple_lock_sourcenames what set a triple-lock rise. It is "floor" whenever neither input exceeds 2.5% (a tie with the floor included); otherwise it is the larger input, and CPI when the two inputs tie.engine.rate_sourcesandtrajectories.triple_lock_historyboth use it. The Burnham plan's non-top-up years are "cpi" only above 2.5%. A differential test feeds the 2011-2026 published inputs throughrate_sourcesand gets the history table's labels in every year, including April 2017 (earnings exactly 2.5%: the floor) and April 2022 (earnings suspended, tied with CPI: CPI). Before this changerate_sourcessaid "earnings" in both.engine.held_pension_typesreadsstate_pension_typefrom the simulation afterpin(), in every year. It fails the run if any person's type in the model differs from the held one (an input the model ignored, recomputed or reordered), if the array lengths differ, or if a type is unknown.fixed_inputs.held_pension_type_records(unweighted) andheld_pension_type_people(weighted) now come from the model, for every horizon year. The check also runs inrun_historyandrun_coverage.run_jobsreports every failed job, including those that fail while the running jobs finish after the first failure. Previously only the failures seen at the first wake-up were reported. Queued jobs are cancelled; running jobs finish and stay cached. The error says how many failed and how many did not run.run_child,start_new_session=True) and are registered. Ctrl-C, SIGTERM or SIGHUP (terminate_on_signals, used byrun_jobsandcli.main) stops every running job's process group: SIGTERM, then SIGKILL after 10 s. An already-ignored SIGHUP (nohup) stays ignored.watch_parent, polling every 2 s; macOS has no parent-death signal) and kills its own group.slot_lockreplaces the blockingflock. It polls withLOCK_NB, writes the holder's pid into the lock file, logs who holds the directory while waiting, gives up withLockTimeoutafter an hour, and gives up at once withAbortedwhen the build is stopping.config.py's comment says DWP "describes" the plan, as the README and dashboard do.Invariants (each property-tested or differential-tested)
tests/test_cache_key.py, Hypothesis over the real engine files):source_semanticsand move the raw provenance hash.pyproject.toml.ENGINE_FILES, and none of them reads__doc__.tests/test_engine_pure.py,tests/test_rules.py):round_rateisnp.roundfor scalars and arrays.model_triple_lock_rateequals policyengine-uk'sadd_triple_lockon random and half-grid inputs.rate_sourcesequalstriple_lock_historyon 2011-2026.test_resultsrecomputes every run'srate_sourcesfrom its recorded inputs.tests/test_jobs.py):slot_locknames the holder, times out, waits for a holder that lets go, and gives up when stopped.Mutation check before opening this: I reintroduced each old behaviour or likely regression one at a time. The tests caught all 12:
roundinseptember_cpior in the guarantee;round_ratein the model check;Results
Rebuilt in full (every job reran, because the cache key changed), then reassembled from the cache after the rebase onto
main. Compared withmain'sdata/results.jsonacrosscentral,expected_value,trajectories,coverageandbenchmarks:fixed_inputs.held_pension_type_recordsandheld_pension_type_people). These are aggregate counts.Checks
pytest176 passed; dashboardvitest63 passed;eslint0 errors (2 old warnings);next buildsucceeds.Downstream
paper/paperdata.pyreads each path'srate_sources, so the paper needs a re-render on the new results for the random path's April 2035 label.axiom: n/a: pipeline infrastructure (cache key, rounding helper, labels, run checks, job isolation); no policy encoded or changed
🤖 Generated with Claude Code