devcouncil 0.2.0 → 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.
Files changed (205) hide show
  1. package/README.md +191 -253
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +6 -17
  4. package/pyproject.toml +0 -66
  5. package/src/devcouncil/__init__.py +0 -0
  6. package/src/devcouncil/__main__.py +0 -4
  7. package/src/devcouncil/app/__init__.py +0 -28
  8. package/src/devcouncil/app/config.py +0 -320
  9. package/src/devcouncil/app/errors.py +0 -23
  10. package/src/devcouncil/app/events.py +0 -44
  11. package/src/devcouncil/app/orchestrator.py +0 -92
  12. package/src/devcouncil/app/project_status.py +0 -29
  13. package/src/devcouncil/app/run_context.py +0 -39
  14. package/src/devcouncil/app/state_machine.py +0 -108
  15. package/src/devcouncil/artifacts/__init__.py +0 -1
  16. package/src/devcouncil/artifacts/coverage.py +0 -96
  17. package/src/devcouncil/artifacts/graph.py +0 -163
  18. package/src/devcouncil/artifacts/migrations.py +0 -20
  19. package/src/devcouncil/artifacts/schemas.py +0 -23
  20. package/src/devcouncil/artifacts/serializer.py +0 -21
  21. package/src/devcouncil/artifacts/validators.py +0 -27
  22. package/src/devcouncil/assets/__init__.py +0 -1
  23. package/src/devcouncil/assets/devcouncil-logo.svg +0 -60
  24. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  25. package/src/devcouncil/cli/__init__.py +0 -0
  26. package/src/devcouncil/cli/commands/__init__.py +0 -0
  27. package/src/devcouncil/cli/commands/agents.py +0 -292
  28. package/src/devcouncil/cli/commands/artifacts.py +0 -54
  29. package/src/devcouncil/cli/commands/ast.py +0 -22
  30. package/src/devcouncil/cli/commands/baseline.py +0 -35
  31. package/src/devcouncil/cli/commands/check.py +0 -209
  32. package/src/devcouncil/cli/commands/config.py +0 -115
  33. package/src/devcouncil/cli/commands/cost.py +0 -57
  34. package/src/devcouncil/cli/commands/dashboard.py +0 -31
  35. package/src/devcouncil/cli/commands/doctor.py +0 -340
  36. package/src/devcouncil/cli/commands/evidence.py +0 -48
  37. package/src/devcouncil/cli/commands/go.py +0 -656
  38. package/src/devcouncil/cli/commands/handoff.py +0 -69
  39. package/src/devcouncil/cli/commands/hook.py +0 -237
  40. package/src/devcouncil/cli/commands/init.py +0 -289
  41. package/src/devcouncil/cli/commands/integrate.py +0 -1515
  42. package/src/devcouncil/cli/commands/lsp.py +0 -20
  43. package/src/devcouncil/cli/commands/map.py +0 -112
  44. package/src/devcouncil/cli/commands/mcp_server.py +0 -18
  45. package/src/devcouncil/cli/commands/plan.py +0 -488
  46. package/src/devcouncil/cli/commands/prompt.py +0 -61
  47. package/src/devcouncil/cli/commands/repair.py +0 -89
  48. package/src/devcouncil/cli/commands/report.py +0 -137
  49. package/src/devcouncil/cli/commands/reset_demo_state.py +0 -33
  50. package/src/devcouncil/cli/commands/rollback.py +0 -59
  51. package/src/devcouncil/cli/commands/run.py +0 -289
  52. package/src/devcouncil/cli/commands/runs.py +0 -223
  53. package/src/devcouncil/cli/commands/scaffold.py +0 -32
  54. package/src/devcouncil/cli/commands/semantic.py +0 -47
  55. package/src/devcouncil/cli/commands/setup.py +0 -362
  56. package/src/devcouncil/cli/commands/shell.py +0 -73
  57. package/src/devcouncil/cli/commands/show.py +0 -76
  58. package/src/devcouncil/cli/commands/skills.py +0 -88
  59. package/src/devcouncil/cli/commands/status.py +0 -141
  60. package/src/devcouncil/cli/commands/tasks.py +0 -55
  61. package/src/devcouncil/cli/commands/trace.py +0 -88
  62. package/src/devcouncil/cli/commands/verify.py +0 -328
  63. package/src/devcouncil/cli/commands/version.py +0 -20
  64. package/src/devcouncil/cli/commands/watch.py +0 -574
  65. package/src/devcouncil/cli/commands/watch_fs.py +0 -40
  66. package/src/devcouncil/cli/main.py +0 -137
  67. package/src/devcouncil/council/__init__.py +0 -0
  68. package/src/devcouncil/council/prompts/__init__.py +0 -0
  69. package/src/devcouncil/council/prompts/arbiter.md +0 -19
  70. package/src/devcouncil/council/prompts/critic_a.md +0 -10
  71. package/src/devcouncil/council/prompts/critic_b.md +0 -10
  72. package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
  73. package/src/devcouncil/council/prompts/planner_a.md +0 -16
  74. package/src/devcouncil/council/prompts/planner_b.md +0 -16
  75. package/src/devcouncil/council/prompts/rebuttal.md +0 -10
  76. package/src/devcouncil/council/prompts/spec_writer.md +0 -12
  77. package/src/devcouncil/domain/__init__.py +0 -0
  78. package/src/devcouncil/domain/assumption.py +0 -17
  79. package/src/devcouncil/domain/critique.py +0 -32
  80. package/src/devcouncil/domain/evidence.py +0 -47
  81. package/src/devcouncil/domain/gap.py +0 -52
  82. package/src/devcouncil/domain/requirement.py +0 -22
  83. package/src/devcouncil/domain/task.py +0 -55
  84. package/src/devcouncil/execution/__init__.py +0 -1
  85. package/src/devcouncil/execution/checkpoints.py +0 -246
  86. package/src/devcouncil/execution/context_builder.py +0 -60
  87. package/src/devcouncil/execution/executor.py +0 -15
  88. package/src/devcouncil/execution/fs_watcher.py +0 -180
  89. package/src/devcouncil/execution/handoff.py +0 -102
  90. package/src/devcouncil/execution/hook_policy.py +0 -253
  91. package/src/devcouncil/execution/patch.py +0 -77
  92. package/src/devcouncil/execution/paths.py +0 -14
  93. package/src/devcouncil/execution/permissions.py +0 -85
  94. package/src/devcouncil/execution/policy_engine.py +0 -343
  95. package/src/devcouncil/execution/prompt_builder.py +0 -671
  96. package/src/devcouncil/execution/shell_session.py +0 -225
  97. package/src/devcouncil/execution/task_runner.py +0 -170
  98. package/src/devcouncil/executors/__init__.py +0 -1
  99. package/src/devcouncil/executors/agent_registry.py +0 -575
  100. package/src/devcouncil/executors/coding_cli.py +0 -736
  101. package/src/devcouncil/executors/mini_swe.py +0 -73
  102. package/src/devcouncil/executors/native/__init__.py +0 -0
  103. package/src/devcouncil/executors/native/agent.py +0 -208
  104. package/src/devcouncil/executors/openhands.py +0 -71
  105. package/src/devcouncil/gating/__init__.py +0 -1
  106. package/src/devcouncil/gating/checks/__init__.py +0 -0
  107. package/src/devcouncil/gating/checks/clean_git.py +0 -52
  108. package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
  109. package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
  110. package/src/devcouncil/gating/checks/secret_scan_check.py +0 -53
  111. package/src/devcouncil/gating/policy.py +0 -338
  112. package/src/devcouncil/hardware.py +0 -184
  113. package/src/devcouncil/indexing/__init__.py +0 -1
  114. package/src/devcouncil/indexing/ast_matcher.py +0 -168
  115. package/src/devcouncil/indexing/graph_index.py +0 -48
  116. package/src/devcouncil/indexing/lsp.py +0 -161
  117. package/src/devcouncil/indexing/repo_mapper.py +0 -1455
  118. package/src/devcouncil/indexing/semantic_index.py +0 -205
  119. package/src/devcouncil/integrations/actions.py +0 -146
  120. package/src/devcouncil/integrations/check.py +0 -423
  121. package/src/devcouncil/integrations/code_review_graph.py +0 -163
  122. package/src/devcouncil/integrations/github.py +0 -39
  123. package/src/devcouncil/integrations/github_intent.py +0 -142
  124. package/src/devcouncil/integrations/gitnexus.py +0 -62
  125. package/src/devcouncil/integrations/graphify.py +0 -34
  126. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  127. package/src/devcouncil/integrations/mcp/server.py +0 -2122
  128. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +0 -24
  129. package/src/devcouncil/integrations/pr_comments.py +0 -62
  130. package/src/devcouncil/live/__init__.py +0 -2
  131. package/src/devcouncil/live/cards.py +0 -349
  132. package/src/devcouncil/live/models.py +0 -63
  133. package/src/devcouncil/live/repair_prompt.py +0 -83
  134. package/src/devcouncil/live/reviewer.py +0 -70
  135. package/src/devcouncil/live/signals.py +0 -135
  136. package/src/devcouncil/live/summary.py +0 -34
  137. package/src/devcouncil/live/tasks.py +0 -18
  138. package/src/devcouncil/live/transcripts.py +0 -141
  139. package/src/devcouncil/llm/__init__.py +0 -1
  140. package/src/devcouncil/llm/cache.py +0 -42
  141. package/src/devcouncil/llm/model_defaults.yaml +0 -44
  142. package/src/devcouncil/llm/provider.py +0 -627
  143. package/src/devcouncil/llm/router.py +0 -310
  144. package/src/devcouncil/optimization/__init__.py +0 -1
  145. package/src/devcouncil/optimization/gepa_agent.py +0 -318
  146. package/src/devcouncil/planning/__init__.py +0 -1
  147. package/src/devcouncil/planning/arbiter_service.py +0 -57
  148. package/src/devcouncil/planning/correction_manifest.py +0 -303
  149. package/src/devcouncil/planning/critique_service.py +0 -71
  150. package/src/devcouncil/planning/plan_service.py +0 -60
  151. package/src/devcouncil/planning/prompt_enhancer_service.py +0 -167
  152. package/src/devcouncil/planning/repair_service.py +0 -39
  153. package/src/devcouncil/planning/spec_service.py +0 -70
  154. package/src/devcouncil/repo/__init__.py +0 -0
  155. package/src/devcouncil/repo/ci_scaffold.py +0 -157
  156. package/src/devcouncil/repo/gitignore.py +0 -123
  157. package/src/devcouncil/repo/sca.py +0 -374
  158. package/src/devcouncil/reporting/__init__.py +0 -0
  159. package/src/devcouncil/reporting/github_check.py +0 -32
  160. package/src/devcouncil/reporting/json_report.py +0 -30
  161. package/src/devcouncil/reporting/markdown_report.py +0 -83
  162. package/src/devcouncil/reporting/report_builder.py +0 -14
  163. package/src/devcouncil/skills/__init__.py +0 -19
  164. package/src/devcouncil/skills/library/README.md +0 -46
  165. package/src/devcouncil/skills/library/ai-training.md +0 -50
  166. package/src/devcouncil/skills/library/android.md +0 -50
  167. package/src/devcouncil/skills/library/backend.md +0 -52
  168. package/src/devcouncil/skills/library/core-engineering.md +0 -95
  169. package/src/devcouncil/skills/library/data-engineering.md +0 -47
  170. package/src/devcouncil/skills/library/desktop.md +0 -46
  171. package/src/devcouncil/skills/library/devops.md +0 -48
  172. package/src/devcouncil/skills/library/game-dev.md +0 -46
  173. package/src/devcouncil/skills/library/ios.md +0 -48
  174. package/src/devcouncil/skills/library/mobile-cross-platform.md +0 -46
  175. package/src/devcouncil/skills/library/security.md +0 -48
  176. package/src/devcouncil/skills/library/systems.md +0 -48
  177. package/src/devcouncil/skills/library/web.md +0 -47
  178. package/src/devcouncil/skills/library/windows.md +0 -47
  179. package/src/devcouncil/skills/registry.py +0 -330
  180. package/src/devcouncil/storage/__init__.py +0 -0
  181. package/src/devcouncil/storage/db.py +0 -147
  182. package/src/devcouncil/storage/models.py +0 -204
  183. package/src/devcouncil/storage/native.py +0 -557
  184. package/src/devcouncil/storage/repositories.py +0 -485
  185. package/src/devcouncil/telemetry/__init__.py +0 -0
  186. package/src/devcouncil/telemetry/cost.py +0 -140
  187. package/src/devcouncil/telemetry/model_pricing.yaml +0 -48
  188. package/src/devcouncil/telemetry/pricing.py +0 -28
  189. package/src/devcouncil/telemetry/traces.py +0 -146
  190. package/src/devcouncil/telemetry/tracker.py +0 -52
  191. package/src/devcouncil/ui/__init__.py +0 -1
  192. package/src/devcouncil/ui/dashboard.py +0 -423
  193. package/src/devcouncil/utils/__init__.py +0 -1
  194. package/src/devcouncil/utils/redaction.py +0 -147
  195. package/src/devcouncil/utils/subprocess_env.py +0 -69
  196. package/src/devcouncil/verification/__init__.py +0 -1
  197. package/src/devcouncil/verification/acceptance_compiler.py +0 -125
  198. package/src/devcouncil/verification/ad_hoc_check.py +0 -129
  199. package/src/devcouncil/verification/diff_coverage.py +0 -353
  200. package/src/devcouncil/verification/implementation_reviewer.py +0 -55
  201. package/src/devcouncil/verification/next_actions.py +0 -189
  202. package/src/devcouncil/verification/sandbox.py +0 -178
  203. package/src/devcouncil/verification/test_resolver.py +0 -91
  204. package/src/devcouncil/verification/verifier.py +0 -1548
  205. package/uv.lock +0 -1226
