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