Repository navigation
Per-line release pointers, the UK 2024-25 national default and its promotion command (#823) - #966
Conversation
105fc26 to
f998e77
Compare
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Review at f998e774 (worktree checkout of the three commits on cc23ec06). Local: ruff check/format clean on the 14 changed files; ci_test_groups.py --verify ok; microcosm-data 733 passed / 2 skipped; build-side assembler, certification, contract-pin, dense assembler, preflight, posture and scorer suites 166 passed. Merge-tree against main d9a248d0 is clean (main has not touched registry.py, loader.py or the assembler since), and the suites also pass on the materialised merge, so this only needs its base retargeted to main; no rebase.
The pointer mechanics follow Max's ruling where it was explicit: line pointer path/payload/reader beside the two existing ones (release.py:163-204, 1008-1050), LatestPointer.revision defaulting to the release id so latest.json stays byte-identical, latest_release refusing line/revision (:992-997), the writer-side reservation of every latest*.json root path (:378-388), the narrowed #823 refusal (:419-427), the loader fetching the manifest at the pointer revision, and the CLI refusals. Only _publish_atomic writes pointers, so latest.json is immutable under promotion. line_for_release_id and its test, the Slack line, _DATASET_KEY with its lockstep test and the filename refusal all check out.
Blocking
- The documented inspect → promote sequence cannot run. Runbook §4 publishes with
--no-latest --tag-name <cut>, §6 promotes the same cut with--promote-line national --tag-name <cut>. Butpublish_releasealways re-runs the staging commit and_create_release_tag(release.py:733-739, noexist_ok), so the second call fails on the existing tag and no pointer is written; I reproduced it against your ownFakeHub(second publish →tag exists,latest-national.jsonabsent, the staging branch left behind). The runbook's 409 paragraph then says delete the branch and retry the same cut, which hits the same error. The only bypass,--no-create-tag, is refused at:405-411.test_national_cut_promotes_only_its_line_pointerpromotes a fresh cut in one shot, so the two-step path has no test. The same root cause covers the partial-failure case: the tag lands before the pointer commit, so any retry after a failure between them dies oncreate_tag. One fix covers all three: accept an existing tag that already points at the revision being published, or give--promote-linea mode that skips the immutable commit and writes only the pointer. Add the two-step test. - The consumer default moves at merge, not at promotion, and that departs from Max's ruling.
resolve("uk")is a pure registry lookup (loader.py:129-166,latest_yearpicks the highest default-variant year), so the moment this merges it returns("uk", 2025)withpointer_path="latest-national.json"(registry.py:114-126). That file does not exist until the first promotion, and the loader raises rather than falling back, so every defaultload("uk")/download("uk")fails between merge and promotion, andavailable()advertises 2025 before anything is published; only explicit("uk", 2023)callers keep working. Max's item 5 keyed the national line as a variant precisely soresolve("uk")is undisturbed; your proposal 2 (default variant) is the one he never answered. Either get his yes and sequence promotion before merge, or register the 2025 default only after the pointer exists. The body's "pe.uk consumer switch is deferred" is true of policyengine.py's own pins but not of this registry default.
Should fix
test_live_latest_pointer_resolves_to_a_coherent_certified_release(test_loader.py:804-809) is parametrized("us", 2024), ("uk", 2023)only; nothing live exercises the new default orlatest-national.json.- The loader now requires every manifest artifact pinned to the pointer revision (
loader.py:319-331), stricter than the contract'sannual_revisionallowance (contract.py:1317). No live impact today (the live US and June UK manifests are uniformly pinned), but a future mixed-revision default would be refused; state the intent in a comment or a test. prepare_release(line=…)skips the non-default-role refusal (release.py:313) and relies online_for_release_idplus the manifest role. I could not find a contract rule thatmicrocosm-uk-2024-25-nationalmust declarenational_default; without one anon_default_local_areamanifest could ride the national pointer through the loader's line-pointer branch (:293-298).
Nits. _LINE_RE (release.py:87) admits evidence, so line_pointer_path("evidence") collides with latest-evidence.json; guarded downstream, but refuse it at the grammar. The assembler dropped the certification.candidate.filename check (sha binding survives at :166). "Pointer written last" means last CommitOperationAdd in one atomic commit, which is the right property; the body slightly oversells it. Commit prose says P1/P2 were implemented by Codex, but the trailers name only the Claude co-author.
Requesting changes for 1 and 2; retarget to main so CI runs.
Max's ruling on microcosm#823: latest.json stays the frozen repository- global pointer on the June 2023 release, and each promotable UK line gets its own schema-1 pointer, latest-<line>.json, whose payload adds the discriminator `line` and the immutable cut tag as `revision` so the loader can follow a per-cut tag without a schema bump. - release.py: line_pointer_path / line_pointer_payload / latest_line_release beside the latest.json and latest-evidence.json families; LatestPointer gains `revision` (defaults to the release id) and `line`; latest_release refuses `line` and `revision` fields; prepare_release(line=...) refuses the evidence tier, --no-latest and --tag-only, refuses a line that is not the release id's, reserves every latest*.json root path, and narrows the #823 refusal to "latest.json may not name a per-cut tag; a line pointer may"; _publish_atomic writes the chosen pointer last; notify_release announces the promoted line and revision. - contract.py: line_for_release_id (national, local-k<N>) and the shared per-cut tag grammar keyed on the line, so the per-cut revision allowance covers every promotable line. - publish_cli.py: --promote-line LINE (requires --tag-name; refuses --no-latest, --tag-only, --evidence). Implemented by Codex from the reviewed plan; reviewed and verified here (722 microcosm-data tests green). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…line pointers in the loader (#823) - registry.py: DatasetSpec.pointer_path (default latest.json, the literal the release module pins); ("uk", 2025) becomes the default variant on microcosm_uk_2024_25.h5 through latest-national.json, so resolve("uk") lands on the new line by latest_year while ("uk", 2023) keeps latest.json; the dense entry is microcosm_uk_2024_25_dense.h5 and stays tag-only; the local-area line is reserved in a comment for #898. - loader.py: the pointer is read through the spec's pointer_path (latest_release for latest.json, latest_line_release for a latest-<line>.json, anything else refused); the release manifest is fetched at the pointer's immutable revision and every artifact must be pinned to it; artifact selection follows the manifest role (default -> default_datasets.national; non_default_local_area only through a line pointer, with empty default_datasets and exactly one microdata artifact); available() lists default-variant keys only. Implemented by Codex from the reviewed plan (stacked on the line-pointer increment); reviewed and verified here (735 microcosm-data tests green). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…set name (#823) tools/assemble_uk_release_dir.py: `_DATASET_KEY = "microcosm_uk_2024_25"` replaces the key derived from the candidate stem; a candidate not named microcosm_uk_2024_25.h5 (the name tools/build_uk_rowwise_candidate.py --release-role national writes) is refused by name; default_datasets, build_manifest.dataset.filename, the calibration NPZ and the release manifest artifact key follow the constant, and a test pins it to the registry's ("uk", 2025, "compact") filename so the assembler and the loader stay on one name. The JSON summary keeps `publish_command` (the inspect publication, --no-latest --tag-name <cut>) and adds `promote_command` (--promote-line national --tag-name <cut>, without --no-latest). Runbook 806 §4 shows both commands and §6 becomes "Promote the reviewed cut" (only latest-national.json moves; latest.json stays on June 2023; resolve("uk") then lands on 2025; the alert names the line and revision); README "Releasing & alerts" and CLAUDE.md name line promotion. Implemented by Codex from the reviewed, narrowed plan; reviewed and verified here (assembler, certification, scorer and registry suites: 54 green). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ional line waits for its pointer, one role per line
Blocking 1. publish_release always re-ran the staging commit and
create_tag, so the documented inspect -> promote sequence died on the
existing tag and no pointer was written; a retry after a failure between
tag creation and the pointer commit died the same way. _publish_atomic
now recognises an existing tag before touching the staging branch: when
the tagged release_manifest.json is byte-identical to the local one the
immutable revision already exists, so no staging branch, no immutable
commit, no create_tag, and only the main commit carrying the pointer is
written; a tag that describes another release refuses. Tested: the
two-step sequence, the retry after an injected pointer-commit failure,
the foreign-tag refusal both ways.
Blocking 2. resolve("uk") is a registry lookup, so registering the 2025
national entry as the default would have sent every default load to a
pointer that does not exist until the first promotion. The entry is
registered as variant "national" (Max's ruling keys the line as a
variant): load("uk", 2025, variant="national") follows
latest-national.json, resolve("uk") and available() stay on 2023, and
the flip to the default variant is a one-line follow-up after promotion.
Should-fix 3-5. The live pointer test covers the national variant
(skipping until the pointer exists, asserting the cut-tag revision
shape); the loader states why every artifact must pin the pointer's
revision and the latest.json path is tested for it; dataset_role_for_line
binds national -> national_default and local-k<N> ->
non_default_local_area, refused by the publisher and the loader.
Nits. line_pointer_path refuses "evidence" at the grammar; the assembler
binds certification.candidate.filename to the line's dataset filename
again; runbook 806 and the README carry the sequencing.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
f998e77 to
7053b91
Compare
|
Thanks — retargeted to Blocking 1, inspect → promote. Reproduced on the Blocking 2, the default at merge. Agreed: 3. The live test is parametrized over the national variant too, skipping (not passing) until the pointer exists on the Hub, and asserting the cut-tag revision shape once it does. Nits. 🤖 Generated with Claude Code |
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Second pass at 7053b917 (retargeted to main, rebased; merge-tree against 7c5306f3 clean; CI 24/24 green). Local: microcosm-data suite 741 tests, 0 failures, plus the same suites on the materialised merge, 0 failures; ruff clean; ci_test_groups.py --verify ok.
Both blockers closed.
- Inspect → promote.
_publish_atomicnow reads the revision an existing tag points at (release.py:969) and refuses unless that tag'srelease_manifest.jsonis byte-identical to the local one (:989); on a match it skips the staging branch, the immutable commit andcreate_tagand writes only the pointer commit. I re-ran my two-step repro against yourFakeHub: the second publish now succeeds andlatest-national.jsonresolves to the cut tag.test_inspect_then_promote_reuses_the_immutable_tag(test_release.py:2279) andtest_retry_after_a_failed_pointer_commit_reuses_the_tag(:2332) cover the sequence and the retry; the foreign-tag refusal is tested both ways. Runbook §4 now says only a failure before the tag exists can leave the staging branch behind. - Default at merge. The 2025 national entry is registered as variant
national(registry.py:125);test_resolve_uk_default_stays_on_2023_until_the_national_line_is_promoted(test_loader.py:348),test_resolve_uk_national_variant_follows_the_line_pointer(:361) andtest_resolve_uk_2023_still_uses_latest_json(:387) pin it, andtest_registry.py:152names the follow-up flip. Runbook §6 (:207) and the README say the flip is a one-line PR after the first promotion. That is Max's item 5 as written.
Items 3–5 and the nits all landed: the live pointer test is parametrized over the national variant, the loader states the one-revision-per-pointer intent and tests both paths, dataset_role_for_line (contract.py:634) binds national → national_default and local-k<N> → non_default_local_area with refusals in both publisher and loader, line_pointer_path("evidence") is refused at the grammar (release.py:170), and the assembler binds the candidate filename again.
One small question, not blocking: for the national parametrization of the live pointer test, make sure an absent latest-national.json (an EntryNotFoundError) is what skips, and that an auth failure on the private repo does not also skip silently; _is_offline_error lists LocalEntryNotFoundError, and I could not tell from the code which exception the remote absence raises.
Approving.
|
Approval stands. Three small things I'd fold in before merge, since the changelog will publish as written:
|
…ive skip; fix the stale registry fragment Vahid's post-approval notes on #966. _tag_revision treated any repo_info failure as "no such tag", so a transient failure with the tag in place re-entered the create path and died on create_tag with the staging branch left behind, which runbook §4 now says cannot happen; _require_tag_describes_release treated any download failure as "describes another release" and sent the operator to a fresh cut tag, orphaning a good one. Both now swallow only the Hub's (or the fake's) "does not exist" answers (RevisionNotFoundError / KeyError, EntryNotFoundError / FileNotFoundError) and let everything else propagate; tested with injected transient failures on both calls. The live pointer test skipped "not yet promoted" on any 404 or EntryNotFoundError, before the credentials branch, so an anonymous RepositoryNotFoundError or a post-promotion manifest 404 skipped too; the skip now requires an EntryNotFoundError naming the pointer path and runs after the credentials branch. The 823-uk-2025-registry fragment still announced the 2025 default and neither fragment nor the data README recorded that available() lists default-variant pairs only; both say so now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Thanks — all three are in 🤖 Generated with Claude Code |
test_synthetic_smoke_command_refuses_release_options runs the spine builder and expects argparse's refusal within 30 s; reaching argparse imports the build package and the engine, and the 3.14 CI runner took longer than that on #966 while the 3.13 lane passed. 120 s, as #1006 sets it, is what a cold import needs; the assertion is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Checked |
Brings in the 19 main merges since 6442030: the Route A remediation stack (#1016, #1018, #1017, #1025, #1024, #1028), #1029, #1005, #1008, #1004, #992, #1031, #1015, #1033, #994, #954, #966, #1006 and #1010. Eight conflicted paths, each recorded in docs/native-integration-20260923.md: - test.yml keeps the native sharded matrix (main's --durations=25 is already in every native pytest call); - the us_runtime facade stays lazy and gains main's three fiscal-target exclusion exports (the parent union, 920 names; the facade-union test is re-pinned to that union's digest); - reform_validation.py keeps main's _released_engine_state and the native explicit-constructor default_simulate_factory; - build_us_fiscal_refresh_release.py carries the native explicit consumer seams (formula metadata, dataset and microsimulation constructors, SPM selection) onto main's household-batched post-export scorer and batched base materialization; omitted seams keep main's exact calls, and _main supplies none. Three native test files that addressed the removed frame-based factory now address the scorer with the same assertions; - identity pins observed on the merged tree: five EXPECTED_HASHES entries, the regenerated F0 coverage report (42,239/42,239 fields, 41/41 checks), the US spec identity 2dfa51b8... and the loader golden f2047cb9... tools/generate_us_bundle_from_constants.py --check, tools/ci_test_groups.py --verify, the CI matrix contract and ruff pass. The fiscal consumer, formula-metadata, shared target/solve and calibration-attachment files pass; the rest of the fiscal battery and the known pre-existing failures are follow-up work on this branch. No actual-data native run, release certification or publication. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Engine#450) After a --tag-only or --no-latest publish, the standard publish of the same release directory must reuse the release-id tag and write only the latest.json commit. PolicyEngine#966's tag reuse in _publish_atomic already covers this path for every pointer, not only line pointers; these tests pin it for the US default and fail with the original 409 ("tag exists") when the existing-tag lookup is disabled. A tag that describes another cut refuses before any commit. README says how to flip. Closes PolicyEngine#450. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part of #823 (publication). Stacked on #965: the base is that PR's branch, so CI runs here only once #965 merges and this PR is retargeted to
main. Three commits, one per increment of the reviewed plan; the shape follows Max's ruling on #823 (comment 5730335820) and the five proposals posted on 2026-09-18 (comment 5731044925), which this PR assumes and can be revised against his answers.P1 — per-line release pointers (
microcosm-data)latest.jsonstays the frozen repository-global pointer on the June 2023 release. Each promotable UK line gets its own schema-1 pointer,latest-<line>.json, whose payload adds the discriminatorlineand the immutable cut tag asrevision, so a line can follow a per-cut tag with no schema bump.release.py:line_pointer_path,line_pointer_payload,latest_line_releasebeside thelatest.jsonandlatest-evidence.jsonfamilies;LatestPointergainsrevision(defaults to the release id) andline;latest_releaserefuseslineandrevisionfields;prepare_release(line=…)refuses the evidence tier,--no-latestand--tag-only, refuses a line that is not the release id's, reserves everylatest*.jsonroot path, and narrows the Promotion for the constant-id national line: pointer and certified-loader extension #823 refusal to "latest.jsonmay not name a per-cut tag; a line pointer may";_publish_atomicwrites the chosen pointer last; the Slack alert names the promoted line and revision.contract.py:line_for_release_id(microcosm-uk-2024-25-national→national,microcosm-uk-2024-25-local-k<N>→local-k<N>, everything elseNone) and the shared per-cut tag grammar keyed on the line.publish_cli.py --promote-line LINE(requires--tag-name; refuses--no-latest,--tag-only,--evidence).P2 — registry default and loader
registry.py:DatasetSpec.pointer_path(defaultlatest.json); the 2025 national line is registered as variantnationalonmicrocosm_uk_2024_25.h5throughlatest-national.json(superseding the original "default variant at merge" wording, see review round 1 below), while("uk", 2023)keepslatest.jsonloader.py: the pointer is read through the spec's path (latest_releaseforlatest.json,latest_line_releaseforlatest-<line>.json, anything else refused); the release manifest is fetched at the pointer's revision and every artifact must be pinned to it; artifact selection follows the manifest role (national_default→default_datasets.national;non_default_local_areaonly through a line pointer, with emptydefault_datasetsand exactly one microdata artifact);available()lists default-variant keys only.P3 — the national assembler's promotion command and the docs
tools/assemble_uk_release_dir.py:_DATASET_KEY = "microcosm_uk_2024_25", pinned to the registry's("uk", 2025, "compact")filename by a lockstep test; a candidate not namedmicrocosm_uk_2024_25.h5(the name--release-role nationalwrites on Build the UK national line through the rowwise driver's release role (#823) #965) is refused by name;default_datasets, the build manifest's dataset filename, the calibration NPZ and the release manifest artifact key follow the constant; the JSON summary keepspublish_command(inspect,--no-latest --tag-name <cut>) and addspromote_command(--promote-line national --tag-name <cut>).latest-national.jsonmoves;latest.jsonnever;load("uk", 2025, variant="national")then follows the pointer and the default flips in a follow-up; the alert fires); README "Releasing & alerts" and CLAUDE.md name line promotion.Verification
microcosm-datasuite (735 tests) and the build-side assembler, certification, contract-pin, dense assembler, pre-flight, posture and scorer suites green on this stacked tree;ruff check .,tools/ci_test_groups.py --verify.latest.jsonbytes and asserts that promoting a national cut changes onlylatest-national.json, writes the pointer last, tags the cut, and thatlatest_line_releaseresolves to the cut tag.Not in this PR
calibration-diagnostics:pointer_pathon the country registration,loadPointerReleaseIdreturning the pointer's revision) is on its own branch there.pe.ukconsumer switch is deferred; the exact-K local-area line (identity and pointer reserved here) is Complete UK exact-K release parity and make the runtime country-neutral #898.🤖 Generated with Claude Code
cc @MaxGhenis
Review round 1 (2026-09-23): Vahid
Base retargeted to
mainand the three commits rebased over it (clean); round commit7053b917._publish_atomicrecognises an existing cut tag before touching the staging branch: when the taggedrelease_manifest.jsonis byte-identical to the local one the immutable revision already exists, so no staging branch, no immutable commit, nocreate_tag, and only the main commit carrying the line pointer is written; a tag that describes another release refuses. Tested on theFakeHub: the documented two-step sequence (one tag, one extracreate_commit,latest.jsonbyte-identical), the retry after an injected pointer-commit failure, the foreign-tag refusal both ways. Runbook §4 now says which failures still need the staging branch deleted (only a failure before the tag exists).resolve("uk")is a registry lookup, so a 2025 default entry would have sent every default load to a pointer that does not exist until the first promotion. The 2025 national entry is registered as variantnational(Max's Promotion for the constant-id national line: pointer and certified-loader extension #823 ruling keys the line as a variant):load("uk", 2025, variant="national")followslatest-national.jsononce it exists,resolve("uk")andavailable()stay on the June 2023 line, and the flip to the default variant is a one-line follow-up PR after the first promotion, with its test. Runbook §6 and the README carry the sequencing. (This supersedes "resolve('uk') lands on 2025 at merge" in the sections above.)latest.jsonpath is tested for it;dataset_role_for_linebindsnational→national_defaultandlocal-k<N>→non_default_local_area, refused by the publisher on promotion and by the loader on resolution.line_pointer_path("evidence")refuses at the grammar; the assembler bindscertification.candidate.filenameto the line's dataset filename again; the pointer wording above is corrected.Verification at
7053b917:test_release,test_loader,test_registry,test_publish_guard,test_contract, the national and dense assemblers, certification, contract pins and preflight suites green locally (556 + 221 tests across the two runs, one live skip);ruff check .andtools/ci_test_groups.py --verifyclean.Post-approval notes (
72d6094c): the tag-reuse path swallows only the Hub's "does not exist" answers (a transient failure looking up the tag or its manifest now propagates instead of re-entering the create path or sending the operator to a fresh cut tag; tested with injected failures on both calls); the live pointer test skips "not yet promoted" only on anEntryNotFoundErrornaming the pointer path, after the credentials branch; the823-uk-2025-registryfragment and the data README now describe the national variant and the default-variant-onlyavailable().