@@ -1,1455 +0,0 @@
1
- import ast
2
- import hashlib
3
- import json
4
- import logging
5
- import os
6
- import re
7
- import subprocess
8
- import sys
9
- from collections import Counter, defaultdict
10
- from pathlib import Path
11
- from typing import Dict, List, Set, Tuple
12
-
13
- from pydantic import BaseModel, Field
14
-
15
- from devcouncil.indexing.lsp import LspInspector
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
- # File extensions treated as primary source for subsystem inference.
20
- _CODE_EXTENSIONS = {".py", ".ts", ".tsx", ".js", ".jsx", ".go", ".rs", ".java", ".kt", ".rb", ".cs", ".cpp", ".c"}
21
- # Top-level directories grouped as their own area rather than folded into a source root.
22
- _AUX_AREA_ROOTS = {"tests", "test", "docs", "doc", "scripts", "examples", "example", "benchmarks"}
23
- # Filenames that signal an entry point, used to break ties when no import data exists.
24
- _ENTRY_NAME_HINTS = ("__init__", "__main__", "main", "index", "app", "cli", "server", "mod", "lib")
25
-
26
-
27
- class RepoFileEntry(BaseModel):
28
- path: str
29
- area: str
30
- kind: str
31
- language: str | None = None
32
- summary: str
33
-
34
-
35
- class RepoSubsystem(BaseModel):
36
- area: str
37
- summary: str
38
- entry_points: List[str]
39
- critical_files: List[str]
40
- neighbors: List[str] = Field(default_factory=list)
41
- handoff_paths: List[str] = Field(default_factory=list)
42
- role_files: Dict[str, List[str]] = Field(default_factory=dict)
43
-
44
-
45
- class RepoMap(BaseModel):
46
- languages: List[str]
47
- frameworks: List[str]
48
- package_managers: List[str]
49
- test_commands: List[str]
50
- important_files: List[str]
51
- candidate_files: List[Dict[str, str]]
52
- files: List[RepoFileEntry] = Field(default_factory=list)
53
- subsystems: List[RepoSubsystem] = Field(default_factory=list)
54
- # file -> the files that import it (reverse import edges, capped per file). Lets a
55
- # prompt show the blast radius of changing a file without re-parsing the repo.
56
- dependents: Dict[str, List[str]] = Field(default_factory=dict)
57
- # Freshness fingerprints captured at generation: the git HEAD the map was built from
58
- # and a hash of the tracked file set. Consumers compare against the current repo to
59
- # detect a stale map before trusting its structure.
60
- generated_head: str = ""
61
- indexed_hash: str = ""
62
- lsp: Dict[str, object] = Field(default_factory=dict)
63
- # Optional dependency-vulnerability findings. Populated only when `dev map` is
64
- # run with SCA explicitly enabled (off by default so the map stays fast and
65
- # offline-by-default); empty otherwise.
66
- dependency_risks: List[Dict[str, str]] = Field(default_factory=list)
67
-
68
- class RepoMapper:
69
- def __init__(self, project_root: Path):
70
- self.project_root = project_root
71
- # Common source-root prefix of the repo's primary code (e.g. "src/pkg"),
72
- # computed once per map_repo run. Drives generic, non-DevCouncil subsystem
73
- # inference. None until computed.
74
- self._source_root: str | None = None
75
- # True when this is not the DevCouncil source tree, so generic inference is used
76
- # for area bucketing. Set in map_repo.
77
- self._use_generic: bool = False
78
- # Import edges (importer -> imported), computed once per map_repo run and reused
79
- # by subsystem inference, important-file ranking, and the dependents index.
80
- self._edges: List[Tuple[str, str]] | None = None
81
-
82
- _DEPENDENTS_MAX = 12 # cap dependents listed per file to bound repo_map.json size
83
-
84
- _LANGUAGE_BY_EXTENSION = {
85
- ".py": "python",
86
- ".ts": "typescript",
87
- ".tsx": "typescript",
88
- ".js": "javascript",
89
- ".jsx": "javascript",
90
- ".go": "go",
91
- ".rs": "rust",
92
- ".java": "java",
93
- ".c": "c",
94
- ".cpp": "cpp",
95
- ".md": "markdown",
96
- ".yaml": "yaml",
97
- ".yml": "yaml",
98
- ".toml": "toml",
99
- ".json": "json",
100
- ".sh": "shell",
101
- ".ps1": "powershell",
102
- }
103
-
104
- _AREA_SUMMARIES = {
105
- "src/devcouncil/cli": "CLI entrypoints and command registration",
106
- "src/devcouncil/app": "Orchestration runtime and lifecycle state",
107
- "src/devcouncil/artifacts": "Artifact graph, coverage, and serialization",
108
- "src/devcouncil/council": "Council prompts and debate scaffolding",
109
- "src/devcouncil/domain": "Domain entities for requirements, tasks, and evidence",
110
- "src/devcouncil/execution": "Execution plumbing, prompts, permissions, and task runs",
111
- "src/devcouncil/executors": "Executor adapters and CLI agent registry",
112
- "src/devcouncil/gating": "Blocking policies and guardrails",
113
- "src/devcouncil/indexing": "Repo mapping, AST matching, semantic snapshots, and language-server detection",
114
- "src/devcouncil/integrations": "External integrations and graph adapters",
115
- "src/devcouncil/live": "Live review cards, signals, summaries, and transcripts",
116
- "src/devcouncil/llm": "Model provider routing, defaults, and caching",
117
- "src/devcouncil/planning": "Planning, critique, repair, and spec services",
118
- "src/devcouncil/repo": "Repository helpers and filesystem utilities",
119
- "src/devcouncil/reporting": "JSON and markdown report builders",
120
- "src/devcouncil/storage": "SQLite persistence and repository layer",
121
- "src/devcouncil/telemetry": "Trace logging, pricing, and telemetry tracking",
122
- "src/devcouncil/ui": "Dashboard and lightweight UI helpers",
123
- "src/devcouncil/utils": "Shared utilities and redaction helpers",
124
- "src/devcouncil/verification": "Verification gates and implementation review",
125
- "docs": "Repository documentation",
126
- "tests": "Automated tests",
127
- "scripts": "Maintenance and smoke-test scripts",
128
- }
129
-
130
- _SUBSYSTEM_INDEX: Dict[str, Tuple[str, List[str]]] = {
131
- "src/devcouncil/council": (
132
- "Prompt-driven council workflows and debate templates.",
133
- [
134
- "src/devcouncil/council/prompts/spec_writer.md",
135
- "src/devcouncil/council/prompts/planner_a.md",
136
- "src/devcouncil/council/prompts/implementation_reviewer.md",
137
- ],
138
- ),
139
- "src/devcouncil/domain": (
140
- "Shared domain entities for tasks, requirements, evidence, and critique.",
141
- [
142
- "src/devcouncil/domain/task.py",
143
- "src/devcouncil/domain/requirement.py",
144
- "src/devcouncil/domain/gap.py",
145
- "src/devcouncil/domain/evidence.py",
146
- ],
147
- ),
148
- "src/devcouncil/execution": (
149
- "Execution planning and task orchestration runtime, including prompt and permission handling.",
150
- [
151
- "src/devcouncil/execution/task_runner.py",
152
- "src/devcouncil/execution/prompt_builder.py",
153
- "src/devcouncil/execution/permissions.py",
154
- "src/devcouncil/execution/paths.py",
155
- ],
156
- ),
157
- "src/devcouncil/executors": (
158
- "Adapter layer that converts tasks into CLI/API side effects.",
159
- [
160
- "src/devcouncil/executors/agent_registry.py",
161
- "src/devcouncil/executors/coding_cli.py",
162
- "src/devcouncil/executors/mini_swe.py",
163
- "src/devcouncil/executors/openhands.py",
164
- ],
165
- ),
166
- "src/devcouncil/indexing": (
167
- "Repo mapping, AST matching, semantic snapshots, and language-server detection (no live LSP client).",
168
- [
169
- "src/devcouncil/indexing/repo_mapper.py",
170
- "src/devcouncil/indexing/ast_matcher.py",
171
- "src/devcouncil/indexing/semantic_index.py",
172
- "src/devcouncil/indexing/lsp.py",
173
- ],
174
- ),
175
- "src/devcouncil/integrations": (
176
- "External system integrations and MCP/Graph adapters.",
177
- [
178
- "src/devcouncil/integrations/gitnexus.py",
179
- "src/devcouncil/integrations/graphify.py",
180
- "src/devcouncil/integrations/mcp/server.py",
181
- "src/devcouncil/integrations/github.py",
182
- ],
183
- ),
184
- "src/devcouncil/verification": (
185
- "Verification gates, evidence checks, and implementation review.",
186
- [
187
- "src/devcouncil/verification/verifier.py",
188
- "src/devcouncil/verification/patch_reviewer.py",
189
- "src/devcouncil/verification/review_agent.py",
190
- ],
191
- ),
192
- "src/devcouncil/live": (
193
- "Live review cards, signals, summaries, and repair guidance.",
194
- [
195
- "src/devcouncil/live/reviewer.py",
196
- "src/devcouncil/live/cards.py",
197
- "src/devcouncil/live/signals.py",
198
- ],
199
- ),
200
- "src/devcouncil/llm": (
201
- "Model routing, provider registry, and LLM response caching.",
202
- [
203
- "src/devcouncil/llm/router.py",
204
- "src/devcouncil/llm/provider.py",
205
- "src/devcouncil/llm/cache.py",
206
- ],
207
- ),
208
- "src/devcouncil/planning": (
209
- "Task planning, spec generation, critique, and arbitration services.",
210
- [
211
- "src/devcouncil/planning/plan_service.py",
212
- "src/devcouncil/planning/spec_service.py",
213
- "src/devcouncil/planning/critique_service.py",
214
- "src/devcouncil/planning/repair_service.py",
215
- ],
216
- ),
217
- "src/devcouncil/repo": (
218
- "Repository helper helpers for workspace interactions.",
219
- [
220
- "src/devcouncil/repo/__init__.py",
221
- ],
222
- ),
223
- "src/devcouncil/reporting": (
224
- "Report generation and check-writing utilities.",
225
- [
226
- "src/devcouncil/reporting/report_builder.py",
227
- "src/devcouncil/reporting/json_report.py",
228
- "src/devcouncil/reporting/markdown_report.py",
229
- ],
230
- ),
231
- "src/devcouncil/gating": (
232
- "Policy gates and blocking criteria.",
233
- [
234
- "src/devcouncil/gating/gate.py",
235
- "src/devcouncil/gating/policy.py",
236
- "src/devcouncil/gating/rules.py",
237
- ],
238
- ),
239
- "src/devcouncil/storage": (
240
- "Persistence layer for run state, artifacts, and graph-backed history.",
241
- [
242
- "src/devcouncil/storage/repositories.py",
243
- "src/devcouncil/storage/db.py",
244
- "src/devcouncil/storage/models.py",
245
- ],
246
- ),
247
- "src/devcouncil/cli": (
248
- "User command surface and command wiring.",
249
- [
250
- "src/devcouncil/cli/main.py",
251
- "src/devcouncil/cli/commands/map.py",
252
- "src/devcouncil/cli/commands/plan.py",
253
- "src/devcouncil/cli/commands/run.py",
254
- ],
255
- ),
256
- "src/devcouncil/app": (
257
- "Orchestrator and state machine controlling project lifecycle.",
258
- [
259
- "src/devcouncil/app/orchestrator.py",
260
- "src/devcouncil/app/state_machine.py",
261
- "src/devcouncil/app/run_context.py",
262
- ],
263
- ),
264
- "src/devcouncil/artifacts": (
265
- "Artifact graph primitives and evidence linking.",
266
- [
267
- "src/devcouncil/artifacts/graph.py",
268
- "src/devcouncil/artifacts/exports.py",
269
- "src/devcouncil/artifacts/types.py",
270
- ],
271
- ),
272
- "src/devcouncil/telemetry": (
273
- "Telemetry ingestion, tracing, cost, and pricing.",
274
- [
275
- "src/devcouncil/telemetry/traces.py",
276
- "src/devcouncil/telemetry/tracker.py",
277
- "src/devcouncil/telemetry/cost.py",
278
- ],
279
- ),
280
- "src/devcouncil/ui": (
281
- "Dashboard rendering and lightweight user interface glue.",
282
- [
283
- "src/devcouncil/ui/dashboard.py",
284
- ],
285
- ),
286
- "src/devcouncil/utils": (
287
- "Shared utility helpers, redaction, and support functions.",
288
- [
289
- "src/devcouncil/utils/redaction.py",
290
- ],
291
- ),
292
- }
293
-
294
- _SUBSYSTEM_CRITICAL_MAX = 6
295
-
296
- _SUBSYSTEM_NEIGHBORS: Dict[str, List[str]] = {
297
- "src/devcouncil/council": [
298
- "src/devcouncil/planning",
299
- "src/devcouncil/verification",
300
- ],
301
- "src/devcouncil/domain": [
302
- "src/devcouncil/execution",
303
- "src/devcouncil/executors",
304
- "src/devcouncil/storage",
305
- "src/devcouncil/verification",
306
- "src/devcouncil/planning",
307
- "src/devcouncil/gating",
308
- ],
309
- "src/devcouncil/execution": [
310
- "src/devcouncil/executors",
311
- "src/devcouncil/gating",
312
- "src/devcouncil/verification",
313
- "src/devcouncil/storage",
314
- ],
315
- "src/devcouncil/executors": [
316
- "src/devcouncil/execution",
317
- "src/devcouncil/app",
318
- "src/devcouncil/storage",
319
- ],
320
- "src/devcouncil/verification": [
321
- "src/devcouncil/storage",
322
- "src/devcouncil/gating",
323
- "src/devcouncil/app",
324
- ],
325
- "src/devcouncil/gating": [
326
- "src/devcouncil/execution",
327
- "src/devcouncil/verification",
328
- "src/devcouncil/storage",
329
- ],
330
- "src/devcouncil/storage": [
331
- "src/devcouncil/app",
332
- "src/devcouncil/artifacts",
333
- "src/devcouncil/verification",
334
- ],
335
- "src/devcouncil/cli": [
336
- "src/devcouncil/app",
337
- "src/devcouncil/storage",
338
- "src/devcouncil/indexing",
339
- ],
340
- "src/devcouncil/app": [
341
- "src/devcouncil/cli",
342
- "src/devcouncil/execution",
343
- "src/devcouncil/storage",
344
- "src/devcouncil/verification",
345
- ],
346
- "src/devcouncil/artifacts": [
347
- "src/devcouncil/storage",
348
- "src/devcouncil/verification",
349
- ],
350
- "src/devcouncil/indexing": [
351
- "src/devcouncil/llm",
352
- "src/devcouncil/execution",
353
- "src/devcouncil/cli",
354
- ],
355
- "src/devcouncil/integrations": [
356
- "src/devcouncil/cli",
357
- "src/devcouncil/live",
358
- "src/devcouncil/reporting",
359
- "src/devcouncil/telemetry",
360
- ],
361
- "src/devcouncil/live": [
362
- "src/devcouncil/verification",
363
- "src/devcouncil/telemetry",
364
- "src/devcouncil/reporting",
365
- "src/devcouncil/cli",
366
- ],
367
- "src/devcouncil/llm": [
368
- "src/devcouncil/planning",
369
- "src/devcouncil/execution",
370
- "src/devcouncil/verification",
371
- "src/devcouncil/app",
372
- ],
373
- "src/devcouncil/planning": [
374
- "src/devcouncil/domain",
375
- "src/devcouncil/llm",
376
- "src/devcouncil/cli",
377
- "src/devcouncil/execution",
378
- ],
379
- "src/devcouncil/repo": [
380
- "src/devcouncil/cli",
381
- ],
382
- "src/devcouncil/reporting": [
383
- "src/devcouncil/telemetry",
384
- "src/devcouncil/integrations",
385
- "src/devcouncil/cli",
386
- "src/devcouncil/live",
387
- ],
388
- "src/devcouncil/telemetry": [
389
- "src/devcouncil/cli",
390
- "src/devcouncil/app",
391
- "src/devcouncil/execution",
392
- "src/devcouncil/verification",
393
- "src/devcouncil/llm",
394
- ],
395
- "src/devcouncil/ui": [
396
- "src/devcouncil/telemetry",
397
- ],
398
- "src/devcouncil/utils": [
399
- "src/devcouncil/execution",
400
- "src/devcouncil/cli",
401
- "src/devcouncil/verification",
402
- "src/devcouncil/executors",
403
- "src/devcouncil/llm",
404
- ],
405
- }
406
-
407
- _SUBSYSTEM_HANDOFFS: Dict[str, List[str]] = {
408
- "src/devcouncil/council": [
409
- "planning/arbiter_service.py -> planning/plan_service.py",
410
- "planning/spec_service.py -> planning/plan_service.py",
411
- ],
412
- "src/devcouncil/domain": [
413
- "domain/task.py -> execution/task_runner.py",
414
- "domain/evidence.py -> artifacts/graph.py",
415
- "domain/requirement.py -> verification/verifier.py",
416
- ],
417
- "src/devcouncil/execution": [
418
- "execution/task_runner.py -> executors/*",
419
- "execution/task_runner.py -> verification/verifier.py",
420
- "execution/task_runner.py -> storage/repositories.py",
421
- ],
422
- "src/devcouncil/executors": [
423
- "executors/* -> execution/task_runner.py",
424
- "executors/* -> storage/repositories.py",
425
- ],
426
- "src/devcouncil/verification": [
427
- "verification/verifier.py -> storage/repositories.py",
428
- "verification/verifier.py -> gating/policy.py",
429
- "verification/verifier.py -> artifacts/graph.py",
430
- ],
431
- "src/devcouncil/gating": [
432
- "gating/policy.py -> execution/permissions.py",
433
- "gating/policy.py -> verification/verifier.py",
434
- ],
435
- "src/devcouncil/storage": [
436
- "storage/repositories.py -> app/state_machine.py",
437
- "storage/repositories.py -> artifacts/graph.py",
438
- ],
439
- "src/devcouncil/indexing": [
440
- "indexing/repo_mapper.py -> cli/commands/map.py",
441
- "indexing/lsp.py -> execution/task_runner.py",
442
- ],
443
- "src/devcouncil/integrations": [
444
- "integrations/mcp/server.py -> live/reviewer.py",
445
- "integrations/code_review_graph.py -> live/cards.py",
446
- "integrations/gitnexus.py -> reporting/report_builder.py",
447
- ],
448
- "src/devcouncil/live": [
449
- "live/summary.py -> live/cards.py",
450
- "live/reviewer.py -> live/models.py",
451
- "live/tasks.py -> live/signals.py",
452
- ],
453
- "src/devcouncil/llm": [
454
- "llm/router.py -> telemetry/tracker.py",
455
- "llm/router.py -> telemetry/traces.py",
456
- "llm/provider.py -> llm/router.py",
457
- ],
458
- "src/devcouncil/planning": [
459
- "planning/plan_service.py -> execution/task_runner.py",
460
- "planning/repair_service.py -> verification/implementation_reviewer.py",
461
- "planning/arbiter_service.py -> verification/verifier.py",
462
- ],
463
- "src/devcouncil/repo": [
464
- "repo/__init__.py -> cli/commands/map.py",
465
- ],
466
- "src/devcouncil/reporting": [
467
- "reporting/report_builder.py -> reporting/markdown_report.py",
468
- "reporting/report_builder.py -> reporting/json_report.py",
469
- "reporting/github_check.py -> integrations/pr_comments.py",
470
- ],
471
- "src/devcouncil/telemetry": [
472
- "telemetry/traces.py -> live/summary.py",
473
- "telemetry/tracker.py -> reporting/markdown_report.py",
474
- ],
475
- "src/devcouncil/ui": [
476
- "ui/dashboard.py -> live/summary.py",
477
- ],
478
- }
479
-
480
- _SUBSYSTEM_ROLE_FILES: Dict[str, List[Tuple[str, List[str]]]] = {
481
- "src/devcouncil/council": [
482
- ("prompts", ["council/prompts/spec_writer.md", "council/prompts/rebuttal.md", "council/prompts/implementation_reviewer.md"]),
483
- ("planners", ["council/prompts/planner_a.md", "council/prompts/planner_b.md"]),
484
- ("critics", ["council/prompts/critic_a.md", "council/prompts/critic_b.md"]),
485
- ("arbitration", ["council/prompts/arbiter.md"]),
486
- ],
487
- "src/devcouncil/domain": [
488
- ("tasks", ["domain/task.py"]),
489
- ("requirements", ["domain/requirement.py"]),
490
- ("evidence", ["domain/evidence.py"]),
491
- ("gaps", ["domain/gap.py"]),
492
- ("critiques", ["domain/critique.py"]),
493
- ("assumptions", ["domain/assumption.py"]),
494
- ],
495
- "src/devcouncil/indexing": [
496
- ("mapping", ["indexing/repo_mapper.py"]),
497
- ("ast", ["indexing/ast_matcher.py"]),
498
- ("semantic", ["indexing/semantic_index.py"]),
499
- # Detection-only LSP helper (no live client); see lsp.py docstring.
500
- ("lsp", ["indexing/lsp.py"]),
501
- # GraphIndex is consumed by integrations/gitnexus.py — kept, not dead.
502
- ("graph", ["indexing/graph_index.py"]),
503
- ],
504
- "src/devcouncil/integrations": [
505
- ("vcs", ["integrations/github.py"]),
506
- ("graphify", ["integrations/graphify.py"]),
507
- ("code_review", ["integrations/code_review_graph.py"]),
508
- ("comments", ["integrations/pr_comments.py"]),
509
- ("mcp", ["integrations/mcp/server.py"]),
510
- ("third_party", ["integrations/gitnexus.py"]),
511
- ],
512
- "src/devcouncil/cli": [
513
- ("entrypoints", ["cli/main.py"]),
514
- ("commands", ["cli/commands/map.py", "cli/commands/plan.py", "cli/commands/run.py", "cli/commands/verify.py"]),
515
- ("setup", ["cli/commands/init.py", "cli/commands/setup.py", "cli/commands/integrate.py"]),
516
- ("lifecycle", ["cli/commands/status.py", "cli/commands/show.py", "cli/commands/watch.py"]),
517
- ("maintenance", ["cli/commands/doctor.py", "cli/commands/version.py", "cli/commands/config.py"]),
518
- ],
519
- "src/devcouncil/app": [
520
- ("orchestration", ["app/orchestrator.py"]),
521
- ("state", ["app/state_machine.py", "app/run_context.py"]),
522
- ("events", ["app/events.py"]),
523
- ("configuration", ["app/config.py", "app/errors.py", "app/project_status.py"]),
524
- ],
525
- "src/devcouncil/artifacts": [
526
- ("graph", ["artifacts/graph.py"]),
527
- ("coverage", ["artifacts/coverage.py", "artifacts/serializer.py"]),
528
- ("schema", ["artifacts/schemas.py", "artifacts/migrations.py"]),
529
- ("validation", ["artifacts/validators.py"]),
530
- ],
531
- "src/devcouncil/executors": [
532
- ("registry", ["executors/agent_registry.py"]),
533
- ("adapters", ["executors/coding_cli.py", "executors/openhands.py", "executors/mini_swe.py"]),
534
- ("native", ["executors/native/agent.py"]),
535
- ],
536
- "src/devcouncil/execution": [
537
- ("runtime", ["execution/task_runner.py", "execution/context_builder.py"]),
538
- ("prompting", ["execution/prompt_builder.py"]),
539
- ("permissions", ["execution/permissions.py"]),
540
- ("patching", ["execution/patch.py", "execution/executor.py"]),
541
- ("paths", ["execution/paths.py"]),
542
- ],
543
- "src/devcouncil/gating": [
544
- ("policy", ["gating/policy.py"]),
545
- ("checks", ["gating/checks/clean_git.py", "gating/checks/planned_files_check.py"]),
546
- ("coverage", ["gating/checks/requirement_coverage.py", "gating/checks/secret_scan_check.py"]),
547
- ],
548
- "src/devcouncil/live": [
549
- ("cards", ["live/cards.py"]),
550
- ("review", ["live/reviewer.py"]),
551
- ("signals", ["live/signals.py"]),
552
- ("sessions", ["live/tasks.py", "live/transcripts.py"]),
553
- ("summaries", ["live/summary.py"]),
554
- ("models", ["live/models.py"]),
555
- ],
556
- "src/devcouncil/llm": [
557
- ("routing", ["llm/router.py"]),
558
- ("providers", ["llm/provider.py"]),
559
- ("cache", ["llm/cache.py"]),
560
- ("defaults", ["llm/model_defaults.yaml"]),
561
- ],
562
- "src/devcouncil/planning": [
563
- ("plan", ["planning/plan_service.py", "planning/prompt_enhancer_service.py"]),
564
- ("spec", ["planning/spec_service.py"]),
565
- ("critique", ["planning/critique_service.py"]),
566
- ("repair", ["planning/repair_service.py"]),
567
- ("arbiter", ["planning/arbiter_service.py"]),
568
- ],
569
- "src/devcouncil/repo": [
570
- ("api", ["repo/__init__.py"]),
571
- ],
572
- "src/devcouncil/reporting": [
573
- ("builder", ["reporting/report_builder.py"]),
574
- ("markdown", ["reporting/markdown_report.py"]),
575
- ("json", ["reporting/json_report.py"]),
576
- ("checks", ["reporting/github_check.py"]),
577
- ],
578
- "src/devcouncil/verification": [
579
- ("gates", ["verification/verifier.py", "verification/implementation_reviewer.py"]),
580
- ("implementation_reviewer", ["verification/implementation_reviewer.py"]),
581
- ("policy", ["verification/verifier.py"]),
582
- ],
583
- "src/devcouncil/telemetry": [
584
- ("traces", ["telemetry/traces.py"]),
585
- ("tracker", ["telemetry/tracker.py"]),
586
- ("cost", ["telemetry/cost.py"]),
587
- ("pricing", ["telemetry/pricing.py", "telemetry/model_pricing.yaml"]),
588
- ],
589
- "src/devcouncil/storage": [
590
- ("repositories", ["storage/repositories.py"]),
591
- ("schema", ["storage/models.py"]),
592
- ("database", ["storage/db.py"]),
593
- ],
594
- "src/devcouncil/ui": [
595
- ("dashboard", ["ui/dashboard.py"]),
596
- ],
597
- "src/devcouncil/utils": [
598
- ("redaction", ["utils/redaction.py"]),
599
- ],
600
- }
601
-
602
- _COMMAND_SUMMARIES = {
603
- "agents": "CLI agent registry and integration commands",
604
- "artifacts": "Artifact graph inspection commands",
605
- "ast": "AST matching and symbol discovery commands",
606
- "baseline": "Capture or inspect a baseline snapshot",
607
- "config": "Inspect or mutate project configuration",
608
- "dashboard": "Dashboard launch command",
609
- "doctor": "Preflight and environment diagnostics",
610
- "go": "End-to-end task execution alias",
611
- "hook": "Hook configuration commands",
612
- "init": "Project initialization and integration bootstrap",
613
- "integrate": "Coding CLI and MCP integration setup",
614
- "lsp": "LSP inspection commands",
615
- "map": "Repository mapping command",
616
- "mcp_server": "MCP server command",
617
- "plan": "Planning workflow command",
618
- "prompt": "Prompt generation for agent handoff",
619
- "repair": "Repair prompt generation",
620
- "report": "Task and project reporting commands",
621
- "reset_demo_state": "Reset demo state and sample data",
622
- "rollback": "Rollback workflow command",
623
- "run": "Execute an approved task",
624
- "setup": "Interactive project setup command",
625
- "show": "Show current project state",
626
- "status": "Compact workflow status command",
627
- "tasks": "Task graph and task listing commands",
628
- "trace": "Trace inspection commands",
629
- "verify": "Verification workflow command",
630
- "version": "Version display command",
631
- "watch": "Live review and transcript monitoring",
632
- }
633
-
634
- _DOC_SUMMARIES = {
635
- "AGENTS.md": "Workspace guide for coding agents",
636
- "CLAUDE.md": "Workspace guide for Claude-based agents",
637
- "README.md": "Project overview and usage entrypoint",
638
- "architecture.md": "Top-level architecture overview",
639
- "cli-reference.md": "CLI command reference",
640
- "quickstart.md": "First-run installation and workflow",
641
- "workflow.md": "Manual sidecar workflow guide",
642
- "security.md": "Security and privacy model",
643
- "project-status.md": "Subsystem maturity snapshot",
644
- "roadmap.md": "Planned work and roadmap",
645
- }
646
-
647
- def _language_for_file(self, path: str) -> str | None:
648
- suffix = Path(path).suffix.lower()
649
- return self._LANGUAGE_BY_EXTENSION.get(suffix)
650
-
651
- def _kind_for_file(self, path: str) -> str:
652
- normalized = path.replace("\\", "/")
653
- suffix = Path(normalized).suffix.lower()
654
- name = Path(normalized).name
655
- if normalized.startswith("tests/") or name.startswith("test_"):
656
- return "test"
657
- if normalized.startswith("docs/") or suffix == ".md":
658
- return "doc"
659
- if suffix in {".yaml", ".yml", ".toml", ".json", ".ini"}:
660
- return "config"
661
- if suffix in {".sh", ".ps1", ".bat"}:
662
- return "script"
663
- if suffix in {".sqlite", ".db"}:
664
- return "database"
665
- if suffix in {".py", ".ts", ".tsx", ".js", ".jsx", ".go", ".rs", ".java", ".c", ".cpp"}:
666
- return "module" if name != "__init__.py" else "package"
667
- return "file"
668
-
669
- def _summary_for_file(self, path: str) -> str:
670
- normalized = path.replace("\\", "/")
671
- name = Path(normalized).name
672
- parts = normalized.split("/")
673
- if normalized == "README.md":
674
- return self._DOC_SUMMARIES["README.md"]
675
- if normalized.startswith("docs/"):
676
- stem = Path(name).stem.replace("-", " ")
677
- return self._DOC_SUMMARIES.get(name, f"Documentation: {stem}")
678
- if normalized.startswith("tests/"):
679
- remainder = normalized.removeprefix("tests/")
680
- if remainder.startswith("unit/"):
681
- return f"Unit tests for {Path(remainder).stem.replace('test_', '').replace('_', ' ').strip() or 'the package'}"
682
- return f"Tests for {Path(remainder).stem.replace('test_', '').replace('_', ' ').strip() or 'the package'}"
683
- if normalized.startswith("src/devcouncil/cli/commands/"):
684
- stem = Path(name).stem
685
- return self._COMMAND_SUMMARIES.get(stem, f"CLI command module: {stem}")
686
- if normalized == "src/devcouncil/cli/main.py":
687
- return "Typer root command composition"
688
- if normalized == "src/devcouncil/app/orchestrator.py":
689
- return "Orchestration coordinator and run lifecycle"
690
- if normalized == "src/devcouncil/app/state_machine.py":
691
- return "Allowed project phase transitions"
692
- if normalized == "src/devcouncil/artifacts/graph.py":
693
- return "Artifact graph and coverage queries"
694
- if normalized == "src/devcouncil/indexing/repo_mapper.py":
695
- return "Repository mapping and file classification"
696
- if normalized == "src/devcouncil/storage/repositories.py":
697
- return "Persistence repositories for state and artifacts"
698
- if normalized == "src/devcouncil/storage/models.py":
699
- return "SQLModel database schema"
700
- if normalized == "src/devcouncil/verification/verifier.py":
701
- return "Verification gates and evidence checks"
702
- if normalized == "src/devcouncil/planning/plan_service.py":
703
- return "Plan generation service"
704
- if normalized == "src/devcouncil/planning/spec_service.py":
705
- return "Spec generation service"
706
- if normalized == "src/devcouncil/planning/critique_service.py":
707
- return "Plan critique service"
708
- if normalized == "src/devcouncil/planning/repair_service.py":
709
- return "Repair workflow service"
710
- if normalized == "src/devcouncil/planning/arbiter_service.py":
711
- return "Plan arbitration service"
712
- if normalized == "src/devcouncil/execution/task_runner.py":
713
- return "Task execution runner"
714
- if normalized == "src/devcouncil/execution/prompt_builder.py":
715
- return "Prompt assembly for executors"
716
- if normalized == "src/devcouncil/execution/permissions.py":
717
- return "Execution permission policy"
718
- if normalized == "src/devcouncil/executors/agent_registry.py":
719
- return "Built-in and configured CLI agent registry"
720
- if normalized == "src/devcouncil/llm/router.py":
721
- return "LLM provider routing"
722
- if normalized == "src/devcouncil/telemetry/traces.py":
723
- return "Trace logging and event persistence"
724
- if normalized == "src/devcouncil/live/reviewer.py":
725
- return "Live review service"
726
- if normalized == "src/devcouncil/integrations/gitnexus.py":
727
- return "GitNexus integration shim"
728
- if normalized == "src/devcouncil/integrations/graphify.py":
729
- return "Graphify integration shim"
730
- if normalized.startswith("src/devcouncil/"):
731
- area = "/".join(parts[:3]) if len(parts) >= 3 else "src/devcouncil"
732
- return self._AREA_SUMMARIES.get(area, f"{area} subsystem")
733
- if normalized.startswith("scripts/"):
734
- return f"Utility script: {name}"
735
- if name in self._DOC_SUMMARIES:
736
- return self._DOC_SUMMARIES[name]
737
- return Path(name).stem.replace("_", " ")
738
-
739
- def _area_for_file(self, path: str) -> str:
740
- normalized = path.replace("\\", "/")
741
- if normalized.startswith("src/devcouncil/"):
742
- parts = normalized.split("/")
743
- if len(parts) >= 5 and parts[2] == "cli" and parts[3] == "commands":
744
- return "src/devcouncil/cli/commands"
745
- if len(parts) >= 4:
746
- return "/".join(parts[:3])
747
- return "src/devcouncil"
748
- if normalized.startswith("tests/"):
749
- return "tests"
750
- if normalized.startswith("docs/"):
751
- return "docs"
752
- if normalized.startswith("scripts/"):
753
- return "scripts"
754
- # Foreign repos: derive the area from the directory tree. Gated on _use_generic
755
- # so DevCouncil's own map keeps its existing "root" bucketing.
756
- if self._use_generic:
757
- return self._generic_area_for_file(normalized, self._source_root or "")
758
- return "root"
759
-
760
- def _build_subsystem_index(self, files: List[str]) -> List[RepoSubsystem]:
761
- # The hardcoded index is authoritative for DevCouncil's own tree (preserves
762
- # its curated summaries/role buckets). For any other repo it matches nothing,
763
- # so fall back to generic, import-graph-driven inference.
764
- hardcoded = self._build_hardcoded_subsystems(files)
765
- if hardcoded:
766
- return hardcoded
767
- return self._build_generic_subsystems(files)
768
-
769
- def _build_hardcoded_subsystems(self, files: List[str]) -> List[RepoSubsystem]:
770
- file_set = set(files)
771
- subsystems: List[RepoSubsystem] = []
772
- for area, (summary, entry_points) in self._SUBSYSTEM_INDEX.items():
773
- available_entry_points = [path for path in entry_points if path in file_set]
774
- if not available_entry_points:
775
- continue
776
- area_files = sorted(path for path in files if path.startswith(f"{area}/"))
777
- ranked_files = [path for path in available_entry_points if path in file_set]
778
- for path in area_files:
779
- if path in available_entry_points:
780
- continue
781
- if len(ranked_files) >= self._SUBSYSTEM_CRITICAL_MAX:
782
- break
783
- ranked_files.append(path)
784
- critical_files = ranked_files[: self._SUBSYSTEM_CRITICAL_MAX]
785
- neighbors = [n for n in self._SUBSYSTEM_NEIGHBORS.get(area, []) if any(f.startswith(f"{n}/") for f in files)]
786
- handoff_paths = self._SUBSYSTEM_HANDOFFS.get(area, [])
787
- role_files = self._build_role_files(area, area_files)
788
- subsystems.append(
789
- RepoSubsystem(
790
- area=area,
791
- summary=summary,
792
- entry_points=available_entry_points,
793
- critical_files=critical_files,
794
- neighbors=neighbors,
795
- handoff_paths=handoff_paths,
796
- role_files=role_files,
797
- )
798
- )
799
- return subsystems
800
-
801
- def _build_role_files(self, area: str, area_files: List[str]) -> Dict[str, List[str]]:
802
- role_specs = self._SUBSYSTEM_ROLE_FILES.get(area)
803
- if not role_specs:
804
- return {}
805
-
806
- by_role: Dict[str, List[str]] = {}
807
- used = set()
808
- for role, tokens in role_specs:
809
- matches = [path for path in area_files if any(token in path for token in tokens)]
810
- if not matches:
811
- continue
812
- selected = matches[:4]
813
- by_role[role] = selected
814
- used.update(selected)
815
-
816
- if not by_role:
817
- return {}
818
-
819
- leftovers = [path for path in area_files if path not in used][:4]
820
- if leftovers:
821
- by_role.setdefault("other", leftovers)
822
-
823
- return by_role
824
-
825
- # ------------------------------------------------------------------
826
- # Generic (non-DevCouncil) subsystem inference
827
- # ------------------------------------------------------------------
828
-
829
- def _code_files(self, files: List[str]) -> List[str]:
830
- return [f for f in files if Path(f).suffix.lower() in _CODE_EXTENSIONS]
831
-
832
- def _primary_code_files(self, files: List[str]) -> List[str]:
833
- """Code files excluding tests/docs/scripts — the ones that define the repo's
834
- real structure and so determine the source root."""
835
- primary: List[str] = []
836
- for f in self._code_files(files):
837
- top = f.replace("\\", "/").split("/")[0]
838
- name = Path(f).name
839
- if top in _AUX_AREA_ROOTS or name.startswith("test_") or name.endswith("_test.go"):
840
- continue
841
- primary.append(f)
842
- return primary
843
-
844
- def detect_source_root(self, files: List[str]) -> str:
845
- """Longest common directory prefix shared by the primary source files
846
- (e.g. ``src/mypkg``). Empty when the code spans unrelated top-level dirs."""
847
- dirs = [Path(f).parent.as_posix() for f in self._primary_code_files(files)]
848
- dirs = [d for d in dirs if d not in ("", ".")]
849
- if not dirs:
850
- return ""
851
- split = [d.split("/") for d in dirs]
852
- common = split[0]
853
- for parts in split[1:]:
854
- limit = min(len(common), len(parts))
855
- i = 0
856
- while i < limit and common[i] == parts[i]:
857
- i += 1
858
- common = common[:i]
859
- if not common:
860
- break
861
- return "/".join(common)
862
-
863
- def _generic_area_for_file(self, path: str, source_root: str) -> str:
864
- normalized = path.replace("\\", "/")
865
- parts = normalized.split("/")
866
- if parts[0] in _AUX_AREA_ROOTS:
867
- return parts[0]
868
- if source_root and (normalized == source_root or normalized.startswith(f"{source_root}/")):
869
- rest = normalized[len(source_root):].lstrip("/").split("/")
870
- if len(rest) >= 2:
871
- return f"{source_root}/{rest[0]}"
872
- return source_root or "root"
873
- if len(parts) >= 2:
874
- return parts[0]
875
- return "root"
876
-
877
- def _module_suffix_index(self, py_files: List[str]) -> Dict[str, str]:
878
- """Map every dotted suffix of each module's path to its file, so an import
879
- statement's module string resolves to a repo file. Ambiguous suffixes (shared
880
- by two files) are dropped to avoid mislinking. Packages (``__init__.py``) are
881
- also indexed under their package dotted path."""
882
- index: Dict[str, str] = {}
883
- ambiguous: Set[str] = set()
884
-
885
- def _register(dotted: str, file: str) -> None:
886
- comps = [c for c in dotted.split(".") if c]
887
- for i in range(len(comps)):
888
- suffix = ".".join(comps[i:])
889
- if not suffix:
890
- continue
891
- if suffix in index and index[suffix] != file:
892
- ambiguous.add(suffix)
893
- else:
894
- index[suffix] = file
895
-
896
- for f in py_files:
897
- module_path = f[:-3] if f.endswith(".py") else f
898
- if module_path.endswith("/__init__"):
899
- # Package import resolves to the __init__ file under the dir's name.
900
- _register(module_path[: -len("/__init__")].replace("/", "."), f)
901
- else:
902
- _register(module_path.replace("/", "."), f)
903
- for suffix in ambiguous:
904
- index.pop(suffix, None)
905
- return index
906
-
907
- def _resolve_module(self, module: str, index: Dict[str, str]) -> str | None:
908
- comps = [c for c in module.split(".") if c]
909
- # An absolute import of a stdlib module is never a repo file — don't let a
910
- # repo file whose stem happens to equal a stdlib name (e.g. a local json.py)
911
- # create a false edge for `import json`.
912
- if comps and comps[0] in sys.stdlib_module_names:
913
- return None
914
- while comps:
915
- candidate = ".".join(comps)
916
- if candidate in index:
917
- return index[candidate]
918
- comps = comps[:-1] # `from pkg.mod import name` -> try pkg.mod, then pkg
919
- return None
920
-
921
- def _python_import_edges(self, files: List[str]) -> List[Tuple[str, str]]:
922
- """Resolve Python import statements into (importer, imported) file edges."""
923
- py_files = [f for f in self._code_files(files) if f.endswith(".py")]
924
- if not py_files:
925
- return []
926
- index = self._module_suffix_index(py_files)
927
- edges: List[Tuple[str, str]] = []
928
- seen: Set[Tuple[str, str]] = set() # dedupe so in-degree isn't inflated by repeats
929
- for rel in py_files:
930
- try:
931
- source = (self.project_root / rel).read_text(encoding="utf-8", errors="replace")
932
- tree = ast.parse(source)
933
- except (OSError, SyntaxError, ValueError):
934
- continue
935
- pkg_parts = rel[:-3].replace("/", ".").split(".") # importer's dotted path
936
- for node in ast.walk(tree):
937
- modules: List[str] = []
938
- if isinstance(node, ast.Import):
939
- modules = [alias.name for alias in node.names]
940
- elif isinstance(node, ast.ImportFrom):
941
- if node.level:
942
- base = pkg_parts[: -node.level] if node.level <= len(pkg_parts) else []
943
- base_mod = ".".join(base + ([node.module] if node.module else []))
944
- else:
945
- base_mod = node.module or ""
946
- if base_mod:
947
- modules.append(base_mod)
948
- # `from pkg import sub` / `from . import sub` may import a SUBMODULE
949
- # file, not just a symbol — resolve each name as a candidate module so
950
- # those edges aren't silently dropped.
951
- for alias in node.names:
952
- if alias.name and alias.name != "*":
953
- modules.append(f"{base_mod}.{alias.name}" if base_mod else alias.name)
954
- for module in modules:
955
- target = self._resolve_module(module, index)
956
- if target and target != rel and (rel, target) not in seen:
957
- seen.add((rel, target))
958
- edges.append((rel, target))
959
- return edges
960
-
961
- # Module specifiers in import/require statements: import ... from "x"; require("x");
962
- # export ... from "x"; dynamic import("x"). Best-effort; only relative specs resolve.
963
- _JS_IMPORT_RE = re.compile(
964
- r"""(?:import|export)\s[^'"]*?from\s*['"](?P<spec>[^'"]+)['"]"""
965
- r"""|(?:require|import)\s*\(\s*['"](?P<spec2>[^'"]+)['"]\s*\)"""
966
- )
967
- _JS_BARE_IMPORT_RE = re.compile(r"""^\s*import\s*['"](?P<spec>[^'"]+)['"]""")
968
- _JS_RESOLVE_EXTS = (".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs")
969
- _GO_IMPORT_BLOCK_RE = re.compile(r"import\s*\((?P<body>[^)]*)\)", re.DOTALL)
970
- _GO_IMPORT_SINGLE_RE = re.compile(r"""^\s*import\s+(?:[A-Za-z_.]\w*\s+)?['"](?P<spec>[^'"]+)['"]""")
971
- _GO_IMPORT_SPEC_RE = re.compile(r"""['"](?P<spec>[^'"]+)['"]""")
972
- _GO_MODULE_RE = re.compile(r"^\s*module\s+(?P<mod>\S+)", re.MULTILINE)
973
-
974
- def _resolve_js_spec(self, importer: str, spec: str, file_set: Set[str]) -> str | None:
975
- """Resolve a relative TS/JS import specifier (``./x`` / ``../y``) to a repo file.
976
- Bare specifiers (node_modules packages) are intentionally not resolved."""
977
- if not spec.startswith("."):
978
- return None
979
- base = Path(importer).parent
980
- try:
981
- target = (base / spec).as_posix()
982
- except Exception:
983
- return None
984
- # Normalize away any ".." segments without touching the filesystem.
985
- parts: List[str] = []
986
- for comp in target.split("/"):
987
- if comp in ("", "."):
988
- continue
989
- if comp == "..":
990
- if parts:
991
- parts.pop()
992
- continue
993
- parts.append(comp)
994
- norm = "/".join(parts)
995
- if not norm:
996
- return None
997
- candidates = [norm]
998
- candidates += [f"{norm}{ext}" for ext in self._JS_RESOLVE_EXTS]
999
- candidates += [f"{norm}/index{ext}" for ext in self._JS_RESOLVE_EXTS]
1000
- for cand in candidates:
1001
- if cand in file_set:
1002
- return cand
1003
- return None
1004
-
1005
- def _js_import_edges(self, files: List[str], file_set: Set[str]) -> List[Tuple[str, str]]:
1006
- """Resolve TS/JS relative import/require/export-from edges to repo files."""
1007
- js_files = [f for f in files if Path(f).suffix.lower() in {".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"}]
1008
- edges: List[Tuple[str, str]] = []
1009
- seen: Set[Tuple[str, str]] = set()
1010
- for rel in js_files:
1011
- try:
1012
- source = (self.project_root / rel).read_text(encoding="utf-8", errors="replace")
1013
- except OSError:
1014
- continue
1015
- specs: List[str] = []
1016
- for m in self._JS_IMPORT_RE.finditer(source):
1017
- spec = m.group("spec") or m.group("spec2")
1018
- if spec:
1019
- specs.append(spec)
1020
- for line in source.splitlines():
1021
- bare = self._JS_BARE_IMPORT_RE.match(line)
1022
- if bare:
1023
- specs.append(bare.group("spec"))
1024
- for spec in specs:
1025
- target = self._resolve_js_spec(rel, spec, file_set)
1026
- if target and target != rel and (rel, target) not in seen:
1027
- seen.add((rel, target))
1028
- edges.append((rel, target))
1029
- return edges
1030
-
1031
- def _go_module_prefix(self, file_set: Set[str]) -> str | None:
1032
- """The module path declared in go.mod, used to map import paths back to repo dirs."""
1033
- for candidate in (p for p in file_set if p == "go.mod" or p.endswith("/go.mod")):
1034
- try:
1035
- text = (self.project_root / candidate).read_text(encoding="utf-8", errors="replace")
1036
- except OSError:
1037
- continue
1038
- m = self._GO_MODULE_RE.search(text)
1039
- if m:
1040
- return m.group("mod").strip()
1041
- return None
1042
-
1043
- def _go_import_edges(self, files: List[str], file_set: Set[str]) -> List[Tuple[str, str]]:
1044
- """Resolve Go import paths (under the module prefix) to package directories, then
1045
- to the .go files in those directories. Edges target every file in the package."""
1046
- module = self._go_module_prefix(file_set)
1047
- if not module:
1048
- return []
1049
- go_files = [f for f in files if f.endswith(".go")]
1050
- if not go_files:
1051
- return []
1052
- # package dir -> .go files in it (excluding _test.go, which aren't imported).
1053
- pkg_files: Dict[str, List[str]] = defaultdict(list)
1054
- for f in go_files:
1055
- if f.endswith("_test.go"):
1056
- continue
1057
- pkg_files[Path(f).parent.as_posix()].append(f)
1058
- edges: List[Tuple[str, str]] = []
1059
- seen: Set[Tuple[str, str]] = set()
1060
- for rel in go_files:
1061
- try:
1062
- source = (self.project_root / rel).read_text(encoding="utf-8", errors="replace")
1063
- except OSError:
1064
- continue
1065
- specs: List[str] = []
1066
- for block in self._GO_IMPORT_BLOCK_RE.finditer(source):
1067
- for sm in self._GO_IMPORT_SPEC_RE.finditer(block.group("body")):
1068
- specs.append(sm.group("spec"))
1069
- for line in source.splitlines():
1070
- single = self._GO_IMPORT_SINGLE_RE.match(line)
1071
- if single:
1072
- specs.append(single.group("spec"))
1073
- for spec in specs:
1074
- if spec != module and not spec.startswith(module + "/"):
1075
- continue # external/stdlib package
1076
- rel_pkg = spec[len(module):].lstrip("/")
1077
- target_dir = rel_pkg if rel_pkg else "."
1078
- for target in pkg_files.get(target_dir, []):
1079
- if target != rel and (rel, target) not in seen:
1080
- seen.add((rel, target))
1081
- edges.append((rel, target))
1082
- return edges
1083
-
1084
- def _all_import_edges(self, files: List[str]) -> List[Tuple[str, str]]:
1085
- """All cross-file import edges across supported languages, feeding the same
1086
- dependents reverse index. Best-effort; never raises."""
1087
- file_set = set(files)
1088
- edges = list(self._python_import_edges(files))
1089
- try:
1090
- edges.extend(self._js_import_edges(files, file_set))
1091
- except Exception:
1092
- logger.debug("JS/TS import-edge resolution failed", exc_info=True)
1093
- try:
1094
- edges.extend(self._go_import_edges(files, file_set))
1095
- except Exception:
1096
- logger.debug("Go import-edge resolution failed", exc_info=True)
1097
- return edges
1098
-
1099
- def _rank_area_files(self, area_files: List[str], in_degree: Counter) -> List[str]:
1100
- def sort_key(path: str) -> Tuple[int, int, str]:
1101
- name = Path(path).stem.lower()
1102
- entry_rank = next((i for i, hint in enumerate(_ENTRY_NAME_HINTS) if name == hint), len(_ENTRY_NAME_HINTS))
1103
- # Most-imported first, then entry-named, then alphabetical for stability.
1104
- return (-in_degree.get(path, 0), entry_rank, path)
1105
-
1106
- return sorted(area_files, key=sort_key)
1107
-
1108
- def _build_generic_subsystems(self, files: List[str]) -> List[RepoSubsystem]:
1109
- source_root = self._source_root if self._source_root is not None else self.detect_source_root(files)
1110
- code_files = self._code_files(files)
1111
- if not code_files:
1112
- return []
1113
-
1114
- by_area: Dict[str, List[str]] = defaultdict(list)
1115
- area_of: Dict[str, str] = {}
1116
- for f in code_files:
1117
- area = self._generic_area_for_file(f, source_root)
1118
- by_area[area].append(f)
1119
- area_of[f] = area
1120
-
1121
- edges = self._edges if self._edges is not None else self._python_import_edges(files)
1122
- in_degree: Counter = Counter(target for _, target in edges)
1123
- area_neighbors: Dict[str, Set[str]] = defaultdict(set)
1124
- area_handoffs: Dict[str, List[str]] = defaultdict(list)
1125
- for importer, imported in edges:
1126
- a, b = area_of.get(importer), area_of.get(imported)
1127
- if a and b and a != b:
1128
- area_neighbors[a].add(b)
1129
- if len(area_handoffs[a]) < 3:
1130
- area_handoffs[a].append(f"{importer} -> {imported}")
1131
-
1132
- subsystems: List[RepoSubsystem] = []
1133
- for area in sorted(by_area):
1134
- area_files = by_area[area]
1135
- # Skip trivial single-file aux areas (e.g. a lone script) to reduce noise,
1136
- # but keep every real source subsystem.
1137
- if len(area_files) < 2 and area.split("/")[0] in _AUX_AREA_ROOTS:
1138
- continue
1139
- ranked = self._rank_area_files(area_files, in_degree)
1140
- critical_files = ranked[: self._SUBSYSTEM_CRITICAL_MAX]
1141
- entry_points = [p for p in critical_files if in_degree.get(p, 0) > 0][:3] or critical_files[:1]
1142
- stems = ", ".join(Path(p).stem for p in critical_files[:3])
1143
- summary = f"{Path(area).name or area}: {stems}" if stems else f"{area} ({len(area_files)} files)"
1144
- subsystems.append(
1145
- RepoSubsystem(
1146
- area=area,
1147
- summary=summary,
1148
- entry_points=entry_points,
1149
- critical_files=critical_files,
1150
- neighbors=sorted(area_neighbors.get(area, set()))[:6],
1151
- handoff_paths=area_handoffs.get(area, []),
1152
- role_files={},
1153
- )
1154
- )
1155
- return subsystems
1156
-
1157
- def generic_important_files(self, files: List[str]) -> List[str]:
1158
- """The most-depended-on source files across the repo (highest import in-degree),
1159
- used to seed 'important surfaces' on repos without a curated index."""
1160
- edges = self._edges if self._edges is not None else self._python_import_edges(files)
1161
- if not edges:
1162
- return []
1163
- in_degree = Counter(target for _, target in edges)
1164
- ranked = [path for path, _ in in_degree.most_common()]
1165
- return ranked[:8]
1166
-
1167
- def build_dependents(self, edges: List[Tuple[str, str]]) -> Dict[str, List[str]]:
1168
- """Reverse the import edges into a file -> dependents map (who imports each file),
1169
- capped per file. This is the blast radius an agent needs before changing a file."""
1170
- reverse: Dict[str, Set[str]] = defaultdict(set)
1171
- for importer, imported in edges:
1172
- reverse[imported].add(importer)
1173
- return {
1174
- path: sorted(importers)[: self._DEPENDENTS_MAX]
1175
- for path, importers in sorted(reverse.items())
1176
- if importers
1177
- }
1178
-
1179
- def describe_file(self, path: str) -> RepoFileEntry:
1180
- return RepoFileEntry(
1181
- path=path,
1182
- area=self._area_for_file(path),
1183
- kind=self._kind_for_file(path),
1184
- language=self._language_for_file(path),
1185
- summary=self._summary_for_file(path),
1186
- )
1187
-
1188
- def _is_runtime_or_generated_file(self, path: str) -> bool:
1189
- normalized = path.replace("\\", "/")
1190
- parts = set(normalized.split("/"))
1191
- name = Path(normalized).name
1192
- if "__pycache__" in parts or normalized.endswith(".pyc"):
1193
- return True
1194
- if parts.intersection({".git", ".devcouncil", ".pytest_cache", ".ruff_cache", ".mypy_cache", ".venv"}):
1195
- return True
1196
- if normalized.startswith("dist/") or normalized.startswith("build/"):
1197
- return True
1198
- if name.startswith(("tmp", "temp", ".tmp", "debug")) or name.endswith("~"):
1199
- return True
1200
- return False
1201
-
1202
- def _git_head(self) -> str:
1203
- try:
1204
- return subprocess.check_output(
1205
- ["git", "rev-parse", "HEAD"], cwd=self.project_root, stderr=subprocess.DEVNULL
1206
- ).decode("utf-8", errors="replace").strip()
1207
- except Exception:
1208
- return ""
1209
-
1210
- def _files_fingerprint(self, files: List[str]) -> str:
1211
- return hashlib.sha1("\n".join(sorted(files)).encode("utf-8")).hexdigest()
1212
-
1213
- def map_is_stale(self, repo_map: Dict[str, object]) -> bool:
1214
- """True when the stored map no longer matches the repo's current git HEAD or
1215
- tracked file set — i.e. commits or file add/removes happened since ``dev map``
1216
- last ran. Returns False for maps written before fingerprinting (no false alarms)."""
1217
- stored_head = str(repo_map.get("generated_head") or "")
1218
- stored_hash = str(repo_map.get("indexed_hash") or "")
1219
- if not stored_head and not stored_hash:
1220
- return False
1221
- try:
1222
- files = self.get_git_files()
1223
- except Exception:
1224
- return False
1225
- return self._git_head() != stored_head or self._files_fingerprint(files) != stored_hash
1226
-
1227
- def get_git_files(self) -> List[str]:
1228
- try:
1229
- output = subprocess.check_output(
1230
- ["git", "ls-files", "--cached", "--others", "--exclude-standard"],
1231
- cwd=self.project_root,
1232
- stderr=subprocess.DEVNULL
1233
- ).decode().splitlines()
1234
- return [path for path in output if not self._is_runtime_or_generated_file(path)]
1235
- except Exception:
1236
- # Fallback to os.walk if not a git repo or git missing
1237
- files = []
1238
- for root, _, filenames in os.walk(self.project_root):
1239
- for f in filenames:
1240
- rel_path = os.path.relpath(os.path.join(root, f), self.project_root)
1241
- if not rel_path.startswith(".") and not self._is_runtime_or_generated_file(rel_path):
1242
- files.append(rel_path)
1243
- return files
1244
-
1245
- def detect_languages(self, files: List[str]) -> List[str]:
1246
- exts = {os.path.splitext(f)[1] for f in files}
1247
- lang_map = {
1248
- ".py": "python",
1249
- ".ts": "typescript",
1250
- ".tsx": "typescript",
1251
- ".js": "javascript",
1252
- ".jsx": "javascript",
1253
- ".go": "go",
1254
- ".rs": "rust",
1255
- ".java": "java",
1256
- ".c": "c",
1257
- ".cpp": "cpp",
1258
- }
1259
- return sorted(list({lang_map[ext] for ext in exts if ext in lang_map}))
1260
-
1261
- def detect_frameworks(self, files: List[str]) -> List[str]:
1262
- frameworks = []
1263
- file_set = set(files)
1264
- if "package.json" in file_set:
1265
- content = (self.project_root / "package.json").read_text()
1266
- if "next" in content:
1267
- frameworks.append("nextjs")
1268
- if "react" in content:
1269
- frameworks.append("react")
1270
- if "vue" in content:
1271
- frameworks.append("vue")
1272
- if "express" in content:
1273
- frameworks.append("express")
1274
-
1275
- if "requirements.txt" in file_set or "pyproject.toml" in file_set:
1276
- try:
1277
- content = ""
1278
- if "requirements.txt" in file_set:
1279
- content += (self.project_root / "requirements.txt").read_text()
1280
- if "pyproject.toml" in file_set:
1281
- content += (self.project_root / "pyproject.toml").read_text()
1282
-
1283
- if "fastapi" in content.lower():
1284
- frameworks.append("fastapi")
1285
- if "flask" in content.lower():
1286
- frameworks.append("flask")
1287
- if "django" in content.lower():
1288
- frameworks.append("django")
1289
- except Exception as e:
1290
- logger.debug("Failed to read Python config files: %s", e)
1291
- return frameworks
1292
-
1293
- def detect_package_managers(self, files: List[str]) -> List[str]:
1294
- managers = []
1295
- file_set = set(files)
1296
- if "package-lock.json" in file_set:
1297
- managers.append("npm")
1298
- elif "package.json" in file_set:
1299
- managers.append("npm")
1300
- if "yarn.lock" in file_set:
1301
- managers.append("yarn")
1302
- if "pnpm-lock.yaml" in file_set:
1303
- managers.append("pnpm")
1304
- if "requirements.txt" in file_set:
1305
- managers.append("pip")
1306
- if "uv.lock" in file_set:
1307
- managers.append("uv")
1308
- if "go.sum" in file_set:
1309
- managers.append("go mod")
1310
- return managers
1311
-
1312
- def detect_test_commands(self, files: List[str]) -> List[str]:
1313
- """Detect test, lint, and typecheck commands from project config."""
1314
- commands: List[str] = []
1315
- file_set = set(files)
1316
-
1317
- # Node.js projects: read scripts from package.json
1318
- if "package.json" in file_set:
1319
- try:
1320
- pkg = json.loads((self.project_root / "package.json").read_text())
1321
- scripts = pkg.get("scripts", {})
1322
- pm = "pnpm" if "pnpm-lock.yaml" in file_set else (
1323
- "yarn" if "yarn.lock" in file_set else "npm"
1324
- )
1325
- for key in ["test", "lint", "typecheck", "check", "type-check"]:
1326
- if key in scripts:
1327
- if pm == "npm" and key != "test":
1328
- commands.append(f"npm run {key}")
1329
- else:
1330
- commands.append(f"{pm} {key}")
1331
- except Exception as e:
1332
- logger.debug("Failed to parse package.json scripts: %s", e)
1333
-
1334
- # Python projects
1335
- if "pyproject.toml" in file_set or "setup.py" in file_set:
1336
- if any(f.startswith("tests/") or f.startswith("test_") for f in files):
1337
- commands.append("pytest")
1338
- commands.append("ruff check .")
1339
- commands.append("mypy .")
1340
-
1341
- # Go projects
1342
- if "go.mod" in file_set:
1343
- commands.append("go test ./...")
1344
- commands.append("go vet ./...")
1345
-
1346
- # Rust projects
1347
- if "Cargo.toml" in file_set:
1348
- commands.append("cargo test")
1349
- commands.append("cargo clippy")
1350
-
1351
- return commands
1352
-
1353
- def _ripgrep_search(self, goal: str, files: List[str]) -> List[Dict[str, str]]:
1354
- """Use ripgrep for goal-keyword search if available, else fall back to naive matching."""
1355
- candidates: List[Dict[str, str]] = []
1356
- try:
1357
- # Try ripgrep first for better matching
1358
- result = subprocess.run(
1359
- ["rg", "--files-with-matches", "--ignore-case", "--glob", "!.git", goal],
1360
- capture_output=True, text=True, cwd=self.project_root, timeout=10,
1361
- )
1362
- if result.returncode == 0:
1363
- for line in sorted(result.stdout.strip().splitlines())[:10]:
1364
- candidates.append({"path": line.strip(), "reason": f"ripgrep match for '{goal}'"})
1365
- return candidates
1366
- except Exception:
1367
- pass # Fall back to naive matching
1368
-
1369
- # Naive keyword matching fallback
1370
- goal_words = set(goal.lower().split())
1371
- scored_candidates: list[tuple[int, str]] = []
1372
- for f in files:
1373
- f_lower = f.lower()
1374
- score = sum(1 for word in goal_words if word in f_lower)
1375
- if score > 0:
1376
- scored_candidates.append((score, f))
1377
- candidates = [
1378
- {"path": path, "reason": f"Matches goal keywords (score: {score})"}
1379
- for score, path in sorted(scored_candidates, key=lambda item: (item[0], item[1]), reverse=True)
1380
- ][:10]
1381
- return candidates
1382
-
1383
- def _scan_dependency_risks(self) -> List[Dict[str, str]]:
1384
- """Best-effort SCA scan; isolated so map_repo stays simple and never raises."""
1385
- try:
1386
- from devcouncil.repo.sca import scan_dependency_risks
1387
-
1388
- return scan_dependency_risks(self.project_root)
1389
- except Exception:
1390
- logger.debug("Dependency-risk scan failed", exc_info=True)
1391
- return []
1392
-
1393
- def map_repo(self, goal: str = "", *, scan_dependencies: bool = False) -> RepoMap:
1394
- """Build the repo map.
1395
-
1396
- ``scan_dependencies`` is opt-in (default off) so the common ``dev map`` path
1397
- stays fast and never shells out to a vulnerability auditor. When enabled, a
1398
- best-effort SCA scan runs locally (only if an auditor is installed) and its
1399
- findings are attached as ``dependency_risks``.
1400
- """
1401
- files = self.get_git_files()
1402
- # Decide DevCouncil-vs-generic and the source root BEFORE describing files, so
1403
- # area bucketing and subsystem inference agree within a single run.
1404
- self._use_generic = not any(path.startswith("src/devcouncil/") for path in files)
1405
- self._source_root = self.detect_source_root(files)
1406
- # Compute the import graph once; reused by subsystem inference, important-file
1407
- # ranking, and the dependents (blast-radius) index. Spans Python, TS/JS, and Go
1408
- # so non-Python repos get dependents/neighbors too.
1409
- self._edges = self._all_import_edges(files)
1410
- file_entries = [self.describe_file(path) for path in sorted(files)]
1411
- file_set = set(files)
1412
-
1413
- important_candidates = [
1414
- "README.md",
1415
- "AGENTS.md",
1416
- "CLAUDE.md",
1417
- "package.json",
1418
- "pyproject.toml",
1419
- "src/devcouncil/cli/main.py",
1420
- "src/devcouncil/app/orchestrator.py",
1421
- "src/devcouncil/app/state_machine.py",
1422
- "src/devcouncil/artifacts/graph.py",
1423
- "src/devcouncil/indexing/repo_mapper.py",
1424
- "src/devcouncil/storage/repositories.py",
1425
- "src/devcouncil/execution/task_runner.py",
1426
- "src/devcouncil/verification/verifier.py",
1427
- ]
1428
- important_files = [path for path in important_candidates if path in file_set]
1429
- important_files.extend(sorted(path for path in files if path.startswith(".github/workflows/")))
1430
- # On non-DevCouncil repos the curated candidates above mostly miss, so seed
1431
- # important surfaces from the most-depended-on source files.
1432
- if self._use_generic:
1433
- for path in self.generic_important_files(files):
1434
- if path not in important_files:
1435
- important_files.append(path)
1436
-
1437
- candidates: List[Dict[str, str]] = []
1438
- if goal:
1439
- candidates = self._ripgrep_search(goal, files)
1440
-
1441
- return RepoMap(
1442
- languages=self.detect_languages(files),
1443
- frameworks=self.detect_frameworks(files),
1444
- package_managers=self.detect_package_managers(files),
1445
- test_commands=self.detect_test_commands(files),
1446
- important_files=important_files,
1447
- candidate_files=candidates,
1448
- files=file_entries,
1449
- subsystems=self._build_subsystem_index(files),
1450
- dependents=self.build_dependents(self._edges or []),
1451
- generated_head=self._git_head(),
1452
- indexed_hash=self._files_fingerprint(files),
1453
- lsp=LspInspector(self.project_root).summary(files),
1454
- dependency_risks=self._scan_dependency_risks() if scan_dependencies else [],
1455
- )