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,355 @@
|
|
|
1
|
+
"""Drift detection for codebase components.
|
|
2
|
+
|
|
3
|
+
This module detects architectural drift between new/modified code
|
|
4
|
+
and established component patterns in the baseline.
|
|
5
|
+
|
|
6
|
+
Drift types detected:
|
|
7
|
+
- Location drift: Files in unexpected directories
|
|
8
|
+
- Naming drift: Symbols not following naming conventions
|
|
9
|
+
- Documentation drift: Missing docstrings on public APIs
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from raise_cli.discovery.drift import detect_drift
|
|
13
|
+
>>> warnings = detect_drift(baseline=components, scanned=symbols)
|
|
14
|
+
>>> for w in warnings:
|
|
15
|
+
... print(f"{w.severity}: {w.issue}")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Literal
|
|
22
|
+
|
|
23
|
+
from pydantic import BaseModel, Field
|
|
24
|
+
|
|
25
|
+
from raise_cli.discovery.scanner import Symbol
|
|
26
|
+
|
|
27
|
+
# Severity levels for drift warnings
|
|
28
|
+
DriftSeverity = Literal["info", "warning", "error"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DriftWarning(BaseModel):
|
|
32
|
+
"""A warning about architectural drift.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
file: Path to the file with drift.
|
|
36
|
+
issue: Description of the drift issue.
|
|
37
|
+
severity: Severity level (info, warning, error).
|
|
38
|
+
suggestion: Suggested fix for the issue.
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
>>> warning = DriftWarning(
|
|
42
|
+
... file="src/new.py",
|
|
43
|
+
... issue="File in unexpected location",
|
|
44
|
+
... severity="warning",
|
|
45
|
+
... suggestion="Move to src/raise_cli/",
|
|
46
|
+
... )
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
file: str = Field(..., description="Path to file with drift")
|
|
50
|
+
issue: str = Field(..., description="Description of the drift issue")
|
|
51
|
+
severity: DriftSeverity = Field(default="warning", description="Severity level")
|
|
52
|
+
suggestion: str = Field(default="", description="Suggested fix")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class BaselineComponentMetadata(BaseModel):
|
|
56
|
+
"""Metadata for a baseline component."""
|
|
57
|
+
|
|
58
|
+
name: str = Field(default="", description="Symbol name")
|
|
59
|
+
kind: str = Field(
|
|
60
|
+
default="unknown", description="Symbol kind (class, function, etc.)"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BaselineComponent(BaseModel):
|
|
65
|
+
"""A component from the validated baseline.
|
|
66
|
+
|
|
67
|
+
Represents a component from work/discovery/components-validated.json
|
|
68
|
+
used for drift detection.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
source_file: Path to the source file.
|
|
72
|
+
content: Content/docstring of the component.
|
|
73
|
+
metadata: Component metadata (name, kind).
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
source_file: str = Field(default="", description="Path to source file")
|
|
77
|
+
content: str = Field(default="", description="Component content/docstring")
|
|
78
|
+
metadata: BaselineComponentMetadata = Field(
|
|
79
|
+
default_factory=BaselineComponentMetadata,
|
|
80
|
+
description="Component metadata",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _extract_directory_patterns(
|
|
85
|
+
baseline: list[BaselineComponent],
|
|
86
|
+
) -> dict[str, set[str]]:
|
|
87
|
+
"""Extract directory patterns from baseline by category/kind.
|
|
88
|
+
|
|
89
|
+
Groups baseline components by their kind and extracts the directories
|
|
90
|
+
they're typically found in.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
baseline: List of validated baseline components.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dict mapping kind (class, function, etc.) to set of valid directories.
|
|
97
|
+
"""
|
|
98
|
+
patterns: dict[str, set[str]] = {}
|
|
99
|
+
|
|
100
|
+
for comp in baseline:
|
|
101
|
+
if comp.source_file:
|
|
102
|
+
kind = comp.metadata.kind
|
|
103
|
+
directory = str(Path(comp.source_file).parent)
|
|
104
|
+
if kind not in patterns:
|
|
105
|
+
patterns[kind] = set()
|
|
106
|
+
patterns[kind].add(directory)
|
|
107
|
+
|
|
108
|
+
return patterns
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _extract_naming_patterns(
|
|
112
|
+
baseline: list[BaselineComponent],
|
|
113
|
+
) -> dict[str, dict[str, int]]:
|
|
114
|
+
"""Extract naming patterns from baseline by kind.
|
|
115
|
+
|
|
116
|
+
Looks for common prefixes/suffixes in symbol names and counts occurrences.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
baseline: List of validated baseline components.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Dict mapping kind to dict of prefix -> count.
|
|
123
|
+
"""
|
|
124
|
+
patterns: dict[str, dict[str, int]] = {}
|
|
125
|
+
|
|
126
|
+
for comp in baseline:
|
|
127
|
+
kind = comp.metadata.kind
|
|
128
|
+
name = comp.metadata.name
|
|
129
|
+
|
|
130
|
+
if not name:
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
if kind not in patterns:
|
|
134
|
+
patterns[kind] = {}
|
|
135
|
+
|
|
136
|
+
# Extract prefix (e.g., "extract_" from "extract_python_symbols")
|
|
137
|
+
if "_" in name:
|
|
138
|
+
prefix = name.split("_")[0] + "_"
|
|
139
|
+
patterns[kind][prefix] = patterns[kind].get(prefix, 0) + 1
|
|
140
|
+
|
|
141
|
+
return patterns
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _check_baseline_has_docstrings(baseline: list[BaselineComponent]) -> bool:
|
|
145
|
+
"""Check if baseline components typically have docstrings.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
baseline: List of validated baseline components.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
True if majority of baseline components have content (docstrings).
|
|
152
|
+
"""
|
|
153
|
+
if not baseline:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
with_content = sum(1 for c in baseline if c.content)
|
|
157
|
+
return with_content / len(baseline) > 0.5
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _is_private_symbol(name: str) -> bool:
|
|
161
|
+
"""Check if symbol is private (starts with underscore).
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
name: Symbol name.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
True if private (single underscore prefix).
|
|
168
|
+
"""
|
|
169
|
+
return name.startswith("_") and not name.startswith("__")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _normalize_path(path: str) -> str:
|
|
173
|
+
"""Normalize path for comparison (remove leading src/, trailing slashes)."""
|
|
174
|
+
normalized = path.strip("/")
|
|
175
|
+
# Handle both "src/module" and "module" as equivalent
|
|
176
|
+
if normalized.startswith("src/"):
|
|
177
|
+
normalized = normalized[4:]
|
|
178
|
+
return normalized
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _check_location_drift(
|
|
182
|
+
symbol: Symbol,
|
|
183
|
+
directory_patterns: dict[str, set[str]],
|
|
184
|
+
) -> DriftWarning | None:
|
|
185
|
+
"""Check if symbol is in an expected location.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
symbol: Scanned symbol to check.
|
|
189
|
+
directory_patterns: Valid directories by kind.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
DriftWarning if location drift detected, None otherwise.
|
|
193
|
+
"""
|
|
194
|
+
kind = symbol.kind
|
|
195
|
+
if kind not in directory_patterns:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
valid_dirs = directory_patterns[kind]
|
|
199
|
+
if not valid_dirs:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
symbol_dir = _normalize_path(str(Path(symbol.file).parent))
|
|
203
|
+
|
|
204
|
+
# Check if symbol is in any valid directory (normalized comparison)
|
|
205
|
+
for valid_dir in valid_dirs:
|
|
206
|
+
normalized_valid = _normalize_path(valid_dir)
|
|
207
|
+
if (
|
|
208
|
+
symbol_dir == normalized_valid
|
|
209
|
+
or symbol_dir.startswith(normalized_valid + "/")
|
|
210
|
+
or normalized_valid.startswith(symbol_dir + "/")
|
|
211
|
+
):
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
# Location drift detected
|
|
215
|
+
return DriftWarning(
|
|
216
|
+
file=symbol.file,
|
|
217
|
+
issue=f"Location drift: {kind} '{symbol.name}' in unexpected directory",
|
|
218
|
+
severity="warning",
|
|
219
|
+
suggestion=f"Expected directories: {', '.join(sorted(valid_dirs))}",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _check_naming_drift(
|
|
224
|
+
symbol: Symbol,
|
|
225
|
+
naming_patterns: dict[str, dict[str, int]],
|
|
226
|
+
) -> DriftWarning | None:
|
|
227
|
+
"""Check if symbol follows naming conventions.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
symbol: Scanned symbol to check.
|
|
231
|
+
naming_patterns: Dict mapping kind to dict of prefix -> count.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
DriftWarning if naming drift detected, None otherwise.
|
|
235
|
+
"""
|
|
236
|
+
kind = symbol.kind
|
|
237
|
+
name = symbol.name
|
|
238
|
+
|
|
239
|
+
# Check class naming (PascalCase)
|
|
240
|
+
if kind == "class" and not name[0].isupper():
|
|
241
|
+
return DriftWarning(
|
|
242
|
+
file=symbol.file,
|
|
243
|
+
issue=f"Naming drift: class '{name}' should use PascalCase",
|
|
244
|
+
severity="warning",
|
|
245
|
+
suggestion=f"Rename to '{name.title().replace('_', '')}'",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Check function naming patterns
|
|
249
|
+
if kind == "function" and kind in naming_patterns:
|
|
250
|
+
prefix_counts = naming_patterns[kind]
|
|
251
|
+
if prefix_counts:
|
|
252
|
+
# Check if new function follows any established prefix
|
|
253
|
+
matches_pattern = any(name.startswith(p) for p in prefix_counts)
|
|
254
|
+
if not matches_pattern:
|
|
255
|
+
# Find prefixes that appear 2+ times (established pattern)
|
|
256
|
+
common_prefixes = [
|
|
257
|
+
p for p, count in prefix_counts.items() if count >= 2
|
|
258
|
+
]
|
|
259
|
+
if common_prefixes:
|
|
260
|
+
return DriftWarning(
|
|
261
|
+
file=symbol.file,
|
|
262
|
+
issue=f"Naming drift: function '{name}' doesn't follow naming pattern",
|
|
263
|
+
severity="info",
|
|
264
|
+
suggestion=f"Consider using prefix: {common_prefixes[0]}",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _check_docstring_drift(
|
|
271
|
+
symbol: Symbol,
|
|
272
|
+
baseline_has_docstrings: bool,
|
|
273
|
+
) -> DriftWarning | None:
|
|
274
|
+
"""Check if symbol has docstring when baseline expects them.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
symbol: Scanned symbol to check.
|
|
278
|
+
baseline_has_docstrings: Whether baseline typically has docstrings.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
DriftWarning if docstring missing when expected, None otherwise.
|
|
282
|
+
"""
|
|
283
|
+
if not baseline_has_docstrings:
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
# Only check classes and public functions
|
|
287
|
+
if symbol.kind not in ("class", "function"):
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
if symbol.docstring is None or symbol.docstring.strip() == "":
|
|
291
|
+
return DriftWarning(
|
|
292
|
+
file=symbol.file,
|
|
293
|
+
issue=f"Missing docstring: {symbol.kind} '{symbol.name}' has no documentation",
|
|
294
|
+
severity="warning",
|
|
295
|
+
suggestion="Add a docstring describing purpose and usage",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def detect_drift(
|
|
302
|
+
baseline: list[BaselineComponent],
|
|
303
|
+
scanned: list[Symbol],
|
|
304
|
+
) -> list[DriftWarning]:
|
|
305
|
+
"""Detect architectural drift between baseline and new code.
|
|
306
|
+
|
|
307
|
+
Compares scanned symbols against established patterns from the baseline
|
|
308
|
+
to identify potential drift issues.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
baseline: List of validated baseline components from
|
|
312
|
+
work/discovery/components-validated.json.
|
|
313
|
+
scanned: List of Symbol objects from scanning new/modified files.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
List of DriftWarning objects describing detected issues.
|
|
317
|
+
|
|
318
|
+
Examples:
|
|
319
|
+
>>> baseline = [BaselineComponent(metadata=BaselineComponentMetadata(name="Foo", kind="class"))]
|
|
320
|
+
>>> scanned = [Symbol(name="bar", kind="class", ...)]
|
|
321
|
+
>>> warnings = detect_drift(baseline, scanned)
|
|
322
|
+
>>> len(warnings) > 0
|
|
323
|
+
True
|
|
324
|
+
"""
|
|
325
|
+
if not baseline or not scanned:
|
|
326
|
+
return []
|
|
327
|
+
|
|
328
|
+
warnings: list[DriftWarning] = []
|
|
329
|
+
|
|
330
|
+
# Extract patterns from baseline
|
|
331
|
+
directory_patterns = _extract_directory_patterns(baseline)
|
|
332
|
+
naming_patterns = _extract_naming_patterns(baseline)
|
|
333
|
+
baseline_has_docstrings = _check_baseline_has_docstrings(baseline)
|
|
334
|
+
|
|
335
|
+
for symbol in scanned:
|
|
336
|
+
# Skip private symbols
|
|
337
|
+
if _is_private_symbol(symbol.name):
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
# Check for location drift
|
|
341
|
+
location_warning = _check_location_drift(symbol, directory_patterns)
|
|
342
|
+
if location_warning:
|
|
343
|
+
warnings.append(location_warning)
|
|
344
|
+
|
|
345
|
+
# Check for naming drift
|
|
346
|
+
naming_warning = _check_naming_drift(symbol, naming_patterns)
|
|
347
|
+
if naming_warning:
|
|
348
|
+
warnings.append(naming_warning)
|
|
349
|
+
|
|
350
|
+
# Check for docstring drift
|
|
351
|
+
docstring_warning = _check_docstring_drift(symbol, baseline_has_docstrings)
|
|
352
|
+
if docstring_warning:
|
|
353
|
+
warnings.append(docstring_warning)
|
|
354
|
+
|
|
355
|
+
return warnings
|