You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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:
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).
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.
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.
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
isPausedguard here, aMath.maxlimit 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-ledgerwith 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
isPausedguards, 4-way manual cache fan-outs, stale-forever tab titlesuseAnswerAskUserre-seeds the transport from a render-time snapshot — three stale-write bugs (fix pattern already exists inuseCacheMessageActions)Schema / server
taskItems.positionvspages.position— dead rail with a live consequence: AI agents see a different task order than userslastReplyAtpoints at deleted replies, mirror edits don't propagate, interrupted-stream recovery skipslastMessageAt)permissionstable — zero readers/writers, frozen misinformation next to page_permissions #2160 Dormant legacypermissionstable next topage_permissionsBilling / limits
founderfails,enterpriseorphaned); plan limits triplicated and previously divergedusers.subscriptionTiervssubscriptionsrows — webhook-only sync, no reconcilerusers.activeUploadscounter leaks permanently on process restart — bricks uploadsCross-service / constants
PageTypere-declared by hand — FILE and MACHINE silently dropped from glob search and the AI search tool (live bug)apps/adminis a hand-copied, already-diverged fork ofapps/webmodules (Stripe price IDs ×2, auth ×2, onboarding seeds ×2)broadcast-audience's hand-maintained room grammar cites line numbers ~300 lines stalepages.processingStatusvs queue state: fire-and-forget enqueue, stuck-pending proven in production, no reconcilerKnown duplications deliberately NOT filed (managed or accepted)
credit_balancesvscredit_ledger— genuinely dual by design, but it's the one place drift is engineered for:computeBalanceDrift+getBalanceDriftAlertsmonitor continuously, holds are swept by cron. This is the model the unmonitored denormalizations above should follow if they can't be collapsed.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.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.file-security.ts↔ processorsecurity.ts, content-detector's "keep in sync with" triple),credit-balance.ts/credit-gate.tsmirrored date arithmetic andSTRIPE_REF_ARBITER,tenant-export.tshand-listed column arrays ("A mismatch will cause data loss"), client vs server deployment-mode env vars,WorkspaceLayoutScopeDTOmirror, 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:
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.