Skip to content

feat: stop writing the legacy manifest, add a one-shot migration - #59

Merged
mspinola merged 1 commit into
mainfrom
feat/drop-legacy-manifest
Jul 26, 2026
Merged

mspinola merged 1 commit into
mainfrom
feat/drop-legacy-manifest

Conversation

@mspinola

Copy link
Copy Markdown
Owner

Completes the second half of ADR-0007 step 1.

Why now rather than after a clean week

The legacy manifest.json held both producer halves in one file, which was unsafe two ways:

  • The update is a read-modify-write, so two producers lose each other's entries.
  • A file-level sync between two stores resolves it last-writer-wins and silently discards one side.

The second stopped being theoretical when the Windows producer moved from a \\tsclient\ redirected drive to a local store. That turned one shared store into two copies merged by a sync, and a sync pushing a manifest.json containing only price entries would drop every COT entry on arrival.

The per-half files are disjoint, so a file sync merges them correctly by construction.

What changed

Nothing writes manifest.json. _touch_manifest writes only the owning half.

cotdata-update --migrate-manifests splits an existing aggregate into the per-half files. Idempotent, entries already in a half file win so a re-run cannot resurrect stale bookkeeping, and it never touches data.

Until a store is migrated, a domain absent from the per-half files is still read from the aggregate, with a warning naming the domains and the command to run.

reconcile_manifest rewritten to prune each manifest file in place rather than the merged view. It previously wrote its result to the legacy aggregate, which would now be a write to a file nothing reads. It still prunes the aggregate when present, so an un-migrated store can be cleaned without migrating first.

A bug caught by dry-running against the real store

My first version made the fallback per half. Dry-run on a copy of the live store:

BEFORE: {'prices': 94, 'cot_disagg': 27, 'cot_legacy': 95, 'cot_tff': 24}
                                                          ^ metadata MISSING

manifests/prices.json held prices but not metadata, because the price producer had run on the new code while the metadata producer had not. A half file existing does not mean the half is complete, so treating it as migrated hid metadata entirely until the migration ran.

The fallback is now per domain. Same store, after the fix:

BEFORE: {'prices': 94, 'cot_disagg': 27, 'cot_legacy': 95, 'cot_tff': 24, 'metadata': 1}
AFTER : {'prices': 94, 'cot_disagg': 27, 'cot_legacy': 95, 'cot_tff': 24, 'metadata': 1}

241 entries either side. There is a regression test for it.

Verification

Migration dry-run on a copy of the live store moved 147 entries (cot +146, prices +1) with nothing lost. Suite 134 passed, ruff clean.

Operator note

Run once per store after upgrading:

cotdata-update --migrate-manifests

Two stores means running it on both. manifest.json can be deleted once every consumer of that store is on this version.

The legacy manifest.json held both producer halves in ONE file. That was unsafe
two ways: the update is a read-modify-write, so two producers lose each other's
entries, and a file-level sync between two stores resolves the file
last-writer-wins and silently discards one side. The second one stopped being
theoretical when the Windows producer moved from a redirected drive to a local
store, making the two stores separate copies merged by a sync.

Nothing writes manifest.json now. `cotdata-update --migrate-manifests` splits an
existing one into the per-half files. It is idempotent, entries already in a half
file win so a re-run cannot resurrect stale bookkeeping, and it never touches
data.

Until a store is migrated, a domain absent from the per-half files is still read
from the aggregate, with a warning naming the domains and the command to run.

THE FALLBACK IS PER DOMAIN, NOT PER HALF. Found by dry-running the migration
against a copy of the real store: manifests/prices.json held `prices` but not
`metadata`, because the price producer had run on the new code while the metadata
producer had not. A per-half rule treated the whole prices half as migrated and
hid `metadata` entirely. Verified before and after migration now show the same
241 entries.

reconcile_manifest is rewritten to prune each manifest FILE in place rather than
the merged view. It previously wrote its result to the legacy aggregate, which
would now be a write to a file nothing reads. It prunes the aggregate too when
present, so an un-migrated store can still be cleaned.

Suite 134 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mspinola
mspinola merged commit 6640c68 into main Jul 26, 2026
5 checks passed
@mspinola
mspinola deleted the feat/drop-legacy-manifest branch July 26, 2026 21:09
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