libero3 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/COLD_START.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +43 -0
  4. package/data/migrations/001_initial_schema.sql +43 -0
  5. package/data/migrations/002_seed_categories.sql +18 -0
  6. package/data/migrations/003_add_rating.sql +4 -0
  7. package/data/migrations/004_timer_sessions.sql +21 -0
  8. package/data/migrations/005_profile_settings.sql +5 -0
  9. package/data/migrations/006_add_timeblock_status.sql +1 -0
  10. package/data/migrations/007_reminders.sql +17 -0
  11. package/data/migrations/008_timer_target_ms.sql +5 -0
  12. package/data/migrations/009_tasks.sql +22 -0
  13. package/data/migrations/010_timer_task_id.sql +4 -0
  14. package/data/migrations/011_profile_default_confirmed.sql +4 -0
  15. package/data/migrations/012_identity_tables.sql +68 -0
  16. package/data/migrations/013_identity_managed_by.sql +3 -0
  17. package/data/migrations/014_task_difficulty_priority.sql +6 -0
  18. package/data/migrations/015_timeblock_asked_at.sql +5 -0
  19. package/data/migrations/016_intent_columns.sql +8 -0
  20. package/data/migrations/017_ap_wallet.sql +25 -0
  21. package/data/migrations/018_ap_rewards.sql +21 -0
  22. package/data/migrations/019_ap_bounty.sql +10 -0
  23. package/data/migrations/020_chat_turns.sql +10 -0
  24. package/data/migrations/021_portrait.sql +4 -0
  25. package/data/migrations/022_family_emoji_single.sql +3 -0
  26. package/data/migrations/023_ap_balance_view_log.sql +9 -0
  27. package/data/schema.sql +75 -0
  28. package/dist/data/db.js +83 -0
  29. package/dist/data/migrations/001_initial_schema.sql +43 -0
  30. package/dist/data/migrations/002_seed_categories.sql +18 -0
  31. package/dist/data/migrations/003_add_rating.sql +4 -0
  32. package/dist/data/migrations/004_timer_sessions.sql +21 -0
  33. package/dist/data/migrations/005_profile_settings.sql +5 -0
  34. package/dist/data/migrations/006_add_timeblock_status.sql +1 -0
  35. package/dist/data/migrations/007_reminders.sql +17 -0
  36. package/dist/data/migrations/008_timer_target_ms.sql +5 -0
  37. package/dist/data/migrations/009_tasks.sql +22 -0
  38. package/dist/data/migrations/010_timer_task_id.sql +4 -0
  39. package/dist/data/migrations/011_profile_default_confirmed.sql +4 -0
  40. package/dist/data/migrations/012_identity_tables.sql +68 -0
  41. package/dist/data/migrations/013_identity_managed_by.sql +3 -0
  42. package/dist/data/migrations/014_task_difficulty_priority.sql +6 -0
  43. package/dist/data/migrations/015_timeblock_asked_at.sql +5 -0
  44. package/dist/data/migrations/016_intent_columns.sql +8 -0
  45. package/dist/data/migrations/017_ap_wallet.sql +25 -0
  46. package/dist/data/migrations/018_ap_rewards.sql +21 -0
  47. package/dist/data/migrations/019_ap_bounty.sql +10 -0
  48. package/dist/data/migrations/020_chat_turns.sql +10 -0
  49. package/dist/data/migrations/021_portrait.sql +4 -0
  50. package/dist/data/migrations/022_family_emoji_single.sql +3 -0
  51. package/dist/data/migrations/023_ap_balance_view_log.sql +9 -0
  52. package/dist/data/schema.sql +75 -0
  53. package/dist/knowledge/_registry/dimensions.yaml +10 -0
  54. package/dist/knowledge/_registry/personas.yaml +7 -0
  55. package/dist/knowledge/_schema/analysis-model.schema.js +32 -0
  56. package/dist/knowledge/_schema/entry.schema.js +45 -0
  57. package/dist/knowledge/_schema/persona.schema.js +23 -0
  58. package/dist/knowledge/_schema/scenario-pack.schema.js +29 -0
  59. package/dist/knowledge/analysis-models/review-default.yaml +16 -0
  60. package/dist/knowledge/analysis-models/review-summer-plan.yaml +15 -0
  61. package/dist/knowledge/brain-science/brain-deep-warmup.yaml +22 -0
  62. package/dist/knowledge/brain-science/brain-emotion-granularity.yaml +22 -0
  63. package/dist/knowledge/brain-science/brain-env-engineering.yaml +22 -0
  64. package/dist/knowledge/brain-science/brain-extended-mind.yaml +22 -0
  65. package/dist/knowledge/brain-science/brain-friction-barrier.yaml +22 -0
  66. package/dist/knowledge/brain-science/brain-peak-end.yaml +29 -0
  67. package/dist/knowledge/brain-science/brain-pomodoro.yaml +31 -0
  68. package/dist/knowledge/brain-science/brain-revenge-bedtime.yaml +22 -0
  69. package/dist/knowledge/brain-science/brain-single-variable.yaml +15 -0
  70. package/dist/knowledge/brain-science/brain-tiny-habits.yaml +22 -0
  71. package/dist/knowledge/energy/energy-active-recovery.yaml +22 -0
  72. package/dist/knowledge/energy/energy-deliberate-rest.yaml +22 -0
  73. package/dist/knowledge/energy/energy-exercise-embedded.yaml +22 -0
  74. package/dist/knowledge/energy/energy-fixed-wake-time.yaml +22 -0
  75. package/dist/knowledge/energy/energy-four-energy-radar.yaml +22 -0
  76. package/dist/knowledge/energy/energy-intensity-recovery.yaml +22 -0
  77. package/dist/knowledge/scenario-packs/pack-summer-kid.yaml +12 -0
  78. package/dist/knowledge/src/repo/fs-yaml.js +252 -0
  79. package/dist/knowledge/src/repo/types.js +1 -0
  80. package/dist/knowledge/time/time-batch-windows.yaml +22 -0
  81. package/dist/knowledge/time/time-conscious-abandon.yaml +22 -0
  82. package/dist/knowledge/time/time-deep-work-block.yaml +22 -0
  83. package/dist/knowledge/time/time-finite-commitment.yaml +22 -0
  84. package/dist/knowledge/time/time-forced-whitespace.yaml +22 -0
  85. package/dist/knowledge/time/time-gtd-next-action.yaml +22 -0
  86. package/dist/knowledge/time/time-three-tier-list.yaml +22 -0
  87. package/dist/mcp/cold-start.js +43 -0
  88. package/dist/mcp/contract/v1/rest.js +56 -0
  89. package/dist/mcp/rest-server.js +52 -0
  90. package/dist/mcp/server-core.js +1334 -0
  91. package/dist/mcp/server.js +34 -0
  92. package/dist/mcp/src/ap/index.js +14 -0
  93. package/dist/mcp/src/ap/rewards.js +44 -0
  94. package/dist/mcp/src/categories/index.js +12 -0
  95. package/dist/mcp/src/chat/from-env.js +111 -0
  96. package/dist/mcp/src/chat/history.js +35 -0
  97. package/dist/mcp/src/chat/loop.js +131 -0
  98. package/dist/mcp/src/chat/portrait.js +133 -0
  99. package/dist/mcp/src/chat/system-prompt.js +23 -0
  100. package/dist/mcp/src/chat/tool-names.js +27 -0
  101. package/dist/mcp/src/chat/tool-schema.js +20 -0
  102. package/dist/mcp/src/chat/types.js +1 -0
  103. package/dist/mcp/src/clock/index.js +78 -0
  104. package/dist/mcp/src/coachmod/index.js +135 -0
  105. package/dist/mcp/src/host-adapters/hermes-cron.js +185 -0
  106. package/dist/mcp/src/host-adapters/index.js +61 -0
  107. package/dist/mcp/src/host-adapters/types.js +7 -0
  108. package/dist/mcp/src/identity/equivalent-ids.js +41 -0
  109. package/dist/mcp/src/identity/in-memory.js +100 -0
  110. package/dist/mcp/src/identity/resolve.js +60 -0
  111. package/dist/mcp/src/identity/types.js +4 -0
  112. package/dist/mcp/src/matrix/index.js +15 -0
  113. package/dist/mcp/src/profile/confirm-default.js +48 -0
  114. package/dist/mcp/src/profile/index.js +298 -0
  115. package/dist/mcp/src/profile-confirm.js +28 -0
  116. package/dist/mcp/src/profile-helpers.js +109 -0
  117. package/dist/mcp/src/reminder/hermes-channel.js +40 -0
  118. package/dist/mcp/src/reminder/index.js +70 -0
  119. package/dist/mcp/src/reminder/push-channel.js +10 -0
  120. package/dist/mcp/src/repo/factory.js +34 -0
  121. package/dist/mcp/src/repo/in-memory.js +469 -0
  122. package/dist/mcp/src/repo/sqlite-ap.js +188 -0
  123. package/dist/mcp/src/repo/sqlite-helpers.js +39 -0
  124. package/dist/mcp/src/repo/sqlite-identity.js +129 -0
  125. package/dist/mcp/src/repo/sqlite.js +754 -0
  126. package/dist/mcp/src/repo/types.js +4 -0
  127. package/dist/mcp/src/rest/app.js +121 -0
  128. package/dist/mcp/src/rest/invoke-tool.js +98 -0
  129. package/dist/mcp/src/rest/short-token.js +38 -0
  130. package/dist/mcp/src/rest/tokens.js +55 -0
  131. package/dist/mcp/src/scheduler/index.js +319 -0
  132. package/dist/mcp/src/stats/index.js +29 -0
  133. package/dist/mcp/src/stickiness/report.js +174 -0
  134. package/dist/mcp/src/stickiness/types.js +1 -0
  135. package/dist/mcp/src/task/index.js +412 -0
  136. package/dist/mcp/src/timeblock/index.js +206 -0
  137. package/dist/mcp/src/timeline/index.js +705 -0
  138. package/dist/mcp/src/timer/index.js +234 -0
  139. package/dist/mcp/src/user-id-resolver.js +30 -0
  140. package/dist/mcp/src/weixin/app.js +97 -0
  141. package/dist/mcp/src/weixin/custom-send.js +82 -0
  142. package/dist/mcp/src/weixin/handle-text.js +49 -0
  143. package/dist/mcp/src/weixin/reminder-loop.js +39 -0
  144. package/dist/mcp/src/weixin/signature.js +13 -0
  145. package/dist/mcp/src/weixin/tencent-asr.js +101 -0
  146. package/dist/mcp/src/weixin/xml.js +37 -0
  147. package/dist/mcp/weixin-server.js +158 -0
  148. package/knowledge/_registry/dimensions.yaml +10 -0
  149. package/knowledge/_registry/personas.yaml +7 -0
  150. package/knowledge/_schema/analysis-model.schema.ts +36 -0
  151. package/knowledge/_schema/entry.schema.ts +50 -0
  152. package/knowledge/_schema/persona.schema.ts +30 -0
  153. package/knowledge/_schema/scenario-pack.schema.ts +33 -0
  154. package/knowledge/analysis-models/review-default.yaml +16 -0
  155. package/knowledge/analysis-models/review-summer-plan.yaml +15 -0
  156. package/knowledge/brain-science/brain-deep-warmup.yaml +22 -0
  157. package/knowledge/brain-science/brain-emotion-granularity.yaml +22 -0
  158. package/knowledge/brain-science/brain-env-engineering.yaml +22 -0
  159. package/knowledge/brain-science/brain-extended-mind.yaml +22 -0
  160. package/knowledge/brain-science/brain-friction-barrier.yaml +22 -0
  161. package/knowledge/brain-science/brain-peak-end.yaml +29 -0
  162. package/knowledge/brain-science/brain-pomodoro.yaml +31 -0
  163. package/knowledge/brain-science/brain-revenge-bedtime.yaml +22 -0
  164. package/knowledge/brain-science/brain-single-variable.yaml +15 -0
  165. package/knowledge/brain-science/brain-tiny-habits.yaml +22 -0
  166. package/knowledge/energy/energy-active-recovery.yaml +22 -0
  167. package/knowledge/energy/energy-deliberate-rest.yaml +22 -0
  168. package/knowledge/energy/energy-exercise-embedded.yaml +22 -0
  169. package/knowledge/energy/energy-fixed-wake-time.yaml +22 -0
  170. package/knowledge/energy/energy-four-energy-radar.yaml +22 -0
  171. package/knowledge/energy/energy-intensity-recovery.yaml +22 -0
  172. package/knowledge/scenario-packs/pack-summer-kid.yaml +12 -0
  173. package/knowledge/src/repo/fs-yaml.ts +282 -0
  174. package/knowledge/src/repo/types.ts +24 -0
  175. package/knowledge/tests/fixtures/base/_registry/dimensions.yaml +4 -0
  176. package/knowledge/tests/fixtures/base/time/valid-entry.yaml +24 -0
  177. package/knowledge/tests/fixtures/dup/_registry/dimensions.yaml +4 -0
  178. package/knowledge/tests/fixtures/dup/time/entry-a.yaml +13 -0
  179. package/knowledge/tests/fixtures/dup/time/entry-b.yaml +13 -0
  180. package/knowledge/tests/fixtures/unregistered/_registry/dimensions.yaml +7 -0
  181. package/knowledge/tests/fixtures/unregistered/time/anxiety-entry.yaml +13 -0
  182. package/knowledge/tests/repo/fs-yaml-knowledge-repo.test.ts +333 -0
  183. package/knowledge/time/time-batch-windows.yaml +22 -0
  184. package/knowledge/time/time-conscious-abandon.yaml +22 -0
  185. package/knowledge/time/time-deep-work-block.yaml +22 -0
  186. package/knowledge/time/time-finite-commitment.yaml +22 -0
  187. package/knowledge/time/time-forced-whitespace.yaml +22 -0
  188. package/knowledge/time/time-gtd-next-action.yaml +22 -0
  189. package/knowledge/time/time-three-tier-list.yaml +22 -0
  190. package/knowledge/validate.ts +50 -0
  191. package/package.json +76 -0
  192. package/scripts/postinstall-cold-start.js +45 -0
