Motivka

Fix: Local Reconciliation Overwrites Production Blog Statuses

Fixed a high-severity data loss bug where syncing blog posts to a local PocketBase instance would overwrite Obsidian frontmatter statuses, which then corrupted production on the next sync. Added a --local flag to disable reconciliation for development instances.

2 Phases
3 Tasks
1 Days

The Problem

This one had already caused production data loss before I caught it. The blog sync CLI (bun run sync:blog) has a reconciliation phase that pulls statuses from PocketBase back into Obsidian frontmatter -- the idea being that production status always wins. An admin marks a post as "published" in production, and reconciliation writes that status back to the markdown file.

The bug: reconciliation runs against whichever PocketBase instance PB_URL points to. When I synced to my local dev instance, it would fetch the local statuses (often stale "draft" values), write them into frontmatter, and the next production sync would push those corrupted statuses upstream. A post I had published would silently revert to draft.

Root Cause

The reconciliation feature was designed with a single-instance mental model. The code at index.ts:570 checks if (!noPull && syncClient !== null) and runs reconciliation -- but it assumes the target PocketBase IS production. There was no mechanism to distinguish a local dev instance from production, and --no-pull existed but its relationship to multi-instance safety was not documented or obvious.

The Fix

I added a --local flag to the CLI. When passed, it sets local: true in the config and forces noPull: true, completely skipping the reconciliation phase. The implementation reuses the existing noPull code path rather than adding a parallel one.

The key change was small but important:

  • Added local: boolean to the SyncConfig type
  • In buildSyncConfig(), the --local flag sets both local: true and noPull: true
  • Added help text explaining the flag: "Skip status reconciliation -- use when syncing to a local development PocketBase instance to prevent frontmatter overwrites that could corrupt production"
  • Added a log line: [cli] Local mode: reconciliation disabled (--local flag)

I also extracted buildSyncConfig() as a pure function (separating it from the process.exit() calls in parseCLIArgs()) so it could be properly unit tested.

Testing

Defensive TDD again. Safety net tests confirmed all existing flags still work. Reproduction tests asserted that --local produces the right config shape and blocks reconciliation. The integration test simulated the exact multi-instance conflict scenario with temp markdown files and a mocked local PocketBase returning stale statuses.

Lessons

The --local flag is semantic sugar over --no-pull, but intent matters. A developer running --local communicates "I am syncing to dev" far more clearly than remembering to pass --no-pull every time. The footgun was not that the mechanism was missing -- it was that the safe path was not the obvious path.

Features Delivered

Bugs Fixed

Key Decisions

  • Reused existing --no-pull mechanism instead of adding a separate reconciliation guard — Avoids parallel code paths; --local is semantic sugar that forces noPull: true
  • Added --local as explicit flag rather than auto-detecting instance type — Explicit is safer for a destructive operation; auto-detection could fail silently