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
raise_cli/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""RaiSE CLI - Reliable AI Software Engineering.
|
|
2
|
+
|
|
3
|
+
A governance framework for AI-assisted software development.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from raise_cli.exceptions import (
|
|
9
|
+
ArtifactNotFoundError,
|
|
10
|
+
ConfigurationError,
|
|
11
|
+
DependencyError,
|
|
12
|
+
GateFailedError,
|
|
13
|
+
GateNotFoundError,
|
|
14
|
+
KataNotFoundError,
|
|
15
|
+
RaiError,
|
|
16
|
+
StateError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__version__ = "2.2.1"
|
|
21
|
+
__author__ = "Emilio Osorio"
|
|
22
|
+
__license__ = "MIT"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"__version__",
|
|
26
|
+
"__author__",
|
|
27
|
+
"__license__",
|
|
28
|
+
# Exceptions
|
|
29
|
+
"RaiError",
|
|
30
|
+
"ConfigurationError",
|
|
31
|
+
"KataNotFoundError",
|
|
32
|
+
"GateNotFoundError",
|
|
33
|
+
"ArtifactNotFoundError",
|
|
34
|
+
"DependencyError",
|
|
35
|
+
"StateError",
|
|
36
|
+
"ValidationError",
|
|
37
|
+
"GateFailedError",
|
|
38
|
+
]
|
raise_cli/__main__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Entry point for python -m raise_cli."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from raise_cli.cli.main import app
|
|
8
|
+
from raise_cli.exceptions import RaiError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
"""Run the CLI application with error handling.
|
|
13
|
+
|
|
14
|
+
Catches RaiError exceptions and displays them with proper formatting,
|
|
15
|
+
then exits with the appropriate exit code.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
app()
|
|
19
|
+
except RaiError as error:
|
|
20
|
+
# Import here to avoid circular imports and for lazy loading
|
|
21
|
+
from raise_cli.cli.error_handler import handle_error
|
|
22
|
+
from raise_cli.cli.main import get_output_format
|
|
23
|
+
|
|
24
|
+
output_format = get_output_format()
|
|
25
|
+
exit_code = handle_error(error, output_format=output_format)
|
|
26
|
+
sys.exit(exit_code)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
main()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Adapter contracts and registry for raise-cli extensibility.
|
|
2
|
+
|
|
3
|
+
Public API: 7 Protocols + 15 boundary models + 2 sync wrappers
|
|
4
|
+
+ 5 registry functions + 5 group constants.
|
|
5
|
+
KnowledgeGraphBackend + BackendHealth moved to raise_core (E275).
|
|
6
|
+
|
|
7
|
+
Architecture: ADR-033 (PM), ADR-034 (Governance)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from raise_cli.adapters.models import (
|
|
11
|
+
AdapterHealth,
|
|
12
|
+
ArtifactLocator,
|
|
13
|
+
BatchResult,
|
|
14
|
+
Comment,
|
|
15
|
+
CommentRef,
|
|
16
|
+
CoreArtifactType,
|
|
17
|
+
FailureDetail,
|
|
18
|
+
IssueDetail,
|
|
19
|
+
IssueRef,
|
|
20
|
+
IssueSpec,
|
|
21
|
+
IssueSummary,
|
|
22
|
+
PageContent,
|
|
23
|
+
PageSummary,
|
|
24
|
+
PublishResult,
|
|
25
|
+
)
|
|
26
|
+
from raise_cli.adapters.protocols import (
|
|
27
|
+
AsyncDocumentationTarget,
|
|
28
|
+
AsyncProjectManagementAdapter,
|
|
29
|
+
DocumentationTarget,
|
|
30
|
+
GovernanceParser,
|
|
31
|
+
GovernanceSchemaProvider,
|
|
32
|
+
ProjectManagementAdapter,
|
|
33
|
+
)
|
|
34
|
+
from raise_cli.adapters.registry import (
|
|
35
|
+
EP_DOC_TARGETS,
|
|
36
|
+
EP_GOVERNANCE_PARSERS,
|
|
37
|
+
EP_GOVERNANCE_SCHEMAS,
|
|
38
|
+
EP_GRAPH_BACKENDS,
|
|
39
|
+
EP_PM_ADAPTERS,
|
|
40
|
+
get_doc_targets,
|
|
41
|
+
get_governance_parsers,
|
|
42
|
+
get_governance_schemas,
|
|
43
|
+
get_graph_backends,
|
|
44
|
+
get_pm_adapters,
|
|
45
|
+
)
|
|
46
|
+
from raise_cli.adapters.sync import SyncDocsAdapter, SyncPMAdapter
|
|
47
|
+
from raise_core.graph.backends.models import BackendHealth
|
|
48
|
+
from raise_core.graph.backends.protocol import KnowledgeGraphBackend
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
# Protocols — sync (4 local + 1 from raise_core)
|
|
52
|
+
"DocumentationTarget",
|
|
53
|
+
"GovernanceParser",
|
|
54
|
+
"GovernanceSchemaProvider",
|
|
55
|
+
"KnowledgeGraphBackend",
|
|
56
|
+
"ProjectManagementAdapter",
|
|
57
|
+
# Protocols — async (2)
|
|
58
|
+
"AsyncDocumentationTarget",
|
|
59
|
+
"AsyncProjectManagementAdapter",
|
|
60
|
+
# Sync wrappers (2)
|
|
61
|
+
"SyncDocsAdapter",
|
|
62
|
+
"SyncPMAdapter",
|
|
63
|
+
# Models (14 local + 1 from raise_core)
|
|
64
|
+
"AdapterHealth",
|
|
65
|
+
"ArtifactLocator",
|
|
66
|
+
"BackendHealth",
|
|
67
|
+
"BatchResult",
|
|
68
|
+
"Comment",
|
|
69
|
+
"CommentRef",
|
|
70
|
+
"CoreArtifactType",
|
|
71
|
+
"FailureDetail",
|
|
72
|
+
"IssueDetail",
|
|
73
|
+
"IssueRef",
|
|
74
|
+
"IssueSpec",
|
|
75
|
+
"IssueSummary",
|
|
76
|
+
"PageContent",
|
|
77
|
+
"PageSummary",
|
|
78
|
+
"PublishResult",
|
|
79
|
+
# Registry functions
|
|
80
|
+
"get_doc_targets",
|
|
81
|
+
"get_governance_parsers",
|
|
82
|
+
"get_governance_schemas",
|
|
83
|
+
"get_graph_backends",
|
|
84
|
+
"get_pm_adapters",
|
|
85
|
+
# Entry point group constants
|
|
86
|
+
"EP_DOC_TARGETS",
|
|
87
|
+
"EP_GOVERNANCE_PARSERS",
|
|
88
|
+
"EP_GOVERNANCE_SCHEMAS",
|
|
89
|
+
"EP_GRAPH_BACKENDS",
|
|
90
|
+
"EP_PM_ADAPTERS",
|
|
91
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Declarative MCP adapter framework.
|
|
2
|
+
|
|
3
|
+
Enables integration of any MCP server via YAML config instead of Python code.
|
|
4
|
+
|
|
5
|
+
Architecture: ADR-041, E337
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from raise_cli.adapters.declarative.adapter import DeclarativeMcpAdapter
|
|
9
|
+
from raise_cli.adapters.declarative.expressions import ExpressionEvaluator
|
|
10
|
+
from raise_cli.adapters.declarative.schema import (
|
|
11
|
+
AdapterMeta,
|
|
12
|
+
DeclarativeAdapterConfig,
|
|
13
|
+
MethodMapping,
|
|
14
|
+
ResponseMapping,
|
|
15
|
+
)
|
|
16
|
+
from raise_cli.mcp.schema import ServerConnection
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AdapterMeta",
|
|
20
|
+
"DeclarativeAdapterConfig",
|
|
21
|
+
"DeclarativeMcpAdapter",
|
|
22
|
+
"ExpressionEvaluator",
|
|
23
|
+
"MethodMapping",
|
|
24
|
+
"ResponseMapping",
|
|
25
|
+
"ServerConnection",
|
|
26
|
+
]
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Generic declarative MCP adapter driven by YAML config.
|
|
2
|
+
|
|
3
|
+
Implements ``AsyncProjectManagementAdapter`` and ``AsyncDocumentationTarget``
|
|
4
|
+
by dispatching each protocol method to the MCP tool specified in the YAML
|
|
5
|
+
config. Expression templates in args are evaluated against a context dict
|
|
6
|
+
built from method parameters. One class serves both protocols (AR-Q1).
|
|
7
|
+
|
|
8
|
+
Architecture: ADR-041, E337, AR-C1/C2/R1/Q1
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
from typing import Any, cast
|
|
16
|
+
|
|
17
|
+
from raise_cli.adapters.declarative.expressions import ExpressionEvaluator
|
|
18
|
+
from raise_cli.adapters.declarative.schema import (
|
|
19
|
+
DeclarativeAdapterConfig,
|
|
20
|
+
MethodMapping,
|
|
21
|
+
)
|
|
22
|
+
from raise_cli.adapters.models import (
|
|
23
|
+
AdapterHealth,
|
|
24
|
+
BatchResult,
|
|
25
|
+
Comment,
|
|
26
|
+
CommentRef,
|
|
27
|
+
FailureDetail,
|
|
28
|
+
IssueDetail,
|
|
29
|
+
IssueRef,
|
|
30
|
+
IssueSpec,
|
|
31
|
+
IssueSummary,
|
|
32
|
+
PageContent,
|
|
33
|
+
PageSummary,
|
|
34
|
+
PublishResult,
|
|
35
|
+
)
|
|
36
|
+
from raise_cli.mcp.bridge import McpBridge, McpBridgeError, McpToolResult
|
|
37
|
+
from raise_cli.mcp.registry import discover_mcp_servers
|
|
38
|
+
from raise_cli.mcp.schema import ServerConnection
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DeclarativeMcpAdapter:
|
|
44
|
+
"""Async adapter driven by YAML config — serves PM or Docs protocol.
|
|
45
|
+
|
|
46
|
+
Implements ``AsyncProjectManagementAdapter`` and ``AsyncDocumentationTarget``
|
|
47
|
+
via structural typing. Each protocol method dispatches to the MCP tool
|
|
48
|
+
declared in config.methods. One class, both protocols (AR-Q1).
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
config: Parsed YAML adapter configuration.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, config: DeclarativeAdapterConfig) -> None:
|
|
55
|
+
self._config = config
|
|
56
|
+
self._evaluator = ExpressionEvaluator()
|
|
57
|
+
self._bridge = self._create_bridge()
|
|
58
|
+
|
|
59
|
+
def _create_bridge(self) -> McpBridge:
|
|
60
|
+
"""Create McpBridge from server config.
|
|
61
|
+
|
|
62
|
+
Resolves ``server.ref`` via MCP registry if set, otherwise
|
|
63
|
+
uses inline connection fields. (S338.5, AR-C2)
|
|
64
|
+
"""
|
|
65
|
+
server = self._config.server
|
|
66
|
+
if server.ref is not None:
|
|
67
|
+
registry = discover_mcp_servers()
|
|
68
|
+
if server.ref not in registry:
|
|
69
|
+
msg = f"Server ref '{server.ref}' not found in MCP registry"
|
|
70
|
+
raise McpBridgeError(msg)
|
|
71
|
+
conn = registry[server.ref].server
|
|
72
|
+
else:
|
|
73
|
+
assert server.command is not None # guaranteed by ServerRef validator
|
|
74
|
+
conn = ServerConnection(
|
|
75
|
+
command=server.command,
|
|
76
|
+
args=server.args,
|
|
77
|
+
env=server.env,
|
|
78
|
+
)
|
|
79
|
+
env: dict[str, str] | None = None
|
|
80
|
+
if conn.env:
|
|
81
|
+
env = {
|
|
82
|
+
**os.environ,
|
|
83
|
+
**{k: os.environ.get(k, "") for k in conn.env},
|
|
84
|
+
}
|
|
85
|
+
return McpBridge(
|
|
86
|
+
server_command=conn.command,
|
|
87
|
+
server_args=conn.args,
|
|
88
|
+
env=env,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _get_method(self, name: str) -> MethodMapping:
|
|
92
|
+
"""Get method mapping, raising NotImplementedError if null or missing."""
|
|
93
|
+
mapping = self._config.methods.get(name)
|
|
94
|
+
if mapping is None:
|
|
95
|
+
msg = f"Method '{name}' not supported by adapter '{self._config.adapter.name}'"
|
|
96
|
+
raise NotImplementedError(msg)
|
|
97
|
+
return mapping
|
|
98
|
+
|
|
99
|
+
async def _dispatch(
|
|
100
|
+
self, method_name: str, context: dict[str, Any]
|
|
101
|
+
) -> McpToolResult:
|
|
102
|
+
"""Dispatch a protocol method call to the configured MCP tool."""
|
|
103
|
+
mapping = self._get_method(method_name)
|
|
104
|
+
args = self._evaluator.evaluate_args(mapping.args, context)
|
|
105
|
+
return await self._bridge.call(mapping.tool, args)
|
|
106
|
+
|
|
107
|
+
def _parse_single(
|
|
108
|
+
self, method_name: str, result: McpToolResult, context: dict[str, Any]
|
|
109
|
+
) -> dict[str, Any]:
|
|
110
|
+
"""Parse a single-item response using the method's response mapping."""
|
|
111
|
+
mapping = self._get_method(method_name)
|
|
112
|
+
if mapping.response is None:
|
|
113
|
+
return result.data
|
|
114
|
+
|
|
115
|
+
parse_ctx = {**context, "data": result.data}
|
|
116
|
+
return {
|
|
117
|
+
field: self._evaluator.evaluate(tmpl, parse_ctx)
|
|
118
|
+
for field, tmpl in mapping.response.fields.items()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
def _parse_list(
|
|
122
|
+
self, method_name: str, result: McpToolResult
|
|
123
|
+
) -> list[dict[str, Any]]:
|
|
124
|
+
"""Parse a list response using items_path and field mappings."""
|
|
125
|
+
mapping = self._get_method(method_name)
|
|
126
|
+
if mapping.response is None:
|
|
127
|
+
return result.data.get("items", [])
|
|
128
|
+
|
|
129
|
+
# Navigate to list via items_path
|
|
130
|
+
items: Any = result.data
|
|
131
|
+
if mapping.response.items_path:
|
|
132
|
+
for part in mapping.response.items_path.split("."):
|
|
133
|
+
if isinstance(items, dict):
|
|
134
|
+
items = cast(Any, items.get(part, []))
|
|
135
|
+
|
|
136
|
+
if not isinstance(items, list):
|
|
137
|
+
items = []
|
|
138
|
+
|
|
139
|
+
parsed: list[dict[str, Any]] = []
|
|
140
|
+
for raw_item in cast(list[Any], items):
|
|
141
|
+
item_ctx: dict[str, Any] = {"item": raw_item}
|
|
142
|
+
row = {
|
|
143
|
+
field: self._evaluator.evaluate(tmpl, item_ctx)
|
|
144
|
+
for field, tmpl in mapping.response.fields.items()
|
|
145
|
+
}
|
|
146
|
+
parsed.append(row)
|
|
147
|
+
return parsed
|
|
148
|
+
|
|
149
|
+
# ----- CRUD -----
|
|
150
|
+
|
|
151
|
+
async def create_issue(self, project_key: str, issue: IssueSpec) -> IssueRef:
|
|
152
|
+
ctx = {"project_key": project_key, "issue": issue.model_dump()}
|
|
153
|
+
result = await self._dispatch("create_issue", ctx)
|
|
154
|
+
fields = self._parse_single("create_issue", result, ctx)
|
|
155
|
+
return IssueRef(**fields)
|
|
156
|
+
|
|
157
|
+
async def get_issue(self, key: str) -> IssueDetail:
|
|
158
|
+
ctx = {"key": key}
|
|
159
|
+
result = await self._dispatch("get_issue", ctx)
|
|
160
|
+
fields = self._parse_single("get_issue", result, ctx)
|
|
161
|
+
return IssueDetail(**fields)
|
|
162
|
+
|
|
163
|
+
async def update_issue(self, key: str, fields: dict[str, Any]) -> IssueRef:
|
|
164
|
+
ctx = {"key": key, "fields": fields}
|
|
165
|
+
result = await self._dispatch("update_issue", ctx)
|
|
166
|
+
parsed = self._parse_single("update_issue", result, ctx)
|
|
167
|
+
return IssueRef(**parsed)
|
|
168
|
+
|
|
169
|
+
async def transition_issue(self, key: str, status: str) -> IssueRef:
|
|
170
|
+
ctx = {"key": key, "status": status}
|
|
171
|
+
result = await self._dispatch("transition_issue", ctx)
|
|
172
|
+
parsed = self._parse_single("transition_issue", result, ctx)
|
|
173
|
+
return IssueRef(**parsed)
|
|
174
|
+
|
|
175
|
+
# ----- Batch -----
|
|
176
|
+
|
|
177
|
+
async def batch_transition(self, keys: list[str], status: str) -> BatchResult:
|
|
178
|
+
"""Auto-loops over transition_issue (AR design D6)."""
|
|
179
|
+
succeeded: list[IssueRef] = []
|
|
180
|
+
failed: list[FailureDetail] = []
|
|
181
|
+
|
|
182
|
+
for key in keys:
|
|
183
|
+
try:
|
|
184
|
+
ref = await self.transition_issue(key, status)
|
|
185
|
+
succeeded.append(ref)
|
|
186
|
+
except (McpBridgeError, NotImplementedError, Exception) as exc:
|
|
187
|
+
failed.append(FailureDetail(key=key, error=str(exc)))
|
|
188
|
+
|
|
189
|
+
return BatchResult(succeeded=succeeded, failed=failed)
|
|
190
|
+
|
|
191
|
+
# ----- Relationships -----
|
|
192
|
+
|
|
193
|
+
async def link_to_parent(self, child_key: str, parent_key: str) -> None:
|
|
194
|
+
ctx = {"child_key": child_key, "parent_key": parent_key}
|
|
195
|
+
await self._dispatch("link_to_parent", ctx)
|
|
196
|
+
|
|
197
|
+
async def link_issues(self, source: str, target: str, link_type: str) -> None:
|
|
198
|
+
ctx = {"source": source, "target": target, "link_type": link_type}
|
|
199
|
+
await self._dispatch("link_issues", ctx)
|
|
200
|
+
|
|
201
|
+
# ----- Comments -----
|
|
202
|
+
|
|
203
|
+
async def add_comment(self, key: str, body: str) -> CommentRef:
|
|
204
|
+
ctx = {"key": key, "body": body}
|
|
205
|
+
result = await self._dispatch("add_comment", ctx)
|
|
206
|
+
fields = self._parse_single("add_comment", result, ctx)
|
|
207
|
+
return CommentRef(**fields)
|
|
208
|
+
|
|
209
|
+
async def get_comments(self, key: str, limit: int = 10) -> list[Comment]:
|
|
210
|
+
ctx = {"key": key, "limit": limit}
|
|
211
|
+
result = await self._dispatch("get_comments", ctx)
|
|
212
|
+
items = self._parse_list("get_comments", result)
|
|
213
|
+
return [Comment(**item) for item in items]
|
|
214
|
+
|
|
215
|
+
# ----- Query -----
|
|
216
|
+
|
|
217
|
+
async def search(
|
|
218
|
+
self, query: str, limit: int = 50
|
|
219
|
+
) -> list[IssueSummary] | list[PageSummary]:
|
|
220
|
+
"""Protocol-aware search: returns IssueSummary (PM) or PageSummary (docs).
|
|
221
|
+
|
|
222
|
+
SyncDocsAdapter calls .search() — must return the right type based on
|
|
223
|
+
the adapter's configured protocol (QR-C1 fix).
|
|
224
|
+
"""
|
|
225
|
+
ctx = {"query": query, "limit": limit}
|
|
226
|
+
result = await self._dispatch("search", ctx)
|
|
227
|
+
items = self._parse_list("search", result)
|
|
228
|
+
if self._config.adapter.protocol == "docs":
|
|
229
|
+
return [PageSummary(**item) for item in items]
|
|
230
|
+
return [IssueSummary(**item) for item in items]
|
|
231
|
+
|
|
232
|
+
# ----- Docs: Documentation Target (AR-Q1) -----
|
|
233
|
+
|
|
234
|
+
async def can_publish(self, doc_type: str, metadata: dict[str, Any]) -> bool:
|
|
235
|
+
ctx = {"doc_type": doc_type, "metadata": metadata}
|
|
236
|
+
result = await self._dispatch("can_publish", ctx)
|
|
237
|
+
fields = self._parse_single("can_publish", result, ctx)
|
|
238
|
+
return bool(fields.get("result", False))
|
|
239
|
+
|
|
240
|
+
async def publish(
|
|
241
|
+
self, doc_type: str, content: str, metadata: dict[str, Any]
|
|
242
|
+
) -> PublishResult:
|
|
243
|
+
ctx = {"doc_type": doc_type, "content": content, "metadata": metadata}
|
|
244
|
+
result = await self._dispatch("publish", ctx)
|
|
245
|
+
fields = self._parse_single("publish", result, ctx)
|
|
246
|
+
return PublishResult(**fields)
|
|
247
|
+
|
|
248
|
+
async def get_page(self, identifier: str) -> PageContent:
|
|
249
|
+
ctx = {"identifier": identifier}
|
|
250
|
+
result = await self._dispatch("get_page", ctx)
|
|
251
|
+
fields = self._parse_single("get_page", result, ctx)
|
|
252
|
+
return PageContent(**fields)
|
|
253
|
+
|
|
254
|
+
# ----- Lifecycle -----
|
|
255
|
+
|
|
256
|
+
async def health(self) -> AdapterHealth:
|
|
257
|
+
bridge_health = await self._bridge.health()
|
|
258
|
+
return AdapterHealth(
|
|
259
|
+
name=self._config.adapter.name,
|
|
260
|
+
healthy=bridge_health.healthy,
|
|
261
|
+
message=bridge_health.message,
|
|
262
|
+
latency_ms=bridge_health.latency_ms,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
async def aclose(self) -> None:
|
|
266
|
+
"""Close the underlying MCP bridge."""
|
|
267
|
+
await self._bridge.aclose()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""YAML adapter discovery for declarative MCP adapters.
|
|
2
|
+
|
|
3
|
+
Scans ``.raise/adapters/*.yaml`` for adapter configs, parses them with
|
|
4
|
+
``DeclarativeAdapterConfig``, filters by protocol, and returns factory
|
|
5
|
+
closures compatible with ``resolve_entrypoint``.
|
|
6
|
+
|
|
7
|
+
Architecture: ADR-041, E337, S337.3, AR-C1 (factory closure)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
from pydantic import ValidationError
|
|
19
|
+
|
|
20
|
+
from raise_cli.adapters.declarative.schema import DeclarativeAdapterConfig
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _make_factory(cfg: DeclarativeAdapterConfig) -> Callable[[], Any]:
|
|
26
|
+
"""AR-C1: factory closure captures config for no-arg instantiation."""
|
|
27
|
+
|
|
28
|
+
def factory() -> Any:
|
|
29
|
+
from raise_cli.adapters.declarative.adapter import DeclarativeMcpAdapter
|
|
30
|
+
|
|
31
|
+
return DeclarativeMcpAdapter(cfg)
|
|
32
|
+
|
|
33
|
+
return factory
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def discover_yaml_adapters(
|
|
37
|
+
protocol: str,
|
|
38
|
+
*,
|
|
39
|
+
adapters_dir: Path | None = None,
|
|
40
|
+
) -> dict[str, Callable[[], Any]]:
|
|
41
|
+
"""Discover YAML-defined adapters for a given protocol.
|
|
42
|
+
|
|
43
|
+
Scans ``adapters_dir`` (default: ``.raise/adapters/``) for ``*.yaml``
|
|
44
|
+
files, parses each as a ``DeclarativeAdapterConfig``, filters by
|
|
45
|
+
``adapter.protocol``, and returns factory closures.
|
|
46
|
+
|
|
47
|
+
Each factory closure captures its parsed config and returns a new
|
|
48
|
+
``DeclarativeMcpAdapter`` when called with no arguments (AR-C1).
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
protocol: Protocol to filter by (``"pm"`` or ``"docs"``).
|
|
52
|
+
adapters_dir: Directory to scan. Defaults to ``.raise/adapters/``.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Mapping of adapter name to no-arg factory callable.
|
|
56
|
+
"""
|
|
57
|
+
if adapters_dir is None:
|
|
58
|
+
adapters_dir = Path.cwd() / ".raise" / "adapters"
|
|
59
|
+
|
|
60
|
+
if not adapters_dir.is_dir():
|
|
61
|
+
return {}
|
|
62
|
+
|
|
63
|
+
result: dict[str, Callable[[], Any]] = {}
|
|
64
|
+
|
|
65
|
+
for yaml_path in sorted(adapters_dir.glob("*.yaml")):
|
|
66
|
+
try:
|
|
67
|
+
raw = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
|
|
68
|
+
except Exception as exc: # noqa: BLE001
|
|
69
|
+
logger.warning("Skipping %s: YAML parse error: %s", yaml_path.name, exc)
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
config = DeclarativeAdapterConfig.model_validate(raw)
|
|
74
|
+
except ValidationError as exc:
|
|
75
|
+
logger.warning(
|
|
76
|
+
"Skipping %s: schema validation error: %s", yaml_path.name, exc
|
|
77
|
+
)
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
if config.adapter.protocol != protocol:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
name = config.adapter.name
|
|
84
|
+
if name in result:
|
|
85
|
+
logger.warning(
|
|
86
|
+
"Skipping %s: adapter name '%s' already defined by another YAML file",
|
|
87
|
+
yaml_path.name,
|
|
88
|
+
name,
|
|
89
|
+
)
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
result[name] = _make_factory(config)
|
|
93
|
+
|
|
94
|
+
return result
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Mini expression evaluator for declarative adapter templates.
|
|
2
|
+
|
|
3
|
+
Supports: ``{{ var }}``, ``{{ obj.field }}``, ``{{ value | filter }}``.
|
|
4
|
+
Filters: ``str``, ``default('fallback')``, ``pluck('field')``, ``json``.
|
|
5
|
+
|
|
6
|
+
~100 LOC, zero external dependencies.
|
|
7
|
+
|
|
8
|
+
Architecture: ADR-041 (D2 — mini evaluator, not Jinja2)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
from typing import Any, cast
|
|
16
|
+
|
|
17
|
+
# Matches {{ expression }} with optional whitespace
|
|
18
|
+
_TEMPLATE_RE = re.compile(r"\{\{\s*(.+?)\s*\}\}")
|
|
19
|
+
|
|
20
|
+
# Matches filter with optional quoted argument: filter_name('arg')
|
|
21
|
+
_FILTER_RE = re.compile(r"(\w+)\s*(?:\(\s*'([^']*)'\s*\))?")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ExpressionEvaluator:
|
|
25
|
+
"""Evaluate template expressions against a context dict.
|
|
26
|
+
|
|
27
|
+
Designed for declarative MCP adapter YAML configs. Intentionally
|
|
28
|
+
minimal — covers dot-access and 4 filters. Not a general-purpose
|
|
29
|
+
template engine.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def evaluate(self, template: str, context: dict[str, Any]) -> Any:
|
|
33
|
+
"""Evaluate a single template string against context.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
template: Template string, e.g. ``"{{ issue.summary | str }}"``.
|
|
37
|
+
context: Variable context dict.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Resolved value. Type depends on expression — may be str, int,
|
|
41
|
+
list, dict, None, etc.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
KeyError: If a referenced variable is not in context.
|
|
45
|
+
"""
|
|
46
|
+
match = _TEMPLATE_RE.fullmatch(template.strip()) if template else None
|
|
47
|
+
if match is None:
|
|
48
|
+
# Literal passthrough — no {{ }} found
|
|
49
|
+
return template
|
|
50
|
+
|
|
51
|
+
expr = match.group(1).strip()
|
|
52
|
+
return self._eval_expr(expr, context)
|
|
53
|
+
|
|
54
|
+
def evaluate_args(
|
|
55
|
+
self, args: dict[str, str], context: dict[str, Any]
|
|
56
|
+
) -> dict[str, Any]:
|
|
57
|
+
"""Evaluate all values in an args dict.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
args: Mapping of param name → template string.
|
|
61
|
+
context: Variable context dict.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
New dict with all values resolved.
|
|
65
|
+
"""
|
|
66
|
+
return {key: self.evaluate(tmpl, context) for key, tmpl in args.items()}
|
|
67
|
+
|
|
68
|
+
def _eval_expr(self, expr: str, context: dict[str, Any]) -> Any:
|
|
69
|
+
"""Evaluate an expression, optionally with a pipe filter."""
|
|
70
|
+
# Split on pipe: "value | filter('arg')"
|
|
71
|
+
parts = expr.split("|", maxsplit=1)
|
|
72
|
+
var_path = parts[0].strip()
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
value = self._resolve_path(var_path, context)
|
|
76
|
+
except KeyError:
|
|
77
|
+
# If there's a default filter, use it; otherwise re-raise
|
|
78
|
+
if len(parts) > 1 and "default" in parts[1]:
|
|
79
|
+
value = None
|
|
80
|
+
else:
|
|
81
|
+
raise
|
|
82
|
+
|
|
83
|
+
if len(parts) > 1:
|
|
84
|
+
filter_expr = parts[1].strip()
|
|
85
|
+
value = self._apply_filter(filter_expr, value, context)
|
|
86
|
+
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
def _resolve_path(self, path: str, context: dict[str, Any]) -> Any:
|
|
90
|
+
"""Resolve a dot-separated path against context.
|
|
91
|
+
|
|
92
|
+
``"issue.summary"`` → ``context["issue"]["summary"]``
|
|
93
|
+
"""
|
|
94
|
+
parts = path.split(".")
|
|
95
|
+
current: Any = context
|
|
96
|
+
|
|
97
|
+
for part in parts:
|
|
98
|
+
if isinstance(current, dict):
|
|
99
|
+
if part not in current:
|
|
100
|
+
raise KeyError(part)
|
|
101
|
+
current = cast(Any, current[part])
|
|
102
|
+
else:
|
|
103
|
+
raise KeyError(part)
|
|
104
|
+
|
|
105
|
+
return current
|
|
106
|
+
|
|
107
|
+
def _apply_filter(
|
|
108
|
+
self, filter_expr: str, value: Any, context: dict[str, Any]
|
|
109
|
+
) -> Any:
|
|
110
|
+
"""Apply a filter to a value."""
|
|
111
|
+
match = _FILTER_RE.fullmatch(filter_expr)
|
|
112
|
+
if not match:
|
|
113
|
+
msg = f"Invalid filter expression: {filter_expr}"
|
|
114
|
+
raise ValueError(msg)
|
|
115
|
+
|
|
116
|
+
filter_name = match.group(1)
|
|
117
|
+
filter_arg = match.group(2) # None if no arg
|
|
118
|
+
|
|
119
|
+
if filter_name == "str":
|
|
120
|
+
return str(value)
|
|
121
|
+
if filter_name == "json":
|
|
122
|
+
return json.dumps(value)
|
|
123
|
+
if filter_name == "default":
|
|
124
|
+
if value is None:
|
|
125
|
+
return filter_arg
|
|
126
|
+
# Also handle KeyError case — caller catches and retries with default
|
|
127
|
+
return value
|
|
128
|
+
if filter_name == "pluck":
|
|
129
|
+
if not isinstance(value, list):
|
|
130
|
+
msg = f"pluck requires a list, got {type(value).__name__}"
|
|
131
|
+
raise TypeError(msg)
|
|
132
|
+
if filter_arg is None:
|
|
133
|
+
msg = "pluck requires a field argument"
|
|
134
|
+
raise ValueError(msg)
|
|
135
|
+
return [
|
|
136
|
+
self._pluck_item(item, filter_arg) for item in cast(list[Any], value)
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
msg = f"Unknown filter: {filter_name}"
|
|
140
|
+
raise ValueError(msg)
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def _pluck_item(item: Any, field: str) -> Any:
|
|
144
|
+
"""Extract a field from a dict item, raising KeyError if missing."""
|
|
145
|
+
if isinstance(item, dict):
|
|
146
|
+
if field not in item:
|
|
147
|
+
raise KeyError(field)
|
|
148
|
+
return cast(Any, item[field])
|
|
149
|
+
msg = f"pluck expects dict items, got {type(item).__name__}"
|
|
150
|
+
raise TypeError(msg)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Reference YAML adapter configs shipped with the package."""
|