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.
- evolution/__init__.py +1 -0
- evolution/accepted.py +310 -0
- evolution/adapter_scaffold.py +712 -0
- evolution/adapter_security.py +279 -0
- evolution/adapter_validator.py +347 -0
- evolution/adapter_versions.py +162 -0
- evolution/adapters/__init__.py +11 -0
- evolution/adapters/ci/__init__.py +3 -0
- evolution/adapters/ci/github_actions_adapter.py +171 -0
- evolution/adapters/config/__init__.py +3 -0
- evolution/adapters/config/terraform_adapter.py +108 -0
- evolution/adapters/dependency/__init__.py +3 -0
- evolution/adapters/dependency/pip_adapter.py +107 -0
- evolution/adapters/deployment/__init__.py +3 -0
- evolution/adapters/deployment/github_releases_adapter.py +142 -0
- evolution/adapters/git/__init__.py +4 -0
- evolution/adapters/git/git_adapter.py +69 -0
- evolution/adapters/git/git_history_walker.py +608 -0
- evolution/adapters/github_client.py +177 -0
- evolution/adapters/schema/__init__.py +3 -0
- evolution/adapters/schema/openapi_adapter.py +130 -0
- evolution/adapters/security/__init__.py +3 -0
- evolution/adapters/security/github_security_adapter.py +169 -0
- evolution/adapters/security/trivy_adapter.py +113 -0
- evolution/adapters/testing/__init__.py +3 -0
- evolution/adapters/testing/junit_adapter.py +149 -0
- evolution/agents/__init__.py +15 -0
- evolution/agents/anthropic_agent.py +94 -0
- evolution/agents/base.py +149 -0
- evolution/agents/cli_agent.py +120 -0
- evolution/cli.py +3237 -0
- evolution/config.py +369 -0
- evolution/data/adapter_blocklist.json +1 -0
- evolution/data/adapter_catalog.json +28 -0
- evolution/data/pattern_blocklist.json +1 -0
- evolution/data/pattern_index.json +1 -0
- evolution/data/sdk_fingerprints.json +302 -0
- evolution/data/universal_patterns.json +1245 -0
- evolution/data/verified_adapters.json +1 -0
- evolution/fixer.py +870 -0
- evolution/fp_validation.py +355 -0
- evolution/friendly.py +531 -0
- evolution/history.py +346 -0
- evolution/hooks.py +394 -0
- evolution/init.py +339 -0
- evolution/inline_suggestions.py +253 -0
- evolution/investigator.py +250 -0
- evolution/kb_export.py +219 -0
- evolution/kb_security.py +463 -0
- evolution/kb_sync.py +303 -0
- evolution/knowledge_store.c +22466 -0
- evolution/knowledge_store.cp311-win_amd64.pyd +0 -0
- evolution/knowledge_store.py +461 -0
- evolution/license.c +13806 -0
- evolution/license.cp311-win_amd64.pyd +0 -0
- evolution/license.py +290 -0
- evolution/llm_anthropic.py +73 -0
- evolution/llm_openrouter.py +83 -0
- evolution/notifications.py +275 -0
- evolution/orchestrator.py +824 -0
- evolution/pattern_registry.py +504 -0
- evolution/pattern_scaffold.py +230 -0
- evolution/pattern_validator.py +206 -0
- evolution/phase1_engine.py +128 -0
- evolution/phase2_engine.c +27888 -0
- evolution/phase2_engine.cp311-win_amd64.pyd +0 -0
- evolution/phase2_engine.py +548 -0
- evolution/phase3_1_renderer.py +75 -0
- evolution/phase3_engine.c +13787 -0
- evolution/phase3_engine.cp311-win_amd64.pyd +0 -0
- evolution/phase3_engine.py +212 -0
- evolution/phase4_engine.c +34611 -0
- evolution/phase4_engine.cp311-win_amd64.pyd +0 -0
- evolution/phase4_engine.py +1150 -0
- evolution/phase5_engine.c +40399 -0
- evolution/phase5_engine.cp311-win_amd64.pyd +0 -0
- evolution/phase5_engine.py +1216 -0
- evolution/pr_comment.py +242 -0
- evolution/prescan.py +371 -0
- evolution/registry.py +572 -0
- evolution/report_generator.py +1822 -0
- evolution/report_server.py +201 -0
- evolution/setup_ui.py +735 -0
- evolution/telemetry.py +141 -0
- evolution/validation_gate.py +40 -0
- evolution/watcher.py +396 -0
- evolution_engine-0.2.0.dist-info/METADATA +429 -0
- evolution_engine-0.2.0.dist-info/RECORD +91 -0
- evolution_engine-0.2.0.dist-info/WHEEL +5 -0
- evolution_engine-0.2.0.dist-info/entry_points.txt +2 -0
- 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
|