Skip to content

Extract the calibration tree builder into a self-contained workspace package - #220

Merged
anth-volk merged 10 commits into
mainfrom
calibration-tree-package
Oct 20, 2026
Merged

anth-volk merged 10 commits into
mainfrom
calibration-tree-package

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #218

Summary

This moves the calibration tree builder (about 15.5k lines) into a self-contained Bun workspace package, @policyengine/calibration-tree in frontend/packages/calibration-tree/. Its contents:

  • the schema 6 types, parsers and serializers;
  • the release and staging readers;
  • Blob I/O;
  • the publisher.

The dashboard and the publisher both import it. It is a refactor only: a golden record taken from the current code, before anything moved, shows the package reproduces every byte the builder writes.

The package has no imports from the rest of frontend/ (no @/ aliases, Next.js or server-only), declares its dependencies, and carries its own tests and typecheck. bun run publish:calibration-tree and .github/workflows/publish-calibration-tree.yml are unchanged. Moving the package into the collector repository and publishing it to npm is out of scope.

Stacked on #223

This PR is now stacked on #223 (María's UK release fixes). Its base is #223's branch, uk-release-cuts-calibration-fit, so the diff here shows only the package move. The plan has #223 merge first; when it does, GitHub retargets this PR to main.

What the rebase changed

Red and green for the rewritten commits

