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,356 @@
|
|
|
1
|
+
"""Parser for project backlog files.
|
|
2
|
+
|
|
3
|
+
Extracts Project and Epic concepts from governance/backlog.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_core.graph.models import GraphNode
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def normalize_status(raw: str) -> str:
|
|
19
|
+
"""Normalize epic status to standard values.
|
|
20
|
+
|
|
21
|
+
Converts various status representations (emoji, text) to lowercase
|
|
22
|
+
standard values compatible with WorkStatus enum.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
raw: Raw status string from backlog table.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Normalized status: 'complete', 'draft', 'deferred', 'in_progress', or 'pending'.
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
>>> normalize_status("✅ Complete")
|
|
32
|
+
'complete'
|
|
33
|
+
>>> normalize_status("📋 DRAFT")
|
|
34
|
+
'draft'
|
|
35
|
+
>>> normalize_status("Deferred")
|
|
36
|
+
'deferred'
|
|
37
|
+
>>> normalize_status("→ Replaced by E9")
|
|
38
|
+
'deferred'
|
|
39
|
+
"""
|
|
40
|
+
raw_lower = raw.lower().strip()
|
|
41
|
+
if "complete" in raw_lower or "✅" in raw:
|
|
42
|
+
return "complete"
|
|
43
|
+
if "draft" in raw_lower or "📋" in raw:
|
|
44
|
+
return "draft"
|
|
45
|
+
if "deferred" in raw_lower or "replaced" in raw_lower or "→" in raw:
|
|
46
|
+
return "deferred"
|
|
47
|
+
if "progress" in raw_lower:
|
|
48
|
+
return "in_progress"
|
|
49
|
+
if "planning" in raw_lower:
|
|
50
|
+
return "planning"
|
|
51
|
+
return "pending"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _extract_current_focus(text: str) -> str | None:
|
|
55
|
+
"""Extract current epic focus from backlog content.
|
|
56
|
+
|
|
57
|
+
Looks for patterns like:
|
|
58
|
+
- **F&F Scope (Feb 9):** E8 → E7 → E9
|
|
59
|
+
- Epic: E8
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
text: Full backlog file content.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Epic ID (e.g., 'E8') if found, None otherwise.
|
|
66
|
+
"""
|
|
67
|
+
# Try F&F Scope pattern first
|
|
68
|
+
match = re.search(r"\*\*F&F Scope[^:]*:\*\*\s*(E\d+)", text)
|
|
69
|
+
if match:
|
|
70
|
+
return match.group(1)
|
|
71
|
+
|
|
72
|
+
# Try explicit Epic: pattern
|
|
73
|
+
match = re.search(r"^Epic:\s*(E\d+)", text, re.MULTILINE)
|
|
74
|
+
if match:
|
|
75
|
+
return match.group(1)
|
|
76
|
+
|
|
77
|
+
# Try "P0 (next)" pattern in table
|
|
78
|
+
match = re.search(
|
|
79
|
+
r"\|\s*(E\d+)\s*\|[^|]*\|[^|]*\|[^|]*\|\s*\*\*P0\s*\(next\)", text
|
|
80
|
+
)
|
|
81
|
+
if match:
|
|
82
|
+
return match.group(1)
|
|
83
|
+
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _extract_target_date(text: str) -> str | None:
|
|
88
|
+
"""Extract target date from backlog content.
|
|
89
|
+
|
|
90
|
+
Looks for patterns like:
|
|
91
|
+
- **F&F Scope (Feb 9):**
|
|
92
|
+
- Target: 2026-02-09
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
text: Full backlog file content.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Date string if found, None otherwise.
|
|
99
|
+
"""
|
|
100
|
+
# Try F&F Scope with date
|
|
101
|
+
match = re.search(r"\*\*F&F Scope\s*\(([^)]+)\)", text)
|
|
102
|
+
if match:
|
|
103
|
+
return match.group(1)
|
|
104
|
+
|
|
105
|
+
# Try explicit Target: pattern
|
|
106
|
+
match = re.search(r"Target:\s*(\d{4}-\d{2}-\d{2})", text)
|
|
107
|
+
if match:
|
|
108
|
+
return match.group(1)
|
|
109
|
+
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def extract_project(
|
|
114
|
+
file_path: Path, project_root: Path | None = None
|
|
115
|
+
) -> Concept | None:
|
|
116
|
+
"""Extract Project concept from backlog.md file.
|
|
117
|
+
|
|
118
|
+
Parses the backlog file header to extract project metadata including
|
|
119
|
+
name, status, current focus, and target date.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
file_path: Path to backlog.md file.
|
|
123
|
+
project_root: Project root for relative path calculation.
|
|
124
|
+
If None, uses file_path.parent.parent.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Project Concept if successfully parsed, None if file doesn't exist
|
|
128
|
+
or can't be parsed.
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
>>> from pathlib import Path
|
|
132
|
+
>>> backlog = Path("governance/backlog.md")
|
|
133
|
+
>>> project = extract_project(backlog)
|
|
134
|
+
>>> project.id
|
|
135
|
+
'project-raise-cli'
|
|
136
|
+
>>> project.metadata["current_epic"]
|
|
137
|
+
'E8'
|
|
138
|
+
"""
|
|
139
|
+
if not file_path.exists():
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
if project_root is None:
|
|
143
|
+
# governance/backlog.md -> project root is 2 levels up
|
|
144
|
+
project_root = file_path.parent.parent
|
|
145
|
+
|
|
146
|
+
text = file_path.read_text(encoding="utf-8")
|
|
147
|
+
lines = text.split("\n")
|
|
148
|
+
|
|
149
|
+
# Extract project name from H1: # Backlog: {name}
|
|
150
|
+
project_name = None
|
|
151
|
+
header_line = 1
|
|
152
|
+
for i, line in enumerate(lines, 1):
|
|
153
|
+
match = re.match(r"^# Backlog:\s*(.+)$", line)
|
|
154
|
+
if match:
|
|
155
|
+
project_name = match.group(1).strip()
|
|
156
|
+
header_line = i
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
# Fallback: extract from path
|
|
160
|
+
if not project_name:
|
|
161
|
+
project_name = file_path.parent.name
|
|
162
|
+
|
|
163
|
+
# Extract metadata
|
|
164
|
+
current_epic = _extract_current_focus(text)
|
|
165
|
+
target_date = _extract_target_date(text)
|
|
166
|
+
|
|
167
|
+
# Count epics in table (both E{N} and [RAISE-XXX](url) formats)
|
|
168
|
+
epic_count = len(
|
|
169
|
+
re.findall(
|
|
170
|
+
r"^\|\s*(?:E\d+|\[[A-Z]+-\d+\])\s*(?:\([^)]+\)\s*)?\|", text, re.MULTILINE
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Calculate relative file path
|
|
175
|
+
try:
|
|
176
|
+
relative_path = portable_path(file_path, project_root)
|
|
177
|
+
except ValueError:
|
|
178
|
+
relative_path = file_path.name
|
|
179
|
+
|
|
180
|
+
# Extract frontmatter for status
|
|
181
|
+
status = "active"
|
|
182
|
+
status_match = re.search(r">\s*\*\*Status\*\*:\s*(\w+)", text)
|
|
183
|
+
if status_match:
|
|
184
|
+
status = status_match.group(1).lower()
|
|
185
|
+
|
|
186
|
+
# Build content summary
|
|
187
|
+
content = f"Project backlog for {project_name}"
|
|
188
|
+
if current_epic:
|
|
189
|
+
content += f". Current focus: {current_epic}"
|
|
190
|
+
if target_date:
|
|
191
|
+
content += f". Target: {target_date}"
|
|
192
|
+
|
|
193
|
+
return Concept(
|
|
194
|
+
id=f"project-{project_name}",
|
|
195
|
+
type=ConceptType.PROJECT,
|
|
196
|
+
file=relative_path,
|
|
197
|
+
section=f"Backlog: {project_name}",
|
|
198
|
+
lines=(header_line, min(header_line + 10, len(lines))),
|
|
199
|
+
content=content,
|
|
200
|
+
metadata={
|
|
201
|
+
"name": project_name,
|
|
202
|
+
"status": status,
|
|
203
|
+
"current_epic": current_epic,
|
|
204
|
+
"target_date": target_date,
|
|
205
|
+
"epic_count": epic_count,
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def extract_epics(file_path: Path, project_root: Path | None = None) -> list[Concept]:
|
|
211
|
+
"""Extract Epic concepts from backlog.md Epics Overview table.
|
|
212
|
+
|
|
213
|
+
Parses the "Epics Overview" table to extract epic metadata. This extracts
|
|
214
|
+
the epic index only - full epic details come from epic scope docs (F8.2).
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
file_path: Path to backlog.md file.
|
|
218
|
+
project_root: Project root for relative path calculation.
|
|
219
|
+
If None, uses file_path.parent.parent.parent.parent.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of Epic Concepts extracted from the table. Returns empty list
|
|
223
|
+
if file doesn't exist or no epics found.
|
|
224
|
+
|
|
225
|
+
Examples:
|
|
226
|
+
>>> from pathlib import Path
|
|
227
|
+
>>> backlog = Path("governance/backlog.md")
|
|
228
|
+
>>> epics = extract_epics(backlog)
|
|
229
|
+
>>> len(epics)
|
|
230
|
+
9
|
|
231
|
+
>>> epics[0].metadata["epic_id"]
|
|
232
|
+
'E1'
|
|
233
|
+
"""
|
|
234
|
+
if not file_path.exists():
|
|
235
|
+
return []
|
|
236
|
+
|
|
237
|
+
if project_root is None:
|
|
238
|
+
project_root = file_path.parent.parent
|
|
239
|
+
|
|
240
|
+
text = file_path.read_text(encoding="utf-8")
|
|
241
|
+
lines = text.split("\n")
|
|
242
|
+
|
|
243
|
+
# Extract project name from H1: # Backlog: {name}
|
|
244
|
+
project_name = file_path.parent.name # Fallback
|
|
245
|
+
for line in lines:
|
|
246
|
+
h1_match = re.match(r"^# Backlog:\s*(.+)$", line)
|
|
247
|
+
if h1_match:
|
|
248
|
+
project_name = h1_match.group(1).strip()
|
|
249
|
+
break
|
|
250
|
+
|
|
251
|
+
# Calculate relative file path
|
|
252
|
+
try:
|
|
253
|
+
relative_path = portable_path(file_path, project_root)
|
|
254
|
+
except ValueError:
|
|
255
|
+
relative_path = file_path.name
|
|
256
|
+
|
|
257
|
+
concepts: list[Concept] = []
|
|
258
|
+
|
|
259
|
+
# Parse epic table rows
|
|
260
|
+
# Supports two ID formats:
|
|
261
|
+
# Simple: | E1 | **Name** | Status | ...
|
|
262
|
+
# Jira link: | [RAISE-275](url) | **Name** | Status | ...
|
|
263
|
+
epic_pattern = re.compile(
|
|
264
|
+
r"^\|\s*"
|
|
265
|
+
r"(?:\[([A-Z]+-\d+)\]\([^)]+\)|(E\d+))" # Jira link OR simple ID
|
|
266
|
+
r"\s*\|"
|
|
267
|
+
r"\s*\*?\*?([^|*]+?)\*?\*?\s*\|" # Epic name (with optional bold)
|
|
268
|
+
r"\s*([^|]+?)\s*\|" # Status
|
|
269
|
+
r"\s*([^|]*?)\s*\|" # Scope doc (optional)
|
|
270
|
+
r"\s*([^|]*?)\s*\|" # Priority (optional)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
for i, line in enumerate(lines, 1):
|
|
274
|
+
match = epic_pattern.match(line)
|
|
275
|
+
if match:
|
|
276
|
+
# Group 1 = Jira link ID (e.g., "RAISE-275"), Group 2 = simple ID (e.g., "E1")
|
|
277
|
+
epic_id = (match.group(1) or match.group(2)).strip()
|
|
278
|
+
|
|
279
|
+
# Yield to EpicScopeParser when a scope doc exists for this epic.
|
|
280
|
+
# The scope doc is the authoritative source (richer data); the backlog
|
|
281
|
+
# row is only an index entry. Without this check, both BacklogParser
|
|
282
|
+
# and EpicScopeParser produce the same node ID, causing duplicate
|
|
283
|
+
# warnings in `rai graph build`.
|
|
284
|
+
# Only applies to local epic IDs (E{N}), never to Jira keys (RAISE-275).
|
|
285
|
+
e_match = re.match(r"^E0*(\d+)$", epic_id, re.IGNORECASE)
|
|
286
|
+
if e_match:
|
|
287
|
+
canon = int(e_match.group(1)) # E08 -> 8, E1 -> 1
|
|
288
|
+
scope_hit = any(
|
|
289
|
+
re.search(r"^e0*(\d+)", d.name, re.IGNORECASE)
|
|
290
|
+
and int(re.search(r"^e0*(\d+)", d.name, re.IGNORECASE).group(1)) == canon # type: ignore[union-attr]
|
|
291
|
+
for d in project_root.glob("work/epics/e*")
|
|
292
|
+
if (d / "scope.md").exists()
|
|
293
|
+
)
|
|
294
|
+
if scope_hit:
|
|
295
|
+
continue
|
|
296
|
+
|
|
297
|
+
name = match.group(3).strip()
|
|
298
|
+
raw_status = match.group(4).strip()
|
|
299
|
+
scope_doc = match.group(5).strip()
|
|
300
|
+
priority = match.group(6).strip()
|
|
301
|
+
|
|
302
|
+
# Clean up scope doc (remove backticks)
|
|
303
|
+
scope_doc = scope_doc.strip("`").strip()
|
|
304
|
+
if scope_doc == "—" or scope_doc == "-" or not scope_doc:
|
|
305
|
+
scope_doc = None
|
|
306
|
+
|
|
307
|
+
# Clean up priority
|
|
308
|
+
priority = priority.strip("*").strip()
|
|
309
|
+
if priority == "—" or priority == "-" or not priority:
|
|
310
|
+
priority = None
|
|
311
|
+
|
|
312
|
+
# Normalize status
|
|
313
|
+
status = normalize_status(raw_status)
|
|
314
|
+
|
|
315
|
+
# Build content
|
|
316
|
+
content = f"{name} - {raw_status.strip()}"
|
|
317
|
+
|
|
318
|
+
concept = Concept(
|
|
319
|
+
id=f"epic-{epic_id.lower()}",
|
|
320
|
+
type=ConceptType.EPIC,
|
|
321
|
+
file=relative_path,
|
|
322
|
+
section=f"{epic_id}: {name}",
|
|
323
|
+
lines=(i, i),
|
|
324
|
+
content=content,
|
|
325
|
+
metadata={
|
|
326
|
+
"epic_id": epic_id,
|
|
327
|
+
"name": name,
|
|
328
|
+
"status": status,
|
|
329
|
+
"scope_doc": scope_doc,
|
|
330
|
+
"priority": priority,
|
|
331
|
+
"project_id": project_name,
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
concepts.append(concept)
|
|
335
|
+
|
|
336
|
+
return concepts
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class BacklogParser:
|
|
340
|
+
"""GovernanceParser wrapper for Backlog (project + epics)."""
|
|
341
|
+
|
|
342
|
+
def can_parse(self, locator: ArtifactLocator) -> bool:
|
|
343
|
+
"""Match Backlog artifact type."""
|
|
344
|
+
return locator.artifact_type == CoreArtifactType.BACKLOG
|
|
345
|
+
|
|
346
|
+
def parse(self, locator: ArtifactLocator) -> list[GraphNode]:
|
|
347
|
+
"""Parse Backlog file into GraphNode list (project + epics)."""
|
|
348
|
+
root = Path(locator.metadata["project_root"])
|
|
349
|
+
path = root / locator.path
|
|
350
|
+
nodes: list[GraphNode] = []
|
|
351
|
+
project = extract_project(path, root)
|
|
352
|
+
if project:
|
|
353
|
+
nodes.append(concept_to_node(project))
|
|
354
|
+
epics = extract_epics(path, root)
|
|
355
|
+
nodes.extend(concept_to_node(e) for e in epics)
|
|
356
|
+
return nodes
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Parser for Constitution documents.
|
|
2
|
+
|
|
3
|
+
Extracts principles in §N format from Constitution markdown files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from raise_cli.adapters.models import ArtifactLocator, CoreArtifactType
|
|
13
|
+
from raise_cli.compat import portable_path
|
|
14
|
+
from raise_cli.core.text import sanitize_id
|
|
15
|
+
from raise_cli.governance.models import Concept, ConceptType
|
|
16
|
+
from raise_cli.governance.parsers._convert import concept_to_node
|
|
17
|
+
from raise_core.graph.models import GraphNode
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def extract_principles(
|
|
21
|
+
file_path: Path, project_root: Path | None = None
|
|
22
|
+
) -> list[Concept]:
|
|
23
|
+
"""Extract §N principles from Constitution markdown file.
|
|
24
|
+
|
|
25
|
+
Parses Constitution markdown files looking for principle sections with the
|
|
26
|
+
format "### §N. Title" and extracts the principle content along with metadata.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
file_path: Path to Constitution markdown file.
|
|
30
|
+
project_root: Project root for relative path calculation.
|
|
31
|
+
If None, uses file_path.parent.parent.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
List of Concept objects representing principles.
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
>>> from pathlib import Path
|
|
38
|
+
>>> constitution = Path("framework/reference/constitution.md")
|
|
39
|
+
>>> principles = extract_principles(constitution)
|
|
40
|
+
>>> len(principles)
|
|
41
|
+
8
|
|
42
|
+
>>> principles[0].type
|
|
43
|
+
<ConceptType.PRINCIPLE: 'principle'>
|
|
44
|
+
"""
|
|
45
|
+
if not file_path.exists():
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
if project_root is None:
|
|
49
|
+
project_root = file_path.parent.parent
|
|
50
|
+
|
|
51
|
+
text = file_path.read_text(encoding="utf-8")
|
|
52
|
+
lines = text.split("\n")
|
|
53
|
+
concepts: list[Concept] = []
|
|
54
|
+
|
|
55
|
+
for i, line in enumerate(lines, 1):
|
|
56
|
+
# Match principle headers: ### §2. Governance as Code
|
|
57
|
+
match = re.match(r"^### §(\d+)\.\s*(.+)$", line)
|
|
58
|
+
|
|
59
|
+
if match:
|
|
60
|
+
principle_num = match.group(1) # "2"
|
|
61
|
+
principle_name = match.group(2).strip() # "Governance as Code"
|
|
62
|
+
|
|
63
|
+
# Extract section content until next ### heading
|
|
64
|
+
content_lines: list[str] = []
|
|
65
|
+
j = i
|
|
66
|
+
while j < len(lines) and not (lines[j].startswith("###") and j > i):
|
|
67
|
+
content_lines.append(lines[j])
|
|
68
|
+
j += 1
|
|
69
|
+
|
|
70
|
+
# Truncate to first ~30 lines or 500 chars
|
|
71
|
+
content = "\n".join(content_lines[:30])
|
|
72
|
+
if len(content) > 500:
|
|
73
|
+
content = content[:500] + "..."
|
|
74
|
+
|
|
75
|
+
# Generate ID from principle name
|
|
76
|
+
principle_id = sanitize_id(principle_name)
|
|
77
|
+
|
|
78
|
+
# Calculate relative file path
|
|
79
|
+
try:
|
|
80
|
+
relative_path = portable_path(file_path, project_root)
|
|
81
|
+
except ValueError:
|
|
82
|
+
relative_path = file_path.name
|
|
83
|
+
|
|
84
|
+
metadata: dict[str, Any] = {
|
|
85
|
+
"principle_number": principle_num,
|
|
86
|
+
"principle_name": principle_name,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Tag core principles as always_on (S15.8)
|
|
90
|
+
if principle_num in {"1", "3", "7"}:
|
|
91
|
+
metadata["always_on"] = True
|
|
92
|
+
|
|
93
|
+
concept = Concept(
|
|
94
|
+
id=f"principle-{principle_id}",
|
|
95
|
+
type=ConceptType.PRINCIPLE,
|
|
96
|
+
file=relative_path,
|
|
97
|
+
section=f"§{principle_num}. {principle_name}",
|
|
98
|
+
lines=(i, min(i + len(content_lines[:30]), len(lines))),
|
|
99
|
+
content=content.strip(),
|
|
100
|
+
metadata=metadata,
|
|
101
|
+
)
|
|
102
|
+
concepts.append(concept)
|
|
103
|
+
|
|
104
|
+
return concepts
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ConstitutionParser:
|
|
108
|
+
"""GovernanceParser wrapper for Constitution principles."""
|
|
109
|
+
|
|
110
|
+
def can_parse(self, locator: ArtifactLocator) -> bool:
|
|
111
|
+
"""Match Constitution artifact type."""
|
|
112
|
+
return locator.artifact_type == CoreArtifactType.CONSTITUTION
|
|
113
|
+
|
|
114
|
+
def parse(self, locator: ArtifactLocator) -> list[GraphNode]:
|
|
115
|
+
"""Parse Constitution file into GraphNode list."""
|
|
116
|
+
root = Path(locator.metadata["project_root"])
|
|
117
|
+
path = root / locator.path
|
|
118
|
+
concepts = extract_principles(path, root)
|
|
119
|
+
return [concept_to_node(c) for c in concepts]
|