devcouncil 0.1.0 → 0.1.1

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.
Files changed (128) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +62 -543
  3. package/package.json +1 -1
  4. package/pyproject.toml +29 -26
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +135 -108
  8. package/src/devcouncil/app/errors.py +23 -23
  9. package/src/devcouncil/app/events.py +44 -44
  10. package/src/devcouncil/app/orchestrator.py +67 -67
  11. package/src/devcouncil/app/project_status.py +29 -0
  12. package/src/devcouncil/app/run_context.py +39 -39
  13. package/src/devcouncil/app/state_machine.py +108 -108
  14. package/src/devcouncil/artifacts/__init__.py +1 -1
  15. package/src/devcouncil/artifacts/coverage.py +96 -96
  16. package/src/devcouncil/artifacts/graph.py +143 -143
  17. package/src/devcouncil/artifacts/migrations.py +20 -20
  18. package/src/devcouncil/artifacts/schemas.py +23 -23
  19. package/src/devcouncil/artifacts/serializer.py +21 -21
  20. package/src/devcouncil/artifacts/validators.py +27 -27
  21. package/src/devcouncil/cli/commands/artifacts.py +51 -48
  22. package/src/devcouncil/cli/commands/ast.py +22 -0
  23. package/src/devcouncil/cli/commands/baseline.py +35 -32
  24. package/src/devcouncil/cli/commands/config.py +76 -54
  25. package/src/devcouncil/cli/commands/dashboard.py +26 -0
  26. package/src/devcouncil/cli/commands/doctor.py +86 -42
  27. package/src/devcouncil/cli/commands/go.py +237 -0
  28. package/src/devcouncil/cli/commands/hook.py +96 -29
  29. package/src/devcouncil/cli/commands/init.py +67 -56
  30. package/src/devcouncil/cli/commands/integrate.py +320 -14
  31. package/src/devcouncil/cli/commands/lsp.py +20 -0
  32. package/src/devcouncil/cli/commands/map.py +25 -21
  33. package/src/devcouncil/cli/commands/plan.py +257 -206
  34. package/src/devcouncil/cli/commands/prompt.py +36 -33
  35. package/src/devcouncil/cli/commands/repair.py +72 -69
  36. package/src/devcouncil/cli/commands/report.py +112 -54
  37. package/src/devcouncil/cli/commands/reset_demo_state.py +31 -28
  38. package/src/devcouncil/cli/commands/rollback.py +49 -47
  39. package/src/devcouncil/cli/commands/run.py +252 -207
  40. package/src/devcouncil/cli/commands/setup.py +159 -18
  41. package/src/devcouncil/cli/commands/show.py +76 -57
  42. package/src/devcouncil/cli/commands/status.py +117 -105
  43. package/src/devcouncil/cli/commands/tasks.py +55 -41
  44. package/src/devcouncil/cli/commands/trace.py +2 -1
  45. package/src/devcouncil/cli/commands/verify.py +158 -128
  46. package/src/devcouncil/cli/commands/version.py +20 -20
  47. package/src/devcouncil/cli/commands/watch.py +574 -0
  48. package/src/devcouncil/cli/main.py +42 -24
  49. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  50. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  51. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  52. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  53. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  54. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  55. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  56. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  57. package/src/devcouncil/domain/assumption.py +17 -17
  58. package/src/devcouncil/domain/critique.py +32 -32
  59. package/src/devcouncil/domain/evidence.py +27 -27
  60. package/src/devcouncil/domain/gap.py +26 -26
  61. package/src/devcouncil/domain/requirement.py +22 -22
  62. package/src/devcouncil/domain/task.py +26 -26
  63. package/src/devcouncil/execution/__init__.py +1 -1
  64. package/src/devcouncil/execution/context_builder.py +54 -54
  65. package/src/devcouncil/execution/executor.py +15 -15
  66. package/src/devcouncil/execution/hook_policy.py +24 -3
  67. package/src/devcouncil/execution/patch.py +28 -28
  68. package/src/devcouncil/execution/permissions.py +44 -44
  69. package/src/devcouncil/execution/prompt_builder.py +23 -23
  70. package/src/devcouncil/execution/task_runner.py +63 -63
  71. package/src/devcouncil/executors/__init__.py +1 -1
  72. package/src/devcouncil/executors/coding_cli.py +112 -0
  73. package/src/devcouncil/executors/mini_swe.py +63 -63
  74. package/src/devcouncil/executors/native/agent.py +81 -81
  75. package/src/devcouncil/executors/openhands.py +56 -56
  76. package/src/devcouncil/gating/__init__.py +1 -1
  77. package/src/devcouncil/gating/checks/clean_git.py +50 -45
  78. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  79. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  80. package/src/devcouncil/gating/checks/secret_scan_check.py +34 -34
  81. package/src/devcouncil/gating/policy.py +157 -157
  82. package/src/devcouncil/indexing/__init__.py +1 -1
  83. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  84. package/src/devcouncil/indexing/graph_index.py +48 -48
  85. package/src/devcouncil/indexing/lsp.py +120 -0
  86. package/src/devcouncil/indexing/repo_mapper.py +208 -204
  87. package/src/devcouncil/integrations/github.py +35 -35
  88. package/src/devcouncil/integrations/gitnexus.py +27 -27
  89. package/src/devcouncil/integrations/graphify.py +34 -34
  90. package/src/devcouncil/integrations/mcp/server.py +549 -96
  91. package/src/devcouncil/integrations/pr_comments.py +62 -0
  92. package/src/devcouncil/live/__init__.py +2 -0
  93. package/src/devcouncil/live/cards.py +207 -0
  94. package/src/devcouncil/live/models.py +63 -0
  95. package/src/devcouncil/live/repair_prompt.py +83 -0
  96. package/src/devcouncil/live/reviewer.py +70 -0
  97. package/src/devcouncil/live/signals.py +135 -0
  98. package/src/devcouncil/live/summary.py +34 -0
  99. package/src/devcouncil/live/tasks.py +18 -0
  100. package/src/devcouncil/live/transcripts.py +138 -0
  101. package/src/devcouncil/llm/__init__.py +1 -1
  102. package/src/devcouncil/llm/cache.py +38 -38
  103. package/src/devcouncil/llm/provider.py +146 -125
  104. package/src/devcouncil/llm/router.py +111 -111
  105. package/src/devcouncil/planning/__init__.py +1 -1
  106. package/src/devcouncil/planning/arbiter_service.py +57 -57
  107. package/src/devcouncil/planning/critique_service.py +66 -66
  108. package/src/devcouncil/planning/plan_service.py +46 -46
  109. package/src/devcouncil/planning/prompt_enhancer_service.py +86 -0
  110. package/src/devcouncil/planning/repair_service.py +39 -39
  111. package/src/devcouncil/planning/spec_service.py +44 -44
  112. package/src/devcouncil/reporting/github_check.py +32 -32
  113. package/src/devcouncil/reporting/json_report.py +20 -17
  114. package/src/devcouncil/reporting/markdown_report.py +68 -46
  115. package/src/devcouncil/reporting/report_builder.py +14 -14
  116. package/src/devcouncil/storage/db.py +66 -66
  117. package/src/devcouncil/storage/models.py +83 -83
  118. package/src/devcouncil/storage/repositories.py +299 -222
  119. package/src/devcouncil/telemetry/cost.py +34 -34
  120. package/src/devcouncil/telemetry/tracker.py +49 -49
  121. package/src/devcouncil/ui/__init__.py +1 -0
  122. package/src/devcouncil/ui/dashboard.py +122 -0
  123. package/src/devcouncil/utils/__init__.py +1 -1
  124. package/src/devcouncil/utils/redaction.py +141 -141
  125. package/src/devcouncil/verification/__init__.py +1 -1
  126. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  127. package/src/devcouncil/verification/verifier.py +319 -302
  128. package/uv.lock +1 -1
