Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cutover: lossless beads JSONL exchange

sd cutover rehearses a migration on a copy of a beads JSONL board. It requires an explicit local --store and --graph and refuses a configured pendant or remote destination. It never opens or updates a br database. Keep br authoritative until the migration owner approves the cutover.

sd --store rehearsal.db --graph https://example.org/project/trial \
  cutover verify --file board-copy.jsonl
sd --store rehearsal.db --graph https://example.org/project/trial \
  cutover import --file board-copy.jsonl --dry-run
sd --store rehearsal.db --graph https://example.org/project/trial \
  cutover import --file board-copy.jsonl
sd --store rehearsal.db --graph https://example.org/project/trial \
  cutover export --file returned.jsonl

verify imports into an isolated in-memory Quipu store, reads the committed facts back and compares JSON by exact record ID and field path. Its JSON report includes input and output counts, a difference count and every changed field, including nested comment and dependency metadata. Nonzero differences exit 1. Object key order is immaterial; array order and absent versus null remain significant. Malformed records and duplicate IDs are refused.

Each imported seed carries its original JSON in a seeds:beadsJson fact. Normal seed fields are also projected to the seeds vocabulary (The storage model); a time that is not a valid xsd:date/xsd:dateTime refuses the import, naming each record and field. A valid time is stored in its canonical spelling (the same instant); the output’s canonicalized_times counts them, and export restores br’s original spelling. Export overlays edits to those modeled fields on the original JSON. Unknown fields, dependency types, timestamps, author names and nested metadata survive. The legacy relates-to dependency projects as related, retaining its original spelling on export. hooked is retained as a non-ready status. Unknown issue types are retained by the bridge even when the create CLI does not offer them. Unsupported dependency types are carried, not interpreted as blockers.

Seeds-authored items include a _seeds extension holding the revision and unmodeled RDF facts (including literal datatypes and languages). Comments carry their graph indexes there too. An occupied foreign _seeds key is refused when it would be overwritten. A third-party importer must retain that extension to preserve those facts; this command does not change third-party import behavior. Historical br transactions absent from the source JSONL cannot be reconstructed.

New native dependencies retain their creation actor and time in carried seeds:dependencyOrigin facts. Later item edits do not change edge attribution; removing and re-adding an edge records its new creation. Imported dependency metadata remains verbatim, including deliberate peer corrections. A legacy native edge with neither carried JSON metadata nor creation provenance refuses export: the item creator and its last update are not evidence of who created that edge. Reconcile its provenance explicitly before using it in a cutover.

Newly synthesized label/dependency unions use br’s canonical ordering. Unchanged imported arrays retain their original order.

br comment IDs are global integers; a seeds comment index is local to its item. The native bridge reserves global IDs in <store>.cutover-<graph-sha256>.comments.json, bound to the store path and graph. It retains imported IDs and maps each new (item ID, comment index) to an unused positive integer. The high-water mark and deleted slots stay reserved. The map is locked and saved before publishing an export, so another process or a later export destination retains the same identities. Keep this sidecar with the store when backing it up; do not delete it to reset a conflict. A malformed map, identity collision or exhausted integer sequence refuses publication. Concurrent independently allocated IDs may still conflict and require explicit reconciliation; they are never silently reassigned.

Ongoing exchange on a copied workstream

sd --actor migration-operator --store rehearsal.db \
  --graph https://example.org/project/trial \
  cutover sync --file board-copy.jsonl --base trial.cursor.json --dry-run
# Inspect the would-change report, then repeat without --dry-run.

The cursor holds the last common JSON state and is bound to the store path, graph and JSONL path. Reusing it for another pair refuses. A missing cursor means a first exchange, not permission to resolve conflicting existing items. A corrupt or unreadable cursor is an error. Record removals refuse unless explicitly enabled with --allow-deletes. Import is additive and refuses changed existing IDs; use sync to reconcile updates against a common base.

Sync compares each field with that common state. Independent edits merge; conflicting scalar edits report their record IDs and fields and write nothing. Labels, dependencies and comments union on simultaneous changes. updated_at keeps the later UTC timestamp when both sides changed it, including mixed fractional precision. Non-UTC concurrent timestamps require reconciliation. Unmodeled scalar fields have the same conflict protection as modeled ones. An unchanged second sync does not commit or replace the JSONL or cursor.

Each store commit records the supplied --actor and source=beads-sync. Original creator and comment attribution are carried unchanged. A JSONL export does not identify the actor of a later edit; the caller must supply that actor, not infer it from created_by.

Writes hold the normal sd store lock plus a cursor lock and a JSONL sidecar lock (<file>.cutover.lock). Any process producing that JSONL must cooperate with that lock. Do not point this command at a live br export: br’s exporter does not acquire the cutover lock. A pre-publication content check catches changes observed during planning, but cannot fence an uncooperative writer.

A durable <cursor>.pending journal is written before committing the store. It carries both pre-images and the intended common state. After a failure, repeat the identical sync: it completes publication if both sides still match an expected state. Newer edits cause a refusal that preserves the journal for reconciliation. The cursor advances last. This makes interrupted publication recoverable; it does not make two independent stores one atomic transaction. Keep the cursor and pending journal private, like the board itself.

--dry-run creates no store, lock, journal, identity map or cursor. Export dry-run reports its count and destination without writing a file.

This is an explicit one-shot operation. It installs no scheduler or fleet cutover. A trial runner must state its polling interval, maintain a complete rollback br copy, verify both directions and observe the required soak before any production switch. A successful isolated round trip is one gate, not a certificate that the fleet has migrated.

Sync reports aggregate field counts and the first 100 differences for each side, with an explicit truncation flag. Use verify for a complete round-trip difference report. Dry-run also validates the planned graph against the same shapes as a real commit.