evaluate_names_absent returned PASS when its step exited 0 with no output, so five mandatory checks - the ones asserting that provider API keys, forge credentials, a cluster-admin binding, and shared coordinator state are absent - could report a pass on no evidence and turn a NO_GO into a GO. Both name rules now resolve their step through one guard in _line_step, so zero observations are NOT_RUN. Regressions pin all five real catalog specs plus both reachable silence paths: a POSIX pipeline whose status comes from its last stage, and a drifted kubectl -o jsonpath. The pool claim projection emits one <volume>=<claim> line per template volume so a volume without a PVC still counts as an observation rather than reading as drift. Also closes the review's reachable hardening and evidence defects: - pin Gitea paths to atlas/titan-iac on an exact segment boundary and reject relative segments, including percent-encoded ones - forbid impersonation structurally in every mode and vantage; the inner command of kubectl exec is re-checked rather than exempted, and validate_catalog no longer guards only the operator vantage - drop flux and helm from the binary allowlist; they had no pinned release digest, so no allowlisted binary can now be admitted that the executor would refuse to attest - remove the inert --concurrency and --expect-telegram-sessions flags and the dead concurrency bound; Telegram continuity stays mandatory - read the ephemeral pull index page by page, treat the create response as an authoritative source for the pull number, close every number either source names, and surface residue_ref plus exact manual_cleanup commands when creation is uncertain - keep executable_path and executable_sha256 on unrecorded bulk-evidence steps so withholding bytes never withholds binary attestation - revert the repo-wide hygiene legacy-exception mechanism; the contract change here is purely additive and the four pre-existing over-cap files are left to the canonical contract change in PR #14/#15 - correct the runbook ruff format scope so the documented command passes Split hermes_handoff_arming.py out of hermes_handoff_ephemeral.py to keep both modules under the 500-line cap. All 16 handoff modules hold at least 95% line and branch coverage; the mutation gate is 13/13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
61 lines
2.2 KiB
Python
61 lines
2.2 KiB
Python
"""File-size and naming validation for the managed testing surface."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from collections.abc import Iterable
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
def _expand_globs(root: Path, patterns: Iterable[str]) -> list[Path]:
|
|
"""Expand a set of relative glob patterns to unique file paths."""
|
|
matched: set[Path] = set()
|
|
for pattern in patterns:
|
|
matched.update(path for path in root.glob(pattern) if path.is_file())
|
|
return sorted(matched)
|
|
|
|
|
|
def run_check(contract: dict[str, Any], root: Path) -> list[str]:
|
|
"""Return human-readable issues for naming and file-size rules."""
|
|
config = contract.get("hygiene", {})
|
|
max_lines = int(config.get("max_lines", 500))
|
|
issues: list[str] = []
|
|
|
|
for path in _expand_globs(root, config.get("line_limit_globs", [])):
|
|
line_count = sum(1 for _ in path.open("r", encoding="utf-8"))
|
|
if line_count > max_lines:
|
|
issues.append(f"file exceeds {max_lines} LOC: {path.relative_to(root)} ({line_count})")
|
|
|
|
for rule in config.get("naming_rules", []):
|
|
pattern = re.compile(rule["pattern"])
|
|
for path in _expand_globs(root, [rule["glob"]]):
|
|
if path.name == "conftest.py":
|
|
continue
|
|
if not pattern.match(path.name):
|
|
issues.append(
|
|
f"naming rule failed ({rule['description']}): {path.relative_to(root)}"
|
|
)
|
|
|
|
return issues
|
|
|
|
|
|
def count_files_over_line_limit(contract: dict[str, Any], root: Path) -> int:
|
|
"""Return the number of managed files that exceed the configured LOC cap."""
|
|
|
|
config = contract.get("hygiene", {})
|
|
max_lines = int(config.get("max_lines", 500))
|
|
count = 0
|
|
for path in _expand_globs(root, config.get("line_limit_globs", [])):
|
|
line_count = sum(1 for _ in path.open("r", encoding="utf-8"))
|
|
if line_count > max_lines:
|
|
count += 1
|
|
return count
|
|
|
|
|
|
def count_files_with_line_limit(contract: dict[str, Any], root: Path) -> int:
|
|
"""Return the number of managed files included in the LOC cap."""
|
|
|
|
config = contract.get("hygiene", {})
|
|
return len(_expand_globs(root, config.get("line_limit_globs", [])))
|