@@ -1,96 +1,96 @@
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
+ """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,143 +1,143 @@
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, 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
-
39
- # --- Mutation ---
40
-
41
- def add_requirement(self, req: Requirement) -> None:
42
- self.requirements[req.id] = req
43
-
44
- def add_task(self, task: Task) -> None:
45
- self.tasks[task.id] = task
46
-
47
- def add_assumption(self, asm: Assumption) -> None:
48
- self.assumptions[asm.id] = asm
49
-
50
- def add_finding(self, finding: CritiqueFinding) -> None:
51
- self.findings[finding.id] = finding
52
-
53
- def add_gap(self, gap: Gap) -> None:
54
- self.gaps[gap.id] = gap
55
-
56
- def add_test_evidence(self, ev: TestEvidence) -> None:
57
- self.test_evidence.append(ev)
58
-
59
- def add_diff_evidence(self, ev: DiffEvidence) -> None:
60
- self.diff_evidence.append(ev)
61
-
62
- def add_command_result(self, cr: CommandResult) -> None:
63
- self.command_results.append(cr)
64
-
65
- # --- Coverage Queries ---
66
-
67
- def requirements_without_tasks(self) -> List[Requirement]:
68
- """Requirements that are not mapped to any task."""
69
- task_req_ids: Set[str] = set()
70
- for task in self.tasks.values():
71
- task_req_ids.update(task.requirement_ids)
72
- return [r for r in self.requirements.values() if r.id not in task_req_ids]
73
-
74
- def tasks_without_requirements(self) -> List[Task]:
75
- """Tasks that don't map back to any requirement."""
76
- return [t for t in self.tasks.values() if not t.requirement_ids]
77
-
78
- def requirements_without_acceptance_criteria(self) -> List[Requirement]:
79
- """Requirements with no acceptance criteria defined."""
80
- return [r for r in self.requirements.values() if not r.acceptance_criteria]
81
-
82
- def acceptance_criteria_without_evidence(self) -> List[Tuple[str, AcceptanceCriterion]]:
83
- """AC IDs that have no test evidence mapped to them."""
84
- evidenced_ac_ids: Set[str] = set()
85
- for ev in self.test_evidence:
86
- evidenced_ac_ids.add(ev.acceptance_criterion_id)
87
-
88
- results: List[Tuple[str, AcceptanceCriterion]] = []
89
- for req in self.requirements.values():
90
- for ac in req.acceptance_criteria:
91
- if ac.id not in evidenced_ac_ids:
92
- results.append((req.id, ac))
93
- return results
94
-
95
- def tasks_without_changed_files(self) -> List[Task]:
96
- """Tasks that have not produced any diff evidence."""
97
- tasks_with_diffs: Set[str] = set()
98
- for de in self.diff_evidence:
99
- tasks_with_diffs.add(de.task_id)
100
- return [
101
- t for t in self.tasks.values()
102
- if t.id not in tasks_with_diffs and t.status not in ("planned", "ready")
103
- ]
104
-
105
- def open_findings(self, min_severity: str = "low") -> List[CritiqueFinding]:
106
- """Critique findings that are still open."""
107
- severity_order = {"low": 0, "medium": 1, "high": 2, "critical": 3}
108
- min_rank = severity_order.get(min_severity, 0)
109
- return [
110
- f for f in self.findings.values()
111
- if f.status == "open" and severity_order.get(f.severity, 0) >= min_rank
112
- ]
113
-
114
- def blocking_gaps(self) -> List[Gap]:
115
- """Gaps that are blocking progress."""
116
- return [g for g in self.gaps.values() if g.blocking]
117
-
118
- def unconfirmed_high_impact_assumptions(self) -> List[Assumption]:
119
- """Assumptions with high impact that are still open."""
120
- return [
121
- a for a in self.assumptions.values()
122
- if a.status == "open" and a.impact == "high"
123
- ]
124
-
125
- # --- Aggregate ---
126
-
127
- def coverage_summary(self) -> Dict[str, Any]:
128
- """Produce a coverage summary for reporting."""
129
- return {
130
- "total_requirements": len(self.requirements),
131
- "requirements_without_tasks": len(self.requirements_without_tasks()),
132
- "requirements_without_ac": len(self.requirements_without_acceptance_criteria()),
133
- "total_tasks": len(self.tasks),
134
- "tasks_without_requirements": len(self.tasks_without_requirements()),
135
- "tasks_without_diffs": len(self.tasks_without_changed_files()),
136
- "total_ac": sum(len(r.acceptance_criteria) for r in self.requirements.values()),
137
- "ac_without_evidence": len(self.acceptance_criteria_without_evidence()),
138
- "total_gaps": len(self.gaps),
139
- "blocking_gaps": len(self.blocking_gaps()),
140
- "open_findings": len(self.open_findings()),
141
- "high_critical_open_findings": len(self.open_findings("high")),
142
- "unconfirmed_high_assumptions": len(self.unconfirmed_high_impact_assumptions()),
143
- }
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, 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
+
39
+ # --- Mutation ---
40
+
41
+ def add_requirement(self, req: Requirement) -> None:
42
+ self.requirements[req.id] = req
43
+
44
+ def add_task(self, task: Task) -> None:
45
+ self.tasks[task.id] = task
46
+
47
+ def add_assumption(self, asm: Assumption) -> None:
48
+ self.assumptions[asm.id] = asm
49
+
50
+ def add_finding(self, finding: CritiqueFinding) -> None:
51
+ self.findings[finding.id] = finding
52
+
53
+ def add_gap(self, gap: Gap) -> None:
54
+ self.gaps[gap.id] = gap
55
+
56
+ def add_test_evidence(self, ev: TestEvidence) -> None:
57
+ self.test_evidence.append(ev)
58
+
59
+ def add_diff_evidence(self, ev: DiffEvidence) -> None:
60
+ self.diff_evidence.append(ev)
61
+
62
+ def add_command_result(self, cr: CommandResult) -> None:
63
+ self.command_results.append(cr)
64
+
65
+ # --- Coverage Queries ---
66
+
67
+ def requirements_without_tasks(self) -> List[Requirement]:
68
+ """Requirements that are not mapped to any task."""
69
+ task_req_ids: Set[str] = set()
70
+ for task in self.tasks.values():
71
+ task_req_ids.update(task.requirement_ids)
72
+ return [r for r in self.requirements.values() if r.id not in task_req_ids]
73
+
74
+ def tasks_without_requirements(self) -> List[Task]:
75
+ """Tasks that don't map back to any requirement."""
76
+ return [t for t in self.tasks.values() if not t.requirement_ids]
77
+
78
+ def requirements_without_acceptance_criteria(self) -> List[Requirement]:
79
+ """Requirements with no acceptance criteria defined."""
80
+ return [r for r in self.requirements.values() if not r.acceptance_criteria]
81
+
82
+ def acceptance_criteria_without_evidence(self) -> List[Tuple[str, AcceptanceCriterion]]:
83
+ """AC IDs that have no test evidence mapped to them."""
84
+ evidenced_ac_ids: Set[str] = set()
85
+ for ev in self.test_evidence:
86
+ evidenced_ac_ids.add(ev.acceptance_criterion_id)
87
+
88
+ results: List[Tuple[str, AcceptanceCriterion]] = []
89
+ for req in self.requirements.values():
90
+ for ac in req.acceptance_criteria:
91
+ if ac.id not in evidenced_ac_ids:
92
+ results.append((req.id, ac))
93
+ return results
94
+
95
+ def tasks_without_changed_files(self) -> List[Task]:
96
+ """Tasks that have not produced any diff evidence."""
97
+ tasks_with_diffs: Set[str] = set()
98
+ for de in self.diff_evidence:
99
+ tasks_with_diffs.add(de.task_id)
100
+ return [
101
+ t for t in self.tasks.values()
102
+ if t.id not in tasks_with_diffs and t.status not in ("planned", "ready")
103
+ ]
104
+
105
+ def open_findings(self, min_severity: str = "low") -> List[CritiqueFinding]:
106
+ """Critique findings that are still open."""
107
+ severity_order = {"low": 0, "medium": 1, "high": 2, "critical": 3}
108
+ min_rank = severity_order.get(min_severity, 0)
109
+ return [
110
+ f for f in self.findings.values()
111
+ if f.status == "open" and severity_order.get(f.severity, 0) >= min_rank
112
+ ]
113
+
114
+ def blocking_gaps(self) -> List[Gap]:
115
+ """Gaps that are blocking progress."""
116
+ return [g for g in self.gaps.values() if g.blocking]
117
+
118
+ def unconfirmed_high_impact_assumptions(self) -> List[Assumption]:
119
+ """Assumptions with high impact that are still open."""
120
+ return [
121
+ a for a in self.assumptions.values()
122
+ if a.status == "open" and a.impact == "high"
123
+ ]
124
+
125
+ # --- Aggregate ---
126
+
127
+ def coverage_summary(self) -> Dict[str, Any]:
128
+ """Produce a coverage summary for reporting."""
129
+ return {
130
+ "total_requirements": len(self.requirements),
131
+ "requirements_without_tasks": len(self.requirements_without_tasks()),
132
+ "requirements_without_ac": len(self.requirements_without_acceptance_criteria()),
133
+ "total_tasks": len(self.tasks),
134
+ "tasks_without_requirements": len(self.tasks_without_requirements()),
135
+ "tasks_without_diffs": len(self.tasks_without_changed_files()),
136
+ "total_ac": sum(len(r.acceptance_criteria) for r in self.requirements.values()),
137
+ "ac_without_evidence": len(self.acceptance_criteria_without_evidence()),
138
+ "total_gaps": len(self.gaps),
139
+ "blocking_gaps": len(self.blocking_gaps()),
140
+ "open_findings": len(self.open_findings()),
141
+ "high_critical_open_findings": len(self.open_findings("high")),
142
+ "unconfirmed_high_assumptions": len(self.unconfirmed_high_impact_assumptions()),
143
+ }
@@ -1,20 +1,20 @@
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
+ 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 +1,23 @@
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
+ 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 +1,21 @@
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()
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()