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,180 @@
1
+ """Configuration settings for raise-cli using Pydantic Settings.
2
+
3
+ Implements configuration cascade with proper precedence:
4
+ 1. CLI arguments (highest priority)
5
+ 2. Environment variables (RAI_* prefix)
6
+ 3. Project config (pyproject.toml)
7
+ 4. User config (~/.config/rai/config.toml)
8
+ 5. Defaults (lowest priority)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Any, Literal
16
+
17
+ from pydantic import Field
18
+ from pydantic_settings import (
19
+ BaseSettings,
20
+ PydanticBaseSettingsSource,
21
+ SettingsConfigDict,
22
+ )
23
+
24
+ if sys.version_info >= (3, 11): # noqa: UP036
25
+ import tomllib
26
+ else:
27
+ import tomli as tomllib
28
+
29
+ from raise_cli.config.paths import get_config_dir
30
+
31
+
32
+ class TomlConfigSource(PydanticBaseSettingsSource):
33
+ """Custom settings source for TOML configuration files."""
34
+
35
+ def __init__(
36
+ self,
37
+ settings_cls: type[BaseSettings],
38
+ toml_file: Path | None = None,
39
+ toml_table: str = "rai",
40
+ ):
41
+ """Initialize TOML config source.
42
+
43
+ Args:
44
+ settings_cls: The settings class
45
+ toml_file: Path to TOML file (None = auto-detect)
46
+ toml_table: Table name in TOML file (default: "rai")
47
+ """
48
+ super().__init__(settings_cls)
49
+ self.toml_file = toml_file
50
+ self.toml_table = toml_table
51
+
52
+ def get_field_value(self, field: Any, field_name: str) -> tuple[Any, str, bool]:
53
+ """Get field value from TOML file."""
54
+ # Not used in newer pydantic-settings
55
+ return None, "", False
56
+
57
+ def __call__(self) -> dict[str, Any]:
58
+ """Load settings from TOML file."""
59
+ if self.toml_file is None or not self.toml_file.exists():
60
+ return {}
61
+
62
+ try:
63
+ with open(self.toml_file, "rb") as f:
64
+ data = tomllib.load(f)
65
+
66
+ # Handle pyproject.toml with [tool.rai] section
67
+ if self.toml_file.name == "pyproject.toml":
68
+ return data.get("tool", {}).get(self.toml_table, {})
69
+ # Handle user config with [rai] section
70
+ else:
71
+ return data.get(self.toml_table, {})
72
+ except Exception:
73
+ # Silently ignore TOML parsing errors (graceful degradation)
74
+ return {}
75
+
76
+
77
+ class RaiSettings(BaseSettings):
78
+ """Configuration settings for raise-cli with proper cascade precedence.
79
+
80
+ Settings are loaded in order of precedence (highest to lowest):
81
+ 1. Constructor arguments (from CLI)
82
+ 2. Environment variables (RAI_* prefix)
83
+ 3. Project pyproject.toml [tool.rai] section
84
+ 4. User config file (~/.config/rai/config.toml)
85
+ 5. Field defaults
86
+
87
+ Example:
88
+ >>> # Load with defaults and env vars
89
+ >>> settings = RaiSettings()
90
+ >>> # Override specific values (e.g., from CLI)
91
+ >>> settings = RaiSettings(output_format="json", verbosity=2)
92
+ """
93
+
94
+ model_config = SettingsConfigDict(
95
+ env_prefix="RAI_",
96
+ extra="ignore",
97
+ )
98
+
99
+ # Output settings
100
+ output_format: Literal["human", "json", "table"] = Field(
101
+ default="human",
102
+ description="Output format for CLI commands",
103
+ )
104
+ color: bool = Field(
105
+ default=True,
106
+ description="Enable colored output",
107
+ )
108
+ verbosity: int = Field(
109
+ default=0,
110
+ ge=-1,
111
+ le=3,
112
+ description="Verbosity level: -1=quiet, 0=normal, 1-3=verbose",
113
+ )
114
+
115
+ # Paths (project-level)
116
+ raise_dir: Path = Field(
117
+ default=Path(".raise"),
118
+ description="Directory containing RaiSE framework files",
119
+ )
120
+ governance_dir: Path = Field(
121
+ default=Path("governance"),
122
+ description="Directory containing governance artifacts",
123
+ )
124
+ work_dir: Path = Field(
125
+ default=Path("work"),
126
+ description="Directory containing active work",
127
+ )
128
+
129
+ # External tools (graceful degradation handled by core utilities)
130
+ ast_grep_path: str | None = Field(
131
+ default=None,
132
+ description="Path to ast-grep binary (auto-detected if None)",
133
+ )
134
+ ripgrep_path: str | None = Field(
135
+ default=None,
136
+ description="Path to ripgrep binary (auto-detected if None)",
137
+ )
138
+
139
+ # Feature flags
140
+ interactive: bool = Field(
141
+ default=False,
142
+ description="Enable interactive prompts",
143
+ )
144
+
145
+ @classmethod
146
+ def settings_customise_sources(
147
+ cls,
148
+ settings_cls: type[BaseSettings],
149
+ init_settings: PydanticBaseSettingsSource,
150
+ env_settings: PydanticBaseSettingsSource,
151
+ dotenv_settings: PydanticBaseSettingsSource,
152
+ file_secret_settings: PydanticBaseSettingsSource,
153
+ ) -> tuple[PydanticBaseSettingsSource, ...]:
154
+ """Customize settings sources to include TOML files.
155
+
156
+ Order (highest to lowest priority):
157
+ 1. init_settings (constructor args / CLI)
158
+ 2. env_settings (environment variables)
159
+ 3. project_toml (pyproject.toml)
160
+ 4. user_toml (~/.config/rai/config.toml)
161
+ 5. file defaults (from Field definitions)
162
+ """
163
+ # Project-level config
164
+ project_toml = TomlConfigSource(
165
+ settings_cls, toml_file=Path("pyproject.toml"), toml_table="rai"
166
+ )
167
+
168
+ # User-level config
169
+ user_config_file = get_config_dir() / "config.toml"
170
+ user_toml = TomlConfigSource(
171
+ settings_cls, toml_file=user_config_file, toml_table="rai"
172
+ )
173
+
174
+ return (
175
+ init_settings, # CLI args (highest priority)
176
+ env_settings, # Environment variables
177
+ project_toml, # pyproject.toml
178
+ user_toml, # User config
179
+ # Field defaults are always last (implicit)
180
+ )
@@ -0,0 +1,42 @@
1
+ """Context module for cross-domain knowledge retrieval.
2
+
3
+ Re-exports graph domain types from raise_core and local context components.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from raise_cli.context.builder import GraphBuilder
9
+ from raise_cli.context.diff import GraphDiff, NodeChange, diff_graphs
10
+ from raise_core.graph.engine import Graph
11
+ from raise_core.graph.models import (
12
+ CoreEdgeTypes,
13
+ EdgeType,
14
+ GraphEdge,
15
+ GraphNode,
16
+ NodeType,
17
+ )
18
+ from raise_core.graph.query import (
19
+ Query,
20
+ QueryEngine,
21
+ QueryMetadata,
22
+ QueryResult,
23
+ QueryStrategy,
24
+ )
25
+
26
+ __all__ = [
27
+ "CoreEdgeTypes",
28
+ "EdgeType",
29
+ "Graph",
30
+ "GraphDiff",
31
+ "GraphEdge",
32
+ "GraphNode",
33
+ "NodeChange",
34
+ "NodeType",
35
+ "Query",
36
+ "QueryEngine",
37
+ "QueryMetadata",
38
+ "QueryResult",
39
+ "QueryStrategy",
40
+ "GraphBuilder",
41
+ "diff_graphs",
42
+ ]
@@ -0,0 +1,16 @@
1
+ """Code analyzers for extracting module structure from source code.
2
+
3
+ This package provides language-specific analyzers that extract imports,
4
+ exports, and component counts from source modules. Results enrich the
5
+ unified graph with real code data alongside manually-maintained frontmatter.
6
+
7
+ Architecture: S16.1 — Code-Aware Graph
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from raise_cli.context.analyzers.models import ModuleInfo
13
+ from raise_cli.context.analyzers.protocol import CodeAnalyzer
14
+ from raise_cli.context.analyzers.python import PythonAnalyzer
15
+
16
+ __all__ = ["CodeAnalyzer", "ModuleInfo", "PythonAnalyzer"]
@@ -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 raise_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,292 @@
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 raise_cli.compat import portable_path
15
+ from raise_cli.context.analyzers.models import ModuleInfo
16
+
17
+
18
+ class PythonAnalyzer:
19
+ """Analyzes Python modules using ast to extract structure.
20
+
21
+ Attributes:
22
+ src_dir: Relative path to the source directory (e.g., 'src/raise_cli').
23
+ """
24
+
25
+ def __init__(self, src_dir: str) -> None:
26
+ self.src_dir = src_dir
27
+ self._package_name = Path(src_dir).name
28
+
29
+ def detect(self, project_root: Path) -> bool:
30
+ """Check if the project is a Python project.
31
+
32
+ Args:
33
+ project_root: Root directory of the project.
34
+
35
+ Returns:
36
+ True if pyproject.toml or setup.py exists.
37
+ """
38
+ return (project_root / "pyproject.toml").exists() or (
39
+ project_root / "setup.py"
40
+ ).exists()
41
+
42
+ def analyze_modules(self, project_root: Path) -> list[ModuleInfo]:
43
+ """Extract module-level structure from all Python modules.
44
+
45
+ Args:
46
+ project_root: Root directory of the project.
47
+
48
+ Returns:
49
+ List of ModuleInfo for each module directory.
50
+ """
51
+ src_path = project_root / self.src_dir
52
+ if not src_path.exists():
53
+ return []
54
+
55
+ modules: list[ModuleInfo] = []
56
+ for entry in sorted(src_path.iterdir()):
57
+ if not entry.is_dir():
58
+ continue
59
+ if entry.name.startswith("__"):
60
+ continue
61
+ if not (entry / "__init__.py").exists():
62
+ continue
63
+
64
+ info = self._analyze_module(entry, project_root)
65
+ modules.append(info)
66
+
67
+ return modules
68
+
69
+ def _analyze_module(self, module_dir: Path, project_root: Path) -> ModuleInfo:
70
+ """Analyze a single module directory.
71
+
72
+ Args:
73
+ module_dir: Path to the module directory.
74
+ project_root: Root directory of the project.
75
+
76
+ Returns:
77
+ ModuleInfo with extracted data.
78
+ """
79
+ module_name = module_dir.name
80
+ py_files = sorted(module_dir.rglob("*.py"))
81
+
82
+ imports: set[str] = set()
83
+ component_count = 0
84
+
85
+ for py_file in py_files:
86
+ tree = self._parse_file(py_file)
87
+ if tree is None:
88
+ continue
89
+
90
+ # Skip __init__.py for imports (handled separately for exports)
91
+ if py_file.name != "__init__.py":
92
+ file_imports = self._extract_imports(tree, module_name)
93
+ imports.update(file_imports)
94
+ else:
95
+ # __init__.py imports from siblings also count as module deps
96
+ file_imports = self._extract_imports(tree, module_name)
97
+ imports.update(file_imports)
98
+
99
+ component_count += self._count_components(tree)
100
+
101
+ exports = self._extract_exports(module_dir)
102
+
103
+ try:
104
+ source_path = portable_path(module_dir, project_root)
105
+ except ValueError:
106
+ source_path = str(module_dir)
107
+
108
+ return ModuleInfo(
109
+ name=module_name,
110
+ language="python",
111
+ source_path=source_path,
112
+ imports=sorted(imports),
113
+ exports=sorted(exports),
114
+ component_count=component_count,
115
+ entry_points=[],
116
+ )
117
+
118
+ def _parse_file(self, py_file: Path) -> ast.Module | None:
119
+ """Parse a Python file into AST.
120
+
121
+ Args:
122
+ py_file: Path to the .py file.
123
+
124
+ Returns:
125
+ Parsed AST or None on failure.
126
+ """
127
+ try:
128
+ source = py_file.read_text(encoding="utf-8")
129
+ return ast.parse(source, filename=str(py_file))
130
+ except (SyntaxError, UnicodeDecodeError):
131
+ return None
132
+
133
+ def _extract_imports(self, tree: ast.Module, module_name: str) -> set[str]:
134
+ """Extract internal module imports, skipping TYPE_CHECKING blocks.
135
+
136
+ Args:
137
+ tree: Parsed AST.
138
+ module_name: Name of the current module (to exclude self-imports).
139
+
140
+ Returns:
141
+ Set of imported module names (siblings only).
142
+ """
143
+ visitor = _ImportVisitor(self._package_name, module_name)
144
+ visitor.visit(tree)
145
+ return visitor.imports
146
+
147
+ def _extract_exports(self, module_dir: Path) -> list[str]:
148
+ """Extract public API from __init__.py.
149
+
150
+ Uses __all__ if present, otherwise imported names.
151
+
152
+ Args:
153
+ module_dir: Path to the module directory.
154
+
155
+ Returns:
156
+ List of exported names.
157
+ """
158
+ init_file = module_dir / "__init__.py"
159
+ if not init_file.exists():
160
+ return []
161
+
162
+ tree = self._parse_file(init_file)
163
+ if tree is None:
164
+ return []
165
+
166
+ # Check for __all__
167
+ for node in ast.walk(tree):
168
+ if isinstance(node, ast.Assign):
169
+ for target in node.targets:
170
+ if isinstance(target, ast.Name) and target.id == "__all__":
171
+ return self._extract_all_list(node.value)
172
+
173
+ # Fallback: extract imported names from __init__.py
174
+ names: list[str] = []
175
+ for node in ast.iter_child_nodes(tree):
176
+ if isinstance(node, ast.ImportFrom) and node.names:
177
+ for alias in node.names:
178
+ name = alias.asname if alias.asname else alias.name
179
+ if not name.startswith("_"):
180
+ names.append(name)
181
+ return names
182
+
183
+ def _extract_all_list(self, node: ast.expr) -> list[str]:
184
+ """Extract string values from an __all__ assignment.
185
+
186
+ Args:
187
+ node: The value node of the __all__ assignment.
188
+
189
+ Returns:
190
+ List of names from __all__.
191
+ """
192
+ names: list[str] = []
193
+ if isinstance(node, (ast.List, ast.Tuple)):
194
+ for elt in node.elts:
195
+ if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
196
+ names.append(elt.value)
197
+ return names
198
+
199
+ def _count_components(self, tree: ast.Module) -> int:
200
+ """Count top-level classes and functions in a module.
201
+
202
+ Args:
203
+ tree: Parsed AST.
204
+
205
+ Returns:
206
+ Count of top-level class and function definitions.
207
+ """
208
+ count = 0
209
+ for node in ast.iter_child_nodes(tree):
210
+ if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
211
+ count += 1
212
+ return count
213
+
214
+
215
+ class _ImportVisitor(ast.NodeVisitor):
216
+ """AST visitor that extracts imports while skipping TYPE_CHECKING blocks.
217
+
218
+ Attributes:
219
+ package_name: Top-level package name (e.g., 'raise_cli').
220
+ module_name: Current module name (to exclude self-imports).
221
+ imports: Set of discovered sibling module names.
222
+ """
223
+
224
+ def __init__(self, package_name: str, module_name: str) -> None:
225
+ self.package_name = package_name
226
+ self.module_name = module_name
227
+ self.imports: set[str] = set()
228
+ self._in_type_checking = False
229
+
230
+ def visit_If(self, node: ast.If) -> None: # noqa: N802
231
+ """Skip imports inside TYPE_CHECKING blocks."""
232
+ if self._is_type_checking(node.test):
233
+ # Don't visit the body — skip TYPE_CHECKING imports
234
+ for child in node.orelse:
235
+ self.visit(child)
236
+ else:
237
+ self.generic_visit(node)
238
+
239
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None: # noqa: N802
240
+ """Extract module name from 'from X import Y' statements."""
241
+ if self._in_type_checking:
242
+ return
243
+
244
+ if node.module and node.level == 0:
245
+ # Absolute import: from pkg.sibling.foo import bar
246
+ self._resolve_absolute(node.module)
247
+ elif node.level > 0 and node.module:
248
+ # Relative import: from ..sibling import bar
249
+ self._resolve_relative(node.module, node.level)
250
+ elif node.level > 0 and node.names:
251
+ # Relative import without module: from .. import sibling
252
+ for alias in node.names:
253
+ if node.level >= 2:
254
+ # from ..sibling means the name IS the sibling module
255
+ imported = alias.name
256
+ if imported != self.module_name:
257
+ self.imports.add(imported)
258
+
259
+ def visit_Import(self, node: ast.Import) -> None: # noqa: N802
260
+ """Extract module name from 'import X' statements."""
261
+ if self._in_type_checking:
262
+ return
263
+
264
+ for alias in node.names:
265
+ self._resolve_absolute(alias.name)
266
+
267
+ def _resolve_absolute(self, module_path: str) -> None:
268
+ """Resolve an absolute import to a sibling module name."""
269
+ parts = module_path.split(".")
270
+ # Must start with our package name
271
+ if parts[0] != self.package_name:
272
+ return
273
+ if len(parts) < 2:
274
+ return
275
+ sibling = parts[1]
276
+ if sibling != self.module_name:
277
+ self.imports.add(sibling)
278
+
279
+ def _resolve_relative(self, module_path: str, level: int) -> None:
280
+ """Resolve a relative import to a sibling module name."""
281
+ if level >= 2:
282
+ # from ..sibling.foo import bar → sibling is the module
283
+ parts = module_path.split(".")
284
+ sibling = parts[0]
285
+ if sibling != self.module_name:
286
+ self.imports.add(sibling)
287
+
288
+ def _is_type_checking(self, test: ast.expr) -> bool:
289
+ """Check if an if-test is TYPE_CHECKING."""
290
+ return (isinstance(test, ast.Name) and test.id == "TYPE_CHECKING") or (
291
+ isinstance(test, ast.Attribute) and test.attr == "TYPE_CHECKING"
292
+ )