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,213 @@
|
|
|
1
|
+
"""Graph diff engine for detecting changes between unified graph builds.
|
|
2
|
+
|
|
3
|
+
Compares two Graph instances by node presence and semantic fields
|
|
4
|
+
(content, type, metadata). Produces a structured GraphDiff with impact
|
|
5
|
+
classification and affected module list.
|
|
6
|
+
|
|
7
|
+
Architecture: E16 Incremental Coherence — Layer 1 (deterministic)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
from raise_core.graph.engine import Graph
|
|
17
|
+
from raise_core.graph.models import GraphNode, NodeType
|
|
18
|
+
|
|
19
|
+
# Node types that indicate module-level impact
|
|
20
|
+
_MODULE_IMPACT_TYPES: frozenset[NodeType] = frozenset({"module", "component"})
|
|
21
|
+
|
|
22
|
+
# Node types that indicate architectural impact
|
|
23
|
+
_ARCHITECTURAL_IMPACT_TYPES: frozenset[NodeType] = frozenset(
|
|
24
|
+
{"bounded_context", "layer"}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Fields compared for modification detection
|
|
28
|
+
_COMPARED_FIELDS: tuple[str, ...] = ("content", "type", "metadata")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class NodeChange(BaseModel):
|
|
32
|
+
"""A change to a single node between two graph builds.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
node_id: The unique node identifier.
|
|
36
|
+
change_type: Whether the node was added, removed, or modified.
|
|
37
|
+
old_value: The node in the old graph (None for added).
|
|
38
|
+
new_value: The node in the new graph (None for removed).
|
|
39
|
+
changed_fields: Which semantic fields changed (for modified nodes).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
node_id: str
|
|
43
|
+
change_type: Literal["added", "removed", "modified"]
|
|
44
|
+
old_value: GraphNode | None = None
|
|
45
|
+
new_value: GraphNode | None = None
|
|
46
|
+
changed_fields: list[str] = Field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class GraphDiff(BaseModel):
|
|
50
|
+
"""Structured diff between two unified graph builds.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
node_changes: List of individual node changes.
|
|
54
|
+
impact: Overall impact level for downstream consumers.
|
|
55
|
+
affected_modules: Module node IDs that changed (sorted).
|
|
56
|
+
summary: Deterministic human-readable summary string.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
node_changes: list[NodeChange] = Field(default_factory=lambda: [])
|
|
60
|
+
impact: Literal["none", "module", "architectural"] = "none"
|
|
61
|
+
affected_modules: list[str] = Field(default_factory=lambda: [])
|
|
62
|
+
summary: str = "no changes"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def diff_graphs(old: Graph, new: Graph) -> GraphDiff:
|
|
66
|
+
"""Compare two unified graphs and return structured diff.
|
|
67
|
+
|
|
68
|
+
Compares nodes by presence (added/removed) and by semantic fields
|
|
69
|
+
(content, type, metadata) for modification detection. Ignores
|
|
70
|
+
created and source_file fields.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
old: The previous graph (before build).
|
|
74
|
+
new: The current graph (after build).
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
GraphDiff with node changes, impact classification, and affected modules.
|
|
78
|
+
"""
|
|
79
|
+
old_ids = {node.id for node in old.iter_concepts()}
|
|
80
|
+
new_ids = {node.id for node in new.iter_concepts()}
|
|
81
|
+
|
|
82
|
+
changes: list[NodeChange] = []
|
|
83
|
+
|
|
84
|
+
# Added nodes
|
|
85
|
+
for node_id in sorted(new_ids - old_ids):
|
|
86
|
+
node = new.get_concept(node_id)
|
|
87
|
+
changes.append(
|
|
88
|
+
NodeChange(
|
|
89
|
+
node_id=node_id,
|
|
90
|
+
change_type="added",
|
|
91
|
+
old_value=None,
|
|
92
|
+
new_value=node,
|
|
93
|
+
changed_fields=[],
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Removed nodes
|
|
98
|
+
for node_id in sorted(old_ids - new_ids):
|
|
99
|
+
node = old.get_concept(node_id)
|
|
100
|
+
changes.append(
|
|
101
|
+
NodeChange(
|
|
102
|
+
node_id=node_id,
|
|
103
|
+
change_type="removed",
|
|
104
|
+
old_value=node,
|
|
105
|
+
new_value=None,
|
|
106
|
+
changed_fields=[],
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Modified nodes
|
|
111
|
+
for node_id in sorted(old_ids & new_ids):
|
|
112
|
+
old_node = old.get_concept(node_id)
|
|
113
|
+
new_node = new.get_concept(node_id)
|
|
114
|
+
if old_node is None or new_node is None:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
changed_fields = _compare_nodes(old_node, new_node)
|
|
118
|
+
if changed_fields:
|
|
119
|
+
changes.append(
|
|
120
|
+
NodeChange(
|
|
121
|
+
node_id=node_id,
|
|
122
|
+
change_type="modified",
|
|
123
|
+
old_value=old_node,
|
|
124
|
+
new_value=new_node,
|
|
125
|
+
changed_fields=changed_fields,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if not changes:
|
|
130
|
+
return GraphDiff()
|
|
131
|
+
|
|
132
|
+
impact = _classify_impact(changes)
|
|
133
|
+
affected_modules = _derive_affected_modules(changes)
|
|
134
|
+
summary = _build_summary(changes, affected_modules)
|
|
135
|
+
|
|
136
|
+
return GraphDiff(
|
|
137
|
+
node_changes=changes,
|
|
138
|
+
impact=impact,
|
|
139
|
+
affected_modules=affected_modules,
|
|
140
|
+
summary=summary,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _compare_nodes(old: GraphNode, new: GraphNode) -> list[str]:
|
|
145
|
+
"""Compare two nodes on semantic fields only.
|
|
146
|
+
|
|
147
|
+
Returns list of field names that differ. Ignores created and source_file.
|
|
148
|
+
"""
|
|
149
|
+
changed: list[str] = []
|
|
150
|
+
for field in _COMPARED_FIELDS:
|
|
151
|
+
old_val = getattr(old, field)
|
|
152
|
+
new_val = getattr(new, field)
|
|
153
|
+
if old_val != new_val:
|
|
154
|
+
changed.append(field)
|
|
155
|
+
return changed
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _classify_impact(
|
|
159
|
+
changes: list[NodeChange],
|
|
160
|
+
) -> Literal["none", "module", "architectural"]:
|
|
161
|
+
"""Classify overall impact from node changes.
|
|
162
|
+
|
|
163
|
+
architectural > module > none.
|
|
164
|
+
"""
|
|
165
|
+
has_module = False
|
|
166
|
+
|
|
167
|
+
for change in changes:
|
|
168
|
+
node = change.new_value or change.old_value
|
|
169
|
+
if node is None:
|
|
170
|
+
continue
|
|
171
|
+
if node.type in _ARCHITECTURAL_IMPACT_TYPES:
|
|
172
|
+
return "architectural"
|
|
173
|
+
if node.type in _MODULE_IMPACT_TYPES:
|
|
174
|
+
has_module = True
|
|
175
|
+
|
|
176
|
+
return "module" if has_module else "none"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _derive_affected_modules(changes: list[NodeChange]) -> list[str]:
|
|
180
|
+
"""Extract sorted list of module IDs from changed nodes."""
|
|
181
|
+
modules: list[str] = []
|
|
182
|
+
for change in changes:
|
|
183
|
+
node = change.new_value or change.old_value
|
|
184
|
+
if node is not None and node.type == "module":
|
|
185
|
+
modules.append(change.node_id)
|
|
186
|
+
return sorted(modules)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _build_summary(changes: list[NodeChange], affected_modules: list[str]) -> str:
|
|
190
|
+
"""Build deterministic summary string from changes."""
|
|
191
|
+
added = sum(1 for c in changes if c.change_type == "added")
|
|
192
|
+
removed = sum(1 for c in changes if c.change_type == "removed")
|
|
193
|
+
modified = sum(1 for c in changes if c.change_type == "modified")
|
|
194
|
+
|
|
195
|
+
total = len(changes)
|
|
196
|
+
parts: list[str] = []
|
|
197
|
+
if added:
|
|
198
|
+
parts.append(f"{added} added")
|
|
199
|
+
if removed:
|
|
200
|
+
parts.append(f"{removed} removed")
|
|
201
|
+
if modified:
|
|
202
|
+
parts.append(f"{modified} modified")
|
|
203
|
+
|
|
204
|
+
detail = ", ".join(parts)
|
|
205
|
+
summary = f"{total} nodes changed ({detail})"
|
|
206
|
+
|
|
207
|
+
if affected_modules:
|
|
208
|
+
mod_list = ", ".join(affected_modules)
|
|
209
|
+
count = len(affected_modules)
|
|
210
|
+
label = "module" if count == 1 else "modules"
|
|
211
|
+
summary += f", {count} {label} affected ({mod_list})"
|
|
212
|
+
|
|
213
|
+
return summary
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Context extractors for unified graph building."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from raise_cli.context.extractors.skills import (
|
|
6
|
+
extract_all_skills,
|
|
7
|
+
extract_skill_metadata,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"extract_all_skills",
|
|
12
|
+
"extract_skill_metadata",
|
|
13
|
+
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Skill metadata extraction from SKILL.md frontmatter.
|
|
2
|
+
|
|
3
|
+
This module extracts skill metadata from YAML frontmatter in SKILL.md files
|
|
4
|
+
and converts them to GraphNode for the unified context graph.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, cast
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from raise_core.graph.models import GraphNode
|
|
17
|
+
|
|
18
|
+
# Regex to match YAML frontmatter between --- markers
|
|
19
|
+
FRONTMATTER_PATTERN = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def extract_skill_metadata(skill_path: Path) -> GraphNode | None:
|
|
23
|
+
"""Extract metadata from SKILL.md YAML frontmatter.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
skill_path: Path to SKILL.md file.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
GraphNode for the skill, or None if parsing fails or file doesn't exist.
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
>>> node = extract_skill_metadata(Path(".claude/skills/rai-story-plan/SKILL.md"))
|
|
33
|
+
>>> node.id if node else None
|
|
34
|
+
'/rai-story-plan'
|
|
35
|
+
"""
|
|
36
|
+
if not skill_path.exists():
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
content = skill_path.read_text(encoding="utf-8")
|
|
41
|
+
except OSError:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
# Extract YAML frontmatter
|
|
45
|
+
match = FRONTMATTER_PATTERN.match(content)
|
|
46
|
+
if not match:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
frontmatter = yaml.safe_load(match.group(1))
|
|
51
|
+
except yaml.YAMLError:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
if not isinstance(frontmatter, dict):
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
# Cast to typed dict for pyright
|
|
58
|
+
fm: dict[str, Any] = cast(dict[str, Any], frontmatter)
|
|
59
|
+
|
|
60
|
+
# Name is required
|
|
61
|
+
name_value = fm.get("name")
|
|
62
|
+
if not name_value:
|
|
63
|
+
return None
|
|
64
|
+
name: str = str(name_value)
|
|
65
|
+
|
|
66
|
+
# Extract fields
|
|
67
|
+
desc_value = fm.get("description", "")
|
|
68
|
+
description: str = ""
|
|
69
|
+
if isinstance(desc_value, str):
|
|
70
|
+
# Clean up multiline YAML strings
|
|
71
|
+
description = " ".join(desc_value.split())
|
|
72
|
+
|
|
73
|
+
meta_value = fm.get("metadata", {})
|
|
74
|
+
metadata_section: dict[str, Any] = {}
|
|
75
|
+
if isinstance(meta_value, dict):
|
|
76
|
+
metadata_section = cast(dict[str, Any], meta_value)
|
|
77
|
+
|
|
78
|
+
# Get file modification time for created timestamp
|
|
79
|
+
try:
|
|
80
|
+
mtime = skill_path.stat().st_mtime
|
|
81
|
+
created = datetime.fromtimestamp(mtime, tz=UTC).isoformat()
|
|
82
|
+
except OSError:
|
|
83
|
+
created = datetime.now(tz=UTC).isoformat()
|
|
84
|
+
|
|
85
|
+
return GraphNode(
|
|
86
|
+
id=f"/{name}",
|
|
87
|
+
type="skill",
|
|
88
|
+
content=description,
|
|
89
|
+
source_file=str(skill_path),
|
|
90
|
+
created=created,
|
|
91
|
+
metadata=metadata_section,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def extract_all_skills(skills_dir: Path) -> list[GraphNode]:
|
|
96
|
+
"""Extract metadata from all skills in a directory.
|
|
97
|
+
|
|
98
|
+
Searches for SKILL.md files in subdirectories of the given path.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
skills_dir: Path to skills directory (e.g., .claude/skills/).
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of GraphNode for each valid skill found.
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
>>> nodes = extract_all_skills(Path(".claude/skills"))
|
|
108
|
+
>>> len(nodes) > 0
|
|
109
|
+
True
|
|
110
|
+
"""
|
|
111
|
+
if not skills_dir.exists():
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
nodes: list[GraphNode] = []
|
|
115
|
+
|
|
116
|
+
for skill_md in skills_dir.glob("*/SKILL.md"):
|
|
117
|
+
node = extract_skill_metadata(skill_md)
|
|
118
|
+
if node is not None:
|
|
119
|
+
nodes.append(node)
|
|
120
|
+
|
|
121
|
+
return nodes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Core utilities for raise-cli.
|
|
2
|
+
|
|
3
|
+
This package provides:
|
|
4
|
+
- tools: Subprocess wrappers for git, ast-grep, ripgrep
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from raise_cli.core.tools import (
|
|
10
|
+
GitStatus,
|
|
11
|
+
SearchMatch,
|
|
12
|
+
ToolResult,
|
|
13
|
+
check_tool,
|
|
14
|
+
git_branch,
|
|
15
|
+
git_diff,
|
|
16
|
+
git_root,
|
|
17
|
+
git_status,
|
|
18
|
+
require_tool,
|
|
19
|
+
rg_search,
|
|
20
|
+
run_tool,
|
|
21
|
+
sg_search,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ToolResult",
|
|
26
|
+
"GitStatus",
|
|
27
|
+
"SearchMatch",
|
|
28
|
+
"check_tool",
|
|
29
|
+
"require_tool",
|
|
30
|
+
"run_tool",
|
|
31
|
+
"git_root",
|
|
32
|
+
"git_branch",
|
|
33
|
+
"git_status",
|
|
34
|
+
"git_diff",
|
|
35
|
+
"rg_search",
|
|
36
|
+
"sg_search",
|
|
37
|
+
]
|
raise_cli/core/files.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""File and directory utilities.
|
|
2
|
+
|
|
3
|
+
Shared functions for file system operations used across the codebase.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
# Directories to exclude from scanning operations
|
|
11
|
+
EXCLUDED_DIRS: frozenset[str] = frozenset(
|
|
12
|
+
{
|
|
13
|
+
# Version control
|
|
14
|
+
".git",
|
|
15
|
+
".svn",
|
|
16
|
+
".hg",
|
|
17
|
+
# Package managers
|
|
18
|
+
"node_modules",
|
|
19
|
+
# Python
|
|
20
|
+
"__pycache__",
|
|
21
|
+
".tox",
|
|
22
|
+
".nox",
|
|
23
|
+
".mypy_cache",
|
|
24
|
+
".pytest_cache",
|
|
25
|
+
".ruff_cache",
|
|
26
|
+
"venv",
|
|
27
|
+
".venv",
|
|
28
|
+
"env",
|
|
29
|
+
".env",
|
|
30
|
+
# Build outputs
|
|
31
|
+
"dist",
|
|
32
|
+
"build",
|
|
33
|
+
"target",
|
|
34
|
+
# IDE
|
|
35
|
+
".idea",
|
|
36
|
+
".vscode",
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def should_exclude_dir(dir_path: Path) -> bool:
|
|
42
|
+
"""Check if a directory should be excluded from scanning.
|
|
43
|
+
|
|
44
|
+
Excludes hidden directories (starting with .) and known non-project
|
|
45
|
+
directories like node_modules, __pycache__, etc.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
dir_path: Path to check.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
True if the directory should be excluded.
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
>>> should_exclude_dir(Path("node_modules"))
|
|
55
|
+
True
|
|
56
|
+
>>> should_exclude_dir(Path(".git"))
|
|
57
|
+
True
|
|
58
|
+
>>> should_exclude_dir(Path("src"))
|
|
59
|
+
False
|
|
60
|
+
"""
|
|
61
|
+
name = dir_path.name
|
|
62
|
+
# Exclude hidden directories (starting with .)
|
|
63
|
+
if name.startswith("."):
|
|
64
|
+
return True
|
|
65
|
+
# Exclude known non-project directories
|
|
66
|
+
return name in EXCLUDED_DIRS
|
raise_cli/core/text.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Text processing utilities.
|
|
2
|
+
|
|
3
|
+
Shared functions for text manipulation used across the codebase.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
# Comprehensive English stopwords for keyword extraction
|
|
11
|
+
STOPWORDS: frozenset[str] = frozenset(
|
|
12
|
+
{
|
|
13
|
+
# Articles
|
|
14
|
+
"the",
|
|
15
|
+
"a",
|
|
16
|
+
"an",
|
|
17
|
+
# Conjunctions
|
|
18
|
+
"and",
|
|
19
|
+
"but",
|
|
20
|
+
"or",
|
|
21
|
+
"nor",
|
|
22
|
+
"so",
|
|
23
|
+
"yet",
|
|
24
|
+
"both",
|
|
25
|
+
"either",
|
|
26
|
+
"neither",
|
|
27
|
+
# Prepositions
|
|
28
|
+
"in",
|
|
29
|
+
"on",
|
|
30
|
+
"at",
|
|
31
|
+
"to",
|
|
32
|
+
"for",
|
|
33
|
+
"of",
|
|
34
|
+
"with",
|
|
35
|
+
"from",
|
|
36
|
+
"by",
|
|
37
|
+
"as",
|
|
38
|
+
"into",
|
|
39
|
+
"through",
|
|
40
|
+
"during",
|
|
41
|
+
"before",
|
|
42
|
+
"after",
|
|
43
|
+
"above",
|
|
44
|
+
"below",
|
|
45
|
+
"between",
|
|
46
|
+
"under",
|
|
47
|
+
# Demonstratives and pronouns
|
|
48
|
+
"this",
|
|
49
|
+
"that",
|
|
50
|
+
"these",
|
|
51
|
+
"those",
|
|
52
|
+
"it",
|
|
53
|
+
"its",
|
|
54
|
+
# Be verbs
|
|
55
|
+
"is",
|
|
56
|
+
"are",
|
|
57
|
+
"was",
|
|
58
|
+
"were",
|
|
59
|
+
"be",
|
|
60
|
+
"been",
|
|
61
|
+
"being",
|
|
62
|
+
# Have verbs
|
|
63
|
+
"have",
|
|
64
|
+
"has",
|
|
65
|
+
"had",
|
|
66
|
+
# Do verbs
|
|
67
|
+
"do",
|
|
68
|
+
"does",
|
|
69
|
+
"did",
|
|
70
|
+
# Modal verbs
|
|
71
|
+
"will",
|
|
72
|
+
"would",
|
|
73
|
+
"could",
|
|
74
|
+
"should",
|
|
75
|
+
"may",
|
|
76
|
+
"might",
|
|
77
|
+
"must",
|
|
78
|
+
"shall",
|
|
79
|
+
"can",
|
|
80
|
+
"need",
|
|
81
|
+
"dare",
|
|
82
|
+
"ought",
|
|
83
|
+
"used",
|
|
84
|
+
# Adverbs
|
|
85
|
+
"again",
|
|
86
|
+
"further",
|
|
87
|
+
"then",
|
|
88
|
+
"once",
|
|
89
|
+
"not",
|
|
90
|
+
"only",
|
|
91
|
+
"own",
|
|
92
|
+
"same",
|
|
93
|
+
"than",
|
|
94
|
+
"too",
|
|
95
|
+
"very",
|
|
96
|
+
"just",
|
|
97
|
+
"also",
|
|
98
|
+
"now",
|
|
99
|
+
"here",
|
|
100
|
+
"there",
|
|
101
|
+
"when",
|
|
102
|
+
"where",
|
|
103
|
+
"why",
|
|
104
|
+
"how",
|
|
105
|
+
# Quantifiers
|
|
106
|
+
"all",
|
|
107
|
+
"each",
|
|
108
|
+
"every",
|
|
109
|
+
"few",
|
|
110
|
+
"more",
|
|
111
|
+
"most",
|
|
112
|
+
"other",
|
|
113
|
+
"some",
|
|
114
|
+
"such",
|
|
115
|
+
"no",
|
|
116
|
+
"any",
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def extract_keywords(text: str) -> set[str]:
|
|
122
|
+
"""Extract meaningful keywords from text.
|
|
123
|
+
|
|
124
|
+
Filters out stopwords and keeps only words longer than 3 characters.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
text: Text to extract keywords from.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Set of lowercase keywords.
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
>>> keywords = extract_keywords("The system MUST validate inputs")
|
|
134
|
+
>>> "system" in keywords
|
|
135
|
+
True
|
|
136
|
+
>>> "the" in keywords
|
|
137
|
+
False
|
|
138
|
+
"""
|
|
139
|
+
words = re.findall(r"\b\w+\b", text.lower())
|
|
140
|
+
return {w for w in words if len(w) > 3 and w not in STOPWORDS}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def sanitize_id(name: str) -> str:
|
|
144
|
+
"""Sanitize a name for use as an ID.
|
|
145
|
+
|
|
146
|
+
Converts a human-readable name to a lowercase, hyphen-separated
|
|
147
|
+
identifier suitable for use in IDs and keys.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
name: Human-readable name to sanitize.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Sanitized ID string (lowercase, hyphens, alphanumeric only).
|
|
154
|
+
|
|
155
|
+
Examples:
|
|
156
|
+
>>> sanitize_id("Context Generation (MVC)")
|
|
157
|
+
'context-generation-mvc'
|
|
158
|
+
>>> sanitize_id("Governance as Code")
|
|
159
|
+
'governance-as-code'
|
|
160
|
+
>>> sanitize_id("Hello, World!")
|
|
161
|
+
'hello-world'
|
|
162
|
+
"""
|
|
163
|
+
# Convert to lowercase
|
|
164
|
+
sanitized = name.lower()
|
|
165
|
+
# Replace spaces with hyphens
|
|
166
|
+
sanitized = sanitized.replace(" ", "-")
|
|
167
|
+
# Remove all non-alphanumeric characters except hyphens
|
|
168
|
+
sanitized = re.sub(r"[^a-z0-9-]", "", sanitized)
|
|
169
|
+
# Collapse multiple hyphens into one
|
|
170
|
+
sanitized = re.sub(r"-+", "-", sanitized)
|
|
171
|
+
# Remove leading/trailing hyphens
|
|
172
|
+
sanitized = sanitized.strip("-")
|
|
173
|
+
|
|
174
|
+
return sanitized
|