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,141 @@
|
|
|
1
|
+
"""Diagnostic report generation and email submission."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import platform
|
|
7
|
+
import urllib.parse
|
|
8
|
+
import webbrowser
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from raise_cli.doctor.models import CheckResult, CheckStatus
|
|
14
|
+
|
|
15
|
+
SUPPORT_EMAIL = "support@raise.humansys.ai"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _str_list() -> list[str]:
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DiagnosticReport:
|
|
24
|
+
"""Non-sensitive diagnostic snapshot."""
|
|
25
|
+
|
|
26
|
+
timestamp: str
|
|
27
|
+
rai_version: str
|
|
28
|
+
python_version: str
|
|
29
|
+
os_info: str
|
|
30
|
+
check_results: list[CheckResult]
|
|
31
|
+
raise_structure: list[str] = field(default_factory=_str_list)
|
|
32
|
+
installed_extras: list[str] = field(default_factory=_str_list)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def generate_report(
|
|
36
|
+
results: list[CheckResult], working_dir: Path
|
|
37
|
+
) -> DiagnosticReport:
|
|
38
|
+
"""Collect non-sensitive data into report."""
|
|
39
|
+
# Get rai version
|
|
40
|
+
try:
|
|
41
|
+
from importlib.metadata import version
|
|
42
|
+
|
|
43
|
+
rai_ver = version("raise-cli")
|
|
44
|
+
except Exception: # noqa: BLE001
|
|
45
|
+
rai_ver = "unknown"
|
|
46
|
+
|
|
47
|
+
# Collect .raise/ file names (not contents)
|
|
48
|
+
raise_dir = working_dir / ".raise"
|
|
49
|
+
structure: list[str] = []
|
|
50
|
+
if raise_dir.exists():
|
|
51
|
+
for p in sorted(raise_dir.rglob("*")):
|
|
52
|
+
if p.is_file():
|
|
53
|
+
structure.append(str(p.relative_to(working_dir)))
|
|
54
|
+
|
|
55
|
+
# Detect installed extras via find_spec (no actual import)
|
|
56
|
+
extras: list[str] = []
|
|
57
|
+
for pkg in ("mcp", "httpx"):
|
|
58
|
+
if importlib.util.find_spec(pkg) is not None:
|
|
59
|
+
extras.append(pkg)
|
|
60
|
+
|
|
61
|
+
return DiagnosticReport(
|
|
62
|
+
timestamp=datetime.now(UTC).isoformat(),
|
|
63
|
+
rai_version=rai_ver,
|
|
64
|
+
python_version=platform.python_version(),
|
|
65
|
+
os_info=f"{platform.system()} {platform.release()} ({platform.machine()})",
|
|
66
|
+
check_results=results,
|
|
67
|
+
raise_structure=structure[:50], # cap at 50 files
|
|
68
|
+
installed_extras=extras,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def report_to_markdown(report: DiagnosticReport) -> str:
|
|
73
|
+
"""Render report as markdown."""
|
|
74
|
+
extras_str = ", ".join(report.installed_extras) if report.installed_extras else "none"
|
|
75
|
+
lines = [
|
|
76
|
+
"# rai doctor report",
|
|
77
|
+
"",
|
|
78
|
+
f"**Timestamp:** {report.timestamp}",
|
|
79
|
+
f"**raise-cli:** {report.rai_version}",
|
|
80
|
+
f"**Python:** {report.python_version}",
|
|
81
|
+
f"**OS:** {report.os_info}",
|
|
82
|
+
f"**Extras:** {extras_str}",
|
|
83
|
+
"",
|
|
84
|
+
"## Check Results",
|
|
85
|
+
"",
|
|
86
|
+
]
|
|
87
|
+
for r in report.check_results:
|
|
88
|
+
icon = {"pass": "OK", "warn": "!!", "error": "XX"}[r.status.value]
|
|
89
|
+
lines.append(f"- [{icon}] {r.category}: {r.message}")
|
|
90
|
+
if r.fix_hint:
|
|
91
|
+
lines.append(f" - hint: {r.fix_hint}")
|
|
92
|
+
|
|
93
|
+
if report.raise_structure:
|
|
94
|
+
lines.extend(["", "## .raise/ structure", ""])
|
|
95
|
+
for path in report.raise_structure:
|
|
96
|
+
lines.append(f"- {path}")
|
|
97
|
+
|
|
98
|
+
return "\n".join(lines)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def save_report(report: DiagnosticReport, working_dir: Path) -> Path:
|
|
102
|
+
"""Save report to .raise/rai/personal/report-{date}.md. Returns path."""
|
|
103
|
+
personal_dir = working_dir / ".raise" / "rai" / "personal"
|
|
104
|
+
personal_dir.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
date_str = datetime.now(UTC).strftime("%Y-%m-%d")
|
|
106
|
+
path = personal_dir / f"report-{date_str}.md"
|
|
107
|
+
path.write_text(report_to_markdown(report))
|
|
108
|
+
return path
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def open_mailto(report: DiagnosticReport, to: str = SUPPORT_EMAIL) -> bool:
|
|
112
|
+
"""Open default email client via mailto: URI. Returns True if opened."""
|
|
113
|
+
md = report_to_markdown(report)
|
|
114
|
+
|
|
115
|
+
# Count issues for subject
|
|
116
|
+
warns = sum(1 for r in report.check_results if r.status == CheckStatus.WARN)
|
|
117
|
+
errors = sum(1 for r in report.check_results if r.status == CheckStatus.ERROR)
|
|
118
|
+
parts: list[str] = []
|
|
119
|
+
if errors:
|
|
120
|
+
parts.append(f"{errors} error{'s' if errors > 1 else ''}")
|
|
121
|
+
if warns:
|
|
122
|
+
parts.append(f"{warns} warning{'s' if warns > 1 else ''}")
|
|
123
|
+
issue_summary = ", ".join(parts) if parts else "all clear"
|
|
124
|
+
|
|
125
|
+
subject = f"[rai-doctor] {report.rai_version} — {issue_summary}"
|
|
126
|
+
|
|
127
|
+
# Truncate body for mailto (most clients cap at ~2000 chars in URL)
|
|
128
|
+
body = md[:1800]
|
|
129
|
+
if len(md) > 1800:
|
|
130
|
+
body += "\n\n(truncated — full report in local file)"
|
|
131
|
+
|
|
132
|
+
mailto_url = (
|
|
133
|
+
f"mailto:{to}"
|
|
134
|
+
f"?subject={urllib.parse.quote(subject)}"
|
|
135
|
+
f"&body={urllib.parse.quote(body)}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
return webbrowser.open(mailto_url)
|
|
140
|
+
except Exception: # noqa: BLE001
|
|
141
|
+
return False
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Doctor check runner — executes checks in pipeline order.
|
|
2
|
+
|
|
3
|
+
Pipeline order ensures dependencies: environment must pass before
|
|
4
|
+
project checks, project before adapters, etc.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from raise_cli.doctor.models import CheckResult, CheckStatus, DoctorContext
|
|
12
|
+
from raise_cli.doctor.registry import CheckRegistry
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
PIPELINE_ORDER: list[str] = [
|
|
17
|
+
"environment",
|
|
18
|
+
"project",
|
|
19
|
+
"adapters",
|
|
20
|
+
"skills",
|
|
21
|
+
"mcp",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_checks(
|
|
26
|
+
registry: CheckRegistry,
|
|
27
|
+
context: DoctorContext,
|
|
28
|
+
categories: list[str] | None = None,
|
|
29
|
+
) -> list[CheckResult]:
|
|
30
|
+
"""Execute checks in pipeline order.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
registry: Check registry with discovered checks.
|
|
34
|
+
context: Execution context (working_dir, online, verbose).
|
|
35
|
+
categories: If provided, only run checks in these categories.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
All check results in execution order.
|
|
39
|
+
"""
|
|
40
|
+
results: list[CheckResult] = []
|
|
41
|
+
critical_failure = False
|
|
42
|
+
|
|
43
|
+
ordered = categories or PIPELINE_ORDER
|
|
44
|
+
|
|
45
|
+
for category in ordered:
|
|
46
|
+
if critical_failure:
|
|
47
|
+
results.append(
|
|
48
|
+
CheckResult(
|
|
49
|
+
check_id=f"{category}-skipped",
|
|
50
|
+
category=category,
|
|
51
|
+
status=CheckStatus.WARN,
|
|
52
|
+
message="Skipped — previous category had critical errors",
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
checks = registry.get_checks_for_category(category)
|
|
58
|
+
if not checks:
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
for check in checks:
|
|
62
|
+
if check.requires_online and not context.online:
|
|
63
|
+
logger.debug("Skipping online check '%s'", check.check_id)
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
check_results = check.evaluate(context)
|
|
68
|
+
results.extend(check_results)
|
|
69
|
+
except Exception as exc: # noqa: BLE001
|
|
70
|
+
logger.warning("Check '%s' raised: %s", check.check_id, exc)
|
|
71
|
+
results.append(
|
|
72
|
+
CheckResult(
|
|
73
|
+
check_id=check.check_id,
|
|
74
|
+
category=check.category,
|
|
75
|
+
status=CheckStatus.ERROR,
|
|
76
|
+
message=f"Check crashed: {exc}",
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# If any ERROR in this category, flag for downstream skip
|
|
81
|
+
category_errors = [
|
|
82
|
+
r for r in results if r.category == category and r.status == CheckStatus.ERROR
|
|
83
|
+
]
|
|
84
|
+
if category_errors and category == "environment":
|
|
85
|
+
critical_failure = True
|
|
86
|
+
|
|
87
|
+
return results
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def summarize(results: list[CheckResult]) -> tuple[int, int, int]:
|
|
91
|
+
"""Count pass/warn/error results. Returns (pass_count, warn_count, error_count)."""
|
|
92
|
+
passes = sum(1 for r in results if r.status == CheckStatus.PASS)
|
|
93
|
+
warns = sum(1 for r in results if r.status == CheckStatus.WARN)
|
|
94
|
+
errors = sum(1 for r in results if r.status == CheckStatus.ERROR)
|
|
95
|
+
return passes, warns, errors
|
raise_cli/exceptions.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Centralized exception hierarchy for raise-cli.
|
|
2
|
+
|
|
3
|
+
This module defines all exceptions used throughout raise-cli with:
|
|
4
|
+
- Consistent exit codes for scripting
|
|
5
|
+
- Error codes for documentation/troubleshooting
|
|
6
|
+
- Optional hints and details for user guidance
|
|
7
|
+
|
|
8
|
+
Exit Code Table:
|
|
9
|
+
0 - Success
|
|
10
|
+
1 - General error (RaiError)
|
|
11
|
+
2 - Configuration error
|
|
12
|
+
3 - Resource not found (kata, gate)
|
|
13
|
+
4 - Artifact not found
|
|
14
|
+
5 - Dependency unavailable
|
|
15
|
+
6 - State corruption
|
|
16
|
+
7 - Validation error
|
|
17
|
+
10 - Gate failed
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RaiError(Exception):
|
|
26
|
+
"""Base exception for all raise-cli errors.
|
|
27
|
+
|
|
28
|
+
All raise-cli exceptions inherit from this class, providing:
|
|
29
|
+
- exit_code: Process exit code for scripting
|
|
30
|
+
- error_code: Unique identifier for documentation
|
|
31
|
+
- message: Human-readable error description
|
|
32
|
+
- hint: Optional suggestion for resolution
|
|
33
|
+
- details: Optional structured data for debugging
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> raise RaiError("Something went wrong", hint="Try again")
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
exit_code: int = 1
|
|
40
|
+
error_code: str = "E000"
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
message: str,
|
|
45
|
+
*,
|
|
46
|
+
hint: str | None = None,
|
|
47
|
+
details: dict[str, Any] | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Initialize a RaiError.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
message: Human-readable error description.
|
|
53
|
+
hint: Optional suggestion for resolution.
|
|
54
|
+
details: Optional structured data for debugging.
|
|
55
|
+
"""
|
|
56
|
+
self.message = message
|
|
57
|
+
self.hint = hint
|
|
58
|
+
self.details = details or {}
|
|
59
|
+
super().__init__(message)
|
|
60
|
+
|
|
61
|
+
def __str__(self) -> str:
|
|
62
|
+
"""Return the error message."""
|
|
63
|
+
return self.message
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> dict[str, Any]:
|
|
66
|
+
"""Convert exception to dictionary for JSON output.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Dictionary with error_code, exit_code, message, hint, and details.
|
|
70
|
+
"""
|
|
71
|
+
return {
|
|
72
|
+
"error_code": self.error_code,
|
|
73
|
+
"exit_code": self.exit_code,
|
|
74
|
+
"message": self.message,
|
|
75
|
+
"hint": self.hint,
|
|
76
|
+
"details": self.details,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ConfigurationError(RaiError):
|
|
81
|
+
"""Configuration-related errors.
|
|
82
|
+
|
|
83
|
+
Raised when:
|
|
84
|
+
- Config file is malformed
|
|
85
|
+
- Required configuration is missing
|
|
86
|
+
- Configuration values are invalid
|
|
87
|
+
|
|
88
|
+
Exit code: 2
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
exit_code: int = 2
|
|
92
|
+
error_code: str = "E001"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class KataNotFoundError(RaiError):
|
|
96
|
+
"""Kata definition not found.
|
|
97
|
+
|
|
98
|
+
Raised when a requested kata ID does not exist in .raise/katas/.
|
|
99
|
+
|
|
100
|
+
Exit code: 3
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
exit_code: int = 3
|
|
104
|
+
error_code: str = "E002"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class GateNotFoundError(RaiError):
|
|
108
|
+
"""Gate definition not found.
|
|
109
|
+
|
|
110
|
+
Raised when a requested gate ID does not exist in .raise/gates/.
|
|
111
|
+
|
|
112
|
+
Exit code: 3
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
exit_code: int = 3
|
|
116
|
+
error_code: str = "E003"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ArtifactNotFoundError(RaiError):
|
|
120
|
+
"""Artifact file not found.
|
|
121
|
+
|
|
122
|
+
Raised when a referenced artifact path does not exist.
|
|
123
|
+
|
|
124
|
+
Exit code: 4
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
exit_code: int = 4
|
|
128
|
+
error_code: str = "E004"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class DependencyError(RaiError):
|
|
132
|
+
"""External dependency not available.
|
|
133
|
+
|
|
134
|
+
Raised when a required external tool is not installed or accessible:
|
|
135
|
+
- git
|
|
136
|
+
- ast-grep
|
|
137
|
+
- ripgrep
|
|
138
|
+
|
|
139
|
+
Exit code: 5
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
exit_code: int = 5
|
|
143
|
+
error_code: str = "E005"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class StateError(RaiError):
|
|
147
|
+
"""State file corrupted or invalid.
|
|
148
|
+
|
|
149
|
+
Raised when:
|
|
150
|
+
- State file cannot be parsed
|
|
151
|
+
- State schema validation fails
|
|
152
|
+
- State version mismatch
|
|
153
|
+
|
|
154
|
+
Exit code: 6
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
exit_code: int = 6
|
|
158
|
+
error_code: str = "E006"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ValidationError(RaiError):
|
|
162
|
+
"""Schema or artifact validation failed.
|
|
163
|
+
|
|
164
|
+
Raised when:
|
|
165
|
+
- Pydantic schema validation fails
|
|
166
|
+
- Artifact content validation fails
|
|
167
|
+
- Input format is invalid
|
|
168
|
+
|
|
169
|
+
Exit code: 7
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
exit_code: int = 7
|
|
173
|
+
error_code: str = "E007"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class GateFailedError(RaiError):
|
|
177
|
+
"""Gate validation did not pass.
|
|
178
|
+
|
|
179
|
+
Raised when one or more required gate criteria fail.
|
|
180
|
+
This is a "business logic" failure, not a system error.
|
|
181
|
+
|
|
182
|
+
Exit code: 10
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
exit_code: int = 10
|
|
186
|
+
error_code: str = "E010"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class RaiSessionNotFoundError(RaiError):
|
|
190
|
+
"""Session ID not provided or resolvable.
|
|
191
|
+
|
|
192
|
+
Raised when:
|
|
193
|
+
- Neither --session flag nor RAI_SESSION_ID env var is provided
|
|
194
|
+
- Session ID is required but cannot be determined
|
|
195
|
+
|
|
196
|
+
Exit code: 2 (configuration error - user must provide session context)
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
exit_code: int = 2
|
|
200
|
+
error_code: str = "E011"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# All public exceptions
|
|
204
|
+
__all__ = [
|
|
205
|
+
"RaiError",
|
|
206
|
+
"ConfigurationError",
|
|
207
|
+
"KataNotFoundError",
|
|
208
|
+
"GateNotFoundError",
|
|
209
|
+
"ArtifactNotFoundError",
|
|
210
|
+
"DependencyError",
|
|
211
|
+
"StateError",
|
|
212
|
+
"ValidationError",
|
|
213
|
+
"GateFailedError",
|
|
214
|
+
"RaiSessionNotFoundError",
|
|
215
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Workflow gates infrastructure for raise-cli.
|
|
2
|
+
|
|
3
|
+
Provides standalone quality gates that validate workflow transitions.
|
|
4
|
+
Gates are independent of the event emitter (AD-5) and can be invoked
|
|
5
|
+
directly via ``rai gate check``.
|
|
6
|
+
|
|
7
|
+
Architecture: ADR-039 §1 (WorkflowGate Protocol), §5 (Standalone gates)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from raise_cli.gates.models import GateContext, GateResult
|
|
11
|
+
from raise_cli.gates.protocol import WorkflowGate
|
|
12
|
+
from raise_cli.gates.registry import GateRegistry
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"GateContext",
|
|
16
|
+
"GateRegistry",
|
|
17
|
+
"GateResult",
|
|
18
|
+
"WorkflowGate",
|
|
19
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Built-in quality gates for raise-cli (COMMUNITY tier)."""
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Built-in CoverageGate — validates test coverage collection succeeds.
|
|
2
|
+
|
|
3
|
+
Runs ``pytest --cov --cov-report=term-missing -q`` and reports pass/fail.
|
|
4
|
+
Per PAT-E-444, coverage is diagnostic — this gate checks that coverage
|
|
5
|
+
collection succeeds, not a specific percentage threshold.
|
|
6
|
+
|
|
7
|
+
Architecture: ADR-039 §5 (Built-in gates), S248.6
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import subprocess
|
|
13
|
+
from typing import ClassVar
|
|
14
|
+
|
|
15
|
+
from raise_cli.gates.models import GateContext, GateResult
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CoverageGate:
|
|
19
|
+
"""Quality gate that runs pytest with coverage.
|
|
20
|
+
|
|
21
|
+
Registered via ``rai.gates`` entry point in pyproject.toml.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
gate_id: ClassVar[str] = "gate-coverage"
|
|
25
|
+
description: ClassVar[str] = "Coverage collection succeeds"
|
|
26
|
+
workflow_point: ClassVar[str] = "before:release:publish"
|
|
27
|
+
|
|
28
|
+
def evaluate(self, context: GateContext) -> GateResult:
|
|
29
|
+
"""Run pytest --cov and return pass/fail result."""
|
|
30
|
+
try:
|
|
31
|
+
result = subprocess.run(
|
|
32
|
+
["pytest", "--cov", "--cov-report=term-missing", "-q"],
|
|
33
|
+
capture_output=True,
|
|
34
|
+
text=True,
|
|
35
|
+
cwd=str(context.working_dir),
|
|
36
|
+
)
|
|
37
|
+
except Exception as exc: # noqa: BLE001
|
|
38
|
+
return GateResult(
|
|
39
|
+
passed=False,
|
|
40
|
+
gate_id=self.gate_id,
|
|
41
|
+
message=f"{type(exc).__name__}: {exc}",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
passed = result.returncode == 0
|
|
45
|
+
return GateResult(
|
|
46
|
+
passed=passed,
|
|
47
|
+
gate_id=self.gate_id,
|
|
48
|
+
message="Coverage collection succeeds"
|
|
49
|
+
if passed
|
|
50
|
+
else "Coverage check failed",
|
|
51
|
+
details=(result.stdout,) if not passed else (),
|
|
52
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Built-in LintGate — validates linting passes.
|
|
2
|
+
|
|
3
|
+
Runs ``ruff check .`` and reports pass/fail.
|
|
4
|
+
|
|
5
|
+
Architecture: ADR-039 §5 (Built-in gates), S248.6
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import subprocess
|
|
11
|
+
from typing import ClassVar
|
|
12
|
+
|
|
13
|
+
from raise_cli.gates.models import GateContext, GateResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LintGate:
|
|
17
|
+
"""Quality gate that runs ruff.
|
|
18
|
+
|
|
19
|
+
Registered via ``rai.gates`` entry point in pyproject.toml.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
gate_id: ClassVar[str] = "gate-lint"
|
|
23
|
+
description: ClassVar[str] = "Linting passes"
|
|
24
|
+
workflow_point: ClassVar[str] = "before:release:publish"
|
|
25
|
+
|
|
26
|
+
def evaluate(self, context: GateContext) -> GateResult:
|
|
27
|
+
"""Run ruff check and return pass/fail result."""
|
|
28
|
+
try:
|
|
29
|
+
result = subprocess.run(
|
|
30
|
+
["ruff", "check", "."],
|
|
31
|
+
capture_output=True,
|
|
32
|
+
text=True,
|
|
33
|
+
cwd=str(context.working_dir),
|
|
34
|
+
)
|
|
35
|
+
except Exception as exc: # noqa: BLE001
|
|
36
|
+
return GateResult(
|
|
37
|
+
passed=False,
|
|
38
|
+
gate_id=self.gate_id,
|
|
39
|
+
message=f"{type(exc).__name__}: {exc}",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
passed = result.returncode == 0
|
|
43
|
+
return GateResult(
|
|
44
|
+
passed=passed,
|
|
45
|
+
gate_id=self.gate_id,
|
|
46
|
+
message="Linting passes" if passed else "Lint errors found",
|
|
47
|
+
details=(result.stdout,) if not passed else (),
|
|
48
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Built-in TestGate — validates all tests pass.
|
|
2
|
+
|
|
3
|
+
Runs ``pytest -x --tb=short`` and reports pass/fail.
|
|
4
|
+
|
|
5
|
+
Architecture: ADR-039 §5 (Built-in gates), S248.6
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import subprocess
|
|
11
|
+
from typing import ClassVar
|
|
12
|
+
|
|
13
|
+
from raise_cli.gates.models import GateContext, GateResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestGate:
|
|
17
|
+
"""Quality gate that runs pytest.
|
|
18
|
+
|
|
19
|
+
Registered via ``rai.gates`` entry point in pyproject.toml.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
gate_id: ClassVar[str] = "gate-tests"
|
|
23
|
+
description: ClassVar[str] = "All tests pass"
|
|
24
|
+
workflow_point: ClassVar[str] = "before:release:publish"
|
|
25
|
+
|
|
26
|
+
def evaluate(self, context: GateContext) -> GateResult:
|
|
27
|
+
"""Run pytest and return pass/fail result."""
|
|
28
|
+
try:
|
|
29
|
+
result = subprocess.run(
|
|
30
|
+
["pytest", "-x", "--tb=short"],
|
|
31
|
+
capture_output=True,
|
|
32
|
+
text=True,
|
|
33
|
+
cwd=str(context.working_dir),
|
|
34
|
+
)
|
|
35
|
+
except Exception as exc: # noqa: BLE001
|
|
36
|
+
return GateResult(
|
|
37
|
+
passed=False,
|
|
38
|
+
gate_id=self.gate_id,
|
|
39
|
+
message=f"{type(exc).__name__}: {exc}",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
passed = result.returncode == 0
|
|
43
|
+
return GateResult(
|
|
44
|
+
passed=passed,
|
|
45
|
+
gate_id=self.gate_id,
|
|
46
|
+
message="Tests pass" if passed else "Tests failing",
|
|
47
|
+
details=(result.stdout,) if not passed else (),
|
|
48
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Built-in TypeGate — validates no type errors.
|
|
2
|
+
|
|
3
|
+
Runs ``pyright`` and reports pass/fail.
|
|
4
|
+
|
|
5
|
+
Architecture: ADR-039 §5 (Built-in gates), S248.6
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import subprocess
|
|
11
|
+
from typing import ClassVar
|
|
12
|
+
|
|
13
|
+
from raise_cli.gates.models import GateContext, GateResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TypeGate:
|
|
17
|
+
"""Quality gate that runs pyright.
|
|
18
|
+
|
|
19
|
+
Registered via ``rai.gates`` entry point in pyproject.toml.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
gate_id: ClassVar[str] = "gate-types"
|
|
23
|
+
description: ClassVar[str] = "No type errors"
|
|
24
|
+
workflow_point: ClassVar[str] = "before:release:publish"
|
|
25
|
+
|
|
26
|
+
def evaluate(self, context: GateContext) -> GateResult:
|
|
27
|
+
"""Run pyright and return pass/fail result."""
|
|
28
|
+
try:
|
|
29
|
+
result = subprocess.run(
|
|
30
|
+
["pyright"],
|
|
31
|
+
capture_output=True,
|
|
32
|
+
text=True,
|
|
33
|
+
cwd=str(context.working_dir),
|
|
34
|
+
)
|
|
35
|
+
except Exception as exc: # noqa: BLE001
|
|
36
|
+
return GateResult(
|
|
37
|
+
passed=False,
|
|
38
|
+
gate_id=self.gate_id,
|
|
39
|
+
message=f"{type(exc).__name__}: {exc}",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
passed = result.returncode == 0
|
|
43
|
+
return GateResult(
|
|
44
|
+
passed=passed,
|
|
45
|
+
gate_id=self.gate_id,
|
|
46
|
+
message="No type errors" if passed else "Type errors found",
|
|
47
|
+
details=(result.stdout,) if not passed else (),
|
|
48
|
+
)
|