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,40 @@
1
+ """Gate dataclasses — context and result types.
2
+
3
+ Frozen dataclasses (not Pydantic) because these are internal infrastructure,
4
+ not boundary objects. Same rationale as hook events (ADR-039 §2).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class GateContext:
15
+ """Context passed to a gate's ``evaluate()`` method.
16
+
17
+ Attributes:
18
+ gate_id: Identifier of the gate being evaluated.
19
+ working_dir: Project working directory. Defaults to ``Path.cwd()``.
20
+ """
21
+
22
+ gate_id: str
23
+ working_dir: Path = field(default_factory=Path.cwd)
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class GateResult:
28
+ """Result returned by a gate's ``evaluate()`` method.
29
+
30
+ Attributes:
31
+ passed: Whether the gate passed validation.
32
+ gate_id: Identifier of the gate that produced this result.
33
+ message: Human-readable summary (actionable for failures).
34
+ details: Additional detail lines (e.g. individual errors).
35
+ """
36
+
37
+ passed: bool
38
+ gate_id: str
39
+ message: str = ""
40
+ details: tuple[str, ...] = ()
@@ -0,0 +1,41 @@
1
+ """WorkflowGate Protocol — contract for gate implementations.
2
+
3
+ Gates guard workflow transitions. They validate and block operations.
4
+ A gate failure prevents the operation with an actionable message.
5
+
6
+ Architecture: ADR-039 §1 (WorkflowGate Protocol)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import ClassVar, Protocol, runtime_checkable
12
+
13
+ from raise_cli.gates.models import GateContext, GateResult
14
+
15
+
16
+ @runtime_checkable
17
+ class WorkflowGate(Protocol):
18
+ """Contract for workflow gate implementations.
19
+
20
+ Attributes:
21
+ gate_id: Unique identifier (e.g. ``"gate-tests"``).
22
+ description: Human-readable purpose (e.g. ``"All tests pass"``).
23
+ workflow_point: When this gate runs (e.g. ``"before:release:publish"``).
24
+
25
+ Example::
26
+
27
+ class TestGate:
28
+ gate_id = "gate-tests"
29
+ description = "All tests pass"
30
+ workflow_point = "before:release:publish"
31
+
32
+ def evaluate(self, context: GateContext) -> GateResult:
33
+ # run pytest, return result...
34
+ return GateResult(passed=True, gate_id=self.gate_id)
35
+ """
36
+
37
+ gate_id: ClassVar[str]
38
+ description: ClassVar[str]
39
+ workflow_point: ClassVar[str]
40
+
41
+ def evaluate(self, context: GateContext) -> GateResult: ...
@@ -0,0 +1,141 @@
1
+ """Gate registry with entry point discovery.
2
+
3
+ Discovers WorkflowGate implementations registered via Python entry points
4
+ (``[project.entry-points."rai.gates"]`` in pyproject.toml). Validates
5
+ Protocol conformance before accepting gates.
6
+
7
+ Architecture: ADR-039 §3 (Entry point discovery, same as RAISE-211)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import inspect
13
+ import logging
14
+ from importlib.metadata import entry_points
15
+ from typing import Any
16
+
17
+ from raise_cli.gates.protocol import WorkflowGate
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ EP_GATES: str = "rai.gates"
22
+
23
+
24
+ def _dist_name(ep: Any) -> str:
25
+ """Best-effort extraction of the distribution name for an entry point."""
26
+ try:
27
+ return ep.dist.name # type: ignore[union-attr]
28
+ except AttributeError:
29
+ return "unknown"
30
+
31
+
32
+ class GateRegistry:
33
+ """Discovers and manages WorkflowGate implementations.
34
+
35
+ Example::
36
+
37
+ registry = GateRegistry()
38
+ registry.discover() # loads from rai.gates entry points
39
+
40
+ for gate in registry.gates:
41
+ print(f"{gate.gate_id}: {gate.description}")
42
+
43
+ gate = registry.get_gate("gate-tests")
44
+ release_gates = registry.get_gates_for_point("before:release:publish")
45
+ """
46
+
47
+ def __init__(self) -> None:
48
+ self._gates: list[WorkflowGate] = []
49
+
50
+ @property
51
+ def gates(self) -> list[WorkflowGate]:
52
+ """Return a copy of registered gates."""
53
+ return list(self._gates)
54
+
55
+ def discover(self) -> None:
56
+ """Load gates from ``rai.gates`` entry points.
57
+
58
+ Skips entry points that:
59
+ - Fail to load (ImportError, etc.)
60
+ - Are not classes
61
+ - Don't conform to the WorkflowGate Protocol
62
+ """
63
+ for ep in entry_points(group=EP_GATES):
64
+ try:
65
+ loaded: Any = ep.load()
66
+ except Exception as exc: # noqa: BLE001
67
+ logger.warning(
68
+ "Skipping gate entry point '%s' from '%s': %s",
69
+ ep.name,
70
+ _dist_name(ep),
71
+ exc,
72
+ )
73
+ continue
74
+
75
+ if not inspect.isclass(loaded):
76
+ logger.warning(
77
+ "Skipping gate entry point '%s' from '%s': expected a class, got %s",
78
+ ep.name,
79
+ _dist_name(ep),
80
+ type(loaded).__name__,
81
+ )
82
+ continue
83
+
84
+ instance = loaded()
85
+ if not isinstance(instance, WorkflowGate):
86
+ logger.warning(
87
+ "Skipping gate entry point '%s' from '%s': "
88
+ "does not conform to WorkflowGate Protocol",
89
+ ep.name,
90
+ _dist_name(ep),
91
+ )
92
+ continue
93
+
94
+ existing = self.get_gate(instance.gate_id)
95
+ if existing is not None:
96
+ logger.warning(
97
+ "Duplicate gate_id '%s' from entry point '%s' — replacing previous",
98
+ instance.gate_id,
99
+ ep.name,
100
+ )
101
+ self._gates = [g for g in self._gates if g.gate_id != instance.gate_id]
102
+ self._gates.append(instance)
103
+ logger.debug(
104
+ "Loaded gate '%s' (id=%s, point=%s)",
105
+ ep.name,
106
+ instance.gate_id,
107
+ instance.workflow_point,
108
+ )
109
+
110
+ def register(self, gate: WorkflowGate | Any) -> None:
111
+ """Manually register a gate instance (useful for testing).
112
+
113
+ Silently skips non-compliant objects. Warns on duplicate gate IDs.
114
+ """
115
+ if not isinstance(gate, WorkflowGate):
116
+ logger.warning(
117
+ "Skipping manual gate registration: %s does not conform to WorkflowGate Protocol",
118
+ type(gate).__name__,
119
+ )
120
+ return
121
+ existing = self.get_gate(gate.gate_id)
122
+ if existing is not None:
123
+ logger.warning(
124
+ "Duplicate gate_id '%s': replacing %s with %s",
125
+ gate.gate_id,
126
+ type(existing).__name__,
127
+ type(gate).__name__,
128
+ )
129
+ self._gates = [g for g in self._gates if g.gate_id != gate.gate_id]
130
+ self._gates.append(gate)
131
+
132
+ def get_gate(self, gate_id: str) -> WorkflowGate | None:
133
+ """Return the gate with the given ID, or ``None``."""
134
+ for gate in self._gates:
135
+ if gate.gate_id == gate_id:
136
+ return gate
137
+ return None
138
+
139
+ def get_gates_for_point(self, workflow_point: str) -> list[WorkflowGate]:
140
+ """Return gates registered for the given workflow point."""
141
+ return [g for g in self._gates if g.workflow_point == workflow_point]
@@ -0,0 +1,11 @@
1
+ """Governance artifact extraction and parsing.
2
+
3
+ This module provides tools to extract semantic concepts from governance
4
+ markdown files (PRD, Vision, Constitution) to build concept-level graphs
5
+ for Minimum Viable Context (MVC) queries.
6
+ """
7
+
8
+ from raise_cli.governance.extractor import GovernanceExtractor
9
+ from raise_cli.governance.models import Concept, ConceptType, ExtractionResult
10
+
11
+ __all__ = ["Concept", "ConceptType", "ExtractionResult", "GovernanceExtractor"]
@@ -0,0 +1,412 @@
1
+ """Governance concept extractor orchestrator.
2
+
3
+ Coordinates multiple parsers to extract concepts from governance files.
4
+ Supports two paths:
5
+ - extract_all() → registry-based, returns list[GraphNode] (new)
6
+ - extract_with_result() → legacy direct imports, returns ExtractionResult (backward compat)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from raise_cli.adapters.models import ArtifactLocator, CoreArtifactType
16
+ from raise_cli.governance.models import Concept, ConceptType, ExtractionResult
17
+
18
+ # Legacy imports — used only by extract_with_result() and extract_from_file()
19
+ from raise_cli.governance.parsers.adr import extract_all_decisions
20
+ from raise_cli.governance.parsers.backlog import extract_epics, extract_project
21
+ from raise_cli.governance.parsers.constitution import extract_principles
22
+ from raise_cli.governance.parsers.epic import extract_epic_details, extract_stories
23
+ from raise_cli.governance.parsers.glossary import extract_all_terms
24
+ from raise_cli.governance.parsers.guardrails import extract_all_guardrails
25
+ from raise_cli.governance.parsers.prd import extract_requirements
26
+ from raise_cli.governance.parsers.roadmap import extract_releases
27
+ from raise_cli.governance.parsers.vision import extract_outcomes
28
+ from raise_core.graph.models import GraphNode
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class GovernanceExtractor:
34
+ """Orchestrates extraction of concepts from governance markdown files.
35
+
36
+ Two extraction paths:
37
+ - extract_all(): Uses registry-discovered parsers, returns list[GraphNode].
38
+ - extract_with_result(): Legacy path with direct imports, returns ExtractionResult
39
+ with list[Concept]. Maintained for backward compat with ``rai memory extract`` CLI.
40
+
41
+ Attributes:
42
+ project_root: Project root directory for relative path calculation.
43
+
44
+ Examples:
45
+ >>> from pathlib import Path
46
+ >>> extractor = GovernanceExtractor()
47
+ >>> nodes = extractor.extract_all()
48
+ >>> len(nodes) >= 20
49
+ True
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ project_root: Path | None = None,
55
+ parsers: dict[str, type] | None = None,
56
+ ) -> None:
57
+ """Initialize the governance extractor.
58
+
59
+ Args:
60
+ project_root: Project root directory. If None, uses current directory.
61
+ parsers: Optional parser classes for DI/testing. If None, discovers
62
+ via entry points at call time.
63
+ """
64
+ self.project_root = project_root or Path.cwd()
65
+ self._parser_classes = parsers
66
+
67
+ # --- New registry path ---
68
+
69
+ def extract_all(self) -> list[GraphNode]:
70
+ """Extract all governance concepts via registry parsers.
71
+
72
+ Builds ArtifactLocators for known artifact locations, discovers
73
+ parsers via entry points, and delegates parsing to matching parsers.
74
+
75
+ Returns:
76
+ List of GraphNode from all governance artifacts.
77
+ """
78
+ locators = self._build_locators()
79
+ parser_classes = self._get_parser_classes()
80
+
81
+ # Instantiate parser classes
82
+ parsers: list[Any] = []
83
+ for name, cls in parser_classes.items():
84
+ try:
85
+ parsers.append(cls())
86
+ except Exception as exc: # noqa: BLE001
87
+ logger.warning("Failed to instantiate parser '%s': %s", name, exc)
88
+
89
+ nodes: list[GraphNode] = []
90
+ for locator in locators:
91
+ parser = self._find_parser(locator, parsers)
92
+ if parser is None:
93
+ logger.debug(
94
+ "No parser found for artifact type '%s' at '%s'",
95
+ locator.artifact_type,
96
+ locator.path,
97
+ )
98
+ continue
99
+ try:
100
+ result = parser.parse(locator)
101
+ nodes.extend(result)
102
+ except Exception as exc: # noqa: BLE001
103
+ logger.warning(
104
+ "Error parsing '%s' with %s: %s",
105
+ locator.path,
106
+ type(parser).__name__,
107
+ exc,
108
+ )
109
+
110
+ return nodes
111
+
112
+ def _build_locators(self) -> list[ArtifactLocator]:
113
+ """Build ArtifactLocators for all known governance artifact locations.
114
+
115
+ Hardcodes paths (same locations as the legacy path). For ADR and
116
+ epic_scope, globs to produce one locator per file.
117
+
118
+ Returns:
119
+ List of ArtifactLocators for existing files.
120
+ """
121
+ root = self.project_root
122
+ meta = {"project_root": str(root)}
123
+ locators: list[ArtifactLocator] = []
124
+
125
+ # Single-file artifacts
126
+ single_files: list[tuple[str, str]] = [
127
+ (CoreArtifactType.PRD, "governance/prd.md"),
128
+ (CoreArtifactType.VISION, "governance/vision.md"),
129
+ (CoreArtifactType.CONSTITUTION, "framework/reference/constitution.md"),
130
+ (CoreArtifactType.ROADMAP, "governance/roadmap.md"),
131
+ (CoreArtifactType.BACKLOG, "governance/backlog.md"),
132
+ (CoreArtifactType.GUARDRAILS, "governance/guardrails.md"),
133
+ (CoreArtifactType.GLOSSARY, "framework/reference/glossary.md"),
134
+ ]
135
+
136
+ for artifact_type, rel_path in single_files:
137
+ if (root / rel_path).exists():
138
+ locators.append(
139
+ ArtifactLocator(
140
+ path=rel_path,
141
+ artifact_type=artifact_type,
142
+ metadata=dict(meta),
143
+ )
144
+ )
145
+
146
+ # ADR files — one locator per file (two directories)
147
+ for adr_dir in ["dev/decisions", "dev/decisions/v2"]:
148
+ full_dir = root / adr_dir
149
+ if full_dir.exists():
150
+ for adr_file in sorted(full_dir.glob("adr-*.md")):
151
+ rel = str(adr_file.relative_to(root))
152
+ locators.append(
153
+ ArtifactLocator(
154
+ path=rel,
155
+ artifact_type=CoreArtifactType.ADR,
156
+ metadata=dict(meta),
157
+ )
158
+ )
159
+
160
+ # Epic scope files — one locator per scope.md
161
+ for scope_file in sorted(root.glob("work/epics/*/scope.md")):
162
+ rel = str(scope_file.relative_to(root))
163
+ locators.append(
164
+ ArtifactLocator(
165
+ path=rel,
166
+ artifact_type=CoreArtifactType.EPIC_SCOPE,
167
+ metadata=dict(meta),
168
+ )
169
+ )
170
+
171
+ return locators
172
+
173
+ def _find_parser(self, locator: ArtifactLocator, parsers: list[Any]) -> Any | None:
174
+ """Find first parser that can_parse this locator."""
175
+ for parser in parsers:
176
+ if parser.can_parse(locator):
177
+ return parser
178
+ return None
179
+
180
+ def _get_parser_classes(self) -> dict[str, type]:
181
+ """Get parser classes from DI or entry point discovery."""
182
+ if self._parser_classes is not None:
183
+ return self._parser_classes
184
+
185
+ from raise_cli.adapters.registry import get_governance_parsers
186
+
187
+ return get_governance_parsers()
188
+
189
+ # --- Legacy path (backward compat) ---
190
+
191
+ def extract_from_file(
192
+ self, file_path: Path, concept_type: ConceptType | None = None
193
+ ) -> list[Concept]:
194
+ """Extract concepts from a single governance file.
195
+
196
+ Args:
197
+ file_path: Path to governance markdown file.
198
+ concept_type: Type of concepts to extract. If None, infers from file path.
199
+
200
+ Returns:
201
+ List of extracted concepts.
202
+
203
+ Raises:
204
+ ValueError: If concept_type is None and cannot be inferred from file path.
205
+ """
206
+ if not file_path.exists():
207
+ logger.warning(f"File not found, skipping: {file_path}")
208
+ return []
209
+
210
+ if concept_type is None:
211
+ concept_type = self._infer_concept_type(file_path)
212
+
213
+ if concept_type == ConceptType.REQUIREMENT:
214
+ return extract_requirements(file_path, self.project_root)
215
+ elif concept_type == ConceptType.OUTCOME:
216
+ return extract_outcomes(file_path, self.project_root)
217
+ elif concept_type == ConceptType.PRINCIPLE:
218
+ return extract_principles(file_path, self.project_root)
219
+ else:
220
+ logger.warning(f"Unsupported concept type: {concept_type}")
221
+ return []
222
+
223
+ def extract_with_result(self) -> ExtractionResult:
224
+ """Extract all concepts and return detailed result.
225
+
226
+ Legacy path: uses direct parser imports, returns ExtractionResult
227
+ with list[Concept]. Maintained for ``rai memory extract`` CLI which
228
+ depends on Concept fields (file, section, lines).
229
+
230
+ Returns:
231
+ ExtractionResult with concepts, counts, and errors.
232
+ """
233
+ concepts: list[Concept] = []
234
+ errors: list[str] = []
235
+ files_processed = 0
236
+
237
+ # Extract from PRD
238
+ prd_file = self.project_root / "governance" / "prd.md"
239
+ if prd_file.exists():
240
+ try:
241
+ prd_concepts = extract_requirements(prd_file, self.project_root)
242
+ concepts.extend(prd_concepts)
243
+ files_processed += 1
244
+ except Exception as e:
245
+ errors.append(f"Error extracting from {prd_file}: {e}")
246
+
247
+ # Extract from Vision
248
+ vision_file = self.project_root / "governance" / "vision.md"
249
+ if vision_file.exists():
250
+ try:
251
+ vision_concepts = extract_outcomes(vision_file, self.project_root)
252
+ concepts.extend(vision_concepts)
253
+ files_processed += 1
254
+ except Exception as e:
255
+ errors.append(f"Error extracting from {vision_file}: {e}")
256
+
257
+ # Extract from Constitution
258
+ constitution_file = (
259
+ self.project_root / "framework" / "reference" / "constitution.md"
260
+ )
261
+ if constitution_file.exists():
262
+ try:
263
+ constitution_concepts = extract_principles(
264
+ constitution_file, self.project_root
265
+ )
266
+ concepts.extend(constitution_concepts)
267
+ files_processed += 1
268
+ except Exception as e:
269
+ errors.append(f"Error extracting from {constitution_file}: {e}")
270
+
271
+ # Extract work concepts (E8)
272
+ work_concepts = self._extract_work_concepts()
273
+ concepts.extend(work_concepts)
274
+ backlog_file = self.project_root / "governance" / "backlog.md"
275
+ backlog_count = 1 if backlog_file.exists() else 0
276
+ epic_count = len(list(self.project_root.glob("work/epics/*/scope.md")))
277
+ files_processed += backlog_count + epic_count
278
+
279
+ # Extract ADR decisions (E12)
280
+ try:
281
+ adr_concepts = extract_all_decisions(self.project_root)
282
+ concepts.extend(adr_concepts)
283
+ adr_root_count = len(list(self.project_root.glob("dev/decisions/adr-*.md")))
284
+ adr_v2_count = len(
285
+ list(self.project_root.glob("dev/decisions/v2/adr-*.md"))
286
+ )
287
+ files_processed += adr_root_count + adr_v2_count
288
+ except Exception as e:
289
+ errors.append(f"Error extracting ADRs: {e}")
290
+
291
+ # Extract Guardrails (E12 F12.2)
292
+ guardrails_file = self.project_root / "governance" / "guardrails.md"
293
+ if guardrails_file.exists():
294
+ try:
295
+ guardrail_concepts = extract_all_guardrails(self.project_root)
296
+ concepts.extend(guardrail_concepts)
297
+ files_processed += 1
298
+ except Exception as e:
299
+ errors.append(f"Error extracting guardrails: {e}")
300
+
301
+ # Extract Glossary terms (E12 F12.3)
302
+ glossary_file = self.project_root / "framework" / "reference" / "glossary.md"
303
+ if glossary_file.exists():
304
+ try:
305
+ term_concepts = extract_all_terms(self.project_root)
306
+ concepts.extend(term_concepts)
307
+ files_processed += 1
308
+ except Exception as e:
309
+ errors.append(f"Error extracting glossary terms: {e}")
310
+
311
+ # Extract Releases from roadmap
312
+ roadmap_file = self.project_root / "governance" / "roadmap.md"
313
+ if roadmap_file.exists():
314
+ try:
315
+ release_concepts = extract_releases(roadmap_file, self.project_root)
316
+ concepts.extend(release_concepts)
317
+ files_processed += 1
318
+ except Exception as e:
319
+ errors.append(f"Error extracting from {roadmap_file}: {e}")
320
+
321
+ return ExtractionResult(
322
+ concepts=concepts,
323
+ total=len(concepts),
324
+ files_processed=files_processed,
325
+ errors=errors,
326
+ )
327
+
328
+ def _extract_work_concepts(self) -> list[Concept]:
329
+ """Extract work tracking concepts (Project, Epic, Story).
330
+
331
+ Extracts from:
332
+ - governance/backlog.md (Project + Epic index)
333
+ - work/epics/*/scope.md (Epic details + Stories)
334
+
335
+ Returns:
336
+ List of work tracking concepts.
337
+ """
338
+ concepts: list[Concept] = []
339
+
340
+ backlog_file = self.project_root / "governance" / "backlog.md"
341
+ if backlog_file.exists():
342
+ try:
343
+ project = extract_project(backlog_file, self.project_root)
344
+ if project:
345
+ concepts.append(project)
346
+ logger.info(f"Extracted project from {backlog_file.name}")
347
+
348
+ epic_index = extract_epics(backlog_file, self.project_root)
349
+ concepts.extend(epic_index)
350
+ logger.info(
351
+ f"Extracted {len(epic_index)} epics from {backlog_file.name}"
352
+ )
353
+ except Exception as e:
354
+ logger.error(f"Error extracting from {backlog_file}: {e}")
355
+
356
+ for scope_file in self.project_root.glob("work/epics/*/scope.md"):
357
+ try:
358
+ epic_detail = extract_epic_details(scope_file, self.project_root)
359
+ if epic_detail:
360
+ existing = next(
361
+ (c for c in concepts if c.id == epic_detail.id), None
362
+ )
363
+ if existing:
364
+ existing.metadata.update(epic_detail.metadata)
365
+ existing.content = epic_detail.content
366
+ else:
367
+ concepts.append(epic_detail)
368
+ logger.info(f"Extracted epic details from {scope_file.name}")
369
+
370
+ stories = extract_stories(scope_file, self.project_root)
371
+ concepts.extend(stories)
372
+ logger.info(f"Extracted {len(stories)} stories from {scope_file.name}")
373
+ except Exception as e:
374
+ logger.error(f"Error extracting from {scope_file}: {e}")
375
+
376
+ return concepts
377
+
378
+ def _infer_concept_type(self, file_path: Path) -> ConceptType:
379
+ """Infer concept type from file path.
380
+
381
+ Args:
382
+ file_path: Path to governance file.
383
+
384
+ Returns:
385
+ Inferred concept type.
386
+
387
+ Raises:
388
+ ValueError: If concept type cannot be inferred.
389
+ """
390
+ file_name = file_path.name.lower()
391
+
392
+ if "prd" in file_name or "requirements" in file_name:
393
+ return ConceptType.REQUIREMENT
394
+ elif "vision" in file_name:
395
+ return ConceptType.OUTCOME
396
+ elif "constitution" in file_name:
397
+ return ConceptType.PRINCIPLE
398
+ elif "backlog" in file_name:
399
+ return ConceptType.PROJECT
400
+ elif "epic" in file_name and "scope" in file_name:
401
+ return ConceptType.EPIC
402
+ elif "guardrails" in file_name:
403
+ return ConceptType.GUARDRAIL
404
+ elif "glossary" in file_name:
405
+ return ConceptType.TERM
406
+ elif "roadmap" in file_name:
407
+ return ConceptType.RELEASE
408
+ else:
409
+ raise ValueError(
410
+ f"Cannot infer concept type from file path: {file_path}. "
411
+ f"Specify concept_type explicitly."
412
+ )