Files

245 lines
9.0 KiB
Python
Raw Permalink 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.
"""
Agentic QE Fleet — Manifest 管理模块
负责跨战区 manifest JSON 的读写、合并、验证和迁移。
每个战区产出一个 manifest_<zone>.jsonfleet_runner.py 消费。
"""
from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
REPO_ROOT = Path(__file__).resolve().parent.parent
MANIFESTS_DIR = REPO_ROOT / "output" / "manifests"
# 战区顺序
ZONE_ORDER = ["prepare", "analyze", "design", "execute", "review", "monitor"]
def ensure_manifests_dir() -> Path:
MANIFESTS_DIR.mkdir(parents=True, exist_ok=True)
return MANIFESTS_DIR
def manifest_path(base_name: str, zone: str) -> Path:
"""返回战区 manifest 文件路径。"""
return MANIFESTS_DIR / f"{base_name}_{zone}.json"
def load_manifest(base_name: str, zone: str) -> dict[str, Any]:
"""加载指定战区的 manifest。"""
path = manifest_path(base_name, zone)
if not path.exists():
raise FileNotFoundError(f"Manifest 不存在: {path}")
return json.loads(path.read_text(encoding="utf-8"))
def load_manifest_safe(base_name: str, zone: str) -> dict[str, Any] | None:
"""安全加载 manifest,不存在时返回 None。"""
path = manifest_path(base_name, zone)
if not path.exists():
return None
return json.loads(path.read_text(encoding="utf-8"))
def save_manifest(base_name: str, zone: str, data: dict[str, Any]) -> Path:
"""保存战区 manifest。"""
ensure_manifests_dir()
path = manifest_path(base_name, zone)
data.setdefault("_meta", {})
data["_meta"]["zone"] = zone
data["_meta"]["base_name"] = base_name
data["_meta"]["updated_at"] = datetime.now(timezone.utc).isoformat()
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return path
def build_merged_manifest(base_name: str, zones: list[str] | None = None) -> dict[str, Any]:
"""合并多个战区的 manifest 为统一视图。"""
if zones is None:
zones = ZONE_ORDER
merged: dict[str, Any] = {
"_meta": {
"base_name": base_name,
"merged_zones": [],
"merged_at": datetime.now(timezone.utc).isoformat(),
}
}
for zone in zones:
manifest = load_manifest_safe(base_name, zone)
if manifest is None:
continue
merged["_meta"]["merged_zones"].append(zone)
# 战区数据按 zone 命名空间隔离
merged[zone] = manifest
# 同时摊平顶层便捷字段(后者覆盖前者)
for key, value in manifest.items():
if key.startswith("_"):
continue
merged[key] = value
return merged
def save_merged_manifest(base_name: str, zones: list[str] | None = None) -> Path:
"""合并所有战区 manifest 并保存为统一清单文件(供导出等步骤使用)。"""
merged = build_merged_manifest(base_name, zones)
ensure_manifests_dir()
path = MANIFESTS_DIR / f"{base_name}.json"
merged.setdefault("_meta", {})
merged["_meta"]["updated_at"] = datetime.now(timezone.utc).isoformat()
path.write_text(json.dumps(merged, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return path
def get_zone_status(base_name: str) -> dict[str, str]:
"""返回各战区完成状态。"""
status: dict[str, str] = {}
for zone in ZONE_ORDER:
manifest = load_manifest_safe(base_name, zone)
if manifest is None:
status[zone] = "pending"
elif manifest.get("_meta", {}).get("status") == "completed":
status[zone] = "completed"
else:
status[zone] = "in_progress"
return status
def mark_zone_completed(base_name: str, zone: str) -> Path:
"""标记战区为已完成。"""
manifest = load_manifest(base_name, zone)
manifest.setdefault("_meta", {})["status"] = "completed"
return save_manifest(base_name, zone, manifest)
def get_confirmation_gate(base_name: str) -> dict[str, Any] | None:
"""获取当前确认门禁状态(分析战区产出)。"""
manifest = load_manifest_safe(base_name, "analyze")
if manifest is None:
return None
return manifest.get("confirmation_gate")
def get_quality_verdict(base_name: str) -> dict[str, Any] | None:
"""获取质量裁决(评审战区产出)。"""
manifest = load_manifest_safe(base_name, "review")
if manifest is None:
return None
return manifest.get("quality_verdict")
def get_latest_zone(base_name: str) -> str | None:
"""返回最新已完成的战区名称。"""
for zone in reversed(ZONE_ORDER):
manifest = load_manifest_safe(base_name, zone)
if manifest and manifest.get("_meta", {}).get("status") == "completed":
return zone
return None
def get_next_zone(base_name: str) -> str | None:
"""返回下一个待执行的战区名称。"""
for zone in ZONE_ORDER:
manifest = load_manifest_safe(base_name, zone)
if manifest is None or manifest.get("_meta", {}).get("status") != "completed":
return zone
# 全部完成
return None
def migrate_legacy_manifest(base_name: str) -> dict[str, Any]:
"""从旧的单 manifest JSON 迁移到新的多战区 manifest 结构。"""
legacy_path = MANIFESTS_DIR / f"{base_name}.json"
if not legacy_path.exists():
raise FileNotFoundError(f"旧版 manifest 不存在: {legacy_path}")
legacy = json.loads(legacy_path.read_text(encoding="utf-8"))
# 从旧 manifest 推断 prepare 战区数据
prepare_data = {
"_meta": {"zone": "prepare", "base_name": base_name, "status": "completed",
"migrated_from_legacy": True},
"base_name": legacy.get("base_name", base_name),
"normalized_requirement_file": legacy.get("normalized_requirement_file"),
"normalized_technical_solution_files": legacy.get("normalized_technical_solution_files", []),
"technical_solution_files": legacy.get("technical_solution_files", []),
"project_profile_file": legacy.get("project_profile_file"),
"effective_terminology_files": legacy.get("effective_terminology_files", []),
"optional_terminology_files": legacy.get("optional_terminology_files", []),
"knowledge_base_files": legacy.get("knowledge_base_files", []),
}
save_manifest(base_name, "prepare", prepare_data)
# 从旧 manifest 推断 analyze 战区数据
analyze_data = {
"_meta": {"zone": "analyze", "base_name": base_name, "status": "completed",
"migrated_from_legacy": True},
"related_requirements": legacy.get("related_requirements", []),
"conflict_candidates_count": legacy.get("conflict_candidates_count", 0),
"confirmation_gate": legacy.get("confirmation_gate", {}),
"analysis_file": legacy.get("analysis_file"),
"relation_report_file": legacy.get("relation_report_file"),
}
save_manifest(base_name, "analyze", analyze_data)
return build_merged_manifest(base_name)
def get_pending_ai_agents(base_name: str) -> list[dict[str, Any]]:
"""扫描所有战区 manifest,返回状态为 'pending_ai' 的 Agent 列表。
返回按战区依赖顺序排列的待执行 AI Agent 信息。
"""
pending: list[dict[str, Any]] = []
for zone in ZONE_ORDER:
manifest = load_manifest_safe(base_name, zone)
if manifest is None:
continue
agent_notes = manifest.get("agent_notes", {})
for agent_id, note in agent_notes.items():
if isinstance(note, dict) and note.get("status") == "pending_ai":
pending.append({
"agent_id": agent_id,
"zone": zone,
"prompt_file": note.get("prompt_file", ""),
"output_file": note.get("output_file", ""),
"message": note.get("message", ""),
})
elif isinstance(note, str) and "待 AI Agent" in note:
pending.append({
"agent_id": agent_id,
"zone": zone,
"prompt_file": f"agents/{zone}/{agent_id.replace('-', '_')}.md",
"output_file": "",
"message": note,
})
return pending
def update_agent_status(base_name: str, zone: str, agent_id: str,
status: str, message: str = "") -> bool:
"""更新 manifest 中 Agent 的状态。
返回 True 表示更新成功,False 表示 manifest 不存在或 agent 不存在。
"""
manifest = load_manifest_safe(base_name, zone)
if manifest is None:
return False
agent_notes = manifest.setdefault("agent_notes", {})
if agent_id in agent_notes:
if isinstance(agent_notes[agent_id], dict):
agent_notes[agent_id]["status"] = status
agent_notes[agent_id]["message"] = message
else:
agent_notes[agent_id] = {"status": status, "message": message}
else:
agent_notes[agent_id] = {"status": status, "message": message}
save_manifest(base_name, zone, manifest)
return True