pluginbench 0.5.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.
Files changed (58) hide show
  1. pluginbench/__init__.py +3 -0
  2. pluginbench/__main__.py +3 -0
  3. pluginbench/assessment-charts.html +790 -0
  4. pluginbench/assessment.py +316 -0
  5. pluginbench/assessment_charts.py +1225 -0
  6. pluginbench/assessment_metrics.py +1956 -0
  7. pluginbench/assessment_models.py +127 -0
  8. pluginbench/baselines.py +792 -0
  9. pluginbench/claude.py +374 -0
  10. pluginbench/cli.py +672 -0
  11. pluginbench/code_diff.py +207 -0
  12. pluginbench/codex.py +451 -0
  13. pluginbench/codex_events.py +585 -0
  14. pluginbench/collectors.py +591 -0
  15. pluginbench/compare.py +927 -0
  16. pluginbench/components.py +129 -0
  17. pluginbench/coverage_report.py +610 -0
  18. pluginbench/datasets.py +207 -0
  19. pluginbench/evaluation.example.yaml +45 -0
  20. pluginbench/evaluation.py +1651 -0
  21. pluginbench/evaluation_report.py +1004 -0
  22. pluginbench/fallow_parse.mjs +101 -0
  23. pluginbench/harbor_backend.py +2946 -0
  24. pluginbench/isolation.py +314 -0
  25. pluginbench/judge.py +683 -0
  26. pluginbench/language_analysis.py +915 -0
  27. pluginbench/ledger.py +217 -0
  28. pluginbench/measurement_profiles.py +616 -0
  29. pluginbench/native_treatment_runtime.js +221 -0
  30. pluginbench/native_treatments.py +155 -0
  31. pluginbench/observer_data/go-analysis-v1/go.mod +10 -0
  32. pluginbench/observer_data/go-analysis-v1/go.sum +10 -0
  33. pluginbench/observer_data/go-analysis-v1/main.go +64 -0
  34. pluginbench/observer_data/oxc-parser-0.126.0/package-lock.json +449 -0
  35. pluginbench/observer_data/oxc-parser-0.126.0/package.json +5 -0
  36. pluginbench/parallel.py +101 -0
  37. pluginbench/processes.py +100 -0
  38. pluginbench/remeasure.py +992 -0
  39. pluginbench/run_lock.py +227 -0
  40. pluginbench/run_status.py +518 -0
  41. pluginbench/study.py +1858 -0
  42. pluginbench/study_review.py +924 -0
  43. pluginbench/submissions.py +486 -0
  44. pluginbench/task_sources.py +403 -0
  45. pluginbench/treatments.py +388 -0
  46. pluginbench/trial_containers.py +94 -0
  47. pluginbench/vendor/ECHARTS-LICENSE +222 -0
  48. pluginbench/vendor/ECHARTS-NOTICE +5 -0
  49. pluginbench/vendor/ZRENDER-LICENSE +29 -0
  50. pluginbench/vendor/echarts.min.js +48 -0
  51. pluginbench-0.5.0.dist-info/METADATA +294 -0
  52. pluginbench-0.5.0.dist-info/RECORD +58 -0
  53. pluginbench-0.5.0.dist-info/WHEEL +4 -0
  54. pluginbench-0.5.0.dist-info/entry_points.txt +2 -0
  55. pluginbench-0.5.0.dist-info/licenses/LICENSE +201 -0
  56. pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-LICENSE +222 -0
  57. pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ECHARTS-NOTICE +5 -0
  58. pluginbench-0.5.0.dist-info/licenses/src/pluginbench/vendor/ZRENDER-LICENSE +29 -0
