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.
- raise_cli/__init__.py +38 -0
- raise_cli/__main__.py +30 -0
- raise_cli/adapters/__init__.py +91 -0
- raise_cli/adapters/declarative/__init__.py +26 -0
- raise_cli/adapters/declarative/adapter.py +267 -0
- raise_cli/adapters/declarative/discovery.py +94 -0
- raise_cli/adapters/declarative/expressions.py +150 -0
- raise_cli/adapters/declarative/reference/__init__.py +1 -0
- raise_cli/adapters/declarative/reference/github.yaml +143 -0
- raise_cli/adapters/declarative/schema.py +98 -0
- raise_cli/adapters/filesystem.py +299 -0
- raise_cli/adapters/mcp_bridge.py +10 -0
- raise_cli/adapters/mcp_confluence.py +246 -0
- raise_cli/adapters/mcp_jira.py +405 -0
- raise_cli/adapters/models.py +205 -0
- raise_cli/adapters/protocols.py +180 -0
- raise_cli/adapters/registry.py +90 -0
- raise_cli/adapters/sync.py +149 -0
- raise_cli/agents/__init__.py +14 -0
- raise_cli/agents/antigravity.yaml +8 -0
- raise_cli/agents/claude.yaml +8 -0
- raise_cli/agents/copilot.yaml +8 -0
- raise_cli/agents/copilot_plugin.py +124 -0
- raise_cli/agents/cursor.yaml +7 -0
- raise_cli/agents/roo.yaml +8 -0
- raise_cli/agents/windsurf.yaml +8 -0
- raise_cli/artifacts/__init__.py +30 -0
- raise_cli/artifacts/models.py +43 -0
- raise_cli/artifacts/reader.py +55 -0
- raise_cli/artifacts/renderer.py +104 -0
- raise_cli/artifacts/story_design.py +69 -0
- raise_cli/artifacts/writer.py +45 -0
- raise_cli/backlog/__init__.py +1 -0
- raise_cli/backlog/sync.py +115 -0
- raise_cli/cli/__init__.py +3 -0
- raise_cli/cli/commands/__init__.py +3 -0
- raise_cli/cli/commands/_resolve.py +153 -0
- raise_cli/cli/commands/adapters.py +362 -0
- raise_cli/cli/commands/artifact.py +137 -0
- raise_cli/cli/commands/backlog.py +333 -0
- raise_cli/cli/commands/base.py +31 -0
- raise_cli/cli/commands/discover.py +551 -0
- raise_cli/cli/commands/docs.py +130 -0
- raise_cli/cli/commands/doctor.py +177 -0
- raise_cli/cli/commands/gate.py +223 -0
- raise_cli/cli/commands/graph.py +1086 -0
- raise_cli/cli/commands/info.py +81 -0
- raise_cli/cli/commands/init.py +746 -0
- raise_cli/cli/commands/journal.py +167 -0
- raise_cli/cli/commands/mcp.py +524 -0
- raise_cli/cli/commands/memory.py +467 -0
- raise_cli/cli/commands/pattern.py +348 -0
- raise_cli/cli/commands/profile.py +59 -0
- raise_cli/cli/commands/publish.py +80 -0
- raise_cli/cli/commands/release.py +338 -0
- raise_cli/cli/commands/session.py +528 -0
- raise_cli/cli/commands/signal.py +410 -0
- raise_cli/cli/commands/skill.py +350 -0
- raise_cli/cli/commands/skill_set.py +145 -0
- raise_cli/cli/error_handler.py +158 -0
- raise_cli/cli/main.py +163 -0
- raise_cli/compat.py +66 -0
- raise_cli/config/__init__.py +41 -0
- raise_cli/config/agent_plugin.py +105 -0
- raise_cli/config/agent_registry.py +233 -0
- raise_cli/config/agents.py +120 -0
- raise_cli/config/ide.py +32 -0
- raise_cli/config/paths.py +379 -0
- raise_cli/config/settings.py +180 -0
- raise_cli/context/__init__.py +42 -0
- raise_cli/context/analyzers/__init__.py +16 -0
- raise_cli/context/analyzers/models.py +36 -0
- raise_cli/context/analyzers/protocol.py +43 -0
- raise_cli/context/analyzers/python.py +292 -0
- raise_cli/context/builder.py +1569 -0
- raise_cli/context/diff.py +213 -0
- raise_cli/context/extractors/__init__.py +13 -0
- raise_cli/context/extractors/skills.py +121 -0
- raise_cli/core/__init__.py +37 -0
- raise_cli/core/files.py +66 -0
- raise_cli/core/text.py +174 -0
- raise_cli/core/tools.py +441 -0
- raise_cli/discovery/__init__.py +50 -0
- raise_cli/discovery/analyzer.py +691 -0
- raise_cli/discovery/drift.py +355 -0
- raise_cli/discovery/scanner.py +1687 -0
- raise_cli/doctor/__init__.py +4 -0
- raise_cli/doctor/checks/__init__.py +1 -0
- raise_cli/doctor/checks/environment.py +110 -0
- raise_cli/doctor/checks/project.py +238 -0
- raise_cli/doctor/fix.py +80 -0
- raise_cli/doctor/models.py +56 -0
- raise_cli/doctor/protocol.py +43 -0
- raise_cli/doctor/registry.py +100 -0
- raise_cli/doctor/report.py +141 -0
- raise_cli/doctor/runner.py +95 -0
- raise_cli/engines/__init__.py +3 -0
- raise_cli/exceptions.py +215 -0
- raise_cli/gates/__init__.py +19 -0
- raise_cli/gates/builtin/__init__.py +1 -0
- raise_cli/gates/builtin/coverage.py +52 -0
- raise_cli/gates/builtin/lint.py +48 -0
- raise_cli/gates/builtin/tests.py +48 -0
- raise_cli/gates/builtin/types.py +48 -0
- raise_cli/gates/models.py +40 -0
- raise_cli/gates/protocol.py +41 -0
- raise_cli/gates/registry.py +141 -0
- raise_cli/governance/__init__.py +11 -0
- raise_cli/governance/extractor.py +412 -0
- raise_cli/governance/models.py +134 -0
- raise_cli/governance/parsers/__init__.py +35 -0
- raise_cli/governance/parsers/_convert.py +38 -0
- raise_cli/governance/parsers/adr.py +274 -0
- raise_cli/governance/parsers/backlog.py +356 -0
- raise_cli/governance/parsers/constitution.py +119 -0
- raise_cli/governance/parsers/epic.py +323 -0
- raise_cli/governance/parsers/glossary.py +316 -0
- raise_cli/governance/parsers/guardrails.py +345 -0
- raise_cli/governance/parsers/prd.py +112 -0
- raise_cli/governance/parsers/roadmap.py +118 -0
- raise_cli/governance/parsers/vision.py +116 -0
- raise_cli/graph/__init__.py +1 -0
- raise_cli/graph/backends/__init__.py +57 -0
- raise_cli/graph/backends/api.py +137 -0
- raise_cli/graph/backends/dual.py +139 -0
- raise_cli/graph/backends/pending.py +84 -0
- raise_cli/handlers/__init__.py +3 -0
- raise_cli/hooks/__init__.py +54 -0
- raise_cli/hooks/builtin/__init__.py +1 -0
- raise_cli/hooks/builtin/backlog.py +216 -0
- raise_cli/hooks/builtin/gate_bridge.py +83 -0
- raise_cli/hooks/builtin/jira_sync.py +127 -0
- raise_cli/hooks/builtin/memory.py +117 -0
- raise_cli/hooks/builtin/telemetry.py +72 -0
- raise_cli/hooks/emitter.py +184 -0
- raise_cli/hooks/events.py +262 -0
- raise_cli/hooks/protocol.py +38 -0
- raise_cli/hooks/registry.py +117 -0
- raise_cli/mcp/__init__.py +33 -0
- raise_cli/mcp/bridge.py +218 -0
- raise_cli/mcp/models.py +43 -0
- raise_cli/mcp/registry.py +77 -0
- raise_cli/mcp/schema.py +41 -0
- raise_cli/memory/__init__.py +58 -0
- raise_cli/memory/loader.py +247 -0
- raise_cli/memory/migration.py +241 -0
- raise_cli/memory/models.py +169 -0
- raise_cli/memory/writer.py +598 -0
- raise_cli/onboarding/__init__.py +103 -0
- raise_cli/onboarding/bootstrap.py +324 -0
- raise_cli/onboarding/claudemd.py +17 -0
- raise_cli/onboarding/conventions.py +742 -0
- raise_cli/onboarding/detection.py +374 -0
- raise_cli/onboarding/governance.py +443 -0
- raise_cli/onboarding/instructions.py +672 -0
- raise_cli/onboarding/manifest.py +201 -0
- raise_cli/onboarding/memory_md.py +399 -0
- raise_cli/onboarding/migration.py +207 -0
- raise_cli/onboarding/profile.py +624 -0
- raise_cli/onboarding/skill_conflict.py +100 -0
- raise_cli/onboarding/skill_manifest.py +176 -0
- raise_cli/onboarding/skills.py +437 -0
- raise_cli/onboarding/workflows.py +101 -0
- raise_cli/output/__init__.py +28 -0
- raise_cli/output/console.py +394 -0
- raise_cli/output/formatters/__init__.py +9 -0
- raise_cli/output/formatters/adapters.py +135 -0
- raise_cli/output/formatters/discover.py +439 -0
- raise_cli/output/formatters/skill.py +298 -0
- raise_cli/publish/__init__.py +3 -0
- raise_cli/publish/changelog.py +80 -0
- raise_cli/publish/check.py +179 -0
- raise_cli/publish/version.py +172 -0
- raise_cli/rai_base/__init__.py +22 -0
- raise_cli/rai_base/framework/__init__.py +7 -0
- raise_cli/rai_base/framework/methodology.yaml +233 -0
- raise_cli/rai_base/governance/__init__.py +1 -0
- raise_cli/rai_base/governance/architecture/__init__.py +1 -0
- raise_cli/rai_base/governance/architecture/domain-model.md +20 -0
- raise_cli/rai_base/governance/architecture/system-context.md +34 -0
- raise_cli/rai_base/governance/architecture/system-design.md +24 -0
- raise_cli/rai_base/governance/backlog.md +8 -0
- raise_cli/rai_base/governance/guardrails.md +17 -0
- raise_cli/rai_base/governance/prd.md +25 -0
- raise_cli/rai_base/governance/vision.md +16 -0
- raise_cli/rai_base/identity/__init__.py +8 -0
- raise_cli/rai_base/identity/core.md +119 -0
- raise_cli/rai_base/identity/perspective.md +119 -0
- raise_cli/rai_base/memory/__init__.py +7 -0
- raise_cli/rai_base/memory/patterns-base.jsonl +55 -0
- raise_cli/schemas/__init__.py +3 -0
- raise_cli/schemas/journal.py +49 -0
- raise_cli/schemas/session_state.py +117 -0
- raise_cli/session/__init__.py +5 -0
- raise_cli/session/bundle.py +820 -0
- raise_cli/session/close.py +268 -0
- raise_cli/session/journal.py +119 -0
- raise_cli/session/resolver.py +126 -0
- raise_cli/session/state.py +187 -0
- raise_cli/skills/__init__.py +44 -0
- raise_cli/skills/locator.py +141 -0
- raise_cli/skills/name_checker.py +199 -0
- raise_cli/skills/parser.py +145 -0
- raise_cli/skills/scaffold.py +212 -0
- raise_cli/skills/schema.py +132 -0
- raise_cli/skills/skillsets.py +195 -0
- raise_cli/skills/validator.py +197 -0
- raise_cli/skills_base/__init__.py +80 -0
- raise_cli/skills_base/contract-template.md +60 -0
- raise_cli/skills_base/preamble.md +37 -0
- raise_cli/skills_base/rai-architecture-review/SKILL.md +137 -0
- raise_cli/skills_base/rai-debug/SKILL.md +171 -0
- raise_cli/skills_base/rai-discover/SKILL.md +167 -0
- raise_cli/skills_base/rai-discover-document/SKILL.md +128 -0
- raise_cli/skills_base/rai-discover-scan/SKILL.md +147 -0
- raise_cli/skills_base/rai-discover-start/SKILL.md +145 -0
- raise_cli/skills_base/rai-discover-validate/SKILL.md +142 -0
- raise_cli/skills_base/rai-docs-update/SKILL.md +142 -0
- raise_cli/skills_base/rai-doctor/SKILL.md +120 -0
- raise_cli/skills_base/rai-epic-close/SKILL.md +165 -0
- raise_cli/skills_base/rai-epic-close/templates/retrospective.md +68 -0
- raise_cli/skills_base/rai-epic-design/SKILL.md +146 -0
- raise_cli/skills_base/rai-epic-design/templates/design.md +24 -0
- raise_cli/skills_base/rai-epic-design/templates/scope.md +76 -0
- raise_cli/skills_base/rai-epic-plan/SKILL.md +153 -0
- raise_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
- raise_cli/skills_base/rai-epic-plan/templates/plan-section.md +49 -0
- raise_cli/skills_base/rai-epic-run/SKILL.md +208 -0
- raise_cli/skills_base/rai-epic-start/SKILL.md +136 -0
- raise_cli/skills_base/rai-epic-start/templates/brief.md +34 -0
- raise_cli/skills_base/rai-mcp-add/SKILL.md +176 -0
- raise_cli/skills_base/rai-mcp-remove/SKILL.md +120 -0
- raise_cli/skills_base/rai-mcp-status/SKILL.md +147 -0
- raise_cli/skills_base/rai-problem-shape/SKILL.md +138 -0
- raise_cli/skills_base/rai-project-create/SKILL.md +144 -0
- raise_cli/skills_base/rai-project-onboard/SKILL.md +162 -0
- raise_cli/skills_base/rai-quality-review/SKILL.md +189 -0
- raise_cli/skills_base/rai-research/SKILL.md +143 -0
- raise_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
- raise_cli/skills_base/rai-session-close/SKILL.md +176 -0
- raise_cli/skills_base/rai-session-start/SKILL.md +110 -0
- raise_cli/skills_base/rai-story-close/SKILL.md +198 -0
- raise_cli/skills_base/rai-story-design/SKILL.md +203 -0
- raise_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
- raise_cli/skills_base/rai-story-implement/SKILL.md +115 -0
- raise_cli/skills_base/rai-story-plan/SKILL.md +135 -0
- raise_cli/skills_base/rai-story-review/SKILL.md +178 -0
- raise_cli/skills_base/rai-story-run/SKILL.md +282 -0
- raise_cli/skills_base/rai-story-start/SKILL.md +166 -0
- raise_cli/skills_base/rai-story-start/templates/story.md +38 -0
- raise_cli/skills_base/rai-welcome/SKILL.md +134 -0
- raise_cli/telemetry/__init__.py +42 -0
- raise_cli/telemetry/schemas.py +285 -0
- raise_cli/telemetry/writer.py +217 -0
- raise_cli/tier/__init__.py +0 -0
- raise_cli/tier/context.py +134 -0
- raise_cli/viz/__init__.py +7 -0
- raise_cli/viz/generator.py +406 -0
- raise_cli-2.2.1.dist-info/METADATA +433 -0
- raise_cli-2.2.1.dist-info/RECORD +264 -0
- raise_cli-2.2.1.dist-info/WHEEL +4 -0
- raise_cli-2.2.1.dist-info/entry_points.txt +40 -0
- raise_cli-2.2.1.dist-info/licenses/LICENSE +190 -0
- 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 []
|