Skip to content

Estimate society-wide run time by region - #1257

Merged
PavelMakarchuk merged 33 commits into
mainfrom
feat/run-durations-by-region
Oct 14, 2026
Merged

PavelMakarchuk merged 33 commits into
mainfrom
feat/run-durations-by-region

Conversation

@PavelMakarchuk

@PavelMakarchuk PavelMakarchuk commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

The society-wide progress bar divides elapsed time by a fixed duration. While a run is pending, the visible bar (useSyntheticProgress) uses 6 minutes for every run, UK included; SocietyWideCalcStrategy uses 6 minutes for the US and 3 for the UK. Production runs between 6 and 13 October 2026 were much shorter for UK and US state reports, so their bar showed about 35% when the result arrived.

This PR lets app v3 estimate the duration from the report's country and region. Legacy v2 does not change.

v2 impact: none

The region estimate is opt-in. Without it, durations, progress and CalcStatus objects are the same as on main:

  • getDurationForCountry, getDurationForCalcType and UK_SOCIETY_WIDE_DURATION_MS (3 minutes) are as on main.
  • SocietyWideCalcStrategy by default uses the country estimate (UK 3 minutes, US 6 minutes) and sets no estimatedDurationMs.
  • useSyntheticProgress keeps its 6 minute default when no estimate is given.
  • The orchestrator's params stay the same unless the config opts in.

So v2 loading bars do not change. Tests show the default for UK, US state and US district runs: 3 and 6 minutes, with no estimate attached.

v3 estimates

Only the flagship report run (Report.page) and useBillEconomy (bill reports and the reforms page) opt in.

Report v2 (unchanged) v3 Observed, submission to completion (mean / 90th percentile)
UK nationwide 6 min (bar), 3 min (strategy) 110 s 64 s / 108 s
US state 6 min 190 s 130 s / 187 s
US congressional district 6 min 200 s 200 s (one run)
US nationwide 6 min 6 min 338 s / 392 s

Most values use the 90th percentile, so the bar rarely reaches its 95% cap before the result. UK subnational and US place runs have no observed samples, so they use the UK nationwide and US state values.

Changes

  • calculationDurations.ts: new region constants (US_STATE_…, US_DISTRICT_…, UK_REGION_SOCIETY_WIDE_DURATION_MS) and getDurationForRegion(countryId, region). It classifies the region through normalizeRegionCode, so legacy codes such as ca, us-ca and CA-12 also work. The existing constants and functions are unchanged.
  • CalcParams / CalcStartConfig: optional estimateByRegion. CalcOrchestrator passes it through only when it is set.
  • SocietyWideCalcStrategy: with estimateByRegion, estimates from the region and returns estimatedDurationMs on pending statuses.
  • CalcStatus: optional estimatedDurationMs.
  • useSyntheticProgress: uses estimatedDurationMs when it is given. It reads the value through a ref (synced in an effect), so the estimate that arrives with the first status does not restart the bar.
  • useCalculationStatus / useAggregatedCalculationStatus: pass the estimate through; the aggregate uses the longest pending estimate.
  • Report.page / useBillEconomy: set estimateByRegion: true.

Checks

  • New tests: region durations (with legacy codes); the strategy's default and opt-in output per region; the orchestrator's params with and without the option; the status hooks pass the estimate to useSyntheticProgress (the aggregate picks the longest, or none); the v3 callers opt in; the synthetic bar uses the estimate and does not restart when it arrives late.
  • bun run vitest: 4,390 passed, 10 skipped.
  • bun run typecheck, ESLint and Prettier on the changed files: pass.

Follow-ups

🤖 Generated with Claude Code

The progress bar divided elapsed time by 6 minutes for every
society-wide run. Production runs between 6 and 13 October 2026 took
about 64 s (UK nationwide) and 130 s (US state) on average, so their bar
showed about 35% when the result arrived.

