# `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 | 名称、备注、项目、标签、推迟/截止、重复规则;完成/放弃/恢复/重开/删除 | 生命周期: ```mermaid stateDiagram-v2 [*] --> 活跃 活跃 --> 已完成: 完成 已完成 --> 活跃: 重开 活跃 --> 已放弃: 放弃 已放弃 --> 活跃: 恢复 活跃 --> 已删除: 删除 已删除 --> [*] ``` 已删除为逻辑删除(status),常规列表默认不展示。 行左侧圆点为**可用性**派生色:绿 available、琥珀 due soon(默认截止前 2 天)、红 overdue、灰 blocked/终态等。 ### 1.3 项目 / 文件夹 / 标签 - **项目**:侧栏新建;Inspector 可改类型(并行 / 顺序 / 单动作)、文件夹、暂停/恢复、标记已回顾、删除(级联软删子任务)。新建默认开启每周回顾。 - **文件夹**:侧栏可建树;点击后列表展示该文件夹下各项目任务。 - **标签**:侧栏树 + Inspector pill 切换;关联以 `task_tag` 行同步。 --- ## 二、整体时序 GTD 要稳定回答三件事:**此刻可执行什么**(可用性)、**完成后如何推进**(重复克隆)、**同一批任务如何切片展示**(透视)。底层保证多端改动可合并、可解释。 组件关系: ```mermaid 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): ```mermaid 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 ``` 硬规则: 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 行映射,同构为: ```ts type SyncEntity = | 'task' | 'project' | 'folder' | 'tag' | 'perspective' | 'attachment' | 'task_tag' interface EntityRow { entity: E id: string // task_tag 为 `${taskId}|${tagId}` userId: string syncId: number // 每用户单调;仅增量拉取 deleted: boolean // 软删 tombstone,pull 仍下发 data: EntityDataOf } ``` Zod 单一事实源:`packages/gtd/src/sync-schema.ts`。 ### 3.2 实体与引用 ```mermaid 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[]`。 ### 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)`。 ```mermaid 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_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 一致。 ```mermaid 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` 为准**。 --- ## 七、自定义透视 ### 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」: ```jsonc { "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(内置)**——「可执行任务按项目分组」: ```jsonc { "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` 合成临时透视(不落库): ```jsonc { "id": "project:", "filter": { "op": "some", "field": "project", "value": [""] }, "availabilityFilter": "remaining", "groupBy": [], "sortBy": [{ "field": "order", "dir": "asc" }] } ``` 点选标签 / 文件夹同理:`field: "tag"` 或 `"folder"`(folder 经 project.folderId 反查)。 **用户自定义透视**——与上同形,校验通过后 upsert 成 `perspective` 行。例如「有旗标且 due 在未来」: ```jsonc { "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 } ``` ```mermaid 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 ``` ### 7.3 入参校验(UI / MCP 共用) `packages/gtd/src/perspective-input.ts` + `filter/*`: ```mermaid 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。 ### 7.4 过滤 DSL 嵌套 JSON 树,用 `op` 判别逻辑节点与叶子(取值不相交,便于 Zod discriminatedUnion): ```ts 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` ```mermaid 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, …)`。 ```mermaid 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[] ``` --- ## 八、其它领域语义 ### 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`。