@@ -0,0 +1,1956 @@
1
+ """Offline, source-bound measurements of immutable submitted snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import fnmatch
7
+ import hashlib
8
+ import math
9
+ import json
10
+ import re
11
+ import shutil
12
+ import subprocess
13
+ import tempfile
14
+ from collections import Counter
15
+ from collections.abc import Mapping
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import Literal
19
+
20
+ from pluginbench.code_diff import (
21
+ _SOURCE_SUFFIXES,
22
+ _is_source_path,
23
+ _is_test_path,
24
+ _marker_path,
25
+ analyze_patch,
26
+ )
27
+
28
+ EXCLUDED_NAMES = frozenset({".git", ".agents", ".claude", "node_modules", "__pycache__"})
29
+
30
+
31
+ def _digest(data: bytes) -> str:
32
+ return "sha256:" + hashlib.sha256(data).hexdigest()
33
+
34
+
35
+ def _object(value: object, label: str) -> dict[str, object]:
36
+ if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
37
+ raise ValueError(f"{label} must be an object")
38
+ return {key: item for key, item in value.items()}
39
+
40
+
41
+ def _strings(value: object, label: str) -> list[str]:
42
+ if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
43
+ raise ValueError(f"{label} must be a string list")
44
+ if len(set(value)) != len(value):
45
+ raise ValueError(f"{label} contains duplicate identifiers")
46
+ return [item for item in value]
47
+
48
+
49
+ def _number(value: object, label: str) -> float:
50
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
51
+ raise ValueError(f"{label} must be a number")
52
+ if not math.isfinite(value):
53
+ raise ValueError(f"{label} must be finite")
54
+ return float(value)
55
+
56
+
57
+ def _integer(value: object, label: str) -> int:
58
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
59
+ raise ValueError(f"{label} must be a nonnegative integer")
60
+ return value
61
+
62
+
63
+ def _load(path: Path) -> dict[str, object]:
64
+ return _object(json.loads(path.read_text(encoding="utf-8")), str(path))
65
+
66
+
67
+ def inspect_snapshot(root: Path) -> dict[str, object]:
68
+ """Hash every included file; reject links instead of following them."""
69
+ if root.is_symlink() or not root.is_dir():
70
+ raise ValueError(f"snapshot must be a directory, not a link: {root}")
71
+ files: dict[str, str] = {}
72
+ excluded: list[str] = []
73
+ source_lines = test_lines = 0
74
+
75
+ def visit(directory: Path) -> None:
76
+ nonlocal source_lines, test_lines
77
+ for path in sorted(directory.iterdir()):
78
+ relative = path.relative_to(root).as_posix()
79
+ if path.is_symlink():
80
+ raise ValueError(f"snapshot contains a symlink: {relative}")
81
+ if path.name in EXCLUDED_NAMES:
82
+ excluded.append(relative)
83
+ elif path.is_dir():
84
+ visit(path)
85
+ elif path.is_file():
86
+ data = path.read_bytes()
87
+ files[relative] = _digest(data)
88
+ try:
89
+ lines = len(data.decode("utf-8").splitlines())
90
+ except UnicodeDecodeError:
91
+ continue
92
+ if _is_test_path(relative):
93
+ test_lines += lines
94
+ elif _is_source_path(relative):
95
+ source_lines += lines
96
+ else:
97
+ raise ValueError(f"snapshot contains a nonregular file: {relative}")
98
+
99
+ visit(root)
100
+ encoded = json.dumps(files, sort_keys=True, separators=(",", ":")).encode()
101
+ return {
102
+ "digest": _digest(encoded),
103
+ "files": files,
104
+ "excluded_paths": excluded,
105
+ "exclusion_names": sorted(EXCLUDED_NAMES),
106
+ "source_lines": source_lines,
107
+ "test_lines": test_lines,
108
+ }
109
+
110
+
111
+ def _changed_line_ranges(patch: str) -> dict[str, object]:
112
+ """Record zero-context Git hunk ranges, including zero-width edit anchors."""
113
+ ranges: dict[str, dict[str, list[list[int]]]] = {}
114
+ old_path = new_path = None
115
+ in_hunk = False
116
+ for line in patch.splitlines():
117
+ if line.startswith("diff --git "):
118
+ old_path = new_path = None
119
+ in_hunk = False
120
+ elif not in_hunk and line.startswith("--- "):
121
+ old_path = _marker_path(line[4:])
122
+ elif not in_hunk and line.startswith("+++ "):
123
+ new_path = _marker_path(line[4:])
124
+ elif line.startswith("@@ "):
125
+ in_hunk = True
126
+ match = re.match(r"@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@", line)
127
+ if match is None:
128
+ raise ValueError("invalid snapshot diff hunk")
129
+ for path, side, start, count in (
130
+ (old_path, "before", match[1], match[2]),
131
+ (new_path, "after", match[3], match[4]),
132
+ ):
133
+ if path is not None:
134
+ ranges.setdefault(path, {"before": [], "after": []})[side].append(
135
+ [int(start), int(count) if count is not None else 1]
136
+ )
137
+ return dict(ranges)
138
+
139
+
140
+ DUPLICATION_WINDOW = 6
141
+
142
+
143
+ def _test_duplication(patch: str) -> dict[str, object]:
144
+ """Added test lines inside a run of consecutive added lines that repeats elsewhere in added tests."""
145
+ hunks: list[list[str]] = []
146
+ new_path: str | None = None
147
+ for line in patch.splitlines():
148
+ if line.startswith("diff --git "):
149
+ new_path = None
150
+ elif line.startswith("+++ "):
151
+ new_path = _marker_path(line[4:])
152
+ elif line.startswith("@@ "):
153
+ hunks.append([])
154
+ elif (
155
+ line.startswith("+")
156
+ and new_path is not None
157
+ and _is_test_path(new_path)
158
+ and Path(new_path).suffix.lower() in _SOURCE_SUFFIXES
159
+ and hunks
160
+ ):
161
+ text = " ".join(line[1:].split())
162
+ if text:
163
+ hunks[-1].append(text)
164
+ windows: Counter[tuple[str, ...]] = Counter(
165
+ tuple(hunk[index : index + DUPLICATION_WINDOW])
166
+ for hunk in hunks
167
+ for index in range(len(hunk) - DUPLICATION_WINDOW + 1)
168
+ )
169
+ duplicated = 0
170
+ for hunk in hunks:
171
+ marked = [False] * len(hunk)
172
+ for index in range(len(hunk) - DUPLICATION_WINDOW + 1):
173
+ if windows[tuple(hunk[index : index + DUPLICATION_WINDOW])] > 1:
174
+ marked[index : index + DUPLICATION_WINDOW] = [True] * DUPLICATION_WINDOW
175
+ duplicated += sum(marked)
176
+ return {
177
+ "window_lines": DUPLICATION_WINDOW,
178
+ "added_test_lines": sum(len(hunk) for hunk in hunks),
179
+ "duplicated_added_test_lines": duplicated,
180
+ "method": "whitespace-normalized non-blank added lines in test source files (fixture and snapshot data excluded); a line is duplicated when it belongs to a run of consecutive added lines that appears more than once in added test code; copies of existing tests are not detected",
181
+ }
182
+
183
+
184
+ def compare_snapshots(before: Path, after: Path) -> dict[str, object]:
185
+ """Use the existing patch analyzer, with a canonical Git diff of filtered copies."""
186
+ snapshots = [inspect_snapshot(before), inspect_snapshot(after)]
187
+ with tempfile.TemporaryDirectory(prefix="pluginbench-assess-") as directory:
188
+ temporary = Path(directory)
189
+ for root, snapshot, name in zip((before, after), snapshots, ("a", "b"), strict=True):
190
+ target = temporary / name
191
+ target.mkdir()
192
+ for relative in _object(snapshot["files"], "snapshot files"):
193
+ destination = target / relative
194
+ destination.parent.mkdir(parents=True, exist_ok=True)
195
+ shutil.copyfile(root / relative, destination)
196
+ result = subprocess.run(
197
+ [
198
+ "git",
199
+ "diff",
200
+ "--no-index",
201
+ "--no-prefix",
202
+ "--binary",
203
+ "--no-ext-diff",
204
+ "--no-textconv",
205
+ "--no-color",
206
+ "--no-renames",
207
+ "--unified=0",
208
+ "--inter-hunk-context=0",
209
+ "--",
210
+ "a",
211
+ "b",
212
+ ],
213
+ cwd=temporary,
214
+ capture_output=True,
215
+ check=False,
216
+ )
217
+ if result.returncode not in {0, 1}:
218
+ raise ValueError(f"snapshot diff failed: {result.stderr.decode(errors='replace')}")
219
+ patch = result.stdout.decode(errors="surrogateescape")
220
+ metrics: dict[str, object] = dict(analyze_patch(patch))
221
+ metrics["changed_line_ranges"] = _changed_line_ranges(patch)
222
+ metrics["test_duplication"] = _test_duplication(patch)
223
+ for root, original in zip((before, after), snapshots, strict=True):
224
+ if inspect_snapshot(root)["digest"] != original["digest"]:
225
+ raise ValueError("snapshot changed during analysis")
226
+ metrics.update(
227
+ before_digest=snapshots[0]["digest"],
228
+ after_digest=snapshots[1]["digest"],
229
+ before_source_lines=snapshots[0]["source_lines"],
230
+ after_source_lines=snapshots[1]["source_lines"],
231
+ before_test_lines=snapshots[0]["test_lines"],
232
+ after_test_lines=snapshots[1]["test_lines"],
233
+ new_paths=sorted(
234
+ set(_object(snapshots[1]["files"], "files"))
235
+ - set(_object(snapshots[0]["files"], "files"))
236
+ ),
237
+ )
238
+ return metrics
239
+
240
+
241
+ def _scoped_lines(
242
+ *,
243
+ before: Path,
244
+ after: Path,
245
+ scope: list[str],
246
+ change: dict[str, object],
247
+ kind: Literal["source", "test"],
248
+ ) -> dict[str, object]:
249
+ """Measure explicit line scope directly, reusing an already bound source diff."""
250
+ if (
251
+ not isinstance(scope, list)
252
+ or not scope
253
+ or any(not isinstance(pattern, str) or not pattern for pattern in scope)
254
+ ):
255
+ raise ValueError("code-size scope must be an explicit nonempty string list")
256
+ normalized = sorted({pattern.removeprefix("./") for pattern in scope})
257
+ if any(
258
+ not pattern or Path(pattern).is_absolute() or ".." in Path(pattern).parts or "\\" in pattern
259
+ for pattern in normalized
260
+ ):
261
+ raise ValueError("code-size scope requires relative source patterns")
262
+ snapshots = [inspect_snapshot(before), inspect_snapshot(after)]
263
+ for side, snapshot in zip(("before", "after"), snapshots, strict=True):
264
+ if change.get(f"{side}_digest") != snapshot["digest"]:
265
+ raise ValueError("source diff does not bind to the inspected snapshot")
266
+ ranges = _object(change.get("changed_line_ranges"), "changed-line ranges")
267
+ inventories = [_object(snapshot["files"], "snapshot files") for snapshot in snapshots]
268
+ populations: list[dict[str, int]] = []
269
+ classify = _is_source_path if kind == "source" else _is_test_path
270
+ for root, files in zip((before, after), inventories, strict=True):
271
+ lines: dict[str, int] = {}
272
+ for path in sorted(files):
273
+ if not classify(path) or not any(
274
+ fnmatch.fnmatchcase(path, pattern) for pattern in normalized
275
+ ):
276
+ continue
277
+ try:
278
+ data = (root / path).read_bytes()
279
+ except OSError as error:
280
+ raise ValueError("snapshot changed during code-size analysis") from error
281
+ if _digest(data) != files[path]:
282
+ raise ValueError("snapshot changed during code-size analysis")
283
+ try:
284
+ lines[path] = len(data.decode("utf-8").splitlines())
285
+ except UnicodeDecodeError as error:
286
+ if kind == "test" and Path(path).suffix.lower() not in _SOURCE_SUFFIXES:
287
+ continue
288
+ # A file the change leaves byte-identical adds and removes no lines,
289
+ # whatever its encoding.
290
+ if inventories[0].get(path) == inventories[1].get(path):
291
+ continue
292
+ raise ValueError(f"scoped source must be UTF-8 text: {path}") from error
293
+ populations.append(lines)
294
+ added = removed = 0
295
+ before_files, after_files = inventories
296
+ for path in sorted(set(populations[0]) | set(populations[1])):
297
+ if before_files.get(path) != after_files.get(path) and path not in ranges:
298
+ raise ValueError(f"bound source diff omits changed scoped path: {path}")
299
+ row = _object(ranges.get(path, {"before": [], "after": []}), "file ranges")
300
+ if set(row) != {"before", "after"}:
301
+ raise ValueError("changed-line ranges require before and after")
302
+ counts = []
303
+ for side, population in zip(("before", "after"), populations, strict=True):
304
+ items = row[side]
305
+ if not isinstance(items, list):
306
+ raise ValueError("changed-line ranges must be a list")
307
+ count_total = 0
308
+ previous_end = 0
309
+ for item in items:
310
+ if not isinstance(item, list) or len(item) != 2:
311
+ raise ValueError("changed-line range requires start and count")
312
+ start, count = (_integer(value, "changed-line range") for value in item)
313
+ if (count and start <= previous_end) or start + max(count - 1, 0) > population.get(
314
+ path, 0
315
+ ):
316
+ raise ValueError("changed-line range escapes scoped text or overlaps")
317
+ if count:
318
+ previous_end = start + count - 1
319
+ count_total += count
320
+ counts.append(count_total)
321
+ removed += counts[0]
322
+ added += counts[1]
323
+ for root, original in zip((before, after), snapshots, strict=True):
324
+ if inspect_snapshot(root)["digest"] != original["digest"]:
325
+ raise ValueError("snapshot changed during code-size analysis")
326
+ return {
327
+ "status": "available",
328
+ "method": f"scoped-{kind}-lines-v1",
329
+ "scope": normalized,
330
+ "before_digest": snapshots[0]["digest"],
331
+ "after_digest": snapshots[1]["digest"],
332
+ "before_paths": sorted(populations[0]),
333
+ "after_paths": sorted(populations[1]),
334
+ f"before_{kind}_lines": sum(populations[0].values()),
335
+ f"after_{kind}_lines": sum(populations[1].values()),
336
+ f"{kind}_lines_added": added,
337
+ f"{kind}_lines_removed": removed,
338
+ "counting_semantics": f"UTF-8 splitlines totals from snapshot inventory paths matched by fnmatchcase; shared {kind} classification; additions/removals sum supplied zero-context Git hunk counts in scope; no analyzer file-discovery or generated-directory filtering",
339
+ }
340
+
341
+
342
+ def scoped_code_size(
343
+ *, before: Path, after: Path, scope: list[str], change: dict[str, object]
344
+ ) -> dict[str, object]:
345
+ return _scoped_lines(before=before, after=after, scope=scope, change=change, kind="source")
346
+
347
+
348
+ def scoped_test_size(
349
+ *, before: Path, after: Path, scope: list[str], change: dict[str, object]
350
+ ) -> dict[str, object]:
351
+ return _scoped_lines(before=before, after=after, scope=scope, change=change, kind="test")
352
+
353
+
354
+ def _source_path(path: str, files: set[str], scope: list[str]) -> None:
355
+ if path not in files or not any(fnmatch.fnmatchcase(path, pattern) for pattern in scope):
356
+ raise ValueError(f"measurement path escapes source-bound scope: {path}")
357
+
358
+
359
+ def _coordinates(
360
+ value: object, label: str, files: set[str], scope: list[str]
361
+ ) -> tuple[set[str], set[str]]:
362
+ row = _object(value, label)
363
+ if not {"total", "covered"} <= set(row) or set(row) - {
364
+ "total",
365
+ "covered",
366
+ "unit",
367
+ "weights",
368
+ "files_measured",
369
+ }:
370
+ raise ValueError(f"{label} requires total and covered coordinate lists")
371
+ total = set(_strings(row["total"], f"{label}.total"))
372
+ covered = set(_strings(row["covered"], f"{label}.covered"))
373
+ for coordinate in total:
374
+ if ":" not in coordinate:
375
+ raise ValueError("coverage coordinates require relative-path:coordinate")
376
+ _source_path(coordinate.split(":", 1)[0], files, scope)
377
+ if not covered <= total:
378
+ raise ValueError(f"{label} covered coordinates escape the fixed denominator")
379
+ return total, covered
380
+
381
+
382
+ def _percent(numerator: int, denominator: int) -> float | None:
383
+ return 100 * numerator / denominator if denominator else None
384
+
385
+
386
+ def _marginal(value: object, measured: set[str], label: str) -> list[str] | None:
387
+ if value is None:
388
+ return None
389
+ rows = _object(value, label)
390
+ per_test = {name: set(_strings(items, f"{label}.{name}")) for name, items in rows.items()}
391
+ if any(not items <= measured for items in per_test.values()):
392
+ raise ValueError(f"{label} includes unmeasured coordinates")
393
+ return sorted(
394
+ name
395
+ for name, items in per_test.items()
396
+ if not items - set().union(*(other for key, other in per_test.items() if key != name))
397
+ )
398
+
399
+
400
+ def _coverage_limits(
401
+ report: dict[str, object], files: set[str], scope: list[str]
402
+ ) -> dict[str, object]:
403
+ limits = _object(report.get("unavailable_populations", {}), "unavailable populations")
404
+ for kind, value in limits.items():
405
+ row = _object(value, "unavailable population")
406
+ if kind not in {"lines", "statements", "branches", "functions"} or not isinstance(
407
+ row.get("reason"), str
408
+ ):
409
+ raise ValueError("Unavailable coverage requires a known unit and reason")
410
+ for path in _strings(row.get("files"), "unavailable coverage files"):
411
+ _source_path(path, files, scope)
412
+ return limits
413
+
414
+
415
+ def _coverage(report: dict[str, object], files: set[str], scope: list[str]) -> dict[str, object]:
416
+ test_scope = (
417
+ [name.removeprefix("./") for name in _strings(report["test_scope"], "test scope")]
418
+ if report.get("test_scope") is not None
419
+ else None
420
+ )
421
+ excluded_tests = [
422
+ name.removeprefix("./")
423
+ for name in _strings(report.get("excluded_test_files", []), "excluded test files")
424
+ ]
425
+ populations = {
426
+ kind: _coordinates(report[kind], kind, files, scope)
427
+ for kind in ("lines", "statements", "branches", "functions")
428
+ if kind in report
429
+ }
430
+ if not populations:
431
+ raise ValueError("coverage requires at least one explicit measured population")
432
+ covered_branches = populations.get("branches", (set(), set()))[1]
433
+ measured_value = report.get("files_measured")
434
+ measured = (
435
+ set(_strings(measured_value, "files_measured")) if measured_value is not None else set()
436
+ )
437
+ for path in measured:
438
+ _source_path(path, files, scope)
439
+ expected = {
440
+ path
441
+ for path in files
442
+ if any(fnmatch.fnmatchcase(path, pattern) for pattern in scope)
443
+ and _is_source_path(path)
444
+ and Path(path).suffix.lower() in _SOURCE_SUFFIXES
445
+ and not path.endswith((".d.ts", ".d.mts", ".d.cts"))
446
+ }
447
+ units: dict[str, object] = {}
448
+ for kind, singular in (
449
+ ("lines", "line"),
450
+ ("statements", "statement"),
451
+ ("branches", "branch"),
452
+ ("functions", "function"),
453
+ ):
454
+ if kind not in populations:
455
+ units.update(
456
+ {
457
+ f"{singular}_{suffix}": None
458
+ for suffix in ("total", "covered", "percent", "denominator_digest")
459
+ }
460
+ )
461
+ units[f"{singular}_population_complete"] = False
462
+ continue
463
+ total, covered = populations[kind]
464
+ row = _object(report[kind], kind)
465
+ if row.get("unit", singular) != singular:
466
+ raise ValueError(f"coverage {kind} uses an incompatible unit")
467
+ weights = _object(row.get("weights", {key: 1 for key in total}), f"{kind}.weights")
468
+ if set(weights) != total:
469
+ raise ValueError("coverage weights must cover the complete coordinate population")
470
+ counts = {key: _integer(value, "coverage weight") for key, value in weights.items()}
471
+ denominator = sum(counts.values())
472
+ numerator = sum(counts[key] for key in covered)
473
+ unit_files = set(
474
+ _strings(row.get("files_measured", sorted(measured)), f"{kind}.files_measured")
475
+ )
476
+ for path in unit_files:
477
+ _source_path(path, files, scope)
478
+ units.update(
479
+ {
480
+ f"{singular}_total": denominator,
481
+ f"{singular}_covered": numerator,
482
+ f"{singular}_percent": _percent(numerator, denominator),
483
+ f"{singular}_denominator_digest": _digest(
484
+ json.dumps(sorted(counts.items())).encode()
485
+ ),
486
+ f"{singular}_population_complete": bool(expected) and expected <= unit_files,
487
+ }
488
+ )
489
+ lines_total = populations.get("lines", (set(), set()))[0]
490
+ per_test_numbits = None
491
+ if report.get("per_test_numbits") is not None:
492
+ per_test_numbits = {
493
+ name: {
494
+ path: str(encoded)
495
+ for path, encoded in _object(files, f"per_test_numbits.{name}").items()
496
+ }
497
+ for name, files in _object(report["per_test_numbits"], "per_test_numbits").items()
498
+ }
499
+ if any(
500
+ not _numbit_coordinates(files) <= lines_total for files in per_test_numbits.values()
501
+ ):
502
+ raise ValueError("per_test_numbits includes unmeasured line coordinates")
503
+ return {
504
+ **units,
505
+ # Coordinates support changed-line coverage; per-test lines support redundancy.
506
+ "coordinates": {
507
+ unit: {"total": sorted(populations[kind][0]), "covered": sorted(populations[kind][1])}
508
+ for kind, unit in (("lines", "line"), ("branches", "branch"))
509
+ if kind in populations
510
+ },
511
+ # Compact per-test lines (coverage.py numbits per file); decoded only where needed.
512
+ "per_test_numbits": per_test_numbits,
513
+ "available_units": [
514
+ unit
515
+ for kind, unit in (
516
+ ("lines", "line"),
517
+ ("statements", "statement"),
518
+ ("branches", "branch"),
519
+ ("functions", "function"),
520
+ )
521
+ if kind in populations
522
+ ],
523
+ "unavailable_populations": _coverage_limits(report, files, scope),
524
+ "format": report.get("format"),
525
+ "test_command_exit_code": report.get("test_command_exit_code"),
526
+ "population_complete_for_declared_scope": bool(expected) and expected <= measured,
527
+ "files_measured": sorted(measured),
528
+ "unmeasured_source_files": sorted(expected - measured),
529
+ "per_test_detection": "unique contribution among supplied tests only; not proof of redundancy",
530
+ "per_test_count": len(_object(report["per_test"], "per_test"))
531
+ if report.get("per_test") is not None
532
+ else None,
533
+ "zero_unique_branch_contribution_tests": _marginal(
534
+ report.get("per_test"), covered_branches, "per_test"
535
+ ),
536
+ "test_scope": test_scope,
537
+ "excluded_test_files": excluded_tests,
538
+ "selected_test_files": report.get("selected_test_files"),
539
+ "reference_test_files": report.get("reference_test_files"),
540
+ "selection_policy": report.get("selection_policy"),
541
+ **({"clock": report["clock"]} if "clock" in report else {}),
542
+ }
543
+
544
+
545
+ _DEAD_FIELDS = (
546
+ "unused_files",
547
+ "unused_exports",
548
+ "unused_types",
549
+ "unused_dependencies",
550
+ "unused_dev_dependencies",
551
+ "unused_optional_dependencies",
552
+ "unused_enum_members",
553
+ "unused_class_members",
554
+ "unused_catalog_entries",
555
+ "unused_dependency_overrides",
556
+ )
557
+
558
+ _DEAD_IDENTITY_FIELDS = {
559
+ "unused_files": ("path",),
560
+ "unused_exports": ("path", "export_name"),
561
+ "unused_types": ("path", "export_name"),
562
+ "unused_dependencies": ("path", "package_name"),
563
+ "unused_dev_dependencies": ("path", "package_name"),
564
+ "unused_optional_dependencies": ("path", "package_name"),
565
+ "unused_enum_members": ("path", "parent_name", "member_name", "kind"),
566
+ "unused_class_members": ("path", "parent_name", "member_name", "kind"),
567
+ "unused_catalog_entries": ("path", "catalog_name", "entry_name"),
568
+ "unused_dependency_overrides": ("path", "raw_key"),
569
+ }
570
+
571
+
572
+ def _declaration_context(root: Path, row: dict[str, object]) -> str | None:
573
+ line = row.get("line")
574
+ if type(line) is not int or line < 1:
575
+ return None
576
+ path = row["path"]
577
+ assert isinstance(path, str)
578
+ try:
579
+ source = (root / path).read_bytes().decode("utf-8")
580
+ except (OSError, UnicodeDecodeError):
581
+ return None
582
+ lines = source.splitlines(keepends=True)
583
+ if line > len(lines):
584
+ return None
585
+ context = lines[line - 1].strip()
586
+ start = row.get("span_start")
587
+ if start is not None:
588
+ offset = len("".join(lines[: line - 1]).encode("utf-8"))
589
+ if type(start) is not int or not offset <= start < offset + len(
590
+ lines[line - 1].encode("utf-8")
591
+ ):
592
+ return None
593
+ return _digest(context.encode("utf-8")) if context else None
594
+
595
+
596
+ def _dead_identities(
597
+ check: dict[str, object], files: set[str], root: Path
598
+ ) -> list[dict[str, str]] | None:
599
+ """Retain bound declaration context for pair-aware collision disambiguation."""
600
+ findings = []
601
+ for category, fields in _DEAD_IDENTITY_FIELDS.items():
602
+ items = check.get(category)
603
+ if not isinstance(items, list):
604
+ return None
605
+ for item in items:
606
+ if not isinstance(item, dict):
607
+ return None
608
+ row = _object(item, category)
609
+ if any(not isinstance(row.get(key), str) or not row[key] for key in fields):
610
+ return None
611
+ if row["path"] not in files:
612
+ raise ValueError(f"dead finding path escapes bound snapshot: {row['path']}")
613
+ identity = {"category": category, **{key: str(row[key]) for key in fields}}
614
+ context = _declaration_context(root, row)
615
+ if context is not None:
616
+ identity["declaration_context"] = context
617
+ findings.append(identity)
618
+ return findings
619
+
620
+
621
+ def _list_count(report: dict[str, object], key: str) -> int | None:
622
+ value = report.get(key)
623
+ return len(value) if isinstance(value, list) else None
624
+
625
+
626
+ _OXC_VERSION = "0.126.0"
627
+ _OXC_COMMIT = "836fe9d51c11981162461216f37a067fc14441a7"
628
+
629
+
630
+ def _validated_fallow_inputs(
631
+ report: dict[str, object], config: dict[str, object], root: Path, inputs: set[str]
632
+ ) -> bool:
633
+ """Pinned producer execution plus strict primary parses closes silent read/parse omissions."""
634
+ validation = report.get("syntax_validation")
635
+ if not isinstance(validation, dict) or report.get("version") != "3.18.0":
636
+ return False
637
+ if any(
638
+ validation.get(key) != expected
639
+ for key, expected in {
640
+ "schema_version": 1,
641
+ "parser": "oxc-parser",
642
+ "version": _OXC_VERSION,
643
+ "commit": _OXC_COMMIT,
644
+ "producer_version": "3.18.0",
645
+ "parser_options": "fallow-3.18.0-default-primary",
646
+ "package_integrity": "sha512-FktCvLby/mOHyuijZt22+nOt10dS24gGUZE3XwIbUg7Kf4+rer3/5T7RgwzazlNuVsCjPloZ3p8E+4ONT3A8Kw==",
647
+ }.items()
648
+ ):
649
+ return False
650
+ # This audited producer path shares discovery with dupes. Other modes need
651
+ # their own observer proof, especially per-analysis production overrides.
652
+ if set(config) - {"$schema", "entry", "production", "ignorePatterns", "health", "duplicates"}:
653
+ return False
654
+ if config.get("production") is not True:
655
+ return False
656
+ for section in ("health", "duplicates"):
657
+ value = config.get(section, {})
658
+ if not isinstance(value, dict) or "production" in value:
659
+ return False
660
+ duplicates = config.get("duplicates", {})
661
+ assert isinstance(duplicates, dict)
662
+ # Default duplicate options are the audited eligibility path. Do not claim
663
+ # completeness for unexamined nested limits, near-clone modes or overrides.
664
+ if duplicates:
665
+ return False
666
+ executions = validation.get("producer_execution")
667
+ validator_execution = validation.get("validator_execution")
668
+ if (
669
+ not isinstance(executions, list)
670
+ or len(executions) != 2
671
+ or not isinstance(validator_execution, dict)
672
+ or validator_execution.get("exit_code") != 0
673
+ ):
674
+ return False
675
+ for index, execution in enumerate(executions):
676
+ if not isinstance(execution, dict) or execution.get("exit_code") not in (0, 1):
677
+ return False
678
+ argv = execution.get("command")
679
+ if not isinstance(argv, list) or not all(isinstance(item, str) for item in argv):
680
+ return False
681
+ # Exact built-in command shape excludes diff/workspace/top truncation.
682
+ if (
683
+ len(argv) != 10 + index * 2
684
+ or argv[0] != "fallow"
685
+ or argv[1] != "--root"
686
+ or argv[3] != "--config"
687
+ ):
688
+ return False
689
+ if argv[5:10] != ["--format", "json", "--quiet", "--explain", "--no-cache"]:
690
+ return False
691
+ if index and argv[-2:] != ["list", "--files"]:
692
+ return False
693
+ if not isinstance(validation.get("lock_digest"), str) or not str(
694
+ validation["lock_digest"]
695
+ ).startswith("sha256:"):
696
+ return False
697
+ binding = validation.get("binding")
698
+ if (
699
+ not isinstance(binding, dict)
700
+ or len(binding) != 1
701
+ or any(
702
+ not isinstance(value, dict)
703
+ or value.get("version") != _OXC_VERSION
704
+ or not value.get("integrity")
705
+ for value in binding.values()
706
+ )
707
+ ):
708
+ return False
709
+ for key in ("dependency_hashes", "validator_files"):
710
+ hashes = validation.get(key)
711
+ if (
712
+ not isinstance(hashes, dict)
713
+ or not hashes
714
+ or any(
715
+ not isinstance(value, str) or not value.startswith("sha256:")
716
+ for value in hashes.values()
717
+ )
718
+ ):
719
+ return False
720
+ rows = validation.get("files")
721
+ if not isinstance(rows, list) or len(rows) != len(inputs):
722
+ return False
723
+ observed = set()
724
+ for row in rows:
725
+ if not isinstance(row, dict) or row.get("path") not in inputs:
726
+ return False
727
+ path = row["path"]
728
+ assert isinstance(path, str)
729
+ if path in observed or row.get("status") != "valid" or row.get("errors") != []:
730
+ return False
731
+ try:
732
+ data = (root / path).read_bytes()
733
+ data.decode("utf-8")
734
+ except (OSError, UnicodeDecodeError):
735
+ return False
736
+ if row.get("source_digest") != _digest(data):
737
+ return False
738
+ observed.add(path)
739
+ return observed == inputs
740
+
741
+
742
+ def _clone_identities(dupes: dict[str, object], files: set[str], root: Path) -> list[str] | None:
743
+ groups = dupes.get("clone_groups")
744
+ if not isinstance(groups, list):
745
+ return None
746
+ identities = []
747
+ for group in groups:
748
+ if not isinstance(group, dict) or not isinstance(group.get("instances"), list):
749
+ return None
750
+ members = []
751
+ for instance in group["instances"]:
752
+ if not isinstance(instance, dict):
753
+ return None
754
+ path, fragment = instance.get("file"), instance.get("fragment")
755
+ if (
756
+ not isinstance(path, str)
757
+ or path not in files
758
+ or not isinstance(fragment, str)
759
+ or not fragment
760
+ ):
761
+ return None
762
+ try:
763
+ source = (root / path).read_bytes().decode("utf-8")
764
+ except (OSError, UnicodeDecodeError):
765
+ return None
766
+ if fragment not in source:
767
+ return None
768
+ members.append((path, _digest(fragment.encode("utf-8"))))
769
+ if len(members) < 2:
770
+ return None
771
+ identities.append(_digest(json.dumps(sorted(members), separators=(",", ":")).encode()))
772
+ return identities if len(identities) == len(set(identities)) else None
773
+
774
+
775
+ def _analysis_functions(
776
+ report: dict[str, object], *, analyzed: set[str], root: Path, complete: dict[str, bool]
777
+ ) -> list[dict[str, object]]:
778
+ raw = report.get("functions")
779
+ if not isinstance(raw, list):
780
+ raise ValueError("analysis requires function rows")
781
+ functions = []
782
+ seen = set()
783
+ for value in raw:
784
+ row = _object(value, "analysis function")
785
+ path = row.get("path")
786
+ if (
787
+ not isinstance(path, str)
788
+ or path not in analyzed
789
+ or not isinstance(row.get("name"), str)
790
+ ):
791
+ raise ValueError("analysis function escapes analyzed source")
792
+ first = _integer(row.get("line"), "function line")
793
+ last = _integer(row.get("end_line"), "function end line")
794
+ if (
795
+ not 1 <= first <= last <= len((root / path).read_text().splitlines())
796
+ or row.get("line_count") != last - first + 1
797
+ ):
798
+ raise ValueError("analysis function span escapes source")
799
+ identity = (path, first, row.get("col"), row["name"])
800
+ if identity in seen:
801
+ raise ValueError("duplicate analysis function")
802
+ seen.add(identity)
803
+ for key in ("cognitive", "cyclomatic"):
804
+ if complete[key]:
805
+ _integer(row.get(key), key)
806
+ elif row.get(key) is not None:
807
+ raise ValueError("unavailable analysis metric must not provide scores")
808
+ functions.append(row)
809
+ return functions
810
+
811
+
812
+ def _python_declarations(source: str) -> dict[tuple[int, str], list[dict[str, object]]]:
813
+ """Bind declarations to lexical scope and structure, never occurrence ordinals."""
814
+ declarations: dict[tuple[int, str], list[dict[str, object]]] = {}
815
+ scopes: Counter[tuple[str, ...]] = Counter()
816
+
817
+ def visit(node: ast.AST, scope: tuple[str, ...]) -> None:
818
+ symbol = None
819
+ role = None
820
+ line = getattr(node, "lineno", None)
821
+ outer_scope = scope
822
+ if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
823
+ overload_signature = None
824
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and any(
825
+ (isinstance(item, ast.Name) and item.id == "overload")
826
+ or (isinstance(item, ast.Attribute) and item.attr == "overload")
827
+ for item in node.decorator_list
828
+ ):
829
+ overload_signature = [
830
+ ast.dump(node.args),
831
+ ast.dump(node.returns) if node.returns else None,
832
+ ]
833
+ token = json.dumps(
834
+ [
835
+ type(node).__name__,
836
+ node.name,
837
+ [ast.dump(item) for item in node.decorator_list],
838
+ overload_signature,
839
+ ]
840
+ )
841
+ scope = (*scope, token)
842
+ scopes[scope] += 1
843
+ symbol, role = node.name, "class" if isinstance(node, ast.ClassDef) else "function"
844
+ if node.decorator_list:
845
+ line = node.decorator_list[0].lineno
846
+ elif isinstance(node, ast.Lambda):
847
+ scope = (*scope, "Lambda:" + _digest(ast.dump(node).encode()))
848
+ scopes[scope] += 1
849
+ elif isinstance(node, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
850
+ scope = (*scope, "unsupported:" + type(node).__name__)
851
+ elif isinstance(node, ast.arg):
852
+ symbol, role = node.arg, "parameter"
853
+ elif isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
854
+ symbol, role = node.id, "variable"
855
+ elif isinstance(node, ast.Attribute) and isinstance(node.ctx, ast.Store):
856
+ symbol, role = node.attr, "attribute"
857
+ elif isinstance(node, (ast.Import, ast.ImportFrom)):
858
+ for alias in node.names:
859
+ declarations.setdefault(
860
+ (node.lineno, alias.asname or alias.name.split(".")[0]), []
861
+ ).append({"declaration_scope": list(scope), "declaration_role": "import"})
862
+ elif isinstance(node, ast.ExceptHandler) and node.name:
863
+ symbol, role = node.name, "variable"
864
+ if symbol is not None:
865
+ assert isinstance(line, int)
866
+ declarations.setdefault((line, symbol), []).append(
867
+ {"declaration_scope": list(scope), "declaration_role": role}
868
+ )
869
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
870
+ arguments = [*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs]
871
+ arguments += [item for item in (node.args.vararg, node.args.kwarg) if item is not None]
872
+ for argument in arguments:
873
+ visit(argument, scope)
874
+ if argument.annotation is not None:
875
+ visit(argument.annotation, outer_scope)
876
+ for expression in [*node.args.defaults, *node.args.kw_defaults]:
877
+ if expression is not None:
878
+ visit(expression, outer_scope)
879
+ if isinstance(node, ast.Lambda):
880
+ visit(node.body, scope)
881
+ else:
882
+ for decorator in node.decorator_list:
883
+ visit(decorator, outer_scope)
884
+ if node.returns is not None:
885
+ visit(node.returns, outer_scope)
886
+ for statement in node.body:
887
+ visit(statement, scope)
888
+ elif isinstance(node, ast.ClassDef):
889
+ for class_child in [*node.bases, *node.keywords, *node.decorator_list]:
890
+ visit(class_child, outer_scope)
891
+ for statement in node.body:
892
+ visit(statement, scope)
893
+ elif not isinstance(node, ast.arg):
894
+ for child in ast.iter_child_nodes(node):
895
+ visit(child, scope)
896
+
897
+ visit(ast.parse(source), ())
898
+ lines = source.splitlines()
899
+ contexts = Counter(
900
+ json.dumps(
901
+ [row["declaration_scope"], row["declaration_role"], symbol, lines[line - 1].strip()]
902
+ )
903
+ for (line, symbol), rows in declarations.items()
904
+ for row in rows
905
+ )
906
+ bindings = Counter(
907
+ json.dumps([row["declaration_scope"], row["declaration_role"], symbol])
908
+ for (_, symbol), rows in declarations.items()
909
+ for row in rows
910
+ )
911
+ for (line, symbol), rows in declarations.items():
912
+ for row in rows:
913
+ scope = row["declaration_scope"]
914
+ assert isinstance(scope, list)
915
+ row["declaration_context_required"] = (
916
+ bindings[json.dumps([scope, row["declaration_role"], symbol])] > 1
917
+ )
918
+ row["declaration_bound"] = contexts[
919
+ json.dumps([scope, row["declaration_role"], symbol, lines[line - 1].strip()])
920
+ ] == 1 and not any(
921
+ token.startswith("unsupported:") or scopes[tuple(scope[: index + 1])] > 1
922
+ for index, token in enumerate(scope)
923
+ )
924
+ return declarations
925
+
926
+
927
+ def _analysis_candidates(
928
+ report: dict[str, object], *, analyzed: set[str], root: Path
929
+ ) -> tuple[list[object], list[dict[str, object]]]:
930
+ candidates = report.get("unused_candidates")
931
+ if not isinstance(candidates, list):
932
+ raise ValueError("analysis requires unused candidate rows")
933
+ identities = []
934
+ python_sources: dict[str, dict[tuple[int, str], list[dict[str, object]]]] = {}
935
+ for value in candidates:
936
+ row = _object(value, "unused candidate")
937
+ path = row.get("path")
938
+ if not isinstance(path, str) or path not in analyzed:
939
+ raise ValueError("unused candidate escapes analyzed source")
940
+ line = _integer(row.get("line"), "candidate line")
941
+ lines = (root / path).read_text().splitlines()
942
+ if not 1 <= line <= len(lines):
943
+ raise ValueError("unused candidate line escapes source")
944
+ if not all(isinstance(row.get(key), str) and row[key] for key in ("category", "symbol")):
945
+ raise ValueError("unused candidate requires rule and symbol")
946
+ identity = {
947
+ "category": row["category"],
948
+ "path": path,
949
+ "symbol": row["symbol"],
950
+ "declaration_context": lines[line - 1].strip(),
951
+ }
952
+ if report.get("language") == "python":
953
+ if path not in python_sources:
954
+ try:
955
+ python_sources[path] = _python_declarations((root / path).read_text())
956
+ except SyntaxError:
957
+ python_sources[path] = {}
958
+ roles = {
959
+ "vulture-unused-variable": {"variable", "parameter"},
960
+ "vulture-unused-attribute": {"attribute"},
961
+ "vulture-unused-import": {"import"},
962
+ "vulture-unused-class": {"class"},
963
+ "vulture-unused-function": {"function"},
964
+ "vulture-unused-method": {"function"},
965
+ "vulture-unused-property": {"function"},
966
+ }.get(str(row["category"]), set())
967
+ bindings = [
968
+ item
969
+ for item in python_sources[path].get((line, str(row["symbol"])), [])
970
+ if item["declaration_role"] in roles
971
+ ]
972
+ identity.update(bindings[0] if len(bindings) == 1 else {"declaration_bound": False})
973
+ identities.append(identity)
974
+ return candidates, identities
975
+
976
+
977
+ def _analysis(
978
+ report: dict[str, object],
979
+ files: set[str],
980
+ scope: list[str],
981
+ root: Path,
982
+ config: dict[str, object],
983
+ ) -> dict[str, object]:
984
+ if report.get("version") != "language-analysis-v1" or report.get("language") != config.get(
985
+ "language"
986
+ ):
987
+ raise ValueError("language analysis version or language differs from frozen configuration")
988
+ if report.get("tools") != config.get("tool_identities"):
989
+ raise ValueError("analysis tools differ from frozen configuration")
990
+ if report.get("settings") != config.get("settings") or report.get(
991
+ "supported_suffixes"
992
+ ) != config.get("supported_suffixes"):
993
+ raise ValueError("analysis settings or source language differ from frozen configuration")
994
+ suffixes = _strings(report.get("supported_suffixes"), "analysis suffixes")
995
+ expected = {
996
+ path
997
+ for path in files
998
+ if Path(path).suffix in suffixes
999
+ and not _is_test_path(path)
1000
+ and any(fnmatch.fnmatchcase(path, pattern) for pattern in scope)
1001
+ }
1002
+ analyzed = set(_strings(report.get("analyzed_paths"), "analyzed paths"))
1003
+ if analyzed != expected:
1004
+ raise ValueError("analysis paths differ from the declared source population")
1005
+ statuses = _object(report.get("metrics"), "analysis metrics")
1006
+ complete = {
1007
+ key: _object(statuses.get(key), key).get("status") == "available"
1008
+ for key in ("cognitive", "cyclomatic", "dead_code")
1009
+ }
1010
+ functions = _analysis_functions(report, analyzed=analyzed, root=root, complete=complete)
1011
+ candidates, identities = _analysis_candidates(report, analyzed=analyzed, root=root)
1012
+ return {
1013
+ **_function_population(functions),
1014
+ "complexity_complete_for_declared_scope": complete["cognitive"] and complete["cyclomatic"],
1015
+ "metric_statuses": statuses,
1016
+ "dead_code": len(candidates) if complete["dead_code"] else None,
1017
+ "dead_finding_identities": identities if complete["dead_code"] else None,
1018
+ "dead_code_complete_for_declared_scope": complete["dead_code"],
1019
+ "dead_code_detection": "unused candidates under the named analyzer and build settings; not semantic deadness or absence proof",
1020
+ "analyzed_paths": sorted(analyzed),
1021
+ "unused_candidates": candidates,
1022
+ "language": report["language"],
1023
+ "tools": report["tools"],
1024
+ "settings": report.get("settings"),
1025
+ }
1026
+
1027
+
1028
+ def _fallow_production_excluded(path: str) -> bool:
1029
+ # Pinned Fallow 3.18.0 discover/walk.rs uses literal directory separators.
1030
+ parts = Path(path).parts
1031
+ if any(
1032
+ part in {"__tests__", "__mocks__", "__snapshots__", "__fixtures__", "test", "tests"}
1033
+ for part in parts[:-1]
1034
+ ):
1035
+ return True
1036
+ patterns = (
1037
+ "*.test.*",
1038
+ "*.spec.*",
1039
+ "*.e2e.*",
1040
+ "*.e2e-spec.*",
1041
+ "*.bench.*",
1042
+ "*.fixture.*",
1043
+ "*.stories.*",
1044
+ "*.story.*",
1045
+ ".*.js",
1046
+ ".*.ts",
1047
+ ".*.mjs",
1048
+ ".*.cjs",
1049
+ )
1050
+ return any(fnmatch.fnmatchcase(parts[-1], pattern) for pattern in patterns) or (
1051
+ len(parts) == 1 and fnmatch.fnmatchcase(parts[-1], "*.config.*")
1052
+ )
1053
+
1054
+
1055
+ def _fallow(
1056
+ report: dict[str, object],
1057
+ boundary_rules: bool,
1058
+ files: set[str],
1059
+ scope: list[str],
1060
+ root: Path,
1061
+ config: dict[str, object],
1062
+ config_digest: str,
1063
+ ) -> dict[str, object]:
1064
+ if report.get("kind") != "combined":
1065
+ raise ValueError("Fallow evidence must be a combined report")
1066
+ check = _object(report.get("check"), "fallow.check")
1067
+ dupes = _object(report.get("dupes"), "fallow.dupes")
1068
+ health = _object(report.get("health"), "fallow.health")
1069
+ findings = health.get("findings")
1070
+ if not isinstance(findings, list):
1071
+ raise ValueError("fallow.health.findings must be a list")
1072
+ functions: list[dict[str, object]] = []
1073
+ for item in findings:
1074
+ row = _object(item, "function")
1075
+ if not isinstance(row.get("path"), str) or not isinstance(row.get("name"), str):
1076
+ raise ValueError("Fallow functions require path and name")
1077
+ path = row["path"]
1078
+ assert isinstance(path, str)
1079
+ _source_path(path, files, scope)
1080
+ _integer(row.get("line"), "function.line")
1081
+ if row.get("col") is not None:
1082
+ _integer(row["col"], "function.col")
1083
+ functions.append(
1084
+ {
1085
+ key: row.get(key)
1086
+ for key in ("path", "name", "line", "col", "cognitive", "cyclomatic", "line_count")
1087
+ }
1088
+ )
1089
+ for key in ("cognitive", "cyclomatic", "line_count"):
1090
+ _integer(row.get(key), f"function.{key}")
1091
+ summary_value = health.get("summary", {})
1092
+ summary = _object(summary_value, "fallow.health.summary")
1093
+ scoped_files = {
1094
+ path
1095
+ for path in files
1096
+ if any(fnmatch.fnmatchcase(path, pattern) for pattern in scope)
1097
+ and not any(
1098
+ fnmatch.fnmatchcase(path, pattern)
1099
+ for pattern in _strings(config.get("ignorePatterns", []), "Fallow ignored paths")
1100
+ )
1101
+ and (config.get("production") is not True or not _fallow_production_excluded(path))
1102
+ }
1103
+ supported_suffixes = {".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"}
1104
+ scoped_source_files = {
1105
+ path for path in scoped_files if Path(path).suffix.lower() in supported_suffixes
1106
+ }
1107
+ unsupported_source_files = {
1108
+ path
1109
+ for path in scoped_files
1110
+ if Path(path).suffix.lower() in _SOURCE_SUFFIXES
1111
+ and Path(path).suffix.lower() not in supported_suffixes
1112
+ }
1113
+ identities = {(row["path"], row["name"], row["line"], row["col"]) for row in functions}
1114
+ complete_complexity = (
1115
+ bool(scoped_source_files)
1116
+ and not unsupported_source_files
1117
+ and all(type(summary.get(key)) is int for key in ("functions_analyzed", "files_analyzed"))
1118
+ and summary.get("functions_analyzed") == len(functions)
1119
+ and len(identities) == len(functions)
1120
+ and summary.get("files_analyzed") == len(scoped_source_files)
1121
+ )
1122
+ duplicate_stats = _object(dupes.get("stats", {}), "fallow.dupes.stats")
1123
+ dead_counts = {key: _list_count(check, key) for key in _DEAD_FIELDS}
1124
+ values = list(dead_counts.values())
1125
+ total_dead = sum(value for value in values if value is not None) if None not in values else None
1126
+ boundaries = ("boundary_violations", "boundary_coverage_violations", "boundary_call_violations")
1127
+ boundary_counts = [_list_count(check, key) for key in boundaries]
1128
+ boundary_total = (
1129
+ sum(value for value in boundary_counts if value is not None)
1130
+ if None not in boundary_counts and boundary_rules
1131
+ else None
1132
+ )
1133
+ # A separate discovery receipt from the same explicit config closes the file population.
1134
+ discovery = report.get("discovery")
1135
+ discovered = discovery.get("files") if isinstance(discovery, dict) else None
1136
+ entries = check.get("entry_points")
1137
+ rooted = (
1138
+ isinstance(entries, dict) and type(entries.get("total")) is int and entries["total"] > 0
1139
+ )
1140
+ complete_graph = (
1141
+ bool(scoped_source_files)
1142
+ and not unsupported_source_files
1143
+ and isinstance(discovered, list)
1144
+ and all(isinstance(path, str) for path in discovered)
1145
+ and isinstance(discovery, dict)
1146
+ and discovery.get("file_count") == len(discovered)
1147
+ and len(discovered) == len(set(discovered))
1148
+ and set(discovered) == scoped_source_files
1149
+ and check.get("unresolved_imports") == []
1150
+ and rooted
1151
+ )
1152
+ validation = report.get("syntax_validation")
1153
+ syntax_valid = (
1154
+ isinstance(validation, dict)
1155
+ and validation.get("config_digest") == config_digest
1156
+ and isinstance(discovered, list)
1157
+ and all(isinstance(path, str) for path in discovered)
1158
+ and set(discovered) == scoped_source_files
1159
+ and isinstance(discovery, dict)
1160
+ and discovery.get("file_count") == len(discovered)
1161
+ and len(discovered) == len(set(discovered))
1162
+ and _validated_fallow_inputs(report, config, root, scoped_source_files)
1163
+ )
1164
+ if not syntax_valid:
1165
+ complete_complexity = complete_graph = False
1166
+
1167
+ def graph_incomplete_reason() -> str | None:
1168
+ if complete_graph:
1169
+ return None
1170
+ prefix = "dead-code graph is incomplete: "
1171
+ if not scoped_source_files:
1172
+ return prefix + "no scoped source files found"
1173
+ if unsupported_source_files:
1174
+ paths = sorted(unsupported_source_files)
1175
+ preview = ", ".join(paths[:3])
1176
+ if len(paths) > 3:
1177
+ preview += f", and {len(paths) - 3} more"
1178
+ return prefix + f"unsupported source files in scope ({preview})"
1179
+ if not (
1180
+ isinstance(discovered, list)
1181
+ and all(isinstance(path, str) for path in discovered)
1182
+ and isinstance(discovery, dict)
1183
+ and discovery.get("file_count") == len(discovered)
1184
+ and len(discovered) == len(set(discovered))
1185
+ and set(discovered) == scoped_source_files
1186
+ ):
1187
+ return prefix + "the discovered files differ from the scoped source files"
1188
+ unresolved = check.get("unresolved_imports")
1189
+ if unresolved != []:
1190
+ if not isinstance(unresolved, list) or not unresolved:
1191
+ return prefix + "unresolved imports were not reported as an empty list"
1192
+ imports = [
1193
+ f"{item.get('path', 'unknown file')} imports {item.get('specifier', 'unknown target')}"
1194
+ if isinstance(item, dict)
1195
+ else "unknown import"
1196
+ for item in unresolved[:3]
1197
+ ]
1198
+ if len(unresolved) > 3:
1199
+ imports.append(f"and {len(unresolved) - 3} more")
1200
+ noun = "import" if len(unresolved) == 1 else "imports"
1201
+ return prefix + f"{len(unresolved)} unresolved {noun} ({', '.join(imports)})"
1202
+ if not rooted:
1203
+ return prefix + "no entry point found"
1204
+ return prefix + "syntax validation failed"
1205
+
1206
+ graph_reason = graph_incomplete_reason()
1207
+ eligible = duplicate_stats.get("total_files")
1208
+ groups = dupes.get("clone_groups")
1209
+ complete_clones = (
1210
+ bool(scoped_source_files)
1211
+ and not unsupported_source_files
1212
+ and syntax_valid
1213
+ and type(eligible) is int
1214
+ and 0 <= eligible <= len(scoped_source_files)
1215
+ and isinstance(groups, list)
1216
+ and duplicate_stats.get("clone_groups") == len(groups)
1217
+ )
1218
+ return {
1219
+ **_function_population(functions),
1220
+ "complexity_complete_for_declared_scope": complete_complexity,
1221
+ "complexity_detection": "complete for declared source scope"
1222
+ if complete_complexity
1223
+ else "reported functions only; configured thresholds and overrides can omit functions",
1224
+ "scoped_supported_files": sorted(scoped_source_files),
1225
+ "scoped_unsupported_files": sorted(unsupported_source_files),
1226
+ "dead_code_complete_for_declared_scope": complete_graph,
1227
+ **({"dead_code_incomplete_reason": graph_reason} if graph_reason is not None else {}),
1228
+ "cycles_complete_for_declared_scope": complete_graph,
1229
+ "boundary_violations_complete_for_declared_scope": complete_graph and boundary_rules,
1230
+ "graph_detection": "matching discovery population, declared entries, no unresolved imports; static findings only"
1231
+ if complete_graph
1232
+ else "reported findings only; scoped graph population or import resolution incomplete",
1233
+ "clone_groups_complete_for_declared_scope": complete_clones,
1234
+ "clone_detection": "complete configured Fallow analysis; eligibility count producer-reported"
1235
+ if complete_clones
1236
+ else "reported findings only; scoped clone-file population is unsupported or unmeasured",
1237
+ "dead_code": total_dead,
1238
+ "dead_code_categories": dead_counts,
1239
+ "dead_finding_identities": _dead_identities(check, files, root),
1240
+ "clone_input_file_count": len(scoped_source_files),
1241
+ "clone_eligible_stream_count": eligible,
1242
+ "clone_finding_identities": _clone_identities(dupes, files, root),
1243
+ "clone_groups": _list_count(dupes, "clone_groups"),
1244
+ "cycles": _list_count(check, "circular_dependencies"),
1245
+ "boundary_violations": boundary_total,
1246
+ "boundary_rules_declared": boundary_rules,
1247
+ "unresolved_imports": _list_count(check, "unresolved_imports"),
1248
+ }
1249
+
1250
+
1251
+ _EVIDENCE_KINDS = ("fallow", "analysis", "coverage")
1252
+
1253
+
1254
+ def evaluate_evidence(
1255
+ snapshot_root: Path,
1256
+ evidence_refs: dict[str, str] | None,
1257
+ base_dir: Path,
1258
+ ) -> dict[str, object]:
1259
+ """Import bound reports, without running analyzers or mutating submitted source."""
1260
+ snapshot = inspect_snapshot(snapshot_root)
1261
+ refs = evidence_refs or {}
1262
+ files = set(_object(snapshot["files"], "snapshot files"))
1263
+ if set(refs) - set(_EVIDENCE_KINDS):
1264
+ raise ValueError("unsupported evidence kind")
1265
+ result: dict[str, object] = {"snapshot_digest": snapshot["digest"]}
1266
+ for kind in _EVIDENCE_KINDS:
1267
+ if kind not in refs:
1268
+ result[kind] = {
1269
+ "status": "unavailable",
1270
+ "reason": "no source-bound report",
1271
+ "metrics": None,
1272
+ }
1273
+ continue
1274
+ envelope_path = base_dir / refs[kind]
1275
+ envelope = _load(envelope_path)
1276
+ required = {
1277
+ "schema_version",
1278
+ "snapshot_digest",
1279
+ "tool",
1280
+ "tool_version",
1281
+ "config",
1282
+ "config_digest",
1283
+ "scope",
1284
+ "report",
1285
+ "report_digest",
1286
+ }
1287
+ if required - set(envelope) or set(envelope) - required - {"boundary_rules_declared"}:
1288
+ raise ValueError(f"invalid {kind} evidence envelope fields")
1289
+ if (
1290
+ type(envelope["schema_version"]) is not int
1291
+ or envelope["schema_version"] != 1
1292
+ or envelope["snapshot_digest"] != snapshot["digest"]
1293
+ ):
1294
+ raise ValueError(f"{kind} evidence does not bind to this snapshot")
1295
+ for key in ("tool", "tool_version", "config", "config_digest", "report", "report_digest"):
1296
+ if not isinstance(envelope[key], str) or not envelope[key]:
1297
+ raise ValueError(f"{kind}.{key} must be a nonempty string")
1298
+ scope = _strings(envelope["scope"], f"{kind}.scope")
1299
+ if not scope:
1300
+ raise ValueError(f"{kind} analysis scope must be explicit")
1301
+ config_name = envelope["config"]
1302
+ assert isinstance(config_name, str)
1303
+ config_path = envelope_path.parent / config_name
1304
+ if _digest(config_path.read_bytes()) != envelope["config_digest"]:
1305
+ raise ValueError(f"{kind} config digest mismatch")
1306
+ config = _load(config_path)
1307
+ if config.get("extends"):
1308
+ raise ValueError("evidence requires fully resolved JSON configuration without extends")
1309
+ report_name = envelope["report"]
1310
+ assert isinstance(report_name, str)
1311
+ report_path = envelope_path.parent / report_name
1312
+ if _digest(report_path.read_bytes()) != envelope["report_digest"]:
1313
+ raise ValueError(f"{kind} report digest mismatch")
1314
+ report = _load(report_path)
1315
+ boundary_rules = envelope.get("boundary_rules_declared", False)
1316
+ if not isinstance(boundary_rules, bool):
1317
+ raise ValueError("boundary_rules_declared must be boolean")
1318
+ if kind == "fallow" and boundary_rules:
1319
+ boundaries = _object(config.get("boundaries", {}), "config.boundaries")
1320
+ calls = _object(boundaries.get("calls", {}), "config.boundaries.calls")
1321
+ coverage = _object(boundaries.get("coverage", {}), "config.boundaries.coverage")
1322
+ boundary_rules = bool(
1323
+ boundaries.get("preset")
1324
+ or boundaries.get("rules")
1325
+ or calls.get("forbidden")
1326
+ or coverage.get("requireAllFiles")
1327
+ )
1328
+ if kind == "fallow":
1329
+ if report.get("version") != envelope["tool_version"] or envelope["tool"] != "fallow":
1330
+ raise ValueError("Fallow report version/tool disagrees with evidence envelope")
1331
+ metrics = _fallow(
1332
+ report,
1333
+ boundary_rules,
1334
+ files,
1335
+ scope,
1336
+ snapshot_root,
1337
+ config,
1338
+ str(envelope["config_digest"]),
1339
+ )
1340
+ elif kind == "analysis":
1341
+ if envelope["tool"] != "language-analysis" or envelope["tool_version"] != report.get(
1342
+ "version"
1343
+ ):
1344
+ raise ValueError("language analysis envelope disagrees with report")
1345
+ metrics = _analysis(report, files, scope, snapshot_root, config)
1346
+ elif kind == "coverage":
1347
+ if report.get("clock") != config.get("clock"):
1348
+ raise ValueError("coverage clock differs from its bound configuration")
1349
+ if report.get("source_scope") is not None and sorted(
1350
+ _strings(report["source_scope"], "coverage source scope")
1351
+ ) != sorted(scope):
1352
+ raise ValueError("coverage source scope differs from its bound envelope")
1353
+ metrics = _coverage(report, files, scope)
1354
+ provenance = {
1355
+ key: envelope[key] for key in ("tool", "tool_version", "config_digest", "scope")
1356
+ }
1357
+ if kind == "analysis":
1358
+ provenance.update(
1359
+ language=report.get("language"),
1360
+ analyzers=report.get("tools"),
1361
+ metric_settings=report.get("settings"),
1362
+ )
1363
+ if kind == "fallow" and isinstance(report.get("syntax_validation"), dict):
1364
+ validation = _object(report["syntax_validation"], "syntax validation")
1365
+ provenance["syntax_observer"] = {
1366
+ key: validation.get(key)
1367
+ for key in (
1368
+ "parser",
1369
+ "version",
1370
+ "commit",
1371
+ "parser_options",
1372
+ "lock_digest",
1373
+ "dependency_hashes",
1374
+ "validator_files",
1375
+ )
1376
+ }
1377
+ result[kind] = {
1378
+ "status": "available",
1379
+ "provenance": provenance,
1380
+ "metrics": metrics,
1381
+ "evidence_basis": "externally supplied source-bound report; execution not independently attested",
1382
+ }
1383
+ return result
1384
+
1385
+
1386
+ @dataclass(frozen=True)
1387
+ class EvidenceBinding:
1388
+ snapshot: Path
1389
+ report: Path
1390
+ config: Path
1391
+ tool: str
1392
+ tool_version: str
1393
+ scope: tuple[str, ...]
1394
+ boundary_rules_declared: bool = False
1395
+
1396
+
1397
+ def bind_evidence(binding: EvidenceBinding) -> dict[str, object]:
1398
+ """Build a receipt for existing local reports; this does not attest tool execution."""
1399
+ config = _load(binding.config)
1400
+ if config.get("extends"):
1401
+ raise ValueError("evidence requires fully resolved JSON configuration without extends")
1402
+ if not binding.tool or not binding.tool_version or not binding.scope:
1403
+ raise ValueError("tool, version and source scope must be explicit")
1404
+ return {
1405
+ "schema_version": 1,
1406
+ "snapshot_digest": inspect_snapshot(binding.snapshot)["digest"],
1407
+ "tool": binding.tool,
1408
+ "tool_version": binding.tool_version,
1409
+ "config": str(binding.config.resolve()),
1410
+ "config_digest": _digest(binding.config.read_bytes()),
1411
+ "scope": list(binding.scope),
1412
+ "report": str(binding.report.resolve()),
1413
+ "report_digest": _digest(binding.report.read_bytes()),
1414
+ "boundary_rules_declared": binding.boundary_rules_declared,
1415
+ }
1416
+
1417
+
1418
+ def _function_population(functions: list[dict[str, object]]) -> dict[str, object]:
1419
+ return {
1420
+ "functions": functions,
1421
+ "function_count_reported": len(functions),
1422
+ "complexity_distribution": {
1423
+ key: sorted(_integer(row[key], key) for row in functions if row.get(key) is not None)
1424
+ for key in ("cognitive", "cyclomatic", "line_count")
1425
+ },
1426
+ **{
1427
+ f"max_{name}_reported": max(
1428
+ (_integer(row[field], field) for row in functions if row.get(field) is not None),
1429
+ default=None,
1430
+ )
1431
+ if all(row.get(field) is not None for row in functions)
1432
+ else None
1433
+ for name, field in (
1434
+ ("cognitive", "cognitive"),
1435
+ ("cyclomatic", "cyclomatic"),
1436
+ ("unit_size", "line_count"),
1437
+ )
1438
+ },
1439
+ }
1440
+
1441
+
1442
+ def _overlaps_change(row: dict[str, object], ranges: object) -> bool:
1443
+ if not isinstance(ranges, list):
1444
+ return False
1445
+ first = _integer(row["line"], "function.line")
1446
+ last = first + _integer(row["line_count"], "function.line_count") - 1
1447
+ for item in ranges:
1448
+ if not isinstance(item, list) or len(item) != 2:
1449
+ raise ValueError("changed-line range requires start and count")
1450
+ start, count = (_integer(value, "changed-line range") for value in item)
1451
+ # A zero-width edit is after start, so insertions before/after a function
1452
+ # do not turn an unchanged, shifted declaration into a changed function.
1453
+ if (count and first <= start + count - 1 and start <= last) or (
1454
+ count == 0 and first <= start < last
1455
+ ):
1456
+ return True
1457
+ return False
1458
+
1459
+
1460
+ HOTSPOT_COGNITIVE = 15
1461
+
1462
+
1463
+ def _code_added(
1464
+ *,
1465
+ before_all: list[dict[str, object]],
1466
+ after_all: list[dict[str, object]],
1467
+ changed_paths: set[str],
1468
+ new_paths: list[str],
1469
+ complete: bool,
1470
+ ) -> dict[str, object]:
1471
+ """Net source complexity and structure an attempt added, independent of inherited code."""
1472
+ method = "all source functions in changed files after the edit minus the same files before; unchanged functions cancel, new functions count in full, deleted ones subtract; tests and Go closure rows inside a declaration (already in its score) excluded"
1473
+ limitations = "added and grown functions match on path and name with line numbers removed; nested functions, overloads, renames and Go methods sharing a name on different receivers in one file can misclassify them; net sums do not depend on matching"
1474
+ if not complete:
1475
+ return {
1476
+ "status": "incomplete",
1477
+ "complete_for_declared_scope": False,
1478
+ "reason": "complexity population incomplete on one side",
1479
+ "method": method,
1480
+ "limitations": limitations,
1481
+ }
1482
+
1483
+ def enclosed(row: dict[str, object], rows: list[dict[str, object]]) -> bool:
1484
+ # Go closure rows inside another function are already counted in that function's score.
1485
+ if not str(row["name"]).startswith("closure@"):
1486
+ return False
1487
+ first = _integer(row["line"], "function line")
1488
+ return any(
1489
+ other is not row
1490
+ and other["path"] == row["path"]
1491
+ and _integer(other["line"], "function line")
1492
+ <= first
1493
+ < _integer(other["line"], "function line") + _integer(other["line_count"], "line count")
1494
+ and (other["line"], other["line_count"]) != (row["line"], row["line_count"])
1495
+ for other in rows
1496
+ )
1497
+
1498
+ def source(rows: list[dict[str, object]]) -> list[dict[str, object]]:
1499
+ return [
1500
+ row
1501
+ for row in rows
1502
+ if str(row["path"]) in changed_paths
1503
+ and not _is_test_path(str(row["path"]))
1504
+ and not enclosed(row, rows)
1505
+ ]
1506
+
1507
+ def identity(row: dict[str, object]) -> tuple[str, str]:
1508
+ # Go method names embed their line ("method@47:Name"); moving a method must not rename it.
1509
+ return (str(row["path"]), re.sub(r"@\d+(?::\d+)?:?", "@", str(row["name"])))
1510
+
1511
+ def total(rows: list[dict[str, object]], key: str) -> int:
1512
+ return sum(_integer(row[key], key) for row in rows)
1513
+
1514
+ before_rows, after_rows = source(before_all), source(after_all)
1515
+ previous: dict[tuple[str, str], int] = {}
1516
+ for row in before_rows:
1517
+ previous[identity(row)] = max(
1518
+ previous.get(identity(row), -1), _integer(row["cognitive"], "cognitive")
1519
+ )
1520
+ analyzed_paths = {str(row["path"]) for row in after_rows}
1521
+ return {
1522
+ "status": "available",
1523
+ "net_cognitive": total(after_rows, "cognitive") - total(before_rows, "cognitive"),
1524
+ "net_cyclomatic": total(after_rows, "cyclomatic") - total(before_rows, "cyclomatic"),
1525
+ "functions_added": sum(identity(row) not in previous for row in after_rows),
1526
+ "source_files_added": sum(
1527
+ _is_source_path(path) and path in analyzed_paths for path in new_paths
1528
+ ),
1529
+ "hotspots_added": sum(
1530
+ _integer(row["cognitive"], "cognitive") > HOTSPOT_COGNITIVE
1531
+ and _integer(row["cognitive"], "cognitive") > previous.get(identity(row), -1)
1532
+ for row in after_rows
1533
+ ),
1534
+ "hotspot_threshold": HOTSPOT_COGNITIVE,
1535
+ "function_count_reported": len(after_rows),
1536
+ "complete_for_declared_scope": True,
1537
+ "method": method,
1538
+ "limitations": limitations,
1539
+ }
1540
+
1541
+
1542
+ def _function_changes(
1543
+ left: dict[str, object], right: dict[str, object], change: dict[str, object] | None
1544
+ ) -> dict[str, object]:
1545
+ unavailable = {"status": "unavailable", "reason": "bound source diff or function rows missing"}
1546
+ if change is None or not all(isinstance(row.get("functions"), list) for row in (left, right)):
1547
+ return {
1548
+ "changed_functions": unavailable,
1549
+ "new_file_functions": unavailable,
1550
+ "code_added": unavailable,
1551
+ }
1552
+ ranges = _object(change.get("changed_line_ranges"), "changed-line ranges")
1553
+ populations = []
1554
+ complete = all(
1555
+ row.get("complexity_complete_for_declared_scope") is True for row in (left, right)
1556
+ )
1557
+ for side, metrics in (("before", left), ("after", right)):
1558
+ raw = metrics["functions"]
1559
+ assert isinstance(raw, list)
1560
+ functions = [_object(row, "function") for row in raw]
1561
+ changed = [
1562
+ row
1563
+ for row in functions
1564
+ if _overlaps_change(
1565
+ row, _object(ranges.get(str(row["path"]), {}), "file ranges").get(side)
1566
+ )
1567
+ ]
1568
+ populations.append(
1569
+ {
1570
+ **_function_population(changed),
1571
+ "complete_for_declared_scope": metrics.get("complexity_complete_for_declared_scope")
1572
+ is True,
1573
+ }
1574
+ )
1575
+ raw_after = right["functions"]
1576
+ assert isinstance(raw_after, list)
1577
+ new_paths = set(_strings(change.get("new_paths"), "new paths"))
1578
+ new_functions = [
1579
+ _object(row, "function")
1580
+ for row in raw_after
1581
+ if _object(row, "function")["path"] in new_paths
1582
+ ]
1583
+ status = "available" if complete else "incomplete"
1584
+ before_all = left["functions"]
1585
+ assert isinstance(before_all, list)
1586
+ return {
1587
+ "code_added": _code_added(
1588
+ before_all=[_object(row, "function") for row in before_all],
1589
+ after_all=[_object(row, "function") for row in raw_after],
1590
+ changed_paths=set(ranges),
1591
+ new_paths=sorted(new_paths),
1592
+ complete=complete,
1593
+ ),
1594
+ "changed_functions": {
1595
+ "status": status,
1596
+ "before": populations[0],
1597
+ "after": populations[1],
1598
+ "method": "reported function line..line+line_count-1 overlaps a zero-context Git hunk on that snapshot; zero-width edits strictly inside the span count; includes tests when scoped",
1599
+ "limitations": "line-level overlap, not AST identity or semantic change; enclosing/nested spans may both count, same-line changes may overcount, moves/renames count as changes; before/after populations are not matched functions",
1600
+ },
1601
+ "new_file_functions": {
1602
+ "status": "available"
1603
+ if right.get("complexity_complete_for_declared_scope") is True
1604
+ else "incomplete",
1605
+ "after": {
1606
+ **_function_population(new_functions),
1607
+ "complete_for_declared_scope": right.get("complexity_complete_for_declared_scope")
1608
+ is True,
1609
+ },
1610
+ "paths": sorted(new_paths),
1611
+ "method": "reported functions in paths absent from before; no new-function classification inside existing files",
1612
+ },
1613
+ }
1614
+
1615
+
1616
+ def _dead_changes(left: dict[str, object], right: dict[str, object]) -> dict[str, object]:
1617
+ identities = [row.get("dead_finding_identities") for row in (left, right)]
1618
+ if not all(
1619
+ row.get("dead_code_complete_for_declared_scope") is True for row in (left, right)
1620
+ ) or not all(isinstance(value, list) for value in identities):
1621
+ return {
1622
+ "status": "unavailable",
1623
+ "reason": "complete graph populations and unambiguous declaration identities required on both snapshots",
1624
+ "introduced_count": None,
1625
+ "resolved_count": None,
1626
+ }
1627
+ bases = []
1628
+ for items in identities:
1629
+ assert isinstance(items, list)
1630
+ if any(_object(row, "dead identity").get("declaration_bound") is False for row in items):
1631
+ return {
1632
+ "status": "unavailable",
1633
+ "reason": "Python candidates lack unique source-bound lexical declarations",
1634
+ "introduced_count": None,
1635
+ "resolved_count": None,
1636
+ }
1637
+ bases.append(
1638
+ [
1639
+ {
1640
+ key: value
1641
+ for key, value in _object(row, "dead identity").items()
1642
+ if key not in {"declaration_context", "declaration_context_required"}
1643
+ }
1644
+ for row in items
1645
+ ]
1646
+ )
1647
+ collisions = {
1648
+ identity
1649
+ for group in bases
1650
+ for identity, count in Counter(json.dumps(row, sort_keys=True) for row in group).items()
1651
+ if count > 1
1652
+ }
1653
+ for items, group in zip(identities, bases, strict=True):
1654
+ assert isinstance(items, list)
1655
+ collisions.update(
1656
+ json.dumps(base, sort_keys=True)
1657
+ for row, base in zip(items, group, strict=True)
1658
+ if _object(row, "dead identity").get("declaration_context_required") is True
1659
+ )
1660
+ sets = []
1661
+ for items, group in zip(identities, bases, strict=True):
1662
+ assert isinstance(items, list)
1663
+ matched = {}
1664
+ for row, base in zip(items, group, strict=True):
1665
+ identity = dict(base)
1666
+ if json.dumps(base, sort_keys=True) in collisions:
1667
+ context = _object(row, "dead identity").get("declaration_context")
1668
+ if not isinstance(context, str):
1669
+ return {
1670
+ "status": "unavailable",
1671
+ "reason": "colliding declarations lack bound unique context",
1672
+ "introduced_count": None,
1673
+ "resolved_count": None,
1674
+ }
1675
+ identity["declaration_context"] = context
1676
+ encoded = json.dumps(identity, sort_keys=True)
1677
+ if encoded in matched:
1678
+ return {
1679
+ "status": "unavailable",
1680
+ "reason": "colliding declarations lack bound unique context",
1681
+ "introduced_count": None,
1682
+ "resolved_count": None,
1683
+ }
1684
+ matched[encoded] = identity
1685
+ sets.append(matched)
1686
+ introduced = [sets[1][key] for key in sorted(sets[1].keys() - sets[0].keys())]
1687
+ resolved = [sets[0][key] for key in sorted(sets[0].keys() - sets[1].keys())]
1688
+ return {
1689
+ "status": "available",
1690
+ "introduced": introduced,
1691
+ "resolved": resolved,
1692
+ "introduced_count": len(introduced),
1693
+ "resolved_count": len(resolved),
1694
+ "before_count": len(sets[0]),
1695
+ "after_count": len(sets[1]),
1696
+ "method": "category plus declaration path/symbol fields; Python uses AST-bound lexical scopes, structural overload/lambda identities, and declaration roles; colliding identities in either snapshot or Python source additionally use unique bound declaration-line context, excluding coordinates/actions; matching tool/version/config/scope and complete static graph populations required",
1697
+ "limitations": "static finding changes only, not semantic deadness; moves/renames and changed colliding declaration-line content change identity; identical collision contexts remain unknown; externally used APIs and runtime-only references are outside this evidence",
1698
+ }
1699
+
1700
+
1701
+ def _clone_changes(left: dict[str, object], right: dict[str, object]) -> dict[str, object]:
1702
+ before, after = left.get("clone_finding_identities"), right.get("clone_finding_identities")
1703
+ if (
1704
+ not left.get("clone_groups_complete_for_declared_scope")
1705
+ or not right.get("clone_groups_complete_for_declared_scope")
1706
+ or not isinstance(before, list)
1707
+ or not isinstance(after, list)
1708
+ ):
1709
+ return {
1710
+ "status": "unavailable",
1711
+ "introduced_count": None,
1712
+ "resolved_count": None,
1713
+ "reason": "complete configured analysis or unique source-bound clone identities missing",
1714
+ }
1715
+ return {
1716
+ "status": "available",
1717
+ "introduced_count": len(set(after) - set(before)),
1718
+ "resolved_count": len(set(before) - set(after)),
1719
+ "before_count": len(before),
1720
+ "after_count": len(after),
1721
+ "method": "exact reported clone-group path/content membership, independent of line shifts",
1722
+ "limitations": [
1723
+ "Fragment or membership edits change group identity; these are configured static findings, not semantic redundancy."
1724
+ ],
1725
+ }
1726
+
1727
+
1728
+ def _coverage_unit_comparison(
1729
+ left: dict[str, object], right: dict[str, object], unit: str
1730
+ ) -> dict[str, object]:
1731
+ kind = {
1732
+ "line": "lines",
1733
+ "statement": "statements",
1734
+ "branch": "branches",
1735
+ "function": "functions",
1736
+ }[unit]
1737
+ reasons = []
1738
+ for side, metrics in (("before", left), ("after", right)):
1739
+ limits = _object(metrics.get("unavailable_populations", {}), "unavailable populations")
1740
+ limit = _object(limits.get(kind, {}), "coverage unit limit")
1741
+ complete = metrics.get(
1742
+ f"{unit}_population_complete", metrics.get("population_complete_for_declared_scope")
1743
+ )
1744
+ reason = None
1745
+ if complete is not True and limit.get("reason"):
1746
+ reason = limit["reason"]
1747
+ elif metrics.get(f"{unit}_denominator_digest") is None:
1748
+ reason = (
1749
+ f"{unit} population not supplied by {metrics.get('format') or 'this report format'}"
1750
+ )
1751
+ elif complete is not True:
1752
+ reason = f"{unit} declared source population is partial or completeness is unproven"
1753
+ if reason is not None:
1754
+ reasons.append(f"{side}: {reason}")
1755
+ key = f"{unit}_denominator_digest"
1756
+ if not reasons and (not isinstance(left.get(key), str) or left.get(key) != right.get(key)):
1757
+ reasons.append("fixed denominator differs or is unbound")
1758
+ return {
1759
+ "status": "unavailable" if reasons else "available",
1760
+ "reason": "; ".join(reasons) if reasons else None,
1761
+ }
1762
+
1763
+
1764
+ def _numbit_coordinates(files: Mapping[str, object]) -> set[str]:
1765
+ from pluginbench.coverage_report import numbits_to_lines
1766
+
1767
+ return {
1768
+ f"{path}:{line}"
1769
+ for path, encoded in files.items()
1770
+ for line in numbits_to_lines(str(encoded))
1771
+ }
1772
+
1773
+
1774
+ def _added_source_lines(change: dict[str, object]) -> set[str]:
1775
+ ranges = _object(change.get("changed_line_ranges"), "changed-line ranges")
1776
+ lines: set[str] = set()
1777
+ for path, sides in ranges.items():
1778
+ if not _is_source_path(path):
1779
+ continue
1780
+ after = _object(sides, "file ranges").get("after") or []
1781
+ if not isinstance(after, list):
1782
+ raise ValueError("changed-line ranges require a list")
1783
+ for item in after:
1784
+ if not isinstance(item, list) or len(item) != 2:
1785
+ raise ValueError("changed-line range requires start and count")
1786
+ start, count = (_integer(value, "changed-line range") for value in item)
1787
+ lines.update(f"{path}:{line}" for line in range(start, start + count))
1788
+ return lines
1789
+
1790
+
1791
+ def _changed_line_coverage(
1792
+ left: dict[str, object], right: dict[str, object], change: dict[str, object] | None
1793
+ ) -> dict[str, object]:
1794
+ """Coverage of the attempt's own added source lines, and added tests that add no unique coverage."""
1795
+ coordinates = right.get("coordinates")
1796
+ if change is None or not isinstance(coordinates, dict) or "line" not in coordinates:
1797
+ return {"status": "unavailable", "reason": "bound source diff or line coordinates missing"}
1798
+ added = _added_source_lines(change)
1799
+ unmeasured = sorted(
1800
+ set(_strings(right.get("unmeasured_source_files", []), "unmeasured source files"))
1801
+ & {line.rsplit(":", 1)[0] for line in added}
1802
+ )
1803
+ if unmeasured:
1804
+ # Dropping an untested added file from the report would otherwise raise the rate.
1805
+ return {
1806
+ "status": "incomplete",
1807
+ "reason": "added source files missing from the coverage report: "
1808
+ + ", ".join(unmeasured),
1809
+ }
1810
+ result: dict[str, object] = {
1811
+ "status": "available",
1812
+ "method": "added source lines from the bound diff intersected with the submission's executable and executed coordinates; branches belong to the added line they start on",
1813
+ "limitations": "executing a line does not prove its behavior is asserted; redundant tests are candidates: equal coverage does not establish equal assertions",
1814
+ }
1815
+ for unit in ("line", "branch"):
1816
+ population = _object(coordinates.get(unit, {}), unit)
1817
+ if not population:
1818
+ result.update({f"{unit}_total": None, f"{unit}_covered": None, f"{unit}_percent": None})
1819
+ continue
1820
+
1821
+ def on_added_line(coordinate: str) -> bool:
1822
+ path, _, rest = coordinate.partition(":")
1823
+ return f"{path}:{rest.split(':')[0]}" in added
1824
+
1825
+ total = {item for item in _strings(population["total"], unit) if on_added_line(item)}
1826
+ covered = {item for item in _strings(population["covered"], unit) if item in total}
1827
+ result.update(
1828
+ {
1829
+ f"{unit}_total": len(total),
1830
+ f"{unit}_covered": len(covered),
1831
+ f"{unit}_percent": _percent(len(covered), len(total)),
1832
+ }
1833
+ )
1834
+ result["added_executable_lines"] = result.pop("line_total")
1835
+ result["added_lines_covered"] = result.pop("line_covered")
1836
+ before_tests, after_tests = left.get("per_test_numbits"), right.get("per_test_numbits")
1837
+ if not isinstance(before_tests, dict) or not isinstance(after_tests, dict):
1838
+ result.update(
1839
+ added_tests=None,
1840
+ redundant_added_tests=None,
1841
+ redundant_added_test_names=None,
1842
+ added_tests_without_added_lines=None,
1843
+ redundancy_reason="per-test line coverage was not recorded for both snapshots",
1844
+ )
1845
+ return result
1846
+ reached = {
1847
+ name: _numbit_coordinates(_object(files, f"per_test_numbits.{name}")) & added
1848
+ for name, files in after_tests.items()
1849
+ }
1850
+ new_tests = sorted(set(after_tests) - set(before_tests))
1851
+ reaching = [name for name in new_tests if reached[name]]
1852
+ # Flag the smallest tests first and drop each flagged test from the pool, so removing every
1853
+ # flagged test together keeps added-line coverage unchanged (one of two duplicates stays).
1854
+ remaining = dict(reached)
1855
+ redundant = []
1856
+ for name in sorted(reaching, key=lambda item: (len(reached[item]), item)):
1857
+ others = set().union(*(lines for other, lines in remaining.items() if other != name))
1858
+ if reached[name] <= others:
1859
+ redundant.append(name)
1860
+ del remaining[name]
1861
+ result.update(
1862
+ added_tests=len(new_tests),
1863
+ redundant_added_tests=len(redundant),
1864
+ redundant_added_test_names=sorted(redundant),
1865
+ added_tests_without_added_lines=len(new_tests) - len(reaching),
1866
+ )
1867
+ return result
1868
+
1869
+
1870
+ def compare_evidence(
1871
+ before: dict[str, object], after: dict[str, object], *, change: dict[str, object] | None = None
1872
+ ) -> dict[str, object]:
1873
+ """Compare measurements only with compatible collection and denominator provenance."""
1874
+ result: dict[str, object] = {}
1875
+ if change is not None and (
1876
+ change.get("before_digest") != before.get("snapshot_digest")
1877
+ or change.get("after_digest") != after.get("snapshot_digest")
1878
+ ):
1879
+ raise ValueError("changed-line diff does not bind to evidence snapshots")
1880
+ for kind in _EVIDENCE_KINDS:
1881
+ left = _object(before.get(kind, {}), f"before.{kind}")
1882
+ right = _object(after.get(kind, {}), f"after.{kind}")
1883
+ if not isinstance(left.get("metrics"), dict) or not isinstance(right.get("metrics"), dict):
1884
+ result[kind] = {
1885
+ "status": "unavailable",
1886
+ "reason": "before or after evidence missing",
1887
+ "deltas": None,
1888
+ }
1889
+ continue
1890
+ left_provenance = _object(left.get("provenance"), "before provenance")
1891
+ right_provenance = _object(right.get("provenance"), "after provenance")
1892
+ if left_provenance != right_provenance:
1893
+ result[kind] = {
1894
+ "status": "incomplete",
1895
+ "reason": "tool/version/config/scope differ",
1896
+ "deltas": None,
1897
+ }
1898
+ continue
1899
+ left_metrics = _object(left["metrics"], "before metrics")
1900
+ right_metrics = _object(right["metrics"], "after metrics")
1901
+ deltas: dict[str, float] = {}
1902
+ coverage_units: dict[str, object] = {}
1903
+ if kind == "coverage":
1904
+ for unit in ("line", "statement", "branch", "function"):
1905
+ coverage_units[unit] = _coverage_unit_comparison(left_metrics, right_metrics, unit)
1906
+ for key, left_value in left_metrics.items():
1907
+ if kind == "coverage":
1908
+ unit = key.partition("_")[0]
1909
+ if (
1910
+ unit not in coverage_units
1911
+ or _object(coverage_units[unit], unit)["status"] != "available"
1912
+ ):
1913
+ continue
1914
+ right_value = right_metrics.get(key)
1915
+ if (
1916
+ isinstance(left_value, (int, float))
1917
+ and not isinstance(left_value, bool)
1918
+ and isinstance(right_value, (int, float))
1919
+ and not isinstance(right_value, bool)
1920
+ ):
1921
+ deltas[key] = float(right_value) - float(left_value)
1922
+ result[kind] = {
1923
+ "status": "available",
1924
+ "deltas": deltas,
1925
+ "interpretation": "net changes in compatible reported metrics; not identity-grounded newly introduced findings",
1926
+ }
1927
+ if kind == "coverage":
1928
+ result[kind] = {
1929
+ **_object(result[kind], kind),
1930
+ "units": coverage_units,
1931
+ "changed_lines": _changed_line_coverage(left_metrics, right_metrics, change),
1932
+ }
1933
+ if not any(
1934
+ _object(unit, "coverage unit")["status"] == "available"
1935
+ for unit in coverage_units.values()
1936
+ ):
1937
+ result[kind] = {
1938
+ **_object(result[kind], kind),
1939
+ "status": "incomplete",
1940
+ "reason": "no compatible complete coverage populations; see per-unit reasons",
1941
+ "deltas": None,
1942
+ }
1943
+ if kind in {"fallow", "analysis"}:
1944
+ comparison = _object(result[kind], "static analysis comparison")
1945
+ comparison.update(_function_changes(left_metrics, right_metrics, change))
1946
+ comparison["dead_findings"] = _dead_changes(left_metrics, right_metrics)
1947
+ if kind == "analysis":
1948
+ dead = _object(comparison["dead_findings"], "unused candidate comparison")
1949
+ dead["limitations"] = (
1950
+ "candidate changes under the fixed named analyzer/build settings; not proof of semantic deadness or absence; renamed symbols/scopes, changed decorators/overload signatures/lambda expressions, and changed collision contexts can change identity; indistinguishable redefinitions, same-line declarations, and comprehension scopes remain unknown"
1951
+ )
1952
+ comparison["dead_findings"] = dead
1953
+ if kind == "fallow":
1954
+ comparison["clone_findings"] = _clone_changes(left_metrics, right_metrics)
1955
+ result[kind] = comparison
1956
+ return result