Skip to content

Commit b56f571

Browse files
authored
Merge pull request #63 from IntentProof/tiered-coverage-policy
Adopt tiered coverage gates
2 parents f7fcbca + b0bc499 commit b56f571

11 files changed

Lines changed: 261 additions & 37 deletions

.github/workflows/ci.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,4 @@ jobs:
2222
pip install -e ".[dev]"
2323
2424
- name: Run tests with coverage
25-
run: pytest -q
26-
27-
- name: Enforce coverage threshold
28-
run: bash ./scripts/check-coverage.sh 95
25+
run: bash ./scripts/run-coverage-gate.sh

.github/workflows/release-pypi.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,8 @@ jobs:
4242
- name: Install dependencies
4343
run: pip install -e ".[dev]"
4444

45-
- name: Run tests with coverage
46-
run: pytest -q
47-
4845
- name: Enforce coverage threshold
49-
run: bash ./scripts/check-coverage.sh 95
46+
run: bash ./scripts/run-coverage-gate.sh
5047

5148
build:
5249
name: "IntentProof Release: Build Python Distributions"

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ vectors. Run `pytest` locally before publishing.
3737

3838
```bash
3939
pytest
40-
bash ./scripts/check-coverage.sh 95
40+
bash ./scripts/run-coverage-gate.sh
4141
```
4242

43-
CI enforces at least 95% line coverage on `src/intentproof/` (see
44-
`pyproject.toml` and `scripts/check-coverage.sh`).
43+
Tiered coverage: **90%** total and **95%** on `src/intentproof/` (see
44+
`scripts/README-coverage-tiers.md`).
4545

4646
## Release
4747

pyproject.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ source = ["intentproof"]
3636
omit = []
3737

3838
[tool.coverage.report]
39-
fail_under = 95
4039
show_missing = true
4140
skip_empty = true
4241

@@ -46,5 +45,4 @@ pythonpath = ["src"]
4645
addopts = [
4746
"--cov=intentproof",
4847
"--cov-report=term-missing",
49-
"--cov-fail-under=95",
5048
]

scripts/README-coverage-tiers.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Tiered coverage policy (intentproof-sdk-python)
2+
3+
| Tier | Minimum | Scope |
4+
|------|---------|--------|
5+
| **Total** | 90% | `src/intentproof/` |
6+
| **Critical** | 95% | Same — public SDK surface |
7+
8+
Configuration: `scripts/coverage-tiers.conf`.

scripts/check-coverage.sh

Lines changed: 118 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,136 @@
11
#!/usr/bin/env bash
2+
# Tiered statement coverage using coverage.py JSON output.
3+
#
4+
# Usage: check-coverage.sh [coverage_json]
5+
# Default: coverage.json (run pytest with --cov first)
26

37
set -euo pipefail
48

5-
MIN_COVERAGE="${1:-95}"
9+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
10+
CONF="${ROOT}/scripts/coverage-tiers.conf"
11+
COVERAGE_JSON_ARG="${1:-coverage.json}"
612

