Skip to content

Read Microcosm releases from releases.json wherever a repository has it, and from tags otherwise - #241

Merged
anth-volk merged 88 commits into
mainfrom
microcosm-releases-json-readers
Nov 5, 2026
Merged

anth-volk merged 88 commits into
mainfrom
microcosm-releases-json-readers

Conversation

@anth-volk

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

Copy link
Copy Markdown
Contributor

Fixes #240

Every reader of a Microcosm dataset repository now asks that repository for releases.json. A repository that has it is read from it and its build folders only. A repository that has none is read from its tags and latest*.json pointer files, as today. No country registration names the layout any more, so when Microcosm's switch commit writes the UK releases.json and deletes the UK pointer files, the dashboard follows with no change of its own.

Merging is safe before the switch. Until a repository has releases.json, its readers give the same results as on main.

  • Extra requests: each reader makes one more request, the 404 for releases.json. Each publisher step reads it at the commit it resolves for main's head, and the pointer file at that same commit. --release <tag> without --sha now also resolves the head once, which it didn't need before.

The layout Microcosm publishes

releases.json                                       the only record of builds and current builds
releases/<release>/<build>/release_manifest.json    names "release" and "build" in top-level fields
releases/<release>/<build>/calibration_diagnostics.json
releases/<release>/<build>/...                      never changed after publishing

release-record.ts reads releases.json with the rules of microcosm-emitter's microcosm_emitter_interface.releases: at most 4 MiB, exact keys, schema_version 1, safe release and build names, role identifiers, each current listed and not withdrawn, and default listed.

How a reader finds the layout (releasesRecordFromResponse)

Hugging Face's answer for releases.json What the reader does
200, and the record passes the writer's rules reads the record and its build folders, and nothing else: no tags, no pointer files, no listing of releases/
404, with no error code or one other than RepoNotFound / RevisionNotFound the repository has no record: reads its tags and pointer files
404 with RepoNotFound or RevisionNotFound, any other status, or a record the rules refuse an error, with no fallback to tags or pointer files

An error means:

  • Release picker: /api/microcosm/releases answers 502 for that country, with the reason.
  • Tree publisher: reconciliation publishes nothing and leaves the latest release as it was, and the run fails.
  • Staging: a local-area Staging candidate gets no comparison baseline; the comparison request fails. Other candidates never read the record.

The readers

  • Tree publisher (publish-calibration-tree.ts): it reads releases.json at the commit it publishes. For reconciliation and the promotion check, that is the head of main; for --latest and --release, it is --sha or the head of main. All release modes write through the same manifest store.
    • With a record, reconciliation publishes every listed build whose folder holds calibration_diagnostics.json and whose release_manifest.json names the same release and build.
      • A build is pinned to the commit that wrote its folder and dated by its published_at.
      • The latest release becomes the current build of the record's default release.
      • A listed build whose folder or release manifest cannot be read is reported as failed, and the other builds are still reconciled.
    • --release <release>/<build> is refused for a repository without releases.json, and --release <tag> for one with it.
    • The promotion check after --latest or --release: it compares the published build with the record's current build by name, and reads the current build's folder only when they match. If that read fails, the build stays registered, a warning says the latest release was not moved, and the run succeeds; the next reconciliation moves it.
    • When the current build of the default release can't be published, the latest-release failure gives the reason: why it is ineligible, or the error from reading its folder or release manifest.
    • Every Hugging Face request has a timeout. The build folder listing now aborts after 30 seconds, like the locator's other lookups.
    • Blob is written only through the manifest store, whose uploadBundle is required, so a store in a test can never fall back to a real upload.
  • Release picker: loadReleasesRecord reads the record at the configured revision, and recordedReleaseInventory lists every build as <release>/<build>, dated by published_at, with its release's role and its withdrawn flag. Without a record, the picker lists folders and tagged builds as before.
  • Staging page: stagingBaselineReleases uses the same record. The local-area baseline is the newest published build of a local_area release that is not withdrawn.
  • Release loader: unchanged by this round. It accepts <release>/<build> as a release id and reads that folder at the pinned commit, whatever the repository's layout.

