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