devcouncil 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -308
- package/bin/devcouncil.js +130 -32
- package/package.json +22 -19
- package/pyproject.toml +0 -66
- package/src/devcouncil/__init__.py +0 -0
- package/src/devcouncil/__main__.py +0 -4
- package/src/devcouncil/app/__init__.py +0 -28
- package/src/devcouncil/app/config.py +0 -320
- package/src/devcouncil/app/errors.py +0 -23
- package/src/devcouncil/app/events.py +0 -44
- package/src/devcouncil/app/orchestrator.py +0 -92
- package/src/devcouncil/app/project_status.py +0 -29
- package/src/devcouncil/app/run_context.py +0 -39
- package/src/devcouncil/app/state_machine.py +0 -108
- package/src/devcouncil/artifacts/__init__.py +0 -1
- package/src/devcouncil/artifacts/coverage.py +0 -96
- package/src/devcouncil/artifacts/graph.py +0 -163
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/artifacts/validators.py +0 -27
- package/src/devcouncil/assets/__init__.py +0 -1
- package/src/devcouncil/assets/devcouncil-logo.svg +0 -60
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/agents.py +0 -292
- package/src/devcouncil/cli/commands/artifacts.py +0 -54
- package/src/devcouncil/cli/commands/ast.py +0 -22
- package/src/devcouncil/cli/commands/baseline.py +0 -35
- package/src/devcouncil/cli/commands/check.py +0 -209
- package/src/devcouncil/cli/commands/config.py +0 -115
- package/src/devcouncil/cli/commands/cost.py +0 -57
- package/src/devcouncil/cli/commands/dashboard.py +0 -31
- package/src/devcouncil/cli/commands/doctor.py +0 -340
- package/src/devcouncil/cli/commands/evidence.py +0 -48
- package/src/devcouncil/cli/commands/go.py +0 -656
- package/src/devcouncil/cli/commands/handoff.py +0 -69
- package/src/devcouncil/cli/commands/hook.py +0 -237
- package/src/devcouncil/cli/commands/init.py +0 -289
- package/src/devcouncil/cli/commands/integrate.py +0 -1515
- package/src/devcouncil/cli/commands/lsp.py +0 -20
- package/src/devcouncil/cli/commands/map.py +0 -112
- package/src/devcouncil/cli/commands/mcp_server.py +0 -18
- package/src/devcouncil/cli/commands/plan.py +0 -488
- package/src/devcouncil/cli/commands/prompt.py +0 -61
- package/src/devcouncil/cli/commands/repair.py +0 -89
- package/src/devcouncil/cli/commands/report.py +0 -137
- package/src/devcouncil/cli/commands/reset_demo_state.py +0 -33
- package/src/devcouncil/cli/commands/rollback.py +0 -59
- package/src/devcouncil/cli/commands/run.py +0 -289
- package/src/devcouncil/cli/commands/runs.py +0 -223
- package/src/devcouncil/cli/commands/scaffold.py +0 -32
- package/src/devcouncil/cli/commands/semantic.py +0 -47
- package/src/devcouncil/cli/commands/setup.py +0 -362
- package/src/devcouncil/cli/commands/shell.py +0 -73
- package/src/devcouncil/cli/commands/show.py +0 -76
- package/src/devcouncil/cli/commands/skills.py +0 -88
- package/src/devcouncil/cli/commands/status.py +0 -141
- package/src/devcouncil/cli/commands/tasks.py +0 -55
- package/src/devcouncil/cli/commands/trace.py +0 -88
- package/src/devcouncil/cli/commands/verify.py +0 -328
- package/src/devcouncil/cli/commands/version.py +0 -20
- package/src/devcouncil/cli/commands/watch.py +0 -574
- package/src/devcouncil/cli/commands/watch_fs.py +0 -40
- package/src/devcouncil/cli/main.py +0 -137
- package/src/devcouncil/council/__init__.py +0 -0
- package/src/devcouncil/council/prompts/__init__.py +0 -0
- package/src/devcouncil/council/prompts/arbiter.md +0 -19
- package/src/devcouncil/council/prompts/critic_a.md +0 -10
- package/src/devcouncil/council/prompts/critic_b.md +0 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
- package/src/devcouncil/council/prompts/planner_a.md +0 -16
- package/src/devcouncil/council/prompts/planner_b.md +0 -16
- package/src/devcouncil/council/prompts/rebuttal.md +0 -10
- package/src/devcouncil/council/prompts/spec_writer.md +0 -12
- package/src/devcouncil/domain/__init__.py +0 -0
- package/src/devcouncil/domain/assumption.py +0 -17
- package/src/devcouncil/domain/critique.py +0 -32
- package/src/devcouncil/domain/evidence.py +0 -47
- package/src/devcouncil/domain/gap.py +0 -52
- package/src/devcouncil/domain/requirement.py +0 -22
- package/src/devcouncil/domain/task.py +0 -55
- package/src/devcouncil/execution/__init__.py +0 -1
- package/src/devcouncil/execution/checkpoints.py +0 -246
- package/src/devcouncil/execution/context_builder.py +0 -60
- package/src/devcouncil/execution/executor.py +0 -15
- package/src/devcouncil/execution/fs_watcher.py +0 -180
- package/src/devcouncil/execution/handoff.py +0 -102
- package/src/devcouncil/execution/hook_policy.py +0 -253
- package/src/devcouncil/execution/patch.py +0 -77
- package/src/devcouncil/execution/paths.py +0 -14
- package/src/devcouncil/execution/permissions.py +0 -85
- package/src/devcouncil/execution/policy_engine.py +0 -343
- package/src/devcouncil/execution/prompt_builder.py +0 -671
- package/src/devcouncil/execution/shell_session.py +0 -225
- package/src/devcouncil/execution/task_runner.py +0 -170
- package/src/devcouncil/executors/__init__.py +0 -1
- package/src/devcouncil/executors/agent_registry.py +0 -575
- package/src/devcouncil/executors/coding_cli.py +0 -736
- package/src/devcouncil/executors/mini_swe.py +0 -73
- package/src/devcouncil/executors/native/__init__.py +0 -0
- package/src/devcouncil/executors/native/agent.py +0 -208
- package/src/devcouncil/executors/openhands.py +0 -71
- package/src/devcouncil/gating/__init__.py +0 -1
- package/src/devcouncil/gating/checks/__init__.py +0 -0
- package/src/devcouncil/gating/checks/clean_git.py +0 -52
- package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +0 -53
- package/src/devcouncil/gating/policy.py +0 -338
- package/src/devcouncil/hardware.py +0 -184
- package/src/devcouncil/indexing/__init__.py +0 -1
- package/src/devcouncil/indexing/ast_matcher.py +0 -168
- package/src/devcouncil/indexing/graph_index.py +0 -48
- package/src/devcouncil/indexing/lsp.py +0 -161
- package/src/devcouncil/indexing/repo_mapper.py +0 -1455
- package/src/devcouncil/indexing/semantic_index.py +0 -205
- package/src/devcouncil/integrations/actions.py +0 -146
- package/src/devcouncil/integrations/check.py +0 -423
- package/src/devcouncil/integrations/code_review_graph.py +0 -163
- package/src/devcouncil/integrations/github.py +0 -39
- package/src/devcouncil/integrations/github_intent.py +0 -142
- package/src/devcouncil/integrations/gitnexus.py +0 -62
- package/src/devcouncil/integrations/graphify.py +0 -34
- package/src/devcouncil/integrations/mcp/__init__.py +0 -0
- package/src/devcouncil/integrations/mcp/server.py +0 -2122
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +0 -24
- package/src/devcouncil/integrations/pr_comments.py +0 -62
- package/src/devcouncil/live/__init__.py +0 -2
- package/src/devcouncil/live/cards.py +0 -349
- package/src/devcouncil/live/models.py +0 -63
- package/src/devcouncil/live/repair_prompt.py +0 -83
- package/src/devcouncil/live/reviewer.py +0 -70
- package/src/devcouncil/live/signals.py +0 -135
- package/src/devcouncil/live/summary.py +0 -34
- package/src/devcouncil/live/tasks.py +0 -18
- package/src/devcouncil/live/transcripts.py +0 -141
- package/src/devcouncil/llm/__init__.py +0 -1
- package/src/devcouncil/llm/cache.py +0 -42
- package/src/devcouncil/llm/model_defaults.yaml +0 -44
- package/src/devcouncil/llm/provider.py +0 -627
- package/src/devcouncil/llm/router.py +0 -310
- package/src/devcouncil/optimization/__init__.py +0 -1
- package/src/devcouncil/optimization/gepa_agent.py +0 -318
- package/src/devcouncil/planning/__init__.py +0 -1
- package/src/devcouncil/planning/arbiter_service.py +0 -57
- package/src/devcouncil/planning/correction_manifest.py +0 -303
- package/src/devcouncil/planning/critique_service.py +0 -71
- package/src/devcouncil/planning/plan_service.py +0 -60
- package/src/devcouncil/planning/prompt_enhancer_service.py +0 -167
- package/src/devcouncil/planning/repair_service.py +0 -39
- package/src/devcouncil/planning/spec_service.py +0 -70
- package/src/devcouncil/repo/__init__.py +0 -0
- package/src/devcouncil/repo/ci_scaffold.py +0 -157
- package/src/devcouncil/repo/gitignore.py +0 -123
- package/src/devcouncil/repo/sca.py +0 -374
- package/src/devcouncil/reporting/__init__.py +0 -0
- package/src/devcouncil/reporting/github_check.py +0 -32
- package/src/devcouncil/reporting/json_report.py +0 -30
- package/src/devcouncil/reporting/markdown_report.py +0 -83
- package/src/devcouncil/reporting/report_builder.py +0 -14
- package/src/devcouncil/skills/__init__.py +0 -19
- package/src/devcouncil/skills/library/README.md +0 -46
- package/src/devcouncil/skills/library/ai-training.md +0 -50
- package/src/devcouncil/skills/library/android.md +0 -50
- package/src/devcouncil/skills/library/backend.md +0 -52
- package/src/devcouncil/skills/library/core-engineering.md +0 -95
- package/src/devcouncil/skills/library/data-engineering.md +0 -47
- package/src/devcouncil/skills/library/desktop.md +0 -46
- package/src/devcouncil/skills/library/devops.md +0 -48
- package/src/devcouncil/skills/library/game-dev.md +0 -46
- package/src/devcouncil/skills/library/ios.md +0 -48
- package/src/devcouncil/skills/library/mobile-cross-platform.md +0 -46
- package/src/devcouncil/skills/library/security.md +0 -48
- package/src/devcouncil/skills/library/systems.md +0 -48
- package/src/devcouncil/skills/library/web.md +0 -47
- package/src/devcouncil/skills/library/windows.md +0 -47
- package/src/devcouncil/skills/registry.py +0 -330
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +0 -147
- package/src/devcouncil/storage/models.py +0 -204
- package/src/devcouncil/storage/native.py +0 -557
- package/src/devcouncil/storage/repositories.py +0 -485
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +0 -140
- package/src/devcouncil/telemetry/model_pricing.yaml +0 -48
- package/src/devcouncil/telemetry/pricing.py +0 -28
- package/src/devcouncil/telemetry/traces.py +0 -146
- package/src/devcouncil/telemetry/tracker.py +0 -52
- package/src/devcouncil/ui/__init__.py +0 -1
- package/src/devcouncil/ui/dashboard.py +0 -423
- package/src/devcouncil/utils/__init__.py +0 -1
- package/src/devcouncil/utils/redaction.py +0 -147
- package/src/devcouncil/utils/subprocess_env.py +0 -69
- package/src/devcouncil/verification/__init__.py +0 -1
- package/src/devcouncil/verification/acceptance_compiler.py +0 -125
- package/src/devcouncil/verification/ad_hoc_check.py +0 -129
- package/src/devcouncil/verification/diff_coverage.py +0 -353
- package/src/devcouncil/verification/implementation_reviewer.py +0 -55
- package/src/devcouncil/verification/next_actions.py +0 -189
- package/src/devcouncil/verification/sandbox.py +0 -178
- package/src/devcouncil/verification/test_resolver.py +0 -91
- package/src/devcouncil/verification/verifier.py +0 -1548
- package/uv.lock +0 -1226
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
class DevCouncilError(Exception):
|
|
2
|
-
"""Base exception for all DevCouncil errors."""
|
|
3
|
-
pass
|
|
4
|
-
|
|
5
|
-
class GatingError(DevCouncilError):
|
|
6
|
-
"""Raised when a task or plan fails a mandatory gate."""
|
|
7
|
-
pass
|
|
8
|
-
|
|
9
|
-
class ConfigurationError(DevCouncilError):
|
|
10
|
-
"""Raised when the project configuration is invalid or missing."""
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
|
-
class OrchestrationError(DevCouncilError):
|
|
14
|
-
"""Raised when the orchestrator encounters an unexpected state."""
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
class ExecutionError(DevCouncilError):
|
|
18
|
-
"""Raised when an external or internal executor fails to run a task."""
|
|
19
|
-
pass
|
|
20
|
-
|
|
21
|
-
class VerificationError(DevCouncilError):
|
|
22
|
-
"""Raised when the verifier fails to run its checks properly."""
|
|
23
|
-
pass
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
from typing import Any, Callable, Dict, List
|
|
2
|
-
import inspect
|
|
3
|
-
import logging
|
|
4
|
-
|
|
5
|
-
logger = logging.getLogger(__name__)
|
|
6
|
-
|
|
7
|
-
class EventBus:
|
|
8
|
-
"""Simple asynchronous event bus for DevCouncil orchestration."""
|
|
9
|
-
def __init__(self):
|
|
10
|
-
self._listeners: Dict[str, List[Callable]] = {}
|
|
11
|
-
|
|
12
|
-
def subscribe(self, event_type: str, callback: Callable):
|
|
13
|
-
if event_type not in self._listeners:
|
|
14
|
-
self._listeners[event_type] = []
|
|
15
|
-
self._listeners[event_type].append(callback)
|
|
16
|
-
|
|
17
|
-
async def emit(self, event_type: str, payload: Any = None):
|
|
18
|
-
"""Emit an event asynchronously to all registered listeners.
|
|
19
|
-
|
|
20
|
-
Supports both sync and async callbacks.
|
|
21
|
-
"""
|
|
22
|
-
logger.debug(f"Event emitted: {event_type}")
|
|
23
|
-
if event_type in self._listeners:
|
|
24
|
-
for callback in self._listeners[event_type]:
|
|
25
|
-
try:
|
|
26
|
-
if inspect.iscoroutinefunction(callback):
|
|
27
|
-
await callback(payload)
|
|
28
|
-
else:
|
|
29
|
-
callback(payload)
|
|
30
|
-
except Exception as e:
|
|
31
|
-
logger.error(f"Error in event listener for {event_type}: {e}")
|
|
32
|
-
|
|
33
|
-
# Global event bus instance
|
|
34
|
-
bus = EventBus()
|
|
35
|
-
|
|
36
|
-
# Standard Event Types
|
|
37
|
-
class EventTypes:
|
|
38
|
-
PLANNING_STARTED = "planning_started"
|
|
39
|
-
PLANNING_COMPLETED = "planning_completed"
|
|
40
|
-
TASK_EXECUTING = "task_executing"
|
|
41
|
-
TASK_VERIFIED = "task_verified"
|
|
42
|
-
TASK_BLOCKED = "task_blocked"
|
|
43
|
-
GATE_FAILED = "gate_failed"
|
|
44
|
-
MODEL_CALLED = "model_called"
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import json
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Optional, Any
|
|
5
|
-
|
|
6
|
-
from devcouncil.app.state_machine import StateMachine, ProjectPhase
|
|
7
|
-
from devcouncil.app.run_context import RunContext
|
|
8
|
-
from devcouncil.app.events import bus, EventTypes
|
|
9
|
-
from devcouncil.storage.db import get_db
|
|
10
|
-
from devcouncil.storage.repositories import StateRepository
|
|
11
|
-
from devcouncil.telemetry.traces import TraceLogger
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
class Orchestrator:
|
|
16
|
-
"""Central orchestrator managing the workflow lifecycle and state transitions."""
|
|
17
|
-
def __init__(self, project_root: Path, persist_state: bool = True):
|
|
18
|
-
self.project_root = project_root
|
|
19
|
-
self.persist_state = persist_state
|
|
20
|
-
|
|
21
|
-
db = get_db(self.project_root)
|
|
22
|
-
if db:
|
|
23
|
-
with db.get_session() as session:
|
|
24
|
-
repo = StateRepository(session)
|
|
25
|
-
state = repo.get_state()
|
|
26
|
-
if state:
|
|
27
|
-
self.state_machine = StateMachine(ProjectPhase(state.current_phase))
|
|
28
|
-
self.state_machine._history = [ProjectPhase(p) for p in json.loads(state.history_json)]
|
|
29
|
-
else:
|
|
30
|
-
self.state_machine = StateMachine(ProjectPhase.NEW)
|
|
31
|
-
else:
|
|
32
|
-
self.state_machine = StateMachine(ProjectPhase.NEW)
|
|
33
|
-
|
|
34
|
-
self.current_run: Optional[RunContext] = None
|
|
35
|
-
|
|
36
|
-
def reset_state_machine(self, initial: ProjectPhase = ProjectPhase.NEW):
|
|
37
|
-
"""Start a new lifecycle sequence without depending on prior persisted phase."""
|
|
38
|
-
self.state_machine = StateMachine(initial)
|
|
39
|
-
|
|
40
|
-
async def start_run(self, run_id: str, goal: str) -> RunContext:
|
|
41
|
-
"""Start a new orchestration run."""
|
|
42
|
-
self.current_run = RunContext(
|
|
43
|
-
run_id=run_id,
|
|
44
|
-
project_root=str(self.project_root),
|
|
45
|
-
goal=goal
|
|
46
|
-
)
|
|
47
|
-
self.current_run.initialize()
|
|
48
|
-
TraceLogger(self.project_root).log_event(
|
|
49
|
-
"planning_started",
|
|
50
|
-
{"goal": goal},
|
|
51
|
-
run_id=run_id,
|
|
52
|
-
summary=f"Planning started: {goal}",
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
await bus.emit(EventTypes.PLANNING_STARTED, {"run_id": run_id, "goal": goal})
|
|
56
|
-
return self.current_run
|
|
57
|
-
|
|
58
|
-
async def transition_to(self, target_phase: ProjectPhase):
|
|
59
|
-
"""Transition the project phase and emit events."""
|
|
60
|
-
old_phase = self.state_machine.phase
|
|
61
|
-
self.state_machine.transition(target_phase)
|
|
62
|
-
|
|
63
|
-
db = get_db(self.project_root)
|
|
64
|
-
if db and self.persist_state:
|
|
65
|
-
with db.get_session() as session:
|
|
66
|
-
repo = StateRepository(session)
|
|
67
|
-
repo.save_state(
|
|
68
|
-
self.state_machine.phase.value,
|
|
69
|
-
[p.value for p in self.state_machine.history]
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
logger.info(f"Transitioned from {old_phase.value} to {target_phase.value}")
|
|
73
|
-
TraceLogger(self.project_root).log_event(
|
|
74
|
-
"phase_transition",
|
|
75
|
-
{"from": old_phase.value, "to": target_phase.value},
|
|
76
|
-
run_id=self.current_run.run_id if self.current_run else None,
|
|
77
|
-
summary=f"{old_phase.value} -> {target_phase.value}",
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
if target_phase == ProjectPhase.PLAN_APPROVED:
|
|
81
|
-
await bus.emit(EventTypes.PLANNING_COMPLETED, {"run_id": self.current_run.run_id if self.current_run else None})
|
|
82
|
-
|
|
83
|
-
def save_run_artifact(self, name: str, data: Any, is_json: bool = True):
|
|
84
|
-
"""Save a local artifact specific to this run (in .devcouncil/runs/)."""
|
|
85
|
-
if not self.current_run:
|
|
86
|
-
logger.warning("Attempted to save artifact without an active run context.")
|
|
87
|
-
return
|
|
88
|
-
|
|
89
|
-
if is_json:
|
|
90
|
-
self.current_run.save_json_artifact(name, data)
|
|
91
|
-
else:
|
|
92
|
-
self.current_run.save_artifact(name, data)
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from devcouncil.artifacts.graph import ArtifactGraph
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def compute_phase(graph: ArtifactGraph, persisted_phase: str | None = None) -> str:
|
|
7
|
-
"""Compute the current project phase, honoring explicit persisted state first."""
|
|
8
|
-
if persisted_phase:
|
|
9
|
-
return persisted_phase
|
|
10
|
-
|
|
11
|
-
reqs = list(graph.requirements.values())
|
|
12
|
-
tasks = list(graph.tasks.values())
|
|
13
|
-
blocking_gaps = graph.blocking_gaps()
|
|
14
|
-
if not reqs and not tasks:
|
|
15
|
-
return "NEW"
|
|
16
|
-
if reqs and not tasks:
|
|
17
|
-
return "REQUIREMENTS_DRAFTED"
|
|
18
|
-
if blocking_gaps:
|
|
19
|
-
return "TASK_BLOCKED"
|
|
20
|
-
if tasks:
|
|
21
|
-
statuses = {task.status for task in tasks}
|
|
22
|
-
if "running" in statuses:
|
|
23
|
-
return "TASK_EXECUTING"
|
|
24
|
-
if "blocked" in statuses:
|
|
25
|
-
return "TASK_BLOCKED"
|
|
26
|
-
if all(status in {"verified", "done"} for status in statuses):
|
|
27
|
-
return "PROJECT_DONE"
|
|
28
|
-
return "PLAN_APPROVED"
|
|
29
|
-
return "NEW"
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from datetime import datetime, timezone
|
|
3
|
-
from pydantic import BaseModel, Field
|
|
4
|
-
from typing import Optional, List, Dict, Any
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
|
|
8
|
-
logger = logging.getLogger(__name__)
|
|
9
|
-
|
|
10
|
-
class RunContext(BaseModel):
|
|
11
|
-
"""Context object encapsulating the current execution run."""
|
|
12
|
-
run_id: str
|
|
13
|
-
project_root: str
|
|
14
|
-
goal: Optional[str] = None
|
|
15
|
-
start_time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
16
|
-
active_tasks: List[str] = Field(default_factory=list)
|
|
17
|
-
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
18
|
-
|
|
19
|
-
@property
|
|
20
|
-
def run_dir(self) -> Path:
|
|
21
|
-
return Path(self.project_root) / ".devcouncil" / "runs" / self.run_id
|
|
22
|
-
|
|
23
|
-
def initialize(self):
|
|
24
|
-
"""Create the necessary run directory and sub-folders."""
|
|
25
|
-
self.run_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
-
logger.info(f"Initialized run context at {self.run_dir}")
|
|
27
|
-
|
|
28
|
-
def save_artifact(self, filename: str, content: str):
|
|
29
|
-
"""Save a text artifact associated with this run."""
|
|
30
|
-
path = self.run_dir / filename
|
|
31
|
-
path.write_text(content, encoding="utf-8")
|
|
32
|
-
logger.debug(f"Saved artifact to {path}")
|
|
33
|
-
|
|
34
|
-
def save_json_artifact(self, filename: str, data: Any):
|
|
35
|
-
"""Save a JSON artifact associated with this run."""
|
|
36
|
-
path = self.run_dir / filename
|
|
37
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
38
|
-
json.dump(data, f, indent=2)
|
|
39
|
-
logger.debug(f"Saved JSON artifact to {path}")
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
"""Gating state machine for DevCouncil project lifecycle.
|
|
2
|
-
|
|
3
|
-
States (from §12):
|
|
4
|
-
NEW -> REPO_MAPPED -> REQUIREMENTS_DRAFTED -> PLANS_GENERATED
|
|
5
|
-
-> CRITIQUES_GENERATED -> ARBITRATED -> AWAITING_USER_DECISIONS
|
|
6
|
-
-> PLAN_APPROVED -> TASK_READY -> TASK_EXECUTING -> TASK_VERIFYING
|
|
7
|
-
-> TASK_BLOCKED | TASK_VERIFIED -> PROJECT_DONE
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
from enum import Enum
|
|
13
|
-
from typing import Dict, List, Set
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class ProjectPhase(str, Enum):
|
|
18
|
-
NEW = "NEW"
|
|
19
|
-
REPO_MAPPED = "REPO_MAPPED"
|
|
20
|
-
REQUIREMENTS_DRAFTED = "REQUIREMENTS_DRAFTED"
|
|
21
|
-
PLANS_GENERATED = "PLANS_GENERATED"
|
|
22
|
-
CRITIQUES_GENERATED = "CRITIQUES_GENERATED"
|
|
23
|
-
ARBITRATED = "ARBITRATED"
|
|
24
|
-
AWAITING_USER_DECISIONS = "AWAITING_USER_DECISIONS"
|
|
25
|
-
PLAN_APPROVED = "PLAN_APPROVED"
|
|
26
|
-
TASK_READY = "TASK_READY"
|
|
27
|
-
TASK_EXECUTING = "TASK_EXECUTING"
|
|
28
|
-
TASK_VERIFYING = "TASK_VERIFYING"
|
|
29
|
-
TASK_BLOCKED = "TASK_BLOCKED"
|
|
30
|
-
TASK_VERIFIED = "TASK_VERIFIED"
|
|
31
|
-
PROJECT_DONE = "PROJECT_DONE"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# Valid transitions: from_phase -> set of valid next phases
|
|
35
|
-
TRANSITIONS: Dict[ProjectPhase, Set[ProjectPhase]] = {
|
|
36
|
-
ProjectPhase.NEW: {ProjectPhase.REPO_MAPPED},
|
|
37
|
-
ProjectPhase.REPO_MAPPED: {ProjectPhase.REQUIREMENTS_DRAFTED},
|
|
38
|
-
ProjectPhase.REQUIREMENTS_DRAFTED: {ProjectPhase.PLANS_GENERATED},
|
|
39
|
-
ProjectPhase.PLANS_GENERATED: {ProjectPhase.CRITIQUES_GENERATED},
|
|
40
|
-
ProjectPhase.CRITIQUES_GENERATED: {ProjectPhase.ARBITRATED},
|
|
41
|
-
ProjectPhase.ARBITRATED: {
|
|
42
|
-
ProjectPhase.AWAITING_USER_DECISIONS,
|
|
43
|
-
ProjectPhase.PLAN_APPROVED,
|
|
44
|
-
},
|
|
45
|
-
ProjectPhase.AWAITING_USER_DECISIONS: {ProjectPhase.PLAN_APPROVED},
|
|
46
|
-
ProjectPhase.PLAN_APPROVED: {ProjectPhase.TASK_READY},
|
|
47
|
-
ProjectPhase.TASK_READY: {ProjectPhase.TASK_EXECUTING},
|
|
48
|
-
ProjectPhase.TASK_EXECUTING: {ProjectPhase.TASK_VERIFYING},
|
|
49
|
-
ProjectPhase.TASK_VERIFYING: {
|
|
50
|
-
ProjectPhase.TASK_VERIFIED,
|
|
51
|
-
ProjectPhase.TASK_BLOCKED,
|
|
52
|
-
},
|
|
53
|
-
ProjectPhase.TASK_BLOCKED: {
|
|
54
|
-
ProjectPhase.TASK_READY, # after repair
|
|
55
|
-
},
|
|
56
|
-
ProjectPhase.TASK_VERIFIED: {
|
|
57
|
-
ProjectPhase.TASK_READY, # next task
|
|
58
|
-
ProjectPhase.PROJECT_DONE,
|
|
59
|
-
},
|
|
60
|
-
ProjectPhase.PROJECT_DONE: set(),
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class InvalidTransitionError(Exception):
|
|
65
|
-
"""Raised when a state transition is not allowed."""
|
|
66
|
-
|
|
67
|
-
def __init__(self, current: ProjectPhase, target: ProjectPhase):
|
|
68
|
-
self.current = current
|
|
69
|
-
self.target = target
|
|
70
|
-
super().__init__(
|
|
71
|
-
f"Invalid transition: {current.value} -> {target.value}. "
|
|
72
|
-
f"Valid targets: {', '.join(t.value for t in TRANSITIONS.get(current, set()))}"
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class StateMachine:
|
|
77
|
-
"""Manages project phase transitions with validation."""
|
|
78
|
-
|
|
79
|
-
def __init__(self, initial: ProjectPhase = ProjectPhase.NEW):
|
|
80
|
-
self._phase = initial
|
|
81
|
-
self._history: List[ProjectPhase] = [initial]
|
|
82
|
-
|
|
83
|
-
@property
|
|
84
|
-
def phase(self) -> ProjectPhase:
|
|
85
|
-
return self._phase
|
|
86
|
-
|
|
87
|
-
@property
|
|
88
|
-
def history(self) -> List[ProjectPhase]:
|
|
89
|
-
return list(self._history)
|
|
90
|
-
|
|
91
|
-
def can_transition(self, target: ProjectPhase) -> bool:
|
|
92
|
-
"""Check if a transition to the target phase is valid."""
|
|
93
|
-
valid = TRANSITIONS.get(self._phase, set())
|
|
94
|
-
return target in valid
|
|
95
|
-
|
|
96
|
-
def transition(self, target: ProjectPhase) -> None:
|
|
97
|
-
"""Transition to a new phase.
|
|
98
|
-
|
|
99
|
-
Raises InvalidTransitionError if the transition is not allowed.
|
|
100
|
-
"""
|
|
101
|
-
if not self.can_transition(target):
|
|
102
|
-
raise InvalidTransitionError(self._phase, target)
|
|
103
|
-
self._phase = target
|
|
104
|
-
self._history.append(target)
|
|
105
|
-
|
|
106
|
-
def valid_transitions(self) -> Set[ProjectPhase]:
|
|
107
|
-
"""Return the set of valid next phases from the current state."""
|
|
108
|
-
return TRANSITIONS.get(self._phase, set())
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Artifact package: graph, coverage, and validation for the persistent artifact DAG."""
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
"""Coverage analysis utilities for the artifact graph."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Dict, List, Tuple
|
|
6
|
-
|
|
7
|
-
from devcouncil.artifacts.graph import ArtifactGraph
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def requirement_task_matrix(graph: ArtifactGraph) -> List[Dict[str, str]]:
|
|
11
|
-
"""Build a requirement → task → status coverage matrix for reporting.
|
|
12
|
-
|
|
13
|
-
Returns a list of dicts suitable for rendering as a table:
|
|
14
|
-
[{"requirement": "REQ-001", "title": "...", "task": "TASK-001", "task_status": "verified"}, ...]
|
|
15
|
-
"""
|
|
16
|
-
rows: List[Dict[str, str]] = []
|
|
17
|
-
for req in graph.requirements.values():
|
|
18
|
-
linked_tasks = [
|
|
19
|
-
t for t in graph.tasks.values() if req.id in t.requirement_ids
|
|
20
|
-
]
|
|
21
|
-
if not linked_tasks:
|
|
22
|
-
rows.append({
|
|
23
|
-
"requirement": req.id,
|
|
24
|
-
"title": req.title,
|
|
25
|
-
"task": "(none)",
|
|
26
|
-
"task_status": "NOT PLANNED",
|
|
27
|
-
})
|
|
28
|
-
else:
|
|
29
|
-
for task in linked_tasks:
|
|
30
|
-
rows.append({
|
|
31
|
-
"requirement": req.id,
|
|
32
|
-
"title": req.title,
|
|
33
|
-
"task": task.id,
|
|
34
|
-
"task_status": task.status,
|
|
35
|
-
})
|
|
36
|
-
return rows
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def acceptance_criteria_evidence_matrix(
|
|
40
|
-
graph: ArtifactGraph,
|
|
41
|
-
) -> List[Dict[str, str]]:
|
|
42
|
-
"""Build an AC → evidence mapping for the final report.
|
|
43
|
-
|
|
44
|
-
Returns rows with requirement_id, ac_id, ac_description, evidence_status.
|
|
45
|
-
"""
|
|
46
|
-
evidenced: Dict[str, str] = {}
|
|
47
|
-
for ev in graph.test_evidence:
|
|
48
|
-
key = ev.acceptance_criterion_id
|
|
49
|
-
evidenced[key] = ev.status # "passed", "failed", "not_run"
|
|
50
|
-
|
|
51
|
-
rows: List[Dict[str, str]] = []
|
|
52
|
-
for req in graph.requirements.values():
|
|
53
|
-
for ac in req.acceptance_criteria:
|
|
54
|
-
status = evidenced.get(ac.id, "no_evidence")
|
|
55
|
-
rows.append({
|
|
56
|
-
"requirement_id": req.id,
|
|
57
|
-
"ac_id": ac.id,
|
|
58
|
-
"description": ac.description,
|
|
59
|
-
"verification_method": ac.verification_method,
|
|
60
|
-
"status": status,
|
|
61
|
-
})
|
|
62
|
-
return rows
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def can_approve_plan(graph: ArtifactGraph) -> Tuple[bool, List[str]]:
|
|
66
|
-
"""Check if the plan meets the PLAN_APPROVED gate criteria.
|
|
67
|
-
|
|
68
|
-
Returns (passed, list_of_reasons_if_failed).
|
|
69
|
-
|
|
70
|
-
Criteria (from §12):
|
|
71
|
-
- Every requirement has acceptance criteria.
|
|
72
|
-
- Every acceptance criterion has a verification method.
|
|
73
|
-
- Every requirement maps to at least one task.
|
|
74
|
-
- Every task maps to at least one requirement.
|
|
75
|
-
- Every high-impact assumption is confirmed or converted.
|
|
76
|
-
- No critical critique finding remains open.
|
|
77
|
-
- No blocking question remains unanswered.
|
|
78
|
-
"""
|
|
79
|
-
reasons: List[str] = []
|
|
80
|
-
|
|
81
|
-
for req in graph.requirements_without_acceptance_criteria():
|
|
82
|
-
reasons.append(f"{req.id} has no acceptance criteria.")
|
|
83
|
-
|
|
84
|
-
for req in graph.requirements_without_tasks():
|
|
85
|
-
reasons.append(f"{req.id} is not mapped to any task.")
|
|
86
|
-
|
|
87
|
-
for task in graph.tasks_without_requirements():
|
|
88
|
-
reasons.append(f"{task.id} is not mapped to any requirement.")
|
|
89
|
-
|
|
90
|
-
for asm in graph.unconfirmed_high_impact_assumptions():
|
|
91
|
-
reasons.append(f"Assumption {asm.id} (high impact) is still open: {asm.statement}")
|
|
92
|
-
|
|
93
|
-
for finding in graph.open_findings("critical"):
|
|
94
|
-
reasons.append(f"Critical finding {finding.id} is still open: {finding.claim}")
|
|
95
|
-
|
|
96
|
-
return len(reasons) == 0, reasons
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
"""Artifact graph: the persistent, directed graph that links requirements to evidence.
|
|
2
|
-
|
|
3
|
-
This is DevCouncil's core data structure:
|
|
4
|
-
Requirement -> AcceptanceCriterion -> Task -> PlannedFile -> ChangedFile
|
|
5
|
-
-> CommandResult -> TestEvidence -> Gap
|
|
6
|
-
|
|
7
|
-
The graph enables coverage queries like:
|
|
8
|
-
- Which requirements have no tasks?
|
|
9
|
-
- Which tasks produced no changed files?
|
|
10
|
-
- Which acceptance criteria have no evidence?
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
from dataclasses import dataclass, field
|
|
16
|
-
from typing import Any, Dict, List, Set, Tuple
|
|
17
|
-
|
|
18
|
-
from devcouncil.domain.requirement import Requirement, AcceptanceCriterion
|
|
19
|
-
from devcouncil.domain.task import Task
|
|
20
|
-
from devcouncil.domain.assumption import Assumption
|
|
21
|
-
from devcouncil.domain.evidence import CommandResult, DiffCoverageEvidence, DiffEvidence, TestEvidence
|
|
22
|
-
from devcouncil.domain.gap import Gap
|
|
23
|
-
from devcouncil.domain.critique import CritiqueFinding
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@dataclass
|
|
27
|
-
class ArtifactGraph:
|
|
28
|
-
"""Directed graph of all planning and execution artifacts."""
|
|
29
|
-
|
|
30
|
-
requirements: Dict[str, Requirement] = field(default_factory=dict)
|
|
31
|
-
tasks: Dict[str, Task] = field(default_factory=dict)
|
|
32
|
-
assumptions: Dict[str, Assumption] = field(default_factory=dict)
|
|
33
|
-
findings: Dict[str, CritiqueFinding] = field(default_factory=dict)
|
|
34
|
-
gaps: Dict[str, Gap] = field(default_factory=dict)
|
|
35
|
-
test_evidence: List[TestEvidence] = field(default_factory=list)
|
|
36
|
-
diff_evidence: List[DiffEvidence] = field(default_factory=list)
|
|
37
|
-
command_results: List[CommandResult] = field(default_factory=list)
|
|
38
|
-
diff_coverage_evidence: List[DiffCoverageEvidence] = field(default_factory=list)
|
|
39
|
-
|
|
40
|
-
# --- Mutation ---
|
|
41
|
-
|
|
42
|
-
def add_requirement(self, req: Requirement) -> None:
|
|
43
|
-
self.requirements[req.id] = req
|
|
44
|
-
|
|
45
|
-
def add_task(self, task: Task) -> None:
|
|
46
|
-
self.tasks[task.id] = task
|
|
47
|
-
|
|
48
|
-
def add_assumption(self, asm: Assumption) -> None:
|
|
49
|
-
self.assumptions[asm.id] = asm
|
|
50
|
-
|
|
51
|
-
def add_finding(self, finding: CritiqueFinding) -> None:
|
|
52
|
-
self.findings[finding.id] = finding
|
|
53
|
-
|
|
54
|
-
def add_gap(self, gap: Gap) -> None:
|
|
55
|
-
self.gaps[gap.id] = gap
|
|
56
|
-
|
|
57
|
-
def add_test_evidence(self, ev: TestEvidence) -> None:
|
|
58
|
-
self.test_evidence.append(ev)
|
|
59
|
-
|
|
60
|
-
def add_diff_evidence(self, ev: DiffEvidence) -> None:
|
|
61
|
-
self.diff_evidence.append(ev)
|
|
62
|
-
|
|
63
|
-
def add_command_result(self, cr: CommandResult) -> None:
|
|
64
|
-
self.command_results.append(cr)
|
|
65
|
-
|
|
66
|
-
def add_diff_coverage_evidence(self, ev: DiffCoverageEvidence) -> None:
|
|
67
|
-
self.diff_coverage_evidence.append(ev)
|
|
68
|
-
|
|
69
|
-
def diff_coverage_findings(self) -> List[DiffCoverageEvidence]:
|
|
70
|
-
"""Measured diff-coverage runs where the changed lines were NOT fully exercised
|
|
71
|
-
— i.e. a green suite that did not actually run the new code."""
|
|
72
|
-
return [
|
|
73
|
-
ev for ev in self.diff_coverage_evidence
|
|
74
|
-
if ev.measured and ev.changed_lines and ev.covered_lines < ev.changed_lines
|
|
75
|
-
]
|
|
76
|
-
|
|
77
|
-
# --- Coverage Queries ---
|
|
78
|
-
|
|
79
|
-
def requirements_without_tasks(self) -> List[Requirement]:
|
|
80
|
-
"""Requirements that are not mapped to any task."""
|
|
81
|
-
task_req_ids: Set[str] = set()
|
|
82
|
-
for task in self.tasks.values():
|
|
83
|
-
task_req_ids.update(task.requirement_ids)
|
|
84
|
-
return [r for r in self.requirements.values() if r.id not in task_req_ids]
|
|
85
|
-
|
|
86
|
-
def tasks_without_requirements(self) -> List[Task]:
|
|
87
|
-
"""Tasks that don't map back to any requirement."""
|
|
88
|
-
return [t for t in self.tasks.values() if not t.requirement_ids]
|
|
89
|
-
|
|
90
|
-
def requirements_without_acceptance_criteria(self) -> List[Requirement]:
|
|
91
|
-
"""Requirements with no acceptance criteria defined."""
|
|
92
|
-
return [r for r in self.requirements.values() if not r.acceptance_criteria]
|
|
93
|
-
|
|
94
|
-
def acceptance_criteria_without_evidence(self) -> List[Tuple[str, AcceptanceCriterion]]:
|
|
95
|
-
"""AC IDs that have no *passing* test evidence mapped to them.
|
|
96
|
-
|
|
97
|
-
Only ``passed`` evidence counts: a failed or not-run check is not proof, so
|
|
98
|
-
it must not remove a criterion from the unproven list (which would feed a
|
|
99
|
-
falsely-green coverage summary to ``dev status`` and the MCP surface).
|
|
100
|
-
"""
|
|
101
|
-
evidenced_ac_ids: Set[str] = set()
|
|
102
|
-
for ev in self.test_evidence:
|
|
103
|
-
if getattr(ev, "status", "passed") == "passed":
|
|
104
|
-
evidenced_ac_ids.add(ev.acceptance_criterion_id)
|
|
105
|
-
|
|
106
|
-
results: List[Tuple[str, AcceptanceCriterion]] = []
|
|
107
|
-
for req in self.requirements.values():
|
|
108
|
-
for ac in req.acceptance_criteria:
|
|
109
|
-
if ac.id not in evidenced_ac_ids:
|
|
110
|
-
results.append((req.id, ac))
|
|
111
|
-
return results
|
|
112
|
-
|
|
113
|
-
def tasks_without_changed_files(self) -> List[Task]:
|
|
114
|
-
"""Tasks that have not produced any diff evidence."""
|
|
115
|
-
tasks_with_diffs: Set[str] = set()
|
|
116
|
-
for de in self.diff_evidence:
|
|
117
|
-
tasks_with_diffs.add(de.task_id)
|
|
118
|
-
return [
|
|
119
|
-
t for t in self.tasks.values()
|
|
120
|
-
if t.id not in tasks_with_diffs and t.status not in ("planned", "ready")
|
|
121
|
-
]
|
|
122
|
-
|
|
123
|
-
def open_findings(self, min_severity: str = "low") -> List[CritiqueFinding]:
|
|
124
|
-
"""Critique findings that are still open."""
|
|
125
|
-
severity_order = {"low": 0, "medium": 1, "high": 2, "critical": 3}
|
|
126
|
-
min_rank = severity_order.get(min_severity, 0)
|
|
127
|
-
return [
|
|
128
|
-
f for f in self.findings.values()
|
|
129
|
-
if f.status == "open" and severity_order.get(f.severity, 0) >= min_rank
|
|
130
|
-
]
|
|
131
|
-
|
|
132
|
-
def blocking_gaps(self) -> List[Gap]:
|
|
133
|
-
"""Gaps that are blocking progress."""
|
|
134
|
-
return [g for g in self.gaps.values() if g.blocking]
|
|
135
|
-
|
|
136
|
-
def unconfirmed_high_impact_assumptions(self) -> List[Assumption]:
|
|
137
|
-
"""Assumptions with high impact that are still open."""
|
|
138
|
-
return [
|
|
139
|
-
a for a in self.assumptions.values()
|
|
140
|
-
if a.status == "open" and a.impact == "high"
|
|
141
|
-
]
|
|
142
|
-
|
|
143
|
-
# --- Aggregate ---
|
|
144
|
-
|
|
145
|
-
def coverage_summary(self) -> Dict[str, Any]:
|
|
146
|
-
"""Produce a coverage summary for reporting."""
|
|
147
|
-
return {
|
|
148
|
-
"total_requirements": len(self.requirements),
|
|
149
|
-
"requirements_without_tasks": len(self.requirements_without_tasks()),
|
|
150
|
-
"requirements_without_ac": len(self.requirements_without_acceptance_criteria()),
|
|
151
|
-
"total_tasks": len(self.tasks),
|
|
152
|
-
"tasks_without_requirements": len(self.tasks_without_requirements()),
|
|
153
|
-
"tasks_without_diffs": len(self.tasks_without_changed_files()),
|
|
154
|
-
"total_ac": sum(len(r.acceptance_criteria) for r in self.requirements.values()),
|
|
155
|
-
"ac_without_evidence": len(self.acceptance_criteria_without_evidence()),
|
|
156
|
-
"total_gaps": len(self.gaps),
|
|
157
|
-
"blocking_gaps": len(self.blocking_gaps()),
|
|
158
|
-
"diff_coverage_runs": len(self.diff_coverage_evidence),
|
|
159
|
-
"unexercised_diff_findings": len(self.diff_coverage_findings()),
|
|
160
|
-
"open_findings": len(self.open_findings()),
|
|
161
|
-
"high_critical_open_findings": len(self.open_findings("high")),
|
|
162
|
-
"unconfirmed_high_assumptions": len(self.unconfirmed_high_impact_assumptions()),
|
|
163
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
from typing import Any, Dict
|
|
2
|
-
|
|
3
|
-
class ArtifactMigrator:
|
|
4
|
-
"""Migrates artifact schemas across DevCouncil versions."""
|
|
5
|
-
|
|
6
|
-
@staticmethod
|
|
7
|
-
def migrate_requirement(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
8
|
-
"""Upgrade requirement payload to current schema."""
|
|
9
|
-
if "priority" not in data:
|
|
10
|
-
data["priority"] = "medium"
|
|
11
|
-
return data
|
|
12
|
-
|
|
13
|
-
@staticmethod
|
|
14
|
-
def migrate_task(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
15
|
-
"""Upgrade task payload to current schema."""
|
|
16
|
-
if "forbidden_changes" not in data:
|
|
17
|
-
data["forbidden_changes"] = []
|
|
18
|
-
if "expected_tests" not in data:
|
|
19
|
-
data["expected_tests"] = []
|
|
20
|
-
return data
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
from typing import List
|
|
2
|
-
from pydantic import BaseModel, Field
|
|
3
|
-
|
|
4
|
-
# Common schemas that might be shared across artifacts
|
|
5
|
-
class FileModification(BaseModel):
|
|
6
|
-
path: str
|
|
7
|
-
diff: str
|
|
8
|
-
|
|
9
|
-
class CoverageMatrix(BaseModel):
|
|
10
|
-
"""Represents a requirement to task/test mapping coverage."""
|
|
11
|
-
requirement_id: str
|
|
12
|
-
task_ids: List[str] = Field(default_factory=list)
|
|
13
|
-
test_evidence_ids: List[str] = Field(default_factory=list)
|
|
14
|
-
is_covered: bool = False
|
|
15
|
-
missing_tasks: bool = False
|
|
16
|
-
missing_tests: bool = False
|
|
17
|
-
|
|
18
|
-
class ReportSchema(BaseModel):
|
|
19
|
-
project_id: str
|
|
20
|
-
tasks_completed: int
|
|
21
|
-
tasks_blocked: int
|
|
22
|
-
open_gaps: int
|
|
23
|
-
coverage_matrix: List[CoverageMatrix]
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from typing import Any, Dict, TypeVar, Type
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
|
|
5
|
-
T = TypeVar("T", bound=BaseModel)
|
|
6
|
-
|
|
7
|
-
class ArtifactSerializer:
|
|
8
|
-
"""Handles serialization and deserialization of DevCouncil artifacts."""
|
|
9
|
-
|
|
10
|
-
@staticmethod
|
|
11
|
-
def to_json(artifact: BaseModel) -> str:
|
|
12
|
-
return artifact.model_dump_json(indent=2)
|
|
13
|
-
|
|
14
|
-
@staticmethod
|
|
15
|
-
def from_json(json_str: str, model_class: Type[T]) -> T:
|
|
16
|
-
data = json.loads(json_str)
|
|
17
|
-
return model_class.model_validate(data)
|
|
18
|
-
|
|
19
|
-
@staticmethod
|
|
20
|
-
def to_dict(artifact: BaseModel) -> Dict[str, Any]:
|
|
21
|
-
return artifact.model_dump()
|