jitcatch 0.2.0__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.
jitcatch/models.py ADDED
@@ -0,0 +1,150 @@
1
+ """Core data structures shared across the pipeline.
2
+
3
+ Terminology follows Becker et al., "Just-in-Time Catching Test Generation at Meta" (FSE '26):
4
+ * weak catch -- a generated test that passes on the parent revision and fails on the change
5
+ * strong catch -- a weak catch whose failure reveals a real bug (true positive)
6
+ * strictly weak catch -- a weak catch whose failure is a false positive
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field, asdict
11
+ from enum import Enum
12
+ from typing import Any
13
+
14
+
15
+ class Outcome(str, Enum):
16
+ PASS = "pass"
17
+ FAIL = "fail" # test ran, at least one assertion/runtime failure
18
+ ERROR = "error" # test could not build / collect / compile
19
+ INFRA = "infra" # runner itself broken, timeout, missing tool
20
+ NO_TESTS = "no_tests" # runner found no tests in the generated file
21
+
22
+
23
+ @dataclass
24
+ class TestRun:
25
+ __test__ = False # not a pytest test class
26
+ outcome: Outcome
27
+ exit_code: int
28
+ output: str
29
+ duration_s: float = 0.0
30
+ command: list[str] = field(default_factory=list)
31
+
32
+ @property
33
+ def passed(self) -> bool:
34
+ return self.outcome == Outcome.PASS
35
+
36
+
37
+ @dataclass
38
+ class ChangedFile:
39
+ path: str # repo-relative path at head
40
+ old_path: str # repo-relative path at base (same as path unless renamed)
41
+ status: str # M, A, D, R
42
+ patch: str # unified diff for this file
43
+ added_lines: int = 0
44
+ removed_lines: int = 0
45
+
46
+
47
+ @dataclass
48
+ class ChangeContext:
49
+ base_sha: str
50
+ head_sha: str
51
+ title: str
52
+ description: str
53
+ files: list[ChangedFile]
54
+ full_patch: str
55
+
56
+
57
+ @dataclass
58
+ class Risk:
59
+ id: str
60
+ description: str
61
+ file: str
62
+
63
+
64
+ @dataclass
65
+ class Intent:
66
+ summary: str
67
+ change_kind: str # refactor | dead_code_removal | additive | bugfix | feature | other
68
+ risks: list[Risk]
69
+
70
+
71
+ @dataclass
72
+ class Mutant:
73
+ id: str
74
+ risk: Risk
75
+ file: str
76
+ original: str
77
+ mutated: str
78
+ survived_existing_tests: bool | None = None
79
+ note: str = ""
80
+
81
+
82
+ @dataclass
83
+ class GeneratedTest:
84
+ id: str
85
+ workflow: str # intent_aware | dodgy_diff
86
+ target_file: str # source file under test (repo-relative)
87
+ test_path: str # where the test is written (repo-relative)
88
+ code: str
89
+ risk: Risk | None = None
90
+ mutant_id: str | None = None
91
+ base_run: TestRun | None = None
92
+ base_rerun: TestRun | None = None
93
+ mutant_run: TestRun | None = None
94
+ head_run: TestRun | None = None
95
+ head_rerun: TestRun | None = None
96
+ attempts: int = 0
97
+ discarded_reason: str = ""
98
+
99
+ @property
100
+ def is_weak_catch(self) -> bool:
101
+ return (
102
+ self.base_run is not None and self.base_run.passed
103
+ and self.head_run is not None and not self.head_run.passed
104
+ and self.head_run.outcome != Outcome.INFRA
105
+ )
106
+
107
+ @property
108
+ def is_hardening(self) -> bool:
109
+ return (
110
+ self.base_run is not None and self.base_run.passed
111
+ and self.head_run is not None and self.head_run.passed
112
+ )
113
+
114
+ @property
115
+ def is_flaky(self) -> bool:
116
+ for a, b in ((self.base_run, self.base_rerun), (self.head_run, self.head_rerun)):
117
+ if a is not None and b is not None and a.outcome != b.outcome:
118
+ return True
119
+ return False
120
+
121
+
122
+ @dataclass
123
+ class AssessorScore:
124
+ name: str
125
+ score: float | None # -1.0 (false positive) .. 1.0 (true positive); None = no information
126
+ details: dict[str, Any] = field(default_factory=dict)
127
+
128
+
129
+ @dataclass
130
+ class Assessment:
131
+ test_id: str
132
+ scores: list[AssessorScore]
133
+ combined: float
134
+ discarded: bool
135
+ discard_reason: str = ""
136
+ behavior_change: str = "" # plain-language "used to X, now Y"
137
+ question: str = "" # "is this expected?" message for the author
138
+ rationales: list[dict[str, str]] = field(default_factory=list)
139
+
140
+
141
+ def to_jsonable(obj: Any) -> Any:
142
+ if isinstance(obj, Enum):
143
+ return obj.value
144
+ if hasattr(obj, "__dataclass_fields__"):
145
+ return {k: to_jsonable(v) for k, v in asdict(obj).items()}
146
+ if isinstance(obj, dict):
147
+ return {k: to_jsonable(v) for k, v in obj.items()}
148
+ if isinstance(obj, (list, tuple)):
149
+ return [to_jsonable(v) for v in obj]
150
+ return obj
jitcatch/prompts.py ADDED
@@ -0,0 +1,191 @@
1
+ """Prompt templates for every LLM step in the pipeline."""
2
+ from __future__ import annotations
3
+
4
+ GEN_SYSTEM = (
5
+ "You are a meticulous senior software engineer specialising in finding regressions in code changes "
6
+ "before they are merged. You follow output-format instructions exactly."
7
+ )
8
+
9
+ INTENT_USER = """\
10
+ A developer submitted the code change below. Infer what the change is trying to do (its intent), classify it,
11
+ and list the concrete *risks*: specific ways an attempt to implement this intent could introduce a bug that
12
+ changes observable behaviour of the code that existed BEFORE the change and that a unit test could detect.
13
+ Prioritise risks with severe consequences (data loss, wrong results, security/permission mistakes, crashes).
14
+ Do not list style issues or risks that only concern brand-new code paths.
15
+
16
+ ## Title
17
+ {title}
18
+
19
+ ## Description
20
+ {description}
21
+
22
+ ## Diff (parent -> change)
23
+ ```diff
24
+ {patch}
25
+ ```
26
+
27
+ ## Changed files after the change
28
+ {files}
29
+
30
+ Respond with ONLY a JSON object:
31
+ {{
32
+ "intent": "<one or two sentences>",
33
+ "change_kind": "refactor" | "dead_code_removal" | "additive" | "bugfix" | "feature" | "other",
34
+ "risks": [{{"file": "<repo-relative path of a changed file listed above>", "description": "<specific risk>"}}]
35
+ }}
36
+ List at most {max_risks} risks per file, most severe first.
37
+ """
38
+
39
+ MUTANT_USER = """\
40
+ We simulate a bug that could be introduced while implementing a change. Below is the PARENT version (before the
41
+ change) of `{path}`. Create a *mutant*: a minimal, realistic modification of this parent file that exhibits the
42
+ risk described. The mutant must still compile/parse, must change observable behaviour, and must look like a
43
+ plausible developer mistake (not a deliberate sabotage like `return None` everywhere).
44
+
45
+ ## Intended change
46
+ {intent}
47
+
48
+ ## Risk to simulate
49
+ {risk}
50
+
51
+ ## Parent version of `{path}`
52
+ ```{lang}
53
+ {source}
54
+ ```
55
+
56
+ Respond with ONLY a JSON object:
57
+ {{
58
+ "explanation": "<what the mutant changes and why it reflects the risk>",
59
+ "edits": [{{"search": "<exact, unique snippet copied verbatim from the parent file>", "replace": "<new snippet>"}}]
60
+ }}
61
+ Each "search" string must appear exactly once in the parent file. Keep edits small.
62
+ """
63
+
64
+ TEST_FOR_MUTANT_USER = """\
65
+ Write a unit test file for `{path}` that PASSES on the parent version shown below and FAILS on the mutant
66
+ (the mutant diff is shown). The test must assert the parent's correct behaviour on the code path the mutant
67
+ affects, so that the risk "{risk}" would be caught. Prefer simple, deterministic inputs and precise assertions
68
+ on returned values or observable state. Test only the public behaviour.
69
+
70
+ ## Test-file constraints
71
+ {instructions}
72
+ The file will be written to `{test_path}`.
73
+
74
+ ## Parent version of `{path}`
75
+ ```{lang}
76
+ {source}
77
+ ```
78
+
79
+ ## Mutant diff (parent -> mutant)
80
+ ```diff
81
+ {mutant_diff}
82
+ ```
83
+ {example}
84
+ Respond with ONLY the complete test file in a single ```{lang} code block.
85
+ """
86
+
87
+ TEST_DODGY_USER = """\
88
+ Treat the change below as if it were a mutant of its parent. Write a unit test file for `{path}` that PASSES on
89
+ the parent version (shown in full) and would FAIL if the code behaved the way the changed version behaves.
90
+ Focus on the behaviour of the functions/branches touched by the diff. Assert precise parent-version outputs for
91
+ concrete inputs. Test only the public behaviour; keep it deterministic.
92
+
93
+ ## Test-file constraints
94
+ {instructions}
95
+ The file will be written to `{test_path}`.
96
+
97
+ ## Parent version of `{path}`
98
+ ```{lang}
99
+ {source}
100
+ ```
101
+
102
+ ## Diff (parent -> change)
103
+ ```diff
104
+ {patch}
105
+ ```
106
+ {example}{variation}
107
+ Respond with ONLY the complete test file in a single ```{lang} code block.
108
+ """
109
+
110
+ TEST_FIX_USER = """\
111
+ The test file below was supposed to PASS on the parent version of `{path}` but it did not.
112
+ Fix the test so that it passes on the parent version while still asserting meaningful, precise behaviour on the
113
+ same code path (do not delete the assertions that matter or make the test trivial). If an assertion encodes a
114
+ wrong expectation, correct the expected value to the parent's real behaviour.
115
+
116
+ ## Test-file constraints
117
+ {instructions}
118
+
119
+ ## Parent version of `{path}`
120
+ ```{lang}
121
+ {source}
122
+ ```
123
+
124
+ ## Current test file (`{test_path}`)
125
+ ```{lang}
126
+ {test_code}
127
+ ```
128
+
129
+ ## Output when run on the parent version (outcome: {outcome})
130
+ ```
131
+ {output}
132
+ ```
133
+
134
+ Respond with ONLY the complete corrected test file in a single ```{lang} code block.
135
+ """
136
+
137
+ EXAMPLE_BLOCK = """
138
+ ## Existing test in this repository (follow its imports, setup and style)
139
+ `{path}`:
140
+ ```{lang}
141
+ {code}
142
+ ```
143
+ """
144
+
145
+ JUDGE_SYSTEM = (
146
+ "You are an experienced code reviewer judging whether an automatically generated test failure reveals a "
147
+ "real bug introduced by a code change, or whether the behaviour change is intended / the test is wrong. "
148
+ "You are careful, concrete and sceptical of brittle tests."
149
+ )
150
+
151
+ JUDGE_USER = """\
152
+ A generated test PASSES on the parent revision and FAILS on the changed revision.
153
+ Decide how likely it is that this failure reveals an UNEXPECTED behaviour change (a bug) introduced by the change,
154
+ as opposed to an intended behaviour change or a problem with the test itself (brittle assertions on
155
+ implementation details, wrong mocks, API/signature changes, private members, ordering of unordered data, etc.).
156
+
157
+ ## Inferred intent of the change
158
+ {intent}
159
+
160
+ ## Title / description supplied by the author
161
+ {title}
162
+ {description}
163
+
164
+ ## Diff (parent -> change)
165
+ ```diff
166
+ {patch}
167
+ ```
168
+
169
+ ## Generated test (`{test_path}`)
170
+ ```{lang}
171
+ {test_code}
172
+ ```
173
+
174
+ ## Test execution on the changed revision (outcome: {outcome})
175
+ ```
176
+ {output}
177
+ ```
178
+
179
+ Respond with ONLY a JSON object:
180
+ {{
181
+ "likelihood": "High" | "Medium" | "Low",
182
+ "behavior_change": "<one sentence: what the code did before the change vs what it does now, in concrete terms>",
183
+ "question": "<a short, friendly message to the change author describing the observed behaviour change and ending with 'Is this expected?'>",
184
+ "rationale": "<2-4 sentences: why this is or is not likely an unintended bug, given the diff and intent>"
185
+ }}
186
+ """
187
+
188
+ LOGPROB_USER = JUDGE_USER.split("Respond with ONLY")[0] + (
189
+ "Question: Is this test failure most likely an UNEXPECTED bug introduced by the change?\n"
190
+ "Answer with exactly one word: Yes or No."
191
+ )
jitcatch/report.py ADDED
@@ -0,0 +1,163 @@
1
+ """JSON + Markdown reports and on-disk artifacts."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import time
6
+ from pathlib import Path, PurePosixPath
7
+
8
+ from .engine import RunResult
9
+ from .models import Assessment, GeneratedTest, to_jsonable
10
+
11
+ COMMENT_MARKER = "<!-- jitcatch-report -->"
12
+
13
+
14
+ def to_dict(r: RunResult) -> dict:
15
+ return {
16
+ "base_sha": r.ctx.base_sha,
17
+ "head_sha": r.ctx.head_sha,
18
+ "title": r.ctx.title,
19
+ "risk": to_jsonable(r.risk),
20
+ "intent": to_jsonable(r.intent),
21
+ "targets": r.targets,
22
+ "skipped": r.skipped,
23
+ "threshold": r.threshold,
24
+ "summary": summary_counts(r),
25
+ "candidates": [
26
+ {"test_id": t.id, "target_file": t.target_file, "score": a.combined, "question": a.question,
27
+ "behavior_change": a.behavior_change}
28
+ for t, a in r.candidates()
29
+ ],
30
+ "mutants": [to_jsonable(_mutant_summary(m)) for m in r.mutants],
31
+ "tests": [to_jsonable(t) for t in r.tests],
32
+ "assessments": {k: to_jsonable(v) for k, v in r.assessments.items()},
33
+ "errors": r.errors,
34
+ "llm_calls": r.llm_calls,
35
+ "duration_s": round((r.finished_at or time.time()) - r.started_at, 1),
36
+ }
37
+
38
+
39
+ def _mutant_summary(m):
40
+ return {"id": m.id, "risk": m.risk, "file": m.file, "survived_existing_tests": m.survived_existing_tests,
41
+ "note": m.note, "changed": m.original != m.mutated}
42
+
43
+
44
+ def summary_counts(r: RunResult) -> dict:
45
+ tests = r.tests
46
+ return {
47
+ "generated_tests": len(tests),
48
+ "passed_on_parent": sum(1 for t in tests if t.base_run is not None and t.base_run.passed and not t.discarded_reason),
49
+ "weak_catches": len(r.weak_catches),
50
+ "discarded_by_rules": sum(1 for a in r.assessments.values() if a.discarded),
51
+ "reported": len(r.candidates()),
52
+ "hardening_tests": len(r.hardening),
53
+ "mutants_used": sum(1 for m in r.mutants if not m.note.startswith("invalid") and m.survived_existing_tests is not False),
54
+ }
55
+
56
+
57
+ def write_artifacts(r: RunResult, out_root: Path) -> Path:
58
+ run_dir = out_root / "runs" / f"{time.strftime('%Y%m%d-%H%M%S')}-{r.ctx.head_sha[:8]}"
59
+ run_dir.mkdir(parents=True, exist_ok=True)
60
+ (run_dir / "report.json").write_text(json.dumps(to_dict(r), indent=2), encoding="utf-8")
61
+ (run_dir / "report.md").write_text(markdown(r), encoding="utf-8")
62
+ for t in r.weak_catches:
63
+ _save_test(run_dir / "weak_catches", t)
64
+ for t in r.hardening:
65
+ _save_test(run_dir / "hardening", t)
66
+ return run_dir
67
+
68
+
69
+ def _save_test(d: Path, t: GeneratedTest) -> None:
70
+ dest = d / t.test_path
71
+ dest.parent.mkdir(parents=True, exist_ok=True)
72
+ dest.write_text(t.code, encoding="utf-8")
73
+
74
+
75
+ def _fmt(score) -> str:
76
+ return "n/a" if score is None else f"{score:+.2f}"
77
+
78
+
79
+ def _lang_for(path: str) -> str:
80
+ ext = PurePosixPath(path).suffix
81
+ return {".py": "python", ".ts": "ts", ".tsx": "tsx", ".js": "js", ".jsx": "jsx", ".mjs": "js",
82
+ ".java": "java", ".go": "go"}.get(ext, "")
83
+
84
+
85
+ def _candidate_md(i: int, t: GeneratedTest, a: Assessment) -> str:
86
+ lines = [f"#### {i}. `{t.target_file}` — confidence {a.combined:+.2f}"]
87
+ if a.question:
88
+ lines.append(f"> {a.question}")
89
+ elif a.behavior_change:
90
+ lines.append(f"> {a.behavior_change} Is this expected?")
91
+ if t.risk:
92
+ lines.append(f"\n_Risk probed:_ {t.risk.description}")
93
+ lines.append("\n<details><summary>Evidence</summary>\n")
94
+ for s in a.scores:
95
+ extra = ""
96
+ if s.name == "rules":
97
+ fps = [h["rule"] for h in s.details.get("false_positive_patterns", [])]
98
+ tps = [h["rule"] for h in s.details.get("true_positive_patterns", [])]
99
+ extra = f" — TP patterns: {', '.join(tps) or 'none'}; FP patterns: {', '.join(fps) or 'none'}"
100
+ elif s.name == "llm_logprob" and s.score is not None:
101
+ extra = f" — P(yes)={s.details.get('p_yes')}, P(no)={s.details.get('p_no')}"
102
+ lines.append(f"- **{s.name}**: {_fmt(s.score)}{extra}")
103
+ for rat in a.rationales:
104
+ if rat.get("error"):
105
+ lines.append(f"- _{rat.get('judge')}_: error — {rat['error'][:200]}")
106
+ else:
107
+ lines.append(f"- _{rat.get('judge')}_ ({rat.get('likelihood')}): {rat.get('rationale', '')}")
108
+ lines.append(f"\nWorkflow: `{t.workflow}` · test id `{t.id}` · passes on parent, "
109
+ f"`{t.head_run.outcome.value if t.head_run else '?'}` on this change"
110
+ + (f" · {'kills' if t.mutant_run and not t.mutant_run.passed else 'does not kill'} its mutant"
111
+ if t.mutant_id else ""))
112
+ out = (t.head_run.output if t.head_run else "").strip()
113
+ if len(out) > 3000:
114
+ out = "…\n" + out[-3000:]
115
+ lines.append(f"\n**Failure on this change**\n```\n{out}\n```")
116
+ lines.append(f"\n**Generated test** (`{t.test_path}`)\n```{_lang_for(t.test_path)}\n{t.code.rstrip()}\n```")
117
+ lines.append("\n</details>\n")
118
+ return "\n".join(lines)
119
+
120
+
121
+ def markdown(r: RunResult, for_comment: bool = False) -> str:
122
+ c = summary_counts(r)
123
+ cands = r.candidates()
124
+ md = [COMMENT_MARKER if for_comment else "", "## JiTCatch: just-in-time catching tests", ""]
125
+ md.append(f"Change `{r.ctx.base_sha[:8]}..{r.ctx.head_sha[:8]}`"
126
+ + (f" · risk score {r.risk.score:.2f}" if r.risk else ""))
127
+ if r.intent:
128
+ md.append(f"\n**Inferred intent** ({r.intent.change_kind}): {r.intent.summary}")
129
+ md.append("")
130
+ if cands:
131
+ md.append(f"### {len(cands)} behaviour change(s) worth a quick check")
132
+ md.append("Each item is a generated test that passes on the parent revision but fails with this change. "
133
+ "If the change is intended, just dismiss it.\n")
134
+ for i, (t, a) in enumerate(cands, 1):
135
+ md.append(_candidate_md(i, t, a))
136
+ elif r.tests:
137
+ md.append("No likely regressions found by the generated tests.")
138
+ else:
139
+ md.append("No tests were generated for this change.")
140
+ md.append("\n| generated | pass on parent | weak catches | auto-discarded (rules) | reported | hardening |")
141
+ md.append("|---:|---:|---:|---:|---:|---:|")
142
+ md.append(f"| {c['generated_tests']} | {c['passed_on_parent']} | {c['weak_catches']} | "
143
+ f"{c['discarded_by_rules']} | {c['reported']} | {c['hardening_tests']} |")
144
+ low = [(a, next(t for t in r.tests if t.id == a.test_id)) for a in r.assessments.values()
145
+ if a.discarded or a.combined < r.threshold]
146
+ if low:
147
+ md.append("\n<details><summary>Weak catches judged unlikely to be bugs "
148
+ f"({len(low)})</summary>\n")
149
+ for a, t in sorted(low, key=lambda x: -x[0].combined):
150
+ why = a.discard_reason or f"score {a.combined:+.2f} < threshold {r.threshold:+.2f}"
151
+ md.append(f"- `{t.target_file}` test `{t.id}` ({t.workflow}): {why}")
152
+ md.append("\n</details>")
153
+ if r.skipped:
154
+ md.append("\n<details><summary>Skipped files</summary>\n")
155
+ for s in r.skipped:
156
+ md.append(f"- `{s['path']}`: {s['reason']}")
157
+ md.append("\n</details>")
158
+ if r.errors:
159
+ md.append("\n<details><summary>Errors</summary>\n")
160
+ for e in r.errors:
161
+ md.append(f"- {e[:300]}")
162
+ md.append("\n</details>")
163
+ return "\n".join(x for x in md if x is not None).strip() + "\n"
jitcatch/targeting.py ADDED
@@ -0,0 +1,44 @@
1
+ """Change-risk targeting.
2
+
3
+ The paper targets diffs with a learned Diff Risk Score model trained on historical changes. Without
4
+ that history, this module uses a transparent heuristic (size, sensitive paths, untested change,
5
+ spread, deletion ratio). Set `run.min_risk_score` to skip low-risk changes.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import re
11
+ from dataclasses import dataclass, field
12
+
13
+ from .models import ChangeContext
14
+
15
+
16
+ @dataclass
17
+ class RiskScore:
18
+ score: float
19
+ reasons: list[str] = field(default_factory=list)
20
+
21
+
22
+ def score_change(ctx: ChangeContext, sensitive_paths: list[str], is_test_file) -> RiskScore:
23
+ reasons: list[str] = []
24
+ files = [f for f in ctx.files if f.status != "D"]
25
+ src = [f for f in files if not is_test_file(f.path)]
26
+ tests_touched = any(is_test_file(f.path) for f in files)
27
+ changed = sum(f.added_lines + f.removed_lines for f in src)
28
+ removed = sum(f.removed_lines for f in src)
29
+
30
+ size = min(1.0, math.log10(1 + changed) / 3) # ~1000 changed lines => 1.0
31
+ reasons.append(f"{changed} changed source lines")
32
+ sens_hits = sorted({p for f in src for p in sensitive_paths if re.search(rf"(^|[/_.\-]){re.escape(p)}", f.path, re.I)})
33
+ sensitive = 1.0 if sens_hits else 0.0
34
+ if sens_hits:
35
+ reasons.append("touches sensitive areas: " + ", ".join(sens_hits))
36
+ untested = 1.0 if src and not tests_touched else 0.0
37
+ if untested:
38
+ reasons.append("no tests changed alongside source changes")
39
+ spread = min(1.0, len(src) / 10)
40
+ deletion = removed / changed if changed else 0.0
41
+ if deletion > 0.5:
42
+ reasons.append(f"{deletion:.0%} of changed lines are deletions")
43
+ score = 0.35 * size + 0.25 * sensitive + 0.2 * untested + 0.1 * spread + 0.1 * deletion
44
+ return RiskScore(round(score, 3), reasons)
jitcatch/templates.py ADDED
@@ -0,0 +1,76 @@
1
+ CONFIG_TEMPLATE = '''\
2
+ # JiTCatch configuration. API keys are read from the environment variables named below.
3
+
4
+ # ---------------------------------------------------------------- LLM providers
5
+ # type: "anthropic" | "openai_chat" | "openai_responses" (or use a preset)
6
+ # presets: anthropic, openai, openai-chat, zai-coding, zai-coding-anthropic,
7
+ # zai-coding-responses, ollama, lmstudio, vllm
8
+
9
+ [providers.claude]
10
+ preset = "anthropic" # ANTHROPIC_API_KEY
11
+ model = "claude-sonnet-5-5"
12
+
13
+ [providers.glm]
14
+ preset = "zai-coding" # https://api.z.ai/api/coding/paas/v4, ZAI_API_KEY
15
+ model = "glm-5.3-flash" # preset sets max_tokens = 65536, temperature = 1.0 (Z.ai defaults)
16
+ # GLM-5.3 / 5.3-Flash always think; trade depth for Coding Plan quota with:
17
+ # extra_body = { reasoning_effort = "high" } # "max" (default) | "high" | "low"
18
+
19
+ # [providers.gpt]
20
+ # preset = "openai" # Responses API, OPENAI_API_KEY
21
+ # model = "gpt-5"
22
+ # omit_params = ["temperature"] # for models that reject temperature
23
+
24
+ # [providers.local]
25
+ # preset = "ollama" # http://localhost:11434/v1
26
+ # model = "qwen2.5-coder:32b"
27
+ # logprobs = true # enables the token-probability judge (if the server supports it)
28
+
29
+ # [providers.proxy] # any Anthropic-compatible endpoint
30
+ # type = "anthropic"
31
+ # base_url = "https://my-gateway.example.com"
32
+ # api_key_env = "GATEWAY_KEY"
33
+ # auth = "bearer" # or "x-api-key"
34
+ # model = "claude-sonnet-5-5"
35
+
36
+ [roles]
37
+ generator = "claude" # writes intents, mutants and tests
38
+ judges = ["claude", "glm"] # LLM-as-judge ensemble (median of High/Medium/Low)
39
+ # logprob_judge = "local" # optional; must be openai_chat with logprobs = true
40
+
41
+ # ---------------------------------------------------------------- pipeline
42
+ [run]
43
+ workflows = ["intent_aware", "dodgy_diff"]
44
+ max_target_files = 5
45
+ max_risks_per_file = 4
46
+ dodgy_tests_per_file = 2
47
+ max_fix_attempts = 2
48
+ check_mutants_with_existing_tests = true
49
+ flaky_reruns = 1
50
+ test_timeout_s = 300
51
+ min_risk_score = 0.0 # raise to only run on riskier changes
52
+ report_threshold = 0.2 # combined score (-1..1) needed to report a catch
53
+ link_paths = ["node_modules"] # symlinked into the temporary worktrees
54
+ exclude = ["**/generated/**"]
55
+
56
+ [assessors]
57
+ weights = { rules = 1.0, llm_bucket = 1.0, llm_logprob = 1.0 }
58
+ discard_on_high_fp_rule = true
59
+ # rule_overrides = { flakiness = "off", changed_bool = "0.5" }
60
+
61
+ # ---------------------------------------------------------------- languages
62
+ [adapters.python]
63
+ # python = ".venv/bin/python" # interpreter with pytest + project deps
64
+
65
+ [adapters.typescript]
66
+ runner = "auto" # auto | jest | vitest
67
+
68
+ [adapters.java]
69
+ build = "auto" # auto | maven | gradle
70
+
71
+ [adapters.go]
72
+
73
+ [hook]
74
+ blocking = false # true: a reported catch blocks the push
75
+ # default_branch = "main"
76
+ '''