Skip to content

Pass bidirectional Cyd Bluesky archive v2 conformance and enable the writer - #119

Merged
micahflee merged 2 commits into
mainfrom
bluesky-archive-v2-conformance-matrix
Oct 11, 2026
Merged

micahflee merged 2 commits into
mainfrom
bluesky-archive-v2-conformance-matrix

Conversation

@micahflee

Copy link
Copy Markdown
Member

Closes #100.

Mobile built its version 2 writer before the readers that consume it (ADR 0004) and kept export behind __DEV__ until an archive it wrote could be proven readable — by Mobile and by Desktop. This is that proof, the gate that keeps it true, and the export it unblocks.

The matrix

npm run test:archive-matrix names 21 conformance rows across the three acceptance criteria of #100 and ties each to the tests that prove it. It fails three ways, each verified by breaking it on purpose:

  • a row goes red;
  • a row's evidence is renamed or deleted;
  • a row loses one test out of a describe block it was counting on — every selector carries a least= count, because substring-matching a describe is convenient and weak on its own.

It runs in CI on every push and subsumes the test:archive-contract and test:archive-conformance steps, which are now local-only npm scripts.

Beyond Jest it runs the contract's own Python checker over archives Mobile's writer produced — the two committed real-data fixtures and the archives the round-trip test writes — plus the checker's mutation cases, so the thing doing the checking is known to still reject what it should.

Both directions

services/__tests__/archive-contract-exchange.test.ts, gated on the pinned canonical bundle:

  • Desktop → Mobile. A canonical archive goes through intake, restore and merge; what comes out is asserted through Mobile's own browse queries and the files on disk. Importing twice changes nothing. Merging the complete fixture into an account restored from the incomplete one recovers the video it could not carry, through a handle change, by DID.
  • Mobile → Desktop. That restored account is re-exported through Mobile's own writer, compared to the canonical archive by normalized semantics, then handed to the contract's checker. Both fixtures make the trip — an incomplete archive coming back looking complete would be worse than the file it is missing.

The round trip is lossy in seven ways Mobile cannot help. Each is asserted as a loss, under "what a Mobile round trip loses, on purpose": an unasserted loss is indistinguishable from a regression, and fixing one is meant to fail its test. Two of the seven — a like's own CID, and the bookmark record — were not written down before and are now in docs/bluesky-archive-fixtures.md.

No bugs turned up in the writer or the readers: they already interoperated with the canonical fixtures.

The writer

Export is now "Export Bluesky archive" in the app-wide menu beside import, because the two halves of moving your data between Cyd installations belong side by side. The menu has no account in hand, so which account is the export's own first question — asked only when the device holds more than one. DevArchiveExportCard and its __DEV__ gate are gone.

Readers and the writer ship in the same version rather than two. Nobody can export before installing the version that added export, and that version reads archives. What the ordering rule actually guards is an archive existing before anything can read one, so that is a check now rather than a sentence in an ADR: the readers-released row walks every tag and the working tree and fails any that offers export without archive-import, archive-restore and archive-merge. It is written against modules rather than commits, so a rebase cannot retire it and the next archive version inherits it unchanged. ADR 0004 records the reasoning.

Known gap, not addressed here

iOS import/export staging is still eligible for device backup. ADR 0014 says it should be excluded and plugins/android-backup-rules.js does that for Android, but there is no iOS equivalent, so Documents/archive-intake/ and Documents/archive-export/ go to iCloud. expo-file-system 57 exposes no API for NSURLIsExcludedFromBackupKey, so closing it needs a config plugin or a small native module. It predates this branch (#91/#99 territory); the device-backup row proves the Android half. Worth its own issue.

Verification

  • npm run lint (eslint + tsc) clean
  • npm test — 959 tests, 82 suites green
  • npm run test:archive-matrix — 21/21 rows against the bundle pinned at cbd2614

🤖 Generated with Claude Code

micahflee and others added 2 commits September 14, 2026 13:31
Mobile built its version 2 writer before the readers that consume it, and
kept export behind __DEV__ until an archive it wrote could be proven
readable — by Mobile and by Desktop. This is that proof, and the gate that
keeps it true.

The matrix names every conformance row the writer's release depends on and
ties each to the tests that prove it. It fails two ways: a row gone red, and
a row whose evidence was renamed or deleted out from under it. Evidence
spread across eighty test files is evidence nobody can audit.

Both directions are new. A canonical Desktop archive now goes all the way
through intake, restore and merge and is read back through Mobile's own
browse queries; that restored account is then re-exported through Mobile's
writer and handed to the contract's own checker. The round trip is lossy in
ways Mobile cannot help, so each loss is asserted as a loss — an unasserted
one is indistinguishable from a regression.

Export is now "Export Bluesky archive" in the app-wide menu, beside import,
because the two halves of moving your data between Cyd installations belong
side by side. The menu has no account in hand, so which account is the
export's own first question — asked only when there is more than one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review found the gate's own weak spot: a row proven by substring-matching a
describe block stays green when the one test carrying its claim is deleted,
which is the rot the matrix exists to catch. Evidence now carries a count, so
a deletion fails the row and whoever made it has to say so there.

Two claims were also wider than their evidence. Offline browsing asserted
media_asset rows; it now asks the query Browse itself runs, which meant
lifting that query out beside buildFirstPageQuery so something other than a
phone can run it. And only the complete fixture made the round trip, so an
incomplete archive coming back looking complete — worse than the missing file
— was unproven. Both fixtures go both ways now.

Readers and the writer ship in one version rather than two: nobody can export
before installing the version that added export, and that version reads
archives. What the ordering rule actually guards is an archive existing before
anything can read one, so that is a check now instead of a sentence in an ADR
— every tag and the working tree, by module rather than by commit.

The rest is what review named: glossary terms the CONTEXT.md entries tell us
to avoid, a docblock left describing the wrong constant, and the sheet and
handle treatment the two archive modals had each grown their own copy of.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@micahflee
micahflee merged commit e60928d into main Oct 11, 2026
1 check passed
@micahflee
micahflee deleted the bluesky-archive-v2-conformance-matrix branch October 11, 2026 19:25
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.

Pass bidirectional Cyd Bluesky archive v2 conformance and enable the writer

1 participant