evolution-engine 0.2.0__cp311-cp311-win_amd64.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 (91) hide show
  1. evolution/__init__.py +1 -0
  2. evolution/accepted.py +310 -0
  3. evolution/adapter_scaffold.py +712 -0
  4. evolution/adapter_security.py +279 -0
  5. evolution/adapter_validator.py +347 -0
  6. evolution/adapter_versions.py +162 -0
  7. evolution/adapters/__init__.py +11 -0
  8. evolution/adapters/ci/__init__.py +3 -0
  9. evolution/adapters/ci/github_actions_adapter.py +171 -0
  10. evolution/adapters/config/__init__.py +3 -0
  11. evolution/adapters/config/terraform_adapter.py +108 -0
  12. evolution/adapters/dependency/__init__.py +3 -0
  13. evolution/adapters/dependency/pip_adapter.py +107 -0
  14. evolution/adapters/deployment/__init__.py +3 -0
  15. evolution/adapters/deployment/github_releases_adapter.py +142 -0
  16. evolution/adapters/git/__init__.py +4 -0
  17. evolution/adapters/git/git_adapter.py +69 -0
  18. evolution/adapters/git/git_history_walker.py +608 -0
  19. evolution/adapters/github_client.py +177 -0
  20. evolution/adapters/schema/__init__.py +3 -0
  21. evolution/adapters/schema/openapi_adapter.py +130 -0
  22. evolution/adapters/security/__init__.py +3 -0
  23. evolution/adapters/security/github_security_adapter.py +169 -0
  24. evolution/adapters/security/trivy_adapter.py +113 -0
  25. evolution/adapters/testing/__init__.py +3 -0
  26. evolution/adapters/testing/junit_adapter.py +149 -0
  27. evolution/agents/__init__.py +15 -0
  28. evolution/agents/anthropic_agent.py +94 -0
  29. evolution/agents/base.py +149 -0
  30. evolution/agents/cli_agent.py +120 -0
  31. evolution/cli.py +3237 -0
  32. evolution/config.py +369 -0
  33. evolution/data/adapter_blocklist.json +1 -0
  34. evolution/data/adapter_catalog.json +28 -0
  35. evolution/data/pattern_blocklist.json +1 -0
  36. evolution/data/pattern_index.json +1 -0
  37. evolution/data/sdk_fingerprints.json +302 -0
  38. evolution/data/universal_patterns.json +1245 -0
  39. evolution/data/verified_adapters.json +1 -0
  40. evolution/fixer.py +870 -0
  41. evolution/fp_validation.py +355 -0
  42. evolution/friendly.py +531 -0
  43. evolution/history.py +346 -0
  44. evolution/hooks.py +394 -0
  45. evolution/init.py +339 -0
  46. evolution/inline_suggestions.py +253 -0
  47. evolution/investigator.py +250 -0
  48. evolution/kb_export.py +219 -0
  49. evolution/kb_security.py +463 -0
  50. evolution/kb_sync.py +303 -0
  51. evolution/knowledge_store.c +22466 -0
  52. evolution/knowledge_store.cp311-win_amd64.pyd +0 -0
  53. evolution/knowledge_store.py +461 -0
  54. evolution/license.c +13806 -0
  55. evolution/license.cp311-win_amd64.pyd +0 -0
  56. evolution/license.py +290 -0
  57. evolution/llm_anthropic.py +73 -0
  58. evolution/llm_openrouter.py +83 -0
  59. evolution/notifications.py +275 -0
  60. evolution/orchestrator.py +824 -0
  61. evolution/pattern_registry.py +504 -0
  62. evolution/pattern_scaffold.py +230 -0
  63. evolution/pattern_validator.py +206 -0
  64. evolution/phase1_engine.py +128 -0
  65. evolution/phase2_engine.c +27888 -0
  66. evolution/phase2_engine.cp311-win_amd64.pyd +0 -0
  67. evolution/phase2_engine.py +548 -0
  68. evolution/phase3_1_renderer.py +75 -0
  69. evolution/phase3_engine.c +13787 -0
  70. evolution/phase3_engine.cp311-win_amd64.pyd +0 -0
  71. evolution/phase3_engine.py +212 -0
  72. evolution/phase4_engine.c +34611 -0
  73. evolution/phase4_engine.cp311-win_amd64.pyd +0 -0
  74. evolution/phase4_engine.py +1150 -0
  75. evolution/phase5_engine.c +40399 -0
  76. evolution/phase5_engine.cp311-win_amd64.pyd +0 -0
  77. evolution/phase5_engine.py +1216 -0
  78. evolution/pr_comment.py +242 -0
  79. evolution/prescan.py +371 -0
  80. evolution/registry.py +572 -0
  81. evolution/report_generator.py +1822 -0
  82. evolution/report_server.py +201 -0
  83. evolution/setup_ui.py +735 -0
  84. evolution/telemetry.py +141 -0
  85. evolution/validation_gate.py +40 -0
  86. evolution/watcher.py +396 -0
  87. evolution_engine-0.2.0.dist-info/METADATA +429 -0
  88. evolution_engine-0.2.0.dist-info/RECORD +91 -0
  89. evolution_engine-0.2.0.dist-info/WHEEL +5 -0
  90. evolution_engine-0.2.0.dist-info/entry_points.txt +2 -0
  91. evolution_engine-0.2.0.dist-info/top_level.txt +1 -0
