Files
AirPlan-V2/lib/air_runtime/adr_watcher.py
AirPlan 6130478c96 feat: AirPlan V2 — 全专家插件强制路由 + 事件系统规范化
P0-8 扩大: do_mode.py finish_worker 全专家插件强制路由
- GUI→XDB, network→NDB, C/C++→SDB, done→Rvr, blocked/failed→Dbg
- 证据去重: 已有 xdbSessions/ndbSessions/sdbReports/rvrReviewed 则跳过

P1-GAP17: 事件 emit 规范化
- 新增 7 个事件常量 (TASK_ENTERED, TASK_FINISHED, ENGINE_ENTERED 等)
- 全部 emit 调用替换字符串字面量为常量,零残留
- 30 个事件类型常量全部定义且唯一

P1-GAP18: 事件日志原子轮转
- emit 计数器每 128 次检查轮转,避免每次 emit 读文件
- 清除未使用的 _emit_with_completion/_pending_merge_complete
- 原子轮转: tempfile+os.replace 保证不损坏

eng 极端接管: 强制调用全部专家插件 (Dbg/XDB/NDB/SDB/Rvr)
commands/do.md: 更新为全专家插件路由文档

全量测试: 69 通过, 0 失败

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 15:56:44 +08:00

110 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
ADR 文件变更监控 — P1-21 ADR 变更自动检测。
基于 SHA256 hash 对比检测 ADR 文件变更,自动触发级联失效。
"""
from __future__ import annotations
import hashlib
import re
from dataclasses import dataclass, field
from pathlib import Path
@dataclass
class ADRChange:
"""ADR 文件变更记录。"""
adr_id: str
kind: str # "new" | "superseded" | "modified"
path: str = ""
old_hash: str = ""
new_hash: str = ""
class ADRWatcher:
"""监控 ADR 文件变更,自动触发级联失效。
AirEng 在每轮轮询时调用 detect_changes()
发现 superseded 或 modified 变更时自动触发 invalidate_by_adr()。
"""
def __init__(self, adr_dir: Path):
self._adr_dir = adr_dir
self._known_hashes: dict[str, str] = {}
def snapshot(self) -> None:
"""启动时记录所有 ADR 的内容 hash。"""
if not self._adr_dir.exists():
return
for adr_file in sorted(self._adr_dir.glob("ADR-*.md")):
adr_id = self._extract_adr_id(adr_file)
self._known_hashes[adr_id] = hashlib.sha256(
adr_file.read_bytes()
).hexdigest()
def detect_changes(self) -> list[ADRChange]:
"""对比当前 ADR hash 与已知 hash返回变更列表。"""
if not self._adr_dir.exists():
return []
changes: list[ADRChange] = []
seen_ids: set[str] = set()
for adr_file in sorted(self._adr_dir.glob("ADR-*.md")):
adr_id = self._extract_adr_id(adr_file)
seen_ids.add(adr_id)
current_hash = hashlib.sha256(adr_file.read_bytes()).hexdigest()
old_hash = self._known_hashes.get(adr_id)
if old_hash is None:
changes.append(ADRChange(
adr_id=adr_id, kind="new",
path=str(adr_file), old_hash="", new_hash=current_hash,
))
elif current_hash != old_hash:
status = self._parse_status(adr_file)
if status == "superseded":
changes.append(ADRChange(
adr_id=adr_id, kind="superseded",
path=str(adr_file), old_hash=old_hash, new_hash=current_hash,
))
else:
changes.append(ADRChange(
adr_id=adr_id, kind="modified",
path=str(adr_file), old_hash=old_hash, new_hash=current_hash,
))
self._known_hashes[adr_id] = current_hash
# 检查被删除的 ADR
for adr_id in list(self._known_hashes.keys()):
if adr_id not in seen_ids:
changes.append(ADRChange(
adr_id=adr_id, kind="deleted",
path="", old_hash=self._known_hashes[adr_id], new_hash="",
))
del self._known_hashes[adr_id]
return changes
@staticmethod
def _extract_adr_id(adr_file: Path) -> str:
"""从文件名提取 ADR ID'ADR-0005-ffmpeg-decode.md''ADR-0005'"""
match = re.match(r"(ADR-\d+)", adr_file.stem)
if match:
return match.group(1)
return adr_file.stem
@staticmethod
def _parse_status(adr_file: Path) -> str:
"""解析 ADR 文件中的 Status 字段。"""
try:
text = adr_file.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return "unknown"
for line in text.splitlines():
lower = line.lower().strip()
if lower.startswith("status:") or lower.startswith("status :"):
status = line.split(":", 1)[1].strip().lower()
return status
return "unknown"