0.1.4-alpha.2.fix.1
This commit is contained in:
136
scripts/audit_text_hygiene.py
Normal file
136
scripts/audit_text_hygiene.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
# file: scripts/audit_text_hygiene.py
|
||||
# version: 1
|
||||
|
||||
"""Validate mechanical hygiene of selected governed text files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
_IGNORED_DIRECTORIES = frozenset({".git", ".idea", ".venv", "__pycache__", "dist", "node_modules", "target"})
|
||||
_TEXT_SUFFIXES = frozenset(
|
||||
{
|
||||
".cfg",
|
||||
".conf",
|
||||
".css",
|
||||
".gradle",
|
||||
".html",
|
||||
".ini",
|
||||
".js",
|
||||
".json",
|
||||
".jsx",
|
||||
".kts",
|
||||
".md",
|
||||
".prefs",
|
||||
".properties",
|
||||
".py",
|
||||
".rs",
|
||||
".scss",
|
||||
".sh",
|
||||
".toml",
|
||||
".ts",
|
||||
".tsx",
|
||||
".txt",
|
||||
".xml",
|
||||
".yaml",
|
||||
".yml",
|
||||
}
|
||||
)
|
||||
_TEXT_NAMES = frozenset({".gitignore", "VERSION"})
|
||||
|
||||
|
||||
def _is_governed_text(path: pathlib.Path) -> bool:
|
||||
return path.name in _TEXT_NAMES or path.suffix.lower() in _TEXT_SUFFIXES
|
||||
|
||||
|
||||
def _selected_files(raw_paths: list[str], root: pathlib.Path) -> tuple[list[pathlib.Path], list[str]]:
|
||||
files: set[pathlib.Path] = set()
|
||||
errors: list[str] = []
|
||||
|
||||
for raw_path in raw_paths:
|
||||
path = pathlib.Path(raw_path)
|
||||
if path.is_dir() or path.is_file():
|
||||
if not path.resolve().is_relative_to(root):
|
||||
errors.append(f"Text hygiene audit: path outside project root: {path}")
|
||||
continue
|
||||
if path.is_dir():
|
||||
candidates = (
|
||||
candidate
|
||||
for candidate in path.rglob("*")
|
||||
if candidate.is_file()
|
||||
and _is_governed_text(candidate)
|
||||
and not any(part in _IGNORED_DIRECTORIES for part in candidate.parts)
|
||||
)
|
||||
elif path.is_file() and _is_governed_text(path):
|
||||
candidates = iter((path,))
|
||||
else:
|
||||
errors.append(f"Text hygiene audit: unsupported or missing path: {path}")
|
||||
continue
|
||||
|
||||
for candidate in candidates:
|
||||
canonical = candidate.resolve()
|
||||
if not canonical.is_relative_to(root):
|
||||
errors.append(f"Text hygiene audit: path outside project root: {candidate}")
|
||||
else:
|
||||
files.add(canonical)
|
||||
|
||||
return sorted(files), errors
|
||||
|
||||
|
||||
def _audit_file(path: pathlib.Path, root: pathlib.Path) -> list[str]:
|
||||
relative = path.relative_to(root).as_posix()
|
||||
data = path.read_bytes()
|
||||
errors: list[str] = []
|
||||
|
||||
if b"\r" in data:
|
||||
errors.append(f"{relative}: CR byte found; governed text must use LF line endings")
|
||||
|
||||
if not data.endswith(b"\n"):
|
||||
errors.append(f"{relative}: EOF: missing final LF")
|
||||
elif data.endswith(b"\n\n"):
|
||||
errors.append(f"{relative}: EOF: extra blank line; exactly one final LF is required")
|
||||
|
||||
for line_number, line in enumerate(data.split(b"\n")[:-1], start=1):
|
||||
if line.endswith((b" ", b"\t")):
|
||||
errors.append(f"{relative}:{line_number}: trailing whitespace")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run the read-only text hygiene audit from the project root."""
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("paths", nargs="+", help="Governed text files or directories to audit")
|
||||
args = parser.parse_args()
|
||||
|
||||
root = pathlib.Path.cwd().resolve()
|
||||
files, selection_errors = _selected_files(args.paths, root)
|
||||
if selection_errors or not files:
|
||||
for error in selection_errors or ["Text hygiene audit: no governed text files selected"]:
|
||||
print(error, file=sys.stderr)
|
||||
return 2
|
||||
|
||||
errors: list[str] = []
|
||||
for path in files:
|
||||
try:
|
||||
errors.extend(_audit_file(path, root))
|
||||
except OSError as error:
|
||||
print(f"Text hygiene audit: cannot read {path}: {error}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
for error in errors:
|
||||
print(error, file=sys.stderr)
|
||||
if errors:
|
||||
print(f"Text hygiene audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"Text hygiene audit: clean ({len(files)} file(s))")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user