Durations now come from the report's country and region: UK 110 s,
US state 190 s, US congressional district 200 s, and US nationwide
6 minutes (unchanged). The strategy returns the estimate on pending
statuses, and the synthetic bar uses it without restarting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
policyengine-calculator-next Ready Ready Preview Oct 14, 2026 8:24pm UTC
policyengine-website Ready Ready Preview Oct 14, 2026 8:24pm UTC

Request Review

PavelMakarchuk and others added 8 commits October 14, 2026 14:52
Every v3 report, custom or bill, now opens Validation on three cards:
external estimates (the bill's fiscal note, or related published
scores, or one line that none exists), data calibration (official
totals against PolicyEngine in one aligned table), and baseline
comparisons (the scorecard's published measures). Numbers are
right-aligned in tabular figures; each row opens its details. The
fiscal-note range chip stays in Validation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
External scores and the scorecard's published measures assume current
law. A report compared with another policy now marks both scorecard
cards "Not comparable", names what it compares with, and does not ask
the scorecard. Data calibration still shows. Reports on current law
are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One small mapping in libs/flagship/billTracker.ts mirrors the bill
tracker: its place names ("Federal"), its bill-number fallback for a
row with no title, its join of research rows to processed_bills
(title number first, then the id; spacing and leading zeros ignored),
and its page for each bill. The feed now carries that page as
trackerUrl, and reads netCost where a row stores no revenue change,
as the tracker does. The stage stays the tracker's own word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The overview lead links to the bill's page in the bill tracker, beside
the bill text. The report URL stays keyed by the tracker's bill id; an
upper-case or encoded id opens the same report, and a bill not in the
feed links to that bill in the tracker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
US reforms show a "Bill tracker" button beside "New reform" that opens
the tracker's home in a new tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The region estimate and the 110 s UK value applied to every
society-wide run, so legacy v2 loading bars filled against new
durations. The country estimates are back as on main (UK 3 minutes,
US 6 minutes), and getDurationForCountry, getDurationForCalcType and
the strategy's default output match main again.

The region estimate is now opt-in: CalcParams.estimateByRegion (passed
through CalcStartConfig and the orchestrator) makes the strategy use
getDurationForRegion and return estimatedDurationMs. Only the flagship
report run and useBillEconomy set it. The UK region value is a new
constant that only getDurationForRegion uses.

Also: getDurationForRegion classifies legacy US codes (ca, us-ca,
CA-12) through normalizeRegionCode, and useSyntheticProgress syncs the
estimate ref in an effect instead of during render.

Tests: the default strategy output for UK and US state/district runs
keeps 3 and 6 minutes with no estimate attached; the opt-in path per
region; the orchestrator leaves params unchanged by default; the
status hooks pass the estimate to useSyntheticProgress (the aggregate
uses the longest pending one); the v3 callers opt in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PavelMakarchuk and others added 16 commits October 14, 2026 14:57
EmptyState, ErrorState and skeleton placeholders under
components/flagship/states, so every v3 surface waits, says "none yet"
and fails the same way. ErrorState keeps its retry button busy while a
refetch is in flight, so a second click does not stack requests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the parameter index loads, placeholders in the shape of the search
bar (or of the draft's table) hold its place. If the model's metadata
fails, Build says so and asks for it again instead of loading for good.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bills and saved reforms load as placeholder cards in the grid's own
tracks. A failed feed or store says so with Try again. No saved reforms
offers Build; a search that hides every card offers to clear it, and
"No bills match." with "Clear filters" uses the bill feed's wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A link that names no report says "Report not found" at once, without
retries. A report, simulation or policy that fails to load shows
"Could not load the report" with Try again, which asks only for what
failed. Until the run starts, a placeholder holds the run card's place.
The calculation error now uses the shared ErrorState.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The window card and the overview's summary both say the window could
not be calculated, with Try again. The summary drops its progress link
while the window has failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the feed loads, a placeholder in the report's shape holds the
page. A failed feed says so with Try again; a bill the feed does not
carry, or a deployment with no feed, says so with the next step. When
the model's metadata fails, the full results say so and ask for it
again; a failed full run uses the same error state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A policy that fails to load only costs the provision detail, so the
report keeps showing its run instead of the load error. Try again still
asks for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a phone the top bar keeps the brand, the draft count and one menu
that holds both sections and the website links. The shell's padding is
16px, its height follows the visible screen, and buttons, inputs,
selects, tabs, picker buttons and segmented options are at least 40px
tall. The side panel plane moves under the content. Wider screens keep
today's bar and sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Folded, the adjust panel is a bar across the bottom that names it and
its count. Open, it fills the screen, and Escape folds it back. Its
remove buttons are a fingertip wide. Tablets and desktops keep the
spine and the column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a narrow table each row puts its new value on a line under the name,
with the row's buttons beside it. A breakdown's names wrap and its boxes
narrow, the ways to set a value scroll sideways, the add bar asks
without the example, and the pickers and popovers fit the screen with
fingertip-tall choices.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tabs keep one line, the place filter and search take their own line
under them, and the cards never outgrow the screen. Inputs are 16px on a
phone so the browser does not zoom in to type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Drop this PR's processed_bills join from billFeed; #1264 owns it
  (processedBillFor), so a bill gets one stage. Titles now come from
  the research select, which keeps the edits clear of #1264's lines.
- Split the tracker links into BillTrackerLink and TrackerHomeButton,
  so each import sorts away from #1264's and #1252's import edits.
- Economic impacts and the validation drift check read netCost when
  no stateRevenueImpact is stored, as the headline and the tracker do.
- The Reforms header wraps on a narrow screen, so "New reform" shows.
- trackerPlace falls back to the upper-case code.
- Feed and report test data moves into fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A report or bill feed refetch that fails after the data is in no longer
counts as a failed load: the report keeps its run card (not "Could not
load the report" or "Report not found"), and the bill grid shows no
error above its bills.

The run card and its placeholder are each the one live status on the
report wait. When metadata fails, the report overview says "Could not
load the parameters" with Try again, the same title Build and the bill
report now use. Build's table placeholder draws a row per group, as the
table does, so breakdown members no longer make it shrink on arrival.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shell also hosts the legacy v2 pages while the flag is on. Those
now keep the classes they had before the phone work (h-screen, 24px
padding, no touch-target rule), and only Build, Reforms, a report and
a bill report get the phone layout. A test covers both sets of routes.

On a phone the open side panel is a modal dialog with a close icon,
and the page and top bar under it are inert. A test shows that Escape
closes a picker inside the drawer and leaves the drawer open.

The state filter's choices and the year list are a fingertip tall, and
the filter's search is 16px. The policy tree starts under its close
button. The width mock reads em and rem, and throws on a width query it
cannot read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PavelMakarchuk and others added 8 commits October 14, 2026 16:19
# Conflicts:
#	app/src/components/flagship/report/ReportView.tsx
#	app/src/tests/unit/components/flagship/report/ReportView.test.tsx
#	app/src/tests/unit/pages/flagship/Report.test.tsx
ReportValidation now marks external estimates "Not comparable" for a
custom baseline, and its own tests cover that case. The note these two
tests looked for is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	app/src/pages/flagship/BillReport.page.tsx
#	app/src/pages/flagship/Reforms.page.tsx
#	app/src/pages/flagship/Report.page.tsx
# Conflicts:
#	app/src/components/flagship/reform/DraftTable.tsx
#	app/src/pages/flagship/Reforms.page.tsx
# Conflicts:
#	app/src/pages/flagship/BillReport.page.tsx
BillReportStatus takes an optional tracker URL and shows it beside "All
bills" for a bill the feed does not carry. The bill report passes the
tracker page for the bill's id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
billPlace now calls trackerPlace, so the card and the feed's
jurisdiction name a bill's place by one rule. The Reforms search reads
the place once, not as both billPlace and the jurisdiction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	app/src/tests/unit/pages/flagship/Report.test.tsx

This branch was successfully deployed

2 active deployments
Preview – policyengine-calculator-next — 6bd601bf Deployed Oct 14, 2026 by vercel[bot]
Preview – policyengine-website — 6bd601bf Deployed Oct 14, 2026 by vercel[bot]
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.

1 participant