@jesdi/skills 0.0.50 → 0.0.51
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.
- package/package.json +2 -2
- package/skills/test-prune/SKILL.md +156 -0
- package/skills/test-prune/prune.py +202 -0
- package/skills/test-prune/prune_repo.py +319 -0
- package/skills/test-prune/prune_review.py +363 -0
- package/skills/test-prune/prune_runs.py +240 -0
- package/skills/test-prune/prune_source.py +237 -0
- package/skills/test-prune/prune_strengthen.py +301 -0
- package/skills/test-prune/tests/conftest.py +15 -0
- package/skills/test-prune/tests/prune_harness.py +205 -0
- package/skills/test-prune/tests/test_prune_guards.py +871 -0
- package/skills/test-prune/tests/test_prune_inputs.py +97 -0
- package/skills/test-prune/tests/test_prune_review.py +113 -0
- package/skills/test-prune/tests/test_prune_source.py +84 -0
- package/skills/test-prune/tests/test_prune_strengthen.py +183 -0
- package/skills/test-prune/tests/test_prune_unattended.py +225 -0
- package/skills/test-prune/tests/testdata/gh +16 -0
- package/skills/test-prune/tests/testdata/repo/Makefile +25 -0
- package/skills/test-prune/tests/testdata/repo/backend/mutation_baseline.json +16 -0
- package/skills/test-prune/tests/testdata/repo/backend/services/concentration.py +2 -0
- package/skills/test-prune/tests/testdata/repo/backend/services/rebalancer.py +11 -0
- package/skills/test-prune/tests/testdata/repo/backend/tests/conftest.py +6 -0
- package/skills/test-prune/tests/testdata/repo/backend/tests/test_concentration.py +73 -0
- package/skills/test-prune/tests/testdata/repo/backend/tests/test_rebalancer.py +77 -0
- package/skills/test-prune/tests/testdata/repo/fake_engine.py +289 -0
- package/skills/test-prune/tests/testdata/repo/kill_map.json +131 -0
- package/skills-manifest.json +7 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Fake mutmut/pytest/gate engine behind the fixture repo's make targets.
|
|
2
|
+
|
|
3
|
+
Model: kill_map.json maps {module: {mutant id: [test ids that kill it]}}. A mutant is
|
|
4
|
+
killed iff one of its killers exists in the working tree (file::[Class::]func, parametrize
|
|
5
|
+
brackets ignored). Output formats copy the target repo's scripts (check_mutation.py,
|
|
6
|
+
test_candidates.py, check_prune_pr.py). Logs go to --logs, outside the repo.
|
|
7
|
+
|
|
8
|
+
Env knobs: FAKE_SNEAK_EQUIVALENT=<module>:<mutant id> makes mutation-baseline add an
|
|
9
|
+
equivalent id; FAKE_GATE_FAIL=1 makes prune-gate fail at point 1; FAKE_TEST_FAIL=1 makes
|
|
10
|
+
`make test` fail.
|
|
11
|
+
"""
|
|
12
|
+
import argparse
|
|
13
|
+
import ast
|
|
14
|
+
import hashlib
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
SURVIVING = {0, 5, 33}
|
|
22
|
+
KILLED = {1, 3}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def log(logs, name, entry):
|
|
26
|
+
with open(Path(logs) / f"{name}.log", "a") as f:
|
|
27
|
+
f.write(json.dumps(entry) + "\n")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _param_ids(func):
|
|
31
|
+
for dec in func.decorator_list:
|
|
32
|
+
if not (isinstance(dec, ast.Call) and getattr(dec.func, "attr", "") == "parametrize"):
|
|
33
|
+
continue
|
|
34
|
+
for kw in dec.keywords:
|
|
35
|
+
if kw.arg == "ids":
|
|
36
|
+
return list(ast.literal_eval(kw.value))
|
|
37
|
+
return [str(v) for v in ast.literal_eval(dec.args[1])]
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def collect_source(rel_path, source):
|
|
42
|
+
"""Test ids pytest would collect from one file, in file order."""
|
|
43
|
+
ids = []
|
|
44
|
+
|
|
45
|
+
def add(prefix, func):
|
|
46
|
+
if not func.name.startswith("test"):
|
|
47
|
+
return
|
|
48
|
+
params = _param_ids(func)
|
|
49
|
+
base = f"{prefix}::{func.name}"
|
|
50
|
+
ids.extend([f"{base}[{p}]" for p in params] if params else [base])
|
|
51
|
+
|
|
52
|
+
for node in ast.parse(source).body:
|
|
53
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
54
|
+
add(rel_path, node)
|
|
55
|
+
elif isinstance(node, ast.ClassDef) and node.name.startswith("Test"):
|
|
56
|
+
for inner in node.body:
|
|
57
|
+
if isinstance(inner, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
58
|
+
add(f"{rel_path}::{node.name}", inner)
|
|
59
|
+
return ids
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def collect_tree(backend):
|
|
63
|
+
ids = []
|
|
64
|
+
for path in sorted((backend / "tests").rglob("test_*.py")):
|
|
65
|
+
ids += collect_source(path.relative_to(backend).as_posix(), path.read_text())
|
|
66
|
+
return ids
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def collect_ref(root, ref):
|
|
70
|
+
files = subprocess.run(["git", "ls-tree", "-r", "--name-only", ref, "backend/tests"], cwd=root,
|
|
71
|
+
check=True, capture_output=True, text=True).stdout.split()
|
|
72
|
+
ids = []
|
|
73
|
+
for name in sorted(files):
|
|
74
|
+
if Path(name).name.startswith("test_") and name.endswith(".py"):
|
|
75
|
+
source = subprocess.run(["git", "show", f"{ref}:{name}"], cwd=root, check=True,
|
|
76
|
+
capture_output=True, text=True).stdout
|
|
77
|
+
ids += collect_source(name[len("backend/"):], source)
|
|
78
|
+
return ids
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def fn_hash(module, function):
|
|
82
|
+
return hashlib.md5(f"{module}:{function}".encode()).hexdigest()[:12]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def dotted(module):
|
|
86
|
+
return module[:-3].replace("/", ".")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def mutation(root, logs):
|
|
90
|
+
backend = root / "backend"
|
|
91
|
+
mutants = backend / "mutants"
|
|
92
|
+
state = "absent" if not mutants.exists() else ("empty" if not any(mutants.iterdir()) else "populated")
|
|
93
|
+
head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=root, capture_output=True, text=True).stdout.strip()
|
|
94
|
+
log(logs, "mutation", {"mutants": state, "head": head, "KEEP_CACHE": os.environ.get("KEEP_CACHE")})
|
|
95
|
+
present = {i.split("[")[0] for i in collect_tree(backend)}
|
|
96
|
+
kill_map = json.loads((root / "kill_map.json").read_text())
|
|
97
|
+
for module, by_mutant in kill_map.items():
|
|
98
|
+
codes, hashes = {}, {}
|
|
99
|
+
for mutant_id, killers in by_mutant.items():
|
|
100
|
+
killed = any(k.split("[")[0] in present for k in killers)
|
|
101
|
+
codes[f"{dotted(module)}.{mutant_id}"] = 1 if killed else 0
|
|
102
|
+
function = mutant_id.partition("__mutmut_")[0]
|
|
103
|
+
hashes[function] = fn_hash(module, function)
|
|
104
|
+
meta = mutants / f"{module}.meta"
|
|
105
|
+
meta.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
meta.write_text(json.dumps({"exit_code_by_key": codes, "hash_by_function_name": hashes}, indent=2))
|
|
107
|
+
print("done")
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def read_results(backend, module, declared):
|
|
112
|
+
meta_path = backend / "mutants" / f"{module}.meta"
|
|
113
|
+
if not meta_path.exists():
|
|
114
|
+
return None
|
|
115
|
+
meta = json.loads(meta_path.read_text())
|
|
116
|
+
hashes = meta["hash_by_function_name"]
|
|
117
|
+
codes = {k.rpartition(".")[2]: c for k, c in meta["exit_code_by_key"].items()}
|
|
118
|
+
equivalent = {}
|
|
119
|
+
for mutant_id, digest in declared.items():
|
|
120
|
+
if mutant_id not in codes:
|
|
121
|
+
print(f" ! {module}: equivalent {mutant_id} no longer exists, dropped")
|
|
122
|
+
elif hashes.get(mutant_id.partition("__mutmut_")[0]) != digest:
|
|
123
|
+
print(f" ! {module}: equivalent {mutant_id} is stale (function changed), counts as a survivor")
|
|
124
|
+
else:
|
|
125
|
+
equivalent[mutant_id] = digest
|
|
126
|
+
killed = sum(1 for c in codes.values() if c in KILLED)
|
|
127
|
+
survivors = sorted(m for m, c in codes.items() if c in SURVIVING and m not in equivalent)
|
|
128
|
+
tested = len(codes) - len(equivalent)
|
|
129
|
+
score = round(100 * killed / tested, 2) if tested else 0.0
|
|
130
|
+
return {"killed": killed, "total": len(codes), "survivors": survivors, "equivalent": equivalent, "score": score}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def modules(root):
|
|
134
|
+
return list(json.loads((root / "kill_map.json").read_text()))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def mutation_check(root):
|
|
138
|
+
backend = root / "backend"
|
|
139
|
+
baseline = json.loads((backend / "mutation_baseline.json").read_text())
|
|
140
|
+
ok = True
|
|
141
|
+
for module in modules(root):
|
|
142
|
+
entry = baseline.get(module)
|
|
143
|
+
result = read_results(backend, module, (entry or {}).get("equivalent", {}))
|
|
144
|
+
if result is None:
|
|
145
|
+
print(f" ✗ {module}: no mutmut results, run 'make mutation'")
|
|
146
|
+
ok = False
|
|
147
|
+
continue
|
|
148
|
+
count = len(result["survivors"])
|
|
149
|
+
if entry is None:
|
|
150
|
+
print(f" • {module}: no baseline, run the update command ({count} survivors, score {result['score']:.2f})")
|
|
151
|
+
continue
|
|
152
|
+
allowed = entry["survivors"]
|
|
153
|
+
if count > allowed:
|
|
154
|
+
print(f" ✗ {module}: {count} survivors > baseline {allowed} (score {result['score']:.2f})")
|
|
155
|
+
for mutant_id in result["survivors"]:
|
|
156
|
+
print(f" {mutant_id}")
|
|
157
|
+
ok = False
|
|
158
|
+
continue
|
|
159
|
+
print(f" ✓ {module}: {count} survivors, baseline {allowed}, score {result['score']:.2f}")
|
|
160
|
+
if count < allowed:
|
|
161
|
+
print(" ↑ fewer survivors than the baseline, run 'make mutation-baseline' to lock it in")
|
|
162
|
+
print("Mutation ratchet PASSED." if ok else "Mutation ratchet FAILED.")
|
|
163
|
+
return 0 if ok else 1
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def mutation_baseline(root, logs):
|
|
167
|
+
backend = root / "backend"
|
|
168
|
+
path = backend / "mutation_baseline.json"
|
|
169
|
+
baseline = json.loads(path.read_text())
|
|
170
|
+
sneak = os.environ.get("FAKE_SNEAK_EQUIVALENT")
|
|
171
|
+
log(logs, "mutation-baseline", {"sneak": sneak})
|
|
172
|
+
new = {}
|
|
173
|
+
for module in modules(root):
|
|
174
|
+
entry = baseline.get(module)
|
|
175
|
+
declared = dict((entry or {}).get("equivalent", {}))
|
|
176
|
+
if sneak and sneak.split(":")[0] == module:
|
|
177
|
+
mutant_id = sneak.split(":")[1]
|
|
178
|
+
declared[mutant_id] = fn_hash(module, mutant_id.partition("__mutmut_")[0])
|
|
179
|
+
result = read_results(backend, module, declared)
|
|
180
|
+
if result is None:
|
|
181
|
+
print(f" ! {module}: no mutmut results, baseline kept")
|
|
182
|
+
if entry is not None:
|
|
183
|
+
new[module] = entry
|
|
184
|
+
continue
|
|
185
|
+
new[module] = {"equivalent": dict(sorted(result["equivalent"].items())), "killed": result["killed"],
|
|
186
|
+
"score": result["score"], "survivors": len(result["survivors"]), "total": result["total"]}
|
|
187
|
+
print(f" {module}: {entry['survivors'] if entry else 'new'} → {len(result['survivors'])} survivors")
|
|
188
|
+
path.write_text(json.dumps(new, indent=2, sort_keys=True) + "\n")
|
|
189
|
+
return 0
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _function_name(mutant_id):
|
|
193
|
+
mangled = mutant_id.partition("__mutmut_")[0]
|
|
194
|
+
return mangled[2:] if mangled.startswith("x_") else ".".join(mangled.split("ǁ")[1:])
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_candidates(root, logs, args):
|
|
198
|
+
backend = root / "backend"
|
|
199
|
+
log(logs, "test-candidates", {"args": args})
|
|
200
|
+
out = backend / ".mutation"
|
|
201
|
+
out.mkdir(exist_ok=True)
|
|
202
|
+
ids = collect_tree(backend)
|
|
203
|
+
out.joinpath("tests.txt").write_text("\n".join(ids) + f"\n\n{len(ids)} tests collected in 0.01s\n")
|
|
204
|
+
provided = Path(logs) / "candidates.json"
|
|
205
|
+
candidates = json.loads(provided.read_text()) if provided.exists() else []
|
|
206
|
+
if "--protected-only" in args:
|
|
207
|
+
candidates = [c for c in candidates if not c.get("unprotected")]
|
|
208
|
+
candidates = [{k: v for k, v in c.items() if k != "unprotected"} for c in candidates]
|
|
209
|
+
baseline = json.loads((backend / "mutation_baseline.json").read_text())
|
|
210
|
+
groups = {}
|
|
211
|
+
for module in modules(root):
|
|
212
|
+
result = read_results(backend, module, baseline.get(module, {}).get("equivalent", {}))
|
|
213
|
+
for mutant_id in result["survivors"] if result else []:
|
|
214
|
+
groups.setdefault((module, _function_name(mutant_id)), []).append(mutant_id)
|
|
215
|
+
ordered = sorted(groups.items(), key=lambda kv: (-len(kv[1]), kv[0]))
|
|
216
|
+
report = {"candidates": candidates, "no_measured_coverage": [],
|
|
217
|
+
"weak_spots": [{"module": m, "function": f, "mutant_ids": i} for (m, f), i in ordered]}
|
|
218
|
+
out.joinpath("test-candidates.json").write_text(json.dumps(report, indent=2) + "\n")
|
|
219
|
+
print(f"{len(candidates)} candidates, 0 without measured coverage, {len(ordered)} weak-spot functions"
|
|
220
|
+
f" → {out / 'test-candidates.json'}")
|
|
221
|
+
return 0
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def prune_gate(root, logs, base, head):
|
|
225
|
+
log(logs, "prune-gate", {"base": base, "head": head})
|
|
226
|
+
if os.environ.get("FAKE_GATE_FAIL"):
|
|
227
|
+
print("Prune gate FAILED at point 1:")
|
|
228
|
+
print(" ✗ backend/tests/test_rebalancer.py: adds line 3: 'assert True'")
|
|
229
|
+
return 1
|
|
230
|
+
numstat = subprocess.run(["git", "diff", "--numstat", base, head], cwd=root, check=True,
|
|
231
|
+
capture_output=True, text=True).stdout
|
|
232
|
+
problems = []
|
|
233
|
+
for line in numstat.splitlines():
|
|
234
|
+
added, _, path = line.split("\t")
|
|
235
|
+
if not path.startswith("backend/tests/") or Path(path).name == "conftest.py":
|
|
236
|
+
problems.append(f"{path}: outside the deletable test files")
|
|
237
|
+
elif added != "0":
|
|
238
|
+
problems.append(f"{path}: adds {added} lines")
|
|
239
|
+
base_ids, head_ids = set(collect_ref(root, base)), set(collect_ref(root, head))
|
|
240
|
+
problems += [f"{t}: added test" for t in sorted(head_ids - base_ids)]
|
|
241
|
+
if problems:
|
|
242
|
+
print("Prune gate FAILED at point 1:")
|
|
243
|
+
for p in problems:
|
|
244
|
+
print(f" ✗ {p}")
|
|
245
|
+
return 1
|
|
246
|
+
deleted = sorted(base_ids - head_ids)
|
|
247
|
+
print(f"Prune gate PASSED: {len(deleted)} deleted tests, all protected by the ratchet")
|
|
248
|
+
for t in deleted:
|
|
249
|
+
print(f" ✓ {t}")
|
|
250
|
+
return 0
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def run_tests(root, logs):
|
|
254
|
+
log(logs, "test", {})
|
|
255
|
+
if os.environ.get("FAKE_TEST_FAIL"):
|
|
256
|
+
print("1 failed")
|
|
257
|
+
return 1
|
|
258
|
+
for path in (root / "backend" / "tests").rglob("*.py"):
|
|
259
|
+
ast.parse(path.read_text())
|
|
260
|
+
print("passed")
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def main():
|
|
265
|
+
parser = argparse.ArgumentParser()
|
|
266
|
+
parser.add_argument("--logs", required=True)
|
|
267
|
+
parser.add_argument("--root", type=Path, required=True)
|
|
268
|
+
parser.add_argument("target")
|
|
269
|
+
parser.add_argument("rest", nargs=argparse.REMAINDER)
|
|
270
|
+
a = parser.parse_args()
|
|
271
|
+
root = a.root
|
|
272
|
+
if a.target == "mutation":
|
|
273
|
+
return mutation(root, a.logs)
|
|
274
|
+
if a.target == "mutation-check":
|
|
275
|
+
return mutation_check(root)
|
|
276
|
+
if a.target == "mutation-baseline":
|
|
277
|
+
return mutation_baseline(root, a.logs)
|
|
278
|
+
if a.target == "test-candidates":
|
|
279
|
+
return test_candidates(root, a.logs, a.rest)
|
|
280
|
+
if a.target == "prune-gate":
|
|
281
|
+
return prune_gate(root, a.logs, *a.rest)
|
|
282
|
+
if a.target == "test":
|
|
283
|
+
return run_tests(root, a.logs)
|
|
284
|
+
log(a.logs, a.target, {})
|
|
285
|
+
return 0
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if __name__ == "__main__":
|
|
289
|
+
sys.exit(main())
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
{
|
|
2
|
+
"services/concentration.py": {
|
|
3
|
+
"x_herfindahl__mutmut_1": [
|
|
4
|
+
"tests/test_concentration.py::test_hhi_reference",
|
|
5
|
+
"tests/test_concentration.py::test_hhi_case_01"
|
|
6
|
+
],
|
|
7
|
+
"x_herfindahl__mutmut_2": [
|
|
8
|
+
"tests/test_concentration.py::test_hhi_reference",
|
|
9
|
+
"tests/test_concentration.py::test_hhi_case_01"
|
|
10
|
+
],
|
|
11
|
+
"x_herfindahl__mutmut_3": [
|
|
12
|
+
"tests/test_concentration.py::test_hhi_reference",
|
|
13
|
+
"tests/test_concentration.py::test_hhi_case_01"
|
|
14
|
+
],
|
|
15
|
+
"x_herfindahl__mutmut_4": [
|
|
16
|
+
"tests/test_concentration.py::test_hhi_reference",
|
|
17
|
+
"tests/test_concentration.py::test_hhi_case_01"
|
|
18
|
+
],
|
|
19
|
+
"x_herfindahl__mutmut_5": [
|
|
20
|
+
"tests/test_concentration.py::test_hhi_reference",
|
|
21
|
+
"tests/test_concentration.py::test_hhi_case_01"
|
|
22
|
+
],
|
|
23
|
+
"x_herfindahl__mutmut_6": [
|
|
24
|
+
"tests/test_concentration.py::test_hhi_reference",
|
|
25
|
+
"tests/test_concentration.py::test_hhi_case_01"
|
|
26
|
+
],
|
|
27
|
+
"x_herfindahl__mutmut_7": [
|
|
28
|
+
"tests/test_concentration.py::test_hhi_reference",
|
|
29
|
+
"tests/test_concentration.py::test_hhi_case_01"
|
|
30
|
+
],
|
|
31
|
+
"x_herfindahl__mutmut_8": [
|
|
32
|
+
"tests/test_concentration.py::test_hhi_reference",
|
|
33
|
+
"tests/test_concentration.py::test_hhi_case_01"
|
|
34
|
+
],
|
|
35
|
+
"x_herfindahl__mutmut_9": [],
|
|
36
|
+
"x_herfindahl__mutmut_10": []
|
|
37
|
+
},
|
|
38
|
+
"services/rebalancer.py": {
|
|
39
|
+
"x_compute_trades__mutmut_1": [
|
|
40
|
+
"tests/test_rebalancer.py::test_zero_weight",
|
|
41
|
+
"tests/test_rebalancer.py::test_small_weight",
|
|
42
|
+
"tests/test_rebalancer.py::test_compute_trades_sums_to_zero"
|
|
43
|
+
],
|
|
44
|
+
"x_compute_trades__mutmut_2": [
|
|
45
|
+
"tests/test_rebalancer.py::test_zero_weight",
|
|
46
|
+
"tests/test_rebalancer.py::test_small_weight",
|
|
47
|
+
"tests/test_rebalancer.py::test_compute_trades_sums_to_zero"
|
|
48
|
+
],
|
|
49
|
+
"x_compute_trades__mutmut_3": [
|
|
50
|
+
"tests/test_rebalancer.py::test_zero_weight",
|
|
51
|
+
"tests/test_rebalancer.py::test_small_weight",
|
|
52
|
+
"tests/test_rebalancer.py::test_compute_trades_sums_to_zero"
|
|
53
|
+
],
|
|
54
|
+
"x_compute_trades__mutmut_4": [
|
|
55
|
+
"tests/test_rebalancer.py::test_zero_weight",
|
|
56
|
+
"tests/test_rebalancer.py::test_small_weight",
|
|
57
|
+
"tests/test_rebalancer.py::test_compute_trades_sums_to_zero"
|
|
58
|
+
],
|
|
59
|
+
"x_compute_trades__mutmut_5": [
|
|
60
|
+
"tests/test_rebalancer.py::test_zero_weight",
|
|
61
|
+
"tests/test_rebalancer.py::test_small_weight",
|
|
62
|
+
"tests/test_rebalancer.py::test_compute_trades_sums_to_zero"
|
|
63
|
+
],
|
|
64
|
+
"x_compute_trades__mutmut_6": [
|
|
65
|
+
"tests/test_rebalancer.py::test_zero_weight",
|
|
66
|
+
"tests/test_rebalancer.py::test_small_weight",
|
|
67
|
+
"tests/test_rebalancer.py::test_compute_trades_sums_to_zero"
|
|
68
|
+
],
|
|
69
|
+
"x_compute_trades__mutmut_7": [
|
|
70
|
+
"tests/test_rebalancer.py::test_compute_trades_rounding"
|
|
71
|
+
],
|
|
72
|
+
"x_compute_trades__mutmut_8": [
|
|
73
|
+
"tests/test_rebalancer.py::test_small_weight"
|
|
74
|
+
],
|
|
75
|
+
"x_compute_trades__mutmut_9": [
|
|
76
|
+
"tests/test_rebalancer.py::test_compute_trades_rounding"
|
|
77
|
+
],
|
|
78
|
+
"x_apply_drift__mutmut_1": [
|
|
79
|
+
"tests/test_rebalancer.py::test_apply_drift_bounds",
|
|
80
|
+
"tests/test_rebalancer.py::TestDriftHelpers::test_drift_scales"
|
|
81
|
+
],
|
|
82
|
+
"x_apply_drift__mutmut_2": [
|
|
83
|
+
"tests/test_rebalancer.py::test_apply_drift_new"
|
|
84
|
+
],
|
|
85
|
+
"x_apply_drift__mutmut_3": [
|
|
86
|
+
"tests/test_rebalancer.py::test_apply_drift_bounds",
|
|
87
|
+
"tests/test_rebalancer.py::TestDriftHelpers::test_drift_scales"
|
|
88
|
+
],
|
|
89
|
+
"x_apply_drift__mutmut_4": [],
|
|
90
|
+
"x_rebalance__mutmut_1": [],
|
|
91
|
+
"x_rebalance__mutmut_2": [],
|
|
92
|
+
"x_rebalance__mutmut_3": [],
|
|
93
|
+
"x_rebalance__mutmut_4": [],
|
|
94
|
+
"x_rebalance__mutmut_5": [],
|
|
95
|
+
"x_rebalance__mutmut_6": [],
|
|
96
|
+
"x_rebalance__mutmut_7": [],
|
|
97
|
+
"x_rebalance__mutmut_8": [],
|
|
98
|
+
"x_rebalance__mutmut_9": [],
|
|
99
|
+
"x_rebalance__mutmut_10": [],
|
|
100
|
+
"x_rebalance__mutmut_11": [],
|
|
101
|
+
"x_rebalance__mutmut_12": [],
|
|
102
|
+
"x_rebalance__mutmut_13": [],
|
|
103
|
+
"x_rebalance__mutmut_14": [],
|
|
104
|
+
"x_rebalance__mutmut_15": [],
|
|
105
|
+
"x_rebalance__mutmut_16": [],
|
|
106
|
+
"x_rebalance__mutmut_17": [
|
|
107
|
+
"tests/test_rebalancer.py::test_hold_when_on_target",
|
|
108
|
+
"tests/test_rebalancer.py::test_hold_when_within_band",
|
|
109
|
+
"tests/test_rebalancer.py::test_sell_only_rebalance"
|
|
110
|
+
],
|
|
111
|
+
"x_rebalance__mutmut_18": [
|
|
112
|
+
"tests/test_rebalancer.py::test_hold_when_on_target",
|
|
113
|
+
"tests/test_rebalancer.py::test_hold_when_within_band",
|
|
114
|
+
"tests/test_rebalancer.py::test_sell_only_rebalance"
|
|
115
|
+
],
|
|
116
|
+
"x_rebalance__mutmut_19": [
|
|
117
|
+
"tests/test_rebalancer.py::test_hold_when_on_target",
|
|
118
|
+
"tests/test_rebalancer.py::test_hold_when_within_band",
|
|
119
|
+
"tests/test_rebalancer.py::test_sell_only_rebalance"
|
|
120
|
+
],
|
|
121
|
+
"x_rebalance__mutmut_20": [
|
|
122
|
+
"tests/test_rebalancer.py::test_sell_only_rebalance"
|
|
123
|
+
],
|
|
124
|
+
"x_rebalance__mutmut_21": [
|
|
125
|
+
"tests/test_rebalancer.py::test_sell_only_rebalance"
|
|
126
|
+
],
|
|
127
|
+
"x_rebalance__mutmut_22": [
|
|
128
|
+
"tests/test_rebalancer.py::test_sell_only_rebalance"
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
}
|
package/skills-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-19T10:48:11.958Z",
|
|
4
4
|
"skills": [
|
|
5
5
|
{
|
|
6
6
|
"name": "backlog",
|
|
@@ -80,6 +80,12 @@
|
|
|
80
80
|
"hash": "sha256-e8204e4c6f05e2721a20d9ac3f72b8ff84a2854647688bb9bf057312b53a59f4",
|
|
81
81
|
"description": "One interactive HTML page for a plan review or a questionnaire, built from a fixed template. Use when a pipeline session must deliver a plan review or a questionnaire as an interactive page the console shows."
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
"name": "test-prune",
|
|
85
|
+
"version": "0.1.0",
|
|
86
|
+
"hash": "sha256-84743bfc41613291debeacd1ef47a1d3982df7635efa39fc727f5525659761cd",
|
|
87
|
+
"description": "Mutation-guided test pruning: a fixer proposes deleting redundant tests and strengthening weak ones, an independent reviewer accepts or rejects each proposal, and prune.py applies the accepted batch behind a cold mutation run and the ratchet. Use when the user says \"prune tests\", \"test-prune\", \"mutation test pruning\" or \"strengthen weak tests\", or when a scheduled job runs it with `--unattended`."
|
|
88
|
+
},
|
|
83
89
|
{
|
|
84
90
|
"name": "to-openspec",
|
|
85
91
|
"version": "0.1.3",
|