recurrence_promote.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """Recurrence>=3 自动 promote 建议(草案)。
  4. 扫描 error_index.md,定位每条 ERR 对应的单条 .md(errors/ERR-XXX.md),
  5. 解析其中 Recurrence-Count / First-Seen / Last-Seen / Tasks 字段,
  6. 评估是否满足 promote 硬规则:
  7. Recurrence-Count >= 3 且 跨 >= 2 个不同任务(Tasks>=2) 且 在 30 天窗口内
  8. 满足则输出「建议 promote 到项目级 Agent 记忆文件(CLAUDE.md / AGENTS.md / project_memory.md;具体项目的规则文件名不同则用 --target 覆盖)」。
  9. 仅输出建议,不自动改写规则文件(遵守 dev-expert「改规则需用户确认」+ 不跨项目写禁忌)。
  10. 若单条 .md 尚未含 Recurrence 字段(error-ledger 模板未扩展),优雅跳过并提示。
  11. 退出码:
  12. 0 = 完成(无论有无建议)
  13. 2 = 索引缺失
  14. """
  15. import sys
  16. import os
  17. import re
  18. import argparse
  19. from datetime import datetime
  20. RECURRENCE_THRESHOLD = 3
  21. MIN_TASKS = 2
  22. WINDOW_DAYS = 30
  23. def parse_index(path):
  24. rows = []
  25. try:
  26. with open(path, encoding="utf-8") as f:
  27. lines = f.readlines()
  28. except OSError as e:
  29. print("[RECUR-PROMOTE] 错误:读取索引失败: %s" % e, file=sys.stderr)
  30. return None
  31. for line in lines:
  32. if not line.strip().startswith("|"):
  33. continue
  34. cells = [c.strip() for c in line.strip().strip("|").split("|")]
  35. if len(cells) < 5 or not re.match(r"^ERR-\d+$", cells[0]):
  36. continue
  37. rows.append({
  38. "err_id": cells[0],
  39. "desc": cells[1] if len(cells) > 1 else "",
  40. "keywords": cells[3] if len(cells) > 3 else "",
  41. "date": cells[4] if len(cells) > 4 else "",
  42. })
  43. return rows
  44. def parse_recurrence(md_path):
  45. try:
  46. with open(md_path, encoding="utf-8") as f:
  47. content = f.read()
  48. except OSError:
  49. return None
  50. def field(name):
  51. m = re.search(r"\*{0,2}%s\*{0,2}\s*[::]\s*(\S+)" % re.escape(name), content)
  52. return m.group(1).strip() if m else None
  53. return {
  54. "count": field("Recurrence-Count"),
  55. "first": field("First-Seen"),
  56. "last": field("Last-Seen"),
  57. "tasks": field("Tasks"),
  58. }
  59. def to_int(v):
  60. try:
  61. return int(str(v))
  62. except Exception:
  63. return 0
  64. def to_date(v):
  65. for fmt in ("%Y-%m-%d", "%Y-%m-%dT%H:%M:%S%z", "%Y-%m-%d %H:%M:%S"):
  66. try:
  67. return datetime.strptime(str(v), fmt)
  68. except Exception:
  69. continue
  70. return None
  71. def main():
  72. parser = argparse.ArgumentParser(description="Recurrence>=3 promote 建议(草案)")
  73. parser.add_argument("--index", required=True, help="error_index.md 路径")
  74. parser.add_argument(
  75. "--errors-dir", default="",
  76. help="单条 ERR-XXX.md 目录(默认:索引同目录的 errors/)",
  77. )
  78. parser.add_argument(
  79. "--target", default="CLAUDE.md / AGENTS.md / project_memory.md(项目级记忆;具体项目的规则文件名不同则用 --target 覆盖)",
  80. help="建议 promote 的目标(默认项目级 Agent 记忆;若项目用特定规则文件如 主规则.mdc,用 --target 覆盖)",
  81. )
  82. args = parser.parse_args()
  83. rows = parse_index(args.index)
  84. if rows is None:
  85. return 2
  86. if not rows:
  87. print("[RECUR-PROMOTE] 索引无 ERR 条目")
  88. return 0
  89. errors_dir = args.errors_dir or os.path.join(os.path.dirname(args.index), "errors")
  90. today = datetime.now()
  91. suggestions = 0
  92. print("[RECUR-PROMOTE] 扫描 %d 条 ERR,硬规则:>=%d次 / >=%d任务 / %d天窗口"
  93. % (len(rows), RECURRENCE_THRESHOLD, MIN_TASKS, WINDOW_DAYS))
  94. for r in rows:
  95. md_path = os.path.join(errors_dir, r["err_id"] + ".md")
  96. rc = parse_recurrence(md_path)
  97. if not rc or rc["count"] is None:
  98. continue # 字段未扩展,跳过
  99. count = to_int(rc["count"])
  100. tasks = to_int(rc["tasks"])
  101. last = to_date(rc["last"]) or to_date(r["date"])
  102. in_window = (today - last).days <= WINDOW_DAYS if last else False
  103. ok = count >= RECURRENCE_THRESHOLD and tasks >= MIN_TASKS and in_window
  104. tag = "[PROMOTE]" if ok else "[skip]"
  105. print("[RECUR-PROMOTE] %s %s | count=%s tasks=%s last=%s window=%s"
  106. % (r["err_id"], tag, count, tasks, rc["last"] or r["date"], in_window))
  107. if ok:
  108. suggestions += 1
  109. print(" → 建议写入 %s:防重踩规则(来自 %s:%s)"
  110. % (args.target, r["err_id"], r["desc"]))
  111. print("[RECUR-PROMOTE] 共 %d 条满足 promote 条件" % suggestions)
  112. return 0
  113. if __name__ == "__main__":
  114. sys.exit(main())