mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 18:07:24 +01:00
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
74 lines
2.3 KiB
Python
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 "")
|