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,36 @@
1
+ """Pydantic models for code analysis results.
2
+
3
+ Architecture: S16.1 — Code-Aware Graph
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class ModuleInfo(BaseModel):
12
+ """Language-agnostic module analysis result.
13
+
14
+ Attributes:
15
+ name: Module name (e.g., 'memory', 'config').
16
+ language: Programming language (e.g., 'python').
17
+ source_path: Relative path to the module directory.
18
+ imports: Other modules this one imports from.
19
+ exports: Public API names exported by this module.
20
+ component_count: Number of classes + top-level functions.
21
+ entry_points: CLI commands or other entry points, if detectable.
22
+ """
23
+
24
+ name: str = Field(..., description="Module name")
25
+ language: str = Field(..., description="Programming language")
26
+ source_path: str = Field(..., description="Relative path to module directory")
27
+ imports: list[str] = Field(
28
+ default_factory=lambda: list[str](), description="Imported module names"
29
+ )
30
+ exports: list[str] = Field(
31
+ default_factory=lambda: list[str](), description="Exported public API names"
32
+ )
33
+ component_count: int = Field(..., description="Classes + top-level functions")
34
+ entry_points: list[str] = Field(
35
+ default_factory=lambda: list[str](), description="CLI commands or entry points"
36
+ )
@@ -0,0 +1,43 @@
1
+ """Protocol definition for code analyzers.
2
+
3
+ Architecture: S16.1 — Code-Aware Graph
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import Protocol, runtime_checkable
10
+
11
+ from rai_cli.context.analyzers.models import ModuleInfo
12
+
13
+
14
+ @runtime_checkable
15
+ class CodeAnalyzer(Protocol):
16
+ """Contract for language-specific code analyzers.
17
+
18
+ Implementations must provide:
19
+ - detect(): Check if the project uses this language.
20
+ - analyze_modules(): Extract module-level structure.
21
+ """
22
+
23
+ def detect(self, project_root: Path) -> bool:
24
+ """Check if this analyzer applies to the given project.
25
+
26
+ Args:
27
+ project_root: Root directory of the project.
28
+
29
+ Returns:
30
+ True if the project uses this language.
31
+ """
32
+ ...
33
+
34
+ def analyze_modules(self, project_root: Path) -> list[ModuleInfo]:
35
+ """Extract module-level structure from source code.
36
+
37
+ Args:
38
+ project_root: Root directory of the project.
39
+
40
+ Returns:
41
+ List of ModuleInfo for each detected module.
42
+ """
43
+ ...
@@ -0,0 +1,291 @@
1
+ """Python-specific code analyzer using ast module.
2
+
3
+ Extracts imports, exports, and component counts from Python modules
4
+ by parsing source files with ast.parse().
5
+
6
+ Architecture: S16.1 — Code-Aware Graph
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ast
12
+ from pathlib import Path
13
+
14
+ from rai_cli.context.analyzers.models import ModuleInfo
15
+
16
+
17
+ class PythonAnalyzer:
18
+ """Analyzes Python modules using ast to extract structure.
19
+
20
+ Attributes:
21
+ src_dir: Relative path to the source directory (e.g., 'src/rai_cli').
22
+ """
23
+
24
+ def __init__(self, src_dir: str) -> None:
25
+ self.src_dir = src_dir
26
+ self._package_name = Path(src_dir).name
27
+
28
+ def detect(self, project_root: Path) -> bool:
29
+ """Check if the project is a Python project.
30
+
31
+ Args:
32
+ project_root: Root directory of the project.
33
+
34
+ Returns:
35
+ True if pyproject.toml or setup.py exists.
36
+ """
37
+ return (project_root / "pyproject.toml").exists() or (
38
+ project_root / "setup.py"
39
+ ).exists()
40
+
41
+ def analyze_modules(self, project_root: Path) -> list[ModuleInfo]:
42
+ """Extract module-level structure from all Python modules.
43
+
44
+ Args:
45
+ project_root: Root directory of the project.
46
+
47
+ Returns:
48
+ List of ModuleInfo for each module directory.
49
+ """
50
+ src_path = project_root / self.src_dir
51
+ if not src_path.exists():
52
+ return []
53
+
54
+ modules: list[ModuleInfo] = []
55
+ for entry in sorted(src_path.iterdir()):
56
+ if not entry.is_dir():
57
+ continue
58
+ if entry.name.startswith("__"):
59
+ continue
60
+ if not (entry / "__init__.py").exists():
61
+ continue
62
+
63
+ info = self._analyze_module(entry, project_root)
64
+ modules.append(info)
65
+
66
+ return modules
67
+
68
+ def _analyze_module(self, module_dir: Path, project_root: Path) -> ModuleInfo:
69
+ """Analyze a single module directory.
70
+
71
+ Args:
72
+ module_dir: Path to the module directory.
73
+ project_root: Root directory of the project.
74
+
75
+ Returns:
76
+ ModuleInfo with extracted data.
77
+ """
78
+ module_name = module_dir.name
79
+ py_files = sorted(module_dir.rglob("*.py"))
80
+
81
+ imports: set[str] = set()
82
+ component_count = 0
83
+
84
+ for py_file in py_files:
85
+ tree = self._parse_file(py_file)
86
+ if tree is None:
87
+ continue
88
+
89
+ # Skip __init__.py for imports (handled separately for exports)
90
+ if py_file.name != "__init__.py":
91
+ file_imports = self._extract_imports(tree, module_name)
92
+ imports.update(file_imports)
93
+ else:
94
+ # __init__.py imports from siblings also count as module deps
95
+ file_imports = self._extract_imports(tree, module_name)
96
+ imports.update(file_imports)
97
+
98
+ component_count += self._count_components(tree)
99
+
100
+ exports = self._extract_exports(module_dir)
101
+
102
+ try:
103
+ source_path = str(module_dir.relative_to(project_root))
104
+ except ValueError:
105
+ source_path = str(module_dir)
106
+
107
+ return ModuleInfo(
108
+ name=module_name,
109
+ language="python",
110
+ source_path=source_path,
111
+ imports=sorted(imports),
112
+ exports=sorted(exports),
113
+ component_count=component_count,
114
+ entry_points=[],
115
+ )
116
+
117
+ def _parse_file(self, py_file: Path) -> ast.Module | None:
118
+ """Parse a Python file into AST.
119
+
120
+ Args:
121
+ py_file: Path to the .py file.
122
+
123
+ Returns:
124
+ Parsed AST or None on failure.
125
+ """
126
+ try:
127
+ source = py_file.read_text(encoding="utf-8")
128
+ return ast.parse(source, filename=str(py_file))
129
+ except (SyntaxError, UnicodeDecodeError):
130
+ return None
131
+
132
+ def _extract_imports(self, tree: ast.Module, module_name: str) -> set[str]:
133
+ """Extract internal module imports, skipping TYPE_CHECKING blocks.
134
+
135
+ Args:
136
+ tree: Parsed AST.
137
+ module_name: Name of the current module (to exclude self-imports).
138
+
139
+ Returns:
140
+ Set of imported module names (siblings only).
141
+ """
142
+ visitor = _ImportVisitor(self._package_name, module_name)
143
+ visitor.visit(tree)
144
+ return visitor.imports
145
+
146
+ def _extract_exports(self, module_dir: Path) -> list[str]:
147
+ """Extract public API from __init__.py.
148
+
149
+ Uses __all__ if present, otherwise imported names.
150
+
151
+ Args:
152
+ module_dir: Path to the module directory.
153
+
154
+ Returns:
155
+ List of exported names.
156
+ """
157
+ init_file = module_dir / "__init__.py"
158
+ if not init_file.exists():
159
+ return []
160
+
161
+ tree = self._parse_file(init_file)
162
+ if tree is None:
163
+ return []
164
+
165
+ # Check for __all__
166
+ for node in ast.walk(tree):
167
+ if isinstance(node, ast.Assign):
168
+ for target in node.targets:
169
+ if isinstance(target, ast.Name) and target.id == "__all__":
170
+ return self._extract_all_list(node.value)
171
+
172
+ # Fallback: extract imported names from __init__.py
173
+ names: list[str] = []
174
+ for node in ast.iter_child_nodes(tree):
175
+ if isinstance(node, ast.ImportFrom) and node.names:
176
+ for alias in node.names:
177
+ name = alias.asname if alias.asname else alias.name
178
+ if not name.startswith("_"):
179
+ names.append(name)
180
+ return names
181
+
182
+ def _extract_all_list(self, node: ast.expr) -> list[str]:
183
+ """Extract string values from an __all__ assignment.
184
+
185
+ Args:
186
+ node: The value node of the __all__ assignment.
187
+
188
+ Returns:
189
+ List of names from __all__.
190
+ """
191
+ names: list[str] = []
192
+ if isinstance(node, (ast.List, ast.Tuple)):
193
+ for elt in node.elts:
194
+ if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
195
+ names.append(elt.value)
196
+ return names
197
+
198
+ def _count_components(self, tree: ast.Module) -> int:
199
+ """Count top-level classes and functions in a module.
200
+
201
+ Args:
202
+ tree: Parsed AST.
203
+
204
+ Returns:
205
+ Count of top-level class and function definitions.
206
+ """
207
+ count = 0
208
+ for node in ast.iter_child_nodes(tree):
209
+ if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
210
+ count += 1
211
+ return count
212
+
213
+
214
+ class _ImportVisitor(ast.NodeVisitor):
215
+ """AST visitor that extracts imports while skipping TYPE_CHECKING blocks.
216
+
217
+ Attributes:
218
+ package_name: Top-level package name (e.g., 'rai_cli').
219
+ module_name: Current module name (to exclude self-imports).
220
+ imports: Set of discovered sibling module names.
221
+ """
222
+
223
+ def __init__(self, package_name: str, module_name: str) -> None:
224
+ self.package_name = package_name
225
+ self.module_name = module_name
226
+ self.imports: set[str] = set()
227
+ self._in_type_checking = False
228
+
229
+ def visit_If(self, node: ast.If) -> None:
230
+ """Skip imports inside TYPE_CHECKING blocks."""
231
+ if self._is_type_checking(node.test):
232
+ # Don't visit the body — skip TYPE_CHECKING imports
233
+ for child in node.orelse:
234
+ self.visit(child)
235
+ else:
236
+ self.generic_visit(node)
237
+
238
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
239
+ """Extract module name from 'from X import Y' statements."""
240
+ if self._in_type_checking:
241
+ return
242
+
243
+ if node.module and node.level == 0:
244
+ # Absolute import: from pkg.sibling.foo import bar
245
+ self._resolve_absolute(node.module)
246
+ elif node.level > 0 and node.module:
247
+ # Relative import: from ..sibling import bar
248
+ self._resolve_relative(node.module, node.level)
249
+ elif node.level > 0 and node.names:
250
+ # Relative import without module: from .. import sibling
251
+ for alias in node.names:
252
+ if node.level >= 2:
253
+ # from ..sibling means the name IS the sibling module
254
+ imported = alias.name
255
+ if imported != self.module_name:
256
+ self.imports.add(imported)
257
+
258
+ def visit_Import(self, node: ast.Import) -> None:
259
+ """Extract module name from 'import X' statements."""
260
+ if self._in_type_checking:
261
+ return
262
+
263
+ for alias in node.names:
264
+ self._resolve_absolute(alias.name)
265
+
266
+ def _resolve_absolute(self, module_path: str) -> None:
267
+ """Resolve an absolute import to a sibling module name."""
268
+ parts = module_path.split(".")
269
+ # Must start with our package name
270
+ if parts[0] != self.package_name:
271
+ return
272
+ if len(parts) < 2:
273
+ return
274
+ sibling = parts[1]
275
+ if sibling != self.module_name:
276
+ self.imports.add(sibling)
277
+
278
+ def _resolve_relative(self, module_path: str, level: int) -> None:
279
+ """Resolve a relative import to a sibling module name."""
280
+ if level >= 2:
281
+ # from ..sibling.foo import bar → sibling is the module
282
+ parts = module_path.split(".")
283
+ sibling = parts[0]
284
+ if sibling != self.module_name:
285
+ self.imports.add(sibling)
286
+
287
+ def _is_type_checking(self, test: ast.expr) -> bool:
288
+ """Check if an if-test is TYPE_CHECKING."""
289
+ return (
290
+ isinstance(test, ast.Name) and test.id == "TYPE_CHECKING"
291
+ ) or (isinstance(test, ast.Attribute) and test.attr == "TYPE_CHECKING")