devcouncil 0.1.1 → 0.2.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/README.md +218 -96
- package/bin/devcouncil.js +130 -32
- package/package.json +7 -11
- package/pyproject.toml +0 -34
- 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 -158
- 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 -143
- 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/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/artifacts.py +0 -51
- package/src/devcouncil/cli/commands/ast.py +0 -22
- package/src/devcouncil/cli/commands/baseline.py +0 -35
- package/src/devcouncil/cli/commands/config.py +0 -76
- package/src/devcouncil/cli/commands/dashboard.py +0 -26
- package/src/devcouncil/cli/commands/doctor.py +0 -140
- package/src/devcouncil/cli/commands/go.py +0 -237
- package/src/devcouncil/cli/commands/hook.py +0 -128
- package/src/devcouncil/cli/commands/init.py +0 -153
- package/src/devcouncil/cli/commands/integrate.py +0 -726
- package/src/devcouncil/cli/commands/lsp.py +0 -20
- package/src/devcouncil/cli/commands/map.py +0 -42
- package/src/devcouncil/cli/commands/mcp_server.py +0 -18
- package/src/devcouncil/cli/commands/plan.py +0 -327
- package/src/devcouncil/cli/commands/prompt.py +0 -50
- package/src/devcouncil/cli/commands/repair.py +0 -72
- package/src/devcouncil/cli/commands/report.py +0 -129
- package/src/devcouncil/cli/commands/reset_demo_state.py +0 -31
- package/src/devcouncil/cli/commands/rollback.py +0 -60
- package/src/devcouncil/cli/commands/run.py +0 -269
- package/src/devcouncil/cli/commands/setup.py +0 -223
- package/src/devcouncil/cli/commands/show.py +0 -76
- package/src/devcouncil/cli/commands/status.py +0 -117
- package/src/devcouncil/cli/commands/tasks.py +0 -55
- package/src/devcouncil/cli/commands/trace.py +0 -44
- package/src/devcouncil/cli/commands/verify.py +0 -193
- package/src/devcouncil/cli/commands/version.py +0 -20
- package/src/devcouncil/cli/commands/watch.py +0 -574
- package/src/devcouncil/cli/main.py +0 -88
- 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 -27
- package/src/devcouncil/domain/gap.py +0 -26
- package/src/devcouncil/domain/requirement.py +0 -22
- package/src/devcouncil/domain/task.py +0 -26
- package/src/devcouncil/execution/__init__.py +0 -1
- package/src/devcouncil/execution/context_builder.py +0 -60
- package/src/devcouncil/execution/executor.py +0 -15
- package/src/devcouncil/execution/hook_policy.py +0 -165
- package/src/devcouncil/execution/patch.py +0 -28
- package/src/devcouncil/execution/paths.py +0 -14
- package/src/devcouncil/execution/permissions.py +0 -92
- package/src/devcouncil/execution/prompt_builder.py +0 -59
- package/src/devcouncil/execution/task_runner.py +0 -166
- package/src/devcouncil/executors/__init__.py +0 -1
- package/src/devcouncil/executors/coding_cli.py +0 -112
- 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 -107
- 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 -50
- 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 -34
- package/src/devcouncil/gating/policy.py +0 -190
- 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 -120
- package/src/devcouncil/indexing/repo_mapper.py +0 -208
- package/src/devcouncil/indexing/symbol_index.py +0 -0
- package/src/devcouncil/integrations/code_review_graph.py +0 -163
- package/src/devcouncil/integrations/github.py +0 -39
- package/src/devcouncil/integrations/gitnexus.py +0 -27
- 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 -599
- package/src/devcouncil/integrations/pr_comments.py +0 -62
- package/src/devcouncil/live/__init__.py +0 -2
- package/src/devcouncil/live/cards.py +0 -207
- 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 -138
- package/src/devcouncil/llm/__init__.py +0 -1
- package/src/devcouncil/llm/cache.py +0 -38
- package/src/devcouncil/llm/provider.py +0 -146
- package/src/devcouncil/llm/router.py +0 -125
- package/src/devcouncil/planning/__init__.py +0 -1
- package/src/devcouncil/planning/arbiter_service.py +0 -57
- package/src/devcouncil/planning/critique_service.py +0 -66
- package/src/devcouncil/planning/plan_service.py +0 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +0 -86
- package/src/devcouncil/planning/repair_service.py +0 -39
- package/src/devcouncil/planning/spec_service.py +0 -44
- package/src/devcouncil/repo/__init__.py +0 -0
- 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 -20
- package/src/devcouncil/reporting/markdown_report.py +0 -68
- package/src/devcouncil/reporting/report_builder.py +0 -14
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +0 -66
- package/src/devcouncil/storage/models.py +0 -83
- package/src/devcouncil/storage/repositories.py +0 -423
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +0 -34
- package/src/devcouncil/telemetry/traces.py +0 -91
- package/src/devcouncil/telemetry/tracker.py +0 -49
- package/src/devcouncil/ui/__init__.py +0 -1
- package/src/devcouncil/ui/dashboard.py +0 -122
- package/src/devcouncil/utils/__init__.py +0 -1
- package/src/devcouncil/utils/redaction.py +0 -141
- package/src/devcouncil/verification/__init__.py +0 -1
- package/src/devcouncil/verification/implementation_reviewer.py +0 -55
- package/src/devcouncil/verification/verifier.py +0 -530
- package/uv.lock +0 -1085
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
from devcouncil.domain.task import Task
|
|
2
|
-
from devcouncil.domain.gap import Gap
|
|
3
|
-
|
|
4
|
-
class PlannedFilesCheck:
|
|
5
|
-
"""Ensures a task has legitimate files planned for modification."""
|
|
6
|
-
|
|
7
|
-
def check(self, task: Task) -> list[Gap]:
|
|
8
|
-
gaps = []
|
|
9
|
-
if not task.planned_files:
|
|
10
|
-
gaps.append(Gap(
|
|
11
|
-
id=f"GAP-{task.id}-NO-FILES",
|
|
12
|
-
severity="high",
|
|
13
|
-
gap_type="task_not_implemented",
|
|
14
|
-
task_id=task.id,
|
|
15
|
-
description=f"Task {task.id} has no planned files. Agents won't know where to write code.",
|
|
16
|
-
recommended_fix="Update the task to include at least one planned file path.",
|
|
17
|
-
blocking=True
|
|
18
|
-
))
|
|
19
|
-
|
|
20
|
-
has_modify = any(pf.allowed_change in ["create", "modify", "delete"] for pf in task.planned_files)
|
|
21
|
-
if task.planned_files and not has_modify:
|
|
22
|
-
gaps.append(Gap(
|
|
23
|
-
id=f"GAP-{task.id}-READ-ONLY",
|
|
24
|
-
severity="medium",
|
|
25
|
-
gap_type="task_not_implemented",
|
|
26
|
-
task_id=task.id,
|
|
27
|
-
description=f"Task {task.id} only has read-only files. No changes can be made.",
|
|
28
|
-
recommended_fix="Grant 'modify' or 'create' permissions to at least one file.",
|
|
29
|
-
blocking=True
|
|
30
|
-
))
|
|
31
|
-
|
|
32
|
-
return gaps
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
from devcouncil.domain.requirement import Requirement
|
|
2
|
-
from devcouncil.domain.task import Task
|
|
3
|
-
from devcouncil.domain.gap import Gap
|
|
4
|
-
from typing import List
|
|
5
|
-
|
|
6
|
-
class RequirementCoverageCheck:
|
|
7
|
-
"""Detects requirements that are not mapped to any tasks."""
|
|
8
|
-
|
|
9
|
-
def check(self, requirements: List[Requirement], tasks: List[Task]) -> List[Gap]:
|
|
10
|
-
task_req_ids = set()
|
|
11
|
-
for t in tasks:
|
|
12
|
-
task_req_ids.update(t.requirement_ids)
|
|
13
|
-
|
|
14
|
-
gaps = []
|
|
15
|
-
for req in requirements:
|
|
16
|
-
if req.id not in task_req_ids:
|
|
17
|
-
gaps.append(Gap(
|
|
18
|
-
id=f"GAP-PLAN-{req.id}-UNMAPPED",
|
|
19
|
-
severity="high",
|
|
20
|
-
gap_type="requirement_not_planned",
|
|
21
|
-
requirement_id=req.id,
|
|
22
|
-
description=f"Requirement '{req.title}' is not covered by any task.",
|
|
23
|
-
recommended_fix="Decompose this requirement into one or more implementation tasks.",
|
|
24
|
-
blocking=True
|
|
25
|
-
))
|
|
26
|
-
return gaps
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
from typing import List
|
|
2
|
-
from devcouncil.domain.gap import Gap
|
|
3
|
-
from devcouncil.utils.redaction import SECRET_PATTERNS, redact_string
|
|
4
|
-
|
|
5
|
-
class SecretScanner:
|
|
6
|
-
"""Scans code diffs for potential secrets (API keys, tokens, etc.)."""
|
|
7
|
-
|
|
8
|
-
def scan_diff(self, diff_content: str, task_id: str) -> List[Gap]:
|
|
9
|
-
gaps = []
|
|
10
|
-
lines = diff_content.splitlines()
|
|
11
|
-
current_file = "unknown_file"
|
|
12
|
-
|
|
13
|
-
for i, line in enumerate(lines):
|
|
14
|
-
if line.startswith("+++ b/"):
|
|
15
|
-
current_file = line[6:]
|
|
16
|
-
continue
|
|
17
|
-
|
|
18
|
-
# Only scan added lines in diff
|
|
19
|
-
if not line.startswith("+") or line.startswith("+++"):
|
|
20
|
-
continue
|
|
21
|
-
|
|
22
|
-
for key_type, pattern in SECRET_PATTERNS.items():
|
|
23
|
-
if pattern.search(line):
|
|
24
|
-
gaps.append(Gap(
|
|
25
|
-
id=f"GAP-{task_id}-SECRET-{key_type.upper()}-{i}",
|
|
26
|
-
severity="critical",
|
|
27
|
-
gap_type="security_risk",
|
|
28
|
-
task_id=task_id,
|
|
29
|
-
description=f"Potential {key_type} found in {current_file} (diff line {i+1}).",
|
|
30
|
-
evidence=[redact_string(line.strip())],
|
|
31
|
-
recommended_fix="Remove the secret and use environment variables or a secret manager.",
|
|
32
|
-
blocking=True
|
|
33
|
-
))
|
|
34
|
-
return gaps
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
from pydantic import BaseModel
|
|
2
|
-
from typing import Any, List, Optional
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
from devcouncil.domain.requirement import Requirement
|
|
6
|
-
from devcouncil.domain.task import Task
|
|
7
|
-
from devcouncil.domain.gap import Gap
|
|
8
|
-
from devcouncil.domain.assumption import Assumption
|
|
9
|
-
from devcouncil.domain.critique import CritiqueFinding
|
|
10
|
-
from devcouncil.gating.checks.requirement_coverage import RequirementCoverageCheck
|
|
11
|
-
from devcouncil.gating.checks.planned_files_check import PlannedFilesCheck
|
|
12
|
-
from devcouncil.gating.checks.clean_git import CleanGitCheck
|
|
13
|
-
|
|
14
|
-
class GateResult(BaseModel):
|
|
15
|
-
passed: bool
|
|
16
|
-
gaps: List[Gap]
|
|
17
|
-
|
|
18
|
-
class GatePolicy:
|
|
19
|
-
"""Central engine for executing project and task level quality gates."""
|
|
20
|
-
|
|
21
|
-
def __init__(self):
|
|
22
|
-
self.req_coverage = RequirementCoverageCheck()
|
|
23
|
-
self.planned_files = PlannedFilesCheck()
|
|
24
|
-
self.clean_git = CleanGitCheck()
|
|
25
|
-
|
|
26
|
-
def check_plan_approval(
|
|
27
|
-
self,
|
|
28
|
-
requirements: List[Requirement],
|
|
29
|
-
tasks: List[Task],
|
|
30
|
-
assumptions: Optional[List[Assumption]] = None,
|
|
31
|
-
findings: Optional[List[CritiqueFinding]] = None,
|
|
32
|
-
blocking_questions: Optional[List[Any]] = None,
|
|
33
|
-
) -> GateResult:
|
|
34
|
-
"""Determines if the overall project plan is ready for execution."""
|
|
35
|
-
gaps = []
|
|
36
|
-
known_req_ids = {req.id for req in requirements}
|
|
37
|
-
known_ac_ids = {
|
|
38
|
-
ac.id
|
|
39
|
-
for req in requirements
|
|
40
|
-
for ac in req.acceptance_criteria
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
# 1. Check requirement coverage
|
|
44
|
-
gaps.extend(self.req_coverage.check(requirements, tasks))
|
|
45
|
-
|
|
46
|
-
# 2. Check for acceptance criteria presence
|
|
47
|
-
for req in requirements:
|
|
48
|
-
if not req.acceptance_criteria:
|
|
49
|
-
gaps.append(Gap(
|
|
50
|
-
id=f"GAP-PLAN-{req.id}-NO-AC",
|
|
51
|
-
severity="high",
|
|
52
|
-
gap_type="requirement_not_planned",
|
|
53
|
-
requirement_id=req.id,
|
|
54
|
-
description=f"Requirement {req.id} has no acceptance criteria.",
|
|
55
|
-
recommended_fix="Define at least one measurable AC.",
|
|
56
|
-
blocking=True
|
|
57
|
-
))
|
|
58
|
-
|
|
59
|
-
for ac in req.acceptance_criteria:
|
|
60
|
-
if not ac.verification_method:
|
|
61
|
-
gaps.append(Gap(
|
|
62
|
-
id=f"GAP-PLAN-{ac.id}-NO-VERIFY",
|
|
63
|
-
severity="high",
|
|
64
|
-
gap_type="acceptance_criteria_unproven",
|
|
65
|
-
requirement_id=req.id,
|
|
66
|
-
description=f"Acceptance criterion {ac.id} has no verification method.",
|
|
67
|
-
recommended_fix="Define a deterministic verification method for this AC.",
|
|
68
|
-
blocking=True,
|
|
69
|
-
))
|
|
70
|
-
|
|
71
|
-
for task in tasks:
|
|
72
|
-
if not task.requirement_ids:
|
|
73
|
-
gaps.append(Gap(
|
|
74
|
-
id=f"GAP-PLAN-{task.id}-NO-REQ",
|
|
75
|
-
severity="high",
|
|
76
|
-
gap_type="requirement_not_planned",
|
|
77
|
-
task_id=task.id,
|
|
78
|
-
description=f"Task {task.id} is not mapped to any requirement.",
|
|
79
|
-
recommended_fix="Map each task to at least one requirement.",
|
|
80
|
-
blocking=True,
|
|
81
|
-
))
|
|
82
|
-
|
|
83
|
-
unknown_req_ids = [req_id for req_id in task.requirement_ids if req_id not in known_req_ids]
|
|
84
|
-
if unknown_req_ids:
|
|
85
|
-
gaps.append(Gap(
|
|
86
|
-
id=f"GAP-PLAN-{task.id}-UNKNOWN-REQ",
|
|
87
|
-
severity="high",
|
|
88
|
-
gap_type="requirement_not_planned",
|
|
89
|
-
task_id=task.id,
|
|
90
|
-
description=f"Task {task.id} references unknown requirement(s): {', '.join(unknown_req_ids)}.",
|
|
91
|
-
recommended_fix="Remove invalid requirement links or add the missing requirements.",
|
|
92
|
-
blocking=True,
|
|
93
|
-
))
|
|
94
|
-
|
|
95
|
-
unknown_ac_ids = [ac_id for ac_id in task.acceptance_criterion_ids if ac_id not in known_ac_ids]
|
|
96
|
-
if unknown_ac_ids:
|
|
97
|
-
gaps.append(Gap(
|
|
98
|
-
id=f"GAP-PLAN-{task.id}-UNKNOWN-AC",
|
|
99
|
-
severity="high",
|
|
100
|
-
gap_type="acceptance_criteria_unproven",
|
|
101
|
-
task_id=task.id,
|
|
102
|
-
description=f"Task {task.id} references unknown acceptance criteria: {', '.join(unknown_ac_ids)}.",
|
|
103
|
-
recommended_fix="Link tasks only to acceptance criteria declared by requirements.",
|
|
104
|
-
blocking=True,
|
|
105
|
-
))
|
|
106
|
-
|
|
107
|
-
for assumption in assumptions or []:
|
|
108
|
-
if (
|
|
109
|
-
assumption.impact == "high"
|
|
110
|
-
and assumption.status == "open"
|
|
111
|
-
and assumption.requires_user_confirmation
|
|
112
|
-
):
|
|
113
|
-
gaps.append(Gap(
|
|
114
|
-
id=f"GAP-PLAN-{assumption.id}-OPEN",
|
|
115
|
-
severity="high",
|
|
116
|
-
gap_type="assumption_violated",
|
|
117
|
-
requirement_id=assumption.linked_requirement_ids[0] if assumption.linked_requirement_ids else None,
|
|
118
|
-
description=f"High-impact assumption {assumption.id} is still open: {assumption.statement}",
|
|
119
|
-
recommended_fix="Confirm, reject, or convert this assumption before approving the plan.",
|
|
120
|
-
blocking=True,
|
|
121
|
-
))
|
|
122
|
-
|
|
123
|
-
for finding in findings or []:
|
|
124
|
-
if finding.severity in {"high", "critical"} and finding.status == "open":
|
|
125
|
-
gaps.append(Gap(
|
|
126
|
-
id=f"GAP-PLAN-{finding.id}-OPEN",
|
|
127
|
-
severity=finding.severity,
|
|
128
|
-
gap_type="architecture_drift",
|
|
129
|
-
requirement_id=finding.linked_requirement_id,
|
|
130
|
-
description=f"Open {finding.severity} critique finding remains: {finding.claim}",
|
|
131
|
-
recommended_fix="Convert, rebut with evidence, or mark this finding resolved before approval.",
|
|
132
|
-
blocking=True,
|
|
133
|
-
))
|
|
134
|
-
|
|
135
|
-
for question in blocking_questions or []:
|
|
136
|
-
question_id = getattr(question, "id", "QUESTION")
|
|
137
|
-
question_text = getattr(question, "question", str(question))
|
|
138
|
-
gaps.append(Gap(
|
|
139
|
-
id=f"GAP-PLAN-{question_id}-BLOCKING",
|
|
140
|
-
severity="high",
|
|
141
|
-
gap_type="requirement_not_planned",
|
|
142
|
-
description=f"Blocking question remains unanswered: {question_text}",
|
|
143
|
-
recommended_fix="Answer or convert the blocking question before approving the plan.",
|
|
144
|
-
blocking=True,
|
|
145
|
-
))
|
|
146
|
-
|
|
147
|
-
return GateResult(
|
|
148
|
-
passed=len([g for g in gaps if g.blocking]) == 0,
|
|
149
|
-
gaps=gaps
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
def check_task_ready(self, task: Task, project_root: Path) -> GateResult:
|
|
153
|
-
"""Determines if a specific task can begin execution."""
|
|
154
|
-
gaps = []
|
|
155
|
-
|
|
156
|
-
# 1. Check Git state
|
|
157
|
-
gaps.extend(self.clean_git.check(project_root, task.id))
|
|
158
|
-
|
|
159
|
-
# 2. Check planned files
|
|
160
|
-
gaps.extend(self.planned_files.check(task))
|
|
161
|
-
|
|
162
|
-
# 3. Check execution/verification evidence contract
|
|
163
|
-
if not task.allowed_commands:
|
|
164
|
-
gaps.append(Gap(
|
|
165
|
-
id=f"GAP-{task.id}-NO-COMMANDS",
|
|
166
|
-
severity="high",
|
|
167
|
-
gap_type="missing_test",
|
|
168
|
-
task_id=task.id,
|
|
169
|
-
description=f"Task {task.id} has no allowed commands for execution or verification.",
|
|
170
|
-
recommended_fix="Add explicit allowed_commands for the task before execution.",
|
|
171
|
-
blocking=True,
|
|
172
|
-
))
|
|
173
|
-
|
|
174
|
-
if not task.expected_tests:
|
|
175
|
-
gaps.append(Gap(
|
|
176
|
-
id=f"GAP-{task.id}-NO-EXPECTED-EVIDENCE",
|
|
177
|
-
severity="high",
|
|
178
|
-
gap_type="missing_test",
|
|
179
|
-
task_id=task.id,
|
|
180
|
-
description=f"Task {task.id} has no expected verification evidence.",
|
|
181
|
-
recommended_fix="Add expected_tests or targeted static/manual review commands that prove the acceptance criteria.",
|
|
182
|
-
blocking=True,
|
|
183
|
-
))
|
|
184
|
-
|
|
185
|
-
# 4. Check for task dependencies (if implemented)
|
|
186
|
-
|
|
187
|
-
return GateResult(
|
|
188
|
-
passed=len([g for g in gaps if g.blocking]) == 0,
|
|
189
|
-
gaps=gaps
|
|
190
|
-
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import ast
|
|
4
|
-
import re
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@dataclass(frozen=True)
|
|
10
|
-
class AstMatch:
|
|
11
|
-
path: str
|
|
12
|
-
language: str
|
|
13
|
-
kind: str
|
|
14
|
-
name: str
|
|
15
|
-
line: int
|
|
16
|
-
text: str
|
|
17
|
-
engine: str
|
|
18
|
-
|
|
19
|
-
def model_dump(self) -> dict[str, object]:
|
|
20
|
-
return {
|
|
21
|
-
"path": self.path,
|
|
22
|
-
"language": self.language,
|
|
23
|
-
"kind": self.kind,
|
|
24
|
-
"name": self.name,
|
|
25
|
-
"line": self.line,
|
|
26
|
-
"text": self.text,
|
|
27
|
-
"engine": self.engine,
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class AstMatcher:
|
|
32
|
-
"""Tree-sitter-style structural search with optional tree_sitter and deterministic fallbacks."""
|
|
33
|
-
|
|
34
|
-
_EXT_LANGUAGE = {
|
|
35
|
-
".py": "python",
|
|
36
|
-
".ts": "typescript",
|
|
37
|
-
".tsx": "typescript",
|
|
38
|
-
".js": "javascript",
|
|
39
|
-
".jsx": "javascript",
|
|
40
|
-
".go": "go",
|
|
41
|
-
".rs": "rust",
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
_SYMBOL_PATTERNS: dict[str, re.Pattern[str]] = {
|
|
45
|
-
"typescript": re.compile(
|
|
46
|
-
r"^\s*(?:export\s+)?(?:(?:async\s+)?(?:function|class|interface|type)\s+|const\s+)([A-Za-z_$][\w$]*)"
|
|
47
|
-
),
|
|
48
|
-
"javascript": re.compile(
|
|
49
|
-
r"^\s*(?:export\s+)?(?:(?:async\s+)?(?:function|class)\s+|const\s+)([A-Za-z_$][\w$]*)"
|
|
50
|
-
),
|
|
51
|
-
"go": re.compile(r"^\s*func\s+(?:\([^)]+\)\s*)?([A-Za-z_]\w*)\s*\("),
|
|
52
|
-
"rust": re.compile(r"^\s*(?:pub\s+)?(?:async\s+)?(?:fn|struct|enum|trait)\s+([A-Za-z_]\w*)"),
|
|
53
|
-
}
|
|
54
|
-
_IGNORED_DIRS = {".git", ".devcouncil", "__pycache__", ".venv", "node_modules", "dist", "build", "target", "vendor"}
|
|
55
|
-
|
|
56
|
-
def __init__(self, project_root: Path):
|
|
57
|
-
self.project_root = project_root
|
|
58
|
-
self.tree_sitter_available = self._has_tree_sitter()
|
|
59
|
-
|
|
60
|
-
def _has_tree_sitter(self) -> bool:
|
|
61
|
-
try:
|
|
62
|
-
import tree_sitter # noqa: F401
|
|
63
|
-
except Exception:
|
|
64
|
-
return False
|
|
65
|
-
return True
|
|
66
|
-
|
|
67
|
-
def match(
|
|
68
|
-
self,
|
|
69
|
-
*,
|
|
70
|
-
query: str = "",
|
|
71
|
-
language: str | None = None,
|
|
72
|
-
kind: str | None = None,
|
|
73
|
-
limit: int = 100,
|
|
74
|
-
) -> list[AstMatch]:
|
|
75
|
-
language = language.lower() if language else None
|
|
76
|
-
kind = kind.lower() if kind else None
|
|
77
|
-
limit = max(1, limit)
|
|
78
|
-
matches: list[AstMatch] = []
|
|
79
|
-
for path in self._candidate_files(language):
|
|
80
|
-
try:
|
|
81
|
-
text = path.read_text(encoding="utf-8")
|
|
82
|
-
except (OSError, UnicodeDecodeError):
|
|
83
|
-
continue
|
|
84
|
-
rel = path.relative_to(self.project_root).as_posix()
|
|
85
|
-
file_language = self._EXT_LANGUAGE.get(path.suffix.lower(), path.suffix.lower().lstrip("."))
|
|
86
|
-
matches.extend(self._match_file(rel, file_language, text, query=query, kind=kind))
|
|
87
|
-
if len(matches) >= limit:
|
|
88
|
-
return matches[:limit]
|
|
89
|
-
return matches[:limit]
|
|
90
|
-
|
|
91
|
-
def _candidate_files(self, language: str | None) -> list[Path]:
|
|
92
|
-
allowed_exts = {
|
|
93
|
-
ext for ext, ext_language in self._EXT_LANGUAGE.items()
|
|
94
|
-
if language is None or ext_language == language
|
|
95
|
-
}
|
|
96
|
-
files: list[Path] = []
|
|
97
|
-
try:
|
|
98
|
-
for path in self.project_root.rglob("*"):
|
|
99
|
-
if not path.is_file() or path.suffix.lower() not in allowed_exts:
|
|
100
|
-
continue
|
|
101
|
-
if any(part in self._IGNORED_DIRS for part in path.parts):
|
|
102
|
-
continue
|
|
103
|
-
files.append(path)
|
|
104
|
-
except OSError:
|
|
105
|
-
return files
|
|
106
|
-
return sorted(files)
|
|
107
|
-
|
|
108
|
-
def _match_file(self, rel: str, language: str, text: str, *, query: str, kind: str | None) -> list[AstMatch]:
|
|
109
|
-
if language == "python":
|
|
110
|
-
return self._match_python(rel, text, query=query, kind=kind)
|
|
111
|
-
pattern = self._SYMBOL_PATTERNS.get(language)
|
|
112
|
-
if not pattern:
|
|
113
|
-
return []
|
|
114
|
-
results: list[AstMatch] = []
|
|
115
|
-
for lineno, line in enumerate(text.splitlines(), start=1):
|
|
116
|
-
match = pattern.match(line)
|
|
117
|
-
if not match:
|
|
118
|
-
continue
|
|
119
|
-
symbol_name = match.group(1)
|
|
120
|
-
symbol_kind = self._line_kind(line)
|
|
121
|
-
if kind and kind != symbol_kind:
|
|
122
|
-
continue
|
|
123
|
-
if query and query.lower() not in symbol_name.lower() and query.lower() not in line.lower():
|
|
124
|
-
continue
|
|
125
|
-
results.append(AstMatch(rel, language, symbol_kind, symbol_name, lineno, line.strip(), self._engine()))
|
|
126
|
-
return results
|
|
127
|
-
|
|
128
|
-
def _match_python(self, rel: str, text: str, *, query: str, kind: str | None) -> list[AstMatch]:
|
|
129
|
-
try:
|
|
130
|
-
tree = ast.parse(text)
|
|
131
|
-
except SyntaxError:
|
|
132
|
-
return []
|
|
133
|
-
lines = text.splitlines()
|
|
134
|
-
results: list[AstMatch] = []
|
|
135
|
-
for node in ast.walk(tree):
|
|
136
|
-
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
137
|
-
symbol_kind = "function"
|
|
138
|
-
elif isinstance(node, ast.ClassDef):
|
|
139
|
-
symbol_kind = "class"
|
|
140
|
-
else:
|
|
141
|
-
continue
|
|
142
|
-
symbol_name = node.name
|
|
143
|
-
source = lines[node.lineno - 1].strip() if 0 < node.lineno <= len(lines) else symbol_name
|
|
144
|
-
if kind and kind != symbol_kind:
|
|
145
|
-
continue
|
|
146
|
-
if query and query.lower() not in symbol_name.lower() and query.lower() not in source.lower():
|
|
147
|
-
continue
|
|
148
|
-
results.append(AstMatch(rel, "python", symbol_kind, symbol_name, node.lineno, source, self._engine()))
|
|
149
|
-
return sorted(results, key=lambda item: (item.path, item.line))
|
|
150
|
-
|
|
151
|
-
def _line_kind(self, line: str) -> str:
|
|
152
|
-
stripped = line.strip()
|
|
153
|
-
if "class " in stripped:
|
|
154
|
-
return "class"
|
|
155
|
-
if stripped.startswith(("type ", "export type ")):
|
|
156
|
-
return "type"
|
|
157
|
-
if stripped.startswith(("interface ", "export interface ")):
|
|
158
|
-
return "interface"
|
|
159
|
-
if stripped.startswith(("struct ", "pub struct ")):
|
|
160
|
-
return "struct"
|
|
161
|
-
if stripped.startswith(("enum ", "pub enum ")):
|
|
162
|
-
return "enum"
|
|
163
|
-
if stripped.startswith(("trait ", "pub trait ")):
|
|
164
|
-
return "trait"
|
|
165
|
-
return "function"
|
|
166
|
-
|
|
167
|
-
def _engine(self) -> str:
|
|
168
|
-
return "tree-sitter-optional" if self.tree_sitter_available else "fallback-ast"
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
from typing import List, Dict, Any, Set
|
|
2
|
-
from pydantic import BaseModel, Field
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
class GraphNode(BaseModel):
|
|
6
|
-
id: str
|
|
7
|
-
type: str # "file", "symbol", "requirement", "task"
|
|
8
|
-
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
9
|
-
|
|
10
|
-
class GraphEdge(BaseModel):
|
|
11
|
-
source: str
|
|
12
|
-
target: str
|
|
13
|
-
relation: str # "imports", "implements", "validates", "contains"
|
|
14
|
-
|
|
15
|
-
class KnowledgeGraph(BaseModel):
|
|
16
|
-
nodes: List[GraphNode] = []
|
|
17
|
-
edges: List[GraphEdge] = []
|
|
18
|
-
|
|
19
|
-
class GraphIndex:
|
|
20
|
-
def __init__(self, project_root: Path):
|
|
21
|
-
self.project_root = project_root
|
|
22
|
-
self.graph = KnowledgeGraph()
|
|
23
|
-
|
|
24
|
-
def build_initial_graph(self, files: List[str]):
|
|
25
|
-
"""
|
|
26
|
-
Bootstrap the graph from file list.
|
|
27
|
-
"""
|
|
28
|
-
for f in files:
|
|
29
|
-
self.graph.nodes.append(GraphNode(
|
|
30
|
-
id=f,
|
|
31
|
-
type="file",
|
|
32
|
-
metadata={"extension": Path(f).suffix}
|
|
33
|
-
))
|
|
34
|
-
|
|
35
|
-
def add_relation(self, source: str, target: str, relation: str):
|
|
36
|
-
self.graph.edges.append(GraphEdge(source=source, target=target, relation=relation))
|
|
37
|
-
|
|
38
|
-
def get_context_for_file(self, file_path: str) -> Set[str]:
|
|
39
|
-
"""
|
|
40
|
-
Retrieve related paths for a given file.
|
|
41
|
-
"""
|
|
42
|
-
related = {file_path}
|
|
43
|
-
for edge in self.graph.edges:
|
|
44
|
-
if edge.source == file_path:
|
|
45
|
-
related.add(edge.target)
|
|
46
|
-
if edge.target == file_path:
|
|
47
|
-
related.add(edge.source)
|
|
48
|
-
return related
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import shutil
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@dataclass(frozen=True)
|
|
11
|
-
class LspServerCandidate:
|
|
12
|
-
language: str
|
|
13
|
-
command: list[str]
|
|
14
|
-
available: bool
|
|
15
|
-
reason: str
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class LspInspector:
|
|
19
|
-
"""Starter LSP integration focused on discovery and safe initialize payloads."""
|
|
20
|
-
|
|
21
|
-
_LANGUAGE_SERVERS: dict[str, list[list[str]]] = {
|
|
22
|
-
"python": [["pyright-langserver", "--stdio"], ["pylsp"]],
|
|
23
|
-
"typescript": [["typescript-language-server", "--stdio"]],
|
|
24
|
-
"javascript": [["typescript-language-server", "--stdio"]],
|
|
25
|
-
"go": [["gopls"]],
|
|
26
|
-
"rust": [["rust-analyzer"]],
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
_EXTENSIONS: dict[str, str] = {
|
|
30
|
-
".py": "python",
|
|
31
|
-
".ts": "typescript",
|
|
32
|
-
".tsx": "typescript",
|
|
33
|
-
".js": "javascript",
|
|
34
|
-
".jsx": "javascript",
|
|
35
|
-
".go": "go",
|
|
36
|
-
".rs": "rust",
|
|
37
|
-
}
|
|
38
|
-
_IGNORED_DIRS = {".git", ".devcouncil", "__pycache__", ".venv", "node_modules", "dist", "build", "target", "vendor"}
|
|
39
|
-
|
|
40
|
-
def __init__(self, project_root: Path):
|
|
41
|
-
self.project_root = project_root
|
|
42
|
-
|
|
43
|
-
def _is_ignored_path(self, file: str) -> bool:
|
|
44
|
-
return any(part in self._IGNORED_DIRS for part in Path(file).parts)
|
|
45
|
-
|
|
46
|
-
def detect_languages(self, files: list[str] | None = None) -> list[str]:
|
|
47
|
-
if files is None:
|
|
48
|
-
try:
|
|
49
|
-
discovered: list[str] = []
|
|
50
|
-
for path in self.project_root.rglob("*"):
|
|
51
|
-
if path.is_file() and not any(part in self._IGNORED_DIRS for part in path.parts):
|
|
52
|
-
discovered.append(str(path.relative_to(self.project_root)))
|
|
53
|
-
files = discovered
|
|
54
|
-
except OSError:
|
|
55
|
-
files = []
|
|
56
|
-
languages = {
|
|
57
|
-
self._EXTENSIONS[Path(file).suffix.lower()]
|
|
58
|
-
for file in files
|
|
59
|
-
if Path(file).suffix.lower() in self._EXTENSIONS and not self._is_ignored_path(file)
|
|
60
|
-
}
|
|
61
|
-
return sorted(languages)
|
|
62
|
-
|
|
63
|
-
def server_candidates(self, files: list[str] | None = None) -> list[LspServerCandidate]:
|
|
64
|
-
candidates: list[LspServerCandidate] = []
|
|
65
|
-
for language in self.detect_languages(files):
|
|
66
|
-
for command in self._LANGUAGE_SERVERS.get(language, []):
|
|
67
|
-
executable = command[0]
|
|
68
|
-
available = shutil.which(executable) is not None
|
|
69
|
-
candidates.append(
|
|
70
|
-
LspServerCandidate(
|
|
71
|
-
language=language,
|
|
72
|
-
command=command,
|
|
73
|
-
available=available,
|
|
74
|
-
reason="found on PATH" if available else "not found on PATH",
|
|
75
|
-
)
|
|
76
|
-
)
|
|
77
|
-
return candidates
|
|
78
|
-
|
|
79
|
-
def initialize_request(self, language: str) -> dict[str, Any]:
|
|
80
|
-
return {
|
|
81
|
-
"jsonrpc": "2.0",
|
|
82
|
-
"id": 1,
|
|
83
|
-
"method": "initialize",
|
|
84
|
-
"params": {
|
|
85
|
-
"processId": None,
|
|
86
|
-
"rootUri": self.project_root.resolve().as_uri(),
|
|
87
|
-
"capabilities": {
|
|
88
|
-
"textDocument": {
|
|
89
|
-
"publishDiagnostics": {"relatedInformation": True},
|
|
90
|
-
"definition": {"linkSupport": True},
|
|
91
|
-
"references": {},
|
|
92
|
-
"documentSymbol": {"hierarchicalDocumentSymbolSupport": True},
|
|
93
|
-
},
|
|
94
|
-
"workspace": {"symbol": {}},
|
|
95
|
-
},
|
|
96
|
-
"initializationOptions": {"language": language},
|
|
97
|
-
},
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
def summary(self, files: list[str] | None = None) -> dict[str, Any]:
|
|
101
|
-
candidates = self.server_candidates(files)
|
|
102
|
-
return {
|
|
103
|
-
"languages": self.detect_languages(files),
|
|
104
|
-
"servers": [
|
|
105
|
-
{
|
|
106
|
-
"language": candidate.language,
|
|
107
|
-
"command": candidate.command,
|
|
108
|
-
"available": candidate.available,
|
|
109
|
-
"reason": candidate.reason,
|
|
110
|
-
}
|
|
111
|
-
for candidate in candidates
|
|
112
|
-
],
|
|
113
|
-
"initialize_requests": {
|
|
114
|
-
language: self.initialize_request(language)
|
|
115
|
-
for language in sorted({candidate.language for candidate in candidates})
|
|
116
|
-
},
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
def summary_json(self, files: list[str] | None = None) -> str:
|
|
120
|
-
return json.dumps(self.summary(files), indent=2)
|