Skip to content

Operational rename, slice 1: CHRONICLE_* env names, chronicle.db output, configurable buckets, cutover checklist (#143, mechanism 3) - #226

Open
MaxGhenis wants to merge 22 commits into
mainfrom
ops-rename-slice1
Open

Operational rename, slice 1: CHRONICLE_* env names, chronicle.db output, configurable buckets, cutover checklist (#143, mechanism 3)#226
MaxGhenis wants to merge 22 commits into
mainfrom
ops-rename-slice1

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

First slice of #143 mechanism 3 (operational stores migrate by dual-run). Code and docs only; no infrastructure changes and no defaults flipped.

  • chronicle/env.py — one shared reader for the rename window: CHRONICLE_<X> is read first, then POLICYENGINE_LEDGER_<X>, then LEDGER_<X>, with a once-per-process FutureWarning (so CLI operators actually see it) naming the preferred variable. Replaces the three ad-hoc _env helpers in db/supabase_client.py, chronicle/source_package.py, and db/pe_source_inventory.py. Names outside the window are read literally.
  • R2 bucket names are configurable (CHRONICLE_R2_RAW_BUCKET, CHRONICLE_R2_DERIVED_BUCKET, plumbed through fetch-artifact, publish-raw, publish-derived, bootstrap-r2 and their --r2-bucket defaults). Defaults stay ledger-raw / ledger-derived; the flip is a follow-up PR once consumers repoint.
  • chronicle.db for new suite outputs; ledger.db still read and inferred.
  • Docs: docs/storage-architecture.md gains an "Environment Variable Rename Window" section (the previous sentence had the fallback direction backwards) and a "Bucket Cutover" checklist; README and the harness doc swept.

Two defects found and fixed on the way, each with a regression test that passes on main's behavior and fails on the bug:

  • db/cli.py raised ImportError on every db CLI subcommand (a private helper import that pytest never exercised).
  • The consumer-fact boundary guard matched the hard-coded ledger-derived: / r2://ledger-derived/ strings, so the bucket rename would have made it stop firing silently. It now matches on shape (*-derived bucket or derived/ key prefix).

Out of scope, deliberately: the ledger CLI alias, Supabase schema and mirror table names, governance role ids and concept authorities (hashed → mechanism 1), hash domains and schema ids (the epoch PR).

Note for the cutover checklist: this branch's count of manifest-declared raw objects differs from the one on #143 (the checklist tells the operator to recount rather than trust a number). The backfill performed today used the tracked manifests on main: 157 distinct keys, all now present in chronicle-raw (#225 has the integrity findings).

Review fixes

Gate round 1

Both findings are applied on this branch.

[high] fetch-artifact could attach a recorded R2 URI to new bytes. The preserve rule keyed on the bucket, so a repeated fetch that did not re-upload into the same bucket kept the recorded storage.r2 block while rewriting the entry's sha256/size_bytes. Reproduced against this branch's parent by serving two different bodies from one URL: the entry ends up declaring the fetched bytes' sha256 under a key addressed by the superseded bytes' one, both when the fetch only registers the bytes and when the bucket default has moved — the shape #225 hit when the IRS re-published the 2022 IRA tables.

The rule now keys on identity: the recorded key's {sha256}/{filename} tail against the fetched bytes.

  • Identical — the recorded block is preserved exactly, whichever bucket is configured now.
  • DifferentSourceArtifactRevisionError, raised before the cached artifact or its manifest entry is touched and before anything is uploaded, naming recorded and fetched sha256/size_bytes and the ADR rule that same vintage plus new bytes is a new release_revision.
  • --record-revision opts in: the fetched bytes get their own content-addressed key under the configured bucket — never the old key — and the superseded block moves to storage.previous_r2 with its sha256, size_bytes and fetched_at, so the earlier bytes stay addressable at the URI archived witness records pin.
  • publish-raw applies the same check before treating a recorded block as history: a local file the recorded object does not hold is refused as recorded_r2_identity_mismatch with nothing uploaded.

storage.previous_r2 is a sibling key rather than a new shape for storage.r2, because every reader — inventory-artifacts, publish-raw, source_package._artifact_content, the suite's raw-R2-link acceptance check — reads storage.r2 alone, and publish-raw already spreads the rest of the storage block when it writes back, so a revision survives publication untouched. All 180 tracked manifest entries carrying a storage.r2 block are content-addressed and agree with their declared sha256 and filename, so the check never fires on tracked data.

[low] Env isolation was scoped to one module. The autouse fixture moves to tests/conftest.py and clears all three prefixes for every test. db.supabase_client resolves LEDGER_SCHEMA at import — during collection, before any fixture runs — so tests/test_chronicle_namespace.py re-imports it under the cleared environment instead of asserting the constant it bound at collection time.

New tests in tests/test_chronicle_artifacts.py (fake downloader, no network): a repeated fetch of identical bytes preserves the block; a repeated fetch of different bytes is refused on all three routes (re-uploaded, registered without an upload, after the bucket rename) with the cache, manifest and upload log untouched; --record-revision records the new key under the configured bucket and retains the previous object; a revised manifest still inventories as one R2-linked artifact; publish-raw refuses a mismatched local file and publishes a registered revision; and the CLI exits 1 with the message, then 0 with the flag. tests/test_chronicle_env.py asserts no rename-window variable reaches a test.

Verification on the fix head: uv run pytest -q green; CHRONICLE_R2_RAW_BUCKET=zzz CHRONICLE_SCHEMA=zzz uv run pytest -q green (five tests failed this way before the shared fixture); uv run ruff check . clean.

Gate round 2

Seven findings, all applied on this branch. Each has a regression test, and each was first reproduced against the round-1 head (34d1d0f) — the pre-fix behavior quoted under each item is that run's output, not an inference.

[high] CHRONICLE_SCHEMA did not configure the primary Supabase mirror writer. chronicle/mirror.py, its harness wrapper and the --schema CLI default all defaulted to the literal ledger, so setting the variable to rehearse a cutover moved the read-side client and left the writer pointed at production. chronicle/env.py now owns default_chronicle_schema() — one home for the CHRONICLE_SCHEMAPOLICYENGINE_LEDGER_SCHEMALEDGER_SCHEMA"ledger" ladder — and the loader resolves through it whenever no schema is supplied. An explicit --schema still wins, and the default is unchanged. Pre-fix: with CHRONICLE_SCHEMA=chronicle_probe, load_supabase_mirror still reports schema='ledger'.

[high] The derived-fact boundary was not rename-safe. chronicle/consumer_contract.py matched the .ledger_derived suffix literally. It now matches the whole final dot-segment against both ledger_derived and chronicle_derived, so a producer that renames its derived rows cannot walk a downstream target fact through the guard. Pre-fix: '.chronicle_derived'.endswith('.ledger_derived') is False — the guard never fired.

[high] fetch-artifact could not address a package's non-default manifest. Seven tracked packages keep a manifest_*_source_package.yaml, and three publisher directories keep two of them: db/data/irs_soi/ira_contributions/ holds the traditional manifest beside the Roth one. A fetch that always wrote manifest.yaml would write a third manifest neither package reads, so the IRA revision workflow the docs cite would never see the recorded block. --manifest <filename> selects it (default manifest.yaml); the name must be a filename inside --out-dir, not a path. Pre-fix: fetch_source_artifact() rejects manifest_filename as an unexpected keyword, and a fetch into that directory writes manifest.yaml.

[high] Revision protection vanished when the entry had no storage.r2. A fetch that only registered bytes, or one whose upload failed — the state #225 landed in — left an entry with a declared sha256 and no recorded block, and the guard skipped it entirely. A manifest entry identifies its bytes from the moment it is registered, so the comparison is now against the entry's recorded identity: the recorded key's {sha256}/{filename} once published, the declared sha256 before that. An ordinary fetch of different bytes over either is refused unless --record-revision opts in; a revision over a never-published entry supersedes nothing and gets no empty previous_r2. Pre-fix: re-fetching different bytes over a registered entry rewrote its sha256 with no refusal.

[medium] Recorded-R2 validation read the key or the uri, whichever came first. _validated_recorded_r2 now cross-checks every supplied locator field against every other — key against the URI's path, bucket against its authority, provider against its scheme — and the resulting key against the content-addressed {sha256}/{filename} shape. A contradiction raises RecordedR2LocatorError at fetch time and refuses as recorded_r2_locator_invalid at publish time, with nothing uploaded. Pre-fix: a block whose key and uri named different objects was preserved verbatim — key sha c63744a4… beside uri sha 1e9b3fdb… — so the entry kept publishing a URI for bytes it no longer described.

[medium] A malformed manifest read as an absent one. _read_manifest now refuses a document that parses as anything but a mapping, and an unparseable one, with MalformedManifestError — raised before the publisher is read at all, so a refusal costs no fetch. inventory-artifacts and publish-raw report it as a manifest error instead of crashing on .get. An absent or empty document still reads as an empty mapping. Pre-fix: a list-valued manifest.yaml was overwritten by the fetch.

[low] Schema resolution happened at import, during collection. db.supabase_client resolved both schemas into module constants when it was first imported — at collection, before any fixture — so a legacy variable could warn and seed state that no fixture could take back. Both are now functions (chronicle_schema(), targets_schema()), and tests/conftest.py additionally strips the rename window in pytest_configure, before collection imports anything. Pre-fix: importing under LEDGER_SCHEMA=zzz bound LEDGER_SCHEMA='zzz' and emitted a FutureWarning.

All four refusals share a SourceArtifactManifestError base, so fetch-artifact reports every one as exit 1 with nothing written.

New round-2 tests: tests/test_chronicle_mirror.py (the loader and its CLI write to the configured schema, under each name in the window, with an explicit --schema still winning); tests/test_chronicle_artifacts.py (a two-manifest package addressed by name and its revision refused in the right manifest, on the direct and CLI paths; a manifest name that tries to leave the package; revision protection over a registered entry and over a failed upload; every locator contradiction, at fetch and at publish time; malformed manifests refused before the fetch and reported by both sweeps); tests/test_chronicle_consumer_contract.py (the chronicle spelling rejected identically, and the marker matched as a whole segment); tests/test_chronicle_namespace.py and tests/test_chronicle_env.py (schema read per call, no constant frozen at import).

Verification on the round-2 head (ea67f0a): uv run pytest -q — 839 passed, 1 skipped; LEDGER_SCHEMA=zzz uv run pytest -q -W error::FutureWarning tests/test_chronicle_namespace.py — 6 passed; CHRONICLE_R2_RAW_BUCKET=zzz CHRONICLE_SCHEMA=zzz uv run pytest -q — 839 passed, 1 skipped; uv run ruff check . clean; uv run ruff format --check . clean for every file this branch touches (the 13 unformatted files are pre-existing on main and none are touched here).

Scope is unchanged: no boundary change, nothing under releases/ or db/data manifests, defaults still ledger-raw / ledger-derived / ledger, and the shared functions #227 will rebase onto keep their signatures with the new arguments defaulted.

Chronicle Governance

This PR does not touch the source-data boundary: no source packages, facts, profiles, or schemas change. Storage configuration, CLI defaults, and docs only.

Tests

tests/test_chronicle_env.py (new, hermetic: chronicle-first precedence, legacy fallback with one warning per process, out-of-window names read literally), tests/test_chronicle_artifacts.py (bucket configuration, chronicle.db output with ledger.db read-back, the db/cli.py regression), tests/test_chronicle_consumer_contract.py (boundary guard by shape). CI runs the full suite on this PR; the lane that authored the branch ran out of budget before pasting its local run, so treat CI here as the verification of record.

🤖 Generated with Claude Code

MaxGhenis and others added 5 commits September 2, 2026 11:00
Chronicle's operational stores migrate by dual-run (chronicle#143, mechanism
3). Every env read was ledger-first with no chronicle-named alternative, and
each of the three modules that read configuration had grown its own helper.

Add chronicle/env.py: env_value/env_flag expand a name into CHRONICLE_<X>,
LEDGER_<X>, POLICYENGINE_LEDGER_<X> and return the first set value, warning
once per process with ChronicleEnvDeprecationWarning when a ledger-era name
supplied it. Names outside those three prefixes, such as
POLICYENGINE_SUPABASE_URL, are read literally so the helper renames the
ledger-era surface only.

The warning subclasses FutureWarning, not DeprecationWarning, so operators
running the CLI actually see it.

The Supabase schema name stays "ledger"; only the env var that overrides it
moves to CHRONICLE_SCHEMA. Renaming the schema is a later slice.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bucket names: add CHRONICLE_R2_RAW_BUCKET / CHRONICLE_R2_DERIVED_BUCKET,
resolved in the function body rather than bound as keyword defaults so the
setting reaches long-lived processes. Defaults are unchanged at ledger-raw and
ledger-derived; only the follow-up cutover PR flips them. Plumbed through
fetch-artifact, publish-raw, publish-derived and bootstrap-r2, whose
--r2-bucket/--raw-bucket/--derived-bucket now default to the resolver.

Because the bucket can now vary, two manifest write paths could restate
recorded storage.r2 blocks. Archived witness records pin raw R2 URLs by hash,
so both now preserve history: publish-raw reports
recorded_r2_bucket_is_preserved_history instead of uploading, and
fetch-artifact keeps an already-recorded r2 block rather than overwriting it
with a different bucket.

The consumer-fact boundary guard matched the literal strings ledger-derived:
and r2://ledger-derived/, so a renamed bucket would have made it silently stop
firing. It now matches on shape (bucket ends in -derived, or key starts with
derived/) and parses the r2:// URI, which also closes the uri-only hole where
a fact carrying no bucket or key slipped past.

Database artifact: new suite outputs write chronicle.db. infer_build_id reads
chronicle.db then ledger.db (both branches were byte-identical, so the
existing fallback was a no-op), and _derived_artifact_kind classifies both
names (its set literal held one element twice). The sidecar resource list moves
with the write site because _resource_descriptor stats every listed path.

Fix db/cli.py, which imported the private env reader that the shared helper
replaced. That ImportError broke every db CLI subcommand, including the three
the CI job runs, while pytest stayed green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tests/test_chronicle_env.py covers the shared helper end to end: the lookup
ladder, CHRONICLE_* winning over both ledger-era spellings without a warning,
each legacy spelling still working with a once-per-process warning attributed to
the caller, empty values counting as unset, and a migrated operator being able to
turn a flag off without unsetting the stale legacy name. It then exercises the
real call sites -- source-package artifact cache and fetch flag, the db CLI's
--pe-us-root default, the Supabase schema override, and R2 bucket resolution --
so a helper regression cannot pass by only testing the helper. An autouse fixture
strips every rename-window variable from the ambient environment, so the file is
hermetic under any shell.

The artifacts tests cover the chronicle.db write and ledger.db read fallback,
both database names classifying as sqlite_database, publish-derived following the
configured bucket, and the two manifest-preservation paths: publish-raw refusing
to restate a recorded bucket and fetch-artifact keeping the recorded r2 block
while still uploading the backfill copy.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs/storage-architecture.md stated the fallback direction backwards: it claimed
the CHRONICLE_-prefixed variables were the old names kept as migration
fallbacks, when they are the names to migrate to. Replaced with an "Environment
Variable Rename Window" section that gives the actual lookup order, the
once-per-process warning, and a table of all seven variables. Two behaviors get
stated outright because both invert a naive fallback: the CHRONICLE_ name wins
even when it reads false, so a migrated operator can turn a flag off without
hunting down a stale legacy export; and an empty value counts as unset. The
section also records what is deliberately NOT aliased -- POLICYENGINE_SUPABASE_*
and POLICYENGINE_TARGETS_SCHEMA carry no ledger prefix and are read literally --
and that CHRONICLE_SCHEMA renames the override variable, not the schema value,
which moves in a later slice.

Added a "Bucket Cutover" section covering the six steps: create the buckets on
the account wrangler.toml pins, enumerate, backfill-copy, verify, flip the
defaults in a follow-up, and leave the ledger-era buckets read-only forever.
The enumeration and verification are given as commands rather than prose. Every
raw key ends {sha256}/{filename}, so the key is its own checksum witness and
verification needs no manifest lookup. Counted rather than assumed: 186 distinct
ledger-raw objects across 154 tracked manifest files, and the doc says to
recount rather than trust that number, since source packages land continuously.

The derived bucket needs no backfill -- derived artifacts are reproducible and
already keyed by {build_id}, so a rebuild republishes them wherever configured.

README and the harness doc follow: bucket references become archive roles with
the default named, ledger.db becomes chronicle.db with the legacy name noted as
still readable, and bootstrap-r2's example drops the hardcoded bucket flags now
that they default to the resolver.

Left alone: LEDGER_EXPLORER_DATA_DIRS in the README. It configures the explorer
app, which is not in this repository, so renaming it here would document a name
no shipped code honors -- the same defect this commit fixes in the other
direction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The consumer-fact boundary guard now matches derived artifacts on shape, but
nothing tested the cases that motivated the change. Both of these pass every
check the old literal matching applied, so they would have slipped through
silently once the buckets are renamed:

- a fact carrying no bucket and no key, only a
  `r2://chronicle-derived/derived/source/fact.json` URI. The old code compared
  the URI against two hardcoded prefixes, `r2://ledger-derived/` and
  `r2://ledger-raw/derived/`, and matched neither.
- a `source_file` prefixed `chronicle-derived:`. The old code matched the
  literal prefix `ledger-derived:`.

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

Copy link
Copy Markdown
Contributor Author

Fable+Sol gate: changes requested, and the high finding is exactly the failure mode today's backfill hit in the wild (IRS re-published the 2022 IRA tables with different bytes under the same URL, #225):

  1. [high] fetch-artifact can attach the recorded R2 URI to new bytes. When the recorded storage.r2 block is preserved on a repeated fetch, the manifest is rewritten with the newly fetched sha256 and size_bytes but keeps the old content-addressed key and URI. A publisher revision then produces a manifest whose provenance is false. Fix: compare the recorded identity (filename + sha256) with the fetched bytes before preserving anything; identical → preserve the recorded block; different → refuse, or record a new content-addressed key under the configured bucket as a new revision, never the old key. The regression test must repeat with different bytes, not identical ones.
  2. [low] Env isolation is scoped to one test module. Other tests assert default buckets/schemas without clearing CHRONICLE_R2_RAW_BUCKET / CHRONICLE_SCHEMA; move isolation to a shared fixture or clear explicitly where defaults are asserted.

A fix lane is applying both on this branch; re-gate after.

MaxGhenis and others added 7 commits September 2, 2026 12:29
The env-isolation fixture lived in tests/test_chronicle_env.py, so only that
module ran with CHRONICLE_/POLICYENGINE_LEDGER_/LEDGER_ variables stripped.
Tests elsewhere assert the defaults those variables override — the raw and
derived bucket names, the Supabase schema — and failed when an operator's
shell had them set:

    CHRONICLE_R2_RAW_BUCKET=zzz CHRONICLE_SCHEMA=zzz uv run pytest -q
    FAILED tests/test_chronicle_artifacts.py::test_publish_source_artifacts_uploads_manifest_entries

The fixture moves to tests/conftest.py as a suite-wide autouse fixture, and
tests/test_chronicle_env.py gains an assertion that no rename-window variable
reaches a test.

db.supabase_client resolves LEDGER_SCHEMA and TARGETS_SCHEMA at import, which
happens during collection — before any fixture runs — so the namespace test
re-imports the module under the cleared environment instead of asserting on the
constant it bound at collection time.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A raw R2 key is content-addressed, so a recorded storage.r2 block is a claim
about specific bytes. fetch-artifact preserved that block whenever one existed
and the fetch did not re-upload into the same bucket, then rewrote the entry's
sha256 and size_bytes from the newly fetched bytes. When a publisher
re-publishes under the same URL and vintage — the IRS did exactly this to the
2022 IRA tables today (#225) — the manifest ended up
describing the new bytes while pointing at the old object's key and URI.

Verified against this branch's parent: a second fetch of different bytes leaves
sha256=109dcf49… in an entry whose storage.r2 key is addressed by c63744a4…,
both when the fetch only registers the bytes and when the bucket default has
moved.

Identity now decides, not the bucket:

- the recorded key's last two segments (sha256, filename) are compared with the
  fetched bytes. Identical: the recorded block is preserved exactly, whichever
  bucket is configured now. Different: SourceArtifactRevisionError, raised
  before the cached artifact or its manifest entry is touched, naming recorded
  and fetched sha256/size_bytes and the ADR rule that the same vintage with new
  bytes is a new release revision.
- --record-revision opts in: the fetched bytes get their own content-addressed
  key under the configured bucket, never the old key, and the superseded block
  moves to storage.previous_r2 with its sha256, size_bytes and fetched_at so
  the earlier bytes stay addressable.
- publish-raw applies the same check before treating a recorded block as
  history, so a local file the recorded object does not hold is refused rather
  than uploaded.

storage.previous_r2 is a sibling of storage.r2: every reader
(inventory-artifacts, publish-raw, source_package._artifact_content and the
suite's raw-R2-link acceptance check) reads storage.r2 alone, and publish-raw
already spreads the existing storage block when it rewrites, so a revision
survives publication untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The rename-window text said a recorded storage.r2 block is preserved because
the bucket may have moved. That is the weaker half of the rule: the block is
preserved because it addresses the bytes in hand. Adds a Publisher Revisions
section covering the identity check, the refusal, --record-revision, the
storage.previous_r2 shape and why every existing storage.r2 reader is
unaffected, and points the bucket-cutover, publish-flow and harness passages at
it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A storage.r2 block written by Chronicle always carries both key and uri, but a
hand-written one need not. The identity read now falls back to the uri, whose
last two segments are the same {sha256}/{filename}, so a block that records
only where the object lives is still recognized as history for the bytes it
holds instead of being refused as a revision.

Also covers registering a revision without an upload: the entry is left with no
storage.r2 at all rather than a pointer to bytes R2 does not hold, the
superseded object stays in storage.previous_r2, and publish-raw completes the
registration.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dict equality would pass on a block that was rebuilt with the same values in a
different order, which is not what "preserved" means for a manifest that gets
dumped back to YAML.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The hashes named the fix lane's two-body fixture, not IRS bytes.

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

Copy link
Copy Markdown
Contributor Author

Gate round 2: changes requested again, all agreed and being fixed on this branch (round-2 lane): CHRONICLE_SCHEMA did not reach the primary Supabase mirror writer (harness/mirror still default to the literal ledger schema); the .ledger_derived suffix check is not rename-safe; fetch-artifact cannot address a package's non-default manifest file (--manifest added), so the IRA revision workflow the docs cite would miss the recorded block; revision protection vanished when an entry lacked storage.r2; recorded-R2 locator fields must be cross-checked, not key-or-URI; malformed manifests must be rejected rather than treated as absent; collection-time schema resolution must be lazy. Re-gate follows.

MaxGhenis and others added 10 commits September 2, 2026 13:09
The seven findings, in the order the fixes depend on each other, plus what a
scan of every tracked manifest says about how strict the locator check can be:
187 entries, all content-addressed, no contradictory field.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The consumer boundary matched the `.ledger_derived` suffix literally, so a
producer that renamed its derived rows with everything else would have walked
a downstream target fact straight through the guard. Match the whole final
dot-segment against both spellings instead.

Without the fix the two new cases fail: the chronicle-spelled record id
validates clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The hosted mirror loader is the primary writer into the schema, and both it
and its CLI defaulted to the literal "ledger": setting CHRONICLE_SCHEMA to
rehearse a cutover moved the read-side client and left the writer pointed at
production. The default now resolves through the shared helper -- one home for
the CHRONICLE_SCHEMA -> POLICYENGINE_LEDGER_SCHEMA -> LEDGER_SCHEMA -> "ledger"
ladder -- whenever no explicit --schema is supplied. Defaults are unchanged.

Resolution is a function rather than a module constant, so db.supabase_client
no longer binds the schema while being imported. That import happens at
collection, before any fixture, which is why the namespace test needed a
reload dance to assert the defaults; it now just calls the resolver. A
pytest_configure hook clears the rename window before collection too, so no
module can read (or warn from) an operator's shell on the way in.

Six tests fail against the previous code: the loader and its CLI ignore the
variable, and the namespace assertions see the collection-time constant.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Four defects in the state fetch-artifact reads before it writes:

- It always targeted manifest.yaml. Seven tracked packages keep a
  manifest_*_source_package.yaml instead, and three publisher directories keep
  two of them, so the IRA revision workflow the docs cite would have written a
  third manifest beside the real ones and never seen the recorded block. A
  --manifest filename now selects it; the name has to stay inside the package.

- Revision protection vanished when the entry had no storage.r2 -- a fetch that
  only registered bytes, or one whose upload failed, which is the state #225
  landed in. A manifest entry identifies its bytes by its declared sha256
  whether or not it has been published, and a fetch of different bytes over
  either identity is refused unless --record-revision opts in.

- Recorded R2 validation read the key or the uri, whichever came first. A block
  whose key and uri named different objects was preserved verbatim, so the
  entry kept publishing a URI for bytes it no longer described. Every supplied
  locator field is now cross-checked against every other and against the
  content-addressed key shape; a contradiction is an error at fetch time and a
  refusal at publish time, never a silent preserve.

- A manifest that parsed as anything but a mapping was treated as absent, so
  the fetch would replace it with a single entry. It is now refused before the
  publisher is read at all, and inventory-artifacts and publish-raw report it
  rather than crashing on it.

The refusals share a SourceArtifactManifestError base, so the CLI reports all
of them as an exit-1 message with nothing written.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
What a manifest entry identifies (declared sha256 from registration, the
content-addressed key once published), which manifest a fetch addresses in a
package that keeps more than one, what a recorded storage.r2 block has to say
for itself, and that load-supabase-mirror takes its schema default from
CHRONICLE_SCHEMA like every other reader.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
_validated_recorded_storage sits next to _validated_recorded_r2 and above its
only caller, instead of a _recorded_storage_block that read like a variant of
the lenient accessor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The message named key and bucket while the check also requires provider, so a
hand-written block missing it read as a contradiction between the error and
the rule.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The flag's help still described the recorded R2 object alone, which is no
longer the only identity a fetch is refused against.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
They run before the publisher is read, and again before the manifest is
rewritten; the base class claimed the second one also predated the cache write.

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