mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
docs: add TODAY_TAG architecture and improve timing constants docs
- Create docs/ai/today-tag-architecture.md documenting the virtual tag pattern, explaining why TODAY_TAG must never be in task.tagIds and how membership is determined by task.dueDay instead - Add reference section to operation-log.const.ts pointing to related timing constants in other domain files (sync.const.ts, meta-sync) This addresses documentation gaps identified in the maintainability review.
This commit is contained in:
parent
35e991e8f4
commit
402c67a5b1
2 changed files with 194 additions and 0 deletions
184
docs/ai/today-tag-architecture.md
Normal file
184
docs/ai/today-tag-architecture.md
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# TODAY_TAG Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
TODAY_TAG is a **virtual tag** that behaves differently from regular tags. Understanding this distinction is critical for correct implementation of features involving today's tasks.
|
||||
|
||||
## Key Invariant
|
||||
|
||||
**TODAY_TAG (ID: `'TODAY'`) must NEVER be added to `task.tagIds`.**
|
||||
|
||||
Membership in TODAY_TAG is determined by `task.dueDay`, not by `task.tagIds`. The `TODAY_TAG.taskIds` field stores only the **ordering** of today's tasks, not membership.
|
||||
|
||||
## Virtual Tag vs. Board-Style Pattern
|
||||
|
||||
| Aspect | TODAY_TAG (Virtual) | Regular Tags (Board-Style) |
|
||||
| ----------------------- | ----------------------- | ----------------------------- |
|
||||
| **Membership source** | `task.dueDay === today` | `task.tagIds.includes(tagId)` |
|
||||
| **Stored on task** | `task.dueDay` | `task.tagIds` |
|
||||
| **In task.tagIds?** | NO (invariant) | YES (required) |
|
||||
| **tag.taskIds purpose** | Ordering only | Ordering only |
|
||||
| **Use case** | Time-based grouping | Category/label grouping |
|
||||
|
||||
## Why This Pattern?
|
||||
|
||||
1. **Uniform move operations**: Drag/drop and keyboard shortcuts (Ctrl+↑/↓) work identically for TODAY_TAG and regular tags because all tags store ordering in `tag.taskIds`.
|
||||
|
||||
2. **Single source of truth**: `task.dueDay` is the canonical field for "is this task scheduled for today?" - no dual bookkeeping between `dueDay` and a hypothetical `tagIds` entry.
|
||||
|
||||
3. **Planner integration**: The planner view uses `task.dueDay` to organize tasks by day. TODAY_TAG naturally aligns with this.
|
||||
|
||||
4. **Self-healing**: Stale ordering entries are gracefully filtered out by the selector. No manual cleanup needed when a task's `dueDay` changes.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Definition
|
||||
|
||||
**File:** `src/app/features/tag/tag.const.ts`
|
||||
|
||||
```typescript
|
||||
export const TODAY_TAG: Tag = {
|
||||
id: 'TODAY',
|
||||
title: 'Today',
|
||||
icon: 'wb_sunny',
|
||||
// ... theme, colors
|
||||
};
|
||||
```
|
||||
|
||||
### Membership Computation
|
||||
|
||||
**File:** `src/app/features/work-context/store/work-context.selectors.ts`
|
||||
|
||||
The `computeOrderedTaskIdsForToday()` function:
|
||||
|
||||
1. Finds all tasks where `dueDay === today` (membership)
|
||||
2. Orders them according to `TODAY_TAG.taskIds` (ordering)
|
||||
3. Appends any unordered tasks at the end (self-healing)
|
||||
4. Filters out stale entries from `TODAY_TAG.taskIds` (self-healing)
|
||||
|
||||
```typescript
|
||||
const computeOrderedTaskIdsForToday = (todayTag, taskEntities) => {
|
||||
const todayStr = getDbDateStr();
|
||||
|
||||
// Membership: tasks where dueDay === today
|
||||
const tasksForToday = Object.values(taskEntities)
|
||||
.filter((t) => t && !t.parentId && t.dueDay === todayStr)
|
||||
.map((t) => t.id);
|
||||
|
||||
// Ordering: use TODAY_TAG.taskIds, filter stale, append missing
|
||||
const storedOrder = todayTag?.taskIds || [];
|
||||
const tasksForTodaySet = new Set(tasksForToday);
|
||||
|
||||
const orderedTasks = storedOrder.filter((id) => tasksForTodaySet.has(id));
|
||||
const unorderedTasks = tasksForToday.filter((id) => !storedOrder.includes(id));
|
||||
|
||||
return [...orderedTasks, ...unorderedTasks];
|
||||
};
|
||||
```
|
||||
|
||||
### Move Operations
|
||||
|
||||
**File:** `src/app/features/tag/store/tag.reducer.ts`
|
||||
|
||||
All move operations (`moveTaskInTodayList`, `moveTaskUpInTodayList`, etc.) update `tag.taskIds` uniformly for ALL tags, including TODAY_TAG:
|
||||
|
||||
```typescript
|
||||
on(moveTaskInTodayList, (state, { taskId, afterTaskId, workContextId }) => {
|
||||
const tag = state.entities[workContextId];
|
||||
const taskIds = moveItemAfterAnchor(taskId, afterTaskId, tag.taskIds);
|
||||
return tagAdapter.updateOne({ id: workContextId, changes: { taskIds } }, state);
|
||||
}),
|
||||
```
|
||||
|
||||
### Ensuring the Invariant
|
||||
|
||||
**File:** `src/app/root-store/meta/task-shared-meta-reducers/task-shared-helpers.ts`
|
||||
|
||||
Helper functions enforce the invariant:
|
||||
|
||||
```typescript
|
||||
export const filterOutTodayTag = (tagIds: string[]): string[] =>
|
||||
tagIds.filter((id) => id !== TODAY_TAG.id);
|
||||
|
||||
export const hasInvalidTodayTag = (tagIds: string[]): boolean =>
|
||||
tagIds.includes(TODAY_TAG.id);
|
||||
```
|
||||
|
||||
**File:** `src/app/root-store/meta/task-shared-meta-reducers/planner-shared.reducer.ts`
|
||||
|
||||
The planner meta-reducer:
|
||||
|
||||
- Updates `TODAY_TAG.taskIds` when moving tasks to/from today
|
||||
- Removes TODAY_TAG from `task.tagIds` if present (cleanup legacy data)
|
||||
- Never adds TODAY_TAG to `task.tagIds`
|
||||
|
||||
### Consistency Repair
|
||||
|
||||
**File:** `src/app/features/tag/store/tag.effects.ts`
|
||||
|
||||
The `repairTodayTagConsistency$` effect detects and repairs inconsistencies after sync:
|
||||
|
||||
```typescript
|
||||
repairTodayTagConsistency$ = createEffect(() =>
|
||||
this._store$.select(selectTodayTagRepair).pipe(
|
||||
skipDuringSyncWindow(), // Don't fire during sync replay
|
||||
filter((repair) => repair?.needsRepair),
|
||||
map((repair) =>
|
||||
updateTag({
|
||||
tag: { id: TODAY_TAG.id, changes: { taskIds: repair.repairedTaskIds } },
|
||||
isSkipSnack: true,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
This handles state divergence from per-entity conflict resolution during sync.
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
### Wrong: Adding TODAY_TAG to task.tagIds
|
||||
|
||||
```typescript
|
||||
// WRONG - Never do this
|
||||
task.tagIds = [...task.tagIds, TODAY_TAG.id];
|
||||
```
|
||||
|
||||
### Correct: Set task.dueDay
|
||||
|
||||
```typescript
|
||||
// CORRECT - Set dueDay to add to today
|
||||
task.dueDay = getDbDateStr(); // Today's date string
|
||||
```
|
||||
|
||||
### Wrong: Checking tagIds for TODAY membership
|
||||
|
||||
```typescript
|
||||
// WRONG - This will always be false (or indicates legacy bug)
|
||||
const isToday = task.tagIds.includes(TODAY_TAG.id);
|
||||
```
|
||||
|
||||
### Correct: Check dueDay
|
||||
|
||||
```typescript
|
||||
// CORRECT - Check dueDay for TODAY membership
|
||||
const isToday = task.dueDay === getDbDateStr();
|
||||
```
|
||||
|
||||
## Key Files Reference
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| `src/app/features/tag/tag.const.ts` | TODAY_TAG definition |
|
||||
| `src/app/features/work-context/store/work-context.selectors.ts` | Membership computation, repair selector |
|
||||
| `src/app/features/tag/store/tag.reducer.ts` | Move operations (uniform for all tags) |
|
||||
| `src/app/root-store/meta/task-shared-meta-reducers/planner-shared.reducer.ts` | Multi-entity updates for planner actions |
|
||||
| `src/app/root-store/meta/task-shared-meta-reducers/task-shared-helpers.ts` | `filterOutTodayTag()`, `hasInvalidTodayTag()` |
|
||||
| `src/app/features/tag/store/tag.effects.ts` | `repairTodayTagConsistency$` effect |
|
||||
|
||||
## Testing
|
||||
|
||||
Tests demonstrating the virtual tag pattern:
|
||||
|
||||
- `src/app/features/work-context/store/work-context.selectors.spec.ts` - Membership computation tests
|
||||
- `src/app/root-store/meta/task-shared-meta-reducers/planner-shared.reducer.spec.ts` - Move action tests
|
||||
Loading…
Add table
Add a link
Reference in a new issue