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,153 @@
|
|
|
1
|
+
"""Generic entry-point resolver for adapters and targets.
|
|
2
|
+
|
|
3
|
+
Parametrized auto-detect logic:
|
|
4
|
+
- 1 registered → auto-select
|
|
5
|
+
- 0 registered → error with install guidance
|
|
6
|
+
- 2+ registered → error listing names, request flag
|
|
7
|
+
- flag → select by name (override)
|
|
8
|
+
|
|
9
|
+
Auto-wraps async implementations with the provided sync wrapper.
|
|
10
|
+
|
|
11
|
+
Replaces ``_adapter_resolve.py`` (S301.4, D7 — DRY).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import inspect
|
|
17
|
+
import logging
|
|
18
|
+
import sys
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
|
|
25
|
+
from raise_cli.adapters.protocols import DocumentationTarget, ProjectManagementAdapter
|
|
26
|
+
from raise_cli.adapters.registry import get_doc_targets, get_pm_adapters
|
|
27
|
+
from raise_cli.adapters.sync import SyncDocsAdapter, SyncPMAdapter
|
|
28
|
+
from raise_cli.onboarding.manifest import load_manifest
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
console = Console()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def resolve_entrypoint(
|
|
36
|
+
discover: Callable[[], dict[str, Callable[[], Any]]],
|
|
37
|
+
sync_wrapper: type | None,
|
|
38
|
+
async_check_method: str,
|
|
39
|
+
group_label: str,
|
|
40
|
+
flag_name: str,
|
|
41
|
+
selected: str | None,
|
|
42
|
+
) -> Any:
|
|
43
|
+
"""Resolve and instantiate an adapter/target from entry points and YAML configs.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
discover: Function returning {name: factory} from entry points and/or YAML.
|
|
47
|
+
sync_wrapper: Wrapper class for async→sync bridging (or None).
|
|
48
|
+
async_check_method: Method name to check for async (e.g., "get_issue").
|
|
49
|
+
group_label: Human label for error messages (e.g., "PM adapter").
|
|
50
|
+
flag_name: CLI flag name for error messages (e.g., "--adapter").
|
|
51
|
+
selected: Explicit selection by name, or None for auto-detect.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
An instantiated adapter/target, wrapped if async.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
SystemExit: If resolution fails.
|
|
58
|
+
"""
|
|
59
|
+
entries = discover()
|
|
60
|
+
|
|
61
|
+
if selected is not None:
|
|
62
|
+
cls = entries.get(selected)
|
|
63
|
+
if cls is None:
|
|
64
|
+
available = ", ".join(sorted(entries)) if entries else "none"
|
|
65
|
+
console.print(
|
|
66
|
+
f"[red]Error:[/red] {group_label} '{selected}' not found. "
|
|
67
|
+
f"Available: {available}"
|
|
68
|
+
)
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
elif len(entries) == 0:
|
|
71
|
+
console.print(
|
|
72
|
+
f"[red]Error:[/red] No {group_label} installed.\n"
|
|
73
|
+
f"Install one or register via entry points. Use {flag_name} to select."
|
|
74
|
+
)
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
elif len(entries) == 1:
|
|
77
|
+
cls = next(iter(entries.values()))
|
|
78
|
+
else:
|
|
79
|
+
names = ", ".join(sorted(entries))
|
|
80
|
+
console.print(
|
|
81
|
+
f"[red]Error:[/red] Multiple {group_label}s found: {names}.\n"
|
|
82
|
+
f"Use {flag_name} <name> to select."
|
|
83
|
+
)
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
instance = cls()
|
|
88
|
+
except Exception as exc:
|
|
89
|
+
name = selected or next(iter(entries))
|
|
90
|
+
console.print(
|
|
91
|
+
f"[red]Error:[/red] Failed to instantiate {group_label} '{name}': {exc}"
|
|
92
|
+
)
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
|
|
95
|
+
# Auto-wrap async implementations for sync CLI consumption
|
|
96
|
+
if sync_wrapper and inspect.iscoroutinefunction(
|
|
97
|
+
getattr(instance, async_check_method, None)
|
|
98
|
+
):
|
|
99
|
+
instance = sync_wrapper(instance)
|
|
100
|
+
|
|
101
|
+
return instance
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _discover_pm() -> dict[str, Callable[[], Any]]:
|
|
105
|
+
"""Merge YAML and entry point PM adapters. EP wins on name collision."""
|
|
106
|
+
from raise_cli.adapters.declarative.discovery import discover_yaml_adapters
|
|
107
|
+
|
|
108
|
+
return {**discover_yaml_adapters("pm"), **get_pm_adapters()}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _discover_docs() -> dict[str, Callable[[], Any]]:
|
|
112
|
+
"""Merge YAML and entry point docs targets. EP wins on name collision."""
|
|
113
|
+
from raise_cli.adapters.declarative.discovery import discover_yaml_adapters
|
|
114
|
+
|
|
115
|
+
return {**discover_yaml_adapters("docs"), **get_doc_targets()}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def resolve_adapter(adapter_name: str | None) -> ProjectManagementAdapter:
|
|
119
|
+
"""Resolve a ProjectManagementAdapter from entry points and YAML configs.
|
|
120
|
+
|
|
121
|
+
Resolution priority:
|
|
122
|
+
1. Explicit adapter_name (from -a/--adapter flag) → use it
|
|
123
|
+
2. Manifest backlog.adapter_default → use it
|
|
124
|
+
3. Auto-detect (single adapter) or error (0 / 2+)
|
|
125
|
+
"""
|
|
126
|
+
effective = adapter_name
|
|
127
|
+
|
|
128
|
+
if effective is None:
|
|
129
|
+
manifest = load_manifest(Path.cwd())
|
|
130
|
+
if manifest and manifest.backlog and manifest.backlog.adapter_default:
|
|
131
|
+
effective = manifest.backlog.adapter_default
|
|
132
|
+
logger.debug("Using manifest default adapter: %s", effective)
|
|
133
|
+
|
|
134
|
+
return resolve_entrypoint(
|
|
135
|
+
discover=_discover_pm,
|
|
136
|
+
sync_wrapper=SyncPMAdapter,
|
|
137
|
+
async_check_method="get_issue",
|
|
138
|
+
group_label="PM adapter",
|
|
139
|
+
flag_name="--adapter",
|
|
140
|
+
selected=effective,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def resolve_docs_target(target_name: str | None) -> DocumentationTarget:
|
|
145
|
+
"""Resolve a DocumentationTarget from entry points and YAML configs."""
|
|
146
|
+
return resolve_entrypoint(
|
|
147
|
+
discover=_discover_docs,
|
|
148
|
+
sync_wrapper=SyncDocsAdapter,
|
|
149
|
+
async_check_method="get_page",
|
|
150
|
+
group_label="docs target",
|
|
151
|
+
flag_name="--target",
|
|
152
|
+
selected=target_name,
|
|
153
|
+
)
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""CLI commands for adapter discovery and validation.
|
|
2
|
+
|
|
3
|
+
Provides `rai adapter list`, `rai adapter check`, `rai adapter validate`,
|
|
4
|
+
and `rai adapter status` for inspecting, checking, and validating adapter
|
|
5
|
+
configurations.
|
|
6
|
+
|
|
7
|
+
Architecture: ADR-033 (PM), ADR-034 (Governance), ADR-036 (Graph Backend), ADR-041 (Declarative)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import inspect
|
|
13
|
+
import os
|
|
14
|
+
from importlib.metadata import entry_points
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Annotated, Any
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
from raise_cli.adapters.protocols import (
|
|
22
|
+
DocumentationTarget,
|
|
23
|
+
GovernanceParser,
|
|
24
|
+
GovernanceSchemaProvider,
|
|
25
|
+
ProjectManagementAdapter,
|
|
26
|
+
)
|
|
27
|
+
from raise_cli.adapters.registry import (
|
|
28
|
+
EP_DOC_TARGETS,
|
|
29
|
+
EP_GOVERNANCE_PARSERS,
|
|
30
|
+
EP_GOVERNANCE_SCHEMAS,
|
|
31
|
+
EP_GRAPH_BACKENDS,
|
|
32
|
+
EP_PM_ADAPTERS,
|
|
33
|
+
)
|
|
34
|
+
from raise_cli.hooks.emitter import create_emitter
|
|
35
|
+
from raise_cli.hooks.events import AdapterFailedEvent, AdapterLoadedEvent
|
|
36
|
+
from raise_cli.output.formatters.adapters import (
|
|
37
|
+
format_check_human,
|
|
38
|
+
format_check_json,
|
|
39
|
+
format_list_human,
|
|
40
|
+
format_list_json,
|
|
41
|
+
format_validate_human,
|
|
42
|
+
)
|
|
43
|
+
from raise_cli.tier.context import TierContext
|
|
44
|
+
from raise_core.graph.backends.protocol import KnowledgeGraphBackend
|
|
45
|
+
|
|
46
|
+
adapters_app = typer.Typer(
|
|
47
|
+
name="adapter",
|
|
48
|
+
help="Inspect and validate registered adapters",
|
|
49
|
+
no_args_is_help=True,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
console = Console()
|
|
53
|
+
|
|
54
|
+
# Group → (Protocol display name, Protocol class).
|
|
55
|
+
# Only consumer of this mapping — lives here per arch review Q1.
|
|
56
|
+
ADAPTER_GROUPS: dict[str, tuple[str, type]] = {
|
|
57
|
+
EP_GOVERNANCE_PARSERS: ("GovernanceParser", GovernanceParser),
|
|
58
|
+
EP_GRAPH_BACKENDS: ("KnowledgeGraphBackend", KnowledgeGraphBackend),
|
|
59
|
+
EP_PM_ADAPTERS: ("ProjectManagementAdapter", ProjectManagementAdapter),
|
|
60
|
+
EP_GOVERNANCE_SCHEMAS: ("GovernanceSchemaProvider", GovernanceSchemaProvider),
|
|
61
|
+
EP_DOC_TARGETS: ("DocumentationTarget", DocumentationTarget),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _get_dist_name(ep: Any) -> str:
|
|
66
|
+
"""Best-effort extraction of distribution name from an entry point."""
|
|
67
|
+
try:
|
|
68
|
+
return ep.dist.name # type: ignore[union-attr]
|
|
69
|
+
except AttributeError:
|
|
70
|
+
return "unknown"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _collect_groups() -> list[dict[str, Any]]:
|
|
74
|
+
"""Collect adapter info for all entry point groups."""
|
|
75
|
+
groups: list[dict[str, Any]] = []
|
|
76
|
+
for group, (proto_name, _proto_cls) in ADAPTER_GROUPS.items():
|
|
77
|
+
adapters: list[dict[str, str]] = []
|
|
78
|
+
for ep in entry_points(group=group):
|
|
79
|
+
adapters.append({"name": ep.name, "package": _get_dist_name(ep)})
|
|
80
|
+
groups.append(
|
|
81
|
+
{
|
|
82
|
+
"group": group,
|
|
83
|
+
"protocol_name": proto_name,
|
|
84
|
+
"adapters": adapters,
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
return groups
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_tier() -> str:
|
|
91
|
+
"""Get current tier level string."""
|
|
92
|
+
ctx = TierContext.from_manifest(Path.cwd())
|
|
93
|
+
return ctx.tier.value
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@adapters_app.command("list")
|
|
97
|
+
def list_command(
|
|
98
|
+
format: Annotated[
|
|
99
|
+
str,
|
|
100
|
+
typer.Option(
|
|
101
|
+
"--format",
|
|
102
|
+
"-f",
|
|
103
|
+
help="Output format: human or json",
|
|
104
|
+
),
|
|
105
|
+
] = "human",
|
|
106
|
+
) -> None:
|
|
107
|
+
"""List all registered adapters by entry point group.
|
|
108
|
+
|
|
109
|
+
Shows each entry point group with its registered adapters and
|
|
110
|
+
their source package.
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
$ rai adapter list
|
|
114
|
+
$ rai adapter list --format json
|
|
115
|
+
"""
|
|
116
|
+
tier = _get_tier()
|
|
117
|
+
groups = _collect_groups()
|
|
118
|
+
|
|
119
|
+
if format == "json":
|
|
120
|
+
typer.echo(format_list_json(tier, groups))
|
|
121
|
+
else:
|
|
122
|
+
format_list_human(tier, groups, console)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@adapters_app.command("check")
|
|
126
|
+
def check_command(
|
|
127
|
+
format: Annotated[
|
|
128
|
+
str,
|
|
129
|
+
typer.Option(
|
|
130
|
+
"--format",
|
|
131
|
+
"-f",
|
|
132
|
+
help="Output format: human or json",
|
|
133
|
+
),
|
|
134
|
+
] = "human",
|
|
135
|
+
) -> None:
|
|
136
|
+
"""Validate adapters against their Protocol contracts.
|
|
137
|
+
|
|
138
|
+
Loads each registered adapter and checks compliance via isinstance()
|
|
139
|
+
against its corresponding @runtime_checkable Protocol.
|
|
140
|
+
|
|
141
|
+
Examples:
|
|
142
|
+
$ rai adapter check
|
|
143
|
+
$ rai adapter check --format json
|
|
144
|
+
"""
|
|
145
|
+
results: list[dict[str, Any]] = []
|
|
146
|
+
emitter = create_emitter()
|
|
147
|
+
|
|
148
|
+
for group, (proto_name, proto_cls) in ADAPTER_GROUPS.items():
|
|
149
|
+
for ep in entry_points(group=group):
|
|
150
|
+
try:
|
|
151
|
+
loaded: Any = ep.load()
|
|
152
|
+
except Exception as exc: # noqa: BLE001
|
|
153
|
+
emitter.emit(
|
|
154
|
+
AdapterFailedEvent(
|
|
155
|
+
adapter_name=ep.name,
|
|
156
|
+
group=group,
|
|
157
|
+
error=str(exc),
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
results.append(
|
|
161
|
+
{
|
|
162
|
+
"group": group,
|
|
163
|
+
"name": ep.name,
|
|
164
|
+
"package": _get_dist_name(ep),
|
|
165
|
+
"protocol_name": proto_name,
|
|
166
|
+
"compliant": False,
|
|
167
|
+
"error": f"Failed to load: {exc}",
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
compliant = inspect.isclass(loaded) and issubclass(loaded, proto_cls)
|
|
173
|
+
error = None if compliant else f"Not a {proto_name} subclass"
|
|
174
|
+
if compliant:
|
|
175
|
+
emitter.emit(
|
|
176
|
+
AdapterLoadedEvent(
|
|
177
|
+
adapter_name=ep.name,
|
|
178
|
+
group=group,
|
|
179
|
+
adapter_type=type(loaded).__name__,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
emitter.emit(
|
|
184
|
+
AdapterFailedEvent(
|
|
185
|
+
adapter_name=ep.name,
|
|
186
|
+
group=group,
|
|
187
|
+
error=error or "",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
results.append(
|
|
191
|
+
{
|
|
192
|
+
"group": group,
|
|
193
|
+
"name": ep.name,
|
|
194
|
+
"package": _get_dist_name(ep),
|
|
195
|
+
"protocol_name": proto_name,
|
|
196
|
+
"compliant": compliant,
|
|
197
|
+
"error": error,
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if format == "json":
|
|
202
|
+
typer.echo(format_check_json(results))
|
|
203
|
+
else:
|
|
204
|
+
format_check_human(results, console)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@adapters_app.command("validate")
|
|
208
|
+
def validate_command(
|
|
209
|
+
file: Annotated[
|
|
210
|
+
Path,
|
|
211
|
+
typer.Argument(help="Path to YAML adapter config file"),
|
|
212
|
+
],
|
|
213
|
+
) -> None:
|
|
214
|
+
"""Validate a declarative YAML adapter config.
|
|
215
|
+
|
|
216
|
+
Checks that the YAML file conforms to the DeclarativeAdapterConfig
|
|
217
|
+
schema. Reports adapter name, protocol, and method counts on success,
|
|
218
|
+
or specific field errors on failure.
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
$ rai adapter validate .raise/adapters/github.yaml
|
|
222
|
+
$ rai adapter validate my-adapter.yaml
|
|
223
|
+
"""
|
|
224
|
+
import yaml
|
|
225
|
+
from pydantic import ValidationError
|
|
226
|
+
|
|
227
|
+
from raise_cli.adapters.declarative.schema import DeclarativeAdapterConfig
|
|
228
|
+
|
|
229
|
+
if not file.exists():
|
|
230
|
+
console.print(f"[red]Error:[/red] File not found: {file}")
|
|
231
|
+
raise typer.Exit(1)
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
raw = yaml.safe_load(file.read_text(encoding="utf-8"))
|
|
235
|
+
except yaml.YAMLError as exc:
|
|
236
|
+
console.print(f"[red]✗ Invalid adapter config:[/red] {file.name}")
|
|
237
|
+
console.print(f" Cannot parse YAML: {exc}")
|
|
238
|
+
raise typer.Exit(1) from None
|
|
239
|
+
|
|
240
|
+
if not isinstance(raw, dict):
|
|
241
|
+
console.print(f"[red]✗ Invalid adapter config:[/red] {file.name}")
|
|
242
|
+
console.print(" YAML content is not a mapping")
|
|
243
|
+
raise typer.Exit(1)
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
config = DeclarativeAdapterConfig.model_validate(raw)
|
|
247
|
+
except ValidationError as exc:
|
|
248
|
+
console.print(f"[red]✗ Invalid adapter config:[/red] {file.name}")
|
|
249
|
+
for err in exc.errors():
|
|
250
|
+
loc = ".".join(str(p) for p in err["loc"])
|
|
251
|
+
console.print(f" {loc}")
|
|
252
|
+
console.print(f" {err['msg']}")
|
|
253
|
+
raise typer.Exit(1) from None
|
|
254
|
+
|
|
255
|
+
format_validate_human(config, console)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
# rai adapter status — show configuration status for known adapters
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
# Jira env var names expected by McpJiraAdapter._create_bridge
|
|
263
|
+
_JIRA_ENV_VARS: list[tuple[str, str]] = [
|
|
264
|
+
("JIRA_URL", "Jira instance URL"),
|
|
265
|
+
("JIRA_USERNAME", "Jira user email"),
|
|
266
|
+
("JIRA_API_TOKEN", "Jira API token (or JIRA_TOKEN)"),
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _check_jira_config(project_root: Path) -> dict[str, Any]:
|
|
271
|
+
"""Collect Jira adapter configuration status.
|
|
272
|
+
|
|
273
|
+
Returns a dict with keys: yaml_path, yaml_exists, env_vars (list of
|
|
274
|
+
dicts with name, set, description), and ready (bool).
|
|
275
|
+
"""
|
|
276
|
+
yaml_path = project_root / ".raise" / "jira.yaml"
|
|
277
|
+
env_results: list[dict[str, Any]] = []
|
|
278
|
+
for var_name, description in _JIRA_ENV_VARS:
|
|
279
|
+
if var_name == "JIRA_API_TOKEN":
|
|
280
|
+
is_set = bool(os.environ.get("JIRA_API_TOKEN") or os.environ.get("JIRA_TOKEN"))
|
|
281
|
+
else:
|
|
282
|
+
is_set = bool(os.environ.get(var_name))
|
|
283
|
+
env_results.append({"name": var_name, "set": is_set, "description": description})
|
|
284
|
+
|
|
285
|
+
all_env_set = all(e["set"] for e in env_results)
|
|
286
|
+
return {
|
|
287
|
+
"yaml_path": str(yaml_path),
|
|
288
|
+
"yaml_exists": yaml_path.exists(),
|
|
289
|
+
"env_vars": env_results,
|
|
290
|
+
"ready": yaml_path.exists() and all_env_set,
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@adapters_app.command("status")
|
|
295
|
+
def status_command(
|
|
296
|
+
format: Annotated[
|
|
297
|
+
str,
|
|
298
|
+
typer.Option(
|
|
299
|
+
"--format",
|
|
300
|
+
"-f",
|
|
301
|
+
help="Output format: human or json",
|
|
302
|
+
),
|
|
303
|
+
] = "human",
|
|
304
|
+
) -> None:
|
|
305
|
+
"""Show configuration status for known adapters.
|
|
306
|
+
|
|
307
|
+
Checks that required config files exist and environment variables
|
|
308
|
+
are set. Useful for verifying setup after configuring an adapter.
|
|
309
|
+
|
|
310
|
+
Examples:
|
|
311
|
+
$ rai adapter status
|
|
312
|
+
$ rai adapter status --format json
|
|
313
|
+
"""
|
|
314
|
+
import json as json_mod
|
|
315
|
+
|
|
316
|
+
project_root = Path.cwd()
|
|
317
|
+
jira_status = _check_jira_config(project_root)
|
|
318
|
+
|
|
319
|
+
if format == "json":
|
|
320
|
+
typer.echo(json_mod.dumps({"jira": jira_status}, indent=2))
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
console.print("[bold]Adapter Configuration Status[/bold]\n")
|
|
324
|
+
|
|
325
|
+
# --- Jira ---
|
|
326
|
+
console.print("[bold]Jira[/bold]")
|
|
327
|
+
|
|
328
|
+
yaml_path = jira_status["yaml_path"]
|
|
329
|
+
if jira_status["yaml_exists"]:
|
|
330
|
+
console.print(f" [green]\u2713[/green] Config: {yaml_path}")
|
|
331
|
+
else:
|
|
332
|
+
console.print(f" [red]\u2717[/red] Config: {yaml_path} [red](not found)[/red]")
|
|
333
|
+
console.print(
|
|
334
|
+
" [dim]Create .raise/jira.yaml with status_mapping and project config.[/dim]"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
for env_var in jira_status["env_vars"]:
|
|
338
|
+
if env_var["set"]:
|
|
339
|
+
console.print(f" [green]\u2713[/green] {env_var['name']}: set")
|
|
340
|
+
else:
|
|
341
|
+
console.print(
|
|
342
|
+
f" [red]\u2717[/red] {env_var['name']}: [red]not set[/red]"
|
|
343
|
+
f" [dim]({env_var['description']})[/dim]"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
console.print()
|
|
347
|
+
if jira_status["ready"]:
|
|
348
|
+
console.print("[green]Jira adapter is fully configured.[/green]")
|
|
349
|
+
else:
|
|
350
|
+
missing: list[str] = []
|
|
351
|
+
if not jira_status["yaml_exists"]:
|
|
352
|
+
missing.append(".raise/jira.yaml")
|
|
353
|
+
for env_var in jira_status["env_vars"]:
|
|
354
|
+
if not env_var["set"]:
|
|
355
|
+
missing.append(env_var["name"])
|
|
356
|
+
console.print(
|
|
357
|
+
f"[yellow]Jira adapter is not ready.[/yellow] Missing: {', '.join(missing)}"
|
|
358
|
+
)
|
|
359
|
+
console.print(
|
|
360
|
+
"\n[dim]Set env vars in .env or shell. "
|
|
361
|
+
"See CLAUDE.md 'Jira Access' section for details.[/dim]"
|
|
362
|
+
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""CLI commands for artifact validation.
|
|
2
|
+
|
|
3
|
+
Provides ``rai artifact validate`` for checking YAML artifacts
|
|
4
|
+
against the Pydantic type registry.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Annotated
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from pydantic import ValidationError
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
from raise_cli.artifacts.reader import read_artifact
|
|
19
|
+
|
|
20
|
+
artifact_app = typer.Typer(
|
|
21
|
+
name="artifact",
|
|
22
|
+
help="Manage skill artifacts",
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _resolve_project_root() -> Path:
|
|
30
|
+
"""Resolve the project root from env or cwd."""
|
|
31
|
+
env_root = os.environ.get("RAI_PROJECT_ROOT")
|
|
32
|
+
if env_root:
|
|
33
|
+
return Path(env_root)
|
|
34
|
+
return Path.cwd()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@artifact_app.command("validate")
|
|
38
|
+
def validate_command(
|
|
39
|
+
file: Annotated[
|
|
40
|
+
str | None,
|
|
41
|
+
typer.Option("--file", help="Path to a single artifact file to validate"),
|
|
42
|
+
] = None,
|
|
43
|
+
format: Annotated[
|
|
44
|
+
str,
|
|
45
|
+
typer.Option("--format", "-f", help="Output format: human or json"),
|
|
46
|
+
] = "human",
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Validate YAML artifacts against their Pydantic schemas.
|
|
49
|
+
|
|
50
|
+
Reads all .yaml files from .raise/artifacts/ (or a single file with --file)
|
|
51
|
+
and validates each against the type registry.
|
|
52
|
+
|
|
53
|
+
Exit code 0 if all pass, 1 if any fail.
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
$ rai artifact validate
|
|
57
|
+
$ rai artifact validate --file .raise/artifacts/s354.1-design.yaml
|
|
58
|
+
$ rai artifact validate --format json
|
|
59
|
+
"""
|
|
60
|
+
if file:
|
|
61
|
+
paths = _resolve_single_file(Path(file), format)
|
|
62
|
+
else:
|
|
63
|
+
paths = _resolve_all_artifacts(format)
|
|
64
|
+
|
|
65
|
+
if paths is None:
|
|
66
|
+
return # Already handled (exit raised)
|
|
67
|
+
|
|
68
|
+
results: list[dict[str, object]] = []
|
|
69
|
+
for path in paths:
|
|
70
|
+
try:
|
|
71
|
+
read_artifact(path)
|
|
72
|
+
results.append({"file": path.name, "passed": True, "error": None})
|
|
73
|
+
except (ValidationError, Exception) as exc: # noqa: BLE001
|
|
74
|
+
results.append({"file": path.name, "passed": False, "error": str(exc)})
|
|
75
|
+
|
|
76
|
+
failed = [r for r in results if not r["passed"]]
|
|
77
|
+
|
|
78
|
+
if format == "json":
|
|
79
|
+
typer.echo(
|
|
80
|
+
json.dumps(
|
|
81
|
+
{"results": results, "all_passed": len(failed) == 0},
|
|
82
|
+
indent=2,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
for r in results:
|
|
87
|
+
marker = "[green]✓[/green]" if r["passed"] else "[red]✗[/red]"
|
|
88
|
+
console.print(f" {marker} {r['file']}")
|
|
89
|
+
if r["error"]:
|
|
90
|
+
# Show first line of error only
|
|
91
|
+
first_line = str(r["error"]).split("\n")[0]
|
|
92
|
+
console.print(f" {first_line}")
|
|
93
|
+
console.print()
|
|
94
|
+
if failed:
|
|
95
|
+
console.print(
|
|
96
|
+
f"[red bold]FAILED:[/red bold] {len(failed)} of {len(results)} artifacts invalid"
|
|
97
|
+
)
|
|
98
|
+
else:
|
|
99
|
+
console.print(
|
|
100
|
+
f"[green bold]PASSED:[/green bold] {len(results)} artifact(s) valid"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
raise typer.Exit(1 if failed else 0)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _resolve_single_file(path: Path, fmt: str) -> list[Path] | None:
|
|
107
|
+
"""Resolve a single file path, handling not-found."""
|
|
108
|
+
if not path.exists():
|
|
109
|
+
if fmt == "json":
|
|
110
|
+
typer.echo(json.dumps({"error": f"File not found: {path}"}))
|
|
111
|
+
else:
|
|
112
|
+
console.print(f"[red]Error:[/red] File not found: {path}")
|
|
113
|
+
raise typer.Exit(1)
|
|
114
|
+
return [path]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _resolve_all_artifacts(fmt: str) -> list[Path] | None:
|
|
118
|
+
"""Resolve all artifact files from .raise/artifacts/."""
|
|
119
|
+
root = _resolve_project_root()
|
|
120
|
+
artifacts_dir = root / ".raise" / "artifacts"
|
|
121
|
+
|
|
122
|
+
if not artifacts_dir.is_dir():
|
|
123
|
+
if fmt == "json":
|
|
124
|
+
typer.echo(json.dumps({"results": [], "all_passed": True, "message": "No artifacts found"}))
|
|
125
|
+
else:
|
|
126
|
+
console.print("No artifacts found in .raise/artifacts/")
|
|
127
|
+
raise typer.Exit(0)
|
|
128
|
+
|
|
129
|
+
paths = sorted(artifacts_dir.glob("*.yaml"))
|
|
130
|
+
if not paths:
|
|
131
|
+
if fmt == "json":
|
|
132
|
+
typer.echo(json.dumps({"results": [], "all_passed": True, "message": "No artifacts found"}))
|
|
133
|
+
else:
|
|
134
|
+
console.print("No artifacts found in .raise/artifacts/")
|
|
135
|
+
raise typer.Exit(0)
|
|
136
|
+
|
|
137
|
+
return paths
|