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>
110 lines
3.4 KiB
Python
Executable File
110 lines
3.4 KiB
Python
Executable File
"""
|
|
数据契约 — 保持 V1 契约完整性,新增 DeploymentRecord、AirRvr 审查报告等结构。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
|
|
def now_iso() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
@dataclass
|
|
class WorkerResult:
|
|
task_id: str
|
|
status: str # done | blocked | failed
|
|
summary: str = ""
|
|
files_changed: list[str] = field(default_factory=list)
|
|
validations: list[dict] = field(default_factory=list)
|
|
document_updates: list[dict] = field(default_factory=list)
|
|
evidence: list[dict] = field(default_factory=list)
|
|
risks: list[str] = field(default_factory=list)
|
|
blockers: list[str] = field(default_factory=list)
|
|
deploy_required: bool = False
|
|
deploy_info: dict | None = None
|
|
|
|
def validate_for_finalize(self, brief: dict | None = None) -> None:
|
|
if not re.fullmatch(r"[A-Za-z0-9_\-\.]+", self.task_id):
|
|
raise ValidationError(f"invalid task_id: {self.task_id!r}")
|
|
|
|
if self.status not in ("done", "blocked", "failed"):
|
|
raise ValidationError(f"invalid status: {self.status}")
|
|
|
|
if self.status == "done" and not self.validations and not self.files_changed:
|
|
raise ValidationError("done without validations or file changes")
|
|
|
|
if self.deploy_required:
|
|
deploy_validations = [
|
|
v for v in self.validations
|
|
if v.get("kind") in ("remote-deploy-verify", "remote-binary-md5")
|
|
]
|
|
if not deploy_validations:
|
|
raise ValidationError("deploy_required but no deploy verification in validations")
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"taskId": self.task_id,
|
|
"status": self.status,
|
|
"summary": self.summary,
|
|
"filesChanged": self.files_changed,
|
|
"validations": self.validations,
|
|
"documentUpdates": self.document_updates,
|
|
"evidence": self.evidence,
|
|
"risks": self.risks,
|
|
"blockers": self.blockers,
|
|
"deployRequired": self.deploy_required,
|
|
"deployInfo": self.deploy_info,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict) -> WorkerResult:
|
|
return cls(
|
|
task_id=data.get("taskId", ""),
|
|
status=data.get("status", ""),
|
|
summary=data.get("summary", ""),
|
|
files_changed=data.get("filesChanged", []),
|
|
validations=data.get("validations", []),
|
|
document_updates=data.get("documentUpdates", []),
|
|
evidence=data.get("evidence", []),
|
|
risks=data.get("risks", []),
|
|
blockers=data.get("blockers", []),
|
|
deploy_required=data.get("deployRequired", False),
|
|
deploy_info=data.get("deployInfo"),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class TaskRecord:
|
|
task_id: str
|
|
task: str = ""
|
|
files_dirs: str = ""
|
|
done_when: str = ""
|
|
status: str = "TODO"
|
|
validations: str = ""
|
|
adr: str = ""
|
|
|
|
@property
|
|
def text_for_classification(self) -> str:
|
|
return f"{self.task} {self.files_dirs} {self.done_when}"
|
|
|
|
|
|
@dataclass
|
|
class DeploymentRecord:
|
|
task_id: str
|
|
host: str
|
|
binary_path: str
|
|
md5: str = ""
|
|
service_name: str = ""
|
|
service_status: str = ""
|
|
deploy_at: str = field(default_factory=now_iso)
|
|
smoke_test_passed: bool | None = None
|
|
|
|
|
|
class ValidationError(Exception):
|
|
pass
|