-
Notifications
You must be signed in to change notification settings - Fork 0
GTD
本文描述本仓库 GTD 模块的现行实现(以代码为准):产品界面能力、行级统一模型、Postgres 权威落库、Client IndexedDB + SyncEngine、领域纯函数(可用性 / 透视 / 重复 / 排序)。
行业对照上,数据形态接近 Linear / OmniFocus 一类「字段化任务 tracker」:服务端权威、到达序定序、patch 列合并 LWW。
权威契约见 packages/gtd/src/sync-schema.ts、packages/gtd/src/sync.ts
入口:顶部导航 GTD(路由 /gtd)。需登录;每位用户独立一份数据。布局为三栏:侧栏 → 任务列表 → Inspector。
┌─────────────┬──────────────────────────┬─────────────┐
│ 侧栏 │ 任务列表 │ Inspector │
│ · 内置/自定义 │ 标题 + 任务行 / 分组 │ 任务或项目 │
│ · 项目树 │ 快速添加(部分视图) │ 详情编辑 │
│ · 标签树 │ 勾选完成、旗标、选中 │ 重复 / 标签 │
└─────────────┴──────────────────────────┴─────────────┘
侧栏当前选中视图记在浏览器 localStorage,下次打开尽量恢复。编辑经校验后写入本地行库,约 0.4s debounce 后由 SyncEngine push;侧栏可观察同步状态。
| 透视 | 用途 |
|---|---|
| Inbox | 未归入项目的顶层任务 |
| Projects | 按项目分组 |
| Tags | 按标签分组 |
| Forecast | 未来约两周内有 due 的任务,按日期分组 |
| Flagged | 已旗标 |
| Review | 所属项目需回顾的任务 |
| Completed | 已完成,按日期相关分组 |
| Predicted | 有截止日期的任务,按 due 分组 |
另可通过侧栏点选项目 / 文件夹 / 标签,合成临时过滤视图。自定义透视的创建与编辑见侧栏编辑器(§七)。
| 能力 | 说明 |
|---|---|
| 创建 | Inbox 或选中项目后列表顶栏添加;可向父任务添加子任务 |
| 列表 | 勾选完成/重开、旗标、点击打开 Inspector |
| 层级 | 缩进 / 出缩进、Action Group(groupType) |
| Inspector | 名称、备注、项目、标签、推迟/截止、重复规则;完成/放弃/恢复/重开/删除 |
生命周期:
stateDiagram-v2
[*] --> 活跃
活跃 --> 已完成: 完成
已完成 --> 活跃: 重开
活跃 --> 已放弃: 放弃
已放弃 --> 活跃: 恢复
活跃 --> 已删除: 删除
已删除 --> [*]
已删除为逻辑删除(status),常规列表默认不展示。 行左侧圆点为可用性派生色:绿 available、琥珀 due soon(默认截止前 2 天)、红 overdue、灰 blocked/终态等。
- 项目:侧栏新建;Inspector 可改类型(并行 / 顺序 / 单动作)、文件夹、暂停/恢复、标记已回顾、删除(级联软删子任务)。新建默认开启每周回顾。
- 文件夹:侧栏可建树;点击后列表展示该文件夹下各项目任务。
-
标签:侧栏树 + Inspector pill 切换;关联以
task_tag行同步。
GTD 要稳定回答三件事:此刻可执行什么(可用性)、完成后如何推进(重复克隆)、同一批任务如何切片展示(透视)。底层保证多端改动可合并、可解释。
组件关系:
flowchart LR
UI[Client UI] --> Store[GtdStore]
Store --> IDB[(IndexedDB\nrows / outbox / meta)]
Store --> Eng[SyncEngine]
Eng -->|POST /gtd/sync/push| API[Server]
Eng -->|POST /gtd/sync/pull| API
API --> PG[(PostgreSQL\n七表 + clock + 幂等)]
Store --> RS[RowStore]
RS --> Pure["@agent/gtd 纯函数"]
Pure -->|renderPerspective| UI
端到端时序(冷启动 → 乐观写 → push 裁决 → rebase):
sequenceDiagram
autonumber
participant View as View (React)
participant Store as Store (Jotai)
participant IDB as IndexedDB (rows/outbox/meta)
participant Sync as SyncEngine
participant Server as Server (Postgres)
rect rgb(240, 248, 255)
Note over View,Server: 阶段 1:冷启动水合
View->>Store: 挂载 / onUserIdChange → load
Store->>IDB: loadRows + loadLastSyncId
IDB-->>Store: EntityRow[](本地全量,含 tombstone)
Store->>Store: set rowsAtom → rowStoreAtom
Store-->>View: 先渲染本地数据
Store->>Sync: bootstrap(空库则 pull;再 startDaemons)
Sync->>IDB: outbox 空?→ pull : push
Note over Sync,Server: 后台对齐;有 pending outbox 时只 push
end
rect rgb(255, 250, 240)
Note over View,Server: 阶段 2:乐观写
View->>Store: 用户操作(例:改名 / complete)
Store->>Store: build → mutation/command(含 id、clientTs)
Store->>Store: applyPush 同语义 + validateInvariants
Store->>Store: set rowsAtom
Store-->>View: UI 即时刷新
Store->>IDB: persistAndQueue 事务 [rows, outbox]
IDB->>IDB: put 变更行 + append outbox
IDB-->>Store: commit
Store->>Sync: scheduleSync(debounce ~400ms)
end
rect rgb(240, 255, 240)
Note over View,Server: 阶段 3:push 与服务端裁决
Sync->>IDB: loadOutbox + lastSyncId
IDB-->>Sync: mutations[] + commands[]
Sync->>Server: POST /gtd/sync/push { mutations, commands, lastSyncId }
Note over Server: 事务开始
Server->>Server: FOR UPDATE gtd_sync_clocks
Server->>Server: 装载行 + 查 gtd_sync_mutations 幂等
Server->>Server: applyPush(到达序 patch 列合并 LWW;commands 先于 mutations)
Server->>Server: upsert 变更行;更新 clock;记 applied/rejected
Note over Server: 事务提交
Server-->>Sync: { applied, rejected, changes, serverSyncId }
end
rect rgb(253, 245, 253)
Note over View,Server: 阶段 4:rebase
Sync->>IDB: rebaseTransaction [rows, outbox, meta]
IDB->>IDB: 删 outbox 中 applied/rejected
IDB->>IDB: put changes(行级覆盖,含软删)
IDB->>IDB: meta.lastSyncId = serverSyncId
IDB-->>Sync: commit
Sync->>Store: onSynced → mergeChanges → rowsAtom
Store-->>View: 有差异则细粒度重渲染
end
硬规则:
- outbox 非空只走 push(有 pending 时不单独 pull)。
- push body 中 commands 与 mutations 分列;complete / drop / move / 级联删走 command。
- LWW 看服务端到达序(
clientTs入账但不裁决);sync_id只服务增量拉取。 - rebase 同一 IDB 事务:ack/nack outbox + put changes + 更新
lastSyncId。
实现:apps/client/src/stores/gtd-store.ts、gtd/row-store.ts、gtd/sync-engine.ts、apps/server/src/gtd/sync-repository.ts。
重复完成克隆见 §六;透视渲染见 §七。
Client 内存、IndexedDB、HTTP wire、Postgres 行映射,同构为:
type SyncEntity =
| 'task' | 'project' | 'folder' | 'tag'
| 'perspective' | 'attachment' | 'task_tag'
interface EntityRow<E extends SyncEntity = SyncEntity> {
entity: E
id: string // task_tag 为 `${taskId}|${tagId}`
userId: string
syncId: number // 每用户单调;仅增量拉取
deleted: boolean // 软删 tombstone,pull 仍下发
data: EntityDataOf<E>
}Zod 单一事实源:packages/gtd/src/sync-schema.ts。
erDiagram
FOLDER ||--o{ FOLDER : parentId
FOLDER ||--o{ PROJECT : folderId
PROJECT ||--o{ TASK : projectId
TASK ||--o{ TASK : parentId
TASK }o--o{ TASK_TAG : taskId
TAG ||--o{ TASK_TAG : tagId
TAG ||--o{ TAG : parentId
TASK ||--o{ ATTACHMENT : taskId
TASK ||--o| REPEAT_RULE : "data.repeatRule 内联"
PROJECT ||--|| REVIEW : "data.review"
PERSPECTIVE ||--|| FILTER : "data.filter"
| 实体 | 产品语义 | 行 data 要点 |
|---|---|---|
| folder | 项目容器,可嵌套 | name, parentId, order, status |
| project | 动作容器;parallel / sequential / singleAction | folderId, type, review, defaultTagIds, … |
| task | 最小执行单元;可有 groupType | projectId, parentId, order, status, defer/due, repeatRule 内联;标签见 task_tag
|
| tag | 可嵌套标签 | name, parentId, order, color |
| task_tag | 任务–标签关联(独立行,便于并发打标 LWW) | taskId, tagId |
| perspective | 视图规则 | filter, groupBy, sortBy, availabilityFilter, … |
| attachment | 附件元数据 | taskId, kind, url, filename |
RepeatRule:随 task 行内联(task.data.repeatRule jsonb)。导入导出时由 materialize 折叠进 GtdDocument.repeatRules[]。
| 通道 | 形态 | 用途 |
|---|---|---|
| mutation | upsert / delete + patch | 普通字段;task_tag 增删 |
| command | complete / drop / move / delete_* | 高风险原子语义 |
id 为客户端 UUID,作幂等键。applyPush:先 commands,后 mutations;每条独立捕获,违规进 rejected。
运行时真相是 EntityRow[]。materialize / dematerialize 仅导入导出边界。UI 日常吃 RowStore(packages/gtd/src/rows.ts):liveTasks / findLive / tagIdsOf 等。
定义:apps/server/src/db/schema/gtd.ts;迁移 0005_*.sql。
业务七表均带 user_id、sync_id、deleted(行级增量 + 软删);排序列 sort_order(float8)映射行 data order。增量索引 (user_id, sync_id)。
erDiagram
gtd_folders ||--o{ gtd_folders : "parent_id"
gtd_folders ||--o{ gtd_projects : "folder_id"
gtd_projects ||--o{ gtd_tasks : "project_id"
gtd_tasks ||--o{ gtd_tasks : "parent_id"
gtd_tasks ||--o{ gtd_task_tags : "task_id"
gtd_tags ||--o{ gtd_tags : "parent_id"
gtd_tags ||--o{ gtd_task_tags : "tag_id"
gtd_tasks ||--o{ gtd_attachments : "task_id"
gtd_folders {
text id PK
text user_id
text parent_id FK
text name
float8 sort_order
text status
bigint sync_id
boolean deleted
}
gtd_tags {
text id PK
text user_id
text parent_id FK
text name
text color
float8 sort_order
bigint sync_id
boolean deleted
}
gtd_projects {
text id PK
text user_id
text folder_id FK
text name
text type
jsonb review
timestamptz next_review_date
float8 sort_order
bigint sync_id
boolean deleted
}
gtd_tasks {
text id PK
text user_id
text project_id FK
text parent_id FK
text name
text status
text group_type
timestamptz defer_date
timestamptz due_date
jsonb repeat_rule
float8 sort_order
bigint sync_id
boolean deleted
}
gtd_task_tags {
text task_id PK_FK
text tag_id PK_FK
text user_id
bigint sync_id
boolean deleted
}
gtd_perspectives {
text id PK
text user_id
text name
jsonb filter
text_arr group_by
jsonb sort_by
text availability_filter
bigint sync_id
boolean deleted
}
gtd_attachments {
text id PK
text task_id FK
text user_id
text kind
text url
text filename
bigint sync_id
boolean deleted
}
gtd_sync_clocks {
text user_id PK
bigint clock
timestamptz updated_at
}
gtd_sync_mutations {
text user_id PK
text mutation_id PK
bigint sync_id
text status
timestamptz created_at
}
约定摘录:
- 日期列用
timestamptz(defer/due 为业务核心,便于范围查询与按天分组) - 自/互引用 FK:
DEFERRABLE INITIALLY DEFERRED(同事务内可先插后对齐引用) -
sort_orderfloat8 + fractional indexing;不加同级唯一约束(拖拽中间态会冲突);有序性靠应用层 + invariant - Inbox:
gtd_tasksCHECK —(project_id IS NULL AND parent_id IS NULL) OR project_id IS NOT NULL -
repeat_rule内联在 task(无独立规则表) -
next_review_date为普通列,与reviewjsonb 由 mapper 双写(generated column 对 timestamptz 不可行) -
gtd_sync_clocks:push 时FOR UPDATE发号 -
gtd_sync_mutations:幂等,status= applied | rejected
apps/client/src/
├── stores/gtd-store.ts
├── gtd/row-store.ts # IndexedDB
├── gtd/sync-engine.ts
├── apis/gtd-api.ts
├── hooks/useGtd.ts
└── components/gtd/ # Sidebar / TaskList / Inspector / PerspectiveEditor / RepeatEditor
IndexedDB gtd-sync:rows / outbox / meta(lastSyncId)。
-
真相:
rowsAtom;rowStoreAtom = new RowStore(rows)。 -
乐观写:
build → applyPush 同语义 → validateInvariants → persistAndQueue → scheduleSync。 - 状态机:idle / syncing / error / offline;守护 online / visibility / 5min。
| Store 方法 | 产出 |
|---|---|
| patchTask / toggleFlag / setTaskRepeat | task upsert(repeat 含内联 rule) |
| setTaskTags | task_tag upsert / delete |
| completeTask / dropTask | command |
| reorder / indent / outdent | move(+ sibling order upsert) |
| addPerspective / patchPerspective | perspective upsert |
| removeProject / removeTag | delete_* command |
- 行:
task.data.repeatRuleId+task.data.repeatRule(完整规则对象,jsonb 落gtd_tasks.repeat_rule)。 - 配置入口:
GtdRepeatEditor→GtdStore.setTaskRepeat→ task upsert mutation(同时写 id 与内联对象;清除时两者置 null)。 -
anchor === due时任务须已有 dueDate;anchor === defer时须已有 deferDate(Store 内校验)。
| 字段 | 含义 |
|---|---|
| cycle | daily / weekly / monthly / yearly |
| interval | ≥ 1 |
| anchor | completion / due / defer |
| daysOfWeek | weekly 可选;其它周期为空数组 |
| endDate / maxOccurrences | 结束条件 |
| completedOccurrences | 已完成次数(克隆时 ++) |
实现:packages/gtd/src/repeat.ts。
base =
completion → now(完成时刻)
due → 旧 dueDate(缺则 now)
defer → 旧 deferDate(缺则 now)
nextBase = addCycle(base, cycle, interval, daysOfWeek) // UTC,避免时区漂移
gap = 旧 due − 旧 defer(若两者都有)
anchor=defer → 新 defer = nextBase;新 due = nextBase+gap(或保留旧 due)
anchor=due|completion → 新 due = nextBase;新 defer = nextBase−gap(或保留旧 defer)
weekly 且指定 daysOfWeek 时,alignToNextDayOfWeek 对齐到允许的星期。
-
completedOccurrences >= maxOccurrences,或 now > endDate
满足则完成时只把旧任务标 completed,不克隆。
日常同步路径走 applyComplete(packages/gtd/src/sync.ts),Client / Server 同语义:
- 仅
active可 complete;已 completed → noop;其它终态 → reject。 -
willClone = repeatRuleId && repeatRule && !shouldStop。 - 若 willClone:要求
clientGenerated.nextTaskId;占用冲突(异源 id)→ reject;同源软删 → 复活覆盖;同源未删 → 幂等(不建第二实例)。 - 旧任务:
status=completed,completedAt=clientTs;克隆时repeatRule.completedOccurrences++。 - 新任务:复用 nextTaskId,
status=active,新 defer/due,repeatedFromTaskId=旧 id;复制旧实例 livetask_tag。
Client:completeTask 先读行上 rule 预判 willClone,预生成 nextTaskId 再入 outbox——保证双端离线完成时下一实例 id 一致。
flowchart TD
A[complete command] --> B{task active?}
B -->|否| R[noop 或 rejected]
B -->|是| C{willClone?}
C -->|否| D[旧任务 completed]
C -->|是| E{有 nextTaskId?}
E -->|否| R
E -->|是| F[旧任务 completed + occurrences++]
F --> G[克隆新 task + 复制 task_tag]
包内仍保留文档形
applyRepeatOnComplete(state.ts/repeat.ts),供非 sync 路径;多端权威路径以applyComplete为准。
- 行:
entity: perspective,data 含 name / icon / filter / groupBy / sortBy / availabilityFilter / showCompleted / showDropped / flaggedOnly。 - UI:
GtdPerspectiveEditor→validatePerspectiveInput→addPerspective/patchPerspective(upsert mutation)或removePerspective(delete)。 - 侧栏 selection:
resolvePerspective(rowStore, selection)——内置 id、自定义行、或 project/tag/folder 临时过滤透视。
内置 8 个由 builtinPerspectives() 返回(代码内常量,不落库)。它们与用户自定义透视一样,最终都是一份 Perspective 配置,走同一套 renderPerspective。
Inbox(内置)——「未归入项目的顶层活跃任务,按 order」:
管线里还会跑 applyBuiltinFilter('inbox'):额外要求 projectId == null && parentId == null(Inbox 子任务不允许,与不变量一致)。
Projects(内置)——「可执行任务按项目分组」:
{
"id": "projects",
"name": "项目",
"availabilityFilter": "available", // 看 computeStatus,排除 blocked/deferred 等
"filter": null,
"groupBy": ["project"],
"sortBy": [{ "field": "order", "dir": "asc" }]
}无 DSL filter;分组键 project → task.data.projectId,列表按项目拆成 RenderGroup。
侧栏点选某个项目——resolvePerspective 合成临时透视(不落库):
{
"id": "project:<projectId>",
"filter": { "op": "some", "field": "project", "value": ["<projectId>"] },
"availabilityFilter": "remaining",
"groupBy": [],
"sortBy": [{ "field": "order", "dir": "asc" }]
}点选标签 / 文件夹同理:field: "tag" 或 "folder"(folder 经 project.folderId 反查)。
用户自定义透视——与上同形,校验通过后 upsert 成 perspective 行。例如「有旗标且 due 在未来」:
{
"name": "本周旗标",
"filter": {
"op": "and",
"rules": [
{ "op": "is", "field": "flagged", "value": true },
{ "op": "after", "field": "dueDate", "value": "…" }
]
},
"groupBy": ["dueDate"],
"sortBy": [{ "field": "dueDate", "dir": "asc" }],
"availabilityFilter": "remaining",
"showCompleted": false,
"showDropped": false,
"flaggedOnly": null
}flowchart LR
Sel[侧栏 selection] --> R[resolvePerspective]
R -->|id=inbox/projects/…| B[builtinPerspectives]
R -->|kind=project/tag/folder| T[临时 Perspective]
R -->|自定义 id| P[perspective 行]
B --> RP[renderPerspective]
T --> RP
P --> RP
packages/gtd/src/perspective-input.ts + filter/*:
flowchart TD
I[PerspectiveInput] --> Z[Zod 形状]
Z --> N[persist 时名称非空]
N --> U[内置 id 不可覆盖]
U --> D[groupBy / sortBy 无重复字段]
D --> F[validateFilterNode field×op×value]
F --> E[解析 EntityRef / 相对日期]
E --> O[ResolvedPerspectiveSpec]
mode: 'persist' | 'query':持久化更严;一次性查询可允许相对日期 token。
嵌套 JSON 树,用 op 判别逻辑节点与叶子(取值不相交,便于 Zod discriminatedUnion):
type FilterNode =
| { op: 'and' | 'or', children: FilterNode[] }
| { op: 'not', child: FilterNode }
| { field: FilterField, op: LeafOp, value?: unknown }可表达混合逻辑,例如 (旗标 ∧ due 在区间) ∨ (项目 ∈ S ∧ ¬标签 ∈ T)。
顶层开关 不进 DSL:availabilityFilter / showCompleted / showDropped / flaggedOnly 仍由 applyBaseFilter 处理。
结构上限(FILTER_LIMITS):深度 ≤ 5、节点数 ≤ 32。
field × op 矩阵(FILTER_FIELD_OPS,校验 / UI / Prompt 共用):
| 字段 | 允许叶子 op | value 形态 |
|---|---|---|
| status | is / is_not | 标量 status |
| flagged | is / is_not | boolean |
| project / folder / tag | some / empty | some → 实体 id 列表;empty 无 value |
| deferDate / dueDate | before / after / within / exist | 日期或区间;exist 无 value |
| estimate | is / is_not / before / after / within / exist | number 或数值区间 |
求值:packages/gtd/src/filter/engine.ts,上下文 RowStore。
| field | 取值来源 |
|---|---|
| status / project / defer / due / flagged / estimate | task.data |
| folder | 经 project.folderId 反查 |
| tag |
rowStore.tagIdsOf(taskId)(聚合 task_tag) |
and/or 短路;tag 的 some 为与目标集合有交集。
flowchart TD
T[liveTasks + buildTaskTree] --> B[applyBaseFilter]
B --> M[matchFilter DSL]
M --> I[applyBuiltinFilter]
I --> E[expandAncestors]
E --> S[sortBy]
S --> G[groupBy]
G --> O[RenderGroup / RenderItem]
| 步 | 函数 | 行为 |
|---|---|---|
| 1 | applyBaseFilter |
showCompleted / showDropped / flaggedOnly;availabilityFilter = all | remaining | available(后者看 computeStatus) |
| 2 | matchFilter |
用户/内置 filter 树 |
| 3 | applyBuiltinFilter |
按透视 id 追加规则(inbox 无项目;review 看 needsReview;forecast 两周 horizon;predicted 有 due…) |
| 4 | expandAncestors |
命中子任务时补齐祖先,便于树形展示 |
| 5 | sortTasks |
sortBy 多键 |
| 6 | groupBy |
project / tag / due_date 等;tag 可多归属 |
computeStatus(availability.ts)为派生值:终态、未来 defer、祖先/项目阻塞、sequential 前序、再按 due 分 overdue / due_soon / available。
列表组件:GtdTaskList 对当前 resolvePerspective 结果调用 renderPerspective(rowStore, …)。
sequenceDiagram
participant UI as GtdTaskList / Sidebar
participant Store as GtdStore
participant Val as validatePerspectiveInput
participant RP as renderPerspective
UI->>Val: 编辑自定义透视(persist)
Val-->>UI: ResolvedPerspectiveSpec 或 errors
UI->>Store: add/patch perspective upsert
Store-->>UI: rowsAtom 更新
UI->>Store: resolvePerspective(selection)
UI->>RP: renderPerspective(rowStore, perspective, now, dueSoonMs)
RP-->>UI: RenderGroup[]
-
parentId多层子任务;Inbox:projectId与parentId均为 null。 -
groupType:null 叶子;有子任务时为 Action Group。 - 串行:同级
order下仅第一个未完成可执行(可用性 blocked)。 - 树:
buildTaskTree运行时构建。
order.ts:fractional orderBetween;间隙耗尽 reindexSiblings。跨层级用 move command。自动排序透视不提供手动拖拽落点。
validateInvariants(RowStore):引用完整性、Inbox、环、同级 order、终态时间戳等。乐观写前执行。
-
packages/gtd:纯领域(Zod / 纯函数 / Port 类型),不连 Postgres / Redis。 -
apps/server:drizzle schema、sync-repository、HTTP;同步热路径只走 PG。 -
apps/client:IndexedDB + SyncEngine + UI;透视在本地renderPerspective。
同步主路径不用 Redis / 消息队列。提醒、日历推送、重活若以后做,宜挂正交队列(如 BullMQ),改写派生 status 或替代 sync 协议不在本期范围:可用性 / 回顾仍是纯函数派生,不落库。
packages/gtd/src/
├── sync-schema.ts / sync.ts
├── rows.ts / materialize.ts / dematerialize.ts
├── availability.ts / perspective.ts / perspective-input.ts
├── filter/ # DSL schema + engine + 校验
├── order.ts / repeat.ts / tree.ts / invariant.ts
├── review.ts / state.ts / serialize.ts
└── index.ts
apps/server/src/
├── db/schema/gtd.ts
├── gtd/sync-repository.ts
├── handlers/gtd-sync.ts
└── routes/gtd.ts # POST /gtd/sync/push|pull
日常同步入口:上述两个 HTTP 接口(需登录)。
| 层 | 位置 |
|---|---|
| applyPush / complete+repeat | packages/gtd/src/__tests__/sync.test.ts |
| 透视 / 过滤 / 可用性 |
perspective*.test.ts、filter、availability.test.ts
|
| PG e2e | apps/server/src/gtd/sync.e2e.test.ts |
质量门禁:pnpm run lint、pnpm tc;相关测试全绿。
| 议题 | 现行选择 |
|---|---|
| 冲突模型 | 服务端到达序 + patch 列合并 LWW |
| 同步通道 | HTTP push/pull + 定时 |
| 本地真相 | EntityRow + outbox + lastSyncId(IDB) |
| 服务端真相 | Postgres 七表 + clock + 幂等表 |
| 领域包 |
@agent/gtd 纯函数,无 DB/Redis |
| 缓存 / 队列 | 同步热路径不用 Redis;提醒类队列正交、未接 |
| 高风险写 | command(complete/drop/move/级联删) |
| 标签 | task_tag 独立行 |
| 重复规则 | task 行内联 jsonb + complete 克隆 |
| 透视筛选 | FilterNode 嵌套 DSL(深≤5、节点≤32);顶层开关独立 |
| 文档树 | 导入导出边界 |
| 日期列 | timestamptz;FK DEFERRABLE;sort fractional 无同级唯一 |
| 资源 | 说明 |
|---|---|
packages/gtd/src/sync-schema.ts / sync.ts
|
wire 契约与 applyPush / pull |
packages/gtd/src/__tests__/sync.test.ts |
push/pull / command 契约测试 |
packages/gtd/ |
领域纯函数与其它契约测试 |
apps/client/src/gtd/、stores/gtd-store.ts、components/gtd/
|
Client 行库、SyncEngine、UI |
apps/server/src/gtd/、db/schema/gtd.ts、routes/gtd.ts
|
PG 落库与 HTTP |
EntityRow 贯通 Client / wire / Postgres;编辑走 mutation/command + 本地同语义 apply + outbox;服务端 clock 发号、幂等去重、patch LWW;UI 用 RowStore 做可用性与透视;重复靠内联 rule + complete 克隆;自定义透视靠 FilterNode 校验与同一套
renderPerspective。
{ "id": "inbox", "name": "收件箱", "availabilityFilter": "remaining", // 只要 active "filter": { "op": "empty", "field": "project" }, "groupBy": [], "sortBy": [{ "field": "order", "dir": "asc" }], "showCompleted": false, "showDropped": false }