Skip to content

[Epic] Sources of truth: stop synchronizing duplicated state — collapse it #2161

Description

@2witstudios

The pattern

Wherever this codebase stores one fact in two representations, the two drift — reliably, provably, across every layer. The response so far has been to patch individual desyncs: an isPaused guard here, a Math.max limit reconciliation there, a dated repair script, a "keep in sync with X" comment, a version counter, a dual-write helper with a deletion covenant. Every duplicated fact requires a sync protocol; every hand-rolled sync protocol is a standing bug factory. The durable fix is almost never better synchronization — it's collapsing the duplication: one source of truth per fact, everything else derived at read time or generated at build time.

The evidence that drift is systemic, not incidental: the repo carries at least nine repair/backfill scripts that exist only because two representations diverged (repair-orphaned-trashed-pages, sync-legacy-subscriptions, backfill-gifted-subscriptions, backfill-credit-ledger with dated adjustment rows, reenqueue-unprocessed-uploads, backfill-file-storage, calculate-initial-storage, normalize-dead-ai-models, migrate-ai-models-to-openrouter) — plus an entire client store (useEditingStore) whose only job is arbitrating between two cache layers.

Findings (this audit)

Client state

Schema / server

Billing / limits

Cross-service / constants

Known duplications deliberately NOT filed (managed or accepted)

  • credit_balances vs credit_ledger — genuinely dual by design, but it's the one place drift is engineered for: computeBalanceDrift + getBalanceDriftAlerts monitor continuously, holds are swept by cron. This is the model the unmonitored denormalizations above should follow if they can't be collapsed.
  • AI model catalog/pricing maps + persisted model ids — has a drift-report script (sync-openrouter-models.mjs), runtime default-substitution self-heals, and two generations of normalize scripts; bounded, though a merged catalog+pricing table would still be better.
  • SDK hand-mirrored constants — the done-right pattern: every copy has a compile-time or source-parsing drift-guard test (roles-pageperm-drift-guard.test.ts, edge-logger.test.ts). When a copy must exist (bundle isolation), this is the standard to hold it to.
  • Smaller comment-synced mirrors worth folding into the above work as touched: duplicated MIME lists (file-security.ts ↔ processor security.ts, content-detector's "keep in sync with" triple), credit-balance.ts/credit-gate.ts mirrored date arithmetic and STRIPE_REF_ARBITER, tenant-export.ts hand-listed column arrays ("A mismatch will cause data loss"), client vs server deployment-mode env vars, WorkspaceLayoutScopeDTO mirror, form-target field mappings (drift documented in-schema as a v1 limitation).

Decision rule going forward

For any fact about to be stored twice, in order of preference:

  1. Don't. Derive at read time (dead COALESCE fallbacks, tab titles, trash-state-from-ancestor) or import from the one module that owns it (tiers, page types, prices).
  2. If a copy is forced by a boundary (client bundle, separate process, external system): add a drift-guard test (the SDK pattern) or a reconciler with alerting (the credit-balance pattern). Comments are not a sync mechanism.
  3. If both sides must be writable: one shared writer owning the dual-write (task assignees, chat derived fields), never per-call-site reimplementation.

Prior art showing the payoff: #1055 (tool registry), #1452 (builtin provider config), #1052 (env var drift) — each was this same disease in miniature, and each fix was collapse, not sync.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    type:epicEpic: Parent issue tracking multiple related tasks

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions