Repository navigation
Conversation
…shed Adds the pre-publish comparables gate's scorecard side: the producer list by country, the gap threshold, one directory per analysis (sources/analysis-<country>-<slug>/) validated by scorecard_db/comparables.py, the published index policyengine-app-v2 reads, and an ingest that puts each record's claims and the analysis's own figures on the scorecard. First record: the £100,000 childcare dashboard, whose 30-hours cost is 46-50% below CenTax's static cost; the annotation cites microcosm#1126. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/test_comparables.py states and tests the invariants: every committed record passes and the index is what the records build; the gate fails exactly when an unexplained gap exceeds the threshold (Hypothesis); the gap rule's identities; printed values parse back; each rule fails a record that breaks it; the database holds every claim held out with the record's status. The index carries every staged estimate for a How we compare section. docs/COMPARABLES.md is the contract. Analysis claims carry conditions.country so the app files them under their country. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An uncompared claim that shares metric, unit, period and time basis with one of the analysis's estimates must name it (nearest_estimate) and carry a concept_mismatch or pe_gap annotation with a basis. It then sits on the scorecard beside that estimate, labelled, instead of blank. Closes the way a record could dodge an embarrassing comparison with a one-line pe_missing. The childcare record now pins all four published years and annotates the net-of-behaviour CenTax and CBP figures as a concept mismatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Max, 2026-10-21: "even if we've already published something, we should do a fresh comp review before big pushes like newsletters". Adds the prepromotion search kind, promotion_window_days (7, queued for Max), comparables.fresh and its CLI, and puts the window and each claim's found_on in the index, which PolicyEngine/newsletters' CI reads for every analysis an edition links. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The validator now checks each quantity's full identity (basis, programme, fiscal year), search kinds and dates, the results files each search compared, ranges, scales, quotes, sign conventions and bases, and refuses every relabelling the review used to hide a gap. The childcare record is regenerated under the new rules; the docs say what is enforced where. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…feeds The index now carries the tag rules that decide which policyengine.org entries need a record, so app-v2 and newsletters can check they apply the same ones. The promotion window is bounded to 1-90 days, matching the newsletters check. Populations, lanes and the app copies are regenerated for the childcare record's 22 claims. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
signs.py reads 'positive = increase in ...' as the same direction for a spending or tax metric whatever follows, so 'positive = increase in federal revenue' oriented a benefit cost as if it were spending. Such a convention is now unknown, and the claim cannot be compared until it is stated plainly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A policy-tagged calculator must still be searched, but it publishes no aggregate to pin. It declares calculator with a reason, pins no estimates, and its searches compare no results file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e unstated The backfill records quote ranges such as ~£3-3.4bn, whose upper bound the quote check read as negative. A producer that does not say whether its figure allows for behaviour now stages basis unstated, with any account in basis_note; such a claim is never compared as the same quantity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Over £1 billion" is neither a point nor a range. lower_bound and upper_bound join cumulative as kinds that are never compared with a single-year estimate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A figure in a base year's prices (DWP's real-terms table) is not the same quantity as a cash estimate. conditions.prices is cash when absent, real, or (for a claim) unstated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backfill records hash the documents their annotations cite and the pages their searches checked; those lines are provenance, not orphans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sis is served record now prints the latest search (or the one asked for by date or kind), so a revised analysis returning to the site can cite its new search. report runs while gaps are unexplained. analysis.json may list deployments, which the index publishes so a delisted dashboard cannot be promoted through its own URL; the childcare record lists its Vercel deployment. A bill scored on the state legislative tracker is recorded as registry kind tracker, and due lists it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
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.
The scorecard side of the pre-publish comparables gate Max asked for on 2026-10-21 ("how do we ensure we always do a full review of comparables to add to scorecard before publishing an analysis - pe-wide"), and of his follow-up the same day ("even if we've already published something, we should do a fresh comp review before big pushes like newsletters"). Before an analysis goes on policyengine.org, or is promoted again, every comparable external estimate is found, recorded here, compared with the analysis's own figures, and every gap over the threshold is explained with a cited cause. The contract is
docs/COMPARABLES.md.The case that prompted it: the £100,000 childcare dashboard published £0.43bn for the 30 hours in 2027-28 on 12 October. CenTax had published £0.80bn in September (HMRC data), and the dashboard listed CenTax as a benchmark. The cause, a Microcosm hours draw, had been filed as microcosm#1126 on 7 October. Nothing required the gap to be explained. Its record, the first one here, fails the gate on seven rows without the annotation that cites microcosm#1126.
What it adds
data/comparables/producers.json: the one producer list, by country.why: HMRC, DWP, DfE and the Scottish Fiscal Commission (UK); CPSP and CRFB (US).open_searchandacademicon both lists.applies_toscopes, claim-source aliases, and anaddeddate (never in the future), so a later addition never invalidates an old search.data/comparables/policy.json:gap_threshold0.25 (a parameter below 1, queued for Max as d1813),promotion_window_days7 (d1818) and the re-harvest window (183 days).scorecard_db/comparables.py: the gate, and the CLIscaffold,check,index,pins,record,due,reportandfresh. A directorysources/analysis-<country>-<slug>/holdsanalysis.json,search_log.json,manifest.jsonl,claims_staged.jsonlandannotations.json. The validator checks, mechanically where it can:conditions.basis(static, post-behavioural, forecast or outturn) andconditions.program; a fiscal year agrees with the period.prepublish(on or before publication) orbackfill(on or after it), and later ones arereharvestorprepromotion, after publication. Each records the results files it compared (results_pins), and the latest search's pins are the estimates' current ones.found,none_found,found_not_stagedornot_applicable(only when its scope excludes the analysis). A listed producer's claims are found under its own entry;sourceis neveropen_search,academicorpolicyengine.valueparses back fromvalue_rawunder a registered normalisation whose scale matches the printed one (m,bn,%), parentheses, minus words and Unicode minus included, for chart readings too; the quote prints the whole number with its sign; NaN is refused; a claim is never re-signed.constructedstatus. An uncompared claim near an estimate names it asnearest_estimatewith aconcept_mismatchorpe_gapannotation, and one that is the same quantity must be compared.range_group, judged as an interval.signs.pyorients the external value; a change metric's convention must be one-sided and not inverted. A gap over the threshold, a sign disagreement or a published zero needs an annotation withexplains_gap: true, a cause and a basis (a GitHub issue or pull, pinned engine metadata, a document or diagnostic URL with a path, or a file in this repository; placeholders refused).pending, with a tracking issue, only for what a search after publication found.conditions.basis_noteandprogram_notehold free text; a claim may have basisunstated, andconditions.prices(cash by default,real, orunstatedfor a claim) is part of a figure's identity, so DWP's real-terms table is never compared with a cash estimate;lower_bound/upper_boundfor one-sided figures ("over £1 billion"), never compared, likecumulative;calculatorand no estimates;recordcites the latest search (or--search-date/--search-kind), so a revised analysis can cite its new search;reportruns while gaps are unexplained;analysis.jsonmay listdeployments(published in the index) so newsletters can hold a delisted dashboard's own URL to its record; the childcare record lists its Vercel deployment;registry.kind: trackerrecords a bill scored on the state legislative tracker, andduelists such records.data/comparables/index.json: the published index, built from the directories, listing only records that pass. It carries each record's searches with their pins, current pins, comparisons (gap, nearest-estimate gap, sign disagreement, range-group gap) and the entry rules (which tags make an entry an analysis, which may declare no estimate) so app-v2 and newsletters can check they apply the same ones. The scorecard is public, so the fetch needs no token.scorecard_db/ingest_analysis_comparables.py(build stepanalysis_comparables): claims held out, withconditions.analysisandconditions.country; a compared claim's result is the analysis's own pinned figure with the record's status; a claim beside its nearest estimate gets that figure, labelled with the mismatch; a superseded estimate adds a history result. The feeds (populations.json,lanes.jsonand their app copies) are regenerated, and the pinned export counts rise by exactly the record's 22 rows.sources/analysis-uk-childcare-100k-threshold/, a backfill searched on 2026-10-21 across every UK producer, pinned toPolicyEngine/uk-childcare-100k-threshold@f25f0db.docs/COMPARABLES.md, with pointers indocs/ARCHITECTURE.mdandREADME.md, and app labels for the new producer slugs.Invariants and their tests (
tests/test_comparables.py, 148 tests)evaluate;tests/fixtures/comparables_fresh_cases.json(96 cases) is regenerated fromfreshhere and checked by PolicyEngine/newsletters' ownfresh.Checks run
pytest tests/at 6c8eb0b (Python 3.12 for the Modal test, as CI): 2,451 passed, 15 skipped, then the three failures it found (the export pins, the app copies, and the Modal test under a non-3.12 interpreter) fixed and rerun: 240 passed in those files and the build chain.452f64fb…); the tree is clean after the build.ruff format --check: clean.Companion PRs (merge order)
policyengine-comparables-review).A weekly re-harvest job runs the search again for every analysis published in the last 183 days, using
comparables dueandreportfrom this PR, once this merges.Overlaps
models.py,build_db.pyandsourceLabels.ts. The Centre for British Progress label is left to Add CenTax's costing of removing the £100,000 childcare limit #207. Its CenTax and CBP producer-level sources stay the canonical claims; this record re-stages them for the analysis. After Add CenTax's costing of removing the £100,000 childcare limit #207 merges, the childcare record'sacross_worldsbasis moves from Add CenTax's costing of removing the £100,000 childcare limit #207's test file at 661c14d to main.axiom: n/a: scorecard data and tooling; no PolicyEngine country-model change.
🤖 Generated with Claude Code