Skip to content

Sync both stores, with the exclusion lists they actually need - #110

Merged
mspinola merged 1 commit into
mainfrom
claude/sync-both-stores
Aug 21, 2026
Merged

Sync both stores, with the exclusion lists they actually need#110
mspinola merged 1 commit into
mainfrom
claude/sync-both-stores

Conversation

@mspinola

Copy link
Copy Markdown
Owner

ADR-0007 split the data across two stores. The sync scripts still mirrored one, so bars written to $MARKETDATA_STORE reached neither the Mac nor the dash — a producer running green every night while half its output stayed on the Windows box.

Why a second pass with the same flags would have been worse than nothing

The two stores disagree about the one file that matters:

COT store bar store
live bookkeeping manifests/<half>.json manifest.json (root)
root manifest.json dead legacy aggregate — exclude the only index — must be carried

Both robocopy /XF and rsync --exclude match by name at any depth, so copying the COT store's exclusion list onto the bar store strips its whole index in transit and delivers a replica full of parquet it cannot enumerate. Every file is present, so disk usage and a directory listing both look right; only a read notices.

This repo already documents that exact trap one directory over, for vintage/snapshots.json. So: two passes, two lists, per transport.

  • sync-store.cmd — two robocopy /MIR passes. Pass 2 runs even when pass 1 fails (the stores are independent; aborting early lets a COT hiccup silently stop bars), and the exit is the worse of the two codes.
  • push-to-server.cmd — two rsync pairs: data with --delete, then the manifest on its own without --delete, so the mirror cannot remove the replica's copy in the window before the new one lands.
  • pull-store.sh — the same split on the consumer-pull side.

The freshness check needed two signals, not one

verify-replicas.sh goes from two checks to four. This is the part that would have been wrong if copied across:

  • COT. cotdata rewrites status.json on every run, new data or not → "mtime is today" is a clean test.
  • Bars. marketdata has no status.json and rewrites manifest.json only when a bar is actually written. A weekend, a holiday, or a deferred --require-final run legitimately writes nothing, so demanding "today" there fails every Saturday — and a weekly false alarm stops being read by the second month. Bars get a staleness window instead (BAR_MAX_AGE_DAYS, default 4: Friday's write is still fresh on Tuesday).

Both transports preserve timestamps, so a replica's mtime is the producer's write time. That is what makes either test mean anything — and it buys a sharper one for free: both replicas should carry identical mtimes, so a mismatch means one push is behind even while both sit inside the window and neither looks wrong alone.

sync_preflight.py half-worked on a bar store, which is worse than failing

It did not error. The manifest loaded through the legacy per-domain fallback, so the summary looked plausible — while the on-disk orphan check globbed a flat prices/ and never descended into bars/<domain>/<source>/, reporting zero orphans no matter what a mirror would delete.

Layout is now detected per store. Being handed one of each exits 2 with CANNOT JUDGE rather than guessing: that pairing is far likelier to be two swapped paths than an intention, and the mirror it would green-light deletes the entire destination.

Verification

These are the files whose whole job is to notice, so they now have tests:

  • tests/test_sync_preflight.py — 12 tests: layout detection, the nested-bars/ orphan regression, cross-layout refusal, and the original checks unchanged.
  • tests/test_verify_replicas.py — 8 tests running the real bash script with ssh and both --check binaries stubbed on PATH. Covers the case that motivated this: a current COT store beside a stale bar store, green under the old two-check version and red under this one.

219 passed, ruff clean. The .cmd files could not be executed — there is no cmd in this sandbox — so their control flow is reasoned from cmd's parse-time expansion rules, same caveat as #109. What I checked rather than remembered: marketdata's store layout, raw_root(), and that manifest.json is written by _touch_manifest only on an actual write, all read from the marketdata source.

Also in here

  • Markers renamed so none is a prefix of another — REPLACE_WITH_LOCAL_COT was a prefix of REPLACE_WITH_LOCAL_COTDATA_STORE, which quietly corrupts a find-and-replace.
  • verify-replicas.sh epoch formatting made portable: date -r reads a number on BSD and a filename on GNU.

Generated by Claude Code

ADR-0007 split the data across two stores. The sync scripts still mirrored
one, so bars written to $MARKETDATA_STORE reached neither the Mac nor the
dash — a producer running green every night while half its output stayed on
the Windows box.

The fix is not "add a second pass with the same flags". The two stores
disagree about the one file that matters:

  COT store   manifests/<half>.json is live; root manifest.json is a dead
              legacy aggregate, and excluding it is correct.
  bar store   root manifest.json is the ONLY index; there is no manifests/.

Both robocopy /XF and rsync --exclude match by NAME AT ANY DEPTH, so
carrying the COT list over to the bar store strips its whole index in
transit and delivers a replica full of parquet it cannot enumerate. Every
file is present, so size and a directory listing both look right; only a
read notices. That is the same trap this repo already documents one
directory over, for vintage/snapshots.json.

So: two passes, two lists, per transport.

  sync-store.cmd      two robocopy /MIR passes; pass 2 runs even when pass 1
                      fails, and the exit is the worse of the two codes
  push-to-server.cmd  two rsync pairs (data --delete, then the manifest on
                      its own without --delete, so the mirror cannot remove
                      the replica's copy before the new one lands)
  pull-store.sh       the same split on the consumer-pull side

verify-replicas.sh goes from two checks to four, and the two stores need
two different freshness signals — this is the part that would have been
wrong if copied across. cotdata rewrites status.json on EVERY run, so
"mtime is today" is a clean test. marketdata has no status.json and
rewrites manifest.json only when a bar is actually written, so a weekend,
a holiday or a deferred --require-final run legitimately writes nothing:
demanding "today" there would fail every Saturday, and a weekly false
alarm stops being read by the second month. Bars get a staleness window
instead (BAR_MAX_AGE_DAYS, default 4).

Both transports preserve timestamps, so a replica's mtime is the
PRODUCER's write time. That is what makes either test mean anything, and
it buys a sharper one for free: both replicas should carry identical
mtimes, so a mismatch means one push is behind even while both sit inside
the window and neither looks wrong alone.

sync_preflight.py learns both layouts. It did not fail loudly on a bar
store — it half-worked, which is worse: the manifest loaded through the
legacy fallback so the summary looked plausible, while the on-disk check
globbed a flat prices/ and never descended into bars/<domain>/<source>/,
reporting zero orphans no matter what a mirror would delete. Layout is now
detected per store, and being handed one of each exits 2 rather than
guessing — that pairing is far likelier to be two swapped paths than an
intention, and the mirror it would green-light deletes the destination.

Tests, because these are the files whose whole job is to notice:
  test_sync_preflight.py    12 tests — layout detection, the nested-bars
                            orphan regression, cross-layout refusal
  test_verify_replicas.py    8 tests — runs the real bash script with ssh
                            and both --check binaries stubbed on PATH

Also renamed the markers so none is a prefix of another
(REPLACE_WITH_LOCAL_COT was a prefix of REPLACE_WITH_LOCAL_COTDATA_STORE,
which quietly breaks a find-and-replace), and made verify-replicas.sh's
epoch formatting portable — `date -r` reads a number on BSD and a filename
on GNU.

219 passed, ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jo4iovRfc2fzE9MwcLp7r2
@mspinola
mspinola merged commit 0164310 into main Aug 21, 2026
5 checks passed
@mspinola
mspinola deleted the claude/sync-both-stores branch August 21, 2026 00:21
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