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,345 @@
1
+ """Parser for Guardrails from governance documents.
2
+
3
+ Extracts guardrail rules from markdown tables in guardrails.md.
4
+ Supports sections: Code Quality, Testing, Security, Architecture,
5
+ Development Workflow, Inference Economy.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from pathlib import Path
12
+ from typing import Any, cast
13
+
14
+ import yaml
15
+
16
+ from raise_cli.adapters.models import ArtifactLocator, CoreArtifactType
17
+ from raise_cli.compat import portable_path
18
+ from raise_cli.governance.models import Concept, ConceptType
19
+ from raise_cli.governance.parsers._convert import concept_to_node
20
+ from raise_core.graph.models import GraphNode
21
+
22
+
23
+ def _parse_frontmatter(content: str) -> dict[str, Any]:
24
+ """Parse YAML frontmatter from guardrails document.
25
+
26
+ Args:
27
+ content: Full file content.
28
+
29
+ Returns:
30
+ Parsed frontmatter dict, or empty dict if absent/invalid.
31
+ """
32
+ if not content.startswith("---"):
33
+ return {}
34
+
35
+ end = content.find("---", 3)
36
+ if end == -1:
37
+ return {}
38
+
39
+ try:
40
+ result: Any = yaml.safe_load(content[3:end])
41
+ except Exception:
42
+ return {}
43
+
44
+ if not isinstance(result, dict):
45
+ return {}
46
+ return cast(dict[str, Any], result)
47
+
48
+
49
+ def _strip_frontmatter(content: str) -> str:
50
+ """Strip YAML frontmatter from content, returning body only.
51
+
52
+ Args:
53
+ content: Full file content potentially with frontmatter.
54
+
55
+ Returns:
56
+ Content without the frontmatter block.
57
+ """
58
+ if not content.startswith("---"):
59
+ return content
60
+
61
+ end = content.find("---", 3)
62
+ if end == -1:
63
+ return content
64
+
65
+ return content[end + 3 :]
66
+
67
+
68
+ def _extract_prefix(guardrail_id: str) -> str:
69
+ """Extract category prefix from a guardrail ID.
70
+
71
+ Converts ``MUST-ARCH-001`` to ``must-arch`` by lowercasing
72
+ and stripping the trailing ``-NNN`` numeric segment.
73
+
74
+ Args:
75
+ guardrail_id: Raw guardrail ID (e.g., ``MUST-CODE-001``).
76
+
77
+ Returns:
78
+ Lowercase category prefix (e.g., ``must-code``).
79
+ """
80
+ lowered = guardrail_id.lower()
81
+ # Strip last segment (the numeric part)
82
+ parts = lowered.rsplit("-", 1)
83
+ return parts[0]
84
+
85
+
86
+ def _parse_guardrail_table(table_text: str, section: str) -> list[dict[str, Any]]:
87
+ """Parse a markdown table of guardrails.
88
+
89
+ Args:
90
+ table_text: Markdown table text including header and separator rows.
91
+ section: Section name (e.g., "Code Quality", "Testing").
92
+
93
+ Returns:
94
+ List of parsed guardrail dictionaries.
95
+
96
+ Examples:
97
+ >>> table = '''| ID | Level | Guardrail | Verificación |
98
+ ... |----|-------|-----------|--------------|
99
+ ... | `MUST-CODE-001` | MUST | Type hints | pyright |'''
100
+ >>> results = _parse_guardrail_table(table, "Code Quality")
101
+ >>> results[0]["id"]
102
+ 'MUST-CODE-001'
103
+ """
104
+ guardrails: list[dict[str, Any]] = []
105
+
106
+ lines = table_text.strip().split("\n")
107
+ if len(lines) < 3: # Need header, separator, and at least one data row
108
+ return guardrails
109
+
110
+ # Skip header and separator
111
+ for line in lines[2:]:
112
+ if not line.strip() or line.strip().startswith("|-"):
113
+ continue
114
+
115
+ # Parse table row
116
+ cells = [cell.strip() for cell in line.split("|")]
117
+ # Remove empty first/last from | delimiters
118
+ cells = [c for c in cells if c]
119
+
120
+ if len(cells) < 4:
121
+ continue
122
+
123
+ # Extract ID (remove backticks)
124
+ guardrail_id = cells[0].strip("`").strip()
125
+ if not guardrail_id:
126
+ continue
127
+
128
+ # Extract other fields
129
+ level = cells[1].strip() if len(cells) > 1 else ""
130
+ description = cells[2].strip() if len(cells) > 2 else ""
131
+ verification = cells[3].strip("`").strip() if len(cells) > 3 else ""
132
+ derived_from = cells[4].strip() if len(cells) > 4 else ""
133
+
134
+ guardrails.append(
135
+ {
136
+ "id": guardrail_id,
137
+ "level": level,
138
+ "description": description,
139
+ "verification": verification,
140
+ "derived_from": derived_from,
141
+ "section": section,
142
+ }
143
+ )
144
+
145
+ return guardrails
146
+
147
+
148
+ def _find_section_tables(content: str) -> list[tuple[str, str]]:
149
+ """Find all section headers and their associated tables.
150
+
151
+ Args:
152
+ content: Full markdown file content.
153
+
154
+ Returns:
155
+ List of (section_name, table_text) tuples.
156
+
157
+ Examples:
158
+ >>> content = '''### Code Quality
159
+ ... | ID | Level |
160
+ ... |----|-------|
161
+ ... | X | Y |'''
162
+ >>> sections = _find_section_tables(content)
163
+ >>> sections[0][0]
164
+ 'Code Quality'
165
+ """
166
+ sections: list[tuple[str, str]] = []
167
+
168
+ # Pattern to match section headers (### Section Name)
169
+ section_pattern = re.compile(r"^###\s+(.+?)$", re.MULTILINE)
170
+
171
+ # Find all sections
172
+ matches = list(section_pattern.finditer(content))
173
+
174
+ for i, match in enumerate(matches):
175
+ section_name = match.group(1).strip()
176
+ start = match.end()
177
+
178
+ # Find end of section (next ### or end of content)
179
+ end = matches[i + 1].start() if i + 1 < len(matches) else len(content)
180
+
181
+ section_content = content[start:end]
182
+
183
+ # Find table in section (starts with | ID or similar)
184
+ table_match = re.search(
185
+ r"(\|[^\n]+\|\n\|[-|\s]+\|\n(?:\|[^\n]+\|\n?)+)",
186
+ section_content,
187
+ re.MULTILINE,
188
+ )
189
+
190
+ if table_match:
191
+ sections.append((section_name, table_match.group(1)))
192
+
193
+ return sections
194
+
195
+
196
+ def extract_guardrails(
197
+ file_path: Path, project_root: Path | None = None
198
+ ) -> list[Concept]:
199
+ """Extract guardrails from a guardrails markdown file.
200
+
201
+ Parses tables in sections like Code Quality, Testing, Security.
202
+ Each table row becomes a Concept with type GUARDRAIL.
203
+
204
+ Args:
205
+ file_path: Path to guardrails markdown file.
206
+ project_root: Project root for relative path calculation.
207
+
208
+ Returns:
209
+ List of Concept objects representing guardrails.
210
+
211
+ Examples:
212
+ >>> from pathlib import Path
213
+ >>> guardrails = extract_guardrails(Path("governance/guardrails.md"))
214
+ >>> len(guardrails) > 0
215
+ True
216
+ >>> guardrails[0].type
217
+ <ConceptType.GUARDRAIL: 'guardrail'>
218
+ """
219
+ if not file_path.exists():
220
+ return []
221
+
222
+ if project_root is None:
223
+ project_root = file_path.parent.parent # governance/ -> root
224
+
225
+ content = file_path.read_text(encoding="utf-8")
226
+ concepts: list[Concept] = []
227
+
228
+ # Parse frontmatter if present (S15.3 — constraint scopes)
229
+ frontmatter = _parse_frontmatter(content)
230
+ body = _strip_frontmatter(content)
231
+ scopes: dict[str, Any] = frontmatter.get("constraint_scopes", {})
232
+
233
+ # Find all section tables from body (frontmatter stripped)
234
+ section_tables = _find_section_tables(body)
235
+
236
+ # Calculate relative path
237
+ try:
238
+ relative_path = portable_path(file_path, project_root)
239
+ except ValueError:
240
+ relative_path = file_path.name
241
+
242
+ # Track line numbers for each section
243
+ body_lines = body.split("\n")
244
+
245
+ for section_name, table_text in section_tables:
246
+ # Find line number of section header
247
+ section_line = 1
248
+ for i, line in enumerate(body_lines, 1):
249
+ if f"### {section_name}" in line:
250
+ section_line = i
251
+ break
252
+
253
+ # Parse guardrails from table
254
+ parsed = _parse_guardrail_table(table_text, section_name)
255
+
256
+ for guardrail in parsed:
257
+ guardrail_id = guardrail["id"]
258
+ level = guardrail["level"]
259
+ description = guardrail["description"]
260
+
261
+ # Build content string
262
+ content_str = f"[{level}] {description}"
263
+ if guardrail["verification"]:
264
+ content_str += f" — Verify: {guardrail['verification']}"
265
+
266
+ # Truncate if needed
267
+ if len(content_str) > 500:
268
+ content_str = content_str[:500] + "..."
269
+
270
+ metadata: dict[str, Any] = {
271
+ "guardrail_id": guardrail_id,
272
+ "level": level,
273
+ "section": section_name,
274
+ "description": description,
275
+ "verification": guardrail["verification"],
276
+ "derived_from": guardrail["derived_from"],
277
+ }
278
+
279
+ # Tag MUST-level guardrails as always_on (S15.8)
280
+ if level == "MUST":
281
+ metadata["always_on"] = True
282
+
283
+ # Resolve constraint scope from frontmatter (S15.3)
284
+ if scopes:
285
+ prefix = _extract_prefix(guardrail_id)
286
+ overrides: dict[str, Any] = scopes.get("overrides", {})
287
+ scope = overrides.get(prefix, scopes.get("default"))
288
+ if scope is not None:
289
+ metadata["constraint_scope"] = scope
290
+
291
+ concept = Concept(
292
+ id=f"guardrail-{guardrail_id.lower()}",
293
+ type=ConceptType.GUARDRAIL,
294
+ file=relative_path,
295
+ section=f"{section_name}: {guardrail_id}",
296
+ lines=(section_line, section_line + 10), # Approximate
297
+ content=content_str,
298
+ metadata=metadata,
299
+ )
300
+ concepts.append(concept)
301
+
302
+ return concepts
303
+
304
+
305
+ def extract_all_guardrails(project_root: Path | None = None) -> list[Concept]:
306
+ """Extract all guardrails from standard location.
307
+
308
+ Looks for governance/guardrails.md.
309
+
310
+ Args:
311
+ project_root: Project root directory.
312
+
313
+ Returns:
314
+ List of all extracted guardrail Concepts.
315
+
316
+ Examples:
317
+ >>> from pathlib import Path
318
+ >>> guardrails = extract_all_guardrails(Path("."))
319
+ >>> len(guardrails) >= 10
320
+ True
321
+ """
322
+ if project_root is None:
323
+ project_root = Path.cwd()
324
+
325
+ guardrails_file = project_root / "governance" / "guardrails.md"
326
+
327
+ if guardrails_file.exists():
328
+ return extract_guardrails(guardrails_file, project_root)
329
+
330
+ return []
331
+
332
+
333
+ class GuardrailsParser:
334
+ """GovernanceParser wrapper for guardrail rules."""
335
+
336
+ def can_parse(self, locator: ArtifactLocator) -> bool:
337
+ """Match Guardrails artifact type."""
338
+ return locator.artifact_type == CoreArtifactType.GUARDRAILS
339
+
340
+ def parse(self, locator: ArtifactLocator) -> list[GraphNode]:
341
+ """Parse Guardrails file into GraphNode list."""
342
+ root = Path(locator.metadata["project_root"])
343
+ path = root / locator.path
344
+ concepts = extract_guardrails(path, root)
345
+ return [concept_to_node(c) for c in concepts]
@@ -0,0 +1,112 @@
1
+ """Parser for PRD (Product Requirements Document) files.
2
+
3
+ Extracts requirements in RF-XX format from PRD markdown files.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from pathlib import Path
10
+
11
+ from raise_cli.adapters.models import ArtifactLocator, CoreArtifactType
12
+ from raise_cli.compat import portable_path
13
+ from raise_cli.governance.models import Concept, ConceptType
14
+ from raise_cli.governance.parsers._convert import concept_to_node
15
+ from raise_core.graph.models import GraphNode
16
+
17
+
18
+ def extract_requirements(
19
+ file_path: Path, project_root: Path | None = None
20
+ ) -> list[Concept]:
21
+ """Extract RF-XX requirements from PRD markdown file.
22
+
23
+ Parses PRD markdown files looking for requirement sections with the
24
+ format "### RF-XX: Title" and extracts the requirement content along
25
+ with metadata.
26
+
27
+ Args:
28
+ file_path: Path to PRD markdown file.
29
+ project_root: Project root for relative path calculation.
30
+ If None, uses file_path.parent.parent.
31
+
32
+ Returns:
33
+ List of Concept objects representing requirements.
34
+
35
+ Examples:
36
+ >>> from pathlib import Path
37
+ >>> prd = Path("governance/prd.md")
38
+ >>> requirements = extract_requirements(prd)
39
+ >>> len(requirements)
40
+ 8
41
+ >>> requirements[0].id
42
+ 'req-rf-01'
43
+ >>> requirements[0].metadata["requirement_id"]
44
+ 'RF-01'
45
+ """
46
+ if not file_path.exists():
47
+ return []
48
+
49
+ if project_root is None:
50
+ project_root = file_path.parent.parent
51
+
52
+ text = file_path.read_text(encoding="utf-8")
53
+ lines = text.split("\n")
54
+ concepts: list[Concept] = []
55
+
56
+ for i, line in enumerate(lines, 1):
57
+ # Match requirement sections: ### RF-05: Title
58
+ match = re.match(r"^### (RF-\d+):\s*(.+)$", line)
59
+
60
+ if match:
61
+ req_id = match.group(1) # "RF-05"
62
+ title = match.group(2).strip() # "Golden Context Generation"
63
+
64
+ # Extract section content until next ### heading
65
+ content_lines: list[str] = []
66
+ j = i
67
+ while j < len(lines) and not lines[j].startswith("###"):
68
+ content_lines.append(lines[j])
69
+ j += 1
70
+
71
+ # Truncate to first ~20 lines or 500 chars
72
+ content = "\n".join(content_lines[:20])
73
+ if len(content) > 500:
74
+ content = content[:500] + "..."
75
+
76
+ # Generate ID from requirement ID
77
+ concept_id = f"req-{req_id.lower()}"
78
+
79
+ # Calculate relative file path
80
+ try:
81
+ relative_path = portable_path(file_path, project_root)
82
+ except ValueError:
83
+ # If file_path not relative to project_root, use file name
84
+ relative_path = file_path.name
85
+
86
+ concept = Concept(
87
+ id=concept_id,
88
+ type=ConceptType.REQUIREMENT,
89
+ file=relative_path,
90
+ section=f"{req_id}: {title}",
91
+ lines=(i, min(i + len(content_lines[:20]), len(lines))),
92
+ content=content.strip(),
93
+ metadata={"requirement_id": req_id, "title": title},
94
+ )
95
+ concepts.append(concept)
96
+
97
+ return concepts
98
+
99
+
100
+ class PrdParser:
101
+ """GovernanceParser wrapper for PRD requirements."""
102
+
103
+ def can_parse(self, locator: ArtifactLocator) -> bool:
104
+ """Match PRD artifact type."""
105
+ return locator.artifact_type == CoreArtifactType.PRD
106
+
107
+ def parse(self, locator: ArtifactLocator) -> list[GraphNode]:
108
+ """Parse PRD file into GraphNode list."""
109
+ root = Path(locator.metadata["project_root"])
110
+ path = root / locator.path
111
+ concepts = extract_requirements(path, root)
112
+ return [concept_to_node(c) for c in concepts]
@@ -0,0 +1,118 @@
1
+ """Parser for roadmap files.
2
+
3
+ Extracts Release concepts from governance/roadmap.md.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from pathlib import Path
10
+
11
+ from raise_cli.adapters.models import ArtifactLocator, CoreArtifactType
12
+ from raise_cli.compat import portable_path
13
+ from raise_cli.governance.models import Concept, ConceptType
14
+ from raise_cli.governance.parsers._convert import concept_to_node
15
+ from raise_cli.governance.parsers.backlog import normalize_status
16
+ from raise_core.graph.models import GraphNode
17
+
18
+
19
+ def extract_releases(
20
+ file_path: Path, project_root: Path | None = None
21
+ ) -> list[Concept]:
22
+ """Extract Release concepts from roadmap.md Releases table.
23
+
24
+ Parses the "Releases" table to extract release metadata including
25
+ ID, name, target date, status, and associated epics.
26
+
27
+ Args:
28
+ file_path: Path to roadmap.md file.
29
+ project_root: Project root for relative path calculation.
30
+ If None, uses file_path.parent.parent.
31
+
32
+ Returns:
33
+ List of Release Concepts extracted from the table. Returns empty list
34
+ if file doesn't exist or no releases found.
35
+ """
36
+ if not file_path.exists():
37
+ return []
38
+
39
+ if project_root is None:
40
+ project_root = file_path.parent.parent
41
+
42
+ text = file_path.read_text(encoding="utf-8")
43
+ if not text.strip():
44
+ return []
45
+
46
+ lines = text.split("\n")
47
+
48
+ # Calculate relative file path
49
+ try:
50
+ relative_path = portable_path(file_path, project_root)
51
+ except ValueError:
52
+ relative_path = file_path.name
53
+
54
+ concepts: list[Concept] = []
55
+
56
+ # Parse release table rows
57
+ # Pattern: | REL-V2.0 | V2.0 Open Core | 2026-02-15 | In Progress | E18 |
58
+ release_pattern = re.compile(
59
+ r"^\|\s*(REL-[^\s|]+)\s*\|" # Release ID
60
+ r"\s*([^|]+?)\s*\|" # Release name
61
+ r"\s*([^|]+?)\s*\|" # Target date
62
+ r"\s*([^|]+?)\s*\|" # Status
63
+ r"\s*([^|]*?)\s*\|" # Epics (optional)
64
+ )
65
+
66
+ for i, line in enumerate(lines, 1):
67
+ match = release_pattern.match(line)
68
+ if match:
69
+ release_id = match.group(1).strip()
70
+ name = match.group(2).strip()
71
+ target = match.group(3).strip()
72
+ raw_status = match.group(4).strip()
73
+ raw_epics = match.group(5).strip()
74
+
75
+ # Normalize status
76
+ status = normalize_status(raw_status)
77
+
78
+ # Parse epics list
79
+ epics: list[str] = []
80
+ if raw_epics:
81
+ epics = [e.strip() for e in raw_epics.split(",") if e.strip()]
82
+
83
+ # Build content summary
84
+ content = f"{name} — {raw_status}. Target: {target}"
85
+
86
+ concept = Concept(
87
+ id=f"rel-{release_id.removeprefix('REL-').lower()}",
88
+ type=ConceptType.RELEASE,
89
+ file=relative_path,
90
+ section=f"{release_id}: {name}",
91
+ lines=(i, i),
92
+ content=content,
93
+ metadata={
94
+ "release_id": release_id,
95
+ "name": name,
96
+ "target": target,
97
+ "status": status,
98
+ "epics": epics,
99
+ },
100
+ )
101
+ concepts.append(concept)
102
+
103
+ return concepts
104
+
105
+
106
+ class RoadmapParser:
107
+ """GovernanceParser wrapper for Roadmap releases."""
108
+
109
+ def can_parse(self, locator: ArtifactLocator) -> bool:
110
+ """Match Roadmap artifact type."""
111
+ return locator.artifact_type == CoreArtifactType.ROADMAP
112
+
113
+ def parse(self, locator: ArtifactLocator) -> list[GraphNode]:
114
+ """Parse Roadmap file into GraphNode list."""
115
+ root = Path(locator.metadata["project_root"])
116
+ path = root / locator.path
117
+ concepts = extract_releases(path, root)
118
+ return [concept_to_node(c) for c in concepts]
@@ -0,0 +1,116 @@
1
+ """Parser for Vision documents.
2
+
3
+ Extracts outcomes from Vision markdown tables.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from pathlib import Path
10
+
11
+ from raise_cli.adapters.models import ArtifactLocator, CoreArtifactType
12
+ from raise_cli.compat import portable_path
13
+ from raise_cli.core.text import sanitize_id
14
+ from raise_cli.governance.models import Concept, ConceptType
15
+ from raise_cli.governance.parsers._convert import concept_to_node
16
+ from raise_core.graph.models import GraphNode
17
+
18
+
19
+ def extract_outcomes(
20
+ file_path: Path, project_root: Path | None = None
21
+ ) -> list[Concept]:
22
+ """Extract outcomes from Vision markdown tables.
23
+
24
+ Parses Vision markdown files looking for tables with bold outcome names
25
+ in the first column and descriptions in the second column.
26
+
27
+ Args:
28
+ file_path: Path to Vision markdown file.
29
+ project_root: Project root for relative path calculation.
30
+ If None, uses file_path.parent.parent.
31
+
32
+ Returns:
33
+ List of Concept objects representing outcomes.
34
+
35
+ Examples:
36
+ >>> from pathlib import Path
37
+ >>> vision = Path("governance/vision.md")
38
+ >>> outcomes = extract_outcomes(vision)
39
+ >>> len(outcomes)
40
+ 7
41
+ >>> outcomes[0].type
42
+ <ConceptType.OUTCOME: 'outcome'>
43
+ """
44
+ if not file_path.exists():
45
+ return []
46
+
47
+ if project_root is None:
48
+ project_root = file_path.parent.parent
49
+
50
+ text = file_path.read_text(encoding="utf-8")
51
+ lines = text.split("\n")
52
+ concepts: list[Concept] = []
53
+
54
+ in_outcomes_table = False
55
+
56
+ for i, line in enumerate(lines, 1):
57
+ # Detect outcomes table header (only if not already in table)
58
+ if (
59
+ not in_outcomes_table
60
+ and "| **" in line
61
+ and ("outcome" in line.lower() or "context" in line.lower())
62
+ ):
63
+ in_outcomes_table = True
64
+ continue
65
+
66
+ if in_outcomes_table:
67
+ # Parse table row: | **Outcome Name** | Description |
68
+ match = re.match(r"\|\s*\*\*([^*]+)\*\*\s*\|\s*(.+?)\s*\|", line)
69
+
70
+ if match:
71
+ outcome_name = match.group(1).strip()
72
+ description = match.group(2).strip()
73
+
74
+ # Generate ID from name
75
+ outcome_id = sanitize_id(outcome_name)
76
+
77
+ # Calculate relative file path
78
+ try:
79
+ relative_path = portable_path(file_path, project_root)
80
+ except ValueError:
81
+ relative_path = file_path.name
82
+
83
+ concept = Concept(
84
+ id=f"outcome-{outcome_id}",
85
+ type=ConceptType.OUTCOME,
86
+ file=relative_path,
87
+ section=outcome_name,
88
+ lines=(i, i),
89
+ content=description,
90
+ metadata={"outcome_name": outcome_name},
91
+ )
92
+ concepts.append(concept)
93
+
94
+ # End of table: empty line or separator
95
+ if line.strip() == "" or (line.startswith("|") and "---" in line):
96
+ continue
97
+ # Non-table line: exit table mode
98
+ elif not line.startswith("|"):
99
+ in_outcomes_table = False
100
+
101
+ return concepts
102
+
103
+
104
+ class VisionParser:
105
+ """GovernanceParser wrapper for Vision outcomes."""
106
+
107
+ def can_parse(self, locator: ArtifactLocator) -> bool:
108
+ """Match Vision artifact type."""
109
+ return locator.artifact_type == CoreArtifactType.VISION
110
+
111
+ def parse(self, locator: ArtifactLocator) -> list[GraphNode]:
112
+ """Parse Vision file into GraphNode list."""
113
+ root = Path(locator.metadata["project_root"])
114
+ path = root / locator.path
115
+ concepts = extract_outcomes(path, root)
116
+ return [concept_to_node(c) for c in concepts]
@@ -0,0 +1 @@
1
+ """Graph backend implementations for raise-cli."""