quality: add LOC ratchet and platform metrics

This commit is contained in:
Brad Stein 2026-04-17 04:55:27 -03:00
parent aa7098efad
commit 9faabcbc0e
4 changed files with 129 additions and 0 deletions

1
Jenkinsfile vendored
View File

@ -103,6 +103,7 @@ set -euo pipefail
mkdir -p build
python -m pip install --no-cache-dir -r requirements.txt -r requirements-dev.txt
python -m ruff check ariadne --select PLR
python scripts/check_file_sizes.py --roots ariadne scripts tests --max-lines 500 --waivers scripts/loc_hygiene_waivers.tsv
python -m slipcover \
--json \
--out "${COVERAGE_JSON}" \

View File

@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""Enforce a ratcheted source file line-budget contract.
The check fails when:
- a file exceeds the configured line budget and is not allowlisted; or
- an allowlist entry is stale (file removed or now within budget).
"""
from __future__ import annotations
import argparse
from pathlib import Path
def _iter_source_files(roots: list[str], exts: set[str]) -> list[Path]:
files: list[Path] = []
for root_text in roots:
root = Path(root_text)
if not root.exists():
continue
for path in root.rglob("*"):
if not path.is_file():
continue
if path.suffix not in exts:
continue
if "__pycache__" in path.parts or ".venv" in path.parts:
continue
files.append(path.resolve())
return sorted(files)
def _load_waivers(path: Path) -> dict[str, str]:
waivers: dict[str, str] = {}
if not path.exists():
return waivers
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
parts = line.split("\t")
rel_path = parts[0].strip()
reason = parts[1].strip() if len(parts) > 1 else ""
if rel_path:
waivers[rel_path] = reason
return waivers
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--roots", nargs="+", default=["ariadne", "scripts", "tests"])
parser.add_argument("--max-lines", type=int, default=500)
parser.add_argument("--waivers", default="scripts/loc_hygiene_waivers.tsv")
args = parser.parse_args()
repo_root = Path.cwd().resolve()
waivers = _load_waivers(repo_root / args.waivers)
source_files = _iter_source_files(args.roots, {".py", ".sh"})
violations: dict[str, int] = {}
for path in source_files:
rel = path.relative_to(repo_root).as_posix()
lines = len(path.read_text(encoding="utf-8", errors="ignore").splitlines())
if lines > args.max_lines:
violations[rel] = lines
unexpected = sorted(rel for rel in violations if rel not in waivers)
stale = sorted(rel for rel in waivers if rel not in violations)
if not unexpected and not stale:
print(
f"[hygiene] source line budget check passed (limit={args.max_lines}, over_limit={len(violations)}, waivers={len(waivers)})"
)
return 0
if unexpected:
print("[hygiene] files over budget missing from waiver list:")
for rel in unexpected:
print(f"- {rel}: {violations[rel]} lines (limit {args.max_lines})")
if stale:
print("[hygiene] stale waiver entries (remove from waiver list):")
for rel in stale:
print(f"- {rel}")
return 1
if __name__ == "__main__":
raise SystemExit(main())

View File

@ -0,0 +1,14 @@
# relative_path<TAB>why_it_is_allowlisted_for_now
ariadne/app.py core application router/orchestration pending decomposition
ariadne/manager/provisioning.py provisioning workflow hub pending modular extraction
ariadne/services/cluster_state.py legacy cluster-state monolith pending split (tracked by branch scope)
ariadne/services/comms.py legacy comms monolith pending split by concern
ariadne/services/firefly.py firefly integration handlers pending endpoint split
ariadne/services/nextcloud.py nextcloud integration surface pending staged decomposition
ariadne/services/vault.py vault integration flow pending dedicated auth/storage modules
ariadne/services/wger.py wger integration flow pending endpoint-layer split
ariadne/settings.py configuration map pending domain-specific config modules
tests/test_app.py broad integration assertions pending test-suite decomposition
tests/test_keycloak_admin.py keycloak contract tests pending helper extraction
tests/test_provisioning.py provisioning matrix tests pending split by workflow phase
tests/test_services.py service integration matrix pending split by service domain
Can't render this file because it has a wrong number of fields in line 2.

View File

@ -5,10 +5,14 @@ from __future__ import annotations
import json
import os
from pathlib import Path
import sys
import urllib.request
import xml.etree.ElementTree as ET
SOURCE_SCAN_ROOTS = ("ariadne", "scripts", "tests")
SOURCE_EXTENSIONS = {".py", ".sh"}
def _escape_label(value: str) -> str:
return value.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"')
@ -97,7 +101,25 @@ def _fetch_existing_counter(pushgateway_url: str, metric: str, labels: dict[str,
return 0.0
def _count_source_files_over_limit(repo_root: Path, max_lines: int = 500) -> int:
count = 0
for rel_root in SOURCE_SCAN_ROOTS:
base = repo_root / rel_root
if not base.exists():
continue
for path in base.rglob("*"):
if not path.is_file():
continue
if path.suffix not in SOURCE_EXTENSIONS:
continue
lines = len(path.read_text(encoding="utf-8", errors="ignore").splitlines())
if lines > max_lines:
count += 1
return count
def main() -> int:
repo_root = Path(__file__).resolve().parents[1]
coverage_path = os.getenv("COVERAGE_JSON", "build/coverage.json")
junit_path = os.getenv("JUNIT_XML", "build/junit.xml")
pushgateway_url = os.getenv(
@ -114,6 +136,7 @@ def main() -> int:
raise RuntimeError(f"missing junit file {junit_path}")
coverage = _load_coverage(coverage_path)
source_lines_over_500 = _count_source_files_over_limit(repo_root, max_lines=500)
totals = _load_junit(junit_path)
passed = max(totals["tests"] - totals["failures"] - totals["errors"] - totals["skipped"], 0)
@ -154,6 +177,10 @@ def main() -> int:
f'ariadne_quality_gate_tests_total{{suite="{suite}",result="skipped"}} {totals["skipped"]}',
"# TYPE ariadne_quality_gate_coverage_percent gauge",
f'ariadne_quality_gate_coverage_percent{{suite="{suite}"}} {coverage:.3f}',
"# TYPE platform_quality_gate_workspace_line_coverage_percent gauge",
f'platform_quality_gate_workspace_line_coverage_percent{{suite="{suite}"}} {coverage:.3f}',
"# TYPE platform_quality_gate_source_lines_over_500_total gauge",
f'platform_quality_gate_source_lines_over_500_total{{suite="{suite}"}} {source_lines_over_500}',
"# TYPE ariadne_quality_gate_build_info gauge",
f"ariadne_quality_gate_build_info{_label_str(labels)} 1",
]
@ -171,6 +198,7 @@ def main() -> int:
"tests_errors": totals["errors"],
"tests_skipped": totals["skipped"],
"coverage_percent": round(coverage, 3),
"source_lines_over_500": source_lines_over_500,
"ok_counter": ok_count,
"failed_counter": failed_count,
},