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 +3 -0
- harnessdoc/check.py +89 -0
- harnessdoc/cli.py +703 -0
- harnessdoc/compare.py +192 -0
- harnessdoc/data/__init__.py +0 -0
- harnessdoc/data/pricing.json +148 -0
- harnessdoc/experiment.py +404 -0
- harnessdoc/goals.py +102 -0
- harnessdoc/guard.py +106 -0
- harnessdoc/interceptor.py +191 -0
- harnessdoc/log.py +84 -0
- harnessdoc/mcp_server.py +338 -0
- harnessdoc/measure.py +127 -0
- harnessdoc/pricing.py +285 -0
- harnessdoc/report.py +239 -0
- harnessdoc/rules.py +85 -0
- harnessdoc/scan/__init__.py +152 -0
- harnessdoc/scan/checks.py +274 -0
- harnessdoc/scan/model.py +88 -0
- harnessdoc/scan/pyast.py +628 -0
- harnessdoc/sessions.py +317 -0
- harnessdoc/skill/SKILL.md +97 -0
- harnessdoc/usage/__init__.py +7 -0
- harnessdoc/usage/analyze.py +243 -0
- harnessdoc/usage/loader.py +120 -0
- harnessdoc/usage/records.py +299 -0
- harnessdoc-0.1.0.dist-info/METADATA +331 -0
- harnessdoc-0.1.0.dist-info/RECORD +31 -0
- harnessdoc-0.1.0.dist-info/WHEEL +4 -0
- harnessdoc-0.1.0.dist-info/entry_points.txt +2 -0
- harnessdoc-0.1.0.dist-info/licenses/LICENSE +21 -0
harnessdoc/__init__.py
ADDED
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
|