Decided on 2026-10-29 and 2026-11-04, and applied here

  • Detection instead of a setting. The release_layout registration field from the first version of this PR is gone. Detection is what lets the switch need no dashboard change.
  • An invalid releases.json is refused, as in the table above.
  • Withdrawn builds stay listed and selectable in the picker, labelled "Withdrawn", in the Latest option too. The Staging baseline never picks one.
  • Release roles: Microcosm writes national and local_area (microcosm#1505). The fixtures and docs use those names. The classification already treated any role containing local_area as local-area.

Compatibility with published entries

  • Dashboard manifest entries published from tags or release folders stay in the manifest and stay readable by id, for example from saved links. For a repository with releases.json, reconciliation no longer repairs them and the picker no longer lists them.
  • The one exception is the dashboard's latest release. Until reconciliation makes the record's current build the latest release, the latest release is still such an entry, so the picker keeps it listed: the deployment's pre-promotion check requires it.
  • If a repository's releases.json were removed again, its readers would return to tags and pointer files. Build-folder entries already in the manifest are then skipped by the tag listing (loadPublishedCutReleases).
  • Don't use Vercel's instant rollback to a deployment from before this PR once a UK reconciliation has recorded a <release>/<build> entry. That older code refuses release ids containing /.
  • Next's fetch cache: the picker and Staging read releases.json through it (revalidate). Next 16.2.6 stores only status-200 responses, so the "no releases.json" 404 is never cached, and the first request after the switch commit reads the record. Neither reader reads a pointer file.

Before merging

  1. CI passes, and the team reviews it.
  2. microcosm#1505 has defined the build-folder manifest. A build folder's release_manifest.json must have top-level release and build strings equal to the folder's names; emitter#24's check_repository enforces this.
  3. Order of the deploy and the first UK reconciliation after the switch. The publisher runs from main. The production code before this PR can't read a <release>/<build> entry, so this PR must be deployed before Microcosm's switch commit lands in the UK repository.

Environment variables

This PR adds no environment variable and changes no workflow or deployment file. The changed readers use variables that already exist:

Variable Used by Provider Wiring
HF_TOKEN (or HUGGINGFACE_TOKEN) the publisher's Hugging Face reads, now including releases.json GitHub Actions secret HF_TOKEN Confirmed: publish-calibration-tree.yml sets it in the publish job's env: block, and the secret exists.
HF_TOKEN (or HUGGINGFACE_TOKEN) the dashboard runtime's Hugging Face reads, now including releases.json (picker, Staging) Vercel runtime Unchanged. deploy.yml passes the repository Actions secret HF_RELEASE_READ_TOKEN (it exists) to deploy-dashboard.sh, which sets it with vercel deploy --env HF_TOKEN=… on production deployments. The Vercel project also stores its own Production HF_TOKEN, and Previews use the Preview one. Which value a production deployment uses, and the token's scope, are unverified. It already reads the same private UK repository.
BLOB_READ_WRITE_TOKEN the publisher's dashboard manifest and tree writes GitHub Actions secret Confirmed in the same job's env: block; the secret exists.
POPULACE_UK_HF_REPO, POPULACE_UK_HF_REVISION (and the other countries' repo_env / revision_env) optional repository and revision overrides Vercel or GitHub, if set Unchanged. Not set in the workflow.

Commits and tests, review round 2026-11-04

Only the tests this round adds or changes, plus the existing tests that use a test repository fake this round changes, were run locally. Each was selected by exact name: bun test <file> -t "^(<names>)$". CI runs the rest. All fixtures are synthetic; nothing was read from or written to Hugging Face or Vercel Blob.

Commit What Red Green
6c3a498 Merge main (#253 to #276) with a merge commit. There were two conflicts: the publisher and its golden test. Main's reconciliation reports a failed release and moves on (#253). The build-folder inventory joins that loop, and selectLatestRelease takes the function that reads the latest release. After the textual merge, main's 8 new UK reconciliation tests in publish-calibration-tree.test.ts failed: the UK then read releases.json. The build-folder test "without a current build of the default release, …" expected an exception that main's reporting replaces. The 8 passed once they named the release-folder layout. The build-folder test checks latestReleaseFailure and passed. The 65 tests the branch changes passed.
aac028e / 5c4b634 A build folder that can't be read fails only its own build. publish-calibration-tree-build-folders.test.ts -t "^a build whose folder cannot be read is reported as failed, and the other listed builds are still reconciled$" failed on expect(received).resolves.toBeDefined() (the promise rejected). passed
c0822b0 / ba95f64 Detection in place of release_layout, the error rule, Withdrawn, the Staging baseline, and the roles. 55 tests: 52 by name in 8 files (including the golden "a repair refuses a rebuilt index that differs from the recorded digest and records it as that release's failure"), plus publish-calibration-tree-golden.test.ts -t "^a dry-run repair of the recorded" (3: UK, US and Belgium). 28 failed and 27 passed (details below). 55 passed
9cafbdb Comments only, in three test files. — latest-artifact.test.ts -t "^a build-folder entry in the manifest does not stop the tagged builds being listed$" passed

The 28 failures at c0822b0:

  • countries.test.ts, 1: "no registration says how its releases are published…" failed on the UK registration's release_layout.
  • Route, 5: the tests give the handler the new loadReleasesRecord dependency, so at red the handler, still asking the registration for the layout, answered 502 instead of 200. These tests didn't type-check at red.
  • use-microcosm.test.ts, 1: the Withdrawn label, failed on toEndWith.
  • latest-artifact.test.ts, 1: "the release picker reads no record from a repository without releases.json" failed on resolves.toBeNull().
  • publish-calibration-tree-build-folders.test.ts, 3: the single-build refusals (toThrow), a Belgian repository with releases.json (resolves.toBeDefined()), and a RevisionNotFound 404 (toThrow).
  • publish-calibration-tree.test.ts, 13: each old-layout UK test, because the UK readers read releases.json and refused its 404 ("Hugging Face returned 404 for releases.json."), plus the new "a repository without releases.json is reconciled from its tags and pointer files" (resolves.toBeDefined()).
  • Golden, 2: the UK digest-mismatch repair (selected by name) and the UK dry-run repair (from the -t pattern), for the same reason.
  • staging-comparison-release.test.ts, 2: the withdrawn baseline (toBe) and the repository without releases.json (resolves.toBeDefined()).

The 27 that passed at red guard behaviour that already held:

  • the existing build-folder tests, because the UK registration still chose build folders;
  • the picker's record tests, and its RevisionNotFound test (it already refused any non-200);
  • the route's "fails without the dashboard manifest" test (502 for another reason);
  • the US and Belgian golden repairs, and the publisher test that reads the UK pointer file directly (readUpstreamLatest).

bun run lint (tsc --noEmit for the app and the package) is clean at 9cafbdb.

Review round 1, 2026-11-04

The review found one low-severity bug, five quality points and some test gaps. Each fix is test first, except the store change (a refactor with no behaviour change) and the docs.

Commit What Red Green
b26c767 / 640f8e9 The bug: resolveHfBuildFolderSha had no request timeout. It now aborts after 30 seconds, and a stalled listing fails only its own build. publish-calibration-tree-build-folders.test.ts -t "^a build folder listing that never answers times out after 30 seconds and fails only its own build$" failed on toEqual: the request asked for no timeout. passed
8213181 --latest and --release upload, register and promote through the manifest store, whose default is the same Blob calls, so tests can give them a store in memory. — (refactor) the single-build refusal tests and "the latest publication refuses a pointer tag whose release manifest names another build" passed
a16ba8c / 746e2c4 Quality 1 and 2: promote by name; resolve main's head once per step and read the pointer file at the commit releases.json was looked for. Messages: the ineligible current build's reason; Withdrawn on the Latest option. 12 tests: the 11 this commit adds, and the timeout test, already green. 6 failed on assertions (listed below). 6 passed: --latest with releases.json; three releasesRecordFromResponse tests (a 404 without a code, RepoNotFound / RevisionNotFound, 503 and 401); the refused record leaving a local-area Staging candidate without a baseline; and the timeout test. 12 passed
2571995 Docs: "a local-area Staging candidate", and the single-build modes. — —

The 6 failures at a16ba8c:

  • publish-calibration-tree-build-folders.test.ts:
    • "--release of another build publishes it without making it the latest or reading the current build's folder" (toBe: the folder was listed);
    • "--release of the current build still succeeds when its folder cannot be read again for the promotion check" (resolves.toBeUndefined(): the run failed);
    • "when the current build is ineligible, the latest-release failure says why" (toContain).
  • publish-calibration-tree.test.ts:
    • "a repository without releases.json has main's head resolved once per reconciliation, and its pointer file read at that commit" (toHaveLength: 2 head lookups);
    • "--latest without releases.json reads the pointer file at the commit where releases.json was looked for" (toBe: the pointer was read at the later commit).
  • lib/api/hooks/use-microcosm.test.ts: "the picker's Latest option is labelled Withdrawn when the latest release is a withdrawn build" (toEndWith).

After 2571995, the earlier rounds' 55 tests and the golden dry-run repairs pass again, and bun run lint is clean.

Review round 2, 2026-11-04

Commit What Red Green
7542eb8 / a6b9879 The bug, from 746e2c4: when the current build's folder listing failed (timeout or server error), the latest-release failure said "releases.json lists it, but reconciliation found no candidate". recordedLatest now also looks among the failed builds, and says "Latest release failed to reconcile, so the latest-release selection was not changed:", followed by the read failure. publish-calibration-tree-build-folders.test.ts -t "^when the current build's folder cannot be read, the latest-release failure names the read failure$" failed on toContain, receiving the "found no candidate" message passed, with "when the current build is ineligible, the latest-release failure says why"
d06face Quality 3: CalibrationManifestStore.uploadBundle is required, and publishRelease and staging reconciliation lose their fallback to the Blob upload. The golden tests' two dry-run stores gain an upload that refuses, as their manifest writes already do. — (a type requirement; production's Blob store is unchanged) bun run lint clean. The 6 golden tests using those stores passed (the 4 release repairs and the 2 refused staging repair tests), and so did this PR's tests from earlier rounds.

Earlier rounds

  • 20 October: the first version (ba0bdd4, 59b29a8, aa5408e, 4b68e4c), red-green throughout.
    • Its review fixed four bugs, each test first:
      • the Staging target-change view's release id check (708a186 / 1c311ab);
      • the pre-promotion check's latest release (b32e1f4 / 53845a5);
      • a current named like a built-in object property (71bef39 / e03ef2e);
      • the "View on Hugging Face" link for a build folder (79cd203 / 43348c0).
  • 22 October: merged main (List releases published only as a Hugging Face tag in the release picker #258) in (6f0e193). Fixed, test first:
    • a build-folder entry stopping the tagged builds being listed (13564ff / 50c501f);
    • the writer's 4 MiB limit (9bd2471 / 5a53794).

🤖 Generated with Claude Code

anth-volk and others added 30 commits October 20, 2026 01:07
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>
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>
The dashboard groups, filters and lists calibration targets by the
geography's label. Microcosm's UK local-area builds calibrate
parliamentary constituencies and local authorities side by side, and
115 labels belong to one of each. In the published tree of the
19 Sep local candidate, the explorer's geography map therefore shows
one "Ashfield" tile holding 18 constituency and 26 local-authority
targets, and its Geography filter, the targets page and the comparison
maps select both areas at once. The hierarchy validator already keys a
geography by its level and code; only the label merged them.

A label that more than one level uses now carries its level, for
example "Ashfield (constituency)" and "Ashfield (local authority)".
Labels used at one level stay as published, and so does the country's
own national label, because a target with no resolvable geography falls
back to it at the national level and still means the whole country.
Of the 45 trees published today, only the three UK local candidates
have such labels, so US, Belgian and UK national outputs are unchanged;
the golden record still reproduces byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Staging page compared every candidate with the country's latest
release. For the UK that is the national build, so a local-area
candidate (21,523 targets in September) was set against the national
release (1,168 targets): 634 targets matched, 20,889 counted as added
and 534 as removed.

A candidate whose run manifest records a pipeline the country registers
as local-area (the UK's uk-local-candidate) is now compared with the
local-area release. That release has no pointer file and is never
promoted, so its most recently published build, as the dashboard
manifest records it, is used; release roles come from each release
manifest's dataset_role, as in the release picker. No staged receipt
or run manifest records a dataset role, and the local dataset file was
microcosm_uk_2025_local.h5 in September and
microcosm_uk_2024_25_local.h5 in October, so the contract-validated
pipeline id is the stable signal. With no published local-area release,
the current release is used and the page says so.

National candidates, US and Belgium are unchanged: their comparison
still resolves to the current release, and the response gains a
comparison_release field only for local-area candidates. The page
names the release in the comparison column, the map tab and a note,
and the comparison map starts from that release's build.

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>
The dashboard groups, filters and lists calibration targets by the
geography's label. Microcosm's UK local-area builds calibrate
parliamentary constituencies and local authorities side by side, and
115 labels belong to one of each. In the published tree of the
19 Sep local candidate, the explorer's geography map therefore shows
one "Ashfield" tile holding 18 constituency and 26 local-authority
targets, and its Geography filter, the targets page and the comparison
maps select both areas at once. The hierarchy validator already keys a
geography by its level and code; only the label merged them.

A label that more than one level uses now carries its level, for
example "Ashfield (constituency)" and "Ashfield (local authority)".
Labels used at one level stay as published, and so does the country's
own national label, because a target with no resolvable geography falls
back to it at the national level and still means the whole country.
Of the 45 trees published today, only the three UK local candidates
have such labels, so US, Belgian and UK national outputs are unchanged;
the golden record still reproduces byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Staging reconciliation threw at the first finalized run it could not
publish or repair, such as a repair whose rebuilt index differs from
the digest the manifest records. Every run after it then stayed
unpublished on every later invocation. The September UK local-area
trees are staging builds, and the level-qualified geography labels
change what the builder writes for them, so repairing one would stop
UK staging publication.

Each run's error is now recorded as that run's "failed" outcome, with
the error as its reason, and the remaining runs are still reconciled.
After printing the report, the publisher exits with an error naming
every failed run, so the workflow still fails. Errors before the
per-run loop (an unavailable repository, incompatible runs) still stop
reconciliation.

reconcileStagingBuilds now takes the manifest store that release
reconciliation already takes, and runPublisher accepts that store, so
both can be tested without Vercel Blob.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The calibration map hierarchy document still said the dashboard does
not construct labels for schema 8. buildCalibration now qualifies a
geography label that more than one level uses in a build, so the
document describes that rule and its exceptions, and the tree artifact
document notes that published trees keep the labels they were built
with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Staging page compared every candidate with the country's latest
release. For the UK that is the national build, so a local-area
candidate (21,523 targets in September) was set against the national
release (1,168 targets): 634 targets matched, 20,889 counted as added
and 534 as removed.

A candidate whose run manifest records a pipeline the country registers
as local-area (the UK's uk-local-candidate) is now compared with the
local-area release. That release has no pointer file and is never
promoted, so its most recently published build, as the dashboard
manifest records it, is used; release roles come from each release
manifest's dataset_role, as in the release picker. No staged receipt
or run manifest records a dataset role, and the local dataset file was
microcosm_uk_2025_local.h5 in September and
microcosm_uk_2024_25_local.h5 in October, so the contract-validated
pipeline id is the stable signal. With no published local-area release,
the current release is used and the page says so.

National candidates, US and Belgium are unchanged: their comparison
still resolves to the current release, and the response gains a
comparison_release field only for local-area candidates. The page
names the release in the comparison column, the map tab and a note,
and the comparison map starts from that release's build.

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

Adds release-record.ts: it parses a dataset repository's releases.json with
the rules of microcosm-emitter#24's microcosm_emitter_interface.releases
(exact keys, schema version 1, safe release and build names, role
identifiers, each current build listed and not withdrawn, the default
release listed), names a build by its folder under releases/
(<release>/<build>), and checks that a build folder's release manifest
names its release and build in its top-level fields. Nothing calls it yet.

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

The Staging calibration map stored its "From" build in state and only
replaced it when the stored value was not a published build. The build
manifest usually responds before /microcosm/staging/compare, so the
panel first stored the manifest's latest (national) build and kept it
when the comparison then named the local-area release. Because the
target-change query prefers a build over a release, the "Change from
local-area release" tab showed the national comparison.

The panel now stores only the builds the user picks. comparisonMapBuilds
works out both selectors on every render from those picks and the
current responses, so the defaults follow the comparison until the user
picks a build. A pick of the live candidate as "To" also holds now,
instead of being replaced on the next manifest refetch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For a UK local-area candidate, the "Change from local-area release" tab
still said "Current release calibration target" above a target from the
compared build, and its mode help said removed targets subtract their
"current-release contribution". The target detail both maps open also
labelled its estimate, error, chart series and summaries "current
release".

The release label the Staging page already derives for the candidate
(stagingComparisonBaseline, whose columnLabel is now releaseLabel
because the maps use it too) is passed to StagingTargetChangeMap and to
the candidate-fit map's target detail. targetChangeModeHelp and
stagingTargetDetailPresentation name that release, and the detail's
eyebrow now comes from the presentation. National, US and Belgian
candidates keep "Current release"; the removed-target help now reads
"subtract their contribution in the current release".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
newestPublishedLocalAreaRelease sorted builds with its own
buildPublishedInstant, which computes the same order as the dashboard's
calibrationBuildsNewestFirst: createdAt, else updatedAt, an undated
build last, and ties by build ID. The package cannot import from the
dashboard, so calibrationBuildsNewestFirst and its two tests move into
the package's calibration-tree-manifest module, and both callers use
it. No ordering changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The route's loadComparison dependency returned Promise<object>, which
accepts any object. It now returns StagingComparison, the type
loadStagingComparison returns, exported from the package. The route
test's stub, which carries only the field the page reads, says so with
an explicit cast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A country registration can now say release_layout: "build_folders": its
dataset repository publishes each build in releases/<release>/<build>/ and
lists every build and each release's current build in releases.json. For
such a country the tree publisher reads releases.json at the head commit and
nothing else (no tags, no latest*.json pointer files, no listing of
releases/):

- reconciliation publishes every listed build whose folder holds calibration
  diagnostics and whose release_manifest.json names the same release and
  build, pinned to the commit that wrote the folder and dated by the build's
  published_at;
- the latest release becomes the current build of the record's default
  release; without one, reconciliation fails without moving it;
- a build's release id in the dashboard manifest is its folder,
  <release>/<build>, and its comparison metadata names the build;
- --latest and --release <release>/<build> read the same record;
- entries published from tags or release folders stay as they are, and the
  timestamp migration leaves build-folder entries alone.

No country uses the layout yet, so nothing changes in production.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README's API table said only that staging/compare diffs a candidate
against a published release. It now says that release=latest resolves
to the newest published local-area build for a run whose pipeline is
registered as local-area, documents the comparison_release field, and
documents the staging registration's local_area_pipelines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For a country whose registration says release_layout: "build_folders":

- the release picker (/api/microcosm/releases) lists every build
  releases.json names, as <release>/<build>, dated by its published_at,
  with its release's role (the record's default release is the default
  dataset) and its withdrawn flag; a build is selectable for comparison once
  the dashboard manifest holds its tree. Nothing else is read from the
  dataset repository: no listing of releases/, no tags, no pointer files;
- the Staging page compares a local-area candidate with the newest published
  build of a local-area release releases.json lists;
- the release loader accepts a build folder as a release, reads its files
  from releases/<release>/<build>/ at the pinned commit, keeps the folder as
  its release id when its diagnostics name only the release, and cites that
  folder and commit in the target investigation packet;
- the picker labels such a build by its release and whole build name.

How the picker shows a withdrawn build is left open; the response carries
the flag. No country uses the layout yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The UK registration now says release_layout: "build_folders", so its tree
publisher, release picker and Staging comparison baseline read releases.json
and the build folders it lists, and nothing else: no tags, no
latest-national.json or latest.json, no listing of releases/. Must not be
deployed before Microcosm's one-time migration (P1) has written the UK
build folders and releases.json.

The UK keeps pointer_path, unread while it publishes build folders, so that
removing release_layout returns it to the release-folder readers until
Microcosm deletes the pointer files (P5). The tests of those readers now
name the layout they cover. The artifact and country docs describe the
build-folder layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
anth-volk and others added 3 commits November 4, 2026 14:56
Conflicts, resolved to keep both sides:

- publish-calibration-tree.ts: main's reconciliation reports a release that
  fails and goes on (#253), and reads each tag's commit from /revision
  (#273). The build-folder inventory joins main's loop unchanged, keeps
  dating a build by releases.json, and returns no failed lookups yet.
  main's selectLatestRelease now takes the function that reads the latest
  release, so a build-folder country selects releases.json's current build
  of its default release and a missing one becomes the report's
  latestReleaseFailure instead of an exception.
- publish-calibration-tree-golden.test.ts: main's report-based assertion,
  with this branch's release-folder layout argument.

Tests adjusted to the merged behaviour:

- main's eight new UK reconciliation tests failed after the merge (the UK
  registration selects build folders, so they read releases.json), and now
  name the release-folder layout they cover.
- The build-folder test of a record without a current build expected an
  exception; it now expects the report's latestReleaseFailure, as main's
  tests do for the pointer file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's reconciliation (#253) reports a release whose commit cannot be read
as failed and reconciles the others. A listed build whose folder listing
fails still stops the whole build-folder reconciliation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A listed build whose folder listing or release manifest cannot be read is
now a failed outcome, and the other listed builds are still reconciled, as
main's reconciliation does for a release whose commit cannot be read. This
also covers a release manifest that is not JSON, which used to stop the
whole reconciliation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 3 commits November 4, 2026 15:26
…tting

Every reader is to read a country's releases from releases.json and its
build folders when the dataset repository has releases.json, and from its
tags and pointer files when it has none, so a repository's switch needs no
dashboard change:

- No registration names a release layout (countries).
- The tree publisher reconciles a repository with releases.json from it
  whatever its country (Belgium here), and the UK repository without it
  from its tags and pointer files. A single-build publication takes a build
  folder only where releases.json exists, and a tag only where it does not.
- The release picker's route and the Staging baseline do the same.
- Only Hugging Face's "entry not found" 404 means no record: a 404 that
  says the revision is missing, an unreadable record or a refused one is
  an error, with no fallback to tags or pointer files.
- The Staging baseline is the newest local_area build that is not
  withdrawn, and the picker labels a withdrawn build "Withdrawn".
- Fixture releases use the roles national and local_area.

The old-layout test repositories now answer releases.json with Hugging
Face's "entry not found" 404 (test-support/releases-record-responses.ts),
and the tests no longer pass a layout argument.

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

The release_layout registration setting is gone. Every reader now asks the
dataset repository for releases.json:

- With it, the readers read it and its build folders and nothing else.
- Without it (Hugging Face's 404 for the file), they read the tags and
  pointer files as before.
- A 404 whose error code says the repository or revision is missing, any
  other failure, or a record the writer's rules refuse is an error. The
  picker answers 502, reconciliation publishes nothing and leaves the
  latest release as it was, and Staging has no baseline. No reader falls
  back to tags or pointer files (releasesRecordFromResponse).

The tree publisher reads releases.json at the commit it publishes, so
reconciliation, --latest, --release and the promotion check all follow the
repository. A build folder is refused for a repository without
releases.json, and a tag for one with it. The release route and the Staging
baseline read it at the configured revision (loadReleasesRecord,
recordedReleaseInventory).

The Staging baseline skips withdrawn builds, and the picker labels them
"Withdrawn". The docs describe the detection, and the roles national and
local_area.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments only: three test comments still spoke of a registration that
publishes build or release folders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk anth-volk changed the title Read Microcosm UK releases from releases.json and build folders only Read Microcosm releases from releases.json wherever a repository has it, and from tags otherwise Nov 4, 2026
anth-volk and others added 6 commits November 4, 2026 19:26
… build

Review round 1 found that resolveHfBuildFolderSha sends its Hugging Face
request without a timeout, unlike every other request in the locator and
the publisher, so a listing that never answers holds the publisher job
until its 30-minute limit. The listing should ask for the same 30-second
timeout, and a stalled one should fail only its own build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
resolveHfBuildFolderSha now aborts its Hugging Face request after 30
seconds, as the module's other lookups do, so a listing that never answers
fails only its own build instead of holding the publisher job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--latest and --release now upload, register and promote through the same
CalibrationManifestStore as reconciliation. Its default is the Blob store,
whose calls are the ones these modes made before, so production behaviour
is unchanged. Tests can now give these modes a store in memory.

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

Review round 1's quality points, as tests:

- With releases.json, --release of another build is published without the
  promotion check listing the current build's folder. --release of the
  current build succeeds even when that folder can't be read again, and
  leaves the latest release to reconciliation. --latest publishes and
  promotes the current build (this one already passed).
- Without releases.json, reconciliation resolves main's head once, and
  --latest reads the pointer file at the commit where releases.json was
  looked for.
- When the current build is ineligible, the latest-release failure gives
  the reason, not "<id>@ is not completely published".
- The picker's Latest option is labelled Withdrawn when the latest release
  is a withdrawn build.

These pass already and guard decisions 1 and 2:

- a 404 without an error code means no record;
- RepoNotFound, RevisionNotFound, 503 and 401 answers are errors;
- a refused releases.json leaves a local-area Staging candidate without a
  baseline, rather than comparing it with the current release.

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

- The promotion check, for a repository with releases.json, compares the
  published build with the record's current build by name, and reads the
  current build's folder only when they match. If that read fails, the
  build stays registered, a warning says the latest release was not moved,
  and the run succeeds; the next reconciliation moves it.
- Without releases.json, the pointer file is read at the commit where
  releases.json was looked for: --latest passes that commit to
  readUpstreamLatest, and so do reconciliation and the promotion check, so
  main's head is resolved once per step.
- When the current build of the default release is ineligible, the
  latest-release failure gives its reason.
- The picker's Latest option carries " · Withdrawn" when the latest release
  is a withdrawn build.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Nov 4, 2026
Brings in microcosm-releases-json-readers at 2571995: the build folder
listing's timeout, single-build modes through the manifest store, the
promotion check by name, one head lookup per step and the docs. No
conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 3 commits November 4, 2026 19:58
…failure

Review round 2: since 746e2c4, when the current build's folder listing
fails during reconciliation, the latest-release failure says "releases.json
lists it, but reconciliation found no candidate", because recordedLatest
looks for the reason among the ineligible builds only. It should name the
read failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
recordedLatest now looks for the current build among the failed builds as
well as the ineligible ones. A failed one gives #253's "failed to
reconcile, so the latest-release selection was not changed", followed by
the read failure, for example "Hugging Face build folder lookup failed
with 503."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CalibrationManifestStore.uploadBundle is now required, and publishRelease
and staging reconciliation no longer fall back to the Blob upload when a
store leaves it out. The Blob store defines it, as before, so production is
unchanged; a store in a test can no longer send a bundle to the real Blob
store. The golden tests' two dry-run stores gain an upload that refuses, as
their manifest writes already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Nov 4, 2026
Brings in microcosm-releases-json-readers at d06face: the current build's
read failure in the latest-release message, and the store's required
upload method. No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #277 and #280, which change latest-artifact.ts, its test and
use-microcosm.ts alongside this branch, so CI tests the two together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk marked this pull request as ready for review November 5, 2026 18:59
@anth-volk
anth-volk merged commit baf7ff2 into main Nov 5, 2026
5 checks passed
@anth-volk
anth-volk deleted the microcosm-releases-json-readers branch November 5, 2026 18:59

This branch was successfully deployed

1 active deployment
Preview — 7e9d3801 Deployed Nov 5, 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.

Read Microcosm releases from releases.json and build folders instead of tags and pointer files

1 participant