crosscue

Compatibility Matrix — Versioned Contracts

Status: Living — update whenever any contract version bumps. Mixed-version sync policy: ADR-0016.

Crosscue is no longer a single binary: it is an app, a sync format, a server, and a widget that evolve at different speeds. This page is the one place that states every versioned contract, who depends on it, and what must stay compatible with what.

The five contracts

# Contract Current version Written by Read by Compatibility rule Bump procedure
1 Local DB schema (Drift) v7 (app_database.dartschemaVersion) The app The same app Migrations are additive and run forward-only on upgrade; each historical step has a migration test Increment schemaVersion, add migration + test (checklist in app_database.dart)
2 Sync blob envelope schema 1 (SyncBlob.currentSchemaVersion) Every synced device Every synced device Readers ignore unknown payload fields; blobs with a newer schema are skipped (decode() → null). Prefer additive payload fields over schema bumps — see ADR-0016 for the mixed-version policy Bump currentSchemaVersion only under ADR-0016’s flag-day rules
3 Worker D1 schema migrations 0001–0007 (0007 = ops_meta, the retention heartbeat store, #262) Worker deploys Deployed Worker Migrations must be backward-compatible with the currently deployed Worker (additive), because migrate runs before deploy; never edit an applied migration New numbered SQL file; migrate → deploy, staging before prod (DEPLOYMENT.md)
4 Worker HTTP API unversioned paths (contract: API.md) Worker Every shipped app version Worker changes must stay compatible with the oldest app version still in the field, unless that version is deliberately retired via the min-client lever (see below) Additive request/response changes by default; removals require raising MIN_SUPPORTED_CLIENT first
5 Widget payload crosscue_widget_v1, "version": 1 App (HomeWidgetService) iOS widget extension + Android CrosscueWidgetProvider (#223) — either may be older than the app, or newer after a partial update Additive only: new optional keys/rows; never repurpose version for additive fields New key name (_v2) only for breaking shape changes

Related but unversioned: the app↔extension App Group route token (pendingIntentRoute, additive string tokens by design — see ios-app-intents.md) and the export/import backup file from the privacy screen (treat as contract #1’s serialized form; verify import of old exports when bumping #1).

Support-window statements

These are the guarantees to preserve when changing anything above:

  1. Old app + new Worker: must keep working. Deploy order is always Worker-before-app-release, and the Worker supports every fielded client until a minimum-client mechanism exists.
  2. New app + old data: a fresh build must open any database produced by any released version (forward-only migrations, each step tested).
  3. Two devices, different app versions, same sync account: both must converge or fail visibly — never fork silently. Today’s behavior and the target policy are in ADR-0016.
  4. App + older widget extension (or vice versa): the widget renders whatever optional rows it understands; absent/extra keys are not errors.

Minimum-client lever (#256 — shipped)

Contract #4’s “support the oldest fielded client” rule now has a deliberate escape hatch:

Rules for using it: raising the minimum is a deliberate, logged decision — staging first, release-notes mention, and remember it also cuts off app versions that predate the header (< v1.4.3). Until the dual-host/v1.4.3 generation dominates the field, leave it unset.

External dependency: Crosshare (single point of failure)

CrosshareDownloader is an HTTP + HTML scraper, and three user-visible features sit on it: the home “Past puzzles” section, daily auto-download, and Challenge Boards eligibility (crosshare_daily_mini is the only accepted result source). A Crosshare markup change degrades all three at once.

Degradation policy (stale-but-honest):

Risk-reduction tasks:

  1. Scheduled CI canary (#257): .github/workflows/crosshare-canary.yml runs crosscue/tool/crosshare_canary.dart against the live site weekly (Mondays 06:17 UTC, also dispatchable) — month listing + newest-mini download + parse. On failure it opens (or comments on) a deduplicated “Crosshare canary failing” issue. The degradation rendering is locked by test/features/home/presentation/widgets/past_puzzles_section_test.dart.
  2. Ask Crosshare about a stable feed/API (license approval already on file — CONVENTIONS.md source table). (owner action, untracked)
  3. Longer term: a Worker-side canonical puzzle-id registry (D4 in ../challenge-boards-159-breakdown.md) to decouple board eligibility from client-side scraping success.

Quick reference — where each version lives

crosscue/lib/core/database/app_database.dart   schemaVersion (Drift)
crosscue/lib/core/sync/models/sync_blob.dart   currentSchemaVersion (sync envelope)
crosscue/backend/challenge_boards/migrations/  D1 schema (numbered SQL)
crosscue/backend/challenge_boards/API.md       HTTP contract (prose)
docs/architecture/ios-widget-setup.md          widget payload schema