raise-cli 2.2.1__py3-none-any.whl

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 (264) hide show
  1. raise_cli/__init__.py +38 -0
  2. raise_cli/__main__.py +30 -0
  3. raise_cli/adapters/__init__.py +91 -0
  4. raise_cli/adapters/declarative/__init__.py +26 -0
  5. raise_cli/adapters/declarative/adapter.py +267 -0
  6. raise_cli/adapters/declarative/discovery.py +94 -0
  7. raise_cli/adapters/declarative/expressions.py +150 -0
  8. raise_cli/adapters/declarative/reference/__init__.py +1 -0
  9. raise_cli/adapters/declarative/reference/github.yaml +143 -0
  10. raise_cli/adapters/declarative/schema.py +98 -0
  11. raise_cli/adapters/filesystem.py +299 -0
  12. raise_cli/adapters/mcp_bridge.py +10 -0
  13. raise_cli/adapters/mcp_confluence.py +246 -0
  14. raise_cli/adapters/mcp_jira.py +405 -0
  15. raise_cli/adapters/models.py +205 -0
  16. raise_cli/adapters/protocols.py +180 -0
  17. raise_cli/adapters/registry.py +90 -0
  18. raise_cli/adapters/sync.py +149 -0
  19. raise_cli/agents/__init__.py +14 -0
  20. raise_cli/agents/antigravity.yaml +8 -0
  21. raise_cli/agents/claude.yaml +8 -0
  22. raise_cli/agents/copilot.yaml +8 -0
  23. raise_cli/agents/copilot_plugin.py +124 -0
  24. raise_cli/agents/cursor.yaml +7 -0
  25. raise_cli/agents/roo.yaml +8 -0
  26. raise_cli/agents/windsurf.yaml +8 -0
  27. raise_cli/artifacts/__init__.py +30 -0
  28. raise_cli/artifacts/models.py +43 -0
  29. raise_cli/artifacts/reader.py +55 -0
  30. raise_cli/artifacts/renderer.py +104 -0
  31. raise_cli/artifacts/story_design.py +69 -0
  32. raise_cli/artifacts/writer.py +45 -0
  33. raise_cli/backlog/__init__.py +1 -0
  34. raise_cli/backlog/sync.py +115 -0
  35. raise_cli/cli/__init__.py +3 -0
  36. raise_cli/cli/commands/__init__.py +3 -0
  37. raise_cli/cli/commands/_resolve.py +153 -0
  38. raise_cli/cli/commands/adapters.py +362 -0
  39. raise_cli/cli/commands/artifact.py +137 -0
  40. raise_cli/cli/commands/backlog.py +333 -0
  41. raise_cli/cli/commands/base.py +31 -0
  42. raise_cli/cli/commands/discover.py +551 -0
  43. raise_cli/cli/commands/docs.py +130 -0
  44. raise_cli/cli/commands/doctor.py +177 -0
  45. raise_cli/cli/commands/gate.py +223 -0
  46. raise_cli/cli/commands/graph.py +1086 -0
  47. raise_cli/cli/commands/info.py +81 -0
  48. raise_cli/cli/commands/init.py +746 -0
  49. raise_cli/cli/commands/journal.py +167 -0
  50. raise_cli/cli/commands/mcp.py +524 -0
  51. raise_cli/cli/commands/memory.py +467 -0
  52. raise_cli/cli/commands/pattern.py +348 -0
  53. raise_cli/cli/commands/profile.py +59 -0
  54. raise_cli/cli/commands/publish.py +80 -0
  55. raise_cli/cli/commands/release.py +338 -0
  56. raise_cli/cli/commands/session.py +528 -0
  57. raise_cli/cli/commands/signal.py +410 -0
  58. raise_cli/cli/commands/skill.py +350 -0
  59. raise_cli/cli/commands/skill_set.py +145 -0
  60. raise_cli/cli/error_handler.py +158 -0
  61. raise_cli/cli/main.py +163 -0
  62. raise_cli/compat.py +66 -0
  63. raise_cli/config/__init__.py +41 -0
  64. raise_cli/config/agent_plugin.py +105 -0
  65. raise_cli/config/agent_registry.py +233 -0
  66. raise_cli/config/agents.py +120 -0
  67. raise_cli/config/ide.py +32 -0
  68. raise_cli/config/paths.py +379 -0
  69. raise_cli/config/settings.py +180 -0
  70. raise_cli/context/__init__.py +42 -0
  71. raise_cli/context/analyzers/__init__.py +16 -0
  72. raise_cli/context/analyzers/models.py +36 -0
  73. raise_cli/context/analyzers/protocol.py +43 -0
  74. raise_cli/context/analyzers/python.py +292 -0
  75. raise_cli/context/builder.py +1569 -0
  76. raise_cli/context/diff.py +213 -0
  77. raise_cli/context/extractors/__init__.py +13 -0
  78. raise_cli/context/extractors/skills.py +121 -0
  79. raise_cli/core/__init__.py +37 -0
  80. raise_cli/core/files.py +66 -0
  81. raise_cli/core/text.py +174 -0
  82. raise_cli/core/tools.py +441 -0
  83. raise_cli/discovery/__init__.py +50 -0
  84. raise_cli/discovery/analyzer.py +691 -0
  85. raise_cli/discovery/drift.py +355 -0
  86. raise_cli/discovery/scanner.py +1687 -0
  87. raise_cli/doctor/__init__.py +4 -0
  88. raise_cli/doctor/checks/__init__.py +1 -0
  89. raise_cli/doctor/checks/environment.py +110 -0
  90. raise_cli/doctor/checks/project.py +238 -0
  91. raise_cli/doctor/fix.py +80 -0
  92. raise_cli/doctor/models.py +56 -0
  93. raise_cli/doctor/protocol.py +43 -0
  94. raise_cli/doctor/registry.py +100 -0
  95. raise_cli/doctor/report.py +141 -0
  96. raise_cli/doctor/runner.py +95 -0
  97. raise_cli/engines/__init__.py +3 -0
  98. raise_cli/exceptions.py +215 -0
  99. raise_cli/gates/__init__.py +19 -0
  100. raise_cli/gates/builtin/__init__.py +1 -0
  101. raise_cli/gates/builtin/coverage.py +52 -0
  102. raise_cli/gates/builtin/lint.py +48 -0
  103. raise_cli/gates/builtin/tests.py +48 -0
  104. raise_cli/gates/builtin/types.py +48 -0
  105. raise_cli/gates/models.py +40 -0
  106. raise_cli/gates/protocol.py +41 -0
  107. raise_cli/gates/registry.py +141 -0
  108. raise_cli/governance/__init__.py +11 -0
  109. raise_cli/governance/extractor.py +412 -0
  110. raise_cli/governance/models.py +134 -0
  111. raise_cli/governance/parsers/__init__.py +35 -0
  112. raise_cli/governance/parsers/_convert.py +38 -0
  113. raise_cli/governance/parsers/adr.py +274 -0
  114. raise_cli/governance/parsers/backlog.py +356 -0
  115. raise_cli/governance/parsers/constitution.py +119 -0
  116. raise_cli/governance/parsers/epic.py +323 -0
  117. raise_cli/governance/parsers/glossary.py +316 -0
  118. raise_cli/governance/parsers/guardrails.py +345 -0
  119. raise_cli/governance/parsers/prd.py +112 -0
  120. raise_cli/governance/parsers/roadmap.py +118 -0
  121. raise_cli/governance/parsers/vision.py +116 -0
  122. raise_cli/graph/__init__.py +1 -0
  123. raise_cli/graph/backends/__init__.py +57 -0
  124. raise_cli/graph/backends/api.py +137 -0
  125. raise_cli/graph/backends/dual.py +139 -0
  126. raise_cli/graph/backends/pending.py +84 -0
  127. raise_cli/handlers/__init__.py +3 -0
  128. raise_cli/hooks/__init__.py +54 -0
  129. raise_cli/hooks/builtin/__init__.py +1 -0
  130. raise_cli/hooks/builtin/backlog.py +216 -0
  131. raise_cli/hooks/builtin/gate_bridge.py +83 -0
  132. raise_cli/hooks/builtin/jira_sync.py +127 -0
  133. raise_cli/hooks/builtin/memory.py +117 -0
  134. raise_cli/hooks/builtin/telemetry.py +72 -0
  135. raise_cli/hooks/emitter.py +184 -0
  136. raise_cli/hooks/events.py +262 -0
  137. raise_cli/hooks/protocol.py +38 -0
  138. raise_cli/hooks/registry.py +117 -0
  139. raise_cli/mcp/__init__.py +33 -0
  140. raise_cli/mcp/bridge.py +218 -0
  141. raise_cli/mcp/models.py +43 -0
  142. raise_cli/mcp/registry.py +77 -0
  143. raise_cli/mcp/schema.py +41 -0
  144. raise_cli/memory/__init__.py +58 -0
  145. raise_cli/memory/loader.py +247 -0
  146. raise_cli/memory/migration.py +241 -0
  147. raise_cli/memory/models.py +169 -0
  148. raise_cli/memory/writer.py +598 -0
  149. raise_cli/onboarding/__init__.py +103 -0
  150. raise_cli/onboarding/bootstrap.py +324 -0
  151. raise_cli/onboarding/claudemd.py +17 -0
  152. raise_cli/onboarding/conventions.py +742 -0
  153. raise_cli/onboarding/detection.py +374 -0
  154. raise_cli/onboarding/governance.py +443 -0
  155. raise_cli/onboarding/instructions.py +672 -0
  156. raise_cli/onboarding/manifest.py +201 -0
  157. raise_cli/onboarding/memory_md.py +399 -0
  158. raise_cli/onboarding/migration.py +207 -0
  159. raise_cli/onboarding/profile.py +624 -0
  160. raise_cli/onboarding/skill_conflict.py +100 -0
  161. raise_cli/onboarding/skill_manifest.py +176 -0
  162. raise_cli/onboarding/skills.py +437 -0
  163. raise_cli/onboarding/workflows.py +101 -0
  164. raise_cli/output/__init__.py +28 -0
  165. raise_cli/output/console.py +394 -0
  166. raise_cli/output/formatters/__init__.py +9 -0
  167. raise_cli/output/formatters/adapters.py +135 -0
  168. raise_cli/output/formatters/discover.py +439 -0
  169. raise_cli/output/formatters/skill.py +298 -0
  170. raise_cli/publish/__init__.py +3 -0
  171. raise_cli/publish/changelog.py +80 -0
  172. raise_cli/publish/check.py +179 -0
  173. raise_cli/publish/version.py +172 -0
  174. raise_cli/rai_base/__init__.py +22 -0
  175. raise_cli/rai_base/framework/__init__.py +7 -0
  176. raise_cli/rai_base/framework/methodology.yaml +233 -0
  177. raise_cli/rai_base/governance/__init__.py +1 -0
  178. raise_cli/rai_base/governance/architecture/__init__.py +1 -0
  179. raise_cli/rai_base/governance/architecture/domain-model.md +20 -0
  180. raise_cli/rai_base/governance/architecture/system-context.md +34 -0
  181. raise_cli/rai_base/governance/architecture/system-design.md +24 -0
  182. raise_cli/rai_base/governance/backlog.md +8 -0
  183. raise_cli/rai_base/governance/guardrails.md +17 -0
  184. raise_cli/rai_base/governance/prd.md +25 -0
  185. raise_cli/rai_base/governance/vision.md +16 -0
  186. raise_cli/rai_base/identity/__init__.py +8 -0
  187. raise_cli/rai_base/identity/core.md +119 -0
  188. raise_cli/rai_base/identity/perspective.md +119 -0
  189. raise_cli/rai_base/memory/__init__.py +7 -0
  190. raise_cli/rai_base/memory/patterns-base.jsonl +55 -0
  191. raise_cli/schemas/__init__.py +3 -0
  192. raise_cli/schemas/journal.py +49 -0
  193. raise_cli/schemas/session_state.py +117 -0
  194. raise_cli/session/__init__.py +5 -0
  195. raise_cli/session/bundle.py +820 -0
  196. raise_cli/session/close.py +268 -0
  197. raise_cli/session/journal.py +119 -0
  198. raise_cli/session/resolver.py +126 -0
  199. raise_cli/session/state.py +187 -0
  200. raise_cli/skills/__init__.py +44 -0
  201. raise_cli/skills/locator.py +141 -0
  202. raise_cli/skills/name_checker.py +199 -0
  203. raise_cli/skills/parser.py +145 -0
  204. raise_cli/skills/scaffold.py +212 -0
  205. raise_cli/skills/schema.py +132 -0
  206. raise_cli/skills/skillsets.py +195 -0
  207. raise_cli/skills/validator.py +197 -0
  208. raise_cli/skills_base/__init__.py +80 -0
  209. raise_cli/skills_base/contract-template.md +60 -0
  210. raise_cli/skills_base/preamble.md +37 -0
  211. raise_cli/skills_base/rai-architecture-review/SKILL.md +137 -0
  212. raise_cli/skills_base/rai-debug/SKILL.md +171 -0
  213. raise_cli/skills_base/rai-discover/SKILL.md +167 -0
  214. raise_cli/skills_base/rai-discover-document/SKILL.md +128 -0
  215. raise_cli/skills_base/rai-discover-scan/SKILL.md +147 -0
  216. raise_cli/skills_base/rai-discover-start/SKILL.md +145 -0
  217. raise_cli/skills_base/rai-discover-validate/SKILL.md +142 -0
  218. raise_cli/skills_base/rai-docs-update/SKILL.md +142 -0
  219. raise_cli/skills_base/rai-doctor/SKILL.md +120 -0
  220. raise_cli/skills_base/rai-epic-close/SKILL.md +165 -0
  221. raise_cli/skills_base/rai-epic-close/templates/retrospective.md +68 -0
  222. raise_cli/skills_base/rai-epic-design/SKILL.md +146 -0
  223. raise_cli/skills_base/rai-epic-design/templates/design.md +24 -0
  224. raise_cli/skills_base/rai-epic-design/templates/scope.md +76 -0
  225. raise_cli/skills_base/rai-epic-plan/SKILL.md +153 -0
  226. raise_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
  227. raise_cli/skills_base/rai-epic-plan/templates/plan-section.md +49 -0
  228. raise_cli/skills_base/rai-epic-run/SKILL.md +208 -0
  229. raise_cli/skills_base/rai-epic-start/SKILL.md +136 -0
  230. raise_cli/skills_base/rai-epic-start/templates/brief.md +34 -0
  231. raise_cli/skills_base/rai-mcp-add/SKILL.md +176 -0
  232. raise_cli/skills_base/rai-mcp-remove/SKILL.md +120 -0
  233. raise_cli/skills_base/rai-mcp-status/SKILL.md +147 -0
  234. raise_cli/skills_base/rai-problem-shape/SKILL.md +138 -0
  235. raise_cli/skills_base/rai-project-create/SKILL.md +144 -0
  236. raise_cli/skills_base/rai-project-onboard/SKILL.md +162 -0
  237. raise_cli/skills_base/rai-quality-review/SKILL.md +189 -0
  238. raise_cli/skills_base/rai-research/SKILL.md +143 -0
  239. raise_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
  240. raise_cli/skills_base/rai-session-close/SKILL.md +176 -0
  241. raise_cli/skills_base/rai-session-start/SKILL.md +110 -0
  242. raise_cli/skills_base/rai-story-close/SKILL.md +198 -0
  243. raise_cli/skills_base/rai-story-design/SKILL.md +203 -0
  244. raise_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
  245. raise_cli/skills_base/rai-story-implement/SKILL.md +115 -0
  246. raise_cli/skills_base/rai-story-plan/SKILL.md +135 -0
  247. raise_cli/skills_base/rai-story-review/SKILL.md +178 -0
  248. raise_cli/skills_base/rai-story-run/SKILL.md +282 -0
  249. raise_cli/skills_base/rai-story-start/SKILL.md +166 -0
  250. raise_cli/skills_base/rai-story-start/templates/story.md +38 -0
  251. raise_cli/skills_base/rai-welcome/SKILL.md +134 -0
  252. raise_cli/telemetry/__init__.py +42 -0
  253. raise_cli/telemetry/schemas.py +285 -0
  254. raise_cli/telemetry/writer.py +217 -0
  255. raise_cli/tier/__init__.py +0 -0
  256. raise_cli/tier/context.py +134 -0
  257. raise_cli/viz/__init__.py +7 -0
  258. raise_cli/viz/generator.py +406 -0
  259. raise_cli-2.2.1.dist-info/METADATA +433 -0
  260. raise_cli-2.2.1.dist-info/RECORD +264 -0
  261. raise_cli-2.2.1.dist-info/WHEEL +4 -0
  262. raise_cli-2.2.1.dist-info/entry_points.txt +40 -0
  263. raise_cli-2.2.1.dist-info/licenses/LICENSE +190 -0
  264. raise_cli-2.2.1.dist-info/licenses/NOTICE +4 -0
