from __future__ import annotations import hashlib import json import subprocess from pathlib import Path from typing import Any from django.core.exceptions import ValidationError from django.db import transaction from django.utils import timezone from control_plane.agents.models import ProgenySignal from control_plane.events.bus import EventBus from control_plane.projects.models import ( CommitRecord, Decision, ExtensionCandidate, ExtensionPlan, Exploration, ExplorationOpportunity, EvolutionCandidate, EvolutionPlan, Feature, Finding, Milestone, Project, ProjectPlan, RoadmapItem, StewardFinding, Task, TaskDependency, TaskStatus, ) from control_plane.verification.models import Review, TestRun, Verification, VerificationLevel, VerificationResult from graph.langgraph_runtime import LangGraphRuntime from graph.models import GraphRun from graph.task_nodes import TaskExecutionServices, task_execution_registry from model_router.router import ModelCapability, ModelRequestContract, ModelRouter from project_brain.planning import ProjectPlanContract, parse_project_plan_response class LifecyclePlanningError(ValueError): pass class ProjectContextMixin: def project_context(self, project: Project) -> dict[str, object]: files: dict[str, str] = {} tests: dict[str, str] = {} if project.repository_path: root = Path(project.repository_path) for path in sorted(root.rglob("*.py"))[:40]: if any(part in path.parts for part in [".git", "__pycache__", "migrations", ".venv", "venv", "node_modules", ".pytest_cache"]): continue relative = path.relative_to(root).as_posix() content = path.read_text(encoding="utf-8", errors="ignore")[:3000] if relative.startswith("tests") or "test" in path.name: tests[relative] = content else: files[relative] = content return { "project": {"id": str(project.id), "name": project.name, "goal": project.goal, "architecture_summary": project.architecture_summary}, "decisions": list(project.decisions.order_by("-created_at").values("decision_type", "decision", "reason")[:20]), "roadmap_items": list(project.roadmap_items.order_by("-created_at").values("title", "description", "status", "source")[:20]), "findings": list(project.findings.order_by("-created_at").values("finding_type", "severity", "title", "status")[:20]), "steward_findings": list(project.steward_findings.order_by("-created_at").values("finding_type", "severity", "title", "status", "recommended_action")[:20]), "features": list(project.features.order_by("created_at").values("title", "description", "status", "acceptance_criteria")[:50]), "tasks": list(project.tasks.order_by("-created_at").values("task_type", "status", "goal", "acceptance_criteria")[:50]), "files": files, "tests": tests, } def _json_from_sol(self, router: ModelRouter | None, prompt: str, *, fallback: dict[str, object], project: Project | None = None) -> dict[str, object]: if router is None: return fallback response = router.complete(ModelRequestContract(purpose=ModelCapability.PLANNING, prompt=prompt, project=project)) try: payload = json.loads(response.content) except json.JSONDecodeError as exc: raise LifecyclePlanningError("Sol lifecycle planning response must be JSON") from exc if not isinstance(payload, dict): raise LifecyclePlanningError("Sol lifecycle planning response must be a JSON object") return payload def _json_from_project_brain(self, router: ModelRouter | None, prompt: str, *, fallback: dict[str, object], project: Project, category: str) -> dict[str, object]: try: return self._json_from_sol(router, prompt, fallback=fallback, project=project) except Exception as exc: create_project_planning_signal(project, f"Sol {category} planning response invalid; using bounded fallback plan.", {"error": str(exc), "category": category}) return fallback class ExtensionService(ProjectContextMixin): def __init__(self, router: ModelRouter | None = None, bus: EventBus | None = None) -> None: self.router = router self.bus = bus or EventBus() def create_candidate( self, project: Project, *, title: str, description: str, rationale: str = "", source: str = "user", expected_value: str = "", affected_areas: list[str] | None = None, estimated_complexity: str = "MEDIUM", risk: str = "MEDIUM", confidence: float = 0.5, evidence: dict[str, object] | None = None, source_steward_finding: StewardFinding | None = None, source_opportunity: ExplorationOpportunity | None = None, source_roadmap_item: RoadmapItem | None = None, ) -> ExtensionCandidate: return ExtensionCandidate.objects.create( project=project, title=title, description=description, rationale=rationale, source=source, expected_value=expected_value, affected_areas=affected_areas or [], estimated_complexity=estimated_complexity, risk=risk, confidence=confidence, evidence=evidence or {}, source_steward_finding=source_steward_finding, source_opportunity=source_opportunity, source_roadmap_item=source_roadmap_item, ) def plan_with_project_brain(self, candidate: ExtensionCandidate) -> ExtensionPlan: context = self.project_context(candidate.project) fallback = self._fallback_extension_plan(candidate, context) payload = self._json_from_project_brain( self.router, "Plan an EXTENSION for an existing project. Return JSON with extension_plan and project_plan. Do not treat this as greenfield.\n" + json.dumps({"candidate": self._candidate_payload(candidate), "context": context}, default=str), fallback=fallback, project=candidate.project, category="extension", ) raw_plan = payload.get("extension_plan", payload) if not isinstance(raw_plan, dict): raise LifecyclePlanningError("extension_plan must be an object") project_plan_payload = raw_plan.get("project_plan") or payload.get("project_plan") if not isinstance(project_plan_payload, dict): create_project_planning_signal(candidate.project, "Sol extension plan omitted project_plan; using bounded fallback plan.", {"candidate_id": str(candidate.id)}) raw_plan = dict(fallback["extension_plan"]) project_plan_payload = dict(raw_plan["project_plan"]) try: parse_project_plan_response(json.dumps(project_plan_payload)) except Exception as exc: create_project_planning_signal(candidate.project, "Sol extension project_plan failed validation; using bounded fallback plan.", {"candidate_id": str(candidate.id), "error": str(exc)}) raw_plan = dict(fallback["extension_plan"]) project_plan_payload = dict(raw_plan["project_plan"]) parse_project_plan_response(json.dumps(project_plan_payload)) plan = ExtensionPlan.objects.create( candidate=candidate, project=candidate.project, status="PLANNED", strategy=str(raw_plan.get("strategy", candidate.description)), plan=raw_plan, acceptance_criteria=[str(item) for item in raw_plan.get("acceptance_criteria", project_plan_payload.get("acceptance_criteria", []))], context_snapshot=context, ) candidate.status = "PLANNING" candidate.save(update_fields=["status", "updated_at"]) self.bus.publish("EXTENSION_PLAN_CREATED", project=candidate.project, payload={"candidate_id": str(candidate.id), "plan_id": str(plan.id)}) return plan def approve_plan(self, plan: ExtensionPlan) -> ExtensionPlan: plan.status = "APPROVED" plan.approved_at = timezone.now() plan.save(update_fields=["status", "approved_at", "updated_at"]) plan.candidate.status = "READY" plan.candidate.save(update_fields=["status", "updated_at"]) return plan def materialize_project_dag(self, plan: ExtensionPlan) -> ProjectPlan: if plan.project_plan_id: return plan.project_plan contract = parse_project_plan_response(json.dumps(plan.plan.get("project_plan", {}))) project_plan = self._materialize_contract(plan.project, contract, prefix=f"EXT-{str(plan.id)[:8]}") plan.project_plan = project_plan plan.status = "MATERIALIZED" plan.save(update_fields=["project_plan", "status", "updated_at"]) plan.candidate.status = "BUILDING" plan.candidate.save(update_fields=["status", "updated_at"]) return project_plan def execute(self, plan: ExtensionPlan, router: ModelRouter, *, test_command: list[str] | None = None) -> list[GraphRun]: from graph.bootstrap import champion_task_execution_graph_v1 project_plan = self.materialize_project_dag(plan) runs: list[GraphRun] = [] for task in Task.objects.filter(milestone__plan=project_plan).exclude(status=TaskStatus.COMPLETE).order_by("priority", "created_at"): graph_version = champion_task_execution_graph_v1() graph_run = GraphRun.objects.create(execution_graph_version=graph_version, project=task.project, milestone=task.milestone, feature=task.feature, task=task, current_node=graph_version.graph_spec["entry"], metadata={"extension_plan_id": str(plan.id), "extension_candidate_id": str(plan.candidate_id)}) LangGraphRuntime(task_execution_registry(TaskExecutionServices(router, bus=self.bus, test_command=test_command or ["python", "-m", "pytest"])), bus=self.bus).run_until_terminal_or_paused(graph_run) for commit in task.commits.all(): commit.extension_candidate = plan.candidate commit.save(update_fields=["extension_candidate", "updated_at"]) runs.append(graph_run) return runs def verify_extension(self, plan: ExtensionPlan) -> Verification: project_plan = plan.project_plan tasks = Task.objects.filter(milestone__plan=project_plan) if project_plan else Task.objects.none() task_count = tasks.count() completed = task_count > 0 and not tasks.exclude(status=TaskStatus.COMPLETE).exists() tests_pass = not TestRun.objects.filter(task__in=tasks).exclude(status="PASS").exists() review_failures = Review.objects.filter(task__in=tasks).exclude(status="PASS") judge_failures = Verification.objects.filter(task__in=tasks, level=VerificationLevel.TASK).exclude(result=VerificationResult.PASS) criteria = plan.acceptance_criteria or plan.plan.get("acceptance_criteria", []) criteria_present = bool(criteria) passed = completed and tests_pass and not review_failures.exists() and not judge_failures.exists() and criteria_present verification = Verification.objects.create( project=plan.project, milestone=tasks.first().milestone if tasks.exists() else None, level=VerificationLevel.MILESTONE, result=VerificationResult.PASS if passed else VerificationResult.FAIL, contract={"extension_plan_id": str(plan.id), "acceptance_criteria": criteria}, evidence=[{"task_count": task_count, "all_tasks_complete": completed, "tests_pass": tests_pass, "review_failures": review_failures.count(), "judge_failures": judge_failures.count()}], summary="Extension acceptance contract satisfied" if passed else "Extension acceptance contract failed", ) plan.verification = verification plan.status = "COMPLETE" if passed else "FAILED" plan.completed_at = timezone.now() plan.save(update_fields=["verification", "status", "completed_at", "updated_at"]) plan.candidate.status = "COMPLETE" if passed else "BUILDING" plan.candidate.save(update_fields=["status", "updated_at"]) return verification def _fallback_extension_plan(self, candidate: ExtensionCandidate, context: dict[str, object]) -> dict[str, object]: task_goal = candidate.metadata.get("task_goal") or candidate.description or candidate.title acceptance = candidate.metadata.get("acceptance_criteria") or [f"{candidate.title} capability is present", "Deterministic tests pass"] return { "extension_plan": { "strategy": f"Add bounded scope to existing project: {candidate.title}", "acceptance_criteria": acceptance, "project_plan": { "goal": candidate.project.goal, "scope": candidate.description, "acceptance_criteria": acceptance, "milestones": [ { "key": "EXTEND", "title": candidate.title, "goal": candidate.description or candidate.title, "verification_contract": {"extension_candidate_id": str(candidate.id)}, "features": [{"key": "F1", "title": candidate.title, "description": candidate.description, "acceptance_criteria": acceptance, "tasks": [{"id": "T1", "goal": str(task_goal), "type": "implementation", "acceptance_criteria": acceptance, "priority": 50, "dependencies": []}]}], } ], }, } } def _candidate_payload(self, candidate: ExtensionCandidate) -> dict[str, object]: return {"id": str(candidate.id), "title": candidate.title, "description": candidate.description, "rationale": candidate.rationale, "expected_value": candidate.expected_value, "affected_areas": candidate.affected_areas, "evidence": candidate.evidence} def _materialize_contract(self, project: Project, contract: ProjectPlanContract, *, prefix: str) -> ProjectPlan: with transaction.atomic(): version = project.current_plan_version + 1 project_plan = ProjectPlan.objects.create(project=project, version=version, goal=project.goal, scope=contract.scope, stack=contract.stack, architecture=contract.architecture, constraints=contract.constraints, acceptance_criteria=contract.acceptance_criteria, permissions=contract.permissions, budget=contract.budget, open_decisions=contract.open_decisions, approved_at=timezone.now()) project.current_plan_version = version project.save(update_fields=["current_plan_version", "updated_at"]) task_by_external_id: dict[str, Task] = {} dependency_specs: list[tuple[Task, list[str]]] = [] for order, milestone_contract in enumerate(contract.milestones): milestone = Milestone.objects.create(project=project, plan=project_plan, key=f"{prefix}-{milestone_contract.key}"[:50], title=milestone_contract.title, goal=milestone_contract.goal, verification_contract=milestone_contract.verification_contract, order=order) for feature_contract in milestone_contract.features: feature = Feature.objects.create(project=project, milestone=milestone, title=feature_contract.title, description=feature_contract.description, acceptance_criteria=feature_contract.acceptance_criteria) for task_contract in feature_contract.tasks: task = Task.objects.create(project=project, milestone=milestone, feature=feature, task_type=task_contract.task_type, status=TaskStatus.READY, priority=task_contract.priority, goal=task_contract.goal, acceptance_criteria=task_contract.acceptance_criteria) task_by_external_id[task_contract.task_id] = task dependency_specs.append((task, task_contract.dependencies)) for task, deps in dependency_specs: for dep in deps: TaskDependency.objects.create(task=task, depends_on=task_by_external_id[dep]) return project_plan class EvolutionService(ProjectContextMixin): def __init__(self, router: ModelRouter | None = None, bus: EventBus | None = None) -> None: self.router = router self.bus = bus or EventBus() def create_candidate(self, project: Project, *, target: str, objective: str, baseline_measurement: dict[str, object], desired_direction: str, target_measurement: dict[str, object] | None = None, rationale: str = "", source: str = "user", evidence: dict[str, object] | None = None, risk: str = "MEDIUM", confidence: float = 0.5, source_steward_finding: StewardFinding | None = None, source_opportunity: ExplorationOpportunity | None = None, source_roadmap_item: RoadmapItem | None = None) -> EvolutionCandidate: if not baseline_measurement: raise ValidationError("EVOLVE requires a measurable baseline; route to INVESTIGATE or EXPLORE instead.") return EvolutionCandidate.objects.create(project=project, target=target, objective=objective, baseline_measurement=baseline_measurement, desired_direction=desired_direction, target_measurement=target_measurement or {}, rationale=rationale, source=source, evidence=evidence or {}, risk=risk, confidence=confidence, source_steward_finding=source_steward_finding, source_opportunity=source_opportunity, source_roadmap_item=source_roadmap_item) def plan_with_project_brain(self, candidate: EvolutionCandidate) -> EvolutionPlan: if not candidate.baseline_measurement: raise ValidationError("EvolutionCandidate has no baseline measurement.") context = self.project_context(candidate.project) fallback = self._fallback_evolution_plan(candidate) payload = self._json_from_project_brain(self.router, "Plan an EVOLUTION for an existing project. Return JSON with evolution_plan and project_plan. Must preserve measurable baseline and threshold.\n" + json.dumps({"candidate": self._candidate_payload(candidate), "context": context}, default=str), fallback=fallback, project=candidate.project, category="evolution") raw_plan = payload.get("evolution_plan", payload) if not isinstance(raw_plan, dict): raise LifecyclePlanningError("evolution_plan must be an object") project_plan_payload = raw_plan.get("project_plan") or payload.get("project_plan") if not isinstance(project_plan_payload, dict): create_project_planning_signal(candidate.project, "Sol evolution plan omitted project_plan; using bounded fallback plan.", {"candidate_id": str(candidate.id)}) raw_plan = dict(fallback["evolution_plan"]) project_plan_payload = dict(raw_plan["project_plan"]) try: parse_project_plan_response(json.dumps(project_plan_payload)) except Exception as exc: create_project_planning_signal(candidate.project, "Sol evolution project_plan failed validation; using bounded fallback plan.", {"candidate_id": str(candidate.id), "error": str(exc)}) raw_plan = dict(fallback["evolution_plan"]) project_plan_payload = dict(raw_plan["project_plan"]) parse_project_plan_response(json.dumps(project_plan_payload)) plan = EvolutionPlan.objects.create(candidate=candidate, project=candidate.project, status="PLANNED", baseline=dict(raw_plan.get("baseline", candidate.baseline_measurement)), hypothesis=str(raw_plan.get("hypothesis", candidate.objective)), intervention=str(raw_plan.get("intervention", "Implement targeted project improvement")), measurement_method=dict(raw_plan.get("measurement_method", {"type": "metadata"})), success_threshold=dict(raw_plan.get("success_threshold", {"minimum_improvement_percent": 10})), regression_constraints=list(raw_plan.get("regression_constraints", ["Deterministic tests pass"])), affected_components=list(raw_plan.get("affected_components", [candidate.target])), task_plan=project_plan_payload, experiment_requirements=dict(raw_plan.get("experiment_requirements", {})), context_snapshot=context) candidate.status = "PLANNING" candidate.save(update_fields=["status", "updated_at"]) return plan def approve_plan(self, plan: EvolutionPlan) -> EvolutionPlan: plan.status = "APPROVED" plan.approved_at = timezone.now() plan.save(update_fields=["status", "approved_at", "updated_at"]) plan.candidate.status = "READY" plan.candidate.save(update_fields=["status", "updated_at"]) return plan def materialize_work(self, plan: EvolutionPlan) -> ProjectPlan: if plan.project_plan_id: return plan.project_plan contract = parse_project_plan_response(json.dumps(plan.task_plan)) project_plan = ExtensionService(bus=self.bus)._materialize_contract(plan.project, contract, prefix=f"EVO-{str(plan.id)[:8]}") plan.project_plan = project_plan plan.status = "MATERIALIZED" plan.save(update_fields=["project_plan", "status", "updated_at"]) plan.candidate.status = "BUILDING" plan.candidate.save(update_fields=["status", "updated_at"]) return project_plan def execute(self, plan: EvolutionPlan, router: ModelRouter, *, test_command: list[str] | None = None) -> list[GraphRun]: from graph.bootstrap import champion_task_execution_graph_v1 project_plan = self.materialize_work(plan) runs: list[GraphRun] = [] for task in Task.objects.filter(milestone__plan=project_plan).exclude(status=TaskStatus.COMPLETE).order_by("priority", "created_at"): graph_version = champion_task_execution_graph_v1() graph_run = GraphRun.objects.create(execution_graph_version=graph_version, project=task.project, milestone=task.milestone, feature=task.feature, task=task, current_node=graph_version.graph_spec["entry"], metadata={"evolution_plan_id": str(plan.id), "evolution_candidate_id": str(plan.candidate_id)}) LangGraphRuntime(task_execution_registry(TaskExecutionServices(router, bus=self.bus, test_command=test_command or ["python", "-m", "pytest"])), bus=self.bus).run_until_terminal_or_paused(graph_run) for commit in task.commits.all(): commit.evolution_candidate = plan.candidate commit.save(update_fields=["evolution_candidate", "updated_at"]) runs.append(graph_run) return runs def measure_candidate(self, plan: EvolutionPlan) -> dict[str, object]: method = plan.measurement_method or {} if "candidate_measurement" in plan.metadata: measurement = dict(plan.metadata["candidate_measurement"]) elif method.get("type") == "command" and plan.project.repository_path: completed = subprocess.run([str(part) for part in method.get("command", [])], cwd=plan.project.repository_path, capture_output=True, text=True, check=False, timeout=120) measurement = {"returncode": completed.returncode, "stdout": completed.stdout[-4000:], "stderr": completed.stderr[-2000:]} else: measurement = dict(plan.candidate.target_measurement or plan.baseline) plan.candidate_measurement = measurement plan.save(update_fields=["candidate_measurement", "updated_at"]) return measurement def compare_baseline(self, plan: EvolutionPlan) -> dict[str, object]: metric = str(plan.success_threshold.get("metric", plan.baseline.get("metric", "value"))) baseline_value = float(plan.baseline.get(metric, plan.baseline.get("value", 0)) or 0) candidate_value = float(plan.candidate_measurement.get(metric, plan.candidate_measurement.get("value", baseline_value)) or 0) direction = plan.candidate.desired_direction if baseline_value == 0: improvement_percent = 0.0 elif direction == "DECREASE": improvement_percent = ((baseline_value - candidate_value) / baseline_value) * 100 else: improvement_percent = ((candidate_value - baseline_value) / baseline_value) * 100 threshold = float(plan.success_threshold.get("minimum_improvement_percent", 0)) delta = {"metric": metric, "baseline": baseline_value, "candidate": candidate_value, "improvement_percent": improvement_percent, "threshold": threshold} plan.delta = delta plan.save(update_fields=["delta", "updated_at"]) return delta def judge_evolution(self, plan: EvolutionPlan) -> Verification: if not plan.candidate_measurement: self.measure_candidate(plan) delta = self.compare_baseline(plan) project_plan = plan.project_plan tasks = Task.objects.filter(milestone__plan=project_plan) if project_plan else Task.objects.none() tests_pass = not TestRun.objects.filter(task__in=tasks).exclude(status="PASS").exists() implementation_complete = not tasks.exclude(status=TaskStatus.COMPLETE).exists() if tasks.exists() else True improved = float(delta["improvement_percent"]) >= float(delta["threshold"]) verdict = "PASS" if implementation_complete and tests_pass and improved else "NOT_IMPROVED" verification = Verification.objects.create(project=plan.project, milestone=tasks.first().milestone if tasks.exists() else None, level=VerificationLevel.MILESTONE, result=VerificationResult.PASS if verdict == "PASS" else VerificationResult.FAIL, contract={"evolution_plan_id": str(plan.id), "success_threshold": plan.success_threshold, "regression_constraints": plan.regression_constraints}, evidence=[{"implementation_complete": implementation_complete, "tests_pass": tests_pass, "delta": delta, "verdict": verdict}], summary="Evolution objective improved" if verdict == "PASS" else "Evolution implementation did not improve objective") plan.verification = verification plan.verdict = verdict plan.status = "COMPLETE" if verdict == "PASS" else "NOT_IMPROVED" plan.completed_at = timezone.now() plan.save(update_fields=["verification", "verdict", "status", "completed_at", "updated_at"]) plan.candidate.status = "COMPLETE" if verdict == "PASS" else "PROPOSED" plan.candidate.save(update_fields=["status", "updated_at"]) return verification def _fallback_evolution_plan(self, candidate: EvolutionCandidate) -> dict[str, object]: acceptance = ["Deterministic tests pass", f"Objective improves: {candidate.objective}"] task_goal = candidate.metadata.get("task_goal") or f"Improve {candidate.target}: {candidate.objective}" return {"evolution_plan": {"baseline": candidate.baseline_measurement, "hypothesis": candidate.objective, "intervention": str(task_goal), "measurement_method": {"type": "metadata"}, "success_threshold": {"metric": candidate.baseline_measurement.get("metric", "value"), "minimum_improvement_percent": 10}, "regression_constraints": ["Deterministic tests pass"], "affected_components": [candidate.target], "project_plan": {"goal": candidate.project.goal, "scope": candidate.objective, "acceptance_criteria": acceptance, "milestones": [{"key": "EVOLVE", "title": candidate.target, "goal": candidate.objective, "verification_contract": {"evolution_candidate_id": str(candidate.id)}, "features": [{"key": "F1", "title": candidate.target, "description": candidate.objective, "acceptance_criteria": acceptance, "tasks": [{"id": "T1", "goal": str(task_goal), "type": "implementation", "acceptance_criteria": acceptance, "priority": 50, "dependencies": []}]}]}]}}} def _candidate_payload(self, candidate: EvolutionCandidate) -> dict[str, object]: return {"id": str(candidate.id), "target": candidate.target, "objective": candidate.objective, "baseline_measurement": candidate.baseline_measurement, "desired_direction": candidate.desired_direction, "target_measurement": candidate.target_measurement, "evidence": candidate.evidence} class ExplorerService(ProjectContextMixin): def __init__(self, router: ModelRouter | None = None, bus: EventBus | None = None) -> None: self.router = router self.bus = bus or EventBus() def start_exploration(self, project: Project, *, prompt: str = "") -> Exploration: exploration = Exploration.objects.create(project=project, prompt=prompt, context_snapshot=self.project_context(project)) return exploration def generate_opportunities(self, exploration: Exploration) -> list[ExplorationOpportunity]: fallback = {"opportunities": self._fallback_opportunities(exploration)} payload = self._json_from_project_brain(self.router, "Explore an existing project for valuable changes. Return JSON opportunities only; do not create build work.\n" + json.dumps(exploration.context_snapshot, default=str), fallback=fallback, project=exploration.project, category="exploration") raw_items = payload.get("opportunities", []) if not isinstance(raw_items, list): raise LifecyclePlanningError("Explorer response opportunities must be a list") opportunities: list[ExplorationOpportunity] = [] for raw in raw_items: if not isinstance(raw, dict): continue opportunity = self.upsert_opportunity(exploration, raw) opportunities.append(opportunity) exploration.status = "COMPLETE" exploration.completed_at = timezone.now() exploration.save(update_fields=["status", "completed_at", "updated_at"]) return sorted(opportunities, key=lambda item: item.composite_score, reverse=True) def upsert_opportunity(self, exploration: Exploration, raw: dict[str, object]) -> ExplorationOpportunity: title = str(raw.get("title", "Untitled opportunity")) grouping_key = self._grouping_key(exploration.project, title, str(raw.get("opportunity_type", "FEATURE"))) existing = self._known_duplicate(exploration.project, grouping_key, title) scores = self.score(raw) raw_evidence = raw.get("evidence", {}) evidence = raw_evidence if isinstance(raw_evidence, dict) else {"raw": raw_evidence} if existing is not None: existing.metadata = {**existing.metadata, "rediscovered_by": str(exploration.id)} existing.save(update_fields=["metadata", "updated_at"]) return existing return ExplorationOpportunity.objects.create(exploration=exploration, project=exploration.project, title=title, description=str(raw.get("description", "")), opportunity_type=str(raw.get("opportunity_type", "FEATURE")), evidence=evidence, rationale=str(raw.get("rationale", "")), expected_value=str(raw.get("expected_value", "")), effort_estimate=str(raw.get("effort_estimate", "MEDIUM")), risk=str(raw.get("risk", "MEDIUM")), confidence=scores["confidence"], technical_fit=scores["technical_fit"], strategic_fit=scores["strategic_fit"], value_score=scores["value"], effort_score=scores["effort"], risk_score=scores["risk"], composite_score=scores["composite"], recommended_action=str(raw.get("recommended_action", "DEFER")), grouping_key=grouping_key) def score(self, raw: dict[str, object]) -> dict[str, float]: value = self._score_value(raw.get("value", raw.get("value_score", 0.5))) effort = self._score_value(raw.get("effort", raw.get("effort_score", 0.5))) risk = self._score_value(raw.get("risk_score", 0.5)) confidence = self._score_value(raw.get("confidence", 0.5)) technical_fit = self._score_value(raw.get("technical_fit", 0.5)) strategic_fit = self._score_value(raw.get("strategic_fit", 0.5)) composite = (value * 0.35) + ((1 - effort) * 0.15) + ((1 - risk) * 0.15) + (confidence * 0.15) + (technical_fit * 0.1) + (strategic_fit * 0.1) return {"value": value, "effort": effort, "risk": risk, "confidence": confidence, "technical_fit": technical_fit, "strategic_fit": strategic_fit, "composite": composite} def _score_value(self, value: object) -> float: try: score = float(value) except (TypeError, ValueError): return 0.5 return max(0.0, min(1.0, score)) def convert_to_extension(self, opportunity: ExplorationOpportunity) -> ExtensionCandidate: candidate = ExtensionService(bus=self.bus).create_candidate(opportunity.project, title=opportunity.title, description=opportunity.description, rationale=opportunity.rationale, source="Explorer", expected_value=opportunity.expected_value, affected_areas=[opportunity.opportunity_type], estimated_complexity=opportunity.effort_estimate, risk=opportunity.risk, confidence=opportunity.confidence, evidence=opportunity.evidence, source_opportunity=opportunity) opportunity.converted_extension = candidate opportunity.status = "CONVERTED" opportunity.save(update_fields=["converted_extension", "status", "updated_at"]) return candidate def convert_to_evolution(self, opportunity: ExplorationOpportunity, *, baseline_measurement: dict[str, object], desired_direction: str = "DECREASE") -> EvolutionCandidate: candidate = EvolutionService(bus=self.bus).create_candidate(opportunity.project, target=opportunity.opportunity_type, objective=opportunity.description or opportunity.title, baseline_measurement=baseline_measurement, desired_direction=desired_direction, rationale=opportunity.rationale, source="Explorer", evidence=opportunity.evidence, risk=opportunity.risk, confidence=opportunity.confidence, source_opportunity=opportunity) opportunity.converted_evolution = candidate opportunity.status = "CONVERTED" opportunity.save(update_fields=["converted_evolution", "status", "updated_at"]) return candidate def defer(self, opportunity: ExplorationOpportunity) -> ExplorationOpportunity: opportunity.status = "DEFERRED" opportunity.save(update_fields=["status", "updated_at"]) return opportunity def reject(self, opportunity: ExplorationOpportunity) -> ExplorationOpportunity: opportunity.status = "REJECTED" opportunity.save(update_fields=["status", "updated_at"]) return opportunity def _fallback_opportunities(self, exploration: Exploration) -> list[dict[str, object]]: context = exploration.context_snapshot features = str(context.get("features", "")).lower() opportunities: list[dict[str, object]] = [] if "dashboard" not in features: opportunities.append({"title": "Add project health dashboard", "description": "Expose recent runs, findings, and lifecycle status in a project dashboard.", "opportunity_type": "FEATURE", "evidence": {"missing_feature": "dashboard"}, "rationale": "Operators need a fast project health view.", "expected_value": "Improves observability", "effort_estimate": "MEDIUM", "value": 0.8, "effort": 0.45, "risk_score": 0.3, "confidence": 0.7, "technical_fit": 0.8, "strategic_fit": 0.8, "recommended_action": "EXTEND"}) opportunities.append({"title": "Measure repository analysis latency", "description": "Establish and improve latency for repeated project inspection.", "opportunity_type": "PERFORMANCE", "evidence": {"source": "Explorer fallback"}, "rationale": "Faster analysis improves iteration speed.", "expected_value": "Reduces operational latency", "effort_estimate": "LOW", "value": 0.6, "effort": 0.25, "risk_score": 0.2, "confidence": 0.65, "technical_fit": 0.75, "strategic_fit": 0.65, "recommended_action": "EVOLVE"}) return opportunities def _known_duplicate(self, project: Project, grouping_key: str, title: str) -> ExplorationOpportunity | None: existing = ExplorationOpportunity.objects.filter(project=project, grouping_key=grouping_key).first() if existing: return existing lowered = title.lower() if ExtensionCandidate.objects.filter(project=project, title__iexact=title).exists() or RoadmapItem.objects.filter(project=project, title__iexact=title).exists(): return ExplorationOpportunity.objects.filter(project=project, title__iexact=title).first() if any(item in lowered for item in ["authentication", "auth"]) and Decision.objects.filter(project=project, decision__icontains="authentication").exists(): return ExplorationOpportunity.objects.filter(project=project, title__icontains="auth").first() return None def _grouping_key(self, project: Project, title: str, opportunity_type: str) -> str: fingerprint = hashlib.sha256(f"{title.lower()}:{opportunity_type.lower()}".encode("utf-8")).hexdigest()[:16] return f"{project.id}:{opportunity_type}:{fingerprint}"[:240] class LifecycleInspectionService: def project_lifecycle_view(self, project: Project) -> dict[str, object]: return { "project_id": str(project.id), "repairs": list(project.steward_findings.filter(recommended_action="REPAIR").values("id", "title", "status", "severity")), "extensions": [self._extension(candidate) for candidate in project.extension_candidates.order_by("-created_at")], "evolutions": [self._evolution(candidate) for candidate in project.evolution_candidates.order_by("-created_at")], "explorations": [self._exploration(exploration) for exploration in project.explorations.order_by("-created_at")], } def _extension(self, candidate: ExtensionCandidate) -> dict[str, object]: plan = candidate.plans.order_by("-created_at").first() tasks = Task.objects.filter(milestone__plan=plan.project_plan) if plan and plan.project_plan_id else Task.objects.none() return {"candidate": {"id": str(candidate.id), "title": candidate.title, "status": candidate.status}, "plan": str(plan.id) if plan else None, "tasks": list(tasks.values("id", "status", "goal")), "verification": str(plan.verification_id) if plan and plan.verification_id else None, "commits": list(candidate.commits.values("id", "sha", "task_id"))} def _evolution(self, candidate: EvolutionCandidate) -> dict[str, object]: plan = candidate.plans.order_by("-created_at").first() return {"candidate": {"id": str(candidate.id), "target": candidate.target, "objective": candidate.objective, "status": candidate.status}, "baseline": candidate.baseline_measurement, "target": candidate.target_measurement, "measurement": plan.candidate_measurement if plan else {}, "delta": plan.delta if plan else {}, "verdict": plan.verdict if plan else ""} def _exploration(self, exploration: Exploration) -> dict[str, object]: return {"id": str(exploration.id), "status": exploration.status, "opportunities": list(exploration.opportunities.order_by("-composite_score").values("id", "title", "opportunity_type", "status", "recommended_action", "value_score", "effort_score", "risk_score", "confidence", "technical_fit", "strategic_fit", "composite_score"))} def create_project_planning_signal(project: Project, summary: str, evidence: dict[str, object]) -> ProgenySignal: return ProgenySignal.objects.create(project=project, source="project_lifecycle", severity="MEDIUM", failure_category="PROJECT_PLANNING", summary=summary, evidence=evidence, grouping_key=f"project_lifecycle:{project.id}:planning"[:120])