harnessdoc 0.1.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.
harnessdoc/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """harnessdoc: measure what each successful task of your AI app costs, and prove the savings."""
2
+
3
+ __version__ = "0.1.0"
harnessdoc/check.py ADDED
@@ -0,0 +1,89 @@
1
+ """CI gate: fail only on *new* findings, so existing debt doesn't block every pull request.
2
+
3
+ harnessdoc check . --write-baseline harnessdoc-baseline.json # once, commit the file
4
+ harnessdoc check . --baseline harnessdoc-baseline.json --fail-on medium # in CI
5
+
6
+ Findings are matched by fingerprint = rule + file + enclosing function (not line number), so editing
7
+ unrelated lines doesn't make old findings look new. Multiplicity is kept: a second identical finding in the
8
+ same function is new.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from collections import Counter
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+
18
+ from .scan import ScanResult
19
+ from .scan.model import Finding
20
+
21
+ SEVERITY = {"info": 0, "low": 1, "medium": 2, "high": 3}
22
+ BASELINE_VERSION = 1
23
+
24
+
25
+ def fingerprint(f: Finding, functions: dict[str, str | None]) -> str:
26
+ fn = functions.get(f.call_site or "", None) or "-"
27
+ return f"{f.rule}:{f.file}:{fn}"
28
+
29
+
30
+ def _functions(scan: ScanResult) -> dict[str, str | None]:
31
+ return {s.id: s.function for s in scan.call_sites}
32
+
33
+
34
+ def baseline_doc(scan: ScanResult) -> dict:
35
+ fns = _functions(scan)
36
+ items = sorted(
37
+ ({"fingerprint": fingerprint(f, fns), "rule": f.rule, "severity": f.severity, "file": f.file,
38
+ "line": f.line, "title": f.title} for f in scan.findings),
39
+ key=lambda d: (d["fingerprint"], d["line"]),
40
+ )
41
+ return {"harnessdoc_baseline": BASELINE_VERSION, "findings": items}
42
+
43
+
44
+ def write_baseline(scan: ScanResult, path: str | Path) -> Path:
45
+ p = Path(path)
46
+ p.write_text(json.dumps(baseline_doc(scan), indent=2, sort_keys=True) + "\n", encoding="utf-8")
47
+ return p
48
+
49
+
50
+ def read_baseline(path: str | Path) -> Counter:
51
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
52
+ if data.get("harnessdoc_baseline") != BASELINE_VERSION:
53
+ raise ValueError(f"{path} is not a harnessdoc baseline (version {BASELINE_VERSION})")
54
+ return Counter(d["fingerprint"] for d in data.get("findings", []))
55
+
56
+
57
+ @dataclass
58
+ class CheckResult:
59
+ new: list[Finding] = field(default_factory=list)
60
+ unchanged: list[Finding] = field(default_factory=list)
61
+ fixed: list[str] = field(default_factory=list) # fingerprints in the baseline that no longer occur
62
+
63
+ def failing(self, fail_on: str) -> list[Finding]:
64
+ if fail_on == "none":
65
+ return []
66
+ return [f for f in self.new if SEVERITY[f.severity] >= SEVERITY[fail_on]]
67
+
68
+ def as_dict(self, fail_on: str) -> dict:
69
+ return {
70
+ "new": [f.as_dict() for f in self.new],
71
+ "unchanged": len(self.unchanged),
72
+ "fixed": self.fixed,
73
+ "failing": [f"{f.rule} {f.file}:{f.line}" for f in self.failing(fail_on)],
74
+ }
75
+
76
+
77
+ def check(scan: ScanResult, baseline: Counter | None) -> CheckResult:
78
+ fns = _functions(scan)
79
+ remaining = Counter(baseline or {})
80
+ res = CheckResult()
81
+ for f in sorted(scan.findings, key=lambda f: (f.file, f.line, f.rule)):
82
+ fp = fingerprint(f, fns)
83
+ if remaining[fp] > 0:
84
+ remaining[fp] -= 1
85
+ res.unchanged.append(f)
86
+ else:
87
+ res.new.append(f)
88
+ res.fixed = sorted(fp for fp, n in remaining.items() for _ in range(n))
89
+ return res