Files
EVEngine/scripts/check_quality_metadata.py
T

447 lines
15 KiB
Python

#!/usr/bin/env python3
"""Validate the repository's bounded quality-debt allowlist.
The check covers four intentionally narrow inventories:
* ``binding_gap``: non-comment entries in ``check_bindings_gaps.txt``;
* ``todo``: explicit TODO/FIXME/HACK markers in repository source;
* ``soft_skip``: explicit ``soft-skip``/``soft skip`` markers;
* ``fallback``: explicit ``FALLBACK`` markers or fallback wording in comments.
Every inventory entry must match exactly one metadata scope with an owner,
issue, reason, expiry, and an explicit ``max_net_growth`` budget. The baseline
stores the count for each metadata entry. A normal check rejects unmatched
findings and any count increase above its budget; deleting old debt is allowed.
This keeps the gate bounded without requiring a one-shot cleanup of legacy
debt.
Usage::
python3 scripts/check_quality_metadata.py
python3 scripts/check_quality_metadata.py --json
python3 scripts/check_quality_metadata.py --write-baseline
``--write-baseline`` is an explicit maintenance operation. It is not used by
CI, because changing a baseline must remain visible in code review.
"""
from __future__ import annotations
import argparse
import fnmatch
import json
import re
import sys
from collections import Counter
from dataclasses import asdict, dataclass
from datetime import date
from pathlib import Path
from typing import Any, Iterable, Mapping
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_METADATA = ROOT / "scripts" / "quality_debt_allowlist.json"
DEFAULT_BASELINE = ROOT / "scripts" / "quality_debt_baseline.json"
DEFAULT_BINDING_GAPS = ROOT / "scripts" / "check_bindings_gaps.txt"
SCHEMA_VERSION = 1
KINDS = frozenset({"binding_gap", "todo", "soft_skip", "fallback"})
SOURCE_EXTENSIONS = frozenset(
{
".c",
".cc",
".cpp",
".cxx",
".h",
".hh",
".hpp",
".hxx",
".cmake",
".frag",
".glsl",
".nut",
".ps1",
".py",
".sh",
".toml",
".vert",
".yml",
".yaml",
}
)
EXCLUDED_DIRS = frozenset(
{
".git",
".codex",
".agents",
".opencode",
"build",
"docs",
"external",
"third-party",
"fixtures_quality_metadata",
}
)
EXCLUDED_FILES = frozenset(
{
"scripts/check_quality_metadata.py",
"scripts/profile_matrix.py",
"scripts/tests/test_quality_metadata.py",
}
)
CONTROLLED_ROOTS = (
"CMakeLists.txt",
"Makefile",
"cmake",
"scripts",
"src",
"test",
".github",
)
TODO_RE = re.compile(r"\b(?:TODO|FIXME|HACK)\b")
SOFT_SKIP_RE = re.compile(r"\bsoft[- ]skip\b", re.IGNORECASE)
FALLBACK_RE = re.compile(r"\bfallback\b", re.IGNORECASE)
@dataclass(frozen=True)
class Finding:
"""One inventory finding, identified by kind, path, and source line."""
kind: str
path: str
line: int
text: str
def _relative(root: Path, path: Path) -> str:
return path.relative_to(root).as_posix()
def _source_file(path: Path, relative: str) -> bool:
if path.name in {"Makefile", "CMakeLists.txt"}:
return True
return path.suffix.lower() in SOURCE_EXTENSIONS and relative not in EXCLUDED_FILES
def iter_source_files(root: Path) -> Iterable[tuple[Path, str]]:
"""Yield source/config files in the controlled scan area."""
candidates: list[Path] = []
for name in CONTROLLED_ROOTS:
candidate = root / name
if candidate.is_file():
candidates.append(candidate)
elif candidate.is_dir():
candidates.extend(candidate.rglob("*"))
for path in sorted(candidates):
if not path.is_file():
continue
relative = _relative(root, path)
if any(part in EXCLUDED_DIRS for part in Path(relative).parts):
continue
if _source_file(path, relative):
yield path, relative
def scan_source_findings(root: Path) -> list[Finding]:
findings: list[Finding] = []
for path, relative in iter_source_files(root):
text = path.read_text(encoding="utf-8", errors="replace")
for line_number, line in enumerate(text.splitlines(), start=1):
if TODO_RE.search(line):
findings.append(Finding("todo", relative, line_number, line.strip()))
if SOFT_SKIP_RE.search(line):
findings.append(Finding("soft_skip", relative, line_number, line.strip()))
# A lowercase ``fallback`` in an ordinary identifier or parameter
# is not a debt finding. Uppercase FALLBACK is an explicit marker;
# otherwise require the term to occur in a comment line.
stripped = line.lstrip()
in_comment = stripped.startswith(("//", "#", "/*", "*"))
if "FALLBACK" in line or (in_comment and FALLBACK_RE.search(line)):
findings.append(Finding("fallback", relative, line_number, line.strip()))
return findings
def scan_binding_gaps(root: Path, binding_path: Path) -> list[Finding]:
if not binding_path.is_absolute():
binding_path = root / binding_path
if not binding_path.is_file():
return []
relative = _relative(root, binding_path)
findings: list[Finding] = []
for line_number, line in enumerate(
binding_path.read_text(encoding="utf-8", errors="replace").splitlines(), start=1
):
stripped = line.strip()
if stripped and not stripped.startswith("#"):
findings.append(Finding("binding_gap", relative, line_number, stripped))
return findings
def discover_findings(root: Path, binding_path: Path) -> list[Finding]:
return scan_source_findings(root) + scan_binding_gaps(root, binding_path)
def _nonempty_string(value: Any) -> bool:
return isinstance(value, str) and bool(value.strip())
def validate_metadata(metadata: Any, today: date | None = None) -> list[str]:
today = today or date.today()
errors: list[str] = []
if not isinstance(metadata, Mapping):
return ["metadata root must be an object"]
if metadata.get("schema_version") != SCHEMA_VERSION:
errors.append(f"metadata schema_version must be {SCHEMA_VERSION}")
entries = metadata.get("entries")
if not isinstance(entries, list):
return errors + ["metadata entries must be an array"]
seen: set[str] = set()
required = {
"id",
"kind",
"scope",
"owner",
"issue",
"reason",
"expiry",
"max_net_growth",
}
for index, entry in enumerate(entries):
prefix = f"metadata entries[{index}]"
if not isinstance(entry, Mapping):
errors.append(f"{prefix} must be an object")
continue
missing = sorted(required - set(entry))
if missing:
errors.append(f"{prefix} missing: {', '.join(missing)}")
continue
for field in ("id", "scope", "owner", "issue", "reason"):
if not _nonempty_string(entry.get(field)):
errors.append(f"{prefix}.{field} must be a non-empty string")
entry_id = entry.get("id")
if isinstance(entry_id, str) and entry_id in seen:
errors.append(f"{prefix}.id duplicates '{entry_id}'")
if isinstance(entry_id, str):
seen.add(entry_id)
if entry.get("kind") not in KINDS:
errors.append(f"{prefix}.kind must be one of {', '.join(sorted(KINDS))}")
growth = entry.get("max_net_growth")
if isinstance(growth, bool) or not isinstance(growth, int) or growth < 0:
errors.append(f"{prefix}.max_net_growth must be a non-negative integer")
expiry = entry.get("expiry")
if not isinstance(expiry, str):
errors.append(f"{prefix}.expiry must be ISO date YYYY-MM-DD")
else:
try:
expiry_date = date.fromisoformat(expiry)
except ValueError:
errors.append(f"{prefix}.expiry is not an ISO date: {expiry}")
else:
if expiry_date < today:
errors.append(f"{prefix}.expiry has passed: {expiry}")
return errors
def validate_baseline(baseline: Any) -> list[str]:
if not isinstance(baseline, Mapping):
return ["baseline root must be an object"]
errors: list[str] = []
if baseline.get("schema_version") != SCHEMA_VERSION:
errors.append(f"baseline schema_version must be {SCHEMA_VERSION}")
counts = baseline.get("counts")
if not isinstance(counts, Mapping):
errors.append("baseline counts must be an object")
else:
for key, value in counts.items():
if not isinstance(key, str) or isinstance(value, bool) or not isinstance(value, int) or value < 0:
errors.append(f"baseline count {key!r} must be a non-negative integer")
return errors
def _load_json(path: Path) -> Any:
try:
return json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
raise ValueError(f"file not found: {path}")
except json.JSONDecodeError as error:
raise ValueError(f"invalid JSON in {path}: {error}")
def _entries(metadata: Mapping[str, Any]) -> list[Mapping[str, Any]]:
return [entry for entry in metadata.get("entries", []) if isinstance(entry, Mapping)]
def _matching_entries(finding: Finding, entries: list[Mapping[str, Any]]) -> list[Mapping[str, Any]]:
return [
entry
for entry in entries
if entry.get("kind") == finding.kind
and isinstance(entry.get("scope"), str)
and fnmatch.fnmatchcase(finding.path, entry["scope"])
]
def evaluate(
metadata: Mapping[str, Any],
baseline: Mapping[str, Any],
findings: list[Finding],
today: date | None = None,
) -> tuple[list[str], dict[str, int]]:
"""Return gate errors and the current count per metadata entry id."""
errors = validate_metadata(metadata, today=today)
errors.extend(validate_baseline(baseline))
entries = _entries(metadata)
counts: Counter[str] = Counter()
entry_ids = {entry.get("id") for entry in entries}
baseline_counts = baseline.get("counts", {})
if not isinstance(baseline_counts, Mapping):
baseline_counts = {}
for finding in findings:
matches = _matching_entries(finding, entries)
if not matches:
errors.append(
f"unallowlisted {finding.kind} at {finding.path}:{finding.line}: {finding.text}"
)
continue
if len(matches) > 1:
ids = ", ".join(str(entry.get("id")) for entry in matches)
errors.append(
f"ambiguous metadata for {finding.kind} at {finding.path}:{finding.line}: {ids}"
)
continue
entry_id = matches[0].get("id")
if isinstance(entry_id, str):
counts[entry_id] += 1
if isinstance(baseline_counts, Mapping):
unknown_baseline = sorted(set(baseline_counts) - entry_ids)
for entry_id in unknown_baseline:
errors.append(f"baseline has no metadata entry for '{entry_id}'")
for entry in entries:
entry_id = entry.get("id")
if not isinstance(entry_id, str):
continue
if entry_id not in baseline_counts:
errors.append(f"baseline has no count for metadata entry '{entry_id}'")
continue
current = counts.get(entry_id, 0)
previous = baseline_counts[entry_id]
if not isinstance(previous, int) or isinstance(previous, bool):
continue
delta = current - previous
budget = entry.get("max_net_growth", 0)
if isinstance(budget, int) and not isinstance(budget, bool) and delta > budget:
errors.append(
f"{entry_id}: net growth {delta} exceeds budget {budget} "
f"(baseline={previous}, current={current})"
)
return errors, dict(counts)
def write_baseline(path: Path, counts: Mapping[str, int]) -> None:
payload = {
"schema_version": SCHEMA_VERSION,
"generated_at": date.today().isoformat(),
"counts": dict(sorted(counts.items())),
}
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def _report(
errors: list[str], findings: list[Finding], counts: Mapping[str, int], as_json: bool
) -> int:
if as_json:
print(
json.dumps(
{
"findings": [asdict(finding) for finding in findings],
"counts": dict(sorted(counts.items())),
"errors": errors,
},
ensure_ascii=False,
indent=2,
)
)
else:
for error in errors:
print(f"FAIL {error}", file=sys.stderr)
print(
"quality metadata: "
+ ", ".join(f"{kind}={counts.get(kind, 0)}" for kind in sorted(counts))
+ f"; findings={len(findings)}"
)
return 1 if errors else 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=ROOT)
parser.add_argument("--metadata", type=Path, default=DEFAULT_METADATA)
parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE)
parser.add_argument("--binding-gaps", type=Path, default=DEFAULT_BINDING_GAPS)
parser.add_argument("--json", action="store_true", help="emit machine-readable output")
parser.add_argument(
"--write-baseline",
action="store_true",
help="write current matched counts to --baseline (explicit maintenance operation)",
)
args = parser.parse_args(argv)
try:
metadata = _load_json(args.metadata)
except ValueError as error:
print(f"FAIL {error}", file=sys.stderr)
return 1
if not isinstance(metadata, Mapping):
print("FAIL metadata root must be an object", file=sys.stderr)
return 1
binding_path = args.binding_gaps
if not binding_path.is_absolute():
binding_path = args.root / binding_path
findings = discover_findings(args.root, binding_path)
metadata_errors: list[str] = []
if not binding_path.is_file():
metadata_errors.append(f"binding gap file not found: {binding_path}")
if args.write_baseline:
metadata_errors.extend(validate_metadata(metadata))
entries = _entries(metadata)
counts: Counter[str] = Counter()
for finding in findings:
matches = _matching_entries(finding, entries)
if len(matches) != 1 or not isinstance(matches[0].get("id"), str):
metadata_errors.append(
f"cannot baseline unallowlisted/ambiguous finding: "
f"{finding.kind} at {finding.path}:{finding.line}"
)
else:
counts[matches[0]["id"]] += 1
if metadata_errors:
return _report(metadata_errors, findings, counts, args.json)
write_baseline(args.baseline, counts)
print(f"wrote quality debt baseline: {args.baseline}")
return 0
try:
baseline = _load_json(args.baseline)
except ValueError as error:
print(f"FAIL {error}", file=sys.stderr)
return 1
if not isinstance(baseline, Mapping):
print("FAIL baseline root must be an object", file=sys.stderr)
return 1
errors, counts = evaluate(metadata, baseline, findings)
errors = metadata_errors + errors
return _report(errors, findings, counts, args.json)
if __name__ == "__main__":
sys.exit(main())