"""Architect research timeline dashboard. Shows weekly external research findings as a traceable architecture-impact flow: research target -> finding -> impact -> recommendation -> version decision -> governance event. """ from __future__ import annotations from dataclasses import asdict, dataclass, field from datetime import datetime, timezone from typing import Any def utc_now() -> str: return datetime.now(timezone.utc).isoformat() @dataclass(frozen=True) class ResearchTimelineStep: id: str title: str step_type: str status: str source: str timestamp: str impact: str = "none" version_bump: str = "none" detail: dict[str, Any] = field(default_factory=dict) def to_dict(self) -> dict[str, Any]: return asdict(self) class ArchitectResearchTimeline: ORDER = { "research_planned": 10, "finding_created": 20, "impact_analyzed": 30, "recommendation_created": 40, "version_decision_created": 50, "architecture_review_event_created": 60, } def build(self, findings: list[dict[str, Any]] | None = None, decisions: list[dict[str, Any]] | None = None) -> dict[str, Any]: findings = findings or [] decisions = decisions or [] steps: list[ResearchTimelineStep] = [ ResearchTimelineStep( id="research-plan", title="每周技术情报调研计划", step_type="research_planned", status="ok", source="architect-agent", timestamp=utc_now(), detail={"cadence": "weekly"}, ) ] for idx, finding in enumerate(findings): steps.append( ResearchTimelineStep( id=finding.get("id", f"finding-{idx}"), title=finding.get("title", "research finding"), step_type="finding_created", status="ok", source=finding.get("source", "unknown"), timestamp=finding.get("created_at", utc_now()), impact=finding.get("impact", "none"), detail=finding, ) ) steps.append( ResearchTimelineStep( id=f"impact-{finding.get('id', idx)}", title="架构影响分析", step_type="impact_analyzed", status="attention_required" if finding.get("impact") in {"architecture", "breaking", "new_capability"} else "ok", source="architect-agent", timestamp=utc_now(), impact=finding.get("impact", "none"), detail={"relevance": finding.get("relevance"), "recommendation": finding.get("recommendation")}, ) ) for idx, decision in enumerate(decisions): steps.append( ResearchTimelineStep( id=decision.get("id", f"decision-{idx}"), title="C/R/V 版本建议", step_type="version_decision_created", status=decision.get("status", "ok"), source="architect-agent", timestamp=decision.get("created_at", utc_now()), version_bump=decision.get("version_bump", "none"), detail=decision, ) ) steps.sort(key=lambda s: (self.ORDER.get(s.step_type, 999), s.timestamp)) return { "module": "architect-research-timeline", "route": "/admin/ai-evolution/architect/research/timeline", "title": "架构师技术情报时间线", "status": self._status(steps), "generated_at": utc_now(), "steps": [step.to_dict() for step in steps], "summary": self._summary(steps), } def _status(self, steps: list[ResearchTimelineStep]) -> str: if any(step.status == "attention_required" for step in steps): return "attention_required" return "healthy" def _summary(self, steps: list[ResearchTimelineStep]) -> dict[str, Any]: return { "findings": len([s for s in steps if s.step_type == "finding_created"]), "architecture_impacts": len([s for s in steps if s.impact in {"architecture", "breaking", "new_capability"}]), "version_decisions": len([s for s in steps if s.step_type == "version_decision_created"]), "recommended_bumps": sorted({s.version_bump for s in steps if s.version_bump != "none"}), } __all__ = ["ResearchTimelineStep", "ArchitectResearchTimeline"]