SHAs in this section are from the first restack. The second restack only replayed these commits; their current SHAs are under "Commits".

  • Commit 1 (b7c9b18). Red: 1 of 16 failed. The UK dry-run repair stopped at "Unexpected Hugging Face request …/latest-national.json". Green: 16 passed.
  • Commit 2 (469b17b).
    • Red:
      • the boundary test: 1 of 3 failed, with calibration-tree-manifest.ts: "./release-cut" does not resolve inside the package, and the same for latest-artifact.ts;
      • latest-artifact.test.ts: 1 failed (ENOENT on mappings.yaml);
      • the dashboard typecheck: 3 errors, Cannot find module '@/lib/microcosm/…' in the releases route and its test.
    • Green, rerun at the commit: 181 passed, 0 failed across 13 files (the boundary test, the moved Publish Microcosm UK release cuts, open the UK page on the national line, and show local candidates on Staging #223 test files, the mapped-measure test, format, the releases and tree route tests, the publisher's tests and both golden suites); bun run lint (dashboard and package typecheck) clean.
  • Commit 3 (73aa8e7). Only import conflicts were resolved; the original red (3 failed) stands. Green, rerun at the commit: 18 passed across 3 files (boundary, publisher unit and golden repair tests); bun run lint clean.
  • Commits 4 to 9 replayed without conflicts.

Tests run for the rebase

Commits

  1. Record golden calibration tree outputs from the current builder (d426778)
    • Red: 2 failed. Both golden suites could not load because no golden record existed yet. A characterization test cannot fail against the code it pins; commit 2 carries the meaningful red-green.
    • Green: 16 passed. These are 12 builder, reader, comparison and manifest cases, plus 4 publisher repair tests.
    • Sensitivity check (not committed): with gzip level 9 changed to 6 in the bundle module, all 8 UK and Belgium golden tests failed. That includes the publisher refusing each repair with "Rebuilt calibration index differs from immutable manifest entry".
    • Checks run: bun test 619 passed, 0 failed; tsc --noEmit clean; next build passed. The first build attempt failed to fetch Google Fonts, a network error; the rerun passed.
  2. Move the calibration tree builder into a workspace package (6886fce)
    • Red: 31 failed across 29 files, all for missing modules.
      • 28 suites could not load their module, for example Cannot find module './countries'. The golden suite failed on Cannot find module '@policyengine/calibration-tree/calibration-comparison-blob'.
      • The 3 boundary tests failed because the package manifest did not exist yet.
    • Green: 297 passed, 0 failed (package bun test, run from the package directory).
    • Checks run:
      • frontend bun test 622 passed, 0 failed;
      • bun run lint clean (frontend tsc plus the package typecheck);
      • next build passed;
      • bun install --frozen-lockfile clean;
      • bun run publish:calibration-tree without credentials parses its arguments and stops at "BLOB_READ_WRITE_TOKEN is required.";
      • boundary-test sensitivity: adding next/server and @/lib/base-path imports to a package module produced 2 violations (reverted).
  3. Run the calibration tree publisher from the package (035e204)
    • Red: 3 failed. The publisher's unit suite and golden repair suite could not load ./publish-calibration-tree and @policyengine/calibration-tree/publish-calibration-tree, and the boundary test flagged the missing module and export.
    • Green: 15 passed (8 publisher unit tests, 4 golden repairs, 3 boundary tests).
    • Checks run:
      • package bun test 309 passed;
      • package typecheck clean;
      • frontend bun test 622 passed, 0 failed;
      • bun run lint clean;
      • next build passed;
      • the workflow's exact invocation, bun run publish:calibration-tree -- --country uk --reconcile-staging --sha <40 hex> --dry-run, parses and stops at the credential check, as does the package's own script;
      • pytest frontend/tests/test_deploy_workflow.py: 12 passed.
  4. Install frontend dependencies before the deploy job's calibration checks (ed710c0)
    • Bug, found in review: the smoke script now imports @policyengine/calibration-tree/countries, which resolves only through the workspace link bun install creates in frontend/node_modules. deploy.yml set up Bun but never installed, and both calibration checks run bun run frontend/scripts/calibration-deployment-smoke.ts from the repository root. In a git archive of the publisher-move commit (then e78b672) that command stops with Cannot find module '@policyengine/calibration-tree/countries'; on main it reaches argument parsing. CI missed it because its jobs install first and the workflow test only checked the step's command text.
    • Fix: a step "Install frontend dependencies" runs bun install --frozen-lockfile in frontend, after the Vercel candidate upload and before the first calibration check. It runs after the upload because vercel deploy . sends the repository root, so the uploaded files never include node_modules. The relative import was not restored, because the deploy needs the install once the package is published.
    • Red: 3 failed.
      • test_every_bun_script_step_runs_after_its_job_installs_locked_frontend_dependencies listed both calibration steps in deploy.yml. It checks every job in every workflow: a step that runs bun run, bun test or bun <file> must follow a bun install --frozen-lockfile step in frontend.
      • test_deploy_installs_locked_frontend_dependencies_without_credentials_after_the_vercel_upload found no install step.
      • The Bun test calibration-deployment-smoke-checkout.test.ts copies the checkout's files without node_modules, replays the deploy job's bun install steps in order, and loads the smoke script at each calibration step. Both steps failed with Cannot find module '@policyengine/calibration-tree/countries'.
      • The new unit test of the Bun-command pattern passed, as intended.
    • Green: pytest test_deploy_workflow.py 15 passed; Bun 22 passed (the checkout test and the 21 smoke-script tests). The checkout test takes about 2 s; the install from Bun's cache takes 0.8 s.
    • Checks run: bun run lint clean; actionlint clean on deploy.yml.
  5. Read package imports from the TypeScript syntax tree in the boundary test (72c3bc6)
    • Gap, found in review: the boundary test matched imports with regular expressions over .ts files only, so it missed require(), require.resolve(), triple-slash references and other extensions. A relative require() of the dashboard's base-path module passed both the boundary test and the package typecheck.

    • Fix: src/test-support/package-boundary.ts parses every .ts, .tsx, .mts, .cts, .js, .jsx, .mjs and .cjs file in the package with the TypeScript compiler. It collects:

      • static and type-only imports and re-exports;
      • import-equals declarations, import() expressions and import types;
      • require(), require.resolve(), import.meta.resolve() and mock.module();
      • triple-slash path and types references.

      A specifier that is not a string literal, or any use of createRequire, is reported, because the check cannot follow it. Bun's Transpiler.scanImports() was not used: a probe showed it drops import type, triple-slash references, require.resolve(), typeof import() and mock.module().

    • Red: 6 of 9 failed.

      • The four tests that plant a package in a temporary directory beside a stand-in dashboard module failed because the regular expressions found nothing. They cover a relative require() of the dashboard's base-path module, the other source extensions, triple-slash references, and computed specifiers with createRequire.
      • The accepting control failed, because the old resolver did not map ./labels.js to labels.ts.
      • The real-package test failed, because the regular expressions matched import text inside the new tests' fixture strings.
      • The type-only test passed on the regular expressions. It guards against a scanner that drops type-only imports.
    • Green: 9 passed.

    • Checks run:

      • package bun test 315 passed;
      • package typecheck clean;
      • bun run lint clean;
      • the touched tests run from frontend/: 31 passed;
      • pytest frontend/tests/test_deploy_workflow.py: 15 passed;
      • the reviewer's case: adding require("../../../lib/base-path") to program-label.ts now fails the boundary test with src/program-label.ts: "../../../lib/base-path" does not resolve inside the package, while the package typecheck still passes (reverted).
  6. Skip lifecycle scripts in the deploy job's dependency install (1190deb)
    • Why: the deploy job holds Google Cloud credentials (the auth step's credential file) and can request GitHub OIDC tokens, so no project, workspace or dependency install script should run there. The calibration checks need none.
    • Fix: the step runs bun install --frozen-lockfile --ignore-scripts. CI's setup-bun installs Bun 1.4.3. With that version, a probe confirmed that a plain frozen install runs the project's, a workspace package's and a trusted dependency's postinstall, and that the flag skips all three.
    • Red: 2 failed.
      • pytest test_deploy_dependency_install_skips_lifecycle_scripts: the command was bun install --frozen-lockfile.
      • Bun "the deploy job's dependency install runs no lifecycle scripts": install scripts planted in the checkout copy's frontend and calibration-tree manifests ran in both.
    • Green: pytest 16 passed; Bun 23 passed (2 checkout tests and 21 smoke-script tests). The checkout tests also pass under Bun 1.4.3, and the smoke script still loads after an install that skips scripts.
    • Checks run: bun run lint clean; actionlint clean.
  7. Turn off Bun's automatic installs in the deploy job's calibration checks (62cd502)
    • Why: in a directory tree without node_modules, Bun installs any missing package from npm as a script imports it. In a clean checkout the smoke script asks the registry for @policyengine/calibration-tree. That fails today, but once the package is published, a job whose workspace install went missing would load an unpinned copy from npm.
    • Fix: both checks, before and after promotion, run bun run --no-install frontend/scripts/calibration-deployment-smoke.ts. On Bun 1.4.2 and 1.4.3, bun run --no-install <file> and bun --no-install run <file> made no registry request. bun run <file> --no-install passes the flag to the script, and Bun still asks the registry.
    • Red: 2 failed.
      • pytest test_every_calibration_check_turns_off_bun_auto_install: neither step had --no-install before the script path.
      • Bun "the deploy job's calibration checks never install packages from a registry": in a clean copy, each step's own command asked a local recording registry for /@policyengine%2fcalibration-tree.
      • A unit test of the pattern that reads the Bun words before the script path passed, as intended.
    • The checkout tests now run each step's own Bun command, taken from its run text up to the script path, so they use the workflow's flags.
    • Green: pytest 18 passed; Bun 24 passed (3 checkout tests and 21 smoke-script tests). The checkout tests also pass under Bun 1.4.3.
    • Checks run: bun run lint clean; actionlint clean.
  8. Merge main into calibration-tree-package (formerly 1a292d2, a merge commit): dropped by the rebase onto Publish Microcosm UK release cuts, open the UK page on the national line, and show local candidates on Staging #223, whose head already contains main.
  9. Import the country registry only from the calibration-tree package (38da4f0)
    • Change: telemetry-collector-client.ts imports MicrocosmCountry from @policyengine/calibration-tree/countries, and frontend/lib/microcosm/countries.ts is deleted. The use-microcosm hook's comment now points releaseCountry at the package's latest-artifact module.
    • Red: 2 failed. country-registry-imports.test.ts found lib/microcosm/countries.ts at the former location, and the reference lib/server/microcosm/telemetry-collector-client.ts: "@/lib/microcosm/countries". The test reads module references from the TypeScript syntax tree, so type-only imports, import types and mock.module() count.
    • Green: 2 passed. A temporary probe module with a relative mock.module() and an import() type of the old path was reported, then removed.
    • Checks run:
      • package bun test 315 passed; package typecheck clean;
      • the touched frontend tests and the checkout tests: 35 passed;
      • bun run lint clean; next build passed;
      • pytest test_deploy_workflow.py: 18 passed.
  10. Serve a checked national pointer in the golden repair harness (3fed47b)
  • Why: Publish Microcosm UK release cuts, open the UK page on the national line, and show local candidates on Staging #223 now refuses a UK latest-national.json without schema_version 1, the matching line and a revision, and checks that the release manifest at the named tag names it. The harness served only {release_id}.
  • Fix: for a country with its own pointer file, the stubbed repository serves a pointer naming a tag of the recorded release at another commit, with that tag's folder listing and a release manifest naming it. The tag is not among the repository's refs, so the reconciliation under test is unchanged. The golden record is unchanged.
  • Red: 1 of 16 failed. The UK dry-run repair stopped at "latest-national.json has unsupported schema_version undefined."
  • Green: 16 of 16. Rerun at the head: all 51 test files this branch adds or changes, 484 passed and 0 failed; bun run lint clean.
  1. Keep overlapping golden harness scopes from undoing each other (ca4edf5, from review)
  • Why: goldenComparisonRecord builds both sides of a comparison under Promise.all, so two withRegisteredRepositories scopes overlap. The first to finish restored the repository overrides while the second was still building. The second then restored "unset", which deleted the overrides. withHuggingFaceFiles swapped globalThis.fetch in the same way.
  • Effect, checked by hand and not committed: with POPULACE_UK_STAGING_HF_REPO set, the UK comparison golden case run on its own failed with "artifact calibration_diagnostics is declared but absent", because its staging side read the overridden repository. With the fix it passes and the override is still set afterwards.
  • Fix: overlapping repository scopes share one cleared environment. The first to start saves and clears it, and the last to finish restores it; scopes may also nest. Hugging Face file scopes run one after another. The golden record is unchanged.
  • Docs: docs/calibration-tree-artifacts.md and the harness's header comment now say the record covers US, UK and Belgium releases, US and UK staging runs, and a US and a UK release-to-staging comparison.
  • Red: 2 of 2 new tests in test-support/calibration-tree-golden-cases.test.ts failed. In one, the second scope read the override after the first finished. In the other, the second Hugging Face scope's request reached the outside fetch. For this run withHuggingFaceFiles was already exported, unchanged, so the file loaded.
  • Green: 2 of 2. Rerun at the commit: 27 passed across 4 files (the new test, both golden suites and the boundary test); bun run lint clean.

Commits before 3fed47b fail one golden test

A review found that every commit before 3fed47b fails one golden test. At 38da4f0 it is "a dry-run repair of the recorded UK hierarchy release rebuilds its recorded index", which stops with "latest-national.json has unsupported schema_version undefined." The rebase onto #223 brought in #223's stricter pointer check, and 3fed47b updates the harness for it. History isn't rewritten here.

  • With a merge commit, those commits stay on main, and each fails that test when checked out, for example during git bisect.
  • With a squash merge, only the final state lands.

The merge method is the reviewer's choice.

Package boundary

The package is the transitive import closure of the publisher: 31 modules and 15,964 lines (on #223), plus their tests and fixtures. That closure already imported nothing outside itself except Node built-ins and @vercel/blob. It contains:

Area Modules
Schema 6 and builder calibration-tree-artifact, calibration-tree-bundle, calibration-tree-manifest, calibration-tree, calibration-explorer
Comparisons calibration-comparison-bundle, calibration-comparison-blob, calibration-build-comparison, target-change
Blob I/O calibration-tree-blob, calibration-release-locator
Inputs latest-artifact, staging-artifact, staging-contract, countries, release-cut (from #223), reforms, the target readers, loss attribution, label helpers
Publisher publish-calibration-tree, keeping its command-line entry point

Blob I/O moved into the package. It already sits behind the injectable CalibrationTreeBlobClient, which defaults to @vercel/blob. The plan keeps both Vercel Blob stores at first, and the collector becomes their writer. That writer needs the same upload verification and manifest compare-and-swap, and those are part of the byte-exact contract. Removing the default client would have changed dashboard code paths, so it stays.

What stayed in the dashboard:

  • the Next.js routes;
  • calibration-tree-http (imports next/server);
  • calibration-tree-request, target-change-tree and calibration-prefetch;
  • the use-microcosm hook;
  • the route tests;
  • third-country-conformance.test.ts and its zz-release fixture;
  • the reviewed-repository-import test, which spawns app routes.

They import the package through explicit subpath exports, one per module, such as @policyengine/calibration-tree/calibration-tree-bundle. There is no root barrel, so client bundles pull in only what they import.

Location: inside frontend/.

  • Vercel builds with frontend as its root directory. Whether files outside it are available is a project setting this repository cannot verify.
  • CI and the publisher workflow both run bun install --frozen-lockfile in frontend/.
  • Turbopack's root is frontend.

The boundary is enforced by a test and by the package's own tsconfig, not by its directory.

Wiring:

  • frontend/package.json gains workspaces: ["packages/*"] and the workspace:* dependency;
  • lint also runs the package typecheck, so CI covers it without a workflow change;
  • frontend/bunfig.toml sets linker = "hoisted", because Bun otherwise installs a project with workspaces using its isolated linker, which would change the node_modules layout that Next.js and Vercel build against;
  • frontend/tsconfig.json excludes packages;
  • bun.lock gains the workspace entry.

Enforcement: package-boundary.test.ts requires that:

  • package modules import only each other, node: built-ins and declared dependencies (@vercel/blob). The test reads every module reference from each JavaScript or TypeScript source's syntax tree, including require() and triple-slash references;
  • only test code may use bun:test, package self-references or development dependencies;
  • every export resolves to a package module;
  • every module is exported.

Identity proof

The golden record was produced from c630c91's code in commit 1, before any move. It is frontend/packages/calibration-tree/src/fixtures/calibration-tree-golden.json; commit 2 moved it at 100% similarity, with no content change. After the rebase onto #223 it is still the same blob (a4d719e), and every golden test passes.

The inputs reuse the module tests' fixtures:

  • US release: the pinned 5,659-target legacy identities, given values and reported loss attribution. Its bundle spans 2 summary shards, 5 detail shards and 4 tiers.
  • UK releases: a schema 8 hierarchy release built from the reader tests' targets and GSS geographies, and a release with loss attribution.
  • Belgium release: the existing fixture.
  • Staging: a US version 1 run, a UK version 2 run, and a UK run read from its staged dataset bundle.
  • Comparisons: release to staging for US and UK, built through loadCalibrationComparisonSource over the stored bytes, in both modes.
  • Manifest: holding every recorded build.

For every bundle file the record holds the path, the raw JSON SHA-256 and size, the gzip size, and the SHA-256 of the deflate stream. It also holds the build IDs, the index digests (the index file entries), the explorer root responses read back from the stored bytes, one comparison target detail, and the manifest serialization digest. The gzip header is asserted separately, because its operating-system byte is 3 on Linux and 19 on macOS.

publish-calibration-tree-golden.test.ts runs the publisher's own dry-run repair of the recorded US, UK and Belgium releases against a stubbed Hugging Face repository. The manifest entry carries the recorded build ID and index digest, and the publisher refuses any rebuilt index that differs. A further test confirms that a wrong digest is refused.

All 30 modules and their moved tests are byte-identical to their versions on #223's branch except 10 import lines that used the @/ alias, in staging-artifact.ts, latest-artifact.ts and source-label.ts. The publisher differs only in its 9 import lines. One test also moved out: latest-artifact.test.ts no longer holds #229's mapped-measure test, which now lives in the dashboard (see "What the rebase changed"). countries.ts is a 100% copy (git diff -M -C).

Risks and follow-ups

  • countries compatibility re-export (resolved). The move first kept frontend/lib/microcosm/countries.ts as a re-export, because lib/server/microcosm/telemetry-collector-client.ts type-imported the registry from there while Fetch only the collector runs the build monitor shows #216 was changing that file. After Fetch only the collector runs the build monitor shows #216 merged, commit 9 switched that import and deleted the re-export, and a test keeps the old location from coming back.
  • Next.js fetch cache hint. The readers pass next: { revalidate }, which the dashboard's Next.js fetch honors and Bun ignores. A declaration seen only by the package typecheck (types/next-fetch-cache.d.ts) mirrors Next's type. The collector move should replace this with an injected fetch or caching policy.
  • Direct environment reads. The package still reads process.env directly (Hugging Face tokens, repository overrides, size limits, the UK local-run directory), exactly as before.
  • Country registry location. The registry now lives in the package, and AGENTS.md, CLAUDE.md, README.md and the docs point there. Once the package is published, adding a country becomes a package release plus a dashboard bump. Splitting dashboard-only fields (capabilities, labels) from the builder's registry may be worth doing then.
  • Bun version and deflate output. The deflate digests pin the runtime's zlib: Bun bundles its own, while CI and the publisher use unpinned latest Bun. The record was made with Bun 1.4.2 on macOS. A Bun release that changes deflate output would fail these tests. Such a change also matters in production, because the index records gzip sizes and repairs refuse index changes. That risk already existed; these tests now surface it.
  • Test time. The golden suites cost about 30 s of CPU, because the US bundle is built by the builder test and again by the publisher repair. They run in the frontend tests job. The deploy checkout tests add about 3.5 s there: they copy the repository's files three times and run bun install from Bun's cache twice, in temporary directories.
  • Traced fixtures. next build traces the package's JSON fixtures into the standalone output (52 KB), as it already does for lib/ fixtures. Server code is bundled.
  • Unpublished package. The package is private: true until it is published.

Deploy implications

deploy.yml runs after CI succeeds on a push to main, so merging this deploys:

  • a commit-named Modal backend (backend/ is unchanged);
  • a Vercel production candidate that is verified and then promoted.

The frontend change is import paths plus workspace wiring, with the same runtime behavior. Vercel installs from frontend/, where the package lives, and reads the hoisted-linker setting there; an older Bun ignores that key and is hoisted by default. The publisher workflow is unchanged, and its next run executes the package module through the same npm script. Publishing has been paused since 2026-10-19 at 20:58 UTC (CALIBRATION_TREE_BUILD_ENABLED=FALSE), so that run happens only once the variable is re-enabled. The plan re-enables it after #237 and #239 are deployed.

The deploy job gains one step, bun install --frozen-lockfile --ignore-scripts in frontend, after the Vercel upload and before the first calibration check. Without it the job would stop at that check, before promotion (commits 4 and 6). Both calibration checks run bun run --no-install, so they fail if that install is missing, and never install from npm (commit 7).

Environment variables

No variable is added, removed or renamed. The publisher workflow is unchanged. deploy.yml gains one install step, which reads no variables or secrets (see the deploy job table below).

Variable Read by Provider and wiring Verified
BLOB_READ_WRITE_TOKEN publisher Repository secret, mapped in the publish job's env: and inherited by bun run Secret exists (name listed)
HF_TOKEN publisher Repository secret, publish job env: Secret exists
POPULACE_UK_STAGING_HF_TOKEN publisher (UK staging token_env) Repository secret, publish job env: Secret exists
CALIBRATION_TREE_BUILD_ENABLED publish job if: Repository variable, set to FALSE on 2026-10-19 at 20:58 UTC, so the publish job is skipped and nothing publishes Value and update time read with gh variable list
HUGGINGFACE_TOKEN, CALIBRATION_TREE_MAX_RAW_BYTES, CALIBRATION_TREE_MAX_GZIP_BYTES, POPULACE_*_HF_REPO and _REVISION overrides, MICROCOSM_UK_LOCAL_CALIBRATION_DIR publisher and readers Optional; not set in the workflow, so defaults apply Not applicable
HF_TOKEN (dashboard) readers on Vercel deploy.yml passes --env HF_TOKEN=$HF_RELEASE_READ_TOKEN from the repository secret Secret exists
BLOB_READ_WRITE_TOKEN, POPULACE_UK_STAGING_HF_TOKEN (dashboard) tree routes and staging readers on Vercel Vercel project environment, managed in Vercel Unverified: not visible from the repository, and unchanged by this PR

Deploy job: the install step and its neighbors

These are the variables read or declared by the new step and by the steps from the Vercel upload to the end of the job. The job runs with environment: Production, so it can use repository, organization and Production environment secrets. In these steps, each secret reaches a command only through that step's env: block. Provider names were checked with gh secret list and gh variable list, which list names only.

Variable Step(s) Provider and wiring Verified
(none) Install frontend dependencies (new): bun install --frozen-lockfile --ignore-scripts No env: block and no ${{ }} expression. It installs anonymously from the public npm registry: the repository has no .npmrc and bunfig.toml sets only the linker. --ignore-scripts skips every lifecycle script (project, workspace and trusted dependency; checked with Bun 1.4.3), so no project or dependency code runs during the step. Yes
MODAL_ENVIRONMENT, VERCEL_CLI_VERSION, VERCEL_ORG_ID, VERCEL_PROJECT_ID Available to every step. VERCEL_CLI_VERSION pins the CLI in the upload and promote commands, and the Vercel CLI reads VERCEL_ORG_ID and VERCEL_PROJECT_ID. Only the earlier Modal steps use MODAL_ENVIRONMENT. Literal values in the workflow-level env: block, not secrets Yes
HF_RELEASE_READ_TOKEN Build unaliased Vercel production candidate Repository secret; step env:; passed to the candidate as --env HF_TOKEN=... Name listed
MICROCOSM_MODAL_KEY, MICROCOSM_MODAL_SECRET Build unaliased Vercel production candidate Production environment secrets; step env: Names listed
MICROCOSM_TELEMETRY_COLLECTOR_URL Build unaliased Vercel production candidate Production environment variable (vars.); step env: Name listed
MICROCOSM_CALCULATION_URL, MICROCOSM_BACKEND_SOURCE_COMMIT Build unaliased Vercel production candidate Outputs of the earlier backend and identity steps; step env: Yes
VERCEL_TOKEN Build unaliased Vercel production candidate; Promote verified Vercel deployment Production environment secret; step env: in each Name listed
VERCEL_AUTOMATION_BYPASS_SECRET Verify staged frontend metadata; Run staged national and state calculations; Verify staged calibration releases and staging Production environment secret; step env: in each, passed to the scripts as --vercel-bypass-secret Name listed
Any variable whose name ends in TOKEN, SECRET or KEY Both calibration steps (bun run --no-install) The smoke script reads these values only to redact them from error details. It needs none of them. Yes
(none) Verify promoted Vercel route; Verify promoted microcosm.institute route; Verify promoted calibration releases and staging No step env:. These steps check the public aliases without a bypass secret. Yes

The install step needs no secrets, and none are added to its environment. Like every step after google-github-actions/auth, it inherits the credential-file path that action exports through GITHUB_ENV, and the OIDC request variables that id-token: write provides. It does not read them, and because it runs no lifecycle scripts, no dependency code runs alongside them. The calibration checks load only the workspace countries module and node:util.

Re-checked after commits 6 and 7: --ignore-scripts and --no-install read no variables. The install step still has no env: and no expression. The staged calibration check reads only VERCEL_AUTOMATION_BYPASS_SECRET and the frontend step's URL output, and the promoted check reads neither.

Unverified: the secret values. Only their names are visible, so whether each value is current can only be confirmed by a deploy. This PR does not change them.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
calibration-diagnostics Ready Ready Preview Oct 20, 2026 2:14pm UTC

Request Review

@anth-volk
anth-volk force-pushed the calibration-tree-package branch from 6bd770f to 562be80 Compare October 19, 2026 20:22
@anth-volk
anth-volk changed the base branch from main to uk-release-cuts-calibration-fit October 19, 2026 20:23
@anth-volk
anth-volk added this pull request to stack #230 October 19, 2026 20:23
@anth-volk
anth-volk force-pushed the calibration-tree-package branch from 562be80 to 3fed47b Compare October 19, 2026 21:09
anth-volk added a commit that referenced this pull request Oct 20, 2026
Brings in the rewritten 1A stack (#220 ca4edf5, #237 0820b3f, #239 753fb5c).
The publisher's build-folder layout now travels in runPublisher's
dependencies (releaseLayout) beside #239's manifest store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from uk-release-cuts-calibration-fit to main October 20, 2026 13:15
@anth-volk
anth-volk force-pushed the calibration-tree-package branch from ca4edf5 to c60479c Compare October 20, 2026 13:15
anth-volk and others added 7 commits October 20, 2026 18:11
The tree builder is about to move out of the dashboard into its own
package. Its outputs are byte-exact contracts: the index records every
part's raw-JSON SHA-256 and gzip size, build ids derive from the inputs,
and a repair refuses any change to the index digest. So the move has to
be proved to change no byte.

This records, from the builder as it is today, what it writes for
representative inputs built from the fixtures the module tests already
use. The inputs cover US and UK, releases and staging runs (version 1,
version 2, and a staged dataset bundle), Belgium, and a UK release with
reported loss attribution. For each, the record holds every part's path,
raw JSON digest and size, gzip size and deflate-stream digest, plus the
build id. It also records release-to-staging comparison bundles built
through the comparison reader, the explorer's root responses read back
from the stored bytes, and the manifest serialization. The gzip header's
operating-system byte varies by platform, so the header is asserted
separately and the digest covers the stream after it.

A second test drives the publisher's dry-run repair of the recorded US,
UK and Belgium releases against a stubbed Hugging Face repository. The
publisher refuses a rebuilt index whose digest differs, so a successful
repair proves its own reads and hashing reproduce the recorded index.
The stubbed repository serves each country's release pointer at the path
the publisher reads it from: `latest.json`, or `latest-national.json`
for the UK since #223.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tree builder and the schema 6 code the dashboard shares with it are
moving to the collector as a Bun worker, and its types and readers will
be published as a package the dashboard imports. That needs the builder
to be one self-contained unit with explicit dependencies and its own
tests. Inside frontend/lib nothing stopped a dashboard-only import from
reaching it.

The package, @policyengine/calibration-tree in frontend/packages/, is
the builder's import closure: the schema 6 types, parsers, serializers
and bundle builder, the comparison builder, Blob I/O behind the existing
injectable client, and the release and staging readers the builder reads
from, including the country registry. Its 30 modules move unchanged
apart from ten import lines that used the dashboard's path alias. Their
tests and fixtures move with them. Next.js routes, the HTTP helpers and
the client hooks stay in the dashboard and import the package through
its subpath exports.

One of the 30 is release-cut.ts, added by #223. It parses the Hugging
Face tag names Microcosm gives each build of a UK release, and
latest-artifact.ts and calibration-tree-manifest.ts import it, so it
moves with them and is exported as release-cut. The dashboard's shared
formatter and the publisher now import it through that export.

Two other additions since this branch began are adjusted to fit. #223's
releases route and its test import the moved modules through the
package. #229's check that the mapped measure concept table matches
integrations/microcosm_policyengine_us/mappings.yaml moves out of the
package's latest-artifact tests into
lib/microcosm/mapped-measure-concept-variables.test.ts, because it reads
a file of this repository that will not move with the package.

A boundary test requires package modules to import only each other,
Node built-ins and declared dependencies (@vercel/blob), and requires
every module to be exported. The package has its own typecheck, chained
into the frontend lint script, so CI runs it without a workflow change.
The readers pass Next.js's fetch cache hint, which Bun ignores; a
declaration seen only by the package's typecheck mirrors Next's.

The golden tests now import the builder through the package and
reproduce the record taken before the move, which moved unchanged. The
workspace sits inside frontend/ because Vercel, CI and the publisher
workflow all install from there. bunfig keeps Bun's hoisted linker so
node_modules keeps its current layout. The former countries path stays
as a re-export for code not yet switched to the package path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The publisher, meaning its reconciliation, repair, size limits and
manifest promotion, is the part the collector will run as its worker.
Leaving it in frontend/scripts would split the builder across the
package boundary and give the collector move two pieces to carry.

It moves into the package unchanged except for its nine import lines,
keeping its command-line entry point. Its unit tests and the golden
dry-run repairs move with it, and the golden tests import it through
the package's publish-calibration-tree export. `bun run
publish:calibration-tree` in frontend/ now runs the package module, so
the workflow and its arguments are unchanged; the package also carries
the same script for running it on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The calibration smoke script now imports
@policyengine/calibration-tree/countries, a workspace package that Bun
resolves only through the link `bun install` creates in
frontend/node_modules. The production deploy job set up Bun but never
installed, so in its fresh checkout both calibration checks would stop with
"Cannot find module '@policyengine/calibration-tree/countries'" and the
candidate would never be promoted. CI did not notice: its jobs install
first, and the workflow test only checked the step's command text.

The deploy job now runs `bun install --frozen-lockfile` in frontend before
the first calibration check. The step reads no secrets. It runs after the
Vercel candidate upload, because `vercel deploy .` sends the repository
root, and installing earlier would put node_modules into the uploaded
working tree. Restoring the relative import would also work today, but the
deploy will need the install once the package is published.

Two tests would have caught this. A workflow test requires every job step
that runs a Bun script to follow a locked frontend install in that job. A
Bun test copies the checkout without node_modules, replays the deploy job's
install steps, and loads the smoke script at each calibration check.

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

The package boundary test found imports with regular expressions over .ts
files only. It missed require() and require.resolve(), triple-slash
references and every other source extension, so a relative require() of
the dashboard's base-path module in a package module passed both the
boundary test and the package typecheck. The regular expressions also
matched import text inside string literals.

The check now lives in test-support/package-boundary.ts. It parses every
.ts, .tsx, .mts, .cts, .js, .jsx, .mjs and .cjs file in the package with the
TypeScript compiler and collects:

- static and type-only imports and re-exports;
- import-equals declarations, import() expressions and import types;
- require(), require.resolve(), import.meta.resolve() and mock.module();
- triple-slash path and types references.

A module specifier that is not a string literal, or any use of
createRequire, is reported, because the check cannot follow it. Relative
imports resolve with every source extension and with TypeScript's mapping
from .js names to .ts sources. Bun's Transpiler.scanImports() was not used
because it drops type-only imports and triple-slash references.

New tests plant packages in a temporary directory beside a stand-in
dashboard module. The check must report each kind of reference, and must
accept package files, Node built-ins and declared dependencies in every
source format.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The deploy job authenticates to Google Cloud before it installs the
frontend's dependencies, and it may request GitHub OIDC tokens. A plain
`bun install --frozen-lockfile` runs the project's and its workspace
packages' lifecycle scripts, and those of any trusted dependency, with that
credential file and those token variables in the environment. The
calibration checks need no install script, so the step now passes
--ignore-scripts. With Bun 1.4.3, the version CI installs, that flag skips
project, workspace and trusted-dependency scripts alike.

A workflow test pins the exact install command. A Bun test plants install
scripts in the checkout copy's frontend and calibration-tree manifests,
replays the deploy job's install steps, and requires that none of the
scripts ran. The existing checkout test confirms the smoke script still
loads after an install that skips scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a script runs in a directory tree without node_modules, Bun installs
any missing package from the npm registry as the script imports it. In a
clean checkout, `bun run frontend/scripts/calibration-deployment-smoke.ts`
asks the registry for @policyengine/calibration-tree. The package is not
published, so the request fails today. Once it is published, a deploy job
whose workspace install went missing would load an unpinned copy from npm,
not the reviewed workspace code.

Both calibration checks, before and after promotion, now run
`bun run --no-install`. With Bun 1.4.2 and 1.4.3 (the version CI
installs), the flag takes effect before the script path, as here or as
`bun --no-install run`. After the path it is passed to the script.

A workflow test requires --no-install among the Bun words before the
script path in both steps. The checkout tests now run each step's own Bun
command, taken from its run text, against a local registry that records
requests. A new test runs both commands in a clean copy without an install:
each must fail on the missing workspace package without asking the
registry for anything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 3 commits October 20, 2026 18:11
The package move kept frontend/lib/microcosm/countries.ts as a re-export,
because telemetry-collector-client.ts type-imported the registry from there
and #216 was still changing that file. #216 is now merged, so the client
imports MicrocosmCountry from @policyengine/calibration-tree/countries and
the re-export is deleted. The registry now has a single import path.

A new test fails while any file exists at the former location, or while any
dashboard module names it, through `@/` or a relative path. It reads module
references from the TypeScript syntax tree, as the package boundary check
does, so type-only imports, import types and mock.module() count. The
use-microcosm hook's comment now points releaseCountry at the package's
latest-artifact module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#223 now checks the UK's latest-national.json whenever the registration
names it: the file must carry schema_version 1, the matching line and a
revision, and the tag it names must be the build its release manifest
names. The golden repair harness served only {release_id}, so the UK
dry-run repair stopped at "latest-national.json has unsupported
schema_version undefined."

For a country registered with its own pointer file, the stubbed
repository now serves a pointer naming a tag of the recorded release at
another commit, with that tag's folder listing and a release manifest
naming the tag. The tag is not among the repository's refs, so the
reconciliation the test checks is unchanged. Countries on latest.json
keep the old pointer. The golden record is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
goldenComparisonRecord builds both sides of a comparison under
Promise.all, so two withRegisteredRepositories scopes overlap. The first
to finish restored the repository overrides while the second was still
building, and the second then restored "unset", deleting the overrides.
With a UK staging repository override set, the UK comparison case read
its staging run from the override and failed.

Overlapping repository scopes now share one cleared environment: the
first to start saves and clears it, and the last to finish restores it.
withHuggingFaceFiles had the same defect with globalThis.fetch, so its
scopes now run one after another.

The golden record description also names the Belgium release it covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk force-pushed the calibration-tree-package branch from c60479c to 4d03579 Compare October 20, 2026 14:13
@anth-volk
anth-volk merged commit b230b78 into main Oct 20, 2026
5 checks passed
@anth-volk
anth-volk deleted the calibration-tree-package branch October 20, 2026 14:54

This branch was successfully deployed

1 active deployment
Preview — 4d035794 Deployed Oct 20, 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.

Extract the calibration tree builder and its schema 6 readers into a self-contained package

1 participant