Skip to content

Accept chronicle-era Chronicle artifact schema ids and fact-key domains; CHRONICLE_* env names (chronicle#143 consumer side) - #849

Open
MaxGhenis wants to merge 23 commits into
mainfrom
chronicle-dual-accept
Open

Accept chronicle-era Chronicle artifact schema ids and fact-key domains; CHRONICLE_* env names (chronicle#143 consumer side)#849
MaxGhenis wants to merge 23 commits into
mainfrom
chronicle-dual-accept

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Consumer side of the Ledger→Chronicle epoch migration
(PolicyEngine/chronicle#143).
Chronicle will introduce chronicle-era hash domains (chronicle.aggregate_fact.v3,
chronicle.semantic_fact.v3, …) and chronicle-named schema ids
(policyengine_chronicle.consumer_artifact.v2, chronicle.consumer_fact.v2) for
newly emitted rows at a declared cutover; ledger-era ids stay valid history
forever. Dual-domain acceptance has to land in every validator before the emit
flip, so Microcosm accepts both eras starting now. The acceptance half on
Chronicle's side is landing in a parallel lane.

What changed

microcosm.build.chronicle_epoch — one place that knows which identity is which era.
Three rules, encoded rather than asserted:

  1. Fact keys are opaque. Microcosm never reconstructs a Chronicle key from a
    payload, so it never needs to know how the domain string feeds the hash.
    Comparison stays exact string equality — correct in both eras. What goes
    dual-era is the question "is this a Chronicle fact key?".
  2. No validator hard-codes an epoch. Epoch detection is structural — the
    namespace segment of <namespace>.<family>.v<n>:<digest> — not a lookup in a
    frozen list of domain strings. chronicle#143 declares the v3 spelling only
    for the aggregate and semantic fact families; the version numbers the other
    families will take are not declared yet, and this module deliberately does not
    guess them. chronicle.source_cell.v7:… resolves anyway.
  3. Microcosm-minted keys are epoch-independent. They live in Microcosm-owned
    namespaces (microcosm.derived_fact.*, populace_us_trade.*), sit outside
    both eras, and must not move when a source row crosses the cutover.

Only identity strings the spec names explicitly are pinned as literals.

Loading accepts both eras and records which one arrived. The consumer-artifact
manifest's schema_version is a membership test over both eras. provenance()
now carries schema_epoch, fact_key_epochs, and fact_schema_versions
alongside the verbatim observed schema_version, so a release manifest witnesses
which era actually resolved its targets and flags a mixed-epoch feed instead of
silently reporting one era.

Acceptance widened; nothing narrowed. Only the manifest id is gated. The
per-row schema_version is carried and reported, never gated — real feeds stamp
rows from namespaces belonging to neither era, and gating it would fail the build
closed on its own pinned input. The pinned US fiscal-refresh feed
consumer_facts_buildn_v9_4.jsonl declares arch.consumer_fact.v1 on 37,006 of
its 37,405 rows and ledger.consumer_fact.v1 on the other 399;
test_uk_firm_generation.py:584 builds rows in that same arch.* namespace.
Epoch witnessing likewise reads every Chronicle key a published row carries
(nine single-key fields plus two lineage key lists in the captured feed fixture),
not just the four identifiers targets resolve by — Chronicle moves families
independently, so a row can straddle the cutover with a ledger-era aggregate key
and a chronicle-era source-release key.

microcosm.build.logbook_env — the Logbook env dual-read window. This is a
separate Logbook naming cleanup (microcosm#632) riding along on this branch, not
part of the chronicle#143 epoch migration above: POPULACE_LEDGER_URL /
_KEY / _API_KEY / _EXPORT_KEY are the Logbook store's own credentials
(Supabase logbook schema, logbook_writer / logbook_exporter roles), not
Chronicle names — naming the preferred spellings CHRONICLE_* would have
recreated the exact collision the Logbook rename fixed. LOGBOOK_* preferred,
the ledger-era name honored with one DeprecationWarning per process per name
(a build loop reads these repeatedly; one warning, not a storm). One helper,
one table; adding a variable to the window is one row.

Nothing renames on disk or in artifacts. Emitted schema ids, populace_* and
microcosm.* key namespaces, diagnostic field names (ledger_aggregate_fact_key,
ledger_member_fact_keys, ledger_fact_pin, ledger_commit), H5 attributes,
goldens, and fixtures all stay frozen at v1 (#639). No golden was regenerated.

Audit

I audited every site that compares, parses, or mints a Chronicle fact key or
schema id — ledger_targets.py and congressional_district_vintage.py read in
full, plus a repo-wide sweep for ledger.<family>.v<n>, policyengine_ledger,
consumer_artifact, consumer_fact, aggregate_fact_key, semantic_fact_key,
source_cell, dimension_set, concept_alignment, av2:, and every
os.environ read — across 8 surfaces with an adversarial verification pass on
every candidate.

Result: exactly three hard-coded epoch literals exist in non-test source, all
in the two files below. Everything else either carries keys opaquely or mints into
a Microcosm-owned namespace. Every other candidate site was refuted on
verification.

File:line What it was How it is epoch-tolerant now
ledger_artifact.py:58 CONSUMER_ARTIFACT_SCHEMA_VERSION = "policyengine_ledger.consumer_artifact.v1" Now the shared LEDGER_CONSUMER_ARTIFACT_SCHEMA_VERSION, and it names only what Microcosm emits. Acceptance is the set, not this.
ledger_artifact.py:187 schema_version != CONSUMER_ARTIFACT_SCHEMA_VERSION → raise is_accepted_consumer_artifact_schema_version(...) — membership over both eras; the error names both.
ledger_artifact.py:307 (no per-row schema check existed) Still none, deliberately. The per-row id is carried and surfaced through fact_schema_versions; gating it would reject arch.consumer_fact.v1, which the pinned release feed stamps on 37,006 rows.
ledger_artifact.py:160-163 provenance() recorded schema_version only Adds schema_epoch, fact_key_epochs, and fact_schema_versions; the observed id is still recorded verbatim.
import_entry_facts.py:164 "policyengine_ledger.consumer_artifact.v1" literal From the shared module. write_consumer_artifact(…, schema_version=…) (:612) takes the declared id as an argument, membership-checked over both eras; the default is unchanged, so emitted bytes do not move.
import_entry_facts.py:165 "ledger.consumer_fact.v1" literal From the shared module; the loader accepts either spelling on the way back in.
import_entry_facts.py:890 mints populace_us_trade.<kind>.v1:<digest> Unchanged by design. Microcosm namespace, outside both eras, frozen by #639 — and these rows are minted from Census/CBP source bytes, not derived from a Chronicle row whose epoch they could inherit. Pinned by test.
congressional_district_vintage.py:399, 402, 636, 639 mints microcosm.derived_fact.*.v1 / microcosm.semantic_fact.*.v1 Unchanged by design. The digests cover the source row's semantic identity (_translated_fact_key) and the proxy's (source_record_id, geography, value) — never its Chronicle key — so a chronicle-era source mints byte-identical derived keys. Pinned by test; otherwise cutover day would silently re-identify every derived fact a target is pinned to.
ledger_targets.py (all 2203 lines) No epoch literal anywhere in the module. Keys are read through _fact_key/_str_at and compared verbatim; _ledger_fact_index buckets a row under all five key slots, so a pin in either epoch resolves. Left alone, covered by new mixed-epoch tests.
logbook.py:839, 840, 843 os.environ.get("POPULACE_LEDGER_{URL,KEY,API_KEY}") logbook_env(LOGBOOK_{URL,KEY,API_KEY}_ENV) dual-read.
logbook.py:110 LEDGER_API_KEY_ENV = "POPULACE_LEDGER_API_KEY" = LEGACY_API_KEY_ENV; the name stays exported for callers and messages.
tools/logbook.py:260-262 os.environ.get("POPULACE_LEDGER_{URL,EXPORT_KEY,API_KEY}") logbook_env(...); the "remote export requires …" error now names both spellings via describe_logbook_env.
tools/logbook.py:63-64 REMOTE_{EXPORT_KEY,API_KEY}_ENV literals Bound to the shared legacy constants.
firm_generation.py:168-170 LEDGER_{ONS_TURNOVER,ONS_EMPLOYMENT,HMRC}_BANDS These only look like env vars to a grep — they are band maps. Chronicle-named aliases added beside them; the ledger-era names stay, since they are what the generator imports.
source_coverage.py:41 LEDGER_US_SOURCE_COVERAGE_CONTRACT_COMMIT Same: a git commit pin, not an env var. Alias added; the ledger-era name stays because the ledger_commit diagnostic field it feeds is frozen.

Two names from the brief that grep resolves differently, recorded so the next
reader does not go looking:

  • LEDGER_TARGETS is not an environment variable. The only matches are the
    DIRECT_LEDGER_TARGETS / INDICATOR_LEDGER_TARGETS module dicts in
    us_runtime/fiscal_targets.py — mapping tables, no epoch, untouched.
  • The env vars are spelled POPULACE_LEDGER_*, not bare LEDGER_*; there are
    exactly four (URL, KEY, API_KEY, EXPORT_KEY) and they are the ones the
    dual-read window covers.

Also verified absent, so nothing needed regenerating: no golden under
packages/*/tests/golden/** contains a Chronicle domain string, and no H5
attribute name in the repo embeds ledger.

Tests

New files (both engine-free; tools/ci_test_groups.py --verify places them in
rest / shared-spec, alongside test_ledger_artifact.py, and reports ok):

  • test_chronicle_epoch.py (38 cases) — key parsing and epoch resolution for
    both eras, chronicle-era families with undeclared version numbers, Microcosm
    namespaces resolving to neither era, malformed keys returning None rather
    than raising, row/feed epoch reporting across source_cell_keys and
    concept_alignment_key, a synthetic consumer artifact loaded under each
    schema id
    , a mixed-epoch feed whose provenance witnesses both eras, bare
    feeds, rejection of an unknown manifest id naming both accepted spellings,
    a per-row id outside both eras still loading and being reported verbatim,
    every straddling key field witnessed for its own epoch, and an inventory check
    that grounds the witnessed field list in the captured feed fixture rather than
    in a guess.
  • test_logbook_env.py (16 cases) — preferred name wins silently, legacy
    name honored with a warning naming both spellings, the warning fires once per
    process
    across repeated reads, per-name independence, empty-as-unset, and
    logbook._remote_config() reading both eras.

Added to existing suites:

  • test_ledger_targets.py — a mixed-epoch feed compiles to targets whose names
    and metadata carry each row's own key verbatim, and a reference pinned to a
    chronicle-era key resolves.
  • test_us_congressional_district_vintage.py — chronicle-era source rows mint
    byte-identical microcosm.derived_fact.* keys on both the vintage-translation
    and state-total-proxy paths.
  • test_us_trade_facts.py — the minted artifact still declares the ledger-era id,
    loads under either, and the fact bytes are identical when only the declaration
    moves; a bogus declaration is refused.

uv sync --all-packages --extra us · uv run pytest packages/microcosm-build -q
· uv run ruff check . — all green; full output in the lane report. No
publish, promotion, or release action was taken.

Review fixes (round 2)

Eight findings from the Fable+Sol gate, all addressed. The identity half of
the original PR was wrong about which ids exist, so the correction is
structural rather than cosmetic — everything above this section that
describes a two-id accepted set or namespace-based epoch detection is
superseded here.

The ids were one generation off at both ends

Read from PolicyEngine/chronicle at origin/main this pass, not restated:

Identity Source Epoch
policyengine_ledger.consumer_artifact.v2 policyengine_chronicle/consumer.py:30, README line 299 ledger — what Chronicle emits today
policyengine_ledger.consumer_artifact.v1 import_entry_facts.py:164 ledger — what Microcosm mints
policyengine_chronicle.consumer_artifact.v3 chronicle-era successor chronicle
ledger.consumer_fact.v1 chronicle/consumer_contract.py:28 ledger
chronicle.consumer_fact.v2 chronicle-era successor chronicle

The accepted set was {policyengine_ledger.consumer_artifact.v1, policyengine_chronicle.consumer_artifact.v2}. That rejects every artifact
Chronicle publishes right now, and names a .v2 chronicle-era id that will
never exist. All three artifact ids are accepted now, and a test loads a
manifest built field for field from build_consumer_artifact on chronicle
main.

There is no origin/epoch-dual-domain branch on chronicle
(git ls-remote --heads origin | grep -i epoch → empty), so the chronicle-era
successors are declared here under the rename rule the spec states — same
family, new namespace, version bumped by one — with the two the consumer
migration pins spelled out. That rule puts source_cell at
ledger.source_cell.v1chronicle.source_cell.v2, a version behind the
derived families' v2v3.

Epoch is now a declaration, not a parse

DECLARED_IDENTITIES in chronicle_epoch.py is an explicit table of literal
identity strings; DECLARED_IDENTITY_EPOCHS keys it by
(namespace, family, version) → epoch. The ledger-era half is transcribed
from Chronicle's own hash-domain constants (chronicle/core.py,
chronicle/sources/*.py, chronicle/consumer_contract.py), with the file it
came from noted per block.

The previous structural rule read the namespace segment and answered
"chronicle" for chronicle.<anything>.vN. That reports where a key claims
to be from as though it were issued identity — the opposite of what pinning a
fact key is for. chronicle.source_cell.v3 and
chronicle.some_family_nobody_declared.v11 are now labelled undeclared:
parsed, reported in provenance with the domain named
(undeclared_fact_key_domains), never counted as an era. The tests that
certified those spellings as valid are corrected.

Two prose claims went with it. chronicle#143 does not "declare the v3
spelling for the aggregate and semantic fact families" — it lists
"dual-hash window or v3 domains" as open options. That sentence is gone from
the module, the tests, and (annotated in place, dated) the lane journal.

Tests could reach the live Logbook

_remote_config() returns ('https://example.invalid', 'x', 'x') under
LOGBOOK_URL + LOGBOOK_KEY alone. The per-module delenv lists named only
POPULACE_LEDGER_* — they predate the LOGBOOK_* spelling — so a
credentialed developer or runner shell walked straight past them into the
append path of an append-only store.

packages/microcosm-build/tests/conftest.py now carries an autouse fixture
with two independent guards: every variable in the dual-read window is
cleared in both generations (read from the window itself, so adding one there
cannot leave a hole here) plus POPULACE_LOGBOOK_PREV_ROW_DIGEST; and the
Logbook urlopen is replaced with one that raises — on every module holding
it, since tools/logbook.py binds the name at import, and on the opener it
delegates to, which closes the path for any importer the sweep does not find.
allow_logbook_network restores the real callables for a test that means it.
Nothing in the suite asks for it.

Proof, with the polluted environment the finding describes:

LOGBOOK_URL=https://example.invalid LOGBOOK_KEY=x LOGBOOK_API_KEY=y \
LOGBOOK_EXPORT_KEY=z POPULACE_LEDGER_URL=https://legacy.invalid \
POPULACE_LEDGER_KEY=k uv run pytest packages/microcosm-build -q

The rest

  • UK release evidence. uk_runtime/calibration_run.py::_ledger_provenance
    rebuilt the block field by field and so recorded the hashes but not
    schema_epoch / fact_key_epochs / fact_schema_versions. It delegates to
    LedgerConsumerArtifact.provenance() now, keeping only the manifest
    sub-block UK-shaped (it feeds the run's identity digest). Covered by a real
    mixed-epoch artifact through run_uk_calibration, asserted in the build
    record and the signed diagnostics build block, plus a property test that
    every shared field reaches the UK block rather than a list that would go
    stale.
  • import microcosm.build.logbook_env as env bound the function: the
    barrel re-exported it under its own module's name, and import a.b as c
    prefers an existing attribute. The barrel now re-exports only
    logbook_env_names; regression test included.
  • CHRONICLE_US_SOURCE_COVERAGE_CONTRACT_COMMIT now reaches the
    microcosm.build.us_runtime barrel like its ledger-era sibling.
  • Non-string schema_version. A JSON list or object raised
    TypeError: unhashable type out of the frozenset membership test, bypassing
    the loader's own message. The predicates are total over JSON; the loader
    raises the documented unsupported-schema ValueError. Parametrized over
    list, object, empty list, empty object, int, bool, and null.
  • tools/logbook.py --remote help advertised POPULACE_LEDGER_URL alone,
    so an operator configuring from --help set up the spelling the dual-read
    window exists to retire and then got warned for it. LOGBOOK_* first,
    ledger-era names given as the fallback they are.

Round-2 verification

uv sync --all-packages --extra us                     ok
uv run pytest packages/microcosm-build -q             7116 passed, 47 skipped, 2 failed
  ... with LOGBOOK_* + POPULACE_LEDGER_* all exported 7116 passed, 47 skipped, 2 failed
uv run pytest <the other four shards> -q              all passed (2 engine skips)
uv run ruff check .                                   All checks passed!
uv run python tools/ci_test_groups.py --verify        verification=ok

The polluted-environment run is outcome-identical to the clean one, which
is the guarantee finding 2 asked for. It exports LOGBOOK_URL, LOGBOOK_KEY,
LOGBOOK_API_KEY, LOGBOOK_EXPORT_KEY and all four POPULACE_LEDGER_*
names.

The 2 failures are pre-existing and not this PR's. Both are
test_release_target_parity.py::TestRegeneration, and both fail identically
at this branch's previous head (1f78847c) and at origin/main
(47c74225), verified this session in a throwaway worktree of each:

ValueError: Ledger target reference
'cbo.revenue_projection.ty2024.income_by_source.adjusted_gross_income.projected_amount'
assertion_policy='observed_only' does not allow resolved fact assertion
'source_projection'.

They run only where the pinned feed exists — DEFAULT_FEED_PATH is
~/PolicyEngine/_buildh-runtime/inputs/consumer_facts_buildn_v9_4.jsonl,
outside the repo — and _feed_or_skip skips them everywhere else, so they do
not run in PR CI. Nothing in this PR touches ledger_targets.py,
fiscal_targets.py, tools/build_us_target_parity_manifest.py, or that
test. Recording it because the earlier lane reported this suite as "all
green", which on a machine holding that feed it is not.

uv run ruff format --check . reports 116 files as reformat candidates on
this branch and on its merge base — ruff 0.15.16's formatter against a
repo pinned only to ruff>=0.8, whose CI lint lane runs ruff check and not
ruff format --check. All seven source files this PR touches are clean
before and after. Reformatting the other 116 is not this PR's change to make.

tools/ci_test_groups.py --verify places the new test_logbook_isolation.py
in fast: rest / engine: shared-spec, alongside every other test_logbook_*
file, and reports verification=ok.

No golden was regenerated, nothing renamed on disk, and no publish,
promotion, or release action was taken.

🤖 Generated with Claude Code

MaxGhenis and others added 7 commits September 2, 2026 10:46
chronicle#143 consumer side: microcosm must accept both ledger-era and
chronicle-era Chronicle schema ids and hash domains before Chronicle flips
emit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… dual-read

Chronicle (formerly Ledger) introduces chronicle-era hash domains and
chronicle-named schema ids for newly emitted rows at a declared cutover;
ledger-era ids stay valid history forever (PolicyEngine/chronicle#143).
Microcosm is a consumer, so it must accept both eras before Chronicle flips
emit.

microcosm.build.chronicle_epoch is the single place that knows which identity
belongs to which era. Epoch detection is structural — the namespace segment of
a key's domain — rather than a lookup in a frozen list of domain strings, so a
chronicle-era family whose version number the spec has not yet declared still
resolves, and no validator hard-codes one epoch. Fact keys stay opaque:
Microcosm never recomputes a Chronicle digest, so comparison stays exact string
equality and only the 'is this a Chronicle key?' question goes dual-era.

The consumer-artifact loader now tests manifest schema_version for membership
in both eras instead of equality with one, validates a per-row schema id when a
feed carries one (Chronicle-published rows never have), and records the
observed id, its epoch, and the feed's fact-key epochs in provenance() so a
release manifest witnesses which era actually resolved its targets.

microcosm.build.chronicle_env is the dual-read window for the operational env
names: CHRONICLE_URL/KEY/API_KEY/EXPORT_KEY preferred, POPULACE_LEDGER_* still
honored with one DeprecationWarning per process per name. LEDGER_HMRC_BANDS,
LEDGER_ONS_*_BANDS and LEDGER_US_SOURCE_COVERAGE_CONTRACT_COMMIT only look like
env vars to a grep — they are data constants, so they take chronicle-era
aliases instead.

Nothing renames on disk: emitted ids, diagnostic field names, populace_* key
namespaces, goldens and fixtures stay frozen at v1 (microcosm#639).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Covers the epoch module itself, the loader under each schema id, a
mixed-epoch feed straddling the cutover, mixed-epoch target compilation and
a chronicle-era reference pin, and the two producer surfaces: the minted US
trade artifact declares the ledger-era id but loads under either, and the
congressional-district vintage translation mints byte-identical
microcosm.derived_fact keys whether its source rows are ledger-era or
chronicle-era — otherwise cutover day would silently re-identify every
derived fact a target is pinned to.

Also documents the env dual-read in logbook/README.md and adds the towncrier
fragment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dual acceptance must widen what loads and narrow nothing. The loader as
committed added a per-row schema_version check that rejected any id outside
the two chronicle#143 names — a validation main never performed, on a field
main never read.

Real feeds do not honor that set. The pinned US fiscal-refresh feed
consumer_facts_buildn_v9_4.jsonl, which the release path loads through this
loader, declares 'arch.consumer_fact.v1' on 37,006 of its first 200,000 rows
and 'ledger.consumer_fact.v1' on 399 of them; test_uk_firm_generation builds
the same arch-namespace rows. The check therefore failed the build closed on
its own pinned input, and PR CI could not see it: the feed is gated data
outside PR CI. Verified by loading an arch-namespace row through
load_ledger_consumer_artifact — ValueError before, loads after.

The id is now carried and reported through fact_schema_versions, which
provenance records beside the artifact schema id and the fact-key epochs, so
a release manifest still witnesses exactly which producers wrote its rows.

Epoch witnessing was also reading only the four key fields targets resolve a
fact by. Published rows carry nine, plus two key lists: the captured UK feed
fixture has observed_measure_key, source_release_key, source_series_key,
universe_constraint_set_key and lineage.source_row_keys as well. Chronicle
moves families independently — the spec declares v3 only for the aggregate
and semantic families — so a straddling row with a ledger-era aggregate key
and a chronicle-era source-release key was witnessed as pure ledger-era.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…scribe

The map documented six families and said it recorded what this repo's feeds
carry. The captured UK feed fixture carries ten, and the pinned US feed
carries eleven: observed_measure, source_release, source_series,
universe_constraint_set and source_row were all missing. Nothing consults the
map at runtime, which is precisely how such a list rots, so a test now pins it
to the fixture.

The completed map also shows why detection cannot be a lookup in it: the
observed version numbers differ by family, with fact and source_cell at v1
where the rest are at v2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Review (Fable, main): the epoch half is right and lands as specified — structural epoch detection, membership over both manifest ids, per-row ids carried not gated, Microcosm-minted namespaces outside both eras, provenance witnessing the observed era, no on-disk renames. Approving that half as is.

One correction before merge, and it is my brief's fault, not the implementation's: POPULACE_LEDGER_URL / _KEY / _API_KEY / _EXPORT_KEY are not Chronicle variables. They are the Logbook store's credentials (Supabase project populace, logbook_writer / logbook_exporter roles, the logbook schema — see logbook.py's own docstring and logbook/README.md). "Ledger" there is the generic build-ledger sense that was renamed to Logbook on 8/8 precisely so it would not collide with Chronicle, the fact store. Renaming them to CHRONICLE_* would re-create that collision in the one place that had already been disambiguated.

Please rename the preferred spellings to LOGBOOK_URL, LOGBOOK_KEY, LOGBOOK_API_KEY, LOGBOOK_EXPORT_KEY (module logbook_env.py, function logbook_env, test file to match), keep the POPULACE_LEDGER_* legacy names honored with the same once-per-process warning, and drop the chronicle#143 framing from the env half (it is a Logbook naming cleanup that rides along). The CHRONICLE_*_BANDS and CHRONICLE_US_SOURCE_COVERAGE_CONTRACT_COMMIT aliases are fine: those really do translate Chronicle ids and pin a Chronicle commit. A follow-up lane is applying this on the same branch.

MaxGhenis and others added 7 commits September 2, 2026 11:47
POPULACE_LEDGER_URL/_KEY/_API_KEY/_EXPORT_KEY are Logbook store
credentials (microcosm#632), not Chronicle fact-store ones. Naming the
preferred spellings CHRONICLE_* would recreate the exact collision
microcosm#632 renamed Ledger away from. Per PR #849 review: rename
chronicle_env.py to logbook_env.py, its symbols to LOGBOOK_*/logbook_env*,
and every caller (logbook.py, tools/logbook.py, build/__init__.py exports,
and the two module-path comments in firm_generation.py/source_coverage.py
that named the old module). Legacy POPULACE_LEDGER_* names and the
once-per-process DeprecationWarning behavior are unchanged; the warning
text now cites microcosm#632 instead of chronicle#143.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The changelog fragment, logbook/README.md, and the branch journal all
described CHRONICLE_URL/CHRONICLE_KEY and the env dual-read window as
part of the chronicle#143 epoch migration. It is a separate Logbook
naming cleanup (microcosm#632) riding along on this branch: split the
changelog sentence into its own paragraph, update the README's live-store
section to LOGBOOK_URL/LOGBOOK_KEY, and append a dated correction note to
the journal recording why and what changed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
logbook_env's import in build/__init__.py sorted alphabetically after the
rename, and tools/logbook.py's two dual-read imports needed reordering
and reformatting to match.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-*.md only

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The findings turn on which schema ids Chronicle actually emits, so the
identities are recorded here with the file and line they were read from on
chronicle origin/main rather than restated from the PR body.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MaxGhenis and others added 9 commits September 2, 2026 13:07
…istry

The accepted set was {policyengine_ledger.consumer_artifact.v1,
policyengine_chronicle.consumer_artifact.v2}, which is one generation off at
both ends. Chronicle's main emits policyengine_ledger.consumer_artifact.v2
today (policyengine_chronicle/consumer.py:30, README line 299), so the loader
rejected every artifact Chronicle currently publishes, and the chronicle-era
successor of that id is .v3, not .v2. The v1 spelling is Microcosm's own
minted artifact and stays accepted as the history it is.

Epoch now comes from a declaration keyed by (namespace, family, version)
rather than from the namespace segment of the key. Reading the namespace told
us where a key claimed to come from, not that Chronicle issued it, so
chronicle.source_cell.v3 and chronicle.anything.vN were being witnessed as
Chronicle-issued identity. The ledger-era half of the table is transcribed
from Chronicle's own hash-domain constants; the chronicle-era half is each
one's successor under the rename rule, which puts source_cell at v2 where
aggregate_fact is at v3. An undeclared spelling in a Chronicle namespace is
reported as 'undeclared' in provenance, with the domain named, instead of
being folded into an era nobody declared it for.

The membership predicates are total over JSON. A manifest whose schema_version
is a list or an object raised TypeError out of a frozenset membership test,
bypassing the loader's own unsupported-schema message.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The per-module delenv lists named POPULACE_LEDGER_* only, because they were
written before the LOGBOOK_* spelling existed. A shell exporting the
preferred names — a developer who just ran an export, a runner where the
variables are set for a later job — walked straight past them:
logbook._remote_config() returns ('https://example.invalid', 'x', 'x') under
LOGBOOK_URL/LOGBOOK_KEY alone, and the append path would then POST to it.
The store is append-only, so a row that lands cannot be retracted.

An autouse conftest fixture now clears both generations of every variable in
the dual-read window, taken from the window itself so a variable added there
cannot leave a hole here, plus POPULACE_LOGBOOK_PREV_ROW_DIGEST. The second,
independent guard replaces the Logbook urlopen with one that raises —
on every module currently holding it, since tools/logbook.py binds the name
at import, and on the opener it delegates to, which closes the path for any
importer this does not find. allow_logbook_network restores the real
callables for a test that means it; nothing in the suite asks for it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t alias

microcosm.build re-exported the reader function logbook_env under the same
name as its module. 'import a.b as c' returns the attribute when the package
has one, so 'import microcosm.build.logbook_env as env' handed back the
function and every env.LOGBOOK_URL_ENV after it raised AttributeError. The
barrel now re-exports only logbook_env_names, which collides with nothing;
the function is imported from its own module, as logbook.py and
tools/logbook.py already do.

CHRONICLE_US_SOURCE_COVERAGE_CONTRACT_COMMIT was added beside the ledger-era
pin but never re-exported, so it was importable from source_coverage and not
from the us_runtime barrel its sibling reaches — an alias only half the
callers could use.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
_ledger_provenance rebuilt the block from the artifact field by field, which
is how it came to record facts_sha256, fact_row_count and manifest_sha256 but
not schema_epoch, fact_key_epochs or fact_schema_versions. The loader had
already worked out which era resolved the targets; the UK run reported only
the hashes, so a signed diagnostics file could not say whether a
cutover-window feed had straddled the rename.

It now delegates to LedgerConsumerArtifact.provenance() and keeps only the
manifest sub-block UK-shaped, because that one feeds the run's identity
digest and is deliberately narrow. A stand-in without the shared method still
records what it carries rather than fabricating the rest.

Coverage is a real mixed-epoch artifact through run_uk_calibration: a
Chronicle-main manifest id over ledger-era and chronicle-era rows plus one
undeclared Chronicle-namespace key, asserted in the build record and in the
diagnostics 'build' block. The delegation itself is pinned as a property —
every shared field reaches the UK block — rather than as a list that would
go stale the next time the shared block grows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
'logbook export --remote --help' named POPULACE_LEDGER_URL alone, so an
operator configuring from the help text set up the exact spelling the
dual-read window exists to retire, then got a deprecation warning for
following the instructions. The preferred LOGBOOK_* names come first and the
ledger-era ones are named as the fallback they are, since an operator whose
environment predates the rename still has to recognise what is being asked
for.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The lane journal is history once written, but four of its claims read as
current design and are now wrong: the two-id accepted set, the uniform
v2-to-v3 domain bump, CHRONICLE_* env names, and structural epoch detection.
Each is annotated in place with the correction and its date so the file
cannot mislead a later reader, and the changelog fragments now describe the
behavior that is actually on the branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both said chronicle#143 declares the v3 spelling for exactly two fact
families, which the issue does not: it lists 'dual-hash window or v3 domains'
as open options. The import_entry_facts header also named the chronicle-era
artifact id as .v2 and did not say which of the ledger-era ids this module
emits, which is the distinction the whole finding turned on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both microcosm-build suites are outcome-identical, which is the guarantee the
Logbook isolation finding asked for. The two red tests are recorded with the
evidence that they reproduce at origin/main, because the earlier lane
reported this suite as all green and on a machine holding the pinned feed it
is not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant