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,323 @@
1
+ """Parser for epic scope documents.
2
+
3
+ Extracts detailed Epic and Story concepts from work/epics/*/scope.md 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_cli.governance.parsers.backlog import normalize_status
16
+ from raise_core.graph.models import GraphNode
17
+
18
+
19
+ def _extract_frontmatter(text: str) -> dict[str, str | None]:
20
+ """Extract frontmatter metadata from epic scope document.
21
+
22
+ Parses blockquote lines like:
23
+ > **Status:** COMPLETE
24
+ > **Target:** Feb 9, 2026
25
+
26
+ Args:
27
+ text: Full epic scope document content.
28
+
29
+ Returns:
30
+ Dictionary with extracted metadata (status, target, branch, etc.).
31
+ """
32
+ metadata: dict[str, str | None] = {
33
+ "status": None,
34
+ "target": None,
35
+ "branch": None,
36
+ "created": None,
37
+ "completed": None,
38
+ }
39
+
40
+ # Pattern: > **Key:** Value or > Key: Value
41
+ # Note: Markdown bold is **Key:** with colon inside, so pattern is :**
42
+ frontmatter_pattern = re.compile(
43
+ r"^>\s*\*?\*?([^:*]+?):\*?\*?\s*(.+)$", re.MULTILINE
44
+ )
45
+
46
+ for match in frontmatter_pattern.finditer(text):
47
+ key = match.group(1).strip().lower()
48
+ value = match.group(2).strip()
49
+
50
+ if key == "status":
51
+ # Remove emoji and extra text after status
52
+ status_match = re.match(r"^([A-Z]+)", value)
53
+ if status_match:
54
+ metadata["status"] = status_match.group(1).lower()
55
+ else:
56
+ metadata["status"] = normalize_status(value)
57
+ elif key == "target":
58
+ metadata["target"] = value
59
+ elif key == "branch":
60
+ metadata["branch"] = value
61
+ elif key == "created":
62
+ metadata["created"] = value
63
+ elif key == "completed":
64
+ metadata["completed"] = value
65
+
66
+ return metadata
67
+
68
+
69
+ def extract_epic_details(
70
+ file_path: Path, project_root: Path | None = None
71
+ ) -> Concept | None:
72
+ """Extract detailed Epic concept from epic scope document.
73
+
74
+ Parses the epic scope document to extract full epic metadata including
75
+ objective, status, target date, and story count.
76
+
77
+ Args:
78
+ file_path: Path to epic scope document (work/epics/*/scope.md).
79
+ project_root: Project root for relative path calculation.
80
+ If None, uses file_path.parent.parent.parent.
81
+
82
+ Returns:
83
+ Epic Concept with full details if successfully parsed, None if file
84
+ doesn't exist or can't be parsed.
85
+
86
+ Examples:
87
+ >>> from pathlib import Path
88
+ >>> scope_doc = Path("work/epics/e08-backlog/scope.md")
89
+ >>> epic = extract_epic_details(scope_doc)
90
+ >>> epic.id
91
+ 'epic-e8'
92
+ >>> epic.metadata["name"]
93
+ 'Work Tracking Graph'
94
+ """
95
+ if not file_path.exists():
96
+ return None
97
+
98
+ if project_root is None:
99
+ # work/epics/e08-name/scope.md -> project root is 4 levels up
100
+ project_root = file_path.parent.parent.parent.parent
101
+
102
+ text = file_path.read_text(encoding="utf-8")
103
+ lines = text.split("\n")
104
+
105
+ # Extract epic ID from parent directory: e08-backlog -> E8
106
+ parent_dir = file_path.parent.name # e08-backlog
107
+ epic_id_match = re.search(r"^e(\d+)", parent_dir, re.IGNORECASE)
108
+ if not epic_id_match:
109
+ return None
110
+
111
+ epic_id = f"E{int(epic_id_match.group(1))}" # E8 (normalize: e08 -> E8)
112
+
113
+ # Extract epic name from H1: # Epic E8: Work Tracking Graph - Scope
114
+ epic_name = None
115
+ header_line = 1
116
+ for i, line in enumerate(lines, 1):
117
+ match = re.match(r"^#\s*Epic\s+E\d+[:\s]+(.+?)(?:\s*-\s*Scope)?$", line)
118
+ if match:
119
+ epic_name = match.group(1).strip()
120
+ header_line = i
121
+ break
122
+
123
+ if not epic_name:
124
+ # Fallback: use epic ID
125
+ epic_name = f"Epic {epic_id}"
126
+
127
+ # Extract frontmatter
128
+ frontmatter = _extract_frontmatter(text)
129
+
130
+ # Count stories in table
131
+ story_count = len(re.findall(r"^\|\s*F\d+\.\d+\s*\|", text, re.MULTILINE))
132
+
133
+ # Calculate relative file path
134
+ try:
135
+ relative_path = portable_path(file_path, project_root)
136
+ except ValueError:
137
+ relative_path = file_path.name
138
+
139
+ # Extract objective (first paragraph after ## Objective)
140
+ objective = None
141
+ objective_match = re.search(
142
+ r"##\s*Objective\s*\n+(.+?)(?=\n\n|\n##|\Z)", text, re.DOTALL
143
+ )
144
+ if objective_match:
145
+ objective = objective_match.group(1).strip()
146
+ # Truncate if too long
147
+ if len(objective) > 300:
148
+ objective = objective[:297] + "..."
149
+
150
+ # Build content summary
151
+ content = f"Epic {epic_id}: {epic_name}"
152
+ if frontmatter["status"]:
153
+ content += f" ({frontmatter['status']})"
154
+ if objective:
155
+ content += f". {objective}"
156
+
157
+ return Concept(
158
+ id=f"epic-{epic_id.lower()}",
159
+ type=ConceptType.EPIC,
160
+ file=relative_path,
161
+ section=f"Epic {epic_id}: {epic_name}",
162
+ lines=(header_line, min(header_line + 20, len(lines))),
163
+ content=content[:500], # Truncate to 500 chars
164
+ metadata={
165
+ "epic_id": epic_id,
166
+ "name": epic_name,
167
+ "status": frontmatter["status"] or "draft",
168
+ "target": frontmatter["target"],
169
+ "branch": frontmatter["branch"],
170
+ "created": frontmatter["created"],
171
+ "completed": frontmatter["completed"],
172
+ "story_count": story_count,
173
+ "scope_doc": relative_path,
174
+ },
175
+ )
176
+
177
+
178
+ def extract_stories(file_path: Path, project_root: Path | None = None) -> list[Concept]:
179
+ """Extract Story concepts from epic scope document.
180
+
181
+ Parses the "Stories" table to extract story metadata. Supports
182
+ various table formats found in epic scope documents.
183
+
184
+ Args:
185
+ file_path: Path to epic scope document (work/epics/*/scope.md).
186
+ project_root: Project root for relative path calculation.
187
+ If None, uses file_path.parent.parent.parent.parent.
188
+
189
+ Returns:
190
+ List of Story Concepts extracted from the table. Returns empty list
191
+ if file doesn't exist or no stories found.
192
+
193
+ Examples:
194
+ >>> from pathlib import Path
195
+ >>> scope_doc = Path("work/epics/e08-backlog/scope.md")
196
+ >>> stories = extract_stories(scope_doc)
197
+ >>> len(stories)
198
+ 4
199
+ >>> features[0].metadata["story_id"]
200
+ 'F8.1'
201
+ """
202
+ if not file_path.exists():
203
+ return []
204
+
205
+ if project_root is None:
206
+ project_root = file_path.parent.parent.parent.parent
207
+
208
+ text = file_path.read_text(encoding="utf-8")
209
+ lines = text.split("\n")
210
+
211
+ # Extract epic ID from parent directory: e08-backlog -> E8
212
+ parent_dir = file_path.parent.name
213
+ epic_id_match = re.search(r"^e(\d+)", parent_dir, re.IGNORECASE)
214
+ epic_id = f"E{int(epic_id_match.group(1))}" if epic_id_match else "E0"
215
+
216
+ # Calculate relative file path
217
+ try:
218
+ relative_path = portable_path(file_path, project_root)
219
+ except ValueError:
220
+ relative_path = file_path.name
221
+
222
+ concepts: list[Concept] = []
223
+
224
+ # Parse story table rows
225
+ # Pattern variants:
226
+ # | F8.1 | Backlog Parser | S | Pending | Description |
227
+ # | F8.1 | Backlog Parser | S | 2 | Pending | Description |
228
+ # | F2.1 | Concept Extraction | 3 | ✅ Complete | 52 min | 3.5x |
229
+ story_pattern = re.compile(
230
+ r"^\|\s*(F\d+\.\d+)\s*\|" # Story ID
231
+ r"\s*\*?\*?([^|*]+?)\*?\*?\s*\|" # Story name (with optional bold)
232
+ r"\s*([^|]+?)\s*\|" # Size or SP
233
+ r"\s*([^|]+?)\s*\|" # Status or SP (depends on format)
234
+ r"(?:\s*([^|]*?)\s*\|)?" # Optional: Description or Status or Time
235
+ )
236
+
237
+ for i, line in enumerate(lines, 1):
238
+ match = story_pattern.match(line)
239
+ if match:
240
+ story_id = match.group(1).strip()
241
+ name = match.group(2).strip()
242
+ col3 = match.group(3).strip()
243
+ col4 = match.group(4).strip()
244
+ col5 = match.group(5).strip() if match.group(5) else ""
245
+
246
+ # Determine column mapping based on content
247
+ # If col3 looks like size (XS/S/M/L) or small number (1-3), it's size
248
+ # If col4 looks like status, use col3 as size and col4 as status
249
+ size = None
250
+ status = None
251
+ sp = None
252
+ description = None
253
+
254
+ # Check if col3 is size (XS/S/M/L)
255
+ if re.match(r"^(XS|S|M|L)$", col3, re.IGNORECASE):
256
+ size = col3.upper()
257
+ # col4 could be SP or Status
258
+ if re.match(r"^\d+$", col4):
259
+ sp = int(col4)
260
+ status = normalize_status(col5) if col5 else "pending"
261
+ else:
262
+ status = normalize_status(col4)
263
+ description = col5
264
+ # Check if col3 is SP (number)
265
+ elif re.match(r"^\d+$", col3):
266
+ sp = int(col3)
267
+ status = normalize_status(col4)
268
+ # col5 might be actual time or description
269
+ if col5 and not re.match(r"^\d+\s*min", col5):
270
+ description = col5
271
+ else:
272
+ # Fallback: treat col3 as size text, col4 as status
273
+ size = col3[:2].upper() if col3 else None
274
+ status = normalize_status(col4)
275
+ description = col5
276
+
277
+ # Build content
278
+ content = f"{story_id}: {name}"
279
+ if status:
280
+ content += f" ({status})"
281
+ if description:
282
+ content += f" - {description}"
283
+
284
+ concept = Concept(
285
+ id=f"story-{story_id.lower().replace('.', '-')}",
286
+ type=ConceptType.STORY,
287
+ file=relative_path,
288
+ section=f"{story_id}: {name}",
289
+ lines=(i, i),
290
+ content=content[:500],
291
+ metadata={
292
+ "story_id": story_id,
293
+ "name": name,
294
+ "status": status or "pending",
295
+ "size": size,
296
+ "sp": sp,
297
+ "description": description,
298
+ "epic_id": epic_id,
299
+ },
300
+ )
301
+ concepts.append(concept)
302
+
303
+ return concepts
304
+
305
+
306
+ class EpicScopeParser:
307
+ """GovernanceParser wrapper for Epic scope docs (details + stories)."""
308
+
309
+ def can_parse(self, locator: ArtifactLocator) -> bool:
310
+ """Match Epic scope artifact type."""
311
+ return locator.artifact_type == CoreArtifactType.EPIC_SCOPE
312
+
313
+ def parse(self, locator: ArtifactLocator) -> list[GraphNode]:
314
+ """Parse Epic scope file into GraphNode list (epic + stories)."""
315
+ root = Path(locator.metadata["project_root"])
316
+ path = root / locator.path
317
+ nodes: list[GraphNode] = []
318
+ epic_detail = extract_epic_details(path, root)
319
+ if epic_detail:
320
+ nodes.append(concept_to_node(epic_detail))
321
+ stories = extract_stories(path, root)
322
+ nodes.extend(concept_to_node(s) for s in stories)
323
+ return nodes
@@ -0,0 +1,316 @@
1
+ """Parser for Glossary terms from framework documents.
2
+
3
+ Extracts term definitions from markdown glossary files.
4
+ Supports version tags, translations, and deprecated markers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from pathlib import Path
11
+
12
+ from raise_cli.adapters.models import ArtifactLocator, CoreArtifactType
13
+ from raise_cli.compat import portable_path
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
+ # Sections that contain term definitions (extract from these)
19
+ DEFINITION_SECTIONS = {
20
+ "Términos Core de RaiSE",
21
+ "Ontología Agentic AI",
22
+ "Artefactos del Flujo de Trabajo",
23
+ "Conceptos de Preventa/Proyectos",
24
+ }
25
+
26
+ # Sections to skip (tables, references, changelog)
27
+ SKIP_SECTIONS = {
28
+ "Mapeo Español-Inglés",
29
+ "Anti-Términos",
30
+ "Changelog",
31
+ "Jerarquías de Referencia",
32
+ "Métricas de Calidad AI",
33
+ "Formato de Referencia a Principios",
34
+ }
35
+
36
+
37
+ def _parse_term_header(header: str) -> tuple[str | None, str | None, str | None]:
38
+ """Parse a term header line.
39
+
40
+ Extracts term name, optional translation, and optional version/status.
41
+
42
+ Args:
43
+ header: Header line like "### Agent (Agente)" or "### Kata [v2.3: ...]"
44
+
45
+ Returns:
46
+ Tuple of (name, translation, version) where any can be None.
47
+
48
+ Examples:
49
+ >>> _parse_term_header("### Agent (Agente)")
50
+ ('Agent', 'Agente', None)
51
+ >>> _parse_term_header("### Kata [v2.3: Work Cycles]")
52
+ ('Kata', None, 'v2.3: Work Cycles')
53
+ >>> _parse_term_header("### Gate Engine ⚠️ **DEPRECATED v2.6**")
54
+ ('Gate Engine', None, 'DEPRECATED v2.6')
55
+ """
56
+ if not header.startswith("###"):
57
+ return None, None, None
58
+
59
+ # Remove ### prefix and strip
60
+ text = header[3:].strip()
61
+
62
+ name: str | None = None
63
+ translation: str | None = None
64
+ version: str | None = None
65
+
66
+ # Check for DEPRECATED marker (with or without emoji)
67
+ deprecated_match = re.search(r"\*\*DEPRECATED\s+(v[\d.]+)\*\*", text)
68
+ if deprecated_match:
69
+ version = f"DEPRECATED {deprecated_match.group(1)}"
70
+ # Remove deprecated marker from text
71
+ text = text[: deprecated_match.start()].strip()
72
+ # Remove emoji if present
73
+ text = text.replace("⚠️", "").strip()
74
+
75
+ # Check for **[ACTUALIZADO vX.X]** or **[NUEVO vX.X]** pattern
76
+ update_match = re.search(r"\*\*\[(ACTUALIZADO|NUEVO)\s+(v[\d.]+)\]\*\*", text)
77
+ if update_match and not version:
78
+ version = update_match.group(2) # Just the version number
79
+ text = text[: update_match.start()].strip()
80
+
81
+ # Check for [vX.X: description] version tag
82
+ version_match = re.search(r"\[(v[\d.]+[^\]]*)\]", text)
83
+ if version_match and not version: # Don't override DEPRECATED or ACTUALIZADO
84
+ version = version_match.group(1)
85
+ text = text[: version_match.start()].strip()
86
+
87
+ # Check for (Translation) pattern
88
+ trans_match = re.search(r"\(([^)]+)\)$", text)
89
+ if trans_match:
90
+ translation = trans_match.group(1)
91
+ text = text[: trans_match.start()].strip()
92
+
93
+ name = text if text else None
94
+
95
+ return name, translation, version
96
+
97
+
98
+ def _extract_term_content(content: str) -> tuple[str, str | None]:
99
+ """Extract definition and version tag from term content.
100
+
101
+ Args:
102
+ content: Content text after the term header.
103
+
104
+ Returns:
105
+ Tuple of (definition, version_tag) where version_tag may be None.
106
+
107
+ Examples:
108
+ >>> definition, version = _extract_term_content("**[NUEVO v2.0]** Discipline...")
109
+ >>> version
110
+ 'v2.0'
111
+ """
112
+ version_tag: str | None = None
113
+
114
+ # Check for **[NUEVO vX.X]** at start
115
+ bold_match = re.match(r"\*\*\[NUEVO\s+(v[\d.]+)\]\*\*\s*", content)
116
+ if bold_match:
117
+ version_tag = bold_match.group(1)
118
+ content = content[bold_match.end() :]
119
+
120
+ # Check for [NUEVO vX.X] at start (without bold)
121
+ if not version_tag:
122
+ plain_match = re.match(r"\[NUEVO\s+(v[\d.]+)\]\s*", content)
123
+ if plain_match:
124
+ version_tag = plain_match.group(1)
125
+ content = content[plain_match.end() :]
126
+
127
+ # Clean up content - take first paragraph or up to 500 chars
128
+ definition = content.strip()
129
+
130
+ return definition, version_tag
131
+
132
+
133
+ def _find_terms_in_section(
134
+ content: str, section_start: int, section_end: int, lines: list[str]
135
+ ) -> list[tuple[int, str, str]]:
136
+ """Find all term definitions within a section.
137
+
138
+ Args:
139
+ content: Full file content.
140
+ section_start: Character offset where section starts.
141
+ section_end: Character offset where section ends.
142
+ lines: List of lines in the file.
143
+
144
+ Returns:
145
+ List of (line_number, term_header, term_content) tuples.
146
+ """
147
+ section_text = content[section_start:section_end]
148
+ terms: list[tuple[int, str, str]] = []
149
+
150
+ # Find all ### headers in section
151
+ term_pattern = re.compile(r"^###\s+.+$", re.MULTILINE)
152
+ matches = list(term_pattern.finditer(section_text))
153
+
154
+ for i, match in enumerate(matches):
155
+ header = match.group(0)
156
+
157
+ # Find content (until next ### or end of section)
158
+ content_start = match.end()
159
+ content_end = (
160
+ matches[i + 1].start() if i + 1 < len(matches) else len(section_text)
161
+ )
162
+ term_content = section_text[content_start:content_end].strip()
163
+
164
+ # Calculate line number
165
+ # Count newlines from start of section to match
166
+ line_offset = section_text[: match.start()].count("\n")
167
+ # Count newlines from start of file to section
168
+ section_line = content[:section_start].count("\n") + 1
169
+ term_line = section_line + line_offset
170
+
171
+ terms.append((term_line, header, term_content))
172
+
173
+ return terms
174
+
175
+
176
+ def extract_glossary_terms(
177
+ file_path: Path, project_root: Path | None = None
178
+ ) -> list[Concept]:
179
+ """Extract glossary terms from a glossary markdown file.
180
+
181
+ Parses term definitions from sections like "Términos Core de RaiSE".
182
+ Each term becomes a Concept with type TERM.
183
+
184
+ Args:
185
+ file_path: Path to glossary markdown file.
186
+ project_root: Project root for relative path calculation.
187
+
188
+ Returns:
189
+ List of Concept objects representing glossary terms.
190
+
191
+ Examples:
192
+ >>> from pathlib import Path
193
+ >>> terms = extract_glossary_terms(Path("framework/reference/glossary.md"))
194
+ >>> len(terms) > 0
195
+ True
196
+ >>> terms[0].type
197
+ <ConceptType.TERM: 'term'>
198
+ """
199
+ if not file_path.exists():
200
+ return []
201
+
202
+ if project_root is None:
203
+ project_root = file_path.parent.parent.parent # framework/reference/ -> root
204
+
205
+ content = file_path.read_text(encoding="utf-8")
206
+ lines = content.split("\n")
207
+ concepts: list[Concept] = []
208
+
209
+ # Calculate relative path
210
+ try:
211
+ relative_path = portable_path(file_path, project_root)
212
+ except ValueError:
213
+ relative_path = file_path.name
214
+
215
+ # Find ## section boundaries
216
+ section_pattern = re.compile(r"^##\s+(.+?)$", re.MULTILINE)
217
+ section_matches = list(section_pattern.finditer(content))
218
+
219
+ for i, match in enumerate(section_matches):
220
+ section_name = match.group(1).strip()
221
+
222
+ # Skip non-definition sections
223
+ if section_name in SKIP_SECTIONS:
224
+ continue
225
+
226
+ # Only process definition sections (or any section not explicitly skipped)
227
+ # This allows for flexibility if new sections are added
228
+ section_start = match.end()
229
+ section_end = (
230
+ section_matches[i + 1].start()
231
+ if i + 1 < len(section_matches)
232
+ else len(content)
233
+ )
234
+
235
+ # Find terms in this section
236
+ terms = _find_terms_in_section(content, section_start, section_end, lines)
237
+
238
+ for term_line, header, term_content in terms:
239
+ name, translation, header_version = _parse_term_header(header)
240
+
241
+ if not name:
242
+ continue
243
+
244
+ # Extract definition and check for version tag in content
245
+ definition, content_version = _extract_term_content(term_content)
246
+
247
+ # Use header version if present, else content version
248
+ version = header_version or content_version
249
+
250
+ # Generate ID from name (lowercase, hyphenated)
251
+ term_id = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
252
+
253
+ # Truncate definition if needed
254
+ if len(definition) > 500:
255
+ definition = definition[:500] + "..."
256
+
257
+ concept = Concept(
258
+ id=f"term-{term_id}",
259
+ type=ConceptType.TERM,
260
+ file=relative_path,
261
+ section=f"{section_name}: {name}",
262
+ lines=(term_line, term_line + 10), # Approximate end
263
+ content=definition,
264
+ metadata={
265
+ "name": name,
266
+ "translation": translation,
267
+ "version": version,
268
+ "section": section_name,
269
+ },
270
+ )
271
+ concepts.append(concept)
272
+
273
+ return concepts
274
+
275
+
276
+ def extract_all_terms(project_root: Path | None = None) -> list[Concept]:
277
+ """Extract all glossary terms from standard location.
278
+
279
+ Looks for framework/reference/glossary.md.
280
+
281
+ Args:
282
+ project_root: Project root directory.
283
+
284
+ Returns:
285
+ List of all extracted term Concepts.
286
+
287
+ Examples:
288
+ >>> from pathlib import Path
289
+ >>> terms = extract_all_terms(Path("."))
290
+ >>> len(terms) >= 30
291
+ True
292
+ """
293
+ if project_root is None:
294
+ project_root = Path.cwd()
295
+
296
+ glossary_file = project_root / "framework" / "reference" / "glossary.md"
297
+
298
+ if glossary_file.exists():
299
+ return extract_glossary_terms(glossary_file, project_root)
300
+
301
+ return []
302
+
303
+
304
+ class GlossaryParser:
305
+ """GovernanceParser wrapper for glossary terms."""
306
+
307
+ def can_parse(self, locator: ArtifactLocator) -> bool:
308
+ """Match Glossary artifact type."""
309
+ return locator.artifact_type == CoreArtifactType.GLOSSARY
310
+
311
+ def parse(self, locator: ArtifactLocator) -> list[GraphNode]:
312
+ """Parse Glossary file into GraphNode list."""
313
+ root = Path(locator.metadata["project_root"])
314
+ path = root / locator.path
315
+ concepts = extract_glossary_terms(path, root)
316
+ return [concept_to_node(c) for c in concepts]