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,213 @@
1
+ """Graph diff engine for detecting changes between unified graph builds.
2
+
3
+ Compares two Graph instances by node presence and semantic fields
4
+ (content, type, metadata). Produces a structured GraphDiff with impact
5
+ classification and affected module list.
6
+
7
+ Architecture: E16 Incremental Coherence — Layer 1 (deterministic)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Literal
13
+
14
+ from pydantic import BaseModel, Field
15
+
16
+ from raise_core.graph.engine import Graph
17
+ from raise_core.graph.models import GraphNode, NodeType
18
+
19
+ # Node types that indicate module-level impact
20
+ _MODULE_IMPACT_TYPES: frozenset[NodeType] = frozenset({"module", "component"})
21
+
22
+ # Node types that indicate architectural impact
23
+ _ARCHITECTURAL_IMPACT_TYPES: frozenset[NodeType] = frozenset(
24
+ {"bounded_context", "layer"}
25
+ )
26
+
27
+ # Fields compared for modification detection
28
+ _COMPARED_FIELDS: tuple[str, ...] = ("content", "type", "metadata")
29
+
30
+
31
+ class NodeChange(BaseModel):
32
+ """A change to a single node between two graph builds.
33
+
34
+ Attributes:
35
+ node_id: The unique node identifier.
36
+ change_type: Whether the node was added, removed, or modified.
37
+ old_value: The node in the old graph (None for added).
38
+ new_value: The node in the new graph (None for removed).
39
+ changed_fields: Which semantic fields changed (for modified nodes).
40
+ """
41
+
42
+ node_id: str
43
+ change_type: Literal["added", "removed", "modified"]
44
+ old_value: GraphNode | None = None
45
+ new_value: GraphNode | None = None
46
+ changed_fields: list[str] = Field(default_factory=list)
47
+
48
+
49
+ class GraphDiff(BaseModel):
50
+ """Structured diff between two unified graph builds.
51
+
52
+ Attributes:
53
+ node_changes: List of individual node changes.
54
+ impact: Overall impact level for downstream consumers.
55
+ affected_modules: Module node IDs that changed (sorted).
56
+ summary: Deterministic human-readable summary string.
57
+ """
58
+
59
+ node_changes: list[NodeChange] = Field(default_factory=lambda: [])
60
+ impact: Literal["none", "module", "architectural"] = "none"
61
+ affected_modules: list[str] = Field(default_factory=lambda: [])
62
+ summary: str = "no changes"
63
+
64
+
65
+ def diff_graphs(old: Graph, new: Graph) -> GraphDiff:
66
+ """Compare two unified graphs and return structured diff.
67
+
68
+ Compares nodes by presence (added/removed) and by semantic fields
69
+ (content, type, metadata) for modification detection. Ignores
70
+ created and source_file fields.
71
+
72
+ Args:
73
+ old: The previous graph (before build).
74
+ new: The current graph (after build).
75
+
76
+ Returns:
77
+ GraphDiff with node changes, impact classification, and affected modules.
78
+ """
79
+ old_ids = {node.id for node in old.iter_concepts()}
80
+ new_ids = {node.id for node in new.iter_concepts()}
81
+
82
+ changes: list[NodeChange] = []
83
+
84
+ # Added nodes
85
+ for node_id in sorted(new_ids - old_ids):
86
+ node = new.get_concept(node_id)
87
+ changes.append(
88
+ NodeChange(
89
+ node_id=node_id,
90
+ change_type="added",
91
+ old_value=None,
92
+ new_value=node,
93
+ changed_fields=[],
94
+ )
95
+ )
96
+
97
+ # Removed nodes
98
+ for node_id in sorted(old_ids - new_ids):
99
+ node = old.get_concept(node_id)
100
+ changes.append(
101
+ NodeChange(
102
+ node_id=node_id,
103
+ change_type="removed",
104
+ old_value=node,
105
+ new_value=None,
106
+ changed_fields=[],
107
+ )
108
+ )
109
+
110
+ # Modified nodes
111
+ for node_id in sorted(old_ids & new_ids):
112
+ old_node = old.get_concept(node_id)
113
+ new_node = new.get_concept(node_id)
114
+ if old_node is None or new_node is None:
115
+ continue
116
+
117
+ changed_fields = _compare_nodes(old_node, new_node)
118
+ if changed_fields:
119
+ changes.append(
120
+ NodeChange(
121
+ node_id=node_id,
122
+ change_type="modified",
123
+ old_value=old_node,
124
+ new_value=new_node,
125
+ changed_fields=changed_fields,
126
+ )
127
+ )
128
+
129
+ if not changes:
130
+ return GraphDiff()
131
+
132
+ impact = _classify_impact(changes)
133
+ affected_modules = _derive_affected_modules(changes)
134
+ summary = _build_summary(changes, affected_modules)
135
+
136
+ return GraphDiff(
137
+ node_changes=changes,
138
+ impact=impact,
139
+ affected_modules=affected_modules,
140
+ summary=summary,
141
+ )
142
+
143
+
144
+ def _compare_nodes(old: GraphNode, new: GraphNode) -> list[str]:
145
+ """Compare two nodes on semantic fields only.
146
+
147
+ Returns list of field names that differ. Ignores created and source_file.
148
+ """
149
+ changed: list[str] = []
150
+ for field in _COMPARED_FIELDS:
151
+ old_val = getattr(old, field)
152
+ new_val = getattr(new, field)
153
+ if old_val != new_val:
154
+ changed.append(field)
155
+ return changed
156
+
157
+
158
+ def _classify_impact(
159
+ changes: list[NodeChange],
160
+ ) -> Literal["none", "module", "architectural"]:
161
+ """Classify overall impact from node changes.
162
+
163
+ architectural > module > none.
164
+ """
165
+ has_module = False
166
+
167
+ for change in changes:
168
+ node = change.new_value or change.old_value
169
+ if node is None:
170
+ continue
171
+ if node.type in _ARCHITECTURAL_IMPACT_TYPES:
172
+ return "architectural"
173
+ if node.type in _MODULE_IMPACT_TYPES:
174
+ has_module = True
175
+
176
+ return "module" if has_module else "none"
177
+
178
+
179
+ def _derive_affected_modules(changes: list[NodeChange]) -> list[str]:
180
+ """Extract sorted list of module IDs from changed nodes."""
181
+ modules: list[str] = []
182
+ for change in changes:
183
+ node = change.new_value or change.old_value
184
+ if node is not None and node.type == "module":
185
+ modules.append(change.node_id)
186
+ return sorted(modules)
187
+
188
+
189
+ def _build_summary(changes: list[NodeChange], affected_modules: list[str]) -> str:
190
+ """Build deterministic summary string from changes."""
191
+ added = sum(1 for c in changes if c.change_type == "added")
192
+ removed = sum(1 for c in changes if c.change_type == "removed")
193
+ modified = sum(1 for c in changes if c.change_type == "modified")
194
+
195
+ total = len(changes)
196
+ parts: list[str] = []
197
+ if added:
198
+ parts.append(f"{added} added")
199
+ if removed:
200
+ parts.append(f"{removed} removed")
201
+ if modified:
202
+ parts.append(f"{modified} modified")
203
+
204
+ detail = ", ".join(parts)
205
+ summary = f"{total} nodes changed ({detail})"
206
+
207
+ if affected_modules:
208
+ mod_list = ", ".join(affected_modules)
209
+ count = len(affected_modules)
210
+ label = "module" if count == 1 else "modules"
211
+ summary += f", {count} {label} affected ({mod_list})"
212
+
213
+ return summary
@@ -0,0 +1,13 @@
1
+ """Context extractors for unified graph building."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from raise_cli.context.extractors.skills import (
6
+ extract_all_skills,
7
+ extract_skill_metadata,
8
+ )
9
+
10
+ __all__ = [
11
+ "extract_all_skills",
12
+ "extract_skill_metadata",
13
+ ]
@@ -0,0 +1,121 @@
1
+ """Skill metadata extraction from SKILL.md frontmatter.
2
+
3
+ This module extracts skill metadata from YAML frontmatter in SKILL.md files
4
+ and converts them to GraphNode for the unified context graph.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from datetime import UTC, datetime
11
+ from pathlib import Path
12
+ from typing import Any, cast
13
+
14
+ import yaml
15
+
16
+ from raise_core.graph.models import GraphNode
17
+
18
+ # Regex to match YAML frontmatter between --- markers
19
+ FRONTMATTER_PATTERN = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL)
20
+
21
+
22
+ def extract_skill_metadata(skill_path: Path) -> GraphNode | None:
23
+ """Extract metadata from SKILL.md YAML frontmatter.
24
+
25
+ Args:
26
+ skill_path: Path to SKILL.md file.
27
+
28
+ Returns:
29
+ GraphNode for the skill, or None if parsing fails or file doesn't exist.
30
+
31
+ Examples:
32
+ >>> node = extract_skill_metadata(Path(".claude/skills/rai-story-plan/SKILL.md"))
33
+ >>> node.id if node else None
34
+ '/rai-story-plan'
35
+ """
36
+ if not skill_path.exists():
37
+ return None
38
+
39
+ try:
40
+ content = skill_path.read_text(encoding="utf-8")
41
+ except OSError:
42
+ return None
43
+
44
+ # Extract YAML frontmatter
45
+ match = FRONTMATTER_PATTERN.match(content)
46
+ if not match:
47
+ return None
48
+
49
+ try:
50
+ frontmatter = yaml.safe_load(match.group(1))
51
+ except yaml.YAMLError:
52
+ return None
53
+
54
+ if not isinstance(frontmatter, dict):
55
+ return None
56
+
57
+ # Cast to typed dict for pyright
58
+ fm: dict[str, Any] = cast(dict[str, Any], frontmatter)
59
+
60
+ # Name is required
61
+ name_value = fm.get("name")
62
+ if not name_value:
63
+ return None
64
+ name: str = str(name_value)
65
+
66
+ # Extract fields
67
+ desc_value = fm.get("description", "")
68
+ description: str = ""
69
+ if isinstance(desc_value, str):
70
+ # Clean up multiline YAML strings
71
+ description = " ".join(desc_value.split())
72
+
73
+ meta_value = fm.get("metadata", {})
74
+ metadata_section: dict[str, Any] = {}
75
+ if isinstance(meta_value, dict):
76
+ metadata_section = cast(dict[str, Any], meta_value)
77
+
78
+ # Get file modification time for created timestamp
79
+ try:
80
+ mtime = skill_path.stat().st_mtime
81
+ created = datetime.fromtimestamp(mtime, tz=UTC).isoformat()
82
+ except OSError:
83
+ created = datetime.now(tz=UTC).isoformat()
84
+
85
+ return GraphNode(
86
+ id=f"/{name}",
87
+ type="skill",
88
+ content=description,
89
+ source_file=str(skill_path),
90
+ created=created,
91
+ metadata=metadata_section,
92
+ )
93
+
94
+
95
+ def extract_all_skills(skills_dir: Path) -> list[GraphNode]:
96
+ """Extract metadata from all skills in a directory.
97
+
98
+ Searches for SKILL.md files in subdirectories of the given path.
99
+
100
+ Args:
101
+ skills_dir: Path to skills directory (e.g., .claude/skills/).
102
+
103
+ Returns:
104
+ List of GraphNode for each valid skill found.
105
+
106
+ Examples:
107
+ >>> nodes = extract_all_skills(Path(".claude/skills"))
108
+ >>> len(nodes) > 0
109
+ True
110
+ """
111
+ if not skills_dir.exists():
112
+ return []
113
+
114
+ nodes: list[GraphNode] = []
115
+
116
+ for skill_md in skills_dir.glob("*/SKILL.md"):
117
+ node = extract_skill_metadata(skill_md)
118
+ if node is not None:
119
+ nodes.append(node)
120
+
121
+ return nodes
@@ -0,0 +1,37 @@
1
+ """Core utilities for raise-cli.
2
+
3
+ This package provides:
4
+ - tools: Subprocess wrappers for git, ast-grep, ripgrep
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from raise_cli.core.tools import (
10
+ GitStatus,
11
+ SearchMatch,
12
+ ToolResult,
13
+ check_tool,
14
+ git_branch,
15
+ git_diff,
16
+ git_root,
17
+ git_status,
18
+ require_tool,
19
+ rg_search,
20
+ run_tool,
21
+ sg_search,
22
+ )
23
+
24
+ __all__ = [
25
+ "ToolResult",
26
+ "GitStatus",
27
+ "SearchMatch",
28
+ "check_tool",
29
+ "require_tool",
30
+ "run_tool",
31
+ "git_root",
32
+ "git_branch",
33
+ "git_status",
34
+ "git_diff",
35
+ "rg_search",
36
+ "sg_search",
37
+ ]
@@ -0,0 +1,66 @@
1
+ """File and directory utilities.
2
+
3
+ Shared functions for file system operations used across the codebase.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+
10
+ # Directories to exclude from scanning operations
11
+ EXCLUDED_DIRS: frozenset[str] = frozenset(
12
+ {
13
+ # Version control
14
+ ".git",
15
+ ".svn",
16
+ ".hg",
17
+ # Package managers
18
+ "node_modules",
19
+ # Python
20
+ "__pycache__",
21
+ ".tox",
22
+ ".nox",
23
+ ".mypy_cache",
24
+ ".pytest_cache",
25
+ ".ruff_cache",
26
+ "venv",
27
+ ".venv",
28
+ "env",
29
+ ".env",
30
+ # Build outputs
31
+ "dist",
32
+ "build",
33
+ "target",
34
+ # IDE
35
+ ".idea",
36
+ ".vscode",
37
+ }
38
+ )
39
+
40
+
41
+ def should_exclude_dir(dir_path: Path) -> bool:
42
+ """Check if a directory should be excluded from scanning.
43
+
44
+ Excludes hidden directories (starting with .) and known non-project
45
+ directories like node_modules, __pycache__, etc.
46
+
47
+ Args:
48
+ dir_path: Path to check.
49
+
50
+ Returns:
51
+ True if the directory should be excluded.
52
+
53
+ Examples:
54
+ >>> should_exclude_dir(Path("node_modules"))
55
+ True
56
+ >>> should_exclude_dir(Path(".git"))
57
+ True
58
+ >>> should_exclude_dir(Path("src"))
59
+ False
60
+ """
61
+ name = dir_path.name
62
+ # Exclude hidden directories (starting with .)
63
+ if name.startswith("."):
64
+ return True
65
+ # Exclude known non-project directories
66
+ return name in EXCLUDED_DIRS
raise_cli/core/text.py ADDED
@@ -0,0 +1,174 @@
1
+ """Text processing utilities.
2
+
3
+ Shared functions for text manipulation used across the codebase.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+
10
+ # Comprehensive English stopwords for keyword extraction
11
+ STOPWORDS: frozenset[str] = frozenset(
12
+ {
13
+ # Articles
14
+ "the",
15
+ "a",
16
+ "an",
17
+ # Conjunctions
18
+ "and",
19
+ "but",
20
+ "or",
21
+ "nor",
22
+ "so",
23
+ "yet",
24
+ "both",
25
+ "either",
26
+ "neither",
27
+ # Prepositions
28
+ "in",
29
+ "on",
30
+ "at",
31
+ "to",
32
+ "for",
33
+ "of",
34
+ "with",
35
+ "from",
36
+ "by",
37
+ "as",
38
+ "into",
39
+ "through",
40
+ "during",
41
+ "before",
42
+ "after",
43
+ "above",
44
+ "below",
45
+ "between",
46
+ "under",
47
+ # Demonstratives and pronouns
48
+ "this",
49
+ "that",
50
+ "these",
51
+ "those",
52
+ "it",
53
+ "its",
54
+ # Be verbs
55
+ "is",
56
+ "are",
57
+ "was",
58
+ "were",
59
+ "be",
60
+ "been",
61
+ "being",
62
+ # Have verbs
63
+ "have",
64
+ "has",
65
+ "had",
66
+ # Do verbs
67
+ "do",
68
+ "does",
69
+ "did",
70
+ # Modal verbs
71
+ "will",
72
+ "would",
73
+ "could",
74
+ "should",
75
+ "may",
76
+ "might",
77
+ "must",
78
+ "shall",
79
+ "can",
80
+ "need",
81
+ "dare",
82
+ "ought",
83
+ "used",
84
+ # Adverbs
85
+ "again",
86
+ "further",
87
+ "then",
88
+ "once",
89
+ "not",
90
+ "only",
91
+ "own",
92
+ "same",
93
+ "than",
94
+ "too",
95
+ "very",
96
+ "just",
97
+ "also",
98
+ "now",
99
+ "here",
100
+ "there",
101
+ "when",
102
+ "where",
103
+ "why",
104
+ "how",
105
+ # Quantifiers
106
+ "all",
107
+ "each",
108
+ "every",
109
+ "few",
110
+ "more",
111
+ "most",
112
+ "other",
113
+ "some",
114
+ "such",
115
+ "no",
116
+ "any",
117
+ }
118
+ )
119
+
120
+
121
+ def extract_keywords(text: str) -> set[str]:
122
+ """Extract meaningful keywords from text.
123
+
124
+ Filters out stopwords and keeps only words longer than 3 characters.
125
+
126
+ Args:
127
+ text: Text to extract keywords from.
128
+
129
+ Returns:
130
+ Set of lowercase keywords.
131
+
132
+ Examples:
133
+ >>> keywords = extract_keywords("The system MUST validate inputs")
134
+ >>> "system" in keywords
135
+ True
136
+ >>> "the" in keywords
137
+ False
138
+ """
139
+ words = re.findall(r"\b\w+\b", text.lower())
140
+ return {w for w in words if len(w) > 3 and w not in STOPWORDS}
141
+
142
+
143
+ def sanitize_id(name: str) -> str:
144
+ """Sanitize a name for use as an ID.
145
+
146
+ Converts a human-readable name to a lowercase, hyphen-separated
147
+ identifier suitable for use in IDs and keys.
148
+
149
+ Args:
150
+ name: Human-readable name to sanitize.
151
+
152
+ Returns:
153
+ Sanitized ID string (lowercase, hyphens, alphanumeric only).
154
+
155
+ Examples:
156
+ >>> sanitize_id("Context Generation (MVC)")
157
+ 'context-generation-mvc'
158
+ >>> sanitize_id("Governance as Code")
159
+ 'governance-as-code'
160
+ >>> sanitize_id("Hello, World!")
161
+ 'hello-world'
162
+ """
163
+ # Convert to lowercase
164
+ sanitized = name.lower()
165
+ # Replace spaces with hyphens
166
+ sanitized = sanitized.replace(" ", "-")
167
+ # Remove all non-alphanumeric characters except hyphens
168
+ sanitized = re.sub(r"[^a-z0-9-]", "", sanitized)
169
+ # Collapse multiple hyphens into one
170
+ sanitized = re.sub(r"-+", "-", sanitized)
171
+ # Remove leading/trailing hyphens
172
+ sanitized = sanitized.strip("-")
173
+
174
+ return sanitized