ARCHITECTURE-DECISIONS.md
This document tracks significant architectural decisions and patterns in the Super Productivity codebase. When making changes that affect these patterns, reference this document and update it if needed.
Status: ✅ Active (since commit 400ca8c1, 2026-01-29)
Decision: The task.dueDay and task.dueWithTime fields are mutually exclusive in new data. When setting dueWithTime, dueDay must be cleared (set to undefined). When reading, dueWithTime takes priority over dueDay.
Rationale:
Implementation:
dueDay when setting dueWithTime (in meta-reducers)dueWithTime first; only check dueDay if dueWithTime is not set (in selectors)Key Files:
task.model.ts - Field definitions with JSDoctask-shared-scheduling.reducer.ts - Write implementationwork-context.selectors.ts - Read patternplanner.selectors.ts - Read patterntask.selectors.ts - Read patternWhen to Update This Pattern:
Status: ✅ Active (established pattern)
Decision: TODAY_TAG (ID: 'TODAY') is a virtual tag whose membership is determined by task.dueWithTime or task.dueDay, not by task.tagIds. The tag's taskIds field stores only the ordering of tasks, not membership.
Key Invariant: TODAY_TAG.id must NEVER be added to task.tagIds
Rationale:
Related: Uses the dueDay/dueWithTime mutual exclusivity pattern (Decision #1)
Key Files:
tag.const.ts - TODAY_TAG definitionwork-context.selectors.ts - Membership computationtask-shared-helpers.ts - Invariant enforcementWhen to Update This Pattern:
Status: ✅ Active (since May 2026)
Decision: Operation-log sync code is split by dependency direction:
src/app composes host-specific wiring, @sp/sync-providers owns bundled
provider implementations, and @sp/sync-core owns framework-agnostic reusable
sync primitives.
Rationale:
Implementation:
@sp/sync-core has no runtime dependencies and owns vector-clock algorithms
used by client/server compatibility pathspackages/shared-schema compatibility-re-exports generic vector-clock
algorithms from @sp/sync-core; @sp/sync-core must not import
@sp/shared-schema@sp/sync-providers depends on public @sp/sync-core plus provider runtime
helpers, while app factories inject credentials, platform bridges, validators,
OAuth routing, and configDocumentation: docs/sync-and-op-log/package-boundaries.md
Key Files:
packages/sync-core/src/index.ts - Core public APIpackages/sync-providers/src/index.ts - Provider public APIeslint.config.js - Package boundary enforcementsrc/app/op-log/sync-providers/sync-providers.factory.ts - App-side provider compositionWhen to Update This Pattern:
Status: ✅ Active (since May 2026)
Decision: SuperSync batch uploads derive conflict-safety from the shared
user_sync_state.lastSeq row write that reserves server sequence numbers, not
from PostgreSQL RepeatableRead snapshot isolation alone.
Rationale:
user_sync_state.lastSeq row forces
accepted writers for the same user to serialize on that row lockImplementation:
INSERT ... ON CONFLICT ... DO UPDATE SET last_seq = last_seq + deltaskipDuplicates; an unexpected unique conflict
aborts the transaction and lets the request retrylastSeq write requires replacing this safety
mechanism with an equivalent per-user serialization primitiveDocumentation: docs/sync-and-op-log/diagrams/02-server-sync.md
Key Files:
packages/super-sync-server/src/sync/sync.service.ts - Upload transaction and batch primitivepackages/super-sync-server/prisma/schema.prisma - user_sync_state.last_seqWhen to Update This Pattern:
Status: ✅ Active (since 2026-06-06, branch feat/completing-projects-48eeb4)
Decision: "Complete project" is a plain single-entity PROJECT flag flip (completeProject, OpType.Update, mirroring archiveProject → sets isDone/doneOn/isArchived). The accompanying resolution of unfinished tasks ("move to Inbox" / "mark done") runs first, as the normal per-task actions (moveToOtherProject / updateTask isDone) dispatched in a loop with the Rule #6 bulk-dispatch flush — not bundled into a single atomic multi-entity op.
Rationale: An earlier iteration made completion one atomic Batch op (completeProject) that marked/moved tasks inside the project-shared meta-reducer. Because that op deliberately routed around the normal per-task actions, every system that observes those actions had to be re-taught about completeProject separately:
affectedEntities multi-entity-ref feature threaded through sync-core, the sync server (+ a Prisma migration), shared-schema and the op-log — ~1,565 LOC, of which completeProject was the only producer.completeProject listener to re-derive the task changes the atomic op skipped.The atomic op's headline benefit — reversing the whole thing as one unit — was never realized: reopenProject only clears the project flags; it does not un-move or un-complete the resolved tasks. So the bundle paid a large cross-cutting cost for an undo guarantee it didn't provide. Decoupling makes the existing effects and per-entity conflict detection fire naturally and deletes ~1,750 LOC total (revert + decouple). Trade-off accepted: completion now emits N+1 ops (one per resolved task + the flag flip) instead of one, and there is a brief intermediate state — both fine for a rare, user-initiated action whose resolution is not atomically reversible anyway. One behavioral nuance vs. the old atomic op: when unfinished work is moved to Inbox, a task that was being actively tracked stays the current task (it was carried forward, not finished — consistent with Inbox's carry-forward intent); the mark-done path stops tracking the current task via the existing autoSetNextTask$ effect. The atomic op cleared the current task in both cases; the decoupled design intentionally keeps it for the carry-forward case.
Implementation:
completeProject({ id, doneOn }) in project.actions.ts; on(completeProject) flag flip in project.reducer.ts (guards INBOX_PROJECT). reopenProject clears the flags only.ProjectService.complete(id, doneOn) dispatches the flag flip; moveTasksToInbox() / markTasksDone() loop the normal per-task actions + setTimeout(0) flush.work-context-menu resolves unfinished work before calling complete().completeProject op or affectedEntities for it without re-justifying the full downstream cost above. Prior atomic implementation is preserved in history at commit 0893a86162.Key Files:
project.actions.ts, project.reducer.tsproject.service.ts — complete / moveTasksToInbox / markTasksDonework-context-menu.component.ts — completeProject() flowWhen to Update This Decision:
Add a new decision record when:
### N. [Pattern/Decision Name]
**Status**: ✅ Active | 🚧 Draft | ⚠️ Deprecated | ❌ Superseded
**Decision**: [One-sentence summary of the decision]
**Rationale**:
- [Why was this decision made?]
- [What problems does it solve?]
**Implementation**:
- [How is it implemented?]
- [Key techniques or patterns used]
**Documentation**: [Link to detailed docs]
**Key Files**: [List of primary files implementing this pattern]
**When to Update This Pattern**: [Scenarios when someone should review/update this]
docs/sync-and-op-log/ - Operation log architecturedocs/long-term-plans/ - Future architectural plansWhen committing changes related to these patterns, reference this document and the specific decision:
feat(tasks): implement feature X
Uses dueDay/dueWithTime mutual exclusivity pattern (ARCHITECTURE-DECISIONS.md #1)