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.
Files changed (264) hide show
  1. raise_cli/__init__.py +38 -0
  2. raise_cli/__main__.py +30 -0
  3. raise_cli/adapters/__init__.py +91 -0
  4. raise_cli/adapters/declarative/__init__.py +26 -0
  5. raise_cli/adapters/declarative/adapter.py +267 -0
  6. raise_cli/adapters/declarative/discovery.py +94 -0
  7. raise_cli/adapters/declarative/expressions.py +150 -0
  8. raise_cli/adapters/declarative/reference/__init__.py +1 -0
  9. raise_cli/adapters/declarative/reference/github.yaml +143 -0
  10. raise_cli/adapters/declarative/schema.py +98 -0
  11. raise_cli/adapters/filesystem.py +299 -0
  12. raise_cli/adapters/mcp_bridge.py +10 -0
  13. raise_cli/adapters/mcp_confluence.py +246 -0
  14. raise_cli/adapters/mcp_jira.py +405 -0
  15. raise_cli/adapters/models.py +205 -0
  16. raise_cli/adapters/protocols.py +180 -0
  17. raise_cli/adapters/registry.py +90 -0
  18. raise_cli/adapters/sync.py +149 -0
  19. raise_cli/agents/__init__.py +14 -0
  20. raise_cli/agents/antigravity.yaml +8 -0
  21. raise_cli/agents/claude.yaml +8 -0
  22. raise_cli/agents/copilot.yaml +8 -0
  23. raise_cli/agents/copilot_plugin.py +124 -0
  24. raise_cli/agents/cursor.yaml +7 -0
  25. raise_cli/agents/roo.yaml +8 -0
  26. raise_cli/agents/windsurf.yaml +8 -0
  27. raise_cli/artifacts/__init__.py +30 -0
  28. raise_cli/artifacts/models.py +43 -0
  29. raise_cli/artifacts/reader.py +55 -0
  30. raise_cli/artifacts/renderer.py +104 -0
  31. raise_cli/artifacts/story_design.py +69 -0
  32. raise_cli/artifacts/writer.py +45 -0
  33. raise_cli/backlog/__init__.py +1 -0
  34. raise_cli/backlog/sync.py +115 -0
  35. raise_cli/cli/__init__.py +3 -0
  36. raise_cli/cli/commands/__init__.py +3 -0
  37. raise_cli/cli/commands/_resolve.py +153 -0
  38. raise_cli/cli/commands/adapters.py +362 -0
  39. raise_cli/cli/commands/artifact.py +137 -0
  40. raise_cli/cli/commands/backlog.py +333 -0
  41. raise_cli/cli/commands/base.py +31 -0
  42. raise_cli/cli/commands/discover.py +551 -0
  43. raise_cli/cli/commands/docs.py +130 -0
  44. raise_cli/cli/commands/doctor.py +177 -0
  45. raise_cli/cli/commands/gate.py +223 -0
  46. raise_cli/cli/commands/graph.py +1086 -0
  47. raise_cli/cli/commands/info.py +81 -0
  48. raise_cli/cli/commands/init.py +746 -0
  49. raise_cli/cli/commands/journal.py +167 -0
  50. raise_cli/cli/commands/mcp.py +524 -0
  51. raise_cli/cli/commands/memory.py +467 -0
  52. raise_cli/cli/commands/pattern.py +348 -0
  53. raise_cli/cli/commands/profile.py +59 -0
  54. raise_cli/cli/commands/publish.py +80 -0
  55. raise_cli/cli/commands/release.py +338 -0
  56. raise_cli/cli/commands/session.py +528 -0
  57. raise_cli/cli/commands/signal.py +410 -0
  58. raise_cli/cli/commands/skill.py +350 -0
  59. raise_cli/cli/commands/skill_set.py +145 -0
  60. raise_cli/cli/error_handler.py +158 -0
  61. raise_cli/cli/main.py +163 -0
  62. raise_cli/compat.py +66 -0
  63. raise_cli/config/__init__.py +41 -0
  64. raise_cli/config/agent_plugin.py +105 -0
  65. raise_cli/config/agent_registry.py +233 -0
  66. raise_cli/config/agents.py +120 -0
  67. raise_cli/config/ide.py +32 -0
  68. raise_cli/config/paths.py +379 -0
  69. raise_cli/config/settings.py +180 -0
  70. raise_cli/context/__init__.py +42 -0
  71. raise_cli/context/analyzers/__init__.py +16 -0
  72. raise_cli/context/analyzers/models.py +36 -0
  73. raise_cli/context/analyzers/protocol.py +43 -0
  74. raise_cli/context/analyzers/python.py +292 -0
  75. raise_cli/context/builder.py +1569 -0
  76. raise_cli/context/diff.py +213 -0
  77. raise_cli/context/extractors/__init__.py +13 -0
  78. raise_cli/context/extractors/skills.py +121 -0
  79. raise_cli/core/__init__.py +37 -0
  80. raise_cli/core/files.py +66 -0
  81. raise_cli/core/text.py +174 -0
  82. raise_cli/core/tools.py +441 -0
  83. raise_cli/discovery/__init__.py +50 -0
  84. raise_cli/discovery/analyzer.py +691 -0
  85. raise_cli/discovery/drift.py +355 -0
  86. raise_cli/discovery/scanner.py +1687 -0
  87. raise_cli/doctor/__init__.py +4 -0
  88. raise_cli/doctor/checks/__init__.py +1 -0
  89. raise_cli/doctor/checks/environment.py +110 -0
  90. raise_cli/doctor/checks/project.py +238 -0
  91. raise_cli/doctor/fix.py +80 -0
  92. raise_cli/doctor/models.py +56 -0
  93. raise_cli/doctor/protocol.py +43 -0
  94. raise_cli/doctor/registry.py +100 -0
  95. raise_cli/doctor/report.py +141 -0
  96. raise_cli/doctor/runner.py +95 -0
  97. raise_cli/engines/__init__.py +3 -0
  98. raise_cli/exceptions.py +215 -0
  99. raise_cli/gates/__init__.py +19 -0
  100. raise_cli/gates/builtin/__init__.py +1 -0
  101. raise_cli/gates/builtin/coverage.py +52 -0
  102. raise_cli/gates/builtin/lint.py +48 -0
  103. raise_cli/gates/builtin/tests.py +48 -0
  104. raise_cli/gates/builtin/types.py +48 -0
  105. raise_cli/gates/models.py +40 -0
  106. raise_cli/gates/protocol.py +41 -0
  107. raise_cli/gates/registry.py +141 -0
  108. raise_cli/governance/__init__.py +11 -0
  109. raise_cli/governance/extractor.py +412 -0
  110. raise_cli/governance/models.py +134 -0
  111. raise_cli/governance/parsers/__init__.py +35 -0
  112. raise_cli/governance/parsers/_convert.py +38 -0
  113. raise_cli/governance/parsers/adr.py +274 -0
  114. raise_cli/governance/parsers/backlog.py +356 -0
  115. raise_cli/governance/parsers/constitution.py +119 -0
  116. raise_cli/governance/parsers/epic.py +323 -0
  117. raise_cli/governance/parsers/glossary.py +316 -0
  118. raise_cli/governance/parsers/guardrails.py +345 -0
  119. raise_cli/governance/parsers/prd.py +112 -0
  120. raise_cli/governance/parsers/roadmap.py +118 -0
  121. raise_cli/governance/parsers/vision.py +116 -0
  122. raise_cli/graph/__init__.py +1 -0
  123. raise_cli/graph/backends/__init__.py +57 -0
  124. raise_cli/graph/backends/api.py +137 -0
  125. raise_cli/graph/backends/dual.py +139 -0
  126. raise_cli/graph/backends/pending.py +84 -0
  127. raise_cli/handlers/__init__.py +3 -0
  128. raise_cli/hooks/__init__.py +54 -0
  129. raise_cli/hooks/builtin/__init__.py +1 -0
  130. raise_cli/hooks/builtin/backlog.py +216 -0
  131. raise_cli/hooks/builtin/gate_bridge.py +83 -0
  132. raise_cli/hooks/builtin/jira_sync.py +127 -0
  133. raise_cli/hooks/builtin/memory.py +117 -0
  134. raise_cli/hooks/builtin/telemetry.py +72 -0
  135. raise_cli/hooks/emitter.py +184 -0
  136. raise_cli/hooks/events.py +262 -0
  137. raise_cli/hooks/protocol.py +38 -0
  138. raise_cli/hooks/registry.py +117 -0
  139. raise_cli/mcp/__init__.py +33 -0
  140. raise_cli/mcp/bridge.py +218 -0
  141. raise_cli/mcp/models.py +43 -0
  142. raise_cli/mcp/registry.py +77 -0
  143. raise_cli/mcp/schema.py +41 -0
  144. raise_cli/memory/__init__.py +58 -0
  145. raise_cli/memory/loader.py +247 -0
  146. raise_cli/memory/migration.py +241 -0
  147. raise_cli/memory/models.py +169 -0
  148. raise_cli/memory/writer.py +598 -0
  149. raise_cli/onboarding/__init__.py +103 -0
  150. raise_cli/onboarding/bootstrap.py +324 -0
  151. raise_cli/onboarding/claudemd.py +17 -0
  152. raise_cli/onboarding/conventions.py +742 -0
  153. raise_cli/onboarding/detection.py +374 -0
  154. raise_cli/onboarding/governance.py +443 -0
  155. raise_cli/onboarding/instructions.py +672 -0
  156. raise_cli/onboarding/manifest.py +201 -0
  157. raise_cli/onboarding/memory_md.py +399 -0
  158. raise_cli/onboarding/migration.py +207 -0
  159. raise_cli/onboarding/profile.py +624 -0
  160. raise_cli/onboarding/skill_conflict.py +100 -0
  161. raise_cli/onboarding/skill_manifest.py +176 -0
  162. raise_cli/onboarding/skills.py +437 -0
  163. raise_cli/onboarding/workflows.py +101 -0
  164. raise_cli/output/__init__.py +28 -0
  165. raise_cli/output/console.py +394 -0
  166. raise_cli/output/formatters/__init__.py +9 -0
  167. raise_cli/output/formatters/adapters.py +135 -0
  168. raise_cli/output/formatters/discover.py +439 -0
  169. raise_cli/output/formatters/skill.py +298 -0
  170. raise_cli/publish/__init__.py +3 -0
  171. raise_cli/publish/changelog.py +80 -0
  172. raise_cli/publish/check.py +179 -0
  173. raise_cli/publish/version.py +172 -0
  174. raise_cli/rai_base/__init__.py +22 -0
  175. raise_cli/rai_base/framework/__init__.py +7 -0
  176. raise_cli/rai_base/framework/methodology.yaml +233 -0
  177. raise_cli/rai_base/governance/__init__.py +1 -0
  178. raise_cli/rai_base/governance/architecture/__init__.py +1 -0
  179. raise_cli/rai_base/governance/architecture/domain-model.md +20 -0
  180. raise_cli/rai_base/governance/architecture/system-context.md +34 -0
  181. raise_cli/rai_base/governance/architecture/system-design.md +24 -0
  182. raise_cli/rai_base/governance/backlog.md +8 -0
  183. raise_cli/rai_base/governance/guardrails.md +17 -0
  184. raise_cli/rai_base/governance/prd.md +25 -0
  185. raise_cli/rai_base/governance/vision.md +16 -0
  186. raise_cli/rai_base/identity/__init__.py +8 -0
  187. raise_cli/rai_base/identity/core.md +119 -0
  188. raise_cli/rai_base/identity/perspective.md +119 -0
  189. raise_cli/rai_base/memory/__init__.py +7 -0
  190. raise_cli/rai_base/memory/patterns-base.jsonl +55 -0
  191. raise_cli/schemas/__init__.py +3 -0
  192. raise_cli/schemas/journal.py +49 -0
  193. raise_cli/schemas/session_state.py +117 -0
  194. raise_cli/session/__init__.py +5 -0
  195. raise_cli/session/bundle.py +820 -0
  196. raise_cli/session/close.py +268 -0
  197. raise_cli/session/journal.py +119 -0
  198. raise_cli/session/resolver.py +126 -0
  199. raise_cli/session/state.py +187 -0
  200. raise_cli/skills/__init__.py +44 -0
  201. raise_cli/skills/locator.py +141 -0
  202. raise_cli/skills/name_checker.py +199 -0
  203. raise_cli/skills/parser.py +145 -0
  204. raise_cli/skills/scaffold.py +212 -0
  205. raise_cli/skills/schema.py +132 -0
  206. raise_cli/skills/skillsets.py +195 -0
  207. raise_cli/skills/validator.py +197 -0
  208. raise_cli/skills_base/__init__.py +80 -0
  209. raise_cli/skills_base/contract-template.md +60 -0
  210. raise_cli/skills_base/preamble.md +37 -0
  211. raise_cli/skills_base/rai-architecture-review/SKILL.md +137 -0
  212. raise_cli/skills_base/rai-debug/SKILL.md +171 -0
  213. raise_cli/skills_base/rai-discover/SKILL.md +167 -0
  214. raise_cli/skills_base/rai-discover-document/SKILL.md +128 -0
  215. raise_cli/skills_base/rai-discover-scan/SKILL.md +147 -0
  216. raise_cli/skills_base/rai-discover-start/SKILL.md +145 -0
  217. raise_cli/skills_base/rai-discover-validate/SKILL.md +142 -0
  218. raise_cli/skills_base/rai-docs-update/SKILL.md +142 -0
  219. raise_cli/skills_base/rai-doctor/SKILL.md +120 -0
  220. raise_cli/skills_base/rai-epic-close/SKILL.md +165 -0
  221. raise_cli/skills_base/rai-epic-close/templates/retrospective.md +68 -0
  222. raise_cli/skills_base/rai-epic-design/SKILL.md +146 -0
  223. raise_cli/skills_base/rai-epic-design/templates/design.md +24 -0
  224. raise_cli/skills_base/rai-epic-design/templates/scope.md +76 -0
  225. raise_cli/skills_base/rai-epic-plan/SKILL.md +153 -0
  226. raise_cli/skills_base/rai-epic-plan/_references/sequencing-strategies.md +67 -0
  227. raise_cli/skills_base/rai-epic-plan/templates/plan-section.md +49 -0
  228. raise_cli/skills_base/rai-epic-run/SKILL.md +208 -0
  229. raise_cli/skills_base/rai-epic-start/SKILL.md +136 -0
  230. raise_cli/skills_base/rai-epic-start/templates/brief.md +34 -0
  231. raise_cli/skills_base/rai-mcp-add/SKILL.md +176 -0
  232. raise_cli/skills_base/rai-mcp-remove/SKILL.md +120 -0
  233. raise_cli/skills_base/rai-mcp-status/SKILL.md +147 -0
  234. raise_cli/skills_base/rai-problem-shape/SKILL.md +138 -0
  235. raise_cli/skills_base/rai-project-create/SKILL.md +144 -0
  236. raise_cli/skills_base/rai-project-onboard/SKILL.md +162 -0
  237. raise_cli/skills_base/rai-quality-review/SKILL.md +189 -0
  238. raise_cli/skills_base/rai-research/SKILL.md +143 -0
  239. raise_cli/skills_base/rai-research/references/research-prompt-template.md +317 -0
  240. raise_cli/skills_base/rai-session-close/SKILL.md +176 -0
  241. raise_cli/skills_base/rai-session-start/SKILL.md +110 -0
  242. raise_cli/skills_base/rai-story-close/SKILL.md +198 -0
  243. raise_cli/skills_base/rai-story-design/SKILL.md +203 -0
  244. raise_cli/skills_base/rai-story-design/references/tech-design-story-v2.md +293 -0
  245. raise_cli/skills_base/rai-story-implement/SKILL.md +115 -0
  246. raise_cli/skills_base/rai-story-plan/SKILL.md +135 -0
  247. raise_cli/skills_base/rai-story-review/SKILL.md +178 -0
  248. raise_cli/skills_base/rai-story-run/SKILL.md +282 -0
  249. raise_cli/skills_base/rai-story-start/SKILL.md +166 -0
  250. raise_cli/skills_base/rai-story-start/templates/story.md +38 -0
  251. raise_cli/skills_base/rai-welcome/SKILL.md +134 -0
  252. raise_cli/telemetry/__init__.py +42 -0
  253. raise_cli/telemetry/schemas.py +285 -0
  254. raise_cli/telemetry/writer.py +217 -0
  255. raise_cli/tier/__init__.py +0 -0
  256. raise_cli/tier/context.py +134 -0
  257. raise_cli/viz/__init__.py +7 -0
  258. raise_cli/viz/generator.py +406 -0
  259. raise_cli-2.2.1.dist-info/METADATA +433 -0
  260. raise_cli-2.2.1.dist-info/RECORD +264 -0
  261. raise_cli-2.2.1.dist-info/WHEEL +4 -0
  262. raise_cli-2.2.1.dist-info/entry_points.txt +40 -0
  263. raise_cli-2.2.1.dist-info/licenses/LICENSE +190 -0
  264. 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