@@ -0,0 +1,551 @@
1
+ """Discovery CLI commands for codebase scanning and graph integration.
2
+
3
+ This module provides commands to scan codebases, extract structural
4
+ information, and integrate discovered components into the unified context graph.
5
+
6
+ Supports Python, TypeScript, JavaScript, PHP, Svelte, and C#.
7
+
8
+ Example:
9
+ $ raise discover scan src/
10
+ $ raise discover scan . --language typescript --output json
11
+ $ raise discover build --input work/discovery/components-validated.json
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from pathlib import Path
18
+ from typing import Annotated, Any
19
+
20
+ import typer
21
+ from rich.console import Console
22
+
23
+ from raise_cli.cli.error_handler import cli_error
24
+ from raise_cli.discovery.scanner import Language, ScanResult, scan_directory
25
+ from raise_cli.hooks.emitter import create_emitter
26
+ from raise_cli.hooks.events import DiscoverScanEvent
27
+ from raise_cli.output.formatters.discover import (
28
+ format_analyze_result,
29
+ format_build_result,
30
+ format_drift_result,
31
+ format_scan_result,
32
+ )
33
+
34
+ discover_app = typer.Typer(
35
+ name="discover",
36
+ help="Codebase discovery and analysis commands",
37
+ no_args_is_help=True,
38
+ )
39
+
40
+ console = Console()
41
+
42
+
43
+ @discover_app.command("scan")
44
+ def scan_command(
45
+ path: Annotated[
46
+ Path,
47
+ typer.Argument(
48
+ help="Directory to scan for source files",
49
+ exists=True,
50
+ file_okay=False,
51
+ dir_okay=True,
52
+ resolve_path=True,
53
+ ),
54
+ ] = Path("."),
55
+ language: Annotated[
56
+ str | None,
57
+ typer.Option(
58
+ "--language",
59
+ "-l",
60
+ help="Language to scan: python, typescript, javascript, php, svelte, csharp (auto-detect if not set)",
61
+ ),
62
+ ] = None,
63
+ output: Annotated[
64
+ str,
65
+ typer.Option(
66
+ "--output",
67
+ "-o",
68
+ help="Output format: human, json, or summary",
69
+ ),
70
+ ] = "human",
71
+ pattern: Annotated[
72
+ str | None,
73
+ typer.Option(
74
+ "--pattern",
75
+ "-p",
76
+ help="Glob pattern for files (default: language-specific)",
77
+ ),
78
+ ] = None,
79
+ exclude: Annotated[
80
+ list[str] | None,
81
+ typer.Option(
82
+ "--exclude",
83
+ "-e",
84
+ help="Patterns to exclude (can be repeated)",
85
+ ),
86
+ ] = None,
87
+ ) -> None:
88
+ """Scan a directory and extract code symbols.
89
+
90
+ Extracts classes, functions, methods, interfaces, and module docstrings
91
+ from source files. Supports Python, TypeScript, JavaScript, PHP, Svelte, and C#.
92
+
93
+ Output can be human-readable table, JSON, or summary statistics.
94
+
95
+ Examples:
96
+ # Scan current directory (auto-detect languages)
97
+ raise discover scan
98
+
99
+ # Scan Python files only
100
+ raise discover scan src/ --language python
101
+
102
+ # Scan TypeScript project
103
+ raise discover scan ./app --language typescript --output json
104
+
105
+ # Auto-detect but exclude tests
106
+ raise discover scan . --exclude "**/test_*" --exclude "**/__tests__/**"
107
+ """
108
+ # Validate language if provided
109
+ lang: Language | None = None
110
+ if language:
111
+ if language not in (
112
+ "python",
113
+ "typescript",
114
+ "javascript",
115
+ "php",
116
+ "svelte",
117
+ "csharp",
118
+ ):
119
+ cli_error(
120
+ f"Unsupported language: {language}",
121
+ hint="Supported: python, typescript, javascript, php, svelte, csharp",
122
+ exit_code=7,
123
+ )
124
+ lang = language # type: ignore[assignment]
125
+
126
+ # Pass user excludes or None to use DEFAULT_EXCLUDE_PATTERNS from scanner
127
+ exclude_patterns = exclude if exclude else None
128
+
129
+ result = scan_directory(
130
+ path,
131
+ language=lang,
132
+ pattern=pattern,
133
+ exclude_patterns=exclude_patterns,
134
+ )
135
+
136
+ # Emit discover:scan event
137
+ emitter = create_emitter()
138
+ emitter.emit(
139
+ DiscoverScanEvent(
140
+ project_path=path,
141
+ language=lang or "auto",
142
+ component_count=len(result.symbols),
143
+ )
144
+ )
145
+
146
+ format_scan_result(result, path, output, language=lang)
147
+
148
+
149
+ @discover_app.command("analyze")
150
+ def analyze_command(
151
+ input_file: Annotated[
152
+ Path | None,
153
+ typer.Option(
154
+ "--input",
155
+ "-i",
156
+ help="Path to scan result JSON (reads stdin if not provided)",
157
+ ),
158
+ ] = None,
159
+ output: Annotated[
160
+ str,
161
+ typer.Option(
162
+ "--output",
163
+ "-o",
164
+ help="Output format: human, json, or summary",
165
+ ),
166
+ ] = "human",
167
+ category_map_file: Annotated[
168
+ Path | None,
169
+ typer.Option(
170
+ "--category-map",
171
+ "-c",
172
+ help="YAML file with custom path-to-category mappings",
173
+ ),
174
+ ] = None,
175
+ ) -> None:
176
+ """Analyze scan results with confidence scoring and module grouping.
177
+
178
+ Takes raw scan output (from `raise discover scan --output json`) and
179
+ produces an analysis with confidence scores, auto-categorization,
180
+ hierarchical folding, and module grouping for parallel AI synthesis.
181
+
182
+ All analysis is deterministic — no AI inference required.
183
+
184
+ Examples:
185
+ # Analyze from file
186
+ raise discover analyze --input scan-result.json
187
+
188
+ # Pipe from scan
189
+ raise discover scan src/ -l python -o json | raise discover analyze
190
+
191
+ # JSON output
192
+ raise discover analyze --input scan-result.json --output json
193
+
194
+ # Summary only
195
+ raise discover analyze --input scan-result.json --output summary
196
+ """
197
+ import sys
198
+
199
+ from raise_cli.discovery.analyzer import analyze
200
+
201
+ # Load scan result JSON
202
+ scan_json: str = ""
203
+ if input_file:
204
+ if not input_file.exists():
205
+ cli_error(
206
+ f"Input file not found: {input_file}",
207
+ hint="Run 'raise discover scan --output json' first",
208
+ exit_code=4,
209
+ )
210
+ scan_json = input_file.read_text(encoding="utf-8")
211
+ else:
212
+ # Read from stdin
213
+ if sys.stdin.isatty():
214
+ cli_error(
215
+ "No input provided",
216
+ hint="Pipe from scan: raise discover scan -o json | raise discover analyze\n"
217
+ "Or use --input: raise discover analyze --input scan-result.json",
218
+ exit_code=7,
219
+ )
220
+ scan_json = sys.stdin.read()
221
+
222
+ # Parse scan result
223
+ scan_result = ScanResult(symbols=[], files_scanned=0, errors=[])
224
+ try:
225
+ scan_data: dict[str, Any] = json.loads(scan_json)
226
+ scan_result = ScanResult(
227
+ symbols=[],
228
+ files_scanned=scan_data.get("files_scanned", 0),
229
+ errors=scan_data.get("errors", []),
230
+ )
231
+ # Parse symbols from JSON
232
+ from raise_cli.discovery.scanner import Symbol
233
+
234
+ for sym_data in scan_data.get("symbols", []):
235
+ scan_result.symbols.append(Symbol.model_validate(sym_data))
236
+ except (json.JSONDecodeError, KeyError, ValueError) as e:
237
+ cli_error(
238
+ f"Invalid scan result JSON: {e}",
239
+ hint="Input must be JSON from 'raise discover scan --output json'",
240
+ exit_code=7,
241
+ )
242
+
243
+ # Load custom category map if provided
244
+ category_map: dict[str, str] | None = None
245
+ if category_map_file:
246
+ if not category_map_file.exists():
247
+ cli_error(
248
+ f"Category map file not found: {category_map_file}",
249
+ exit_code=4,
250
+ )
251
+ try:
252
+ import yaml
253
+
254
+ category_map = yaml.safe_load(category_map_file.read_text(encoding="utf-8"))
255
+ except ImportError:
256
+ cli_error(
257
+ "PyYAML required for --category-map",
258
+ hint="Install with: pip install pyyaml",
259
+ exit_code=6,
260
+ )
261
+ except Exception as e:
262
+ cli_error(f"Error reading category map: {e}", exit_code=7)
263
+
264
+ # Run analysis
265
+ result = analyze(scan_result, category_map=category_map)
266
+
267
+ # Save analysis.json
268
+ output_dir = Path("work/discovery")
269
+ output_dir.mkdir(parents=True, exist_ok=True)
270
+ output_path = output_dir / "analysis.json"
271
+ output_path.write_text(
272
+ json.dumps(result.model_dump(), indent=2, default=str),
273
+ encoding="utf-8",
274
+ )
275
+
276
+ # Format and print
277
+ format_analyze_result(result, output)
278
+
279
+ if output != "json":
280
+ console.print(f"\n[dim]Saved: {output_path}[/dim]")
281
+
282
+
283
+ @discover_app.command("build")
284
+ def build_command(
285
+ input_file: Annotated[
286
+ Path | None,
287
+ typer.Option(
288
+ "--input",
289
+ "-i",
290
+ help="Path to validated components JSON (default: work/discovery/components-validated.json)",
291
+ ),
292
+ ] = None,
293
+ project_root: Annotated[
294
+ Path,
295
+ typer.Option(
296
+ "--project-root",
297
+ "-r",
298
+ help="Project root directory (default: current directory)",
299
+ ),
300
+ ] = Path("."),
301
+ output: Annotated[
302
+ str,
303
+ typer.Option(
304
+ "--output",
305
+ "-o",
306
+ help="Output format: human, json, or summary",
307
+ ),
308
+ ] = "human",
309
+ ) -> None:
310
+ """Build unified graph with discovered components.
311
+
312
+ Reads validated components from JSON and integrates them into the unified
313
+ context graph. Components become queryable via `raise context query`.
314
+
315
+ The graph is rebuilt from all sources (governance, memory, work, skills,
316
+ and components) and saved to `.raise/graph/unified.json`.
317
+
318
+ Examples:
319
+ # Build with default input file
320
+ raise discover build
321
+
322
+ # Build with custom input
323
+ raise discover build --input my-components.json
324
+
325
+ # Build and show JSON output
326
+ raise discover build --output json
327
+ """
328
+ root = project_root.resolve()
329
+
330
+ # Resolve input file path
331
+ if input_file is None:
332
+ input_path = root / "work" / "discovery" / "components-validated.json"
333
+ else:
334
+ input_path = input_file.resolve()
335
+
336
+ # Check input file exists
337
+ if not input_path.exists():
338
+ cli_error(
339
+ f"Components file not found: {input_path}",
340
+ hint="Run /rai-discover-validate to generate validated components",
341
+ exit_code=4,
342
+ )
343
+
344
+ # Load components to validate and count
345
+ component_count = 0
346
+ try:
347
+ data: dict[str, Any] = json.loads(input_path.read_text(encoding="utf-8"))
348
+ components: list[dict[str, Any]] = data.get("components", [])
349
+ component_count = len(components)
350
+ except (json.JSONDecodeError, KeyError) as e:
351
+ cli_error(f"Invalid JSON in {input_path}: {e}")
352
+
353
+ if component_count == 0:
354
+ cli_error(
355
+ "No components found in input file",
356
+ hint="Run /rai-discover-validate to validate components first",
357
+ )
358
+
359
+ # Build unified graph (includes components automatically)
360
+ from raise_cli.context.builder import GraphBuilder
361
+
362
+ builder = GraphBuilder(project_root=root)
363
+ graph = builder.build()
364
+
365
+ # Save graph via backend
366
+ from raise_cli.graph.backends import get_active_backend
367
+
368
+ graph_path = root / ".raise" / "graph" / "unified.json"
369
+ get_active_backend(graph_path).persist(graph)
370
+
371
+ # Count component nodes in graph
372
+ component_nodes = [n for n in graph.iter_concepts() if n.type == "component"]
373
+ components_in_graph = len(component_nodes)
374
+
375
+ # Build categories dict
376
+ categories: dict[str, int] = {}
377
+ for comp in component_nodes:
378
+ category = comp.metadata.get("category", "unknown")
379
+ categories[category] = categories.get(category, 0) + 1
380
+
381
+ # Build sample components list
382
+ sample_components = [
383
+ (
384
+ comp.metadata.get("name", comp.id),
385
+ comp.metadata.get("kind", ""),
386
+ comp.content[:60],
387
+ )
388
+ for comp in component_nodes[:3]
389
+ ]
390
+
391
+ format_build_result(
392
+ input_path=input_path,
393
+ graph_path=graph_path,
394
+ component_count=component_count,
395
+ components_in_graph=components_in_graph,
396
+ node_count=graph.node_count,
397
+ edge_count=graph.edge_count,
398
+ categories=categories,
399
+ sample_components=sample_components,
400
+ output_format=output,
401
+ )
402
+
403
+
404
+ @discover_app.command("drift")
405
+ def drift_command(
406
+ path: Annotated[
407
+ Path | None,
408
+ typer.Argument(
409
+ help="Directory to scan for drift (default: src/)",
410
+ ),
411
+ ] = None,
412
+ project_root: Annotated[
413
+ Path,
414
+ typer.Option(
415
+ "--project-root",
416
+ "-r",
417
+ help="Project root directory (default: current directory)",
418
+ ),
419
+ ] = Path("."),
420
+ output: Annotated[
421
+ str,
422
+ typer.Option(
423
+ "--output",
424
+ "-o",
425
+ help="Output format: human, json, or summary",
426
+ ),
427
+ ] = "human",
428
+ ) -> None:
429
+ """Check for architectural drift against baseline components.
430
+
431
+ Compares scanned code against the validated component baseline to
432
+ identify potential architectural drift (files in wrong locations,
433
+ naming convention violations, missing documentation).
434
+
435
+ Exit codes:
436
+ 0 - No drift detected
437
+ 1 - Drift warnings found
438
+
439
+ Examples:
440
+ # Check entire project
441
+ raise discover drift
442
+
443
+ # Check specific directory
444
+ raise discover drift src/new_module/
445
+
446
+ # Output as JSON
447
+ raise discover drift --output json
448
+ """
449
+ from raise_cli.discovery.drift import BaselineComponent, DriftWarning, detect_drift
450
+
451
+ root = project_root.resolve()
452
+ scan_path = path.resolve() if path else root / "src"
453
+
454
+ # Load baseline components
455
+ baseline_file = root / "work" / "discovery" / "components-validated.json"
456
+
457
+ if not baseline_file.exists():
458
+ if output == "json":
459
+ console.print_json(
460
+ json.dumps(
461
+ {
462
+ "status": "no_baseline",
463
+ "warnings": [],
464
+ "warning_count": 0,
465
+ "message": "No baseline components found",
466
+ }
467
+ )
468
+ )
469
+ else:
470
+ console.print(
471
+ "[yellow]No baseline components found.[/yellow]\n"
472
+ "[dim]Run /rai-discover-validate to create a baseline first.[/dim]"
473
+ )
474
+ raise typer.Exit(0)
475
+
476
+ # Load baseline
477
+ baseline: list[BaselineComponent] = []
478
+ try:
479
+ baseline_data: dict[str, Any] = json.loads(
480
+ baseline_file.read_text(encoding="utf-8")
481
+ )
482
+ baseline_dicts: list[dict[str, Any]] = baseline_data.get("components", [])
483
+ baseline = [BaselineComponent.model_validate(comp) for comp in baseline_dicts]
484
+ except (json.JSONDecodeError, KeyError) as e:
485
+ cli_error(f"Error reading baseline: {e}")
486
+
487
+ if not baseline:
488
+ if output == "json":
489
+ console.print_json(
490
+ json.dumps(
491
+ {
492
+ "status": "empty_baseline",
493
+ "warnings": [],
494
+ "warning_count": 0,
495
+ "message": "Baseline has no components",
496
+ }
497
+ )
498
+ )
499
+ else:
500
+ console.print(
501
+ "[yellow]Baseline has no components.[/yellow]\n"
502
+ "[dim]Run /rai-discover-validate to add components.[/dim]"
503
+ )
504
+ raise typer.Exit(0)
505
+
506
+ # Warn if baseline is too small for meaningful drift detection
507
+ min_baseline_size = 10
508
+ if len(baseline) < min_baseline_size and output == "human":
509
+ console.print(
510
+ f"[yellow]Note: Baseline has only {len(baseline)} component(s).[/yellow]\n"
511
+ f"[dim]Drift detection works best with {min_baseline_size}+ components "
512
+ "for meaningful patterns.[/dim]\n"
513
+ "[dim]Run /rai-discover-scan and /rai-discover-validate to expand the baseline.[/dim]\n"
514
+ )
515
+
516
+ # Scan for new symbols
517
+ if not scan_path.exists():
518
+ if output == "json":
519
+ console.print_json(
520
+ json.dumps(
521
+ {
522
+ "status": "no_source",
523
+ "warnings": [],
524
+ "warning_count": 0,
525
+ "message": f"Scan path not found: {scan_path}",
526
+ }
527
+ )
528
+ )
529
+ else:
530
+ console.print(f"[yellow]Scan path not found: {scan_path}[/yellow]")
531
+ raise typer.Exit(0)
532
+
533
+ scan_result = scan_directory(scan_path)
534
+
535
+ # Detect drift
536
+ warnings: list[DriftWarning] = detect_drift(
537
+ baseline=baseline,
538
+ scanned=scan_result.symbols,
539
+ )
540
+
541
+ # Output results
542
+ format_drift_result(
543
+ warnings=warnings,
544
+ files_scanned=scan_result.files_scanned,
545
+ symbols_checked=len(scan_result.symbols),
546
+ output_format=output,
547
+ )
548
+
549
+ # Exit with 1 if warnings found
550
+ if warnings:
551
+ raise typer.Exit(1)
@@ -0,0 +1,130 @@
1
+ """CLI commands for governance documentation via DocumentationTarget.
2
+
3
+ Provides the ``rai docs`` command group. All commands delegate to a
4
+ DocumentationTarget discovered via entry points. The target is resolved
5
+ automatically when exactly one is registered, or selected explicitly
6
+ via ``--target NAME``.
7
+
8
+ CLI owns domain logic (artifact type → local path convention).
9
+ Adapter owns platform config (space mapping, parent pages).
10
+
11
+ Architecture: E301 (Agent Tool Abstraction), ADR-034 (Governance)
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+ from typing import Annotated
18
+
19
+ import typer
20
+ from rich.console import Console
21
+
22
+ from raise_cli.cli.commands._resolve import resolve_docs_target
23
+
24
+ docs_app = typer.Typer(
25
+ name="docs",
26
+ help="Manage governance documentation via DocumentationTarget",
27
+ no_args_is_help=True,
28
+ )
29
+
30
+ console = Console()
31
+
32
+ # Common option for target override (D5)
33
+ TargetOption = Annotated[
34
+ str | None,
35
+ typer.Option(
36
+ "--target", "-t", help="Target name override (auto-detect if omitted)"
37
+ ),
38
+ ]
39
+
40
+ # Convention: governance artifacts live at governance/{type}.md
41
+ GOVERNANCE_DIR = "governance"
42
+
43
+
44
+ def _resolve_artifact_path(artifact_type: str) -> Path:
45
+ """Resolve artifact type to local file path by convention.
46
+
47
+ Convention: ``governance/{artifact_type}.md``
48
+
49
+ Args:
50
+ artifact_type: Governance artifact type (e.g., "roadmap", "adr").
51
+
52
+ Returns:
53
+ Path to the governance file.
54
+
55
+ Raises:
56
+ typer.Exit: If the file does not exist.
57
+ """
58
+ path = Path(GOVERNANCE_DIR) / f"{artifact_type}.md"
59
+ if not path.exists():
60
+ console.print(f"[red]Error:[/red] File not found: {path}")
61
+ raise typer.Exit(1)
62
+ return path
63
+
64
+
65
+ @docs_app.command()
66
+ def publish(
67
+ artifact_type: Annotated[
68
+ str, typer.Argument(help="Governance artifact type (e.g., roadmap, adr)")
69
+ ],
70
+ title: Annotated[
71
+ str | None, typer.Option("--title", help="Page title (default: artifact type)")
72
+ ] = None,
73
+ target: TargetOption = None,
74
+ ) -> None:
75
+ """Publish a governance artifact to a documentation target."""
76
+ path = _resolve_artifact_path(artifact_type)
77
+ content = path.read_text(encoding="utf-8")
78
+ doc_target = resolve_docs_target(target)
79
+
80
+ page_title = title or artifact_type
81
+ metadata = {"title": page_title, "path": str(path)}
82
+
83
+ result = doc_target.publish(
84
+ doc_type=artifact_type, content=content, metadata=metadata
85
+ )
86
+ if result.success:
87
+ console.print(f"Published: {artifact_type} → {result.url}")
88
+ else:
89
+ console.print(f"[red]Error:[/red] {result.message}")
90
+ raise typer.Exit(1)
91
+
92
+
93
+ @docs_app.command()
94
+ def get(
95
+ identifier: Annotated[str, typer.Argument(help="Page ID on the remote target")],
96
+ target: TargetOption = None,
97
+ ) -> None:
98
+ """Retrieve a page from the documentation target."""
99
+ doc_target = resolve_docs_target(target)
100
+ page = doc_target.get_page(identifier)
101
+ # Compact header + content
102
+ header_parts = [f"# {page.title}"]
103
+ if page.space_key:
104
+ header_parts.append(f"Space: {page.space_key}")
105
+ if page.version > 1:
106
+ header_parts.append(f"Version: {page.version}")
107
+ if page.url:
108
+ header_parts.append(page.url)
109
+
110
+ console.print(header_parts[0])
111
+ if len(header_parts) > 1:
112
+ console.print(" | ".join(header_parts[1:]))
113
+ console.print()
114
+ console.print(page.content)
115
+
116
+
117
+ @docs_app.command()
118
+ def search(
119
+ query: Annotated[str, typer.Argument(help="Search query")],
120
+ limit: Annotated[int, typer.Option("--limit", "-n", help="Max results")] = 10,
121
+ target: TargetOption = None,
122
+ ) -> None:
123
+ """Search documentation pages on the remote target."""
124
+ doc_target = resolve_docs_target(target)
125
+ results = doc_target.search(query, limit=limit)
126
+ if not results:
127
+ console.print("No results.")
128
+ return
129
+ for page in results:
130
+ console.print(f"{page.id:<8} {page.space_key:<8} {page.title}")