fable-engine 1.3.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 (104) hide show
  1. fable_compressor.py +356 -0
  2. fable_engine/__init__.py +1 -0
  3. fable_engine/actions/__init__.py +291 -0
  4. fable_engine/actions/cas.py +182 -0
  5. fable_engine/actions/deliberation.py +523 -0
  6. fable_engine/actions/fleet.py +807 -0
  7. fable_engine/actions/lifecycle.py +298 -0
  8. fable_engine/actions/scrapers.py +116 -0
  9. fable_engine/actions/system3.py +815 -0
  10. fable_engine/browser.py +824 -0
  11. fable_engine/cas.py +974 -0
  12. fable_engine/fable_session.json +510 -0
  13. fable_engine/guards.py +283 -0
  14. fable_engine/schema.py +714 -0
  15. fable_engine/scrapers/__init__.py +32 -0
  16. fable_engine/scrapers/arxiv.py +115 -0
  17. fable_engine/scrapers/base.py +386 -0
  18. fable_engine/scrapers/github.py +129 -0
  19. fable_engine/scrapers/reddit.py +154 -0
  20. fable_engine/scrapers/web.py +120 -0
  21. fable_engine/scrapers/x.py +125 -0
  22. fable_engine/scrapers/youtube.py +132 -0
  23. fable_engine/server.py +414 -0
  24. fable_engine/session.py +1819 -0
  25. fable_engine/test_server.py +1362 -0
  26. fable_engine/updater.py +541 -0
  27. fable_engine-1.3.1.dist-info/LICENSE +22 -0
  28. fable_engine-1.3.1.dist-info/METADATA +173 -0
  29. fable_engine-1.3.1.dist-info/RECORD +104 -0
  30. fable_engine-1.3.1.dist-info/WHEEL +5 -0
  31. fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
  32. fable_engine-1.3.1.dist-info/top_level.txt +6 -0
  33. fable_mode/__init__.py +3 -0
  34. fable_mode/__main__.py +4 -0
  35. fable_mode/adapters.py +1014 -0
  36. fable_mode/installer.py +553 -0
  37. fable_mode/launcher.py +437 -0
  38. fable_mode/manifest.py +142 -0
  39. fable_mode/resources.json +114 -0
  40. fable_mode/safety.py +103 -0
  41. fable_mode_entry.py +10 -0
  42. fable_v2/__init__.py +146 -0
  43. fable_v2/adapters.py +151 -0
  44. fable_v2/coder_fleet/__init__.py +100 -0
  45. fable_v2/coder_fleet/ast_tools.py +158 -0
  46. fable_v2/coder_fleet/compute.py +199 -0
  47. fable_v2/coder_fleet/design_engine.py +1316 -0
  48. fable_v2/coder_fleet/diagnostics.py +293 -0
  49. fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
  50. fable_v2/coder_fleet/mock_auditor.py +306 -0
  51. fable_v2/coder_fleet/mutation.py +216 -0
  52. fable_v2/coder_fleet/property_oracle.py +260 -0
  53. fable_v2/coder_fleet/receipt_attestor.py +122 -0
  54. fable_v2/coder_fleet/red_team_swarm.py +908 -0
  55. fable_v2/coder_fleet/test_harness.py +198 -0
  56. fable_v2/coder_fleet/vector_engine.py +1287 -0
  57. fable_v2/coder_fleet/visual.py +357 -0
  58. fable_v2/coder_fleet/workspace.py +153 -0
  59. fable_v2/cortical/__init__.py +20 -0
  60. fable_v2/cortical/plasticity_engine.py +992 -0
  61. fable_v2/execution_broker.py +811 -0
  62. fable_v2/proof_engine.py +1141 -0
  63. fable_v2/protocol.py +485 -0
  64. fable_v2/runtime.py +1010 -0
  65. fable_v2/system3/__init__.py +204 -0
  66. fable_v2/system3/causal.py +558 -0
  67. fable_v2/system3/dialectical.py +577 -0
  68. fable_v2/system3/evolution.py +503 -0
  69. fable_v2/system3/executive.py +338 -0
  70. fable_v2/system3/free_energy.py +479 -0
  71. fable_v2/system3/hyperbolic.py +555 -0
  72. fable_v2/system3/induction.py +336 -0
  73. fable_v2/system3/kripke.py +548 -0
  74. fable_v2/system3/oracle.py +745 -0
  75. fable_v2/verifiers.py +72 -0
  76. tests/__init__.py +1 -0
  77. tests/test_anti_loop_circuit_breaker.py +64 -0
  78. tests/test_auto_updater.py +407 -0
  79. tests/test_coder_fleet.py +535 -0
  80. tests/test_delegation_compiler.py +54 -0
  81. tests/test_descriptor_boundaries.py +126 -0
  82. tests/test_design_engine.py +603 -0
  83. tests/test_epistemic_evidence_validator.py +66 -0
  84. tests/test_execution_broker.py +233 -0
  85. tests/test_fable_v2.py +406 -0
  86. tests/test_fleet_transitions.py +116 -0
  87. tests/test_fsm_redteam_evolution.py +406 -0
  88. tests/test_goal_rubric_and_pipeline.py +367 -0
  89. tests/test_hebbian_plasticity.py +585 -0
  90. tests/test_packaging_runtime.py +194 -0
  91. tests/test_proof_engine.py +259 -0
  92. tests/test_red_team_swarm.py +645 -0
  93. tests/test_redteam_remediation.py +169 -0
  94. tests/test_registration_transaction.py +375 -0
  95. tests/test_requested_regressions.py +467 -0
  96. tests/test_scrapers.py +370 -0
  97. tests/test_server_actions.py +93 -0
  98. tests/test_server_frontier_actions.py +269 -0
  99. tests/test_server_protocol.py +88 -0
  100. tests/test_stealth_browser.py +970 -0
  101. tests/test_system3.py +381 -0
  102. tests/test_system3_deep_integration.py +385 -0
  103. tests/test_system3_frontier.py +436 -0
  104. tests/test_vector_engine.py +608 -0
