Skip to content
jian edited this page Jul 22, 2026 · 1 revision

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;侧栏可观察同步状态。

1.1 内置透视(8 个)

透视 用途
Inbox 未归入项目的顶层任务
Projects 按项目分组
Tags 按标签分组
Forecast 未来约两周内有 due 的任务,按日期分组
Flagged 已旗标
Review 所属项目需回顾的任务
Completed 已完成,按日期相关分组
Predicted 有截止日期的任务,按 due 分组

另可通过侧栏点选项目 / 文件夹 / 标签,合成临时过滤视图。自定义透视的创建与编辑见侧栏编辑器(§七)。

1.2 任务

能力 说明
创建 Inbox 或选中项目后列表顶栏添加;可向父任务添加子任务
列表 勾选完成/重开、旗标、点击打开 Inspector
层级 缩进 / 出缩进、Action Group(groupType)
Inspector 名称、备注、项目、标签、推迟/截止、重复规则;完成/放弃/恢复/重开/删除

生命周期:

stateDiagram-v2
  [*] --> 活跃
  活跃 --> 已完成: 完成
  已完成 --> 活跃: 重开
  活跃 --> 已放弃: 放弃
  已放弃 --> 活跃: 恢复
  活跃 --> 已删除: 删除
  已删除 --> [*]
Loading

已删除为逻辑删除(status),常规列表默认不展示。 行左侧圆点为可用性派生色:绿 available、琥珀 due soon(默认截止前 2 天)、红 overdue、灰 blocked/终态等。

1.3 项目 / 文件夹 / 标签

  • 项目:侧栏新建;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
Loading

端到端时序(冷启动 → 乐观写 → 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
Loading

硬规则:

  1. outbox 非空只走 push(有 pending 时不单独 pull)。
  2. push body 中 commands 与 mutations 分列;complete / drop / move / 级联删走 command。
  3. LWW 看服务端到达序(clientTs 入账但不裁决);sync_id 只服务增量拉取。
  4. 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。
重复完成克隆见 §六;透视渲染见 §七。


三、统一模型:EntityRow

3.1 形状

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。

3.2 实体与引用

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"
Loading
实体 产品语义 行 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[]。

3.3 Mutation 与 Command

通道 形态 用途
mutation upsert / delete + patch 普通字段;task_tag 增删
command complete / drop / move / delete_* 高风险原子语义

id 为客户端 UUID,作幂等键。applyPush:先 commands,后 mutations;每条独立捕获,违规进 rejected。

3.4 GtdDocument 与 RowStore

运行时真相是 EntityRow[]。materialize / dematerialize 仅导入导出边界。UI 日常吃 RowStore(packages/gtd/src/rows.ts):liveTasks / findLive / tagIdsOf 等。


四、表设计(Postgres)

定义: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
  }
Loading

约定摘录:

  • 日期列用 timestamptz(defer/due 为业务核心,便于范围查询与按天分组)
  • 自/互引用 FK:DEFERRABLE INITIALLY DEFERRED(同事务内可先插后对齐引用)
  • sort_order float8 + fractional indexing;不加同级唯一约束(拖拽中间态会冲突);有序性靠应用层 + invariant
  • Inbox:gtd_tasks CHECK — (project_id IS NULL AND parent_id IS NULL) OR project_id IS NOT NULL
  • repeat_rule 内联在 task(无独立规则表)
  • next_review_date 为普通列,与 review jsonb 由 mapper 双写(generated column 对 timestamptz 不可行)
  • gtd_sync_clocks:push 时 FOR UPDATE 发号
  • gtd_sync_mutations:幂等,status = applied | rejected

五、Client 落地

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

六、重复规则

6.1 数据落点

  • 行: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 已完成次数(克隆时 ++)

6.2 下一期日期:computeNextDates

实现: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 对齐到允许的星期。

6.3 终止:shouldStop

  • completedOccurrences >= maxOccurrences,或
  • now > endDate

满足则完成时只把旧任务标 completed,不克隆。

6.4 完成路径(权威):command complete

日常同步路径走 applyComplete(packages/gtd/src/sync.ts),Client / Server 同语义:

  1. 仅 active 可 complete;已 completed → noop;其它终态 → reject。
  2. willClone = repeatRuleId && repeatRule && !shouldStop。
  3. 若 willClone:要求 clientGenerated.nextTaskId;占用冲突(异源 id)→ reject;同源软删 → 复活覆盖;同源未删 → 幂等(不建第二实例)。
  4. 旧任务:status=completed,completedAt=clientTs;克隆时 repeatRule.completedOccurrences++。
  5. 新任务:复用 nextTaskId,status=active,新 defer/due,repeatedFromTaskId=旧 id;复制旧实例 live task_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]
Loading

包内仍保留文档形 applyRepeatOnComplete(state.ts / repeat.ts),供非 sync 路径;多端权威路径以 applyComplete 为准。


七、自定义透视

7.1 数据与 UI

  • 行: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。

7.2 示例

Inbox(内置)——「未归入项目的顶层活跃任务,按 order」:

{
  "id": "inbox",
  "name": "收件箱",
  "availabilityFilter": "remaining",   // 只要 active
  "filter": { "op": "empty", "field": "project" },
  "groupBy": [],
  "sortBy": [{ "field": "order", "dir": "asc" }],
  "showCompleted": false,
  "showDropped": false
}

管线里还会跑 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
Loading

7.3 入参校验(UI / MCP 共用)

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]
Loading

mode: 'persist' | 'query':持久化更严;一次性查询可允许相对日期 token。

7.4 过滤 DSL

嵌套 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 为与目标集合有交集。

7.5 渲染管线:renderPerspective

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]
Loading
步 函数 行为
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[]
Loading

八、其它领域语义

8.1 任务结构

  • parentId 多层子任务;Inbox:projectId 与 parentId 均为 null。
  • groupType:null 叶子;有子任务时为 Action Group。
  • 串行:同级 order 下仅第一个未完成可执行(可用性 blocked)。
  • 树:buildTaskTree 运行时构建。

8.2 排序与拖拽

order.ts:fractional orderBetween;间隙耗尽 reindexSiblings。跨层级用 move command。自动排序透视不提供手动拖拽落点。

8.3 不变量

validateInvariants(RowStore):引用完整性、Inbox、环、同级 order、终态时间戳等。乐观写前执行。


九、包与服务边界

9.1 分层

  • packages/gtd:纯领域(Zod / 纯函数 / Port 类型),不连 Postgres / Redis。
  • apps/server:drizzle schema、sync-repository、HTTP;同步热路径只走 PG。
  • apps/client:IndexedDB + SyncEngine + UI;透视在本地 renderPerspective。

同步主路径不用 Redis / 消息队列。提醒、日历推送、重活若以后做,宜挂正交队列(如 BullMQ),改写派生 status 或替代 sync 协议不在本期范围:可用性 / 回顾仍是纯函数派生,不落库。

9.2 packages/gtd

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

9.3 Server

apps/server/src/
├── db/schema/gtd.ts
├── gtd/sync-repository.ts
├── handlers/gtd-sync.ts
└── routes/gtd.ts          # POST /gtd/sync/push|pull

日常同步入口:上述两个 HTTP 接口(需登录)。

9.4 测试锚点

层 位置
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。