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.
Files changed (137) hide show
  1. rai_cli/__init__.py +38 -0
  2. rai_cli/__main__.py +30 -0
  3. rai_cli/cli/__init__.py +3 -0
  4. rai_cli/cli/commands/__init__.py +3 -0
  5. rai_cli/cli/commands/base.py +101 -0
  6. rai_cli/cli/commands/discover.py +547 -0
  7. rai_cli/cli/commands/init.py +460 -0
  8. rai_cli/cli/commands/memory.py +1626 -0
  9. rai_cli/cli/commands/profile.py +51 -0
  10. rai_cli/cli/commands/session.py +264 -0
  11. rai_cli/cli/commands/skill.py +226 -0
  12. rai_cli/cli/error_handler.py +158 -0
  13. rai_cli/cli/main.py +137 -0
  14. rai_cli/config/__init__.py +11 -0
  15. rai_cli/config/paths.py +309 -0
  16. rai_cli/config/settings.py +180 -0
  17. rai_cli/context/__init__.py +42 -0
  18. rai_cli/context/analyzers/__init__.py +16 -0
  19. rai_cli/context/analyzers/models.py +36 -0
  20. rai_cli/context/analyzers/protocol.py +43 -0
  21. rai_cli/context/analyzers/python.py +291 -0
  22. rai_cli/context/builder.py +1566 -0
  23. rai_cli/context/diff.py +213 -0
  24. rai_cli/context/extractors/__init__.py +13 -0
  25. rai_cli/context/extractors/skills.py +121 -0
  26. rai_cli/context/graph.py +300 -0
  27. rai_cli/context/models.py +134 -0
  28. rai_cli/context/query.py +507 -0
  29. rai_cli/core/__init__.py +37 -0
  30. rai_cli/core/files.py +66 -0
  31. rai_cli/core/text.py +174 -0
  32. rai_cli/core/tools.py +441 -0
  33. rai_cli/discovery/__init__.py +50 -0
  34. rai_cli/discovery/analyzer.py +601 -0
  35. rai_cli/discovery/drift.py +355 -0
  36. rai_cli/discovery/scanner.py +1200 -0
  37. rai_cli/engines/__init__.py +3 -0
  38. rai_cli/exceptions.py +200 -0
  39. rai_cli/governance/__init__.py +11 -0
  40. rai_cli/governance/extractor.py +311 -0
  41. rai_cli/governance/models.py +132 -0
  42. rai_cli/governance/parsers/__init__.py +35 -0
  43. rai_cli/governance/parsers/adr.py +255 -0
  44. rai_cli/governance/parsers/backlog.py +302 -0
  45. rai_cli/governance/parsers/constitution.py +100 -0
  46. rai_cli/governance/parsers/epic.py +299 -0
  47. rai_cli/governance/parsers/glossary.py +297 -0
  48. rai_cli/governance/parsers/guardrails.py +326 -0
  49. rai_cli/governance/parsers/prd.py +93 -0
  50. rai_cli/governance/parsers/vision.py +97 -0
  51. rai_cli/handlers/__init__.py +3 -0
  52. rai_cli/memory/__init__.py +58 -0
  53. rai_cli/memory/loader.py +247 -0
  54. rai_cli/memory/migration.py +247 -0
  55. rai_cli/memory/models.py +169 -0
  56. rai_cli/memory/writer.py +485 -0
  57. rai_cli/onboarding/__init__.py +96 -0
  58. rai_cli/onboarding/bootstrap.py +164 -0
  59. rai_cli/onboarding/claudemd.py +209 -0
  60. rai_cli/onboarding/conventions.py +742 -0
  61. rai_cli/onboarding/detection.py +155 -0
  62. rai_cli/onboarding/governance.py +443 -0
  63. rai_cli/onboarding/manifest.py +101 -0
  64. rai_cli/onboarding/memory_md.py +387 -0
  65. rai_cli/onboarding/migration.py +207 -0
  66. rai_cli/onboarding/profile.py +457 -0
  67. rai_cli/onboarding/skills.py +114 -0
  68. rai_cli/output/__init__.py +28 -0
  69. rai_cli/output/console.py +394 -0
  70. rai_cli/output/formatters/__init__.py +9 -0
  71. rai_cli/output/formatters/discover.py +442 -0
  72. rai_cli/output/formatters/skill.py +293 -0
  73. rai_cli/rai_base/__init__.py +22 -0
  74. rai_cli/rai_base/framework/__init__.py +7 -0
  75. rai_cli/rai_base/framework/methodology.yaml +235 -0
  76. rai_cli/rai_base/governance/__init__.py +1 -0
  77. rai_cli/rai_base/governance/architecture/__init__.py +1 -0
  78. rai_cli/rai_base/governance/architecture/domain-model.md +20 -0
  79. rai_cli/rai_base/governance/architecture/system-context.md +34 -0
  80. rai_cli/rai_base/governance/architecture/system-design.md +24 -0
  81. rai_cli/rai_base/governance/backlog.md +8 -0
  82. rai_cli/rai_base/governance/guardrails.md +18 -0
  83. rai_cli/rai_base/governance/prd.md +25 -0
  84. rai_cli/rai_base/governance/vision.md +16 -0
  85. rai_cli/rai_base/identity/__init__.py +8 -0
  86. rai_cli/rai_base/identity/core.md +119 -0
  87. rai_cli/rai_base/identity/perspective.md +119 -0
  88. rai_cli/rai_base/memory/__init__.py +7 -0
  89. rai_cli/rai_base/memory/patterns-base.jsonl +20 -0
  90. rai_cli/schemas/__init__.py +3 -0
  91. rai_cli/schemas/session_state.py +106 -0
  92. rai_cli/session/__init__.py +5 -0
  93. rai_cli/session/bundle.py +389 -0
  94. rai_cli/session/close.py +255 -0
  95. rai_cli/session/state.py +108 -0
  96. rai_cli/skills/__init__.py +44 -0
  97. rai_cli/skills/locator.py +129 -0
  98. rai_cli/skills/name_checker.py +203 -0
  99. rai_cli/skills/parser.py +145 -0
  100. rai_cli/skills/scaffold.py +185 -0
  101. rai_cli/skills/schema.py +130 -0
  102. rai_cli/skills/validator.py +172 -0
  103. rai_cli/skills_base/__init__.py +59 -0
  104. rai_cli/skills_base/rai-debug/SKILL.md +296 -0
  105. rai_cli/skills_base/rai-discover-document/SKILL.md +292 -0
  106. rai_cli/skills_base/rai-discover-scan/SKILL.md +325 -0
  107. rai_cli/skills_base/rai-discover-start/SKILL.md +213 -0
  108. rai_cli/skills_base/rai-discover-validate/SKILL.md +310 -0
  109. rai_cli/skills_base/rai-epic-close/SKILL.md +369 -0
  110. rai_cli/skills_base/rai-epic-design/SKILL.md +622 -0
  111. rai_cli/skills_base/rai-epic-plan/SKILL.md +672 -0
  112. rai_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
  113. rai_cli/skills_base/rai-epic-start/SKILL.md +217 -0
  114. rai_cli/skills_base/rai-project-create/SKILL.md +455 -0
  115. rai_cli/skills_base/rai-project-onboard/SKILL.md +503 -0
  116. rai_cli/skills_base/rai-research/SKILL.md +264 -0
  117. rai_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
  118. rai_cli/skills_base/rai-session-close/SKILL.md +151 -0
  119. rai_cli/skills_base/rai-session-start/SKILL.md +110 -0
  120. rai_cli/skills_base/rai-story-close/SKILL.md +367 -0
  121. rai_cli/skills_base/rai-story-design/SKILL.md +339 -0
  122. rai_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
  123. rai_cli/skills_base/rai-story-implement/SKILL.md +256 -0
  124. rai_cli/skills_base/rai-story-plan/SKILL.md +307 -0
  125. rai_cli/skills_base/rai-story-review/SKILL.md +276 -0
  126. rai_cli/skills_base/rai-story-start/SKILL.md +288 -0
  127. rai_cli/telemetry/__init__.py +42 -0
  128. rai_cli/telemetry/schemas.py +285 -0
  129. rai_cli/telemetry/writer.py +210 -0
  130. rai_cli/viz/__init__.py +7 -0
  131. rai_cli/viz/generator.py +404 -0
  132. rai_cli-2.0.0a1.dist-info/METADATA +289 -0
  133. rai_cli-2.0.0a1.dist-info/RECORD +137 -0
  134. rai_cli-2.0.0a1.dist-info/WHEEL +4 -0
  135. rai_cli-2.0.0a1.dist-info/entry_points.txt +2 -0
  136. rai_cli-2.0.0a1.dist-info/licenses/LICENSE +190 -0
  137. 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)