@@ -0,0 +1,306 @@
1
+ """Mock Auditor Engine for Detecting Fake Tautological Tests and Mock Leakage.
2
+
3
+ Audits test suites for trivial assertions (assert True, 1==1), excessive mocking,
4
+ and absence of negative / error path tests.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import ast
9
+ from typing import Any
10
+
11
+
12
+ class MockAuditorEngine:
13
+ """Engine for auditing test assertions, tautologies, mock stubs, and negative branch coverage."""
14
+
15
+ def audit_assertions(self, test_code: str) -> dict[str, Any]:
16
+ """AST visitor detecting trivial assertions (assert True, assert 1 == 1, assert x is not None as lone check),
17
+
18
+ calculating assertion density and flagging tautologies.
19
+ """
20
+ try:
21
+ tree = ast.parse(test_code)
22
+ except SyntaxError:
23
+ return {
24
+ "total_assertions": 0,
25
+ "trivial_assertions_count": 0,
26
+ "tautologies": [],
27
+ "assertion_density": 0.0,
28
+ "passed_audit": False,
29
+ "error": "SyntaxError parsing test code",
30
+ }
31
+
32
+ tautologies: list[dict[str, Any]] = []
33
+ total_assertions = 0
34
+ test_functions = 0
35
+
36
+ class AssertionVisitor(ast.NodeVisitor):
37
+ def __init__(self) -> None:
38
+ nonlocal test_functions
39
+ super().__init__()
40
+
41
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
42
+ nonlocal test_functions
43
+ if node.name.startswith("test_") or node.name.endswith("_test"):
44
+ test_functions += 1
45
+
46
+ # Check for lone 'assert x is not None'
47
+ assert_stmts = [s for s in node.body if isinstance(s, ast.Assert)]
48
+ if len(node.body) == 1 and len(assert_stmts) == 1:
49
+ target_assert = assert_stmts[0]
50
+ if isinstance(target_assert.test, ast.Compare):
51
+ ops = target_assert.test.ops
52
+ comparators = target_assert.test.comparators
53
+ if any(isinstance(op, ast.IsNot) for op in ops) and any(
54
+ isinstance(c, ast.Constant) and c.value is None for c in comparators
55
+ ):
56
+ tautologies.append({
57
+ "line": target_assert.lineno,
58
+ "col": target_assert.col_offset,
59
+ "type": "lone_not_none_assertion",
60
+ "code_snippet": ast.unparse(target_assert) if hasattr(ast, "unparse") else "assert x is not None",
61
+ "reason": "Test function contains only a single 'assert x is not None' check without behavior verification",
62
+ })
63
+
64
+ self.generic_visit(node)
65
+
66
+ def visit_Assert(self, node: ast.Assert) -> None:
67
+ nonlocal total_assertions
68
+ total_assertions += 1
69
+
70
+ # Check assert True / assert 1
71
+ if isinstance(node.test, ast.Constant):
72
+ if node.test.value is True or node.test.value == 1:
73
+ tautologies.append({
74
+ "line": node.lineno,
75
+ "col": node.col_offset,
76
+ "type": "constant_true_assertion",
77
+ "code_snippet": ast.unparse(node) if hasattr(ast, "unparse") else "assert True",
78
+ "reason": f"Tautological assertion with constant truth value '{node.test.value}'",
79
+ })
80
+
81
+ # Check assert 1 == 1, assert "a" == "a"
82
+ elif isinstance(node.test, ast.Compare):
83
+ cmp = node.test
84
+ if isinstance(cmp.left, ast.Constant) and len(cmp.comparators) == 1 and isinstance(cmp.comparators[0], ast.Constant):
85
+ if cmp.left.value == cmp.comparators[0].value and any(isinstance(op, (ast.Eq, ast.Is)) for op in cmp.ops):
86
+ tautologies.append({
87
+ "line": node.lineno,
88
+ "col": node.col_offset,
89
+ "type": "literal_equality_tautology",
90
+ "code_snippet": ast.unparse(node) if hasattr(ast, "unparse") else "assert a == a",
91
+ "reason": f"Trivial assertion comparing literal {cmp.left.value!r} to itself",
92
+ })
93
+
94
+ self.generic_visit(node)
95
+
96
+ def visit_Call(self, node: ast.Call) -> None:
97
+ nonlocal total_assertions
98
+ func_name = ""
99
+ if isinstance(node.func, ast.Attribute):
100
+ func_name = node.func.attr
101
+ elif isinstance(node.func, ast.Name):
102
+ func_name = node.func.id
103
+
104
+ if func_name.startswith("assert"):
105
+ total_assertions += 1
106
+
107
+ if func_name in ("assertTrue", "assert_true") and node.args:
108
+ first_arg = node.args[0]
109
+ if isinstance(first_arg, ast.Constant) and first_arg.value is True:
110
+ tautologies.append({
111
+ "line": node.lineno,
112
+ "col": node.col_offset,
113
+ "type": "assertTrue_true_call",
114
+ "code_snippet": ast.unparse(node) if hasattr(ast, "unparse") else "self.assertTrue(True)",
115
+ "reason": "Trivial assertion call assertTrue(True)",
116
+ })
117
+
118
+ elif func_name in ("assertEqual", "assert_equal") and len(node.args) >= 2:
119
+ a1, a2 = node.args[0], node.args[1]
120
+ if isinstance(a1, ast.Constant) and isinstance(a2, ast.Constant) and a1.value == a2.value:
121
+ tautologies.append({
122
+ "line": node.lineno,
123
+ "col": node.col_offset,
124
+ "type": "assertEqual_identical_call",
125
+ "code_snippet": ast.unparse(node) if hasattr(ast, "unparse") else "self.assertEqual(a, a)",
126
+ "reason": f"Trivial assertion call assertEqual with identical constant {a1.value!r}",
127
+ })
128
+
129
+ self.generic_visit(node)
130
+
131
+ visitor = AssertionVisitor()
132
+ visitor.visit(tree)
133
+
134
+ trivial_count = len(tautologies)
135
+ effective_tests = max(1, test_functions)
136
+ density = round(total_assertions / effective_tests, 2)
137
+ passed = (trivial_count == 0) and (total_assertions > 0)
138
+
139
+ return {
140
+ "total_assertions": total_assertions,
141
+ "trivial_assertions_count": trivial_count,
142
+ "tautologies": tautologies,
143
+ "assertion_density": density,
144
+ "passed_audit": passed,
145
+ }
146
+
147
+ def detect_mock_leakage(self, test_code: str) -> dict[str, Any]:
148
+ """Check for excessive mock usage where core logic is stubbed out."""
149
+ try:
150
+ tree = ast.parse(test_code)
151
+ except SyntaxError:
152
+ return {
153
+ "mock_count": 0,
154
+ "mock_ratio": 0.0,
155
+ "flagged_mocks": [],
156
+ "has_excessive_mocking": False,
157
+ }
158
+
159
+ mock_identifiers = {"Mock", "MagicMock", "patch", "mocker", "monkeypatch", "PropertyMock"}
160
+ mock_count = 0
161
+ total_calls = 0
162
+ flagged: list[dict[str, Any]] = []
163
+
164
+ class MockVisitor(ast.NodeVisitor):
165
+ def visit_Call(self, node: ast.Call) -> None:
166
+ nonlocal mock_count, total_calls
167
+ total_calls += 1
168
+ name = ""
169
+ if isinstance(node.func, ast.Name):
170
+ name = node.func.id
171
+ elif isinstance(node.func, ast.Attribute):
172
+ name = node.func.attr
173
+
174
+ if name in mock_identifiers or "mock" in name.lower():
175
+ mock_count += 1
176
+ flagged.append({
177
+ "line": node.lineno,
178
+ "name": name,
179
+ "reason": f"Invocation of mock factory or stub '{name}'",
180
+ })
181
+ self.generic_visit(node)
182
+
183
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
184
+ nonlocal mock_count
185
+ for dec in node.decorator_list:
186
+ dec_name = ""
187
+ if isinstance(dec, ast.Name):
188
+ dec_name = dec.id
189
+ elif isinstance(dec, ast.Attribute):
190
+ dec_name = dec.attr
191
+ elif isinstance(dec, ast.Call):
192
+ if isinstance(dec.func, ast.Name):
193
+ dec_name = dec.func.id
194
+ elif isinstance(dec.func, ast.Attribute):
195
+ dec_name = dec.func.attr
196
+
197
+ if "patch" in dec_name.lower():
198
+ mock_count += 1
199
+ flagged.append({
200
+ "line": dec.lineno if hasattr(dec, "lineno") else node.lineno,
201
+ "name": dec_name,
202
+ "reason": f"Patch decorator '@{dec_name}' stubbing dependencies",
203
+ })
204
+ self.generic_visit(node)
205
+
206
+ MockVisitor().visit(tree)
207
+ ratio = round(mock_count / max(1, total_calls), 2)
208
+ excessive = ratio > 0.5 or (mock_count >= 5 and ratio > 0.3)
209
+
210
+ return {
211
+ "mock_count": mock_count,
212
+ "mock_ratio": ratio,
213
+ "flagged_mocks": flagged,
214
+ "has_excessive_mocking": excessive,
215
+ }
216
+
217
+ def enforce_negative_paths(self, test_code: str) -> dict[str, Any]:
218
+ """Verify presence of negative test cases (assertRaises, except, pytest.raises)."""
219
+ try:
220
+ tree = ast.parse(test_code)
221
+ except SyntaxError:
222
+ return {
223
+ "has_negative_tests": False,
224
+ "negative_test_count": 0,
225
+ "total_test_cases": 0,
226
+ "negative_ratio": 0.0,
227
+ "details": [],
228
+ }
229
+
230
+ negative_details: list[dict[str, Any]] = []
231
+ total_test_cases = 0
232
+
233
+ class NegativePathVisitor(ast.NodeVisitor):
234
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
235
+ nonlocal total_test_cases
236
+ if node.name.startswith("test_") or node.name.endswith("_test"):
237
+ total_test_cases += 1
238
+ # Check if function name indicates negative test
239
+ lower_name = node.name.lower()
240
+ if any(w in lower_name for w in ("error", "invalid", "fail", "exception", "reject", "negative")):
241
+ negative_details.append({
242
+ "line": node.lineno,
243
+ "function": node.name,
244
+ "type": "naming_convention",
245
+ "description": f"Negative test detected from name '{node.name}'",
246
+ })
247
+ self.generic_visit(node)
248
+
249
+ def visit_With(self, node: ast.With) -> None:
250
+ for item in node.items:
251
+ expr = item.context_expr
252
+ expr_name = ""
253
+ if isinstance(expr, ast.Call):
254
+ if isinstance(expr.func, ast.Attribute):
255
+ expr_name = expr.func.attr
256
+ elif isinstance(expr.func, ast.Name):
257
+ expr_name = expr.func.id
258
+
259
+ if expr_name in ("assertRaises", "raises", "assert_raises"):
260
+ negative_details.append({
261
+ "line": node.lineno,
262
+ "function": None,
263
+ "type": "with_assertRaises",
264
+ "description": f"Context manager '{expr_name}' testing error expectation",
265
+ })
266
+ self.generic_visit(node)
267
+
268
+ def visit_Call(self, node: ast.Call) -> None:
269
+ func_name = ""
270
+ if isinstance(node.func, ast.Attribute):
271
+ func_name = node.func.attr
272
+ elif isinstance(node.func, ast.Name):
273
+ func_name = node.func.id
274
+
275
+ if func_name in ("assertRaises", "assert_raises", "assertFalse"):
276
+ negative_details.append({
277
+ "line": node.lineno,
278
+ "function": None,
279
+ "type": "negative_assert_call",
280
+ "description": f"Assertion call '{func_name}' testing negative/failure path",
281
+ })
282
+ self.generic_visit(node)
283
+
284
+ def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
285
+ # Presence of an except handler inside a test
286
+ negative_details.append({
287
+ "line": node.lineno,
288
+ "function": None,
289
+ "type": "try_except_block",
290
+ "description": "Explicit exception handling checking error recovery",
291
+ })
292
+ self.generic_visit(node)
293
+
294
+ NegativePathVisitor().visit(tree)
295
+
296
+ neg_count = len(negative_details)
297
+ effective_total = max(1, total_test_cases)
298
+ ratio = round(neg_count / effective_total, 2)
299
+
300
+ return {
301
+ "has_negative_tests": neg_count > 0,
302
+ "negative_test_count": neg_count,
303
+ "total_test_cases": total_test_cases,
304
+ "negative_ratio": ratio,
305
+ "details": negative_details,
306
+ }
@@ -0,0 +1,216 @@
1
+ """Mutation Verifier Engine for AST-based Mutant Injection and Test Rigor Auditing.
2
+
3
+ Generates semantic AST mutations and verifies whether test suites actually catch mutants
4
+ or whether tests are fake / tautological.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import ast
9
+ import copy
10
+ from typing import Any
11
+
12
+ from .test_harness import TestHarnessEngine
13
+
14
+
15
+ class MutationVerifierEngine:
16
+ """Engine for generating AST-based code mutants and assessing test suite kill rates."""
17
+
18
+ def __init__(self, test_harness: TestHarnessEngine | None = None) -> None:
19
+ self._harness = test_harness or TestHarnessEngine()
20
+
21
+ def inject_mutants(self, source_code: str) -> list[dict[str, Any]]:
22
+ """Generate 3-5 AST-based mutants:
23
+
24
+ - inverts >/<, ==/!=
25
+ - flips True/False
26
+ - replaces numeric constants
27
+ - removes return statements / alters return values
28
+ - inverts arithmetic operators (+/-)
29
+ """
30
+ try:
31
+ tree = ast.parse(source_code)
32
+ except SyntaxError:
33
+ return []
34
+
35
+ # Find mutation candidates
36
+ candidates: list[tuple[ast.AST, str, Any]] = []
37
+
38
+ for node in ast.walk(tree):
39
+ # 1. Comparison operations
40
+ if isinstance(node, ast.Compare):
41
+ for op in node.ops:
42
+ if isinstance(op, ast.Gt):
43
+ candidates.append((node, "invert_comparison_gt_to_lt", (op, ast.Lt())))
44
+ elif isinstance(op, ast.Lt):
45
+ candidates.append((node, "invert_comparison_lt_to_gt", (op, ast.Gt())))
46
+ elif isinstance(op, ast.GtE):
47
+ candidates.append((node, "invert_comparison_gte_to_lte", (op, ast.LtE())))
48
+ elif isinstance(op, ast.LtE):
49
+ candidates.append((node, "invert_comparison_lte_to_gte", (op, ast.GtE())))
50
+ elif isinstance(op, ast.Eq):
51
+ candidates.append((node, "invert_comparison_eq_to_neq", (op, ast.NotEq())))
52
+ elif isinstance(op, ast.NotEq):
53
+ candidates.append((node, "invert_comparison_neq_to_eq", (op, ast.Eq())))
54
+
55
+ # 2. Boolean and numeric constants
56
+ elif isinstance(node, ast.Constant):
57
+ if node.value is True:
58
+ candidates.append((node, "flip_boolean_true_to_false", False))
59
+ elif node.value is False:
60
+ candidates.append((node, "flip_boolean_false_to_true", True))
61
+ elif isinstance(node.value, (int, float)) and not isinstance(node.value, bool):
62
+ new_val = 1 if node.value == 0 else (node.value + 1)
63
+ candidates.append((node, "mutate_numeric_constant", new_val))
64
+
65
+ # 3. Return statements
66
+ elif isinstance(node, ast.Return):
67
+ if node.value is not None:
68
+ candidates.append((node, "remove_return_value", None))
69
+
70
+ # 4. Arithmetic operators
71
+ elif isinstance(node, ast.BinOp):
72
+ if isinstance(node.op, ast.Add):
73
+ candidates.append((node, "invert_arithmetic_add_to_sub", ast.Sub()))
74
+ elif isinstance(node.op, ast.Sub):
75
+ candidates.append((node, "invert_arithmetic_sub_to_add", ast.Add()))
76
+
77
+ mutants: list[dict[str, Any]] = []
78
+ seen_codes: set[str] = {source_code.strip()}
79
+
80
+ for target_node, mut_type, mut_arg in candidates:
81
+ if len(mutants) >= 5:
82
+ break
83
+
84
+ mutated_tree = copy.deepcopy(tree)
85
+
86
+ class Mutator(ast.NodeTransformer):
87
+ def __init__(self, target_line: int, target_col: int) -> None:
88
+ self.target_line = target_line
89
+ self.target_col = target_col
90
+ self.mutated = False
91
+
92
+ def visit_Compare(self, n: ast.Compare) -> ast.AST:
93
+ self.generic_visit(n)
94
+ if not self.mutated and getattr(n, "lineno", -1) == self.target_line and getattr(n, "col_offset", -1) == self.target_col:
95
+ if mut_type.startswith("invert_comparison"):
96
+ old_op, new_op = mut_arg
97
+ new_ops = []
98
+ for op in n.ops:
99
+ if type(op) is type(old_op) and not self.mutated:
100
+ new_ops.append(new_op)
101
+ self.mutated = True
102
+ else:
103
+ new_ops.append(op)
104
+ n.ops = new_ops
105
+ return n
106
+
107
+ def visit_Constant(self, n: ast.Constant) -> ast.AST:
108
+ if not self.mutated and getattr(n, "lineno", -1) == self.target_line and getattr(n, "col_offset", -1) == self.target_col:
109
+ if mut_type in ("flip_boolean_true_to_false", "flip_boolean_false_to_true", "mutate_numeric_constant"):
110
+ n.value = mut_arg
111
+ self.mutated = True
112
+ return n
113
+
114
+ def visit_Return(self, n: ast.Return) -> ast.AST:
115
+ self.generic_visit(n)
116
+ if not self.mutated and getattr(n, "lineno", -1) == self.target_line and getattr(n, "col_offset", -1) == self.target_col:
117
+ if mut_type == "remove_return_value":
118
+ n.value = ast.Constant(value=None)
119
+ self.mutated = True
120
+ return n
121
+
122
+ def visit_BinOp(self, n: ast.BinOp) -> ast.AST:
123
+ self.generic_visit(n)
124
+ if not self.mutated and getattr(n, "lineno", -1) == self.target_line and getattr(n, "col_offset", -1) == self.target_col:
125
+ if mut_type.startswith("invert_arithmetic"):
126
+ n.op = mut_arg
127
+ self.mutated = True
128
+ return n
129
+
130
+ line = getattr(target_node, "lineno", 1)
131
+ col = getattr(target_node, "col_offset", 0)
132
+ mutator = Mutator(line, col)
133
+ mutated_tree = mutator.visit(mutated_tree)
134
+ ast.fix_missing_locations(mutated_tree)
135
+
136
+ try:
137
+ mutated_code = ast.unparse(mutated_tree)
138
+ except Exception:
139
+ continue
140
+
141
+ if mutated_code.strip() not in seen_codes:
142
+ seen_codes.add(mutated_code.strip())
143
+ mutants.append({
144
+ "mutant_id": f"mutant_{len(mutants) + 1}",
145
+ "mutated_code": mutated_code,
146
+ "mutation_type": mut_type,
147
+ "line": line,
148
+ })
149
+
150
+ return mutants
151
+
152
+ def audit_test_strength(self, source_code: str, test_code: str, timeout_sec: float = 3.0) -> dict[str, Any]:
153
+ """Run test_code against each mutant using run_scratch_test.
154
+
155
+ If tests still pass against mutant, mutant survived (test is weak/fake!).
156
+ If tests fail, mutant killed.
157
+ Returns {mutants_total, mutants_killed, mutants_survived, mutation_score, is_thorough}.
158
+ """
159
+ baseline_code = f"{source_code}\n\n{test_code}"
160
+ baseline_run = self._harness.run_scratch_test(baseline_code, timeout_sec=timeout_sec)
161
+
162
+ if not baseline_run["success"]:
163
+ return {
164
+ "mutants_total": 0,
165
+ "mutants_killed": 0,
166
+ "mutants_survived": 0,
167
+ "mutation_score": 0.0,
168
+ "is_thorough": False,
169
+ "error": f"Baseline test failed on unmutated code: {baseline_run.get('stderr')}",
170
+ "survived_mutant_details": [],
171
+ }
172
+
173
+ mutants = self.inject_mutants(source_code)
174
+ if not mutants:
175
+ return {
176
+ "mutants_total": 0,
177
+ "mutants_killed": 0,
178
+ "mutants_survived": 0,
179
+ "mutation_score": 1.0,
180
+ "is_thorough": True,
181
+ "survived_mutant_details": [],
182
+ }
183
+
184
+ killed: list[dict[str, Any]] = []
185
+ survived: list[dict[str, Any]] = []
186
+
187
+ for m in mutants:
188
+ mutant_test = f"{m['mutated_code']}\n\n{test_code}"
189
+ run_res = self._harness.run_scratch_test(mutant_test, timeout_sec=timeout_sec)
190
+
191
+ if not run_res["success"]:
192
+ # Mutant killed! The test failed as expected on mutated code.
193
+ killed.append(m)
194
+ else:
195
+ # Mutant survived! The test erroneously passed despite bug injected.
196
+ survived.append({
197
+ "mutant_id": m["mutant_id"],
198
+ "mutation_type": m["mutation_type"],
199
+ "line": m["line"],
200
+ "reason": "Test suite passed despite mutation (weak or missing assertion)",
201
+ })
202
+
203
+ total = len(mutants)
204
+ killed_count = len(killed)
205
+ survived_count = len(survived)
206
+ score = round(killed_count / total, 4) if total > 0 else 1.0
207
+ is_thorough = score >= 0.75
208
+
209
+ return {
210
+ "mutants_total": total,
211
+ "mutants_killed": killed_count,
212
+ "mutants_survived": survived_count,
213
+ "mutation_score": score,
214
+ "is_thorough": is_thorough,
215
+ "survived_mutant_details": survived,
216
+ }