utils/agent_ready/report.py
Krzysztof kuhy Rudnicki a0fb279b1b agent_ready: AgentReady v2.0 checker + repo_contract web-site gate
New agent_ready/ package (dist and live-url modes, 100% branch coverage)
checks robots, sitemap, llms.txt, ai-catalog, 404, no-JS text, markdown
mirrors and JSON-LD. repo_contract gains web-repo detection and installs the
agent-ready pre-commit hook, CI workflow and check_agent_ready.sh shim;
scripts/check_agent_ready.sh is the shared entry point.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBScELfMUiieK9ManFbS3Y
2026-10-22 21:18:49 +02:00

74 lines
2.3 KiB
Python

"""Result lines (``PASS|FAIL|SKIP AR-XXX-NN msg``) and the exit-code policy."""
from __future__ import annotations
import sys
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TextIO
EXIT_OK, EXIT_FAIL, EXIT_USAGE, EXIT_NETWORK = 0, 1, 2, 3
class Undeterminable(Exception):
"""A network-level failure (refused, timeout, deadline): no verdict."""
@dataclass
class Report:
"""Collects check lines and decides the exit code.
Every in-scope item is required for kuhy's sites, so a FAIL of any
strength (MUST/SHOULD/MAY) fails the run. SKIP never fails it.
"""
out: TextIO = field(default_factory=lambda: sys.stdout)
fails: int = 0
undetermined: int = 0
def emit(self, status: str, ar_id: str, msg: str) -> None:
"""Print one result line."""
if status == "FAIL":
self.fails += 1
print(f"{status} {ar_id} {msg}", file=self.out, flush=True)
def ok(self, ar_id: str, msg: str) -> None:
"""Emit PASS."""
self.emit("PASS", ar_id, msg)
def fail(self, ar_id: str, msg: str) -> None:
"""Emit FAIL."""
self.emit("FAIL", ar_id, msg)
def skip(self, ar_id: str, msg: str) -> None:
"""Emit SKIP."""
self.emit("SKIP", ar_id, msg)
def verdict(self, ar_id: str, problems: list[str], pass_msg: str) -> bool:
"""FAIL with the problems joined, or PASS with ``pass_msg``."""
if problems:
self.fail(ar_id, "; ".join(problems))
return False
self.ok(ar_id, pass_msg)
return True
def guard(self, ar_id: str, fn: Callable[[], object]) -> None:
"""Run a check; a network failure becomes an undeterminable SKIP."""
try:
fn()
except Undeterminable as exc:
self.undetermined += 1
self.skip(ar_id, f"undeterminable (network): {exc}")
def exit_code(self) -> int:
"""0 all pass, 1 any fail, 3 nothing failed but something undeterminable."""
if self.fails:
return EXIT_FAIL
return EXIT_NETWORK if self.undetermined else EXIT_OK
def summarize(items: list[str], limit: int = 4) -> str:
"""Join up to ``limit`` items and note how many more there were."""
head = ", ".join(items[:limit])
return head + (f" (+{len(items) - limit} more)" if len(items) > limit else "")