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,22 @@
1
+ id: energy-exercise-embedded
2
+ kind: prescription
3
+ dimension: energy
4
+ title: 运动嵌入日程
5
+ triggers:
6
+ signals:
7
+ - quadrant: 可放弃
8
+ symptoms:
9
+ - 运动缺失
10
+ - 精力不足
11
+ prescription:
12
+ one_liner: 从频率开始而非强度:每周固定 3 次,每次 20 分钟快走即可
13
+ steps:
14
+ - 选定每周 3 个固定时间
15
+ - 每次 20 分钟快走(不追求强度)
16
+ - 只关注"是否做了"不关注"做得多好"
17
+ micro_experiment: 本周固定 3 次 20 分钟快走
18
+ verify: 运动后精力感知是否改善
19
+ source:
20
+ ref: 《运动改造大脑》(Spark, John Ratey)
21
+ evidence_level: book
22
+ tags: [运动, 精力]
@@ -0,0 +1,22 @@
1
+ id: energy-fixed-wake-time
2
+ kind: prescription
3
+ dimension: energy
4
+ title: 先固化起床时间
5
+ triggers:
6
+ signals:
7
+ - quadrant: 可放弃
8
+ symptoms:
9
+ - 睡眠缺失
10
+ - 作息混乱
11
+ prescription:
12
+ one_liner: 连续两周固定起床时间(如 7:00),起床后立刻接触自然光 5 分钟;睡前一小时断屏
13
+ steps:
14
+ - 选定一个起床时间(如 7:00)
15
+ - 起床后立刻接触自然光 5 分钟
16
+ - 睡前一小时断屏
17
+ micro_experiment: 连续两周固定 7:00 起床+起床后自然光 5 分钟
18
+ verify: 两周后入睡时间和睡眠质量的变化
19
+ source:
20
+ ref: 《为什么我们要睡觉》(Why We Sleep, Matthew Walker)
21
+ evidence_level: book
22
+ tags: [睡眠, 节律]
@@ -0,0 +1,22 @@
1
+ id: energy-four-energy-radar
2
+ kind: prescription
3
+ dimension: energy
4
+ title: 四类精力雷达图
5
+ triggers:
6
+ signals:
7
+ - quadrant: 能耗黑洞
8
+ symptoms:
9
+ - 整体评分趋势下降
10
+ - 倦怠
11
+ prescription:
12
+ one_liner: 身体/情绪/心智/意义各打分 1-10,找最低维度先修
13
+ steps:
14
+ - 给身体/情绪/心智/意义各打 1-10 分
15
+ - 找出最低维度
16
+ - 针对最低维度选一条处方
17
+ micro_experiment: 今晚给四类精力各打分,找出最低维度
18
+ verify: 一周后重新打分看是否改善
19
+ source:
20
+ ref: 《精力管理》(The Power of Full Engagement, Jim Loehr & Tony Schwartz)
21
+ evidence_level: book
22
+ tags: [精力管理, 自评]
@@ -0,0 +1,22 @@
1
+ id: energy-intensity-recovery
2
+ kind: prescription
3
+ dimension: energy
4
+ title: 强度+恢复交替
5
+ triggers:
6
+ signals:
7
+ - quadrant: 能耗黑洞
8
+ symptoms:
9
+ - 连续高强度无恢复
10
+ - 精力透支
11
+ prescription:
12
+ one_liner: 高强度 1-1.5 小时后强制物理走动 5-10 分钟
13
+ steps:
14
+ - 设定 60-90 分钟高强度工作块
15
+ - 时间到强制起身物理走动 5-10 分钟
16
+ - 循环
17
+ micro_experiment: 明天每工作 90 分钟后强制走动 5 分钟
18
+ verify: 一天结束时的疲惫感是否低于平时
19
+ source:
20
+ ref: Peak Performance (Brad Stulberg & Steve Magness)
21
+ evidence_level: book
22
+ tags: [恢复, 节奏]
@@ -0,0 +1,12 @@
1
+ id: pack-summer-kid
2
+ kind: scenario-pack
3
+ title: 暑期计划(小学生)
4
+ triggers:
5
+ context_tags: [小学生, 暑假, 帮孩子, 暑期, 孩子]
6
+ recommends:
7
+ persona_id: kid-primary-school
8
+ analysis_model_ids: [review-summer-plan]
9
+ subskill_prompt: |
10
+ 对象是小学生 + 家长协作:语言更具体、目标更小、
11
+ 强调玩与学平衡、尊重孩子自主,不把家长焦虑转嫁孩子。
12
+ description: 家长帮小学生规划/复盘暑假的情境包
@@ -0,0 +1,282 @@
1
+ import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
2
+ import { join, dirname, extname, relative } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import yaml from "js-yaml";
5
+ import type { KnowledgeEntry } from "../../_schema/entry.schema.js";
6
+ import { validateEntry } from "../../_schema/entry.schema.js";
7
+ import type { AnalysisModel } from "../../_schema/analysis-model.schema.js";
8
+ import { validateAnalysisModel } from "../../_schema/analysis-model.schema.js";
9
+ import type { ScenarioPack } from "../../_schema/scenario-pack.schema.js";
10
+ import { validateScenarioPack } from "../../_schema/scenario-pack.schema.js";
11
+ import type { Persona } from "../../_schema/persona.schema.js";
12
+ import { validatePersonaRegistry } from "../../_schema/persona.schema.js";
13
+ import type { TriggerSignal } from "./types.js";
14
+
15
+ /** Repository root dir (two levels up from knowledge/src/repo/ → knowledge/) */
16
+ function defaultBasePath(): string {
17
+ // When running via tsx, import.meta.dirname is the dir of this file
18
+ return join(import.meta.dirname, "..", "..");
19
+ }
20
+
21
+ interface DimensionRegistry {
22
+ dimensions: Array<{ id: string; label: string; proposition: string }>;
23
+ }
24
+
25
+ export class FsYamlKnowledgeRepo {
26
+ private basePath: string;
27
+ private _all: KnowledgeEntry[] | null = null;
28
+ private _dimensions: string[] | null = null;
29
+ private _analysisModels: AnalysisModel[] | null = null;
30
+ private _scenarioPacks: ScenarioPack[] | null = null;
31
+ private _personas: Persona[] | null = null;
32
+
33
+ constructor(basePath?: string) {
34
+ this.basePath = basePath ?? defaultBasePath();
35
+ }
36
+
37
+ /** Load the dimensions registry from _registry/dimensions.yaml */
38
+ private loadDimensions(): string[] {
39
+ if (this._dimensions) return this._dimensions;
40
+ const path = join(this.basePath, "_registry", "dimensions.yaml");
41
+ if (!existsSync(path)) {
42
+ throw new Error(`维度注册表不存在: ${path}`);
43
+ }
44
+ const raw = yaml.load(readFileSync(path, "utf-8")) as DimensionRegistry;
45
+ if (!raw || !Array.isArray(raw.dimensions)) {
46
+ throw new Error(`维度注册表格式错误: ${path}`);
47
+ }
48
+ const dims = raw.dimensions.map((d) => d.id);
49
+ this._dimensions = dims;
50
+ return dims;
51
+ }
52
+
53
+ /** Collect all .yaml file paths under basePath, excluding _schema/, _registry/ and tests/ */
54
+ private collectYamlFiles(dir: string, root: string): string[] {
55
+ const files: string[] = [];
56
+ const entries = readdirSync(dir, { withFileTypes: true });
57
+ for (const entry of entries) {
58
+ const fullPath = join(dir, entry.name);
59
+ if (entry.isDirectory()) {
60
+ // Skip non-content directories.
61
+ // analysis-models/ 与 scenario-packs/ 是 COACH-MOD 新增内容类型,
62
+ // 用独立 schema(非 knowledgeEntrySchema),不能被 loadAll 当处方条目校验,
63
+ // 否则 validate 报错 / T13 迁移等价性断言崩。它们由 loadAnalysisModels
64
+ // / loadScenarioPacks 单独加载。
65
+ if (
66
+ entry.name === "_schema" ||
67
+ entry.name === "_registry" ||
68
+ entry.name === "tests" ||
69
+ entry.name === "analysis-models" ||
70
+ entry.name === "scenario-packs"
71
+ )
72
+ continue;
73
+ files.push(...this.collectYamlFiles(fullPath, root));
74
+ } else if (entry.isFile() && entry.name.endsWith(".yaml")) {
75
+ files.push(fullPath);
76
+ }
77
+ }
78
+ return files;
79
+ }
80
+
81
+ async loadAll(): Promise<KnowledgeEntry[]> {
82
+ if (this._all) return this._all;
83
+
84
+ // Load registry first
85
+ const validDimensions = this.loadDimensions();
86
+
87
+ const yamlFiles = this.collectYamlFiles(this.basePath, this.basePath);
88
+ const entries: KnowledgeEntry[] = [];
89
+ const seenIds = new Set<string>();
90
+
91
+ for (const filePath of yamlFiles) {
92
+ const raw = yaml.load(readFileSync(filePath, "utf-8"));
93
+ const entry = validateEntry(raw);
94
+
95
+ // Check dimension is registered
96
+ if (!validDimensions.includes(entry.dimension)) {
97
+ throw new Error(
98
+ `维度 "${entry.dimension}" 未在 _registry/dimensions.yaml 中登记 ` +
99
+ `(文件: ${filePath})`
100
+ );
101
+ }
102
+
103
+ // Check duplicate ID
104
+ if (seenIds.has(entry.id)) {
105
+ throw new Error(`重复 ID: "${entry.id}" (文件: ${filePath})`);
106
+ }
107
+ seenIds.add(entry.id);
108
+
109
+ entries.push(entry);
110
+ }
111
+
112
+ this._all = entries;
113
+ return entries;
114
+ }
115
+
116
+ async getById(id: string): Promise<KnowledgeEntry | null> {
117
+ const entries = await this.loadAll();
118
+ return entries.find((e) => e.id === id) ?? null;
119
+ }
120
+
121
+ async byDimension(dimension: string): Promise<KnowledgeEntry[]> {
122
+ const entries = await this.loadAll();
123
+ return entries.filter((e) => e.dimension === dimension);
124
+ }
125
+
126
+ async matchBySignal(signal: TriggerSignal): Promise<KnowledgeEntry[]> {
127
+ const entries = await this.loadAll();
128
+ return entries.filter((e) => {
129
+ const signals = e.triggers.signals ?? [];
130
+ return signals.some((s) => {
131
+ // Quadrant match: exact
132
+ if (signal.quadrant && s.quadrant) {
133
+ if (s.quadrant !== signal.quadrant) return false;
134
+ } else if (signal.quadrant && !s.quadrant) {
135
+ return false;
136
+ }
137
+
138
+ // Daily total minutes: parse threshold
139
+ if (signal.daily_total_minutes && s.daily_total_minutes) {
140
+ const sv = parseThreshold(s.daily_total_minutes);
141
+ const qv = parseThreshold(signal.daily_total_minutes);
142
+ if (sv === null || qv === null) {
143
+ // If we can't parse, fall back to string equality
144
+ if (s.daily_total_minutes !== signal.daily_total_minutes) return false;
145
+ } else if (!matchesThreshold(s.daily_total_minutes, signal.daily_total_minutes)) {
146
+ return false;
147
+ }
148
+ } else if (signal.daily_total_minutes && !s.daily_total_minutes) {
149
+ return false;
150
+ }
151
+
152
+ // If neither signal field is being queried, no match
153
+ if (!signal.quadrant && !signal.daily_total_minutes) return false;
154
+
155
+ return true;
156
+ });
157
+ });
158
+ }
159
+
160
+ async matchBySymptom(symptom: string): Promise<KnowledgeEntry[]> {
161
+ const entries = await this.loadAll();
162
+ const lowerSymptom = symptom.toLowerCase();
163
+ return entries.filter((e) => {
164
+ const symptoms = e.triggers.symptoms ?? [];
165
+ return symptoms.some((s) => s.toLowerCase().includes(lowerSymptom));
166
+ });
167
+ }
168
+
169
+ /** Collect .yaml files directly under one content subdir (non-recursive). */
170
+ private collectDirYamlFiles(subdir: string): string[] {
171
+ const dir = join(this.basePath, subdir);
172
+ if (!existsSync(dir)) return [];
173
+ return readdirSync(dir, { withFileTypes: true })
174
+ .filter((e) => e.isFile() && e.name.endsWith(".yaml"))
175
+ .map((e) => join(dir, e.name));
176
+ }
177
+
178
+ /** COACH-MOD: load all analysis models from analysis-models/. */
179
+ async loadAnalysisModels(): Promise<AnalysisModel[]> {
180
+ if (this._analysisModels) return this._analysisModels;
181
+ const files = this.collectDirYamlFiles("analysis-models");
182
+ const models: AnalysisModel[] = [];
183
+ const seenIds = new Set<string>();
184
+ for (const filePath of files) {
185
+ const raw = yaml.load(readFileSync(filePath, "utf-8"));
186
+ const model = validateAnalysisModel(raw);
187
+ if (seenIds.has(model.id)) {
188
+ throw new Error(`重复分析模型 ID: "${model.id}" (文件: ${filePath})`);
189
+ }
190
+ seenIds.add(model.id);
191
+ models.push(model);
192
+ }
193
+ this._analysisModels = models;
194
+ return models;
195
+ }
196
+
197
+ /** COACH-MOD: load all scenario packs from scenario-packs/. */
198
+ async loadScenarioPacks(): Promise<ScenarioPack[]> {
199
+ if (this._scenarioPacks) return this._scenarioPacks;
200
+ const files = this.collectDirYamlFiles("scenario-packs");
201
+ const packs: ScenarioPack[] = [];
202
+ const seenIds = new Set<string>();
203
+ for (const filePath of files) {
204
+ const raw = yaml.load(readFileSync(filePath, "utf-8"));
205
+ const pack = validateScenarioPack(raw);
206
+ if (seenIds.has(pack.id)) {
207
+ throw new Error(`重复场景包 ID: "${pack.id}" (文件: ${filePath})`);
208
+ }
209
+ seenIds.add(pack.id);
210
+ packs.push(pack);
211
+ }
212
+ this._scenarioPacks = packs;
213
+ return packs;
214
+ }
215
+
216
+ /** COACH-MOD: load personas from _registry/personas.yaml. */
217
+ async loadPersonas(): Promise<Persona[]> {
218
+ if (this._personas) return this._personas;
219
+ const path = join(this.basePath, "_registry", "personas.yaml");
220
+ if (!existsSync(path)) {
221
+ this._personas = [];
222
+ return [];
223
+ }
224
+ const raw = yaml.load(readFileSync(path, "utf-8"));
225
+ const { personas } = validatePersonaRegistry(raw ?? {});
226
+ const seenIds = new Set<string>();
227
+ for (const p of personas) {
228
+ if (seenIds.has(p.id)) {
229
+ throw new Error(`重复画像 ID: "${p.id}" (文件: ${path})`);
230
+ }
231
+ seenIds.add(p.id);
232
+ }
233
+ this._personas = personas;
234
+ return personas;
235
+ }
236
+
237
+ /**
238
+ * COACH-MOD: return the single default analysis model (is_default: true).
239
+ * §4.5 显式声明:0 个或 >1 个默认都抛错(不静默返回 undefined / 第一个),
240
+ * 因为"复盘默认装一个基线"是硬约束(spec §2 ①原则·基线)。
241
+ */
242
+ async getDefaultAnalysisModel(): Promise<AnalysisModel> {
243
+ const models = await this.loadAnalysisModels();
244
+ const defaults = models.filter((m) => m.is_default);
245
+ if (defaults.length === 0) {
246
+ throw new Error("没有默认分析模型(需要恰好一个 is_default: true)");
247
+ }
248
+ if (defaults.length > 1) {
249
+ throw new Error(
250
+ `默认分析模型不唯一:${defaults.map((m) => m.id).join(", ")}(需要恰好一个 is_default: true)`,
251
+ );
252
+ }
253
+ return defaults[0]!;
254
+ }
255
+ }
256
+
257
+ /** Parse a threshold string like ">600" into { op: ">", value: 600 } */
258
+ function parseThreshold(s: string): { op: string; value: number } | null {
259
+ const match = s.match(/^([><=]?)\s*(\d+)$/);
260
+ if (!match) return null;
261
+ return { op: match[1] || ">=", value: parseInt(match[2], 10) };
262
+ }
263
+
264
+ /** Check if the entry's daily_total_minutes matches the query threshold */
265
+ function matchesThreshold(entryValue: string, queryValue: string): boolean {
266
+ const pv = parseThreshold(queryValue);
267
+ if (!pv) return entryValue === queryValue;
268
+
269
+ // For entry values that are plain numbers or threshold expressions
270
+ const entryMatch = entryValue.match(/^(\d+)$/);
271
+ if (entryMatch) {
272
+ const num = parseInt(entryMatch[1], 10);
273
+ switch (pv.op) {
274
+ case ">": return num > pv.value;
275
+ case "<": return num < pv.value;
276
+ case "=": return num === pv.value;
277
+ default: return num >= pv.value;
278
+ }
279
+ }
280
+
281
+ return entryValue === queryValue;
282
+ }
@@ -0,0 +1,24 @@
1
+ import type { KnowledgeEntry } from "../../_schema/entry.schema.js";
2
+ import type { AnalysisModel } from "../../_schema/analysis-model.schema.js";
3
+ import type { ScenarioPack } from "../../_schema/scenario-pack.schema.js";
4
+ import type { Persona } from "../../_schema/persona.schema.js";
5
+
6
+ export type { KnowledgeEntry, AnalysisModel, ScenarioPack, Persona };
7
+
8
+ export interface TriggerSignal {
9
+ quadrant?: "能耗黑洞" | "可放弃" | "保持区" | "亮点区";
10
+ daily_total_minutes?: string; // ">600" style
11
+ }
12
+
13
+ export interface KnowledgeRepo {
14
+ loadAll(): Promise<KnowledgeEntry[]>;
15
+ getById(id: string): Promise<KnowledgeEntry | null>;
16
+ byDimension(dimension: string): Promise<KnowledgeEntry[]>;
17
+ matchBySignal(signal: TriggerSignal): Promise<KnowledgeEntry[]>;
18
+ matchBySymptom(symptom: string): Promise<KnowledgeEntry[]>;
19
+ // COACH-MOD §3 复盘模块知识层
20
+ loadAnalysisModels(): Promise<AnalysisModel[]>;
21
+ loadScenarioPacks(): Promise<ScenarioPack[]>;
22
+ loadPersonas(): Promise<Persona[]>;
23
+ getDefaultAnalysisModel(): Promise<AnalysisModel>;
24
+ }
@@ -0,0 +1,4 @@
1
+ dimensions:
2
+ - id: time
3
+ label: 时间·做减法
4
+ proposition: 问题不是时间不够,而是塞得太满
@@ -0,0 +1,24 @@
1
+ id: test-valid-entry
2
+ kind: prescription
3
+ dimension: time
4
+ title: 测试有效条目
5
+ triggers:
6
+ signals:
7
+ - quadrant: 能耗黑洞
8
+ symptoms:
9
+ - 拖延
10
+ - 承诺过多
11
+ prescription:
12
+ one_liner: 这是一个测试处方
13
+ steps:
14
+ - 步骤一
15
+ micro_experiment: 测试微实验
16
+ verify: 验证方法
17
+ source:
18
+ ref: 测试来源
19
+ evidence_level: book
20
+ applicability:
21
+ personas_fit: []
22
+ personas_avoid: []
23
+ boundaries: []
24
+ tags: [测试]
@@ -0,0 +1,4 @@
1
+ dimensions:
2
+ - id: time
3
+ label: 时间·做减法
4
+ proposition: 问题不是时间不够,而是塞得太满
@@ -0,0 +1,13 @@
1
+ id: dup-entry
2
+ kind: prescription
3
+ dimension: time
4
+ title: 重复条目 A
5
+ triggers:
6
+ symptoms:
7
+ - 测试
8
+ prescription:
9
+ one_liner: 测试
10
+ micro_experiment: 测试
11
+ source:
12
+ ref: 测试
13
+ evidence_level: book
@@ -0,0 +1,13 @@
1
+ id: dup-entry
2
+ kind: prescription
3
+ dimension: time
4
+ title: 重复条目 B
5
+ triggers:
6
+ symptoms:
7
+ - 测试
8
+ prescription:
9
+ one_liner: 测试
10
+ micro_experiment: 测试
11
+ source:
12
+ ref: 测试
13
+ evidence_level: book
@@ -0,0 +1,7 @@
1
+ dimensions:
2
+ - id: time
3
+ label: 时间·做减法
4
+ proposition: 问题不是时间不够,而是塞得太满
5
+ - id: energy
6
+ label: 精力·做循环
7
+ proposition: 表现 = 刺激 + 恢复
@@ -0,0 +1,13 @@
1
+ id: anxiety-test
2
+ kind: prescription
3
+ dimension: anxiety
4
+ title: 未注册维度测试
5
+ triggers:
6
+ symptoms:
7
+ - 测试
8
+ prescription:
9
+ one_liner: 测试
10
+ micro_experiment: 测试
11
+ source:
12
+ ref: 测试
13
+ evidence_level: book