#!/usr/bin/env python3 """Fail closed on transport wrappers, merge debris, and retired scratch references.""" from __future__ import annotations import argparse import json from pathlib import Path import re import sys from typing import Any DEFAULT_ROOT = Path(__file__).resolve().parents[2] MANIFEST = Path("harness/source/generation-manifest.json") REPOMIX_IGNORE = Path(".repomixignore") SCHEMA = "source-hygiene-result/v1" TEXT_SUFFIXES = {".json", ".md", ".py", ".toml", ".txt", ".yaml", ".yml"} TRANSPORT_RE = re.compile(r"^\s*]*)?>\s*$", re.IGNORECASE) MERGE_RE = re.compile(r"^(?:<{7}|={7}|>{7})(?:\s|$)") SCRATCH_RE = re.compile( r"(?:\.agents/plugins/wiki-superpowers/scratch(?:/|\b)|" r"(?:analyze_audit_results|build_master_report|build_per_file_findings|" r"extract_findings_details|generate_markdown_tables|generate_sed_proofs)\.py)" ) CONTROL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]") PLACEHOLDER_RE = re.compile(r"\{\{([^{}\n]*)\}\}") REQUIRED_REPOMIX_RULES = { ".agents/plugins/wiki-superpowers/scratch/**", "**/__pycache__/**", "**/*.pyc", } RETIRED_SCRATCH = Path(".agents/plugins/wiki-superpowers/scratch") class HygieneError(ValueError): """The hygiene scan could not be constructed safely.""" def _safe_rel(value: Any, field: str) -> Path: if not isinstance(value, str) or not value: raise HygieneError(f"{field} must be a non-empty repository-relative path") path = Path(value) if path.is_absolute() or ".." in path.parts: raise HygieneError(f"{field} escapes repository: {value}") return path def _manifest_paths(root: Path) -> tuple[set[Path], set[Path]]: manifest_path = root / MANIFEST data = json.loads(manifest_path.read_text(encoding="utf-8")) if data.get("schema_version") != 1: raise HygieneError("generation manifest must use schema_version 1") sources = data.get("sources") if not isinstance(sources, list) or not sources: raise HygieneError("generation manifest sources must be non-empty") neutral: set[Path] = set() for item in sources: if not isinstance(item, dict): raise HygieneError("generation manifest source entry must be an object") neutral.add(_safe_rel(item.get("metadata"), "source metadata")) neutral.add(_safe_rel(item.get("body"), "source body")) neutral.update( path.relative_to(root) for path in (root / "harness/source").rglob("*") if path.is_file() ) targets: set[Path] = set() for metadata in sorted(path for path in neutral if path.suffix == ".json"): absolute = root / metadata document = json.loads(absolute.read_text(encoding="utf-8")) if document.get("source_kind") not in {"workflow", "agent"}: continue raw_targets = document.get("targets") if not isinstance(raw_targets, list): raise HygieneError(f"{metadata}: targets must be a list") for target in raw_targets: if not isinstance(target, dict): raise HygieneError(f"{metadata}: target must be an object") targets.add(_safe_rel(target.get("path"), "generated target")) return neutral, targets def _scan_text(path: Path, relative: Path, *, neutral_source: bool = False, generated_target: bool = False) -> list[dict[str, Any]]: if path.suffix.lower() not in TEXT_SUFFIXES: return [] text = path.read_text(encoding="utf-8") findings: list[dict[str, Any]] = [] for line_number, line in enumerate(text.splitlines(), 1): code: str | None = None if TRANSPORT_RE.fullmatch(line): code = "TRANSPORT_WRAPPER" elif MERGE_RE.match(line): code = "MERGE_MARKER" elif SCRATCH_RE.search(line): code = "SCRATCH_REFERENCE" if code: findings.append( { "code": code, "path": relative.as_posix(), "line": line_number, } ) for match in CONTROL_RE.finditer(text): findings.append( { "code": "FORBIDDEN_CONTROL_CHARACTER", "path": relative.as_posix(), "line": text.count("\n", 0, match.start()) + 1, "codepoint": f"U+{ord(match.group(0)):04X}", } ) for match in PLACEHOLDER_RE.finditer(text): placeholder = match.group(1).strip().casefold() if generated_target and placeholder == "arguments": findings.append( { "code": "UNRESOLVED_GENERATOR_PLACEHOLDER", "path": relative.as_posix(), "line": text.count("\n", 0, match.start()) + 1, } ) elif neutral_source and placeholder != "arguments": findings.append( { "code": "UNAPPROVED_NEUTRAL_PLACEHOLDER", "path": relative.as_posix(), "line": text.count("\n", 0, match.start()) + 1, "placeholder": match.group(0), } ) return findings def _content_roots(root: Path) -> list[Path]: """콘텐츠 실파일이 놓인 활성 write root 를 레이아웃 계약에서 읽어 돌려준다. "vault" 를 하드코딩하면 canonical 모드에서만 맞고, compatibility/shadow(cutover 이전) 에서는 콘텐츠가 raw/·wiki/ 실파일이라 스캔 대상이 0건이 되어 hygiene 검사가 조용히 아무것도 안 훑는다. 모드에 맞는 write root(canonical→vault, 그 외→raw·wiki)만 골라 심링크 이중 스캔도 피한다. 레이아웃을 못 읽으면 존재하는 표준 콘텐츠 루트로 폴백한다. """ try: import layout_check authority = layout_check.resolve_authority(root, require_clean=False) roots = [root / r for r in authority.get("write_roots", []) if isinstance(r, str)] roots = [r for r in roots if r.is_dir()] if roots: return roots except Exception: pass if (root / "vault").is_dir(): return [root / "vault"] return [root / name for name in ("raw", "wiki") if (root / name).is_dir()] def check(root: Path) -> dict[str, Any]: root = root.resolve(strict=True) neutral, targets = _manifest_paths(root) # 콘텐츠 문서도 transport 잔여물 검사 대상이다. 서브에이전트가 작성한 문서에 # `` 같은 wrapper 가 남는 사례가 실제로 발생했으므로 활성 write root 를 훑는다. content: set[Path] = { path.relative_to(root) for base in _content_roots(root) for path in base.rglob("*.md") if path.is_file() } findings: list[dict[str, Any]] = [] scanned = 0 for relative in sorted(neutral | targets | content, key=lambda item: item.as_posix()): absolute = root / relative if not absolute.is_file(): findings.append({"code": "MISSING_CONTEXT_FILE", "path": relative.as_posix()}) continue scanned += 1 findings.extend( _scan_text( absolute, relative, neutral_source=relative in neutral, generated_target=relative in targets, ) ) ignore_path = root / REPOMIX_IGNORE if not ignore_path.is_file(): findings.append({"code": "MISSING_REPOMIX_IGNORE", "path": REPOMIX_IGNORE.as_posix()}) else: rules = { line.strip() for line in ignore_path.read_text(encoding="utf-8").splitlines() if line.strip() and not line.lstrip().startswith("#") } for rule in sorted(REQUIRED_REPOMIX_RULES - rules): findings.append( { "code": "MISSING_REPOMIX_EXCLUSION", "path": REPOMIX_IGNORE.as_posix(), "rule": rule, } ) scratch = root / RETIRED_SCRATCH if scratch.is_dir(): for path in sorted(scratch.rglob("*")): if path.is_file() and path.suffix.lower() in TEXT_SUFFIXES: findings.append( { "code": "ACTIVE_SCRATCH_FILE", "path": path.relative_to(root).as_posix(), } ) findings.sort(key=lambda item: (item.get("path", ""), item.get("line", 0), item["code"])) return { "schema_version": SCHEMA, "status": "PASS" if not findings else "FAIL", "scanned_files": scanned, "findings": findings, } def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--check", action="store_true", help="explicit no-write mode") parser.add_argument("--root", type=Path, default=DEFAULT_ROOT) args = parser.parse_args(argv) try: result = check(args.root) json.dump(result, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True) sys.stdout.write("\n") return 0 if result["status"] == "PASS" else 1 except (OSError, UnicodeError, json.JSONDecodeError, HygieneError) as exc: json.dump( {"schema_version": SCHEMA, "status": "ERROR", "error": str(exc)}, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True, ) sys.stdout.write("\n") return 2 if __name__ == "__main__": raise SystemExit(main())