feat: 3.2.17中途变更处理(git驱动) + 3.2.9a Worker git强制 + 3.2.5 Phase7 squash merge + Arc git初始化检测
- 新增 ChangeClassifier(爆炸半径分类:IMPLEMENTATION/INTERFACE/GLOBAL_CONSTRAINT) - 新增 ImpactPropagator(BFS影响传播:IMPACTED/BOUNDARY/SAFE差异化标记) - task_graph.py:invalidate_by_adr()差异化失效 + CascadeReport扩展字段(向后兼容) - eng_mode.py:三阶段差异化流程(分类→传播→失效→git操作→验证任务→重规划) - eng_mode.py:_git_squash_merge_and_tag() + Phase 7 集成 - do_mode.py:_ensure_all_committed() Worker git操作强制 - adr_watcher.py:内容快照 + get_content_for_classification() - events.py:ADR_CLASSIFIED/IMPACT_PROPAGATED/BOUNDARY_VERIFICATION_GENERATED - partial_replanner.py:replan_with_constraints() + generate_verification_tasks() - project_bootstrap.py:ensure_git_initialized() Arc规划前检测 - test_p1_21_phase2.py:21个新测试,95个全量测试0失败 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -15,10 +15,11 @@ from pathlib import Path
|
||||
class ADRChange:
|
||||
"""ADR 文件变更记录。"""
|
||||
adr_id: str
|
||||
kind: str # "new" | "superseded" | "modified"
|
||||
kind: str # "new" | "superseded" | "modified" | "deleted"
|
||||
path: str = ""
|
||||
old_hash: str = ""
|
||||
new_hash: str = ""
|
||||
old_content: str | None = None # 3.2.17: 变更前内容,供 ChangeClassifier 分类
|
||||
|
||||
|
||||
class ADRWatcher:
|
||||
@@ -31,16 +32,17 @@ class ADRWatcher:
|
||||
def __init__(self, adr_dir: Path):
|
||||
self._adr_dir = adr_dir
|
||||
self._known_hashes: dict[str, str] = {}
|
||||
self._content_snapshots: dict[str, str] = {} # 3.2.17: 内容快照供 ChangeClassifier 使用
|
||||
|
||||
def snapshot(self) -> None:
|
||||
"""启动时记录所有 ADR 的内容 hash。"""
|
||||
"""启动时记录所有 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()
|
||||
raw = adr_file.read_bytes()
|
||||
self._known_hashes[adr_id] = hashlib.sha256(raw).hexdigest()
|
||||
self._content_snapshots[adr_id] = adr_file.read_text(encoding="utf-8")
|
||||
|
||||
def detect_changes(self) -> list[ADRChange]:
|
||||
"""对比当前 ADR hash 与已知 hash,返回变更列表。"""
|
||||
@@ -57,35 +59,56 @@ class ADRWatcher:
|
||||
old_hash = self._known_hashes.get(adr_id)
|
||||
|
||||
if old_hash is None:
|
||||
content = adr_file.read_text(encoding="utf-8")
|
||||
changes.append(ADRChange(
|
||||
adr_id=adr_id, kind="new",
|
||||
path=str(adr_file), old_hash="", new_hash=current_hash,
|
||||
))
|
||||
self._content_snapshots[adr_id] = content
|
||||
elif current_hash != old_hash:
|
||||
old_content = self._content_snapshots.get(adr_id)
|
||||
new_content = adr_file.read_text(encoding="utf-8")
|
||||
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,
|
||||
))
|
||||
kind = "superseded" if status == "superseded" else "modified"
|
||||
changes.append(ADRChange(
|
||||
adr_id=adr_id, kind=kind,
|
||||
path=str(adr_file), old_hash=old_hash, new_hash=current_hash,
|
||||
old_content=old_content,
|
||||
))
|
||||
self._content_snapshots[adr_id] = new_content
|
||||
self._known_hashes[adr_id] = current_hash
|
||||
|
||||
# 检查被删除的 ADR
|
||||
for adr_id in list(self._known_hashes.keys()):
|
||||
if adr_id not in seen_ids:
|
||||
old_content = self._content_snapshots.get(adr_id)
|
||||
changes.append(ADRChange(
|
||||
adr_id=adr_id, kind="deleted",
|
||||
path="", old_hash=self._known_hashes[adr_id], new_hash="",
|
||||
old_content=old_content,
|
||||
))
|
||||
del self._known_hashes[adr_id]
|
||||
|
||||
return changes
|
||||
|
||||
def get_content_for_classification(self, change: ADRChange) -> tuple[str | None, str | None]:
|
||||
"""3.2.17: 获取 ADR 变更的旧/新内容,供 ChangeClassifier 爆炸半径分类。
|
||||
|
||||
返回 (old_content, new_content):
|
||||
- modified/superseded/deleted: old=change.old_content, new=从磁盘读取
|
||||
- new: old=None, new=从磁盘读取
|
||||
"""
|
||||
if change.kind == "deleted":
|
||||
return (change.old_content, None)
|
||||
if change.kind == "new":
|
||||
new_path = Path(change.path) if change.path else None
|
||||
new_content = new_path.read_text(encoding="utf-8") if new_path and new_path.exists() else None
|
||||
return (None, new_content)
|
||||
# modified / superseded: old_content already captured in change object
|
||||
new_path = Path(change.path) if change.path else None
|
||||
new_content = new_path.read_text(encoding="utf-8") if new_path and new_path.exists() else None
|
||||
return (change.old_content, new_content)
|
||||
|
||||
@staticmethod
|
||||
def _extract_adr_id(adr_file: Path) -> str:
|
||||
"""从文件名提取 ADR ID,如 'ADR-0005-ffmpeg-decode.md' → 'ADR-0005'。"""
|
||||
|
||||
Reference in New Issue
Block a user