Rail fares freeze: the engine ships both legs of the counterfactual (#105) - #107
Open
vahid-ahmadi wants to merge 1 commit into
Open
Rail fares freeze: the engine ships both legs of the counterfactual (#105)#107vahid-ahmadi wants to merge 1 commit into
vahid-ahmadi wants to merge 1 commit into
Conversation
…105) The 2026 regulated rail fares freeze is already in the certified baseline, so the scored object is the counterfactual — what fares would have done under prior law. Unusually, the engine carries that leg too: year fare_index prior_law_fare_index gap 2026 1.217 1.288 +5.83% 2027 1.268 1.342 +5.84% 2028 1.318 1.394 +5.77% #13 and #67 both had to construct an indexed counterfactual by hand. Here it is a parameter, which is what makes this lane cheap. It reaches an outcome: rail_usage x fare_index -> rail_subsidy_spending -> dft_subsidy_spending. That factorisation is deliberate — the engine's own docstring says it exists "allowing reforms to modify fare prices independently of usage quantity". The validator refuses four things, each a way this could be wrong while still looking right: - a counterfactual that is not a change (scores nothing, reads as agreement), checked PER YEAR so a freeze that lapses mid-horizon cannot pass on 2026 alone - swapped legs, which would score a fare CUT and still look plausible - staged value claims: DfT's saving figures are DfT's (#86/#91), and belong in their own harvest lane from primary documents - unresolved paths — --resolve reads every path and head variable out of a real 2.89.2, because this repo has published two false engine gaps from paths that were never resolved (#100, #101) The scope mismatch is named up front rather than discovered later: regulated fares are a minority of tickets sold, and DfT counts passengers where the engine counts households. Unnamed, either gap gets attributed to the engine (#59). No counterpart is computed here — UK compute runs through #51. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #105. Branches from
main— independent of the other UK PRs.Same shape as #102: a reform specification with every path resolved against a real
policyengine-uk2.89.2, and no value claims staged.Why this lane is cheap
The 2026 regulated fares freeze is already in the certified baseline, so the scored object is the counterfactual — what fares would have done under prior law. Unusually, the engine carries that leg too:
gov.dft.rail.fare_indexgov.dft.rail.prior_law_fare_index#13 and #67 both had to construct an indexed counterfactual by hand — the axis #67 found undecomposed in OBR's own re-estimates. Here it is a parameter. The reform is one line:
fare_index := prior_law_fare_index.It reaches an outcome, which is what makes it a lane rather than a curiosity:
That factorisation is deliberate.
rail_subsidy_spending's own docstring says it exists "allowing reforms to modify fare prices independently of usage quantity."What the validator refuses
Each of these is a way the spec could be wrong while still looking right:
--resolvereads every path and head variable out of a real engine. This repo has published two false engine gaps from paths that were never resolved (Autumn Budget 2026 scoreability registry: ready before 28 October (#96) #100, Baseline integrity: does the certified world carry the law legislated into its future? (#99) #101).Scope, named up front
Regulated fares are a minority of tickets sold in England, and DfT reports savings per passenger where the engine's quantities are per household. Both are recorded as named divergence axes (#59) before any comparison runs — unnamed, either gap gets attributed to the engine.
Not computed here
This PR specifies and resolves; it computes no counterpart. UK counterpart compute runs through #51, which isn't merged. Publishing a number before that lands would bypass the compute contract.
Gates
275 tests (12 new),
ruff format --checkclean, twobuild_dbruns agree oncontent_hash, no drift.#106 is the other half of the pair — bus, which is not expressible.