""" Agentic QE Fleet — Manifest 管理模块 负责跨战区 manifest JSON 的读写、合并、验证和迁移。 每个战区产出一个 manifest_.json,fleet_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