evolution/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
evolution/accepted.py ADDED
@@ -0,0 +1,310 @@
1
+ """
2
+ Accepted Deviations — Manage acknowledged deviations in .evo/accepted.json.
3
+
4
+ Users accept deviations they've reviewed and consider intentional.
5
+ Accepted deviations are excluded from future Phase 5 advisories.
6
+
7
+ Scope types:
8
+ permanent — always suppress this family:metric (default, backward compat)
9
+ commits — suppress only when trigger events are within a commit range
10
+ dates — suppress only when trigger events are within a date range
11
+ this-run — one-time dismiss, expires on next analysis
12
+
13
+ Usage:
14
+ from evolution.accepted import AcceptedDeviations
15
+ ad = AcceptedDeviations(evo_dir)
16
+ ad.add("git:dispersion", "git", "dispersion", reason="Known refactoring spike")
17
+ ad.add("ci:run_duration", "ci", "run_duration",
18
+ scope={"type": "commits", "from": "abc123", "to": "def456"},
19
+ reason="Planned CI migration")
20
+ ad.is_accepted("git", "dispersion") # True (permanent)
21
+ ad.is_accepted_in_context("ci", "run_duration", commit_sha="ccc000") # depends on range
22
+ """
23
+
24
+ import json
25
+ from datetime import datetime, timezone
26
+ from pathlib import Path
27
+ from typing import Optional
28
+
29
+
30
+ VERSION = 2
31
+
32
+ SCOPE_PERMANENT = "permanent"
33
+ SCOPE_COMMITS = "commits"
34
+ SCOPE_DATES = "dates"
35
+ SCOPE_THIS_RUN = "this-run"
36
+ VALID_SCOPE_TYPES = {SCOPE_PERMANENT, SCOPE_COMMITS, SCOPE_DATES, SCOPE_THIS_RUN}
37
+
38
+
39
+ class AcceptedDeviations:
40
+ """Manage accepted/acknowledged deviations in .evo/accepted.json."""
41
+
42
+ def __init__(self, evo_dir: Path | str):
43
+ self.path = Path(evo_dir) / "accepted.json"
44
+
45
+ def load(self) -> list[dict]:
46
+ """Load accepted deviations list."""
47
+ if not self.path.exists():
48
+ return []
49
+ try:
50
+ data = json.loads(self.path.read_text(encoding="utf-8"))
51
+ return data.get("accepted", [])
52
+ except (json.JSONDecodeError, KeyError):
53
+ return []
54
+
55
+ def save(self, entries: list[dict]) -> None:
56
+ """Write accepted deviations to disk."""
57
+ self.path.parent.mkdir(parents=True, exist_ok=True)
58
+ data = {"version": VERSION, "accepted": entries}
59
+ self.path.write_text(json.dumps(data, indent=2), encoding="utf-8")
60
+
61
+ def add(
62
+ self,
63
+ key: str,
64
+ family: str,
65
+ metric: str,
66
+ reason: str = "",
67
+ advisory_id: str = "",
68
+ scope: Optional[dict] = None,
69
+ ) -> bool:
70
+ """Accept a deviation with optional scope.
71
+
72
+ Args:
73
+ key: The family:metric key (e.g. "git:dispersion").
74
+ family: Signal family.
75
+ metric: Signal metric name.
76
+ reason: Human reason for acceptance.
77
+ advisory_id: The advisory this acceptance came from.
78
+ scope: Scope dict, e.g.:
79
+ {"type": "permanent"} (default)
80
+ {"type": "commits", "from": "abc123", "to": "def456"}
81
+ {"type": "dates", "from": "2026-02-10", "to": "2026-02-14"}
82
+ {"type": "this-run", "advisory_id": "..."}
83
+
84
+ Returns:
85
+ True if added, False if an identical permanent acceptance exists.
86
+ """
87
+ if scope is None:
88
+ scope = {"type": SCOPE_PERMANENT}
89
+
90
+ scope_type = scope.get("type", SCOPE_PERMANENT)
91
+ if scope_type not in VALID_SCOPE_TYPES:
92
+ raise ValueError(f"Invalid scope type: {scope_type}. Must be one of {VALID_SCOPE_TYPES}")
93
+
94
+ entries = self.load()
95
+
96
+ # For permanent scope, check for duplicate keys (backward compat)
97
+ if scope_type == SCOPE_PERMANENT:
98
+ existing_permanent = {
99
+ e["key"] for e in entries
100
+ if e.get("scope", {}).get("type", SCOPE_PERMANENT) == SCOPE_PERMANENT
101
+ }
102
+ if key in existing_permanent:
103
+ return False
104
+
105
+ entry = {
106
+ "key": key,
107
+ "family": family,
108
+ "metric": metric,
109
+ "reason": reason,
110
+ "accepted_at": datetime.now(timezone.utc).isoformat(),
111
+ "from_advisory": advisory_id,
112
+ "scope": scope,
113
+ }
114
+
115
+ entries.append(entry)
116
+ self.save(entries)
117
+ return True
118
+
119
+ def remove(self, key: str) -> bool:
120
+ """Remove an accepted deviation by key. Removes ALL scopes for that key.
121
+
122
+ Returns False if not found.
123
+ """
124
+ entries = self.load()
125
+ new_entries = [e for e in entries if e["key"] != key]
126
+ if len(new_entries) == len(entries):
127
+ return False
128
+ self.save(new_entries)
129
+ return True
130
+
131
+ def remove_scoped(self, key: str, scope_type: str) -> bool:
132
+ """Remove accepted deviations for a key with a specific scope type.
133
+
134
+ Returns False if none found.
135
+ """
136
+ entries = self.load()
137
+ new_entries = [
138
+ e for e in entries
139
+ if not (e["key"] == key and e.get("scope", {}).get("type", SCOPE_PERMANENT) == scope_type)
140
+ ]
141
+ if len(new_entries) == len(entries):
142
+ return False
143
+ self.save(new_entries)
144
+ return True
145
+
146
+ def clear(self) -> int:
147
+ """Remove all accepted deviations. Returns count removed."""
148
+ entries = self.load()
149
+ count = len(entries)
150
+ if count > 0:
151
+ self.save([])
152
+ return count
153
+
154
+ def cleanup_expired(self, current_advisory_id: str = "") -> int:
155
+ """Remove expired this-run entries that don't match the current advisory.
156
+
157
+ Called at the start of each analysis. Returns count removed.
158
+ """
159
+ entries = self.load()
160
+ kept = []
161
+ removed = 0
162
+ for e in entries:
163
+ scope = e.get("scope", {})
164
+ if scope.get("type") == SCOPE_THIS_RUN:
165
+ # Keep only if it matches the current advisory
166
+ if scope.get("advisory_id") == current_advisory_id and current_advisory_id:
167
+ kept.append(e)
168
+ else:
169
+ removed += 1
170
+ else:
171
+ kept.append(e)
172
+ if removed > 0:
173
+ self.save(kept)
174
+ return removed
175
+
176
+ def is_accepted(self, family: str, metric: str) -> bool:
177
+ """Check if a family:metric pair has a permanent acceptance."""
178
+ key = f"{family}:{metric}"
179
+ for e in self.load():
180
+ if e["key"] != key:
181
+ continue
182
+ scope_type = e.get("scope", {}).get("type", SCOPE_PERMANENT)
183
+ if scope_type == SCOPE_PERMANENT:
184
+ return True
185
+ return False
186
+
187
+ def is_accepted_in_context(
188
+ self,
189
+ family: str,
190
+ metric: str,
191
+ commit_sha: str = "",
192
+ event_date: str = "",
193
+ advisory_id: str = "",
194
+ commit_list: Optional[list[str]] = None,
195
+ ) -> bool:
196
+ """Check if a family:metric is accepted given the current context.
197
+
198
+ This is the full scope-aware check used by Phase 5.
199
+
200
+ Args:
201
+ family: Signal family.
202
+ metric: Signal metric.
203
+ commit_sha: The commit SHA triggering the signal.
204
+ event_date: ISO date string of the event.
205
+ advisory_id: Current advisory ID (for this-run scope).
206
+ commit_list: Ordered list of all commit SHAs in the analysis window.
207
+ Used for commit range checking.
208
+
209
+ Returns:
210
+ True if any matching acceptance applies in this context.
211
+ """
212
+ key = f"{family}:{metric}"
213
+ entries = [e for e in self.load() if e["key"] == key]
214
+
215
+ for e in entries:
216
+ scope = e.get("scope", {})
217
+ scope_type = scope.get("type", SCOPE_PERMANENT)
218
+
219
+ if scope_type == SCOPE_PERMANENT:
220
+ return True
221
+
222
+ if scope_type == SCOPE_THIS_RUN:
223
+ if scope.get("advisory_id") == advisory_id and advisory_id:
224
+ return True
225
+
226
+ if scope_type == SCOPE_COMMITS and commit_sha:
227
+ if _commit_in_range(
228
+ commit_sha,
229
+ scope.get("from", ""),
230
+ scope.get("to", ""),
231
+ commit_list or [],
232
+ ):
233
+ return True
234
+
235
+ if scope_type == SCOPE_DATES and event_date:
236
+ date_from = scope.get("from", "")
237
+ date_to = scope.get("to", "")
238
+ if _date_in_range(event_date, date_from, date_to):
239
+ return True
240
+
241
+ return False
242
+
243
+ def accepted_keys(self) -> set[str]:
244
+ """Return set of permanently accepted family:metric keys (backward compat)."""
245
+ keys = set()
246
+ for e in self.load():
247
+ scope_type = e.get("scope", {}).get("type", SCOPE_PERMANENT)
248
+ if scope_type == SCOPE_PERMANENT:
249
+ keys.add(e["key"])
250
+ return keys
251
+
252
+ def all_entries_for_key(self, key: str) -> list[dict]:
253
+ """Return all acceptance entries for a given key (all scopes)."""
254
+ return [e for e in self.load() if e["key"] == key]
255
+
256
+
257
+ def _commit_in_range(
258
+ sha: str,
259
+ range_from: str,
260
+ range_to: str,
261
+ commit_list: list[str],
262
+ ) -> bool:
263
+ """Check if a commit SHA falls within a range.
264
+
265
+ Uses commit_list (ordered oldest→newest) for positional comparison.
266
+ Falls back to prefix matching if the commit isn't in the list.
267
+ """
268
+ if not range_from:
269
+ return False
270
+
271
+ # Single commit scope (no "to")
272
+ if not range_to or range_to == range_from:
273
+ return sha.startswith(range_from) or range_from.startswith(sha)
274
+
275
+ # Prefix-match the from/to in the commit list
276
+ from_idx = None
277
+ to_idx = None
278
+ sha_idx = None
279
+
280
+ for i, c in enumerate(commit_list):
281
+ if c.startswith(range_from) or range_from.startswith(c):
282
+ from_idx = i
283
+ if c.startswith(range_to) or range_to.startswith(c):
284
+ to_idx = i
285
+ if c.startswith(sha) or sha.startswith(c):
286
+ sha_idx = i
287
+
288
+ if from_idx is not None and to_idx is not None and sha_idx is not None:
289
+ low = min(from_idx, to_idx)
290
+ high = max(from_idx, to_idx)
291
+ return low <= sha_idx <= high
292
+
293
+ # Fallback: direct prefix match against from/to
294
+ return sha.startswith(range_from) or sha.startswith(range_to)
295
+
296
+
297
+ def _date_in_range(event_date: str, date_from: str, date_to: str) -> bool:
298
+ """Check if an event date falls within a date range.
299
+
300
+ Dates are compared as strings (ISO format: YYYY-MM-DD or full ISO).
301
+ """
302
+ if not date_from:
303
+ return False
304
+
305
+ # Normalize to date-only for comparison
306
+ event_day = event_date[:10]
307
+ from_day = date_from[:10]
308
+ to_day = (date_to[:10] if date_to else from_day)
309
+
310
+ return from_day <= event_day <= to_day