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,134 @@
1
+ """Pydantic models for governance concept extraction.
2
+
3
+ This module defines the core data structures for representing semantic
4
+ concepts extracted from governance markdown files.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import StrEnum
10
+ from typing import Any, Self
11
+
12
+ from pydantic import BaseModel, Field, model_validator
13
+
14
+
15
+ class ConceptType(StrEnum):
16
+ """Types of concepts extracted from governance documents.
17
+
18
+ Attributes:
19
+ REQUIREMENT: Functional requirements from PRD (RF-XX format).
20
+ OUTCOME: Desired outcomes from Vision document.
21
+ PRINCIPLE: Core principles from Constitution (§N format).
22
+ PATTERN: Design patterns from technical design (future).
23
+ PRACTICE: Process practices from katas (future).
24
+ PROJECT: Project from backlog (governance/backlog.md).
25
+ EPIC: Epic from backlog or epic scope documents.
26
+ STORY: Story from epic scope documents.
27
+ DECISION: Architecture decision records (ADR-XXX format).
28
+ GUARDRAIL: Code standards and quality rules.
29
+ TERM: Glossary definitions for canonical terminology.
30
+ """
31
+
32
+ REQUIREMENT = "requirement"
33
+ OUTCOME = "outcome"
34
+ PRINCIPLE = "principle"
35
+ PATTERN = "pattern"
36
+ PRACTICE = "practice"
37
+ # Work tracking types (E8)
38
+ PROJECT = "project"
39
+ EPIC = "epic"
40
+ STORY = "story"
41
+ # Knowledge graph completion types (E12)
42
+ DECISION = "decision"
43
+ GUARDRAIL = "guardrail"
44
+ TERM = "term"
45
+ # Release ontology
46
+ RELEASE = "release"
47
+
48
+
49
+ class Concept(BaseModel):
50
+ """A semantic concept extracted from governance markdown.
51
+
52
+ Represents a single extractable unit of governance information
53
+ (e.g., a requirement, outcome, or principle) with its location
54
+ and metadata.
55
+
56
+ Attributes:
57
+ id: Unique identifier (e.g., 'req-rf-05', 'outcome-context-generation').
58
+ type: Type of concept (requirement, outcome, principle, etc.).
59
+ file: Source file relative path from project root.
60
+ section: Section heading where concept was found.
61
+ lines: Tuple of (start_line, end_line) in source file.
62
+ content: Extracted content, truncated if >500 chars.
63
+ metadata: Type-specific metadata (e.g., requirement_id, title).
64
+
65
+ Examples:
66
+ >>> concept = Concept(
67
+ ... id="req-rf-05",
68
+ ... type=ConceptType.REQUIREMENT,
69
+ ... file="governance/prd.md",
70
+ ... section="RF-05: Golden Context Generation",
71
+ ... lines=(206, 214),
72
+ ... content="The system MUST generate CLAUDE.md...",
73
+ ... metadata={"requirement_id": "RF-05", "title": "Golden Context Generation"}
74
+ ... )
75
+ >>> concept.id
76
+ 'req-rf-05'
77
+ >>> concept.type
78
+ <ConceptType.REQUIREMENT: 'requirement'>
79
+ """
80
+
81
+ id: str = Field(..., description="Unique identifier (e.g., 'req-rf-05')")
82
+ type: ConceptType = Field(..., description="Concept type")
83
+ file: str = Field(..., description="Source file relative path")
84
+ section: str = Field(..., description="Section heading")
85
+ lines: tuple[int, int] = Field(..., description="Line range (start, end)")
86
+ content: str = Field(..., description="Extracted content (truncated if long)")
87
+ metadata: dict[str, Any] = Field(
88
+ default_factory=dict, description="Type-specific metadata"
89
+ )
90
+
91
+ @model_validator(mode="after")
92
+ def validate_line_range(self) -> Self:
93
+ """Validate that line range is valid (start <= end).
94
+
95
+ Returns:
96
+ Self if valid.
97
+
98
+ Raises:
99
+ ValueError: If line range is invalid (start > end).
100
+ """
101
+ if self.lines[0] > self.lines[1]:
102
+ raise ValueError(
103
+ f"Invalid line range: start ({self.lines[0]}) > end ({self.lines[1]})"
104
+ )
105
+ return self
106
+
107
+
108
+ class ExtractionResult(BaseModel):
109
+ """Result of a concept extraction operation.
110
+
111
+ Contains the extracted concepts along with metadata about the
112
+ extraction process (files processed, errors encountered).
113
+
114
+ Attributes:
115
+ concepts: List of extracted concepts.
116
+ total: Total number of concepts extracted.
117
+ files_processed: Number of files successfully parsed.
118
+ errors: List of error messages from failed extractions.
119
+
120
+ Examples:
121
+ >>> result = ExtractionResult(
122
+ ... concepts=[concept1, concept2],
123
+ ... total=2,
124
+ ... files_processed=1,
125
+ ... errors=[]
126
+ ... )
127
+ >>> result.total
128
+ 2
129
+ """
130
+
131
+ concepts: list[Concept] = Field(default_factory=lambda: list[Concept]())
132
+ total: int = Field(..., description="Total concepts extracted")
133
+ files_processed: int = Field(..., description="Number of files parsed")
134
+ errors: list[str] = Field(default_factory=list)
@@ -0,0 +1,35 @@
1
+ """Parsers for governance markdown files.
2
+
3
+ This module contains parsers for different governance document types
4
+ (PRD, Vision, Constitution, Backlog, Epic, ADR, Guardrails) that extract semantic concepts.
5
+ """
6
+
7
+ from raise_cli.governance.parsers.adr import (
8
+ extract_all_decisions,
9
+ extract_decision_from_file,
10
+ extract_decisions,
11
+ )
12
+ from raise_cli.governance.parsers.backlog import extract_epics, extract_project
13
+ from raise_cli.governance.parsers.constitution import extract_principles
14
+ from raise_cli.governance.parsers.epic import extract_epic_details, extract_stories
15
+ from raise_cli.governance.parsers.guardrails import (
16
+ extract_all_guardrails,
17
+ extract_guardrails,
18
+ )
19
+ from raise_cli.governance.parsers.prd import extract_requirements
20
+ from raise_cli.governance.parsers.vision import extract_outcomes
21
+
22
+ __all__ = [
23
+ "extract_requirements",
24
+ "extract_outcomes",
25
+ "extract_principles",
26
+ "extract_project",
27
+ "extract_epics",
28
+ "extract_epic_details",
29
+ "extract_stories",
30
+ "extract_decisions",
31
+ "extract_decision_from_file",
32
+ "extract_all_decisions",
33
+ "extract_guardrails",
34
+ "extract_all_guardrails",
35
+ ]
@@ -0,0 +1,38 @@
1
+ """Concept to GraphNode conversion utility.
2
+
3
+ Extracted from GraphBuilder._concept_to_node() for reuse
4
+ by GovernanceParser wrapper classes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import UTC, datetime
10
+
11
+ from raise_cli.governance.models import Concept
12
+ from raise_core.graph.models import GraphNode
13
+
14
+
15
+ def concept_to_node(concept: Concept) -> GraphNode:
16
+ """Convert governance Concept to GraphNode (GraphNode).
17
+
18
+ Preserves section and lines in metadata since GraphNode has no
19
+ dedicated fields for these (R3 from quality review).
20
+
21
+ Args:
22
+ concept: Governance concept to convert.
23
+
24
+ Returns:
25
+ GraphNode with mapped fields.
26
+ """
27
+ metadata = dict(concept.metadata)
28
+ metadata["section"] = concept.section
29
+ metadata["lines"] = concept.lines
30
+
31
+ return GraphNode(
32
+ id=concept.id,
33
+ type=concept.type.value,
34
+ content=concept.content,
35
+ source_file=concept.file,
36
+ created=datetime.now(tz=UTC).isoformat(),
37
+ metadata=metadata,
38
+ )
@@ -0,0 +1,274 @@
1
+ """Parser for Architecture Decision Records (ADRs).
2
+
3
+ Extracts decisions from ADR markdown files with YAML frontmatter.
4
+ Supports ADRs in dev/decisions/ (root and v2 subdirectory).
5
+ Legacy v1 ADRs without frontmatter are skipped.
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(text: str) -> tuple[dict[str, Any], str, int]:
24
+ """Parse YAML frontmatter from markdown text.
25
+
26
+ Args:
27
+ text: Full markdown text content.
28
+
29
+ Returns:
30
+ Tuple of (frontmatter dict, remaining content, frontmatter end line).
31
+ Returns empty dict if no frontmatter found.
32
+
33
+ Examples:
34
+ >>> text = "---\\nid: ADR-001\\n---\\n# Title"
35
+ >>> fm, content, end_line = _parse_frontmatter(text)
36
+ >>> fm["id"]
37
+ 'ADR-001'
38
+ """
39
+ if not text.startswith("---"):
40
+ return {}, text, 0
41
+
42
+ # Find closing ---
43
+ lines = text.split("\n")
44
+ end_idx = None
45
+ for i, line in enumerate(lines[1:], 1):
46
+ if line.strip() == "---":
47
+ end_idx = i
48
+ break
49
+
50
+ if end_idx is None:
51
+ return {}, text, 0
52
+
53
+ # Parse YAML
54
+ frontmatter_text = "\n".join(lines[1:end_idx])
55
+ try:
56
+ loaded = yaml.safe_load(frontmatter_text)
57
+ frontmatter: dict[str, Any] = cast(dict[str, Any], loaded) if loaded else {}
58
+ except yaml.YAMLError:
59
+ return {}, text, 0
60
+
61
+ remaining = "\n".join(lines[end_idx + 1 :])
62
+ return frontmatter, remaining, end_idx + 1
63
+
64
+
65
+ def _extract_decision_summary(content: str) -> str:
66
+ """Extract the Decision section content as summary.
67
+
68
+ Args:
69
+ content: Markdown content after frontmatter.
70
+
71
+ Returns:
72
+ Decision section content, truncated to ~500 chars.
73
+
74
+ Examples:
75
+ >>> content = "## Context\\nSome context.\\n## Decision\\nWe decided X."
76
+ >>> _extract_decision_summary(content)
77
+ 'We decided X.'
78
+ """
79
+ # Look for ## Decision or ## Decisión section
80
+ match = re.search(
81
+ r"^##\s+(?:Decision|Decisión)\s*\n(.*?)(?=^##\s|\Z)",
82
+ content,
83
+ re.MULTILINE | re.DOTALL | re.IGNORECASE,
84
+ )
85
+
86
+ if match:
87
+ decision_text = match.group(1).strip()
88
+ # Truncate if too long
89
+ if len(decision_text) > 500:
90
+ decision_text = decision_text[:500] + "..."
91
+ return decision_text
92
+
93
+ # Fallback: use first paragraph after title
94
+ lines = content.strip().split("\n")
95
+ for i, line in enumerate(lines):
96
+ if line.startswith("# "):
97
+ # Skip title, get next non-empty content
98
+ for j in range(i + 1, min(i + 10, len(lines))):
99
+ if lines[j].strip() and not lines[j].startswith("#"):
100
+ return lines[j].strip()[:500]
101
+
102
+ return ""
103
+
104
+
105
+ def extract_decisions(
106
+ directory: Path, project_root: Path | None = None
107
+ ) -> list[Concept]:
108
+ """Extract ADR decisions from a directory.
109
+
110
+ Parses ADR files with YAML frontmatter containing id, title, status, date.
111
+ Files without frontmatter are skipped (legacy v1 format).
112
+
113
+ Args:
114
+ directory: Directory containing ADR markdown files.
115
+ project_root: Project root for relative path calculation.
116
+
117
+ Returns:
118
+ List of Concept objects representing decisions.
119
+
120
+ Examples:
121
+ >>> from pathlib import Path
122
+ >>> decisions = extract_decisions(Path("dev/decisions"))
123
+ >>> len(decisions) > 0
124
+ True
125
+ >>> decisions[0].type
126
+ <ConceptType.DECISION: 'decision'>
127
+ """
128
+ if not directory.exists():
129
+ return []
130
+
131
+ if project_root is None:
132
+ project_root = directory.parent
133
+
134
+ concepts: list[Concept] = []
135
+
136
+ # Process ADR files in directory
137
+ for adr_file in sorted(directory.glob("adr-*.md")):
138
+ concept = extract_decision_from_file(adr_file, project_root)
139
+ if concept:
140
+ concepts.append(concept)
141
+
142
+ return concepts
143
+
144
+
145
+ def extract_decision_from_file(
146
+ file_path: Path, project_root: Path | None = None
147
+ ) -> Concept | None:
148
+ """Extract a single ADR decision from a file.
149
+
150
+ Args:
151
+ file_path: Path to ADR markdown file.
152
+ project_root: Project root for relative path calculation.
153
+
154
+ Returns:
155
+ Concept object if valid ADR with frontmatter, None otherwise.
156
+
157
+ Examples:
158
+ >>> from pathlib import Path
159
+ >>> decision = extract_decision_from_file(Path("dev/decisions/adr-019.md"))
160
+ >>> decision.id if decision else None
161
+ 'ADR-019'
162
+ """
163
+ if not file_path.exists():
164
+ return None
165
+
166
+ if project_root is None:
167
+ project_root = file_path.parent.parent
168
+
169
+ text = file_path.read_text(encoding="utf-8")
170
+ frontmatter, content, _ = _parse_frontmatter(text)
171
+
172
+ # Skip files without frontmatter (legacy format)
173
+ if not frontmatter:
174
+ return None
175
+
176
+ # Required fields
177
+ adr_id = frontmatter.get("id")
178
+ if not adr_id:
179
+ return None
180
+
181
+ title = frontmatter.get("title", "")
182
+ status = frontmatter.get("status", "unknown")
183
+ date = frontmatter.get("date", "")
184
+ related_to = frontmatter.get("related_to", [])
185
+
186
+ # Extract decision summary
187
+ decision_summary = _extract_decision_summary(content)
188
+
189
+ # Build content string
190
+ content_str = f"{title}"
191
+ if decision_summary:
192
+ content_str = f"{title} — {decision_summary}"
193
+
194
+ # Truncate if needed
195
+ if len(content_str) > 500:
196
+ content_str = content_str[:500] + "..."
197
+
198
+ # Calculate relative path
199
+ try:
200
+ relative_path = portable_path(file_path, project_root)
201
+ except ValueError:
202
+ relative_path = file_path.name
203
+
204
+ # Count lines for line range
205
+ total_lines = len(text.split("\n"))
206
+
207
+ return Concept(
208
+ id=f"decision-{adr_id.lower()}",
209
+ type=ConceptType.DECISION,
210
+ file=relative_path,
211
+ section=f"{adr_id}: {title}",
212
+ lines=(1, total_lines),
213
+ content=content_str,
214
+ metadata={
215
+ "adr_id": adr_id,
216
+ "title": title,
217
+ "status": status,
218
+ "date": str(date) if date else "",
219
+ "related_to": related_to if isinstance(related_to, list) else [],
220
+ },
221
+ )
222
+
223
+
224
+ def extract_all_decisions(project_root: Path | None = None) -> list[Concept]:
225
+ """Extract all ADR decisions from standard locations.
226
+
227
+ Searches dev/decisions/ (root) and dev/decisions/v2/ for ADRs.
228
+ Skips dev/decisions/v1/ (legacy format without frontmatter).
229
+
230
+ Args:
231
+ project_root: Project root directory.
232
+
233
+ Returns:
234
+ List of all extracted decision Concepts.
235
+
236
+ Examples:
237
+ >>> from pathlib import Path
238
+ >>> decisions = extract_all_decisions(Path("."))
239
+ >>> len(decisions) >= 20
240
+ True
241
+ """
242
+ if project_root is None:
243
+ project_root = Path.cwd()
244
+
245
+ concepts: list[Concept] = []
246
+
247
+ # Root level ADRs
248
+ root_decisions = project_root / "dev" / "decisions"
249
+ if root_decisions.exists():
250
+ concepts.extend(extract_decisions(root_decisions, project_root))
251
+
252
+ # v2 subdirectory ADRs
253
+ v2_decisions = project_root / "dev" / "decisions" / "v2"
254
+ if v2_decisions.exists():
255
+ concepts.extend(extract_decisions(v2_decisions, project_root))
256
+
257
+ # Note: v1 is intentionally skipped (legacy format without frontmatter)
258
+
259
+ return concepts
260
+
261
+
262
+ class AdrParser:
263
+ """GovernanceParser wrapper for ADR decisions. Per-file parsing."""
264
+
265
+ def can_parse(self, locator: ArtifactLocator) -> bool:
266
+ """Match ADR artifact type."""
267
+ return locator.artifact_type == CoreArtifactType.ADR
268
+
269
+ def parse(self, locator: ArtifactLocator) -> list[GraphNode]:
270
+ """Parse single ADR file pointed to by locator."""
271
+ root = Path(locator.metadata["project_root"])
272
+ path = root / locator.path
273
+ concept = extract_decision_from_file(path, root)
274
+ return [concept_to_node(concept)] if concept else []