@@ -0,0 +1,174 @@
1
+ /** 「真实动作」日:reward 行 或 completed timeblock(COALESCE(updated_at, created_at) 取日)。 */
2
+ export function activeDaysByUser(db, from, to) {
3
+ const out = new Map();
4
+ const add = (user, day) => {
5
+ let s = out.get(user);
6
+ if (!s) {
7
+ s = new Set();
8
+ out.set(user, s);
9
+ }
10
+ s.add(day);
11
+ };
12
+ const rewards = db
13
+ .prepare("SELECT user_id, substr(created_at,1,10) AS d FROM ap_reward_log WHERE created_at >= ? AND created_at < ?")
14
+ .all(from, to);
15
+ for (const r of rewards)
16
+ add(r.user_id, r.d);
17
+ const blocks = db
18
+ .prepare(`SELECT user_id, substr(COALESCE(updated_at, created_at),1,10) AS d
19
+ FROM time_blocks
20
+ WHERE status='completed' AND deleted_at IS NULL
21
+ AND COALESCE(updated_at, created_at) >= ? AND COALESCE(updated_at, created_at) < ?`)
22
+ .all(from, to);
23
+ for (const r of blocks)
24
+ add(r.user_id, r.d);
25
+ return out;
26
+ }
27
+ /** UTC 日期字符串加减 n 天(yyyy-mm-dd → yyyy-mm-dd) */
28
+ export function addUtcDays(day, n) {
29
+ const t = Date.parse(`${day}T00:00:00.000Z`);
30
+ return new Date(t + n * 86_400_000).toISOString().slice(0, 10);
31
+ }
32
+ /** 每用户「真实动作」时间戳(ms,升序):reward 行 UNION completed timeblock。 */
33
+ export function activityTimestamps(db, from, to) {
34
+ const out = new Map();
35
+ const push = (user, at) => {
36
+ const t = Date.parse(at);
37
+ if (Number.isNaN(t))
38
+ return;
39
+ const arr = out.get(user) ?? [];
40
+ arr.push(t);
41
+ out.set(user, arr);
42
+ };
43
+ const rewards = db
44
+ .prepare("SELECT user_id, created_at FROM ap_reward_log WHERE created_at >= ? AND created_at < ?")
45
+ .all(from, to);
46
+ for (const r of rewards)
47
+ push(r.user_id, r.created_at);
48
+ const blocks = db
49
+ .prepare(`SELECT user_id, COALESCE(updated_at, created_at) AS at FROM time_blocks
50
+ WHERE status='completed' AND deleted_at IS NULL
51
+ AND COALESCE(updated_at, created_at) >= ? AND COALESCE(updated_at, created_at) < ?`)
52
+ .all(from, to);
53
+ for (const r of blocks)
54
+ push(r.user_id, r.at);
55
+ for (const arr of out.values())
56
+ arr.sort((x, y) => x - y);
57
+ return out;
58
+ }
59
+ /** trigger 事件(reward/spend/balance_view)后 24h 内是否有真实动作;分母为空 → null */
60
+ function pullRateAfter(triggers, activity) {
61
+ if (triggers.length === 0)
62
+ return null;
63
+ let hit = 0;
64
+ for (const tr of triggers) {
65
+ const ts = activity.get(tr.user_id);
66
+ if (!ts)
67
+ continue;
68
+ // 找是否存在 t 满足 trigger < t <= trigger+24h(ts 升序,线性找第一个 > trigger 即可)
69
+ for (const t of ts) {
70
+ if (t > tr.at) {
71
+ if (t - tr.at <= 86_400_000)
72
+ hit++;
73
+ break;
74
+ }
75
+ }
76
+ }
77
+ return hit / triggers.length;
78
+ }
79
+ /** Pearson 相关;样本 <3 或方差 0 → null */
80
+ function pearson(xs, ys) {
81
+ const n = xs.length;
82
+ if (n < 3)
83
+ return null;
84
+ const mx = xs.reduce((a, b) => a + b, 0) / n;
85
+ const my = ys.reduce((a, b) => a + b, 0) / n;
86
+ let cov = 0;
87
+ let vx = 0;
88
+ let vy = 0;
89
+ for (let i = 0; i < n; i++) {
90
+ const dx = xs[i] - mx;
91
+ const dy = ys[i] - my;
92
+ cov += dx * dy;
93
+ vx += dx * dx;
94
+ vy += dy * dy;
95
+ }
96
+ if (vx === 0 || vy === 0)
97
+ return null;
98
+ return cov / Math.sqrt(vx * vy);
99
+ }
100
+ export function buildStickinessReport(db, window) {
101
+ const { fromIso: from, toIso: to } = window;
102
+ const daysByUser = activeDaysByUser(db, from, to);
103
+ const users = daysByUser.size;
104
+ // D1/D3/D7 cohort 留存:以首次真实动作日为 day0,day0+n 是否仍活跃。
105
+ // 分母=有 day0 且 day0+n 仍在窗口 to 之前的用户(观察窗够长)。
106
+ const toDay = to.slice(0, 10);
107
+ const retentionDays = [1, 3, 7];
108
+ const cohortSizes = { d1: 0, d3: 0, d7: 0 };
109
+ const hitCounts = { d1: 0, d3: 0, d7: 0 };
110
+ for (const days of daysByUser.values()) {
111
+ const first = [...days].sort()[0];
112
+ for (const n of retentionDays) {
113
+ const target = addUtcDays(first, n);
114
+ if (target >= toDay)
115
+ continue; // 观察窗不够长,不进分母
116
+ cohortSizes[`d${n}`]++;
117
+ if (days.has(target))
118
+ hitCounts[`d${n}`]++;
119
+ }
120
+ }
121
+ const rate = (n) => cohortSizes[`d${n}`] === 0 ? null : hitCounts[`d${n}`] / cohortSizes[`d${n}`];
122
+ // 7 日内 ≥3 天有动作占比:活跃天数 ≥3 的用户 / 活跃天数 ≥1 的用户
123
+ let active3plus = 0;
124
+ for (const days of daysByUser.values())
125
+ if (days.size >= 3)
126
+ active3plus++;
127
+ const active_3plus_share = users === 0 ? null : active3plus / users;
128
+ // 游戏拉动:回流 / 出击后动作 / 看余额后动作(个体前后,无对照组)
129
+ const activity = activityTimestamps(db, from, to);
130
+ const parseTs = (rows) => rows
131
+ .map((r) => ({ user_id: r.user_id, at: Date.parse(r.at) }))
132
+ .filter((r) => !Number.isNaN(r.at));
133
+ const rewardTriggers = parseTs(db
134
+ .prepare("SELECT user_id, created_at AS at FROM ap_reward_log WHERE created_at >= ? AND created_at < ?")
135
+ .all(from, to));
136
+ const spendTriggers = parseTs(db
137
+ .prepare("SELECT user_id, created_at AS at FROM ap_spend_log WHERE created_at >= ? AND created_at < ?")
138
+ .all(from, to));
139
+ const viewTriggers = parseTs(db
140
+ .prepare("SELECT user_id, created_at AS at FROM ap_balance_view_log WHERE created_at >= ? AND created_at < ?")
141
+ .all(from, to));
142
+ // 个体相关:每用户 reward_count vs spend_count(样本 ≥3 用户才算)
143
+ const rewardCountByUser = new Map();
144
+ for (const t of rewardTriggers)
145
+ rewardCountByUser.set(t.user_id, (rewardCountByUser.get(t.user_id) ?? 0) + 1);
146
+ const spendCountByUser = new Map();
147
+ for (const t of spendTriggers)
148
+ spendCountByUser.set(t.user_id, (spendCountByUser.get(t.user_id) ?? 0) + 1);
149
+ const userIds = new Set([...rewardCountByUser.keys(), ...spendCountByUser.keys()]);
150
+ const xs = [];
151
+ const ys = [];
152
+ for (const u of userIds) {
153
+ xs.push(rewardCountByUser.get(u) ?? 0);
154
+ ys.push(spendCountByUser.get(u) ?? 0);
155
+ }
156
+ const correlation_reward_vs_spend = pearson(xs, ys);
157
+ return {
158
+ window: { from, to },
159
+ users,
160
+ retention: {
161
+ d1: rate(1),
162
+ d3: rate(3),
163
+ d7: rate(7),
164
+ cohort_sizes: cohortSizes,
165
+ },
166
+ active_3plus_share,
167
+ pull: {
168
+ after_reward_24h: pullRateAfter(rewardTriggers, activity),
169
+ after_spend_24h: pullRateAfter(spendTriggers, activity),
170
+ after_balance_view_24h: pullRateAfter(viewTriggers, activity),
171
+ },
172
+ correlation_reward_vs_spend,
173
+ };
174
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,412 @@
1
+ import { assertProfileConfirmed, } from "../profile-confirm.js";
2
+ import { lookupTargetUserLabel } from "../profile-helpers.js";
3
+ // ── Status machine ──
4
+ const VALID_TRANSITIONS = {
5
+ draft: ["aligned", "executing", "abandoned"],
6
+ aligned: ["executing", "abandoned"],
7
+ executing: ["done", "abandoned"],
8
+ done: [],
9
+ abandoned: [],
10
+ };
11
+ function isValidTransition(from, to) {
12
+ return VALID_TRANSITIONS[from]?.includes(to) ?? false;
13
+ }
14
+ // ── Helpers ──
15
+ function nowStr() {
16
+ // High-resolution timestamp — mirrors sqlite.ts line 173-178.
17
+ // Appends performance.now() micro-digits so consecutive calls within the
18
+ // same millisecond still produce distinct strings.
19
+ return (new Date().toISOString().replace("T", " ").replace("Z", "") +
20
+ String(Math.floor(performance.now() * 100000) % 100000).padStart(5, "0"));
21
+ }
22
+ // S46: difficulty/priority 归一化(不合法值 → medium)
23
+ function normalizeDiff(v) {
24
+ return v === "easy" || v === "medium" || v === "hard" ? v : "medium";
25
+ }
26
+ function normalizePrio(v) {
27
+ return v === "high" || v === "medium" || v === "low" ? v : "medium";
28
+ }
29
+ function rowToTask(row) {
30
+ return {
31
+ id: row.id,
32
+ user_id: row.user_id,
33
+ parent_id: row.parent_id ?? null,
34
+ title: row.title,
35
+ status: row.status,
36
+ est_minutes: row.est_minutes ?? null,
37
+ category_id: row.category_id ?? null,
38
+ linked_timeblock_id: row.linked_timeblock_id ?? null,
39
+ order_index: row.order_index ?? 0,
40
+ // S46: difficulty/priority(migration 014 加的列;旧库未 migrate 时回退 medium)
41
+ difficulty: row.difficulty ?? "medium",
42
+ priority: row.priority ?? "medium",
43
+ created_at: row.created_at,
44
+ updated_at: row.updated_at,
45
+ deleted_at: row.deleted_at ?? null,
46
+ };
47
+ }
48
+ // ── Factory ──
49
+ export function createTaskTools(db) {
50
+ // -- Draft plan store(进程内 in-memory;per-MCP-process)--
51
+ // draftPlan 写这里,commitPlan 读 + 删。进程重启会丢——可接受:
52
+ // draft 本就是临时方案,用户重提即可。
53
+ const draftPlans = new Map();
54
+ // -- Decompose.split plan store(S33 v3,同型 per-MCP-process Map)--
55
+ // decompose.split 第一次调用写入,第二次(confirm_token)读 + 删。
56
+ const decomposePlans = new Map();
57
+ // -- Internal helpers --
58
+ function getTaskRow(id) {
59
+ return db
60
+ .prepare("SELECT * FROM tasks WHERE id = ? AND deleted_at IS NULL")
61
+ .get(id);
62
+ }
63
+ function buildSubtree(parentId) {
64
+ const rows = db
65
+ .prepare("SELECT * FROM tasks WHERE parent_id = ? AND deleted_at IS NULL ORDER BY order_index, created_at")
66
+ .all(parentId);
67
+ return rows.map((row) => {
68
+ const task = rowToTask(row);
69
+ return {
70
+ ...task,
71
+ children: buildSubtree(task.id),
72
+ };
73
+ });
74
+ }
75
+ return {
76
+ async createTask(input) {
77
+ // Validate title
78
+ if (!input.title || input.title.trim() === "") {
79
+ throw new Error("任务名不能为空");
80
+ }
81
+ const userId = input.target_user_id ?? input.user_id;
82
+ const gate = assertProfileConfirmed(db, userId);
83
+ if (gate)
84
+ return gate;
85
+ // Validate parent exists (if provided)
86
+ if (input.parent_id !== undefined) {
87
+ const parent = getTaskRow(input.parent_id);
88
+ if (!parent) {
89
+ throw new Error("父任务不存在");
90
+ }
91
+ }
92
+ const id = crypto.randomUUID();
93
+ const now = nowStr();
94
+ // S46: difficulty/priority 默认 medium,校验入参合法值
95
+ const difficulty = normalizeDiff(input.difficulty);
96
+ const priority = normalizePrio(input.priority);
97
+ // S46 fix: 同 parent 下 order_index 单调递增(避免 created_at 精度问题导致排序 flaky)
98
+ const parentFilter = input.parent_id ? "AND parent_id = ?" : "AND parent_id IS NULL";
99
+ const parentParams = input.parent_id ? [input.parent_id] : [];
100
+ const maxOrderRow = db
101
+ .prepare(`SELECT COALESCE(MAX(order_index), -1) as max_idx FROM tasks WHERE user_id = ? ${parentFilter} AND deleted_at IS NULL`)
102
+ .get(userId, ...parentParams);
103
+ const orderIndex = maxOrderRow.max_idx + 1;
104
+ db.prepare(`INSERT INTO tasks (id, user_id, parent_id, title, status, est_minutes, category_id, linked_timeblock_id, order_index, difficulty, priority, created_at, updated_at, deleted_at)
105
+ VALUES (?, ?, ?, ?, 'draft', ?, NULL, NULL, ?, ?, ?, ?, ?, NULL)`).run(id, userId, input.parent_id ?? null, input.title, input.est_minutes ?? null, orderIndex, difficulty, priority, now, now);
106
+ const row = getTaskRow(id);
107
+ return {
108
+ ...rowToTask(row),
109
+ target_user_label: lookupTargetUserLabel(db, userId),
110
+ };
111
+ },
112
+ async getTask(id) {
113
+ const row = getTaskRow(id);
114
+ if (!row)
115
+ return null;
116
+ const task = rowToTask(row);
117
+ return {
118
+ ...task,
119
+ children: buildSubtree(task.id),
120
+ };
121
+ },
122
+ async listTasks(filters) {
123
+ const userId = filters.target_user_id ?? filters.user_id;
124
+ let sql = "SELECT * FROM tasks WHERE user_id = ? AND deleted_at IS NULL";
125
+ const params = [userId];
126
+ if (filters.parent_id === null) {
127
+ sql += " AND parent_id IS NULL";
128
+ }
129
+ else if (filters.parent_id !== undefined) {
130
+ sql += " AND parent_id = ?";
131
+ params.push(filters.parent_id);
132
+ }
133
+ sql += " ORDER BY order_index, created_at";
134
+ const rows = db.prepare(sql).all(...params);
135
+ return rows.map(rowToTask);
136
+ },
137
+ async updateTask(id, patch) {
138
+ // Check existence (exclude soft-deleted)
139
+ const existing = getTaskRow(id);
140
+ if (!existing) {
141
+ throw new Error("任务不存在");
142
+ }
143
+ // Status transition validation
144
+ if (patch.status !== undefined) {
145
+ const currentStatus = existing.status;
146
+ if (!isValidTransition(currentStatus, patch.status)) {
147
+ throw new Error("非法状态流转");
148
+ }
149
+ }
150
+ // Build SET clauses
151
+ const sets = [];
152
+ const params = [];
153
+ if (patch.title !== undefined) {
154
+ sets.push("title = ?");
155
+ params.push(patch.title);
156
+ }
157
+ if (patch.est_minutes !== undefined) {
158
+ sets.push("est_minutes = ?");
159
+ params.push(patch.est_minutes);
160
+ }
161
+ if (patch.status !== undefined) {
162
+ sets.push("status = ?");
163
+ params.push(patch.status);
164
+ }
165
+ // S46: difficulty/priority 可独立更新
166
+ if (patch.difficulty !== undefined) {
167
+ sets.push("difficulty = ?");
168
+ params.push(normalizeDiff(patch.difficulty));
169
+ }
170
+ if (patch.priority !== undefined) {
171
+ sets.push("priority = ?");
172
+ params.push(normalizePrio(patch.priority));
173
+ }
174
+ // Always refresh updated_at
175
+ sets.push("updated_at = ?");
176
+ params.push(nowStr());
177
+ params.push(id);
178
+ db.prepare(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`).run(...params);
179
+ const row = getTaskRow(id);
180
+ return rowToTask(row);
181
+ },
182
+ async linkTimeblock(taskId, blockId) {
183
+ // Check task exists (exclude soft-deleted)
184
+ const task = getTaskRow(taskId);
185
+ if (!task) {
186
+ throw new Error("任务不存在");
187
+ }
188
+ // Check time_block exists
189
+ const block = db
190
+ .prepare("SELECT id FROM time_blocks WHERE id = ?")
191
+ .get(blockId);
192
+ if (!block) {
193
+ throw new Error("时间块不存在");
194
+ }
195
+ const now = nowStr();
196
+ db.prepare("UPDATE tasks SET linked_timeblock_id = ?, updated_at = ? WHERE id = ?").run(blockId, now, taskId);
197
+ const row = getTaskRow(taskId);
198
+ return rowToTask(row);
199
+ },
200
+ /** S46: 仅更新 difficulty/priority(用户主动调整) */
201
+ async updateDifficulty(taskId, patch) {
202
+ const existing = getTaskRow(taskId);
203
+ if (!existing) {
204
+ throw new Error("任务不存在");
205
+ }
206
+ if (patch.difficulty === undefined && patch.priority === undefined) {
207
+ throw new Error("至少传一个字段 difficulty 或 priority");
208
+ }
209
+ const sets = [];
210
+ const params = [];
211
+ if (patch.difficulty !== undefined) {
212
+ sets.push("difficulty = ?");
213
+ params.push(normalizeDiff(patch.difficulty));
214
+ }
215
+ if (patch.priority !== undefined) {
216
+ sets.push("priority = ?");
217
+ params.push(normalizePrio(patch.priority));
218
+ }
219
+ sets.push("updated_at = ?");
220
+ params.push(nowStr());
221
+ params.push(taskId);
222
+ db.prepare(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`).run(...params);
223
+ const row = getTaskRow(taskId);
224
+ return rowToTask(row);
225
+ },
226
+ async findPlan(input) {
227
+ // exact-match a startable root task (aligned/executing) by title
228
+ const rootRow = db
229
+ .prepare("SELECT * FROM tasks WHERE user_id = ? AND parent_id IS NULL AND title = ? AND status IN ('aligned','executing') AND deleted_at IS NULL LIMIT 1")
230
+ .get(input.user_id, input.title);
231
+ if (!rootRow)
232
+ return null;
233
+ // first undone subtask (skip done/abandoned)
234
+ const subRow = db
235
+ .prepare("SELECT * FROM tasks WHERE parent_id = ? AND status NOT IN ('done','abandoned') AND deleted_at IS NULL ORDER BY order_index, created_at LIMIT 1")
236
+ .get(rootRow.id);
237
+ if (!subRow)
238
+ return null;
239
+ return { root: rowToTask(rootRow), nextSubtask: rowToTask(subRow) };
240
+ },
241
+ // ── 拆解"先对齐再落地"契约(事一 / task-split-align-before-commit)──
242
+ // draftPlan 出方案不写 DB;commitPlan 用户确认后原子写根(aligned)+子(draft)。
243
+ // 现有 createTask 不动(向后兼容);decompose SKILL 第四步改走 draft→STOP→commit。
244
+ async draftPlan(input) {
245
+ if (!input.root_title || input.root_title.trim() === "") {
246
+ throw new Error("根任务名不能为空");
247
+ }
248
+ const user = db
249
+ .prepare("SELECT id FROM user_profile WHERE id = ?")
250
+ .get(input.user_id);
251
+ if (!user) {
252
+ throw new Error("用户不存在");
253
+ }
254
+ if (!input.children || input.children.length === 0) {
255
+ throw new Error("子任务不能为空");
256
+ }
257
+ for (const c of input.children) {
258
+ if (!c.title || c.title.trim() === "") {
259
+ throw new Error("子任务名不能为空");
260
+ }
261
+ }
262
+ const plan_id = crypto.randomUUID();
263
+ const plan = {
264
+ plan_id,
265
+ user_id: input.user_id,
266
+ root_title: input.root_title,
267
+ children: input.children.map((c) => ({
268
+ title: c.title,
269
+ est_minutes: c.est_minutes ?? null,
270
+ })),
271
+ created_at: nowStr(),
272
+ };
273
+ draftPlans.set(plan_id, plan);
274
+ const totalMin = plan.children.reduce((s, c) => s + (c.est_minutes ?? 0), 0);
275
+ const lines = plan.children.map((c, i) => `${i + 1}. ${c.title}${c.est_minutes ? ` — ${c.est_minutes} 分钟` : ""}`);
276
+ const summary = `📝 待确认方案:「${plan.root_title}」(${plan.children.length} 个子任务` +
277
+ `${totalMin ? `,共 ${totalMin} 分钟` : ""})\n` +
278
+ `${lines.join("\n")}\n\n请确认,或告诉我怎么调整。`;
279
+ return {
280
+ plan_id,
281
+ root_title: plan.root_title,
282
+ children: plan.children,
283
+ summary,
284
+ };
285
+ },
286
+ async commitPlan(input) {
287
+ const plan = draftPlans.get(input.plan_id);
288
+ if (!plan) {
289
+ throw new Error("方案不存在或已落地(plan_id 无效)");
290
+ }
291
+ if (plan.user_id !== input.user_id) {
292
+ throw new Error("方案不属于该用户");
293
+ }
294
+ // 原子写:根(aligned = 已确认) + N 子(draft = 待开工),单个事务包裹,
295
+ // 要么全落要么全不落。
296
+ const result = db.transaction(() => {
297
+ const rootId = crypto.randomUUID();
298
+ const now = nowStr();
299
+ db.prepare(`INSERT INTO tasks (id, user_id, parent_id, title, status, est_minutes, category_id, linked_timeblock_id, order_index, difficulty, priority, created_at, updated_at, deleted_at)
300
+ VALUES (?, ?, NULL, ?, 'aligned', NULL, NULL, NULL, 0, 'medium', 'medium', ?, ?, NULL)`).run(rootId, plan.user_id, plan.root_title, now, now);
301
+ const children = plan.children.map((c, i) => {
302
+ const childId = crypto.randomUUID();
303
+ const cnow = nowStr();
304
+ db.prepare(`INSERT INTO tasks (id, user_id, parent_id, title, status, est_minutes, category_id, linked_timeblock_id, order_index, difficulty, priority, created_at, updated_at, deleted_at)
305
+ VALUES (?, ?, ?, ?, 'draft', ?, NULL, NULL, ?, 'medium', 'medium', ?, ?, NULL)`).run(childId, plan.user_id, rootId, c.title, c.est_minutes, i, cnow, cnow);
306
+ return rowToTask(getTaskRow(childId));
307
+ });
308
+ return { root: rowToTask(getTaskRow(rootId)), children };
309
+ })();
310
+ // 一次性:已确认的方案不能重复提交。
311
+ draftPlans.delete(input.plan_id);
312
+ return result;
313
+ },
314
+ // ── S33 v3:decompose.split —— 拆解唯一入口 ──
315
+ // 状态机:第一次(无 confirm_token)出方案不写库;第二次(带 confirm_token)原子写
316
+ // root(aligned, 挂 timeblock) + children(draft, 挂 root)。
317
+ // 错误用 DecomposeErrorResult 返回(不 throw),让 MCP 层包 isError content——避 hermes 熔断。
318
+ async splitDecompose(input) {
319
+ // 通用校验:user 必须存在
320
+ const user = db
321
+ .prepare("SELECT id FROM user_profile WHERE id = ?")
322
+ .get(input.user_id);
323
+ if (!user) {
324
+ return { ok: false, error: "用户不存在" };
325
+ }
326
+ // 校验 children(zod 已 min(1),但这里也兜底)
327
+ if (!input.children || input.children.length === 0) {
328
+ return { ok: false, error: "子任务不能为空" };
329
+ }
330
+ for (const c of input.children) {
331
+ if (!c.title || c.title.trim() === "") {
332
+ return { ok: false, error: "子任务名不能为空" };
333
+ }
334
+ }
335
+ // 校验 timeblock 存在 + 派生 root_title
336
+ const tb = db
337
+ .prepare("SELECT id, user_id, title FROM time_blocks WHERE id = ? AND deleted_at IS NULL")
338
+ .get(input.timeblock_id);
339
+ if (!tb) {
340
+ return { ok: false, error: "时间块不存在" };
341
+ }
342
+ const rootTitle = tb.title;
343
+ // ── 分支 A:第一次调用(无 confirm_token)→ 出方案不写库 ──
344
+ if (!input.confirm_token) {
345
+ const plan_id = crypto.randomUUID();
346
+ const plan = {
347
+ plan_id,
348
+ user_id: input.user_id,
349
+ timeblock_id: input.timeblock_id,
350
+ root_title: rootTitle,
351
+ children: input.children.map((c) => ({
352
+ title: c.title,
353
+ est_minutes: c.est_minutes ?? null,
354
+ })),
355
+ created_at: nowStr(),
356
+ };
357
+ decomposePlans.set(plan_id, plan);
358
+ const totalMin = plan.children.reduce((s, c) => s + (c.est_minutes ?? 0), 0);
359
+ const lines = plan.children.map((c, i) => `${i + 1}. ${c.title}${c.est_minutes ? ` — ${c.est_minutes} 分钟` : ""}`);
360
+ const summary = `📝 待确认方案:「${plan.root_title}」(${plan.children.length} 个子任务` +
361
+ `${totalMin ? `,共 ${totalMin} 分钟` : ""})\n` +
362
+ `${lines.join("\n")}\n\n请确认,或告诉我怎么调整。`;
363
+ return {
364
+ plan_id,
365
+ status: "awaiting_confirmation",
366
+ summary,
367
+ next_step: "请把方案呈现给用户,等用户明确确认后调本工具并带 confirm_token(=返回的 plan_id)落地",
368
+ };
369
+ }
370
+ // ── 分支 B:第二次调用(带 confirm_token)→ 落地 ──
371
+ const plan = decomposePlans.get(input.confirm_token);
372
+ if (!plan) {
373
+ return {
374
+ ok: false,
375
+ error: "方案不存在或已落地(confirm_token 无效或已被消费)",
376
+ };
377
+ }
378
+ if (plan.user_id !== input.user_id) {
379
+ return { ok: false, error: "方案不属于该用户" };
380
+ }
381
+ if (plan.timeblock_id !== input.timeblock_id) {
382
+ return {
383
+ ok: false,
384
+ error: "timeblock_id 与方案不匹配(拆解方案绑定到该 timeblock)",
385
+ };
386
+ }
387
+ // 原子写:root(aligned, 挂 timeblock) + N 子(draft, 挂 root)。
388
+ // 复用 commitPlan 同型事务模式(db.transaction 包裹)。
389
+ const result = db.transaction(() => {
390
+ const rootId = crypto.randomUUID();
391
+ const now = nowStr();
392
+ db.prepare(`INSERT INTO tasks (id, user_id, parent_id, title, status, est_minutes, category_id, linked_timeblock_id, order_index, difficulty, priority, created_at, updated_at, deleted_at)
393
+ VALUES (?, ?, NULL, ?, 'aligned', NULL, NULL, ?, 0, 'medium', 'medium', ?, ?, NULL)`).run(rootId, plan.user_id, plan.root_title, plan.timeblock_id, now, now);
394
+ const children = plan.children.map((c, i) => {
395
+ const childId = crypto.randomUUID();
396
+ const cnow = nowStr();
397
+ db.prepare(`INSERT INTO tasks (id, user_id, parent_id, title, status, est_minutes, category_id, linked_timeblock_id, order_index, difficulty, priority, created_at, updated_at, deleted_at)
398
+ VALUES (?, ?, ?, ?, 'draft', ?, NULL, NULL, ?, 'medium', 'medium', ?, ?, NULL)`).run(childId, plan.user_id, rootId, c.title, c.est_minutes, i, cnow, cnow);
399
+ return {
400
+ id: childId,
401
+ title: c.title,
402
+ est_minutes: c.est_minutes,
403
+ };
404
+ });
405
+ return { root_task_id: rootId, children };
406
+ })();
407
+ // 一次性:已消费的 token 不能再用
408
+ decomposePlans.delete(input.confirm_token);
409
+ return { status: "committed", ...result };
410
+ },
411
+ };
412
+ }