Files
Aperant/auto-build/implementation_plan.py
T
2025-12-10 09:10:55 +01:00

617 lines
22 KiB
Python

#!/usr/bin/env python3
"""
Implementation Plan Manager
============================
Core data structures and utilities for chunk-based implementation plans.
Replaces the test-centric feature_list.json with implementation_plan.json.
The key insight: Tests verify outcomes, but CHUNKS define implementation steps.
For complex multi-service features, implementation order matters.
Workflow Types:
- feature: Standard multi-service feature (phases = services)
- refactor: Migration/refactor work (phases = stages: add, migrate, remove)
- investigation: Bug hunting (phases = investigate, hypothesize, fix)
- migration: Data migration (phases = prepare, test, execute, cleanup)
- simple: Single-service enhancement (minimal overhead)
"""
import json
from dataclasses import dataclass, field, asdict
from datetime import datetime
from enum import Enum
from pathlib import Path
from typing import Any, Optional
class WorkflowType(str, Enum):
"""Types of workflows with different phase structures."""
FEATURE = "feature" # Multi-service feature (phases = services)
REFACTOR = "refactor" # Stage-based (add new, migrate, remove old)
INVESTIGATION = "investigation" # Bug hunting (investigate, hypothesize, fix)
MIGRATION = "migration" # Data migration (prepare, test, execute, cleanup)
SIMPLE = "simple" # Single-service, minimal overhead
class PhaseType(str, Enum):
"""Types of phases within a workflow."""
SETUP = "setup" # Project scaffolding, environment setup
IMPLEMENTATION = "implementation" # Writing code
INVESTIGATION = "investigation" # Research, debugging, analysis
INTEGRATION = "integration" # Wiring services together
CLEANUP = "cleanup" # Removing old code, polish
class ChunkStatus(str, Enum):
"""Status of a chunk."""
PENDING = "pending" # Not started
IN_PROGRESS = "in_progress" # Currently being worked on
COMPLETED = "completed" # Completed successfully (matches JSON format)
BLOCKED = "blocked" # Can't start (dependency not met or undefined)
FAILED = "failed" # Attempted but failed
class VerificationType(str, Enum):
"""How to verify a chunk is complete."""
COMMAND = "command" # Run a shell command
API = "api" # Make an API request
BROWSER = "browser" # Browser automation check
COMPONENT = "component" # Component renders correctly
MANUAL = "manual" # Requires human verification
NONE = "none" # No verification needed (investigation)
@dataclass
class Verification:
"""How to verify a chunk is complete."""
type: VerificationType
run: Optional[str] = None # Command to run
url: Optional[str] = None # URL for API/browser tests
method: Optional[str] = None # HTTP method for API tests
expect_status: Optional[int] = None # Expected HTTP status
expect_contains: Optional[str] = None # Expected content
scenario: Optional[str] = None # Description for browser/manual tests
def to_dict(self) -> dict:
result = {"type": self.type.value}
for key in ["run", "url", "method", "expect_status", "expect_contains", "scenario"]:
val = getattr(self, key)
if val is not None:
result[key] = val
return result
@classmethod
def from_dict(cls, data: dict) -> "Verification":
return cls(
type=VerificationType(data.get("type", "none")),
run=data.get("run"),
url=data.get("url"),
method=data.get("method"),
expect_status=data.get("expect_status"),
expect_contains=data.get("expect_contains"),
scenario=data.get("scenario"),
)
@dataclass
class Chunk:
"""A single unit of implementation work."""
id: str
description: str
status: ChunkStatus = ChunkStatus.PENDING
# Scoping
service: Optional[str] = None # Which service (backend, frontend, worker)
all_services: bool = False # True for integration chunks
# Files
files_to_modify: list[str] = field(default_factory=list)
files_to_create: list[str] = field(default_factory=list)
patterns_from: list[str] = field(default_factory=list)
# Verification
verification: Optional[Verification] = None
# For investigation chunks
expected_output: Optional[str] = None # Knowledge/decision output
actual_output: Optional[str] = None # What was discovered
# Tracking
started_at: Optional[str] = None
completed_at: Optional[str] = None
session_id: Optional[int] = None # Which session completed this
# Self-Critique
critique_result: Optional[dict] = None # Results from self-critique before completion
def to_dict(self) -> dict:
result = {
"id": self.id,
"description": self.description,
"status": self.status.value,
}
if self.service:
result["service"] = self.service
if self.all_services:
result["all_services"] = True
if self.files_to_modify:
result["files_to_modify"] = self.files_to_modify
if self.files_to_create:
result["files_to_create"] = self.files_to_create
if self.patterns_from:
result["patterns_from"] = self.patterns_from
if self.verification:
result["verification"] = self.verification.to_dict()
if self.expected_output:
result["expected_output"] = self.expected_output
if self.actual_output:
result["actual_output"] = self.actual_output
if self.started_at:
result["started_at"] = self.started_at
if self.completed_at:
result["completed_at"] = self.completed_at
if self.session_id is not None:
result["session_id"] = self.session_id
if self.critique_result:
result["critique_result"] = self.critique_result
return result
@classmethod
def from_dict(cls, data: dict) -> "Chunk":
verification = None
if "verification" in data:
verification = Verification.from_dict(data["verification"])
return cls(
id=data["id"],
description=data["description"],
status=ChunkStatus(data.get("status", "pending")),
service=data.get("service"),
all_services=data.get("all_services", False),
files_to_modify=data.get("files_to_modify", []),
files_to_create=data.get("files_to_create", []),
patterns_from=data.get("patterns_from", []),
verification=verification,
expected_output=data.get("expected_output"),
actual_output=data.get("actual_output"),
started_at=data.get("started_at"),
completed_at=data.get("completed_at"),
session_id=data.get("session_id"),
critique_result=data.get("critique_result"),
)
def start(self, session_id: int):
"""Mark chunk as in progress."""
self.status = ChunkStatus.IN_PROGRESS
self.started_at = datetime.now().isoformat()
self.session_id = session_id
def complete(self, output: Optional[str] = None):
"""Mark chunk as done."""
self.status = ChunkStatus.COMPLETED
self.completed_at = datetime.now().isoformat()
if output:
self.actual_output = output
def fail(self, reason: Optional[str] = None):
"""Mark chunk as failed."""
self.status = ChunkStatus.FAILED
if reason:
self.actual_output = f"FAILED: {reason}"
@dataclass
class Phase:
"""A group of chunks with dependencies."""
phase: int
name: str
type: PhaseType = PhaseType.IMPLEMENTATION
chunks: list[Chunk] = field(default_factory=list)
depends_on: list[int] = field(default_factory=list)
parallel_safe: bool = False # Can chunks in this phase run in parallel?
def to_dict(self) -> dict:
result = {
"phase": self.phase,
"name": self.name,
"type": self.type.value,
"chunks": [c.to_dict() for c in self.chunks],
}
if self.depends_on:
result["depends_on"] = self.depends_on
if self.parallel_safe:
result["parallel_safe"] = True
return result
@classmethod
def from_dict(cls, data: dict) -> "Phase":
return cls(
phase=data["phase"],
name=data["name"],
type=PhaseType(data.get("type", "implementation")),
chunks=[Chunk.from_dict(c) for c in data.get("chunks", [])],
depends_on=data.get("depends_on", []),
parallel_safe=data.get("parallel_safe", False),
)
def is_complete(self) -> bool:
"""Check if all chunks in this phase are done."""
return all(c.status == ChunkStatus.COMPLETED for c in self.chunks)
def get_pending_chunks(self) -> list[Chunk]:
"""Get chunks that can be worked on."""
return [c for c in self.chunks if c.status == ChunkStatus.PENDING]
def get_progress(self) -> tuple[int, int]:
"""Get (completed, total) chunk counts."""
done = sum(1 for c in self.chunks if c.status == ChunkStatus.COMPLETED)
return done, len(self.chunks)
@dataclass
class ImplementationPlan:
"""Complete implementation plan for a feature/task."""
feature: str
workflow_type: WorkflowType = WorkflowType.FEATURE
services_involved: list[str] = field(default_factory=list)
phases: list[Phase] = field(default_factory=list)
final_acceptance: list[str] = field(default_factory=list)
# Metadata
created_at: Optional[str] = None
updated_at: Optional[str] = None
spec_file: Optional[str] = None
def to_dict(self) -> dict:
return {
"feature": self.feature,
"workflow_type": self.workflow_type.value,
"services_involved": self.services_involved,
"phases": [p.to_dict() for p in self.phases],
"final_acceptance": self.final_acceptance,
"created_at": self.created_at,
"updated_at": self.updated_at,
"spec_file": self.spec_file,
}
@classmethod
def from_dict(cls, data: dict) -> "ImplementationPlan":
return cls(
feature=data["feature"],
workflow_type=WorkflowType(data.get("workflow_type", "feature")),
services_involved=data.get("services_involved", []),
phases=[Phase.from_dict(p) for p in data.get("phases", [])],
final_acceptance=data.get("final_acceptance", []),
created_at=data.get("created_at"),
updated_at=data.get("updated_at"),
spec_file=data.get("spec_file"),
)
def save(self, path: Path):
"""Save plan to JSON file."""
self.updated_at = datetime.now().isoformat()
if not self.created_at:
self.created_at = self.updated_at
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, "w") as f:
json.dump(self.to_dict(), f, indent=2)
@classmethod
def load(cls, path: Path) -> "ImplementationPlan":
"""Load plan from JSON file."""
with open(path) as f:
return cls.from_dict(json.load(f))
def get_available_phases(self) -> list[Phase]:
"""Get phases whose dependencies are satisfied."""
completed_phases = {p.phase for p in self.phases if p.is_complete()}
available = []
for phase in self.phases:
if phase.is_complete():
continue
deps_met = all(d in completed_phases for d in phase.depends_on)
if deps_met:
available.append(phase)
return available
def get_next_chunk(self) -> Optional[tuple[Phase, Chunk]]:
"""Get the next chunk to work on, respecting dependencies."""
for phase in self.get_available_phases():
pending = phase.get_pending_chunks()
if pending:
return phase, pending[0]
return None
def get_progress(self) -> dict:
"""Get overall progress statistics."""
total_chunks = sum(len(p.chunks) for p in self.phases)
done_chunks = sum(
1 for p in self.phases
for c in p.chunks
if c.status == ChunkStatus.COMPLETED
)
failed_chunks = sum(
1 for p in self.phases
for c in p.chunks
if c.status == ChunkStatus.FAILED
)
completed_phases = sum(1 for p in self.phases if p.is_complete())
return {
"total_phases": len(self.phases),
"completed_phases": completed_phases,
"total_chunks": total_chunks,
"completed_chunks": done_chunks,
"failed_chunks": failed_chunks,
"percent_complete": round(100 * done_chunks / total_chunks, 1) if total_chunks > 0 else 0,
"is_complete": done_chunks == total_chunks and failed_chunks == 0,
}
def get_status_summary(self) -> str:
"""Get a human-readable status summary."""
progress = self.get_progress()
lines = [
f"Feature: {self.feature}",
f"Workflow: {self.workflow_type.value}",
f"Progress: {progress['completed_chunks']}/{progress['total_chunks']} chunks ({progress['percent_complete']}%)",
f"Phases: {progress['completed_phases']}/{progress['total_phases']} complete",
]
if progress['failed_chunks'] > 0:
lines.append(f"Failed: {progress['failed_chunks']} chunks need attention")
if progress['is_complete']:
lines.append("Status: COMPLETE - Ready for final acceptance testing")
else:
next_work = self.get_next_chunk()
if next_work:
phase, chunk = next_work
lines.append(f"Next: Phase {phase.phase} ({phase.name}) - {chunk.description}")
else:
lines.append("Status: BLOCKED - No available chunks")
return "\n".join(lines)
def create_feature_plan(
feature: str,
services: list[str],
phases_config: list[dict],
) -> ImplementationPlan:
"""
Create a standard feature implementation plan.
Args:
feature: Name of the feature
services: List of services involved
phases_config: List of phase configurations
Returns:
ImplementationPlan ready for use
"""
phases = []
for i, config in enumerate(phases_config, 1):
chunks = [Chunk.from_dict(c) for c in config.get("chunks", [])]
phase = Phase(
phase=i,
name=config["name"],
type=PhaseType(config.get("type", "implementation")),
chunks=chunks,
depends_on=config.get("depends_on", []),
parallel_safe=config.get("parallel_safe", False),
)
phases.append(phase)
return ImplementationPlan(
feature=feature,
workflow_type=WorkflowType.FEATURE,
services_involved=services,
phases=phases,
created_at=datetime.now().isoformat(),
)
def create_investigation_plan(
bug_description: str,
services: list[str],
) -> ImplementationPlan:
"""
Create an investigation plan for debugging.
This creates a structured approach:
1. Reproduce & Instrument
2. Investigate
3. Fix (blocked until investigation complete)
"""
phases = [
Phase(
phase=1,
name="Reproduce & Instrument",
type=PhaseType.INVESTIGATION,
chunks=[
Chunk(
id="add-logging",
description="Add detailed logging around suspected areas",
expected_output="Logs capture relevant state and events",
),
Chunk(
id="create-repro",
description="Create reliable reproduction steps",
expected_output="Can reproduce bug on demand",
),
],
),
Phase(
phase=2,
name="Identify Root Cause",
type=PhaseType.INVESTIGATION,
depends_on=[1],
chunks=[
Chunk(
id="analyze",
description="Analyze logs and behavior",
expected_output="Root cause hypothesis with evidence",
),
],
),
Phase(
phase=3,
name="Implement Fix",
type=PhaseType.IMPLEMENTATION,
depends_on=[2],
chunks=[
Chunk(
id="fix",
description="[TO BE DETERMINED FROM INVESTIGATION]",
status=ChunkStatus.BLOCKED,
),
Chunk(
id="regression-test",
description="Add regression test to prevent recurrence",
status=ChunkStatus.BLOCKED,
),
],
),
]
return ImplementationPlan(
feature=f"Fix: {bug_description}",
workflow_type=WorkflowType.INVESTIGATION,
services_involved=services,
phases=phases,
created_at=datetime.now().isoformat(),
)
def create_refactor_plan(
refactor_description: str,
services: list[str],
stages: list[dict],
) -> ImplementationPlan:
"""
Create a refactor plan with stage-based phases.
Typical stages:
1. Add new system alongside old
2. Migrate consumers
3. Remove old system
4. Cleanup
"""
phases = []
for i, stage in enumerate(stages, 1):
chunks = [Chunk.from_dict(c) for c in stage.get("chunks", [])]
phase = Phase(
phase=i,
name=stage["name"],
type=PhaseType(stage.get("type", "implementation")),
chunks=chunks,
depends_on=stage.get("depends_on", [i - 1] if i > 1 else []),
)
phases.append(phase)
return ImplementationPlan(
feature=refactor_description,
workflow_type=WorkflowType.REFACTOR,
services_involved=services,
phases=phases,
created_at=datetime.now().isoformat(),
)
# CLI for testing
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Usage: python implementation_plan.py <plan.json>")
print(" python implementation_plan.py --demo")
sys.exit(1)
if sys.argv[1] == "--demo":
# Create a demo plan
plan = create_feature_plan(
feature="Avatar Upload with Processing",
services=["backend", "worker", "frontend"],
phases_config=[
{
"name": "Backend Foundation",
"parallel_safe": True,
"chunks": [
{
"id": "avatar-model",
"service": "backend",
"description": "Add avatar fields to User model",
"files_to_modify": ["app/models/user.py"],
"files_to_create": ["migrations/add_avatar.py"],
"verification": {"type": "command", "run": "flask db upgrade"},
},
{
"id": "avatar-endpoint",
"service": "backend",
"description": "POST /api/users/avatar endpoint",
"files_to_modify": ["app/routes/users.py"],
"patterns_from": ["app/routes/profile.py"],
"verification": {"type": "api", "method": "POST", "url": "/api/users/avatar"},
},
],
},
{
"name": "Worker Pipeline",
"depends_on": [1],
"chunks": [
{
"id": "image-task",
"service": "worker",
"description": "Celery task for image processing",
"files_to_create": ["app/tasks/images.py"],
"patterns_from": ["app/tasks/reports.py"],
},
],
},
{
"name": "Frontend",
"depends_on": [1],
"chunks": [
{
"id": "avatar-component",
"service": "frontend",
"description": "AvatarUpload React component",
"files_to_create": ["src/components/AvatarUpload.tsx"],
"patterns_from": ["src/components/FileUpload.tsx"],
},
],
},
{
"name": "Integration",
"depends_on": [2, 3],
"type": "integration",
"chunks": [
{
"id": "e2e-wiring",
"all_services": True,
"description": "Connect frontend → backend → worker",
"verification": {"type": "browser", "scenario": "Upload → Process → Display"},
},
],
},
],
)
plan.final_acceptance = [
"User can upload avatar from profile page",
"Avatar is automatically resized",
"Large/invalid files show error",
]
print(json.dumps(plan.to_dict(), indent=2))
print("\n---\n")
print(plan.get_status_summary())
else:
# Load and display existing plan
plan = ImplementationPlan.load(Path(sys.argv[1]))
print(plan.get_status_summary())