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.
- package/LICENSE +201 -201
- package/README.md +62 -543
- package/package.json +1 -1
- package/pyproject.toml +29 -26
- package/src/devcouncil/__main__.py +4 -4
- package/src/devcouncil/app/__init__.py +28 -28
- package/src/devcouncil/app/config.py +135 -108
- package/src/devcouncil/app/errors.py +23 -23
- package/src/devcouncil/app/events.py +44 -44
- package/src/devcouncil/app/orchestrator.py +67 -67
- package/src/devcouncil/app/project_status.py +29 -0
- package/src/devcouncil/app/run_context.py +39 -39
- package/src/devcouncil/app/state_machine.py +108 -108
- package/src/devcouncil/artifacts/__init__.py +1 -1
- package/src/devcouncil/artifacts/coverage.py +96 -96
- package/src/devcouncil/artifacts/graph.py +143 -143
- package/src/devcouncil/artifacts/migrations.py +20 -20
- package/src/devcouncil/artifacts/schemas.py +23 -23
- package/src/devcouncil/artifacts/serializer.py +21 -21
- package/src/devcouncil/artifacts/validators.py +27 -27
- package/src/devcouncil/cli/commands/artifacts.py +51 -48
- package/src/devcouncil/cli/commands/ast.py +22 -0
- package/src/devcouncil/cli/commands/baseline.py +35 -32
- package/src/devcouncil/cli/commands/config.py +76 -54
- package/src/devcouncil/cli/commands/dashboard.py +26 -0
- package/src/devcouncil/cli/commands/doctor.py +86 -42
- package/src/devcouncil/cli/commands/go.py +237 -0
- package/src/devcouncil/cli/commands/hook.py +96 -29
- package/src/devcouncil/cli/commands/init.py +67 -56
- package/src/devcouncil/cli/commands/integrate.py +320 -14
- package/src/devcouncil/cli/commands/lsp.py +20 -0
- package/src/devcouncil/cli/commands/map.py +25 -21
- package/src/devcouncil/cli/commands/plan.py +257 -206
- package/src/devcouncil/cli/commands/prompt.py +36 -33
- package/src/devcouncil/cli/commands/repair.py +72 -69
- package/src/devcouncil/cli/commands/report.py +112 -54
- package/src/devcouncil/cli/commands/reset_demo_state.py +31 -28
- package/src/devcouncil/cli/commands/rollback.py +49 -47
- package/src/devcouncil/cli/commands/run.py +252 -207
- package/src/devcouncil/cli/commands/setup.py +159 -18
- package/src/devcouncil/cli/commands/show.py +76 -57
- package/src/devcouncil/cli/commands/status.py +117 -105
- package/src/devcouncil/cli/commands/tasks.py +55 -41
- package/src/devcouncil/cli/commands/trace.py +2 -1
- package/src/devcouncil/cli/commands/verify.py +158 -128
- package/src/devcouncil/cli/commands/version.py +20 -20
- package/src/devcouncil/cli/commands/watch.py +574 -0
- package/src/devcouncil/cli/main.py +42 -24
- package/src/devcouncil/council/prompts/arbiter.md +19 -19
- package/src/devcouncil/council/prompts/critic_a.md +10 -10
- package/src/devcouncil/council/prompts/critic_b.md +10 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
- package/src/devcouncil/council/prompts/planner_a.md +16 -16
- package/src/devcouncil/council/prompts/planner_b.md +16 -16
- package/src/devcouncil/council/prompts/rebuttal.md +10 -10
- package/src/devcouncil/council/prompts/spec_writer.md +12 -12
- package/src/devcouncil/domain/assumption.py +17 -17
- package/src/devcouncil/domain/critique.py +32 -32
- package/src/devcouncil/domain/evidence.py +27 -27
- package/src/devcouncil/domain/gap.py +26 -26
- package/src/devcouncil/domain/requirement.py +22 -22
- package/src/devcouncil/domain/task.py +26 -26
- package/src/devcouncil/execution/__init__.py +1 -1
- package/src/devcouncil/execution/context_builder.py +54 -54
- package/src/devcouncil/execution/executor.py +15 -15
- package/src/devcouncil/execution/hook_policy.py +24 -3
- package/src/devcouncil/execution/patch.py +28 -28
- package/src/devcouncil/execution/permissions.py +44 -44
- package/src/devcouncil/execution/prompt_builder.py +23 -23
- package/src/devcouncil/execution/task_runner.py +63 -63
- package/src/devcouncil/executors/__init__.py +1 -1
- package/src/devcouncil/executors/coding_cli.py +112 -0
- package/src/devcouncil/executors/mini_swe.py +63 -63
- package/src/devcouncil/executors/native/agent.py +81 -81
- package/src/devcouncil/executors/openhands.py +56 -56
- package/src/devcouncil/gating/__init__.py +1 -1
- package/src/devcouncil/gating/checks/clean_git.py +50 -45
- package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +34 -34
- package/src/devcouncil/gating/policy.py +157 -157
- package/src/devcouncil/indexing/__init__.py +1 -1
- package/src/devcouncil/indexing/ast_matcher.py +168 -0
- package/src/devcouncil/indexing/graph_index.py +48 -48
- package/src/devcouncil/indexing/lsp.py +120 -0
- package/src/devcouncil/indexing/repo_mapper.py +208 -204
- package/src/devcouncil/integrations/github.py +35 -35
- package/src/devcouncil/integrations/gitnexus.py +27 -27
- package/src/devcouncil/integrations/graphify.py +34 -34
- package/src/devcouncil/integrations/mcp/server.py +549 -96
- package/src/devcouncil/integrations/pr_comments.py +62 -0
- package/src/devcouncil/live/__init__.py +2 -0
- package/src/devcouncil/live/cards.py +207 -0
- package/src/devcouncil/live/models.py +63 -0
- package/src/devcouncil/live/repair_prompt.py +83 -0
- package/src/devcouncil/live/reviewer.py +70 -0
- package/src/devcouncil/live/signals.py +135 -0
- package/src/devcouncil/live/summary.py +34 -0
- package/src/devcouncil/live/tasks.py +18 -0
- package/src/devcouncil/live/transcripts.py +138 -0
- package/src/devcouncil/llm/__init__.py +1 -1
- package/src/devcouncil/llm/cache.py +38 -38
- package/src/devcouncil/llm/provider.py +146 -125
- package/src/devcouncil/llm/router.py +111 -111
- package/src/devcouncil/planning/__init__.py +1 -1
- package/src/devcouncil/planning/arbiter_service.py +57 -57
- package/src/devcouncil/planning/critique_service.py +66 -66
- package/src/devcouncil/planning/plan_service.py +46 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +86 -0
- package/src/devcouncil/planning/repair_service.py +39 -39
- package/src/devcouncil/planning/spec_service.py +44 -44
- package/src/devcouncil/reporting/github_check.py +32 -32
- package/src/devcouncil/reporting/json_report.py +20 -17
- package/src/devcouncil/reporting/markdown_report.py +68 -46
- package/src/devcouncil/reporting/report_builder.py +14 -14
- package/src/devcouncil/storage/db.py +66 -66
- package/src/devcouncil/storage/models.py +83 -83
- package/src/devcouncil/storage/repositories.py +299 -222
- package/src/devcouncil/telemetry/cost.py +34 -34
- package/src/devcouncil/telemetry/tracker.py +49 -49
- package/src/devcouncil/ui/__init__.py +1 -0
- package/src/devcouncil/ui/dashboard.py +122 -0
- package/src/devcouncil/utils/__init__.py +1 -1
- package/src/devcouncil/utils/redaction.py +141 -141
- package/src/devcouncil/verification/__init__.py +1 -1
- package/src/devcouncil/verification/implementation_reviewer.py +55 -55
- package/src/devcouncil/verification/verifier.py +319 -302
- package/uv.lock +1 -1
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
from typing import List
|
|
2
|
-
import json
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
from devcouncil.domain.gap import Gap
|
|
5
|
-
from devcouncil.domain.task import Task
|
|
6
|
-
from devcouncil.llm.router import ModelRouter
|
|
7
|
-
|
|
8
|
-
class RepairOutput(BaseModel):
|
|
9
|
-
suggested_tasks: List[Task]
|
|
10
|
-
|
|
11
|
-
class RepairService:
|
|
12
|
-
"""Uses LLM to infer focused repair tasks from blocking gaps."""
|
|
13
|
-
|
|
14
|
-
def __init__(self, router: ModelRouter):
|
|
15
|
-
self.router = router
|
|
16
|
-
|
|
17
|
-
async def generate_repair_plan(self, gaps: List[Gap], project_context: str) -> RepairOutput:
|
|
18
|
-
prompt = f"""
|
|
19
|
-
The following blocking gaps were detected during verification.
|
|
20
|
-
Gaps:
|
|
21
|
-
{json.dumps([g.model_dump() for g in gaps], indent=2)}
|
|
22
|
-
|
|
23
|
-
Project Context:
|
|
24
|
-
{project_context}
|
|
25
|
-
|
|
26
|
-
Your task is to generate focused implementation tasks to fix these gaps.
|
|
27
|
-
- Each task must have a clear description and recommended fix.
|
|
28
|
-
- Specify 'planned_files' that need modification (infer from gap evidence).
|
|
29
|
-
- Link each task to the relevant 'requirement_id' mentioned in the gap.
|
|
30
|
-
|
|
31
|
-
Return a JSON object with 'suggested_tasks'.
|
|
32
|
-
"""
|
|
33
|
-
messages = [{"role": "user", "content": prompt}]
|
|
34
|
-
|
|
35
|
-
return await self.router.complete_structured(
|
|
36
|
-
role="planner_a", # Pragmatic tech lead is best suited for repair task generation
|
|
37
|
-
messages=messages,
|
|
38
|
-
schema=RepairOutput
|
|
39
|
-
)
|
|
1
|
+
from typing import List
|
|
2
|
+
import json
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from devcouncil.domain.gap import Gap
|
|
5
|
+
from devcouncil.domain.task import Task
|
|
6
|
+
from devcouncil.llm.router import ModelRouter
|
|
7
|
+
|
|
8
|
+
class RepairOutput(BaseModel):
|
|
9
|
+
suggested_tasks: List[Task]
|
|
10
|
+
|
|
11
|
+
class RepairService:
|
|
12
|
+
"""Uses LLM to infer focused repair tasks from blocking gaps."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, router: ModelRouter):
|
|
15
|
+
self.router = router
|
|
16
|
+
|
|
17
|
+
async def generate_repair_plan(self, gaps: List[Gap], project_context: str) -> RepairOutput:
|
|
18
|
+
prompt = f"""
|
|
19
|
+
The following blocking gaps were detected during verification.
|
|
20
|
+
Gaps:
|
|
21
|
+
{json.dumps([g.model_dump() for g in gaps], indent=2)}
|
|
22
|
+
|
|
23
|
+
Project Context:
|
|
24
|
+
{project_context}
|
|
25
|
+
|
|
26
|
+
Your task is to generate focused implementation tasks to fix these gaps.
|
|
27
|
+
- Each task must have a clear description and recommended fix.
|
|
28
|
+
- Specify 'planned_files' that need modification (infer from gap evidence).
|
|
29
|
+
- Link each task to the relevant 'requirement_id' mentioned in the gap.
|
|
30
|
+
|
|
31
|
+
Return a JSON object with 'suggested_tasks'.
|
|
32
|
+
"""
|
|
33
|
+
messages = [{"role": "user", "content": prompt}]
|
|
34
|
+
|
|
35
|
+
return await self.router.complete_structured(
|
|
36
|
+
role="planner_a", # Pragmatic tech lead is best suited for repair task generation
|
|
37
|
+
messages=messages,
|
|
38
|
+
schema=RepairOutput
|
|
39
|
+
)
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
from typing import List
|
|
2
|
-
from pydantic import BaseModel
|
|
3
|
-
from devcouncil.domain.requirement import Requirement
|
|
4
|
-
from devcouncil.domain.assumption import Assumption
|
|
5
|
-
from devcouncil.llm.router import ModelRouter
|
|
6
|
-
|
|
7
|
-
class BlockingQuestion(BaseModel):
|
|
8
|
-
id: str
|
|
9
|
-
question: str
|
|
10
|
-
reason: str
|
|
11
|
-
|
|
12
|
-
class SpecOutput(BaseModel):
|
|
13
|
-
requirements: List[Requirement]
|
|
14
|
-
assumptions: List[Assumption]
|
|
15
|
-
blocking_questions: List[BlockingQuestion]
|
|
16
|
-
|
|
17
|
-
class SpecService:
|
|
18
|
-
def __init__(self, router: ModelRouter):
|
|
19
|
-
self.router = router
|
|
20
|
-
|
|
21
|
-
async def generate_spec(self, goal: str, repo_map_json: str) -> SpecOutput:
|
|
22
|
-
prompt = f"""
|
|
23
|
-
Goal: {goal}
|
|
24
|
-
|
|
25
|
-
Repository Map:
|
|
26
|
-
{repo_map_json}
|
|
27
|
-
|
|
28
|
-
Your task is to draft the initial software specification for this goal.
|
|
29
|
-
1. Identify functional and non-functional requirements.
|
|
30
|
-
2. Extract any assumptions you are making about the codebase or architecture.
|
|
31
|
-
3. List any blocking questions that the user must answer before implementation can proceed.
|
|
32
|
-
|
|
33
|
-
Each requirement MUST have clear acceptance criteria with verification methods.
|
|
34
|
-
Each assumption MUST have a confidence and impact level.
|
|
35
|
-
"""
|
|
36
|
-
messages = [
|
|
37
|
-
{"role": "user", "content": prompt}
|
|
38
|
-
]
|
|
39
|
-
|
|
40
|
-
return await self.router.complete_structured(
|
|
41
|
-
role="spec_writer",
|
|
42
|
-
messages=messages,
|
|
43
|
-
schema=SpecOutput
|
|
44
|
-
)
|
|
1
|
+
from typing import List
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from devcouncil.domain.requirement import Requirement
|
|
4
|
+
from devcouncil.domain.assumption import Assumption
|
|
5
|
+
from devcouncil.llm.router import ModelRouter
|
|
6
|
+
|
|
7
|
+
class BlockingQuestion(BaseModel):
|
|
8
|
+
id: str
|
|
9
|
+
question: str
|
|
10
|
+
reason: str
|
|
11
|
+
|
|
12
|
+
class SpecOutput(BaseModel):
|
|
13
|
+
requirements: List[Requirement]
|
|
14
|
+
assumptions: List[Assumption]
|
|
15
|
+
blocking_questions: List[BlockingQuestion]
|
|
16
|
+
|
|
17
|
+
class SpecService:
|
|
18
|
+
def __init__(self, router: ModelRouter):
|
|
19
|
+
self.router = router
|
|
20
|
+
|
|
21
|
+
async def generate_spec(self, goal: str, repo_map_json: str) -> SpecOutput:
|
|
22
|
+
prompt = f"""
|
|
23
|
+
Goal: {goal}
|
|
24
|
+
|
|
25
|
+
Repository Map:
|
|
26
|
+
{repo_map_json}
|
|
27
|
+
|
|
28
|
+
Your task is to draft the initial software specification for this goal.
|
|
29
|
+
1. Identify functional and non-functional requirements.
|
|
30
|
+
2. Extract any assumptions you are making about the codebase or architecture.
|
|
31
|
+
3. List any blocking questions that the user must answer before implementation can proceed.
|
|
32
|
+
|
|
33
|
+
Each requirement MUST have clear acceptance criteria with verification methods.
|
|
34
|
+
Each assumption MUST have a confidence and impact level.
|
|
35
|
+
"""
|
|
36
|
+
messages = [
|
|
37
|
+
{"role": "user", "content": prompt}
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
return await self.router.complete_structured(
|
|
41
|
+
role="spec_writer",
|
|
42
|
+
messages=messages,
|
|
43
|
+
schema=SpecOutput
|
|
44
|
+
)
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
from devcouncil.artifacts.graph import ArtifactGraph
|
|
2
|
-
|
|
3
|
-
class GitHubCheckGenerator:
|
|
4
|
-
"""Generates GitHub Checks API payloads."""
|
|
5
|
-
|
|
6
|
-
@staticmethod
|
|
7
|
-
def generate(graph: ArtifactGraph) -> dict:
|
|
8
|
-
summary = graph.coverage_summary()
|
|
9
|
-
blocking_gaps = graph.blocking_gaps()
|
|
10
|
-
|
|
11
|
-
status = "completed"
|
|
12
|
-
conclusion = "failure" if summary["blocking_gaps"] > 0 else "success"
|
|
13
|
-
|
|
14
|
-
text = f"**Requirements**: {summary['total_requirements']} | "
|
|
15
|
-
text += f"**Tasks**: {summary['total_tasks']} | "
|
|
16
|
-
text += f"**Gaps**: {summary['blocking_gaps']} blocking\n\n"
|
|
17
|
-
|
|
18
|
-
if blocking_gaps:
|
|
19
|
-
text += "### Blocking Gaps\n"
|
|
20
|
-
for gap in blocking_gaps:
|
|
21
|
-
text += f"- **{gap.id}**: {gap.description}\n"
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
"name": "DevCouncil Verification",
|
|
25
|
-
"status": status,
|
|
26
|
-
"conclusion": conclusion,
|
|
27
|
-
"output": {
|
|
28
|
-
"title": f"DevCouncil: {conclusion.capitalize()}",
|
|
29
|
-
"summary": f"Found {summary['blocking_gaps']} blocking gaps.",
|
|
30
|
-
"text": text
|
|
31
|
-
}
|
|
32
|
-
}
|
|
1
|
+
from devcouncil.artifacts.graph import ArtifactGraph
|
|
2
|
+
|
|
3
|
+
class GitHubCheckGenerator:
|
|
4
|
+
"""Generates GitHub Checks API payloads."""
|
|
5
|
+
|
|
6
|
+
@staticmethod
|
|
7
|
+
def generate(graph: ArtifactGraph) -> dict:
|
|
8
|
+
summary = graph.coverage_summary()
|
|
9
|
+
blocking_gaps = graph.blocking_gaps()
|
|
10
|
+
|
|
11
|
+
status = "completed"
|
|
12
|
+
conclusion = "failure" if summary["blocking_gaps"] > 0 else "success"
|
|
13
|
+
|
|
14
|
+
text = f"**Requirements**: {summary['total_requirements']} | "
|
|
15
|
+
text += f"**Tasks**: {summary['total_tasks']} | "
|
|
16
|
+
text += f"**Gaps**: {summary['blocking_gaps']} blocking\n\n"
|
|
17
|
+
|
|
18
|
+
if blocking_gaps:
|
|
19
|
+
text += "### Blocking Gaps\n"
|
|
20
|
+
for gap in blocking_gaps:
|
|
21
|
+
text += f"- **{gap.id}**: {gap.description}\n"
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
"name": "DevCouncil Verification",
|
|
25
|
+
"status": status,
|
|
26
|
+
"conclusion": conclusion,
|
|
27
|
+
"output": {
|
|
28
|
+
"title": f"DevCouncil: {conclusion.capitalize()}",
|
|
29
|
+
"summary": f"Found {summary['blocking_gaps']} blocking gaps.",
|
|
30
|
+
"text": text
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from devcouncil.artifacts.graph import ArtifactGraph
|
|
3
|
-
|
|
4
|
-
class JsonReportGenerator:
|
|
5
|
-
"""Generates a JSON evidence report."""
|
|
6
|
-
|
|
7
|
-
@staticmethod
|
|
8
|
-
def generate(graph: ArtifactGraph) -> str:
|
|
9
|
-
summary = graph.coverage_summary()
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import json
|
|
2
|
+
from devcouncil.artifacts.graph import ArtifactGraph
|
|
3
|
+
|
|
4
|
+
class JsonReportGenerator:
|
|
5
|
+
"""Generates a JSON evidence report."""
|
|
6
|
+
|
|
7
|
+
@staticmethod
|
|
8
|
+
def generate(graph: ArtifactGraph, live_review: dict | None = None) -> str:
|
|
9
|
+
summary = graph.coverage_summary()
|
|
10
|
+
live_blockers = len((live_review or {}).get("blocking_cards", []))
|
|
11
|
+
|
|
12
|
+
report = {
|
|
13
|
+
"verdict": "blocked" if summary["blocking_gaps"] > 0 or live_blockers > 0 else "passed",
|
|
14
|
+
"coverage_summary": summary,
|
|
15
|
+
"blocking_gaps": [g.model_dump() for g in graph.blocking_gaps()]
|
|
16
|
+
}
|
|
17
|
+
if live_review is not None:
|
|
18
|
+
report["live_review"] = live_review
|
|
19
|
+
|
|
20
|
+
return json.dumps(report, indent=2)
|
|
@@ -1,46 +1,68 @@
|
|
|
1
|
-
from devcouncil.artifacts.graph import ArtifactGraph
|
|
2
|
-
|
|
3
|
-
class MarkdownReportGenerator:
|
|
4
|
-
"""Generates a Markdown evidence report."""
|
|
5
|
-
|
|
6
|
-
MAX_INLINE_GAPS = 25
|
|
7
|
-
|
|
8
|
-
@staticmethod
|
|
9
|
-
def generate(graph: ArtifactGraph) -> str:
|
|
10
|
-
summary = graph.coverage_summary()
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
md_output
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
md_output += "
|
|
26
|
-
md_output += "
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
1
|
+
from devcouncil.artifacts.graph import ArtifactGraph
|
|
2
|
+
|
|
3
|
+
class MarkdownReportGenerator:
|
|
4
|
+
"""Generates a Markdown evidence report."""
|
|
5
|
+
|
|
6
|
+
MAX_INLINE_GAPS = 25
|
|
7
|
+
|
|
8
|
+
@staticmethod
|
|
9
|
+
def generate(graph: ArtifactGraph, live_review: dict | None = None) -> str:
|
|
10
|
+
summary = graph.coverage_summary()
|
|
11
|
+
live_blockers = (live_review or {}).get("blocking_cards", [])
|
|
12
|
+
|
|
13
|
+
md_output = "# DevCouncil Report\n\n"
|
|
14
|
+
md_output += "## Verdict\n"
|
|
15
|
+
if summary["blocking_gaps"] > 0 or live_blockers:
|
|
16
|
+
parts = []
|
|
17
|
+
if summary["blocking_gaps"] > 0:
|
|
18
|
+
parts.append(f"{summary['blocking_gaps']} high-severity gap(s)")
|
|
19
|
+
if live_blockers:
|
|
20
|
+
parts.append(f"{len(live_blockers)} live-review blocker(s)")
|
|
21
|
+
md_output += f"**Blocked**: {', '.join(parts)} remain.\n\n"
|
|
22
|
+
else:
|
|
23
|
+
md_output += "**Passed**: Ready for release.\n\n"
|
|
24
|
+
|
|
25
|
+
md_output += "## Coverage Summary\n"
|
|
26
|
+
md_output += f"- **Requirements**: {summary['total_requirements']} ({summary['requirements_without_tasks']} unmapped)\n"
|
|
27
|
+
md_output += f"- **Tasks**: {summary['total_tasks']} ({summary['tasks_without_requirements']} orphaned)\n"
|
|
28
|
+
md_output += f"- **Evidence**: {summary['total_ac'] - summary['ac_without_evidence']}/{summary['total_ac']} AC verified\n\n"
|
|
29
|
+
|
|
30
|
+
md_output += "## Requirements Coverage Table\n"
|
|
31
|
+
md_output += "| Requirement | Task Mapping | Status |\n"
|
|
32
|
+
md_output += "|---|---|---|\n"
|
|
33
|
+
|
|
34
|
+
for req in graph.requirements.values():
|
|
35
|
+
linked_tasks = [t for t in graph.tasks.values() if req.id in t.requirement_ids]
|
|
36
|
+
task_str = ", ".join([t.id for t in linked_tasks]) if linked_tasks else "*None*"
|
|
37
|
+
status_str = "Covered" if linked_tasks else "**Unmapped**"
|
|
38
|
+
md_output += f"| {req.id} {req.title} | {task_str} | {status_str} |\n"
|
|
39
|
+
|
|
40
|
+
md_output += "\n## Blocking Gaps\n"
|
|
41
|
+
blocking_gaps = graph.blocking_gaps()
|
|
42
|
+
if not blocking_gaps:
|
|
43
|
+
md_output += "None.\n"
|
|
44
|
+
else:
|
|
45
|
+
for gap in blocking_gaps[:MarkdownReportGenerator.MAX_INLINE_GAPS]:
|
|
46
|
+
md_output += f"### {gap.id}: {gap.description}\n"
|
|
47
|
+
md_output += f"**Recommended fix**: {gap.recommended_fix}\n\n"
|
|
48
|
+
if len(blocking_gaps) > MarkdownReportGenerator.MAX_INLINE_GAPS:
|
|
49
|
+
remaining = len(blocking_gaps) - MarkdownReportGenerator.MAX_INLINE_GAPS
|
|
50
|
+
md_output += f"_Omitted {remaining} additional blocking gap(s). Use JSON output for the full list._\n"
|
|
51
|
+
|
|
52
|
+
if live_review is not None:
|
|
53
|
+
md_output += "\n## Live Review\n"
|
|
54
|
+
cards = live_review.get("cards", {})
|
|
55
|
+
md_output += f"- **Pending signals**: {live_review.get('pending_signals', 0)}\n"
|
|
56
|
+
md_output += f"- **Open cards**: {cards.get('open', 0)}\n"
|
|
57
|
+
md_output += f"- **Open critical cards**: {cards.get('critical_open', 0)}\n"
|
|
58
|
+
if not live_blockers:
|
|
59
|
+
md_output += "- **Blocking cards in scope**: None.\n"
|
|
60
|
+
else:
|
|
61
|
+
md_output += "\n### Blocking Live-Review Cards\n"
|
|
62
|
+
for card in live_blockers[:MarkdownReportGenerator.MAX_INLINE_GAPS]:
|
|
63
|
+
md_output += f"- **{card['id']}**"
|
|
64
|
+
if card.get("task_id"):
|
|
65
|
+
md_output += f" (`{card['task_id']}`)"
|
|
66
|
+
md_output += f": {card['summary']}\n"
|
|
67
|
+
|
|
68
|
+
return md_output
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
from devcouncil.artifacts.graph import ArtifactGraph
|
|
2
|
-
from devcouncil.reporting.markdown_report import MarkdownReportGenerator
|
|
3
|
-
from devcouncil.reporting.json_report import JsonReportGenerator
|
|
4
|
-
|
|
5
|
-
class ReportBuilder:
|
|
6
|
-
"""Builds reports in various formats from the artifact graph."""
|
|
7
|
-
|
|
8
|
-
@staticmethod
|
|
9
|
-
def build_markdown(graph: ArtifactGraph) -> str:
|
|
10
|
-
return MarkdownReportGenerator.generate(graph)
|
|
11
|
-
|
|
12
|
-
@staticmethod
|
|
13
|
-
def build_json(graph: ArtifactGraph) -> str:
|
|
14
|
-
return JsonReportGenerator.generate(graph)
|
|
1
|
+
from devcouncil.artifacts.graph import ArtifactGraph
|
|
2
|
+
from devcouncil.reporting.markdown_report import MarkdownReportGenerator
|
|
3
|
+
from devcouncil.reporting.json_report import JsonReportGenerator
|
|
4
|
+
|
|
5
|
+
class ReportBuilder:
|
|
6
|
+
"""Builds reports in various formats from the artifact graph."""
|
|
7
|
+
|
|
8
|
+
@staticmethod
|
|
9
|
+
def build_markdown(graph: ArtifactGraph, live_review: dict | None = None) -> str:
|
|
10
|
+
return MarkdownReportGenerator.generate(graph, live_review=live_review)
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def build_json(graph: ArtifactGraph, live_review: dict | None = None) -> str:
|
|
14
|
+
return JsonReportGenerator.generate(graph, live_review=live_review)
|
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
from contextlib import contextmanager
|
|
2
|
-
|
|
3
|
-
from sqlmodel import SQLModel, create_engine, Session
|
|
4
|
-
from sqlalchemy.exc import OperationalError
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Optional
|
|
7
|
-
|
|
8
|
-
from devcouncil.storage.models import SchemaVersionModel
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
SCHEMA_VERSION = 1
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Database:
|
|
15
|
-
def __init__(self, db_path: Path):
|
|
16
|
-
self.db_path = db_path
|
|
17
|
-
self.engine = create_engine(f"sqlite:///{db_path}")
|
|
18
|
-
|
|
19
|
-
def create_db_and_tables(self):
|
|
20
|
-
self._create_tables()
|
|
21
|
-
self.ensure_schema_version()
|
|
22
|
-
|
|
23
|
-
def ensure_schema_version(self):
|
|
24
|
-
self._create_tables()
|
|
25
|
-
with Session(self.engine) as session:
|
|
26
|
-
current = session.get(SchemaVersionModel, "singleton")
|
|
27
|
-
if current is None:
|
|
28
|
-
session.add(SchemaVersionModel(id="singleton", version=SCHEMA_VERSION))
|
|
29
|
-
session.commit()
|
|
30
|
-
return
|
|
31
|
-
if current.version != SCHEMA_VERSION:
|
|
32
|
-
raise RuntimeError(
|
|
33
|
-
f"Unsupported DevCouncil schema version {current.version}; "
|
|
34
|
-
f"expected {SCHEMA_VERSION}."
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
def _create_tables(self):
|
|
38
|
-
try:
|
|
39
|
-
SQLModel.metadata.create_all(self.engine)
|
|
40
|
-
except OperationalError as exc:
|
|
41
|
-
if "already exists" not in str(exc):
|
|
42
|
-
raise
|
|
43
|
-
|
|
44
|
-
@contextmanager
|
|
45
|
-
def get_session(self):
|
|
46
|
-
"""Yield a session with automatic commit/rollback/close."""
|
|
47
|
-
session = Session(self.engine)
|
|
48
|
-
try:
|
|
49
|
-
yield session
|
|
50
|
-
session.commit()
|
|
51
|
-
except Exception:
|
|
52
|
-
session.rollback()
|
|
53
|
-
raise
|
|
54
|
-
finally:
|
|
55
|
-
session.close()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def get_db(project_root: Path = Path(".")) -> Optional[Database]:
|
|
59
|
-
dev_dir = project_root / ".devcouncil"
|
|
60
|
-
if not dev_dir.exists():
|
|
61
|
-
return None
|
|
62
|
-
|
|
63
|
-
db_path = dev_dir / "state.sqlite"
|
|
64
|
-
db = Database(db_path)
|
|
65
|
-
db.ensure_schema_version()
|
|
66
|
-
return db
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
|
|
3
|
+
from sqlmodel import SQLModel, create_engine, Session
|
|
4
|
+
from sqlalchemy.exc import OperationalError
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from devcouncil.storage.models import SchemaVersionModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
SCHEMA_VERSION = 1
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Database:
|
|
15
|
+
def __init__(self, db_path: Path):
|
|
16
|
+
self.db_path = db_path
|
|
17
|
+
self.engine = create_engine(f"sqlite:///{db_path}")
|
|
18
|
+
|
|
19
|
+
def create_db_and_tables(self):
|
|
20
|
+
self._create_tables()
|
|
21
|
+
self.ensure_schema_version()
|
|
22
|
+
|
|
23
|
+
def ensure_schema_version(self):
|
|
24
|
+
self._create_tables()
|
|
25
|
+
with Session(self.engine) as session:
|
|
26
|
+
current = session.get(SchemaVersionModel, "singleton")
|
|
27
|
+
if current is None:
|
|
28
|
+
session.add(SchemaVersionModel(id="singleton", version=SCHEMA_VERSION))
|
|
29
|
+
session.commit()
|
|
30
|
+
return
|
|
31
|
+
if current.version != SCHEMA_VERSION:
|
|
32
|
+
raise RuntimeError(
|
|
33
|
+
f"Unsupported DevCouncil schema version {current.version}; "
|
|
34
|
+
f"expected {SCHEMA_VERSION}."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def _create_tables(self):
|
|
38
|
+
try:
|
|
39
|
+
SQLModel.metadata.create_all(self.engine)
|
|
40
|
+
except OperationalError as exc:
|
|
41
|
+
if "already exists" not in str(exc):
|
|
42
|
+
raise
|
|
43
|
+
|
|
44
|
+
@contextmanager
|
|
45
|
+
def get_session(self):
|
|
46
|
+
"""Yield a session with automatic commit/rollback/close."""
|
|
47
|
+
session = Session(self.engine)
|
|
48
|
+
try:
|
|
49
|
+
yield session
|
|
50
|
+
session.commit()
|
|
51
|
+
except Exception:
|
|
52
|
+
session.rollback()
|
|
53
|
+
raise
|
|
54
|
+
finally:
|
|
55
|
+
session.close()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_db(project_root: Path = Path(".")) -> Optional[Database]:
|
|
59
|
+
dev_dir = project_root / ".devcouncil"
|
|
60
|
+
if not dev_dir.exists():
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
db_path = dev_dir / "state.sqlite"
|
|
64
|
+
db = Database(db_path)
|
|
65
|
+
db.ensure_schema_version()
|
|
66
|
+
return db
|