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