7-
COVERAGE=()
8-
if command -v coverage >/dev/null 2>&1; then
9-
COVERAGE=(coverage)
13+
if [[ "$COVERAGE_JSON_ARG" == /* ]]; then
14+
COVERAGE_JSON_PATH="$COVERAGE_JSON_ARG"
1015
else
11-
for py in python python3; do
12-
if command -v "$py" >/dev/null 2>&1 && "$py" -m coverage --version >/dev/null 2>&1; then
13-
COVERAGE=("$py" -m coverage)
14-
break
15-
fi
16-
done
16+
COVERAGE_JSON_PATH="${ROOT}/${COVERAGE_JSON_ARG}"
1717
fi
1818

19-
if [[ ${#COVERAGE[@]} -eq 0 ]]; then
20-
echo "coverage CLI not found; install dev deps with: pip install -e \".[dev]\"" >&2
19+
if [[ ! -f "$CONF" ]]; then
20+
echo "coverage tiers config not found: $CONF" >&2
2121
exit 2
2222
fi
2323

24-
TOTAL_LINE="$("${COVERAGE[@]}" report --include='src/intentproof/*' | awk '/^TOTAL/{print; exit}')"
25-
if [[ -z "$TOTAL_LINE" ]]; then
26-
echo "unable to read total coverage; run pytest with --cov first" >&2
24+
# shellcheck disable=SC1090
25+
source "$CONF"
26+
27+
if [[ -z "${TOTAL_MIN:-}" ]]; then
28+
echo "coverage-tiers.conf must set TOTAL_MIN" >&2
29+
exit 2
30+
fi
31+
32+
if [[ ! -f "$COVERAGE_JSON_PATH" ]]; then
33+
echo "coverage json not found: $COVERAGE_JSON_PATH (run pytest with --cov first)" >&2
34+
exit 2
35+
fi
36+
37+
rules_file="$(mktemp)"
38+
trap 'rm -f "$rules_file"' EXIT
39+
printf '%s\n' "${CRITICAL_RULES[@]}" >"$rules_file"
40+
41+
coverage_percent_display() {
42+
awk -v c="$1" -v t="$2" 'BEGIN {
43+
if (t == 0) { print "0.0"; exit }
44+
printf "%.1f", int(1000 * c / t) / 10
45+
}'
46+
}
47+
48+
threshold_met() {
49+
awk -v c="$1" -v t="$2" -v min="$3" \
50+
'BEGIN { exit !(t > 0 && c * 100 >= t * min) }'
51+
}
52+
53+
report_threshold() {
54+
local label="$1" covered="$2" total="$3" min="$4"
55+
local pct
56+
pct="$(coverage_percent_display "$covered" "$total")"
57+
echo "${label}: ${pct}% (${covered}/${total} statements), minimum ${min}%"
58+
if threshold_met "$covered" "$total" "$min"; then
59+
echo " PASS"
60+
return 0
61+
fi
62+
echo " FAIL" >&2
63+
return 1
64+
}
65+
66+
stats_for_prefix() {
67+
local prefix="$1"
68+
python3 - "$COVERAGE_JSON_PATH" "$prefix" <<'PY'
69+
import json
70+
import sys
71+
72+
path = sys.argv[1]
73+
prefix = sys.argv[2]
74+
data = json.load(open(path))
75+
covered = 0
76+
total = 0
77+
78+
def covered_statements(summary, entry, stmts):
79+
if "covered_lines" in summary:
80+
return int(summary["covered_lines"])
81+
missing_lines = entry.get("missing_lines")
82+
if isinstance(missing_lines, list):
83+
return stmts - len(missing_lines)
84+
missing_count = summary.get("missing_lines")
85+
if isinstance(missing_count, int):
86+
return stmts - missing_count
87+
return 0
88+
89+
for file_path, entry in data.get("files", {}).items():
90+
if prefix not in file_path.replace("\\", "/"):
91+
continue
92+
summary = entry.get("summary", {})
93+
stmts = int(summary.get("num_statements", 0))
94+
if stmts == 0:
95+
continue
96+
total += stmts
97+
covered += covered_statements(summary, entry, stmts)
98+
print(covered, total)
99+
PY
100+
}
101+
102+
read -r TOTAL_COVERED TOTAL_STMTS <<EOF
103+
$(stats_for_prefix "")
104+
EOF
105+
106+
if [[ -z "$TOTAL_STMTS" || "$TOTAL_STMTS" -eq 0 ]]; then
107+
echo "unable to read total coverage from $COVERAGE_JSON_PATH" >&2
27108
exit 2
28109
fi
29110

30-
TOTAL_PERCENT="$(printf '%s' "$TOTAL_LINE" | awk '{print $NF}' | tr -d '%')"
111+
fail=0
112+
report_threshold "Total coverage" "$TOTAL_COVERED" "$TOTAL_STMTS" "$TOTAL_MIN" || fail=1
31113

32-
echo "Total coverage: ${TOTAL_PERCENT}%"
33-
echo "Minimum required: ${MIN_COVERAGE}%"
114+
echo "Critical tiers:"
115+
while IFS= read -r rule; do
116+
[[ -n "$rule" ]] || continue
117+
min="${rule%%:*}"
118+
prefix="${rule#*:}"
119+
read -r c t <<EOF
120+
$(stats_for_prefix "$prefix")
121+
EOF
122+
if [[ "$t" -eq 0 ]]; then
123+
echo " ${prefix} (min ${min}%): no statements in profile, FAIL" >&2
124+
fail=1
125+
continue
126+
fi
127+
report_threshold " ${prefix}" "$c" "$t" "$min" || fail=1
128+
done <"$rules_file"
34129

35-
if awk -v got="$TOTAL_PERCENT" -v min="$MIN_COVERAGE" 'BEGIN { exit !(got + 0 >= min + 0) }'; then
36-
echo "PASS: coverage threshold met"
37-
exit 0
130+
if [[ "$fail" -ne 0 ]]; then
131+
echo "FAIL: coverage threshold not met" >&2
132+
exit 1
38133
fi
39134

40-
echo "FAIL: coverage threshold not met" >&2
41-
exit 1
135+
echo "PASS: coverage thresholds met"
136+
exit 0
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env bash
2+
# Regression test: critical tiers must fail when a prefix matches nothing.
3+
set -euo pipefail
4+
5+
root="$(mktemp -d)"
6+
trap 'rm -rf "$root"' EXIT
7+
8+
mkdir -p "$root/scripts"
9+
cp "$(dirname "$0")/check-coverage.sh" "$root/scripts/"
10+
11+
cat >"$root/scripts/coverage-tiers.conf" <<'EOF'
12+
TOTAL_MIN=50
13+
CRITICAL_RULES=(
14+
"95:src/intentproof/missing/"
15+
)
16+
EOF
17+
18+
cat >"$root/coverage.json" <<'EOF'
19+
{
20+
"files": {
21+
"src/intentproof/canon.py": {
22+
"summary": {
23+
"covered_lines": 2,
24+
"num_statements": 2,
25+
"percent_covered": 66.66666666666666
26+
}
27+
}
28+
}
29+
}
30+
EOF
31+
32+
output=""
33+
code=0
34+
output="$(cd "$root" && bash scripts/check-coverage.sh coverage.json 2>&1)" || code=$?
35+
36+
if [[ "$code" -ne 1 ]]; then
37+
echo "FAIL: expected exit 1 for missing critical prefix, got ${code}" >&2
38+
echo "$output" >&2
39+
exit 1
40+
fi
41+
42+
if ! grep -q "no statements in profile, FAIL" <<<"$output"; then
43+
echo "FAIL: expected critical-prefix failure message, got:" >&2
44+
echo "$output" >&2
45+
exit 1
46+
fi
47+
48+
echo "PASS: missing critical prefix fails the gate"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env bash
2+
# Regression test: displayed percent must not round up past a failing gate.
3+
set -euo pipefail
4+
5+
pct="$(awk -v c=9399 -v t=10000 'BEGIN {
6+
if (t == 0) { print "0.0"; exit }
7+
printf "%.1f", int(1000 * c / t) / 10
8+
}')"
9+
10+
if [[ "$pct" != "93.9" ]]; then
11+
echo "FAIL: expected truncated display 93.9%, got ${pct}%" >&2
12+
exit 1
13+
fi
14+
15+
if awk -v c=9399 -v t=10000 -v min=94 \
16+
'BEGIN { exit !(t > 0 && c * 100 >= t * min) }'; then
17+
echo "FAIL: expected integer gate to fail at 9399/10000 min 94%" >&2
18+
exit 1
19+
fi
20+
21+
echo "PASS: display percent matches integer gate semantics"
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env bash
2+
# Regression test: absolute and relative coverage JSON paths resolve correctly.
3+
set -euo pipefail
4+
5+
root="$(mktemp -d)"
6+
trap 'rm -rf "$root"' EXIT
7+
8+
mkdir -p "$root/scripts"
9+
cp "$(dirname "$0")/check-coverage.sh" "$root/scripts/"
10+
11+
cat >"$root/scripts/coverage-tiers.conf" <<'EOF'
12+
TOTAL_MIN=50
13+
CRITICAL_RULES=(
14+
"95:src/intentproof/"
15+
)
16+
EOF
17+
18+
cat >"$root/coverage.json" <<'EOF'
19+
{
20+
"files": {
21+
"src/intentproof/canon.py": {
22+
"summary": {
23+
"covered_lines": 2,
24+
"num_statements": 2,
25+
"percent_covered": 100.0
26+
}
27+
}
28+
}
29+
}
30+
EOF
31+
32+
if ! (cd "$root" && bash scripts/check-coverage.sh coverage.json >/dev/null); then
33+
echo "FAIL: relative coverage.json path should pass" >&2
34+
exit 1
35+
fi
36+
37+
if ! bash "$root/scripts/check-coverage.sh" "$root/coverage.json" >/dev/null; then
38+
echo "FAIL: absolute coverage.json path should pass" >&2
39+
exit 1
40+
fi
41+
42+
echo "PASS: coverage JSON path resolution"

scripts/coverage-tiers.conf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# IntentProof tiered coverage (intentproof-sdk-python)
2+
3+
TOTAL_MIN=90
4+
5+
CRITICAL_RULES=(
6+
"95:/intentproof/"
7+
)

0 commit comments

Comments
 (0)