init: llm-wiki-haness 하네스 설계
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
#!/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*</?(?:content|file)(?:\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 잔여물 검사 대상이다. 서브에이전트가 작성한 문서에
|
||||
# `</content>` 같은 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())
|
||||
Reference in New Issue
Block a user