P1-19.1: Arc 边界测试强制 - TaskNode 新增 test_required 字段 - _inject_boundary_tests() 为每个模块注入接口测试和单元测试任务 - Done When 验证必须包含"测试通过" P1-19.2: AirRvr 高风险审计 - 新增 HighRiskAudit, HighRiskFinding 数据类 - ReviewReport 新增 highRiskAudit 字段,含 lifecycle/nullPointer/danglingPointer/exceptionSafety/concurrency + overallRisk + deliveryVerdict - 序列化/反序列化支持 P1-19.3: block-release 集成 - dispatch_worker_group() 派发前扫描最新审查报告 - deliveryVerdict=block-release 时阻止所有后续派发 - 记录 eng.blocked 事件 P1-20: frontend-design Skill 集成 - is_ui_task() UI 任务检测 - ensure_frontend_design_skill() 自动安装 Skill - route_ui_task() UI 任务路由决策 - enter_worker() 集成 UI 检测,skill 不可用时阻止执行 - commands/do.md 更新 UI 处理说明 - SKILL.md 新增 INV-12/INV-13/INV-14 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
103 lines
3.3 KiB
Python
103 lines
3.3 KiB
Python
"""
|
||
事件日志模块 — V2 可观测性基础设施。
|
||
JSONL 格式无限流式追加,与 state.json 互补:state.json 是当前快照,事件日志是完整时间线。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import contextlib
|
||
import json
|
||
import logging
|
||
import os
|
||
import tempfile
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 事件类型常量
|
||
TASK_DISPATCHED = "task.dispatched"
|
||
TASK_COMPLETED = "task.completed"
|
||
TASK_BLOCKED = "task.blocked"
|
||
MERGE_STARTED = "merge.started"
|
||
MERGE_COMPLETED = "merge.completed"
|
||
REPAIR_CREATED = "repair.created"
|
||
REPAIR_RESOLVED = "repair.resolved"
|
||
INTERVENTION_STALL = "intervention.stall"
|
||
XDB_CAPTURED = "xdb.captured"
|
||
DEBUG_SESSION = "debug.session"
|
||
CONTEXT_COMPACTED = "context.compacted"
|
||
DEPLOY_COMPLETED = "deploy.completed"
|
||
TEST_RUN = "test.run"
|
||
SEC_SCAN = "sec.scan"
|
||
REVIEW_SESSION = "review.session"
|
||
ENGINE_CYCLE = "engine.cycle"
|
||
WORKER_TIMEOUT = "worker.timeout"
|
||
LOCK_ACQUIRED = "lock.acquired"
|
||
LOCK_RELEASED = "lock.released"
|
||
STALE_LOCK_CLEANED = "stale_lock.cleaned"
|
||
|
||
|
||
def now_iso() -> str:
|
||
return datetime.now(timezone.utc).isoformat()
|
||
|
||
|
||
class EventLog:
|
||
"""结构化事件日志(JSONL 格式),支持轮转截断。"""
|
||
|
||
MAX_LINES = 10000
|
||
|
||
def __init__(self, path: Path, max_lines: int = MAX_LINES):
|
||
self._path = path
|
||
self._max_lines = max_lines
|
||
self._pending_merge_complete = None
|
||
|
||
def emit(self, event_type: str, payload: dict | None = None) -> None:
|
||
entry = {
|
||
"ts": now_iso(),
|
||
"type": event_type,
|
||
**(payload or {}),
|
||
}
|
||
self._path.parent.mkdir(parents=True, exist_ok=True)
|
||
with open(self._path, "a") as f:
|
||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||
self._maybe_rotate()
|
||
|
||
def _emit_with_completion(self, event_type: str, payload: dict) -> None:
|
||
"""emit 并确保 MERGE_STARTED / MERGE_COMPLETED 成对。"""
|
||
self.emit(event_type, payload)
|
||
|
||
# 自动补全配对事件
|
||
if event_type == "merge.started":
|
||
self._pending_merge_complete = payload.get("taskId")
|
||
elif event_type == "merge.completed":
|
||
self._pending_merge_complete = None
|
||
|
||
def _maybe_rotate(self) -> None:
|
||
"""原子轮转:先写临时文件,再 os.replace。"""
|
||
if not self._path.exists():
|
||
return
|
||
try:
|
||
with open(self._path, "r", encoding="utf-8", errors="replace") as f:
|
||
lines = f.readlines()
|
||
if len(lines) <= self._max_lines:
|
||
return
|
||
|
||
keep_count = self._max_lines // 2
|
||
kept_lines = lines[-keep_count:] if len(lines) > keep_count else lines
|
||
|
||
fd, tmp_path = tempfile.mkstemp(dir=self._path.parent, suffix=".tmp")
|
||
os.close(fd)
|
||
|
||
try:
|
||
with open(tmp_path, "w", encoding="utf-8") as f:
|
||
f.writelines(kept_lines)
|
||
os.replace(tmp_path, self._path)
|
||
logger.info("rotated event log %s, kept %d/%d lines", self._path, len(kept_lines), len(lines))
|
||
except Exception:
|
||
with contextlib.suppress(Exception):
|
||
os.unlink(tmp_path)
|
||
raise
|
||
except OSError:
|
||
pass
|