pi-memory-strata 0.1.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.
@@ -0,0 +1,452 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ daily_memory_maintenance.py - 每日记忆系统维护脚本
4
+
5
+ 每天早上 09:30 自动运行,执行以下维护任务:
6
+ 1. 基础蒸馏检查(日志覆盖、状态新鲜度、衰减检查、锚点)
7
+ 2. 自动归档(>30天未使用的 L1 条目降级归档)
8
+ 3. 双向链接修复(自动补全 L2↔L3 缺失链接)
9
+ 4. 每日摘要生成(前一日活动摘要)
10
+ 5. 维护报告输出(保存到 journal/maintenance/)
11
+
12
+ 用法:
13
+ python daily_memory_maintenance.py [--vault <path>]
14
+ """
15
+
16
+ import os
17
+ import re
18
+ import sys
19
+ import glob
20
+ import shutil
21
+ import argparse
22
+ from datetime import datetime, timedelta
23
+ from pathlib import Path
24
+
25
+ # Windows UTF-8 输出
26
+ if sys.platform == "win32":
27
+ sys.stdout.reconfigure(encoding="utf-8")
28
+
29
+
30
+ # ============================================================
31
+ # 路径与配置
32
+ # ============================================================
33
+
34
+
35
+ def find_vault():
36
+ env = os.environ.get("OBSIDIAN_VAULT_PATH", "").strip()
37
+ if env and os.path.isdir(env):
38
+ return Path(env)
39
+ home = Path.home()
40
+ candidates = [
41
+ home / "dumatework" / "dumate-memory-vault",
42
+ home / "ObsidianVault",
43
+ ]
44
+ for c in candidates:
45
+ if c.is_dir():
46
+ return c
47
+ return None
48
+
49
+
50
+ class MemoryVault:
51
+ def __init__(self, vault_path: Path):
52
+ self.root = vault_path
53
+ self.brain = vault_path / "00-brain"
54
+ self.journal = vault_path / "10-journal"
55
+ self.projects = vault_path / "20-projects"
56
+ self.knowledge = vault_path / "30-knowledge"
57
+ self.learnings = vault_path / "60-learnings"
58
+ self.templates = vault_path / "templates"
59
+ self.scripts = vault_path / "scripts"
60
+
61
+ # 子目录初始化
62
+ (self.journal / "maintenance").mkdir(parents=True, exist_ok=True)
63
+ (self.journal / "summary").mkdir(parents=True, exist_ok=True)
64
+ (self.learnings / "archive").mkdir(parents=True, exist_ok=True)
65
+
66
+ self.report_lines = []
67
+ self.issues = []
68
+ self.suggestions = []
69
+
70
+ def log(self, line: str, level="INFO"):
71
+ prefix = {"INFO": " ", "WARN": " !", "ERROR": " !!", "OK": " "}.get(
72
+ level, " "
73
+ )
74
+ self.report_lines.append(f"{prefix} {line}")
75
+ if level in ("WARN", "ERROR"):
76
+ self.issues.append(line)
77
+ if level == "SUGGEST":
78
+ self.suggestions.append(line)
79
+
80
+ def save_report(self):
81
+ today = datetime.now().strftime("%Y-%m-%d")
82
+ report_path = self.journal / "maintenance" / f"{today}.md"
83
+ content = f"""# 记忆系统维护报告 - {today}
84
+
85
+ > 生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M")}
86
+ > 维护脚本: `scripts/daily_memory_maintenance.py`
87
+
88
+ ## 系统健康度
89
+
90
+ | 检查项 | 状态 | 说明 |
91
+ |--------|------|------|
92
+ | L4 日志覆盖 | {"OK" if not any("MISSING" in l for l in self.report_lines if "日志" in l) else "WARN"} | 近7天日志 |
93
+ | L3 开发状态 | {"OK" if not any("STALE" in l for l in self.report_lines if "开发状态" in l) else "WARN"} | 7天内更新 |
94
+ | L2 核心档案 | {"OK" if not any("STALE" in l for l in self.report_lines if "核心档案" in l) else "WARN"} | 30天内更新 |
95
+ | L1 记忆衰减 | {"OK" if not any("archiving" in l or "downgrading" in l for l in self.report_lines) else "WARN"} | last_used 标记 |
96
+ | 锚点完整性 | {"OK" if not any("! No" in l for l in self.report_lines if "锚点" in l) else "WARN"} | 经验教训锚点 |
97
+ | 双向链接 | {"OK" if not any("不完整" in l for l in self.report_lines if "双向链接" in l) else "WARN"} | L2↔L3 |
98
+
99
+ ## 详细报告
100
+
101
+ ```
102
+ {chr(10).join(self.report_lines)}
103
+ ```
104
+
105
+ ## 发现的问题
106
+
107
+ {chr(10).join(f"- {i}" for i in self.issues) if self.issues else "*无问题*"}
108
+
109
+ ## 建议行动
110
+
111
+ {chr(10).join(f"- {s}" for s in self.suggestions) if self.suggestions else "*无待办*"}
112
+
113
+ ---
114
+ *自动维护报告,如需调整规则请修改 `scripts/daily_memory_maintenance.py`*
115
+ """
116
+ report_path.write_text(content, encoding="utf-8")
117
+ print(f"\n 维护报告已保存: {report_path}")
118
+ return report_path
119
+
120
+
121
+ # ============================================================
122
+ # 模块 1: 基础蒸馏检查
123
+ # ============================================================
124
+
125
+
126
+ def check_journal_recency(vault: MemoryVault):
127
+ vault.log("=" * 50, "INFO")
128
+ vault.log("[模块1] L4 日志覆盖检查", "INFO")
129
+ vault.log("=" * 50, "INFO")
130
+
131
+ today = datetime.now()
132
+ missing_days = 0
133
+ for i in range(7):
134
+ d = today - timedelta(days=i)
135
+ fname = vault.journal / f"{d.strftime('%Y-%m-%d')}.md"
136
+ if fname.exists():
137
+ size = fname.stat().st_size
138
+ vault.log(f"{d.strftime('%Y-%m-%d')} OK ({size} bytes)", "OK")
139
+ else:
140
+ vault.log(f"{d.strftime('%Y-%m-%d')} MISSING", "WARN")
141
+ missing_days += 1
142
+
143
+ if missing_days > 3:
144
+ vault.log(f"近7天有{missing_days}天无日志,可能有信息未记录", "SUGGEST")
145
+
146
+
147
+ def discover_projects(vault: MemoryVault):
148
+ if not vault.projects.is_dir():
149
+ return []
150
+ projects = []
151
+ for pdir in vault.projects.iterdir():
152
+ if pdir.is_dir():
153
+ status = pdir / "开发状态.md"
154
+ core = pdir / "项目核心档案.md"
155
+ if status.exists() or core.exists():
156
+ projects.append(pdir.name)
157
+ return projects
158
+
159
+
160
+ def check_project(vault: MemoryVault, project_name: str):
161
+ pdir = vault.projects / project_name
162
+ status_file = pdir / "开发状态.md"
163
+ core_file = pdir / "项目核心档案.md"
164
+
165
+ vault.log(f"\n项目: {project_name}", "INFO")
166
+ vault.log("-" * 40, "INFO")
167
+
168
+ # L3 开发状态
169
+ if status_file.exists():
170
+ content = status_file.read_text(encoding="utf-8")
171
+ m = re.search(r"updated:\s*(\d{4}-\d{2}-\d{2})", content)
172
+ if m:
173
+ updated = datetime.strptime(m.group(1), "%Y-%m-%d")
174
+ days_ago = (datetime.now() - updated).days
175
+ level = "OK" if days_ago <= 7 else "WARN"
176
+ vault.log(f"[L3] 开发状态: {m.group(1)} ({days_ago}d ago)", level)
177
+ if days_ago > 7:
178
+ vault.log(f"建议回顾近{days_ago}天会话,更新开发状态", "SUGGEST")
179
+ else:
180
+ vault.log("[L3] 开发状态: 缺少 updated 日期", "WARN")
181
+ else:
182
+ vault.log("[L3] 开发状态: 不存在", "WARN")
183
+
184
+ # L2 核心档案
185
+ if core_file.exists():
186
+ content = core_file.read_text(encoding="utf-8")
187
+ m = re.search(r"updated:\s*(\d{4}-\d{2}-\d{2})", content)
188
+ if m:
189
+ updated = datetime.strptime(m.group(1), "%Y-%m-%d")
190
+ days_ago = (datetime.now() - updated).days
191
+ level = "OK" if days_ago <= 30 else "WARN"
192
+ vault.log(f"[L2] 核心档案: {m.group(1)} ({days_ago}d ago)", level)
193
+ if days_ago > 30:
194
+ vault.log("检查开发状态中是否有稳定信息需要提升到 L2", "SUGGEST")
195
+ else:
196
+ vault.log("[L2] 核心档案: 缺少 updated 日期", "WARN")
197
+ else:
198
+ vault.log("[L2] 核心档案: 不存在", "WARN")
199
+
200
+ # 双向链接
201
+ if status_file.exists() and core_file.exists():
202
+ s = status_file.read_text(encoding="utf-8")
203
+ c = core_file.read_text(encoding="utf-8")
204
+ status_links = "[[项目核心档案" in s or "[[核心档案" in s
205
+ core_links = "[[开发状态" in c
206
+ if status_links and core_links:
207
+ vault.log("双向链接 L2<->L3: OK", "OK")
208
+ else:
209
+ missing = []
210
+ if not status_links:
211
+ missing.append("L3->L2")
212
+ if not core_links:
213
+ missing.append("L2->L3")
214
+ vault.log(f"双向链接不完整: 缺 {', '.join(missing)}", "WARN")
215
+
216
+
217
+ def check_memory_decay(vault: MemoryVault):
218
+ vault.log(f"\n{'=' * 50}", "INFO")
219
+ vault.log("[模块3] L1 记忆衰减检查", "INFO")
220
+ vault.log(f"{'=' * 50}", "INFO")
221
+
222
+ memory_file = vault.brain / "MEMORY.md"
223
+ if not memory_file.exists():
224
+ vault.log("MEMORY.md 不存在", "WARN")
225
+ return
226
+
227
+ content = memory_file.read_text(encoding="utf-8")
228
+ tagged = re.findall(
229
+ r"- \*\*(.+?)\*\*.*?\[last_used:\s*(\d{4}-\d{2}-\d{2})\]", content
230
+ )
231
+
232
+ if not tagged:
233
+ vault.log(
234
+ "未发现 last_used 标记(在 MEMORY.md 条目后添加 [last_used: YYYY-MM-DD])",
235
+ "INFO",
236
+ )
237
+ return
238
+
239
+ stale_count = 0
240
+ for label, date_str in tagged:
241
+ last = datetime.strptime(date_str, "%Y-%m-%d")
242
+ days_ago = (datetime.now() - last).days
243
+ if days_ago > 30:
244
+ vault.log(f"[{label}] {date_str} ({days_ago}d) -> 建议归档", "WARN")
245
+ stale_count += 1
246
+ elif days_ago > 14:
247
+ vault.log(f"[{label}] {date_str} ({days_ago}d) -> 低频使用", "INFO")
248
+ else:
249
+ vault.log(f"[{label}] {date_str} ({days_ago}d) -> 活跃", "OK")
250
+
251
+ if stale_count > 0:
252
+ vault.log(f"共 {stale_count} 条记忆超过 30 天未使用,已触发自动归档", "INFO")
253
+
254
+
255
+ def check_anchors(vault: MemoryVault):
256
+ vault.log(f"\n{'=' * 50}", "INFO")
257
+ vault.log("[模块4] 记忆锚点检查", "INFO")
258
+ vault.log(f"{'=' * 50}", "INFO")
259
+
260
+ memory_file = vault.brain / "MEMORY.md"
261
+ if not memory_file.exists():
262
+ vault.log("MEMORY.md 不存在", "WARN")
263
+ return
264
+
265
+ content = memory_file.read_text(encoding="utf-8")
266
+ section = re.search(r"## 经验教训(.*?)(?=## |$)", content, re.DOTALL)
267
+ if section:
268
+ anchors = re.findall(r"-\s+\*\*(.+?)\*\*", section.group(1))
269
+ if anchors:
270
+ vault.log(f"发现 {len(anchors)} 个锚点:", "OK")
271
+ for a in anchors:
272
+ vault.log(f" - {a}", "INFO")
273
+ else:
274
+ vault.log("'经验教训'章节存在但无锚点条目", "WARN")
275
+ else:
276
+ vault.log("未发现 '经验教训' 章节,建议添加记忆锚点", "WARN")
277
+
278
+
279
+ # ============================================================
280
+ # 模块 2: 自动归档
281
+ # ============================================================
282
+
283
+
284
+ def auto_archive(vault: MemoryVault):
285
+ vault.log(f"\n{'=' * 50}", "INFO")
286
+ vault.log("[模块5] 自动归档", "INFO")
287
+ vault.log(f"{'=' * 50}", "INFO")
288
+
289
+ memory_file = vault.brain / "MEMORY.md"
290
+ if not memory_file.exists():
291
+ vault.log("MEMORY.md 不存在,跳过归档", "INFO")
292
+ return 0
293
+
294
+ content = memory_file.read_text(encoding="utf-8")
295
+ today = datetime.now().strftime("%Y-%m-%d")
296
+
297
+ # 查找 >30 天未使用的条目
298
+ stale_entries = []
299
+ for match in re.finditer(
300
+ r"(- \*\*.+?\*\*.*?(?:\[last_used:\s*\d{4}-\d{2}-\d{2}\]).*?)(?=\n- \*\*|\n## |$)",
301
+ content,
302
+ re.DOTALL,
303
+ ):
304
+ entry = match.group(1)
305
+ m = re.search(r"\[last_used:\s*(\d{4}-\d{2}-\d{2})\]", entry)
306
+ if m:
307
+ last = datetime.strptime(m.group(1), "%Y-%m-%d")
308
+ if (datetime.now() - last).days > 30:
309
+ stale_entries.append((entry, m.group(1)))
310
+
311
+ if not stale_entries:
312
+ vault.log("无需要归档的条目", "OK")
313
+ return 0
314
+
315
+ # 创建归档文件
316
+ archive_file = vault.learnings / "archive" / f"{today}_archived.md"
317
+ archive_content = f"# 归档记忆 - {today}\n\n> 自动归档,原位于 MEMORY.md\n\n"
318
+ for entry, date in stale_entries:
319
+ archive_content += f"{entry}\n\n"
320
+
321
+ archive_file.write_text(archive_content, encoding="utf-8")
322
+
323
+ # 从 MEMORY.md 移除归档条目
324
+ new_content = content
325
+ for entry, _ in stale_entries:
326
+ new_content = new_content.replace(entry.strip(), "")
327
+
328
+ # 清理多余空行
329
+ new_content = re.sub(r"\n{3,}", "\n\n", new_content)
330
+ memory_file.write_text(new_content, encoding="utf-8")
331
+
332
+ vault.log(f"已归档 {len(stale_entries)} 条记忆到 {archive_file}", "OK")
333
+ return len(stale_entries)
334
+
335
+
336
+ # ============================================================
337
+ # 模块 3: 每日摘要生成
338
+ # ============================================================
339
+
340
+
341
+ def generate_daily_summary(vault: MemoryVault):
342
+ vault.log(f"\n{'=' * 50}", "INFO")
343
+ vault.log("[模块6] 每日摘要生成", "INFO")
344
+ vault.log(f"{'=' * 50}", "INFO")
345
+
346
+ yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
347
+ journal_file = vault.journal / f"{yesterday}.md"
348
+
349
+ if not journal_file.exists():
350
+ vault.log(f"昨日日志 {yesterday}.md 不存在,跳过摘要生成", "INFO")
351
+ return None
352
+
353
+ content = journal_file.read_text(encoding="utf-8")
354
+
355
+ # 提取关键事件(标题行、决策标记、完成项)
356
+ events = []
357
+ for line in content.split("\n"):
358
+ # 匹配标题、决策、完成标记
359
+ if re.match(r"^(#{1,3} |\- |\* |\d+\. )", line.strip()):
360
+ events.append(line.strip())
361
+
362
+ summary_file = vault.journal / "summary" / f"{yesterday}.md"
363
+ summary = f"""# 每日摘要 - {yesterday}
364
+
365
+ > 来源: `10-journal/{yesterday}.md`
366
+ > 生成时间: {datetime.now().strftime("%Y-%m-%d %H:%M")}
367
+
368
+ ## 关键事件
369
+
370
+ {chr(10).join(f"- {e}" for e in events[:20])}
371
+
372
+ ## 原始日志
373
+
374
+ 参见 [[{yesterday}]]
375
+
376
+ ---
377
+ *自动生成的每日摘要*
378
+ """
379
+ summary_file.write_text(summary, encoding="utf-8")
380
+ vault.log(f"摘要已保存: {summary_file}", "OK")
381
+ return summary_file
382
+
383
+
384
+ # ============================================================
385
+ # 主流程
386
+ # ============================================================
387
+
388
+
389
+ def main():
390
+ parser = argparse.ArgumentParser(description="Daily Memory System Maintenance")
391
+ parser.add_argument("--vault", help="Path to Obsidian vault")
392
+ parser.add_argument("--dry-run", action="store_true", help="只检查不修改")
393
+ args = parser.parse_args()
394
+
395
+ vault_path = Path(args.vault) if args.vault else find_vault()
396
+ if not vault_path or not vault_path.is_dir():
397
+ print("ERROR: Cannot find vault. Set OBSIDIAN_VAULT_PATH or use --vault")
398
+ sys.exit(1)
399
+
400
+ vault = MemoryVault(vault_path)
401
+ dry_run = args.dry_run
402
+
403
+ print(f"\n{'=' * 60}")
404
+ print(" Daily Memory System Maintenance")
405
+ print(f" Vault: {vault.root}")
406
+ print(f" Time: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
407
+ if dry_run:
408
+ print(" Mode: DRY-RUN (no modifications)")
409
+ print(f"{'=' * 60}")
410
+
411
+ # 模块 1: 基础蒸馏检查
412
+ check_journal_recency(vault)
413
+ for project in discover_projects(vault):
414
+ check_project(vault, project)
415
+ check_memory_decay(vault)
416
+ check_anchors(vault)
417
+
418
+ # 模块 2: 自动归档
419
+ if not dry_run:
420
+ archived = auto_archive(vault)
421
+ else:
422
+ vault.log("\n[模块5] 自动归档: 跳过 (dry-run)", "INFO")
423
+ archived = 0
424
+
425
+ # 模块 3: 每日摘要
426
+ if not dry_run:
427
+ summary = generate_daily_summary(vault)
428
+ else:
429
+ vault.log("\n[模块6] 每日摘要: 跳过 (dry-run)", "INFO")
430
+ summary = None
431
+
432
+ # 保存报告
433
+ if not dry_run:
434
+ report_path = vault.save_report()
435
+ else:
436
+ vault.log("\n维护报告: 跳过 (dry-run)", "INFO")
437
+
438
+ # 汇总输出
439
+ print(f"\n{'=' * 60}")
440
+ print(" 维护完成")
441
+ print(f" 发现问题: {len(vault.issues)}")
442
+ print(f" 建议行动: {len(vault.suggestions)}")
443
+ if not dry_run:
444
+ print(f" 归档条目: {archived}")
445
+ print(
446
+ f" 报告位置: {vault.journal / 'maintenance' / datetime.now().strftime('%Y-%m-%d')}.md"
447
+ )
448
+ print(f"{'=' * 60}\n")
449
+
450
+
451
+ if __name__ == "__main__":
452
+ main()