init: llm-wiki-haness 하네스 설계
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate platform rule slices from repository-neutral root rule SSOT files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
DEFAULT_ROOT = Path(__file__).resolve().parents[2]
|
||||
CONFIG = Path("harness/source/rule-adapters.json")
|
||||
sys.path.insert(0, str(DEFAULT_ROOT / "harness/runtime"))
|
||||
from fs_transaction import replace_many # noqa: E402
|
||||
|
||||
|
||||
class RuleGenerationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def _safe_path(value: Any) -> Path:
|
||||
if not isinstance(value, str) or not value:
|
||||
raise RuleGenerationError("path must be a non-empty string")
|
||||
path = Path(value)
|
||||
if path.is_absolute() or ".." in path.parts:
|
||||
raise RuleGenerationError(f"path escapes repository: {value}")
|
||||
return path
|
||||
|
||||
|
||||
def _slice(text: str, start: str, end: str | None, source: Path) -> str:
|
||||
lines = text.splitlines(keepends=True)
|
||||
try:
|
||||
start_index = next(index for index, line in enumerate(lines) if line.rstrip("\n") == start)
|
||||
except StopIteration as exc:
|
||||
raise RuleGenerationError(f"{source}: missing start heading {start!r}") from exc
|
||||
end_index = len(lines)
|
||||
if end is not None:
|
||||
try:
|
||||
end_index = next(
|
||||
index for index, line in enumerate(lines[start_index + 1 :], start_index + 1)
|
||||
if line.rstrip("\n") == end
|
||||
)
|
||||
except StopIteration as exc:
|
||||
raise RuleGenerationError(f"{source}: missing end heading {end!r}") from exc
|
||||
return "".join(lines[start_index:end_index]).rstrip() + "\n"
|
||||
|
||||
|
||||
def render(root: Path) -> dict[Path, str]:
|
||||
config_path = root / CONFIG
|
||||
config = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
if config.get("schema_version") != "rule-adapters/v1":
|
||||
raise RuleGenerationError("expected rule-adapters/v1")
|
||||
groups = config.get("groups")
|
||||
if not isinstance(groups, list) or not groups:
|
||||
raise RuleGenerationError("groups must be a non-empty list")
|
||||
rendered: dict[Path, str] = {}
|
||||
for group in groups:
|
||||
if not isinstance(group, dict):
|
||||
raise RuleGenerationError("group must be an object")
|
||||
source_rel = _safe_path(group.get("source"))
|
||||
target_dir = _safe_path(group.get("target_dir"))
|
||||
source = root / source_rel
|
||||
source_text = source.read_text(encoding="utf-8")
|
||||
digest = hashlib.sha256(source_text.encode("utf-8")).hexdigest()
|
||||
slices = group.get("slices")
|
||||
if not isinstance(slices, list) or not slices:
|
||||
raise RuleGenerationError(f"{source_rel}: slices must be non-empty")
|
||||
links: list[str] = []
|
||||
for item in slices:
|
||||
target_name = _safe_path(item.get("target"))
|
||||
if len(target_name.parts) != 1:
|
||||
raise RuleGenerationError("rule slice target must be a filename")
|
||||
start = item.get("start")
|
||||
end = item.get("end")
|
||||
if not isinstance(start, str) or (end is not None and not isinstance(end, str)):
|
||||
raise RuleGenerationError("slice start/end must be headings")
|
||||
target = target_dir / target_name
|
||||
if target in rendered:
|
||||
raise RuleGenerationError(f"duplicate rule target: {target}")
|
||||
marker = f"<!-- GENERATED from {source_rel.as_posix()} sha256:{digest}; DO NOT EDIT -->"
|
||||
rendered[target] = (
|
||||
f"{marker}\n\n"
|
||||
f"Root SSOT: [`{source_rel.as_posix()}`](../../../../../{source_rel.as_posix()})\n\n"
|
||||
f"{_slice(source_text, start, end, source_rel)}"
|
||||
)
|
||||
links.append(f"- [`{target_name.as_posix()}`]({target_name.as_posix()}): `{start}`")
|
||||
index_name = _safe_path(group.get("index"))
|
||||
index = target_dir / index_name
|
||||
marker = f"<!-- GENERATED from {source_rel.as_posix()} sha256:{digest}; DO NOT EDIT -->"
|
||||
rendered[index] = (
|
||||
f"{marker}\n\n"
|
||||
f"# 생성된 rule index\n\n"
|
||||
f"Root SSOT: [`{source_rel.as_posix()}`](../../../../../{source_rel.as_posix()})\n\n"
|
||||
"아래 파일은 root SSOT의 heading 구간에서 생성된다. 직접 편집하지 않는다.\n\n"
|
||||
+ "\n".join(links)
|
||||
+ "\n"
|
||||
)
|
||||
return rendered
|
||||
|
||||
|
||||
def generate(root: Path, check: bool) -> tuple[list[str], list[str]]:
|
||||
expected = render(root)
|
||||
stale = [path.as_posix() for path, text in expected.items() if not (root / path).is_file() or (root / path).read_text(encoding="utf-8") != text]
|
||||
written: list[str] = []
|
||||
if not check and stale:
|
||||
changes = {root / path: expected[path].encode("utf-8") for path in expected if path.as_posix() in stale}
|
||||
replace_many(changes)
|
||||
written = sorted(stale)
|
||||
return sorted(stale), written
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
mode = parser.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("--check", action="store_true")
|
||||
mode.add_argument("--write", action="store_true")
|
||||
parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
root = args.root.resolve(strict=True)
|
||||
stale, written = generate(root, args.check)
|
||||
result = {
|
||||
"schema_version": "rule-adapter-result/v1",
|
||||
"status": "DRIFT" if args.check and stale else "UPDATED" if written else "CURRENT",
|
||||
"stale": stale,
|
||||
"written": written,
|
||||
"target_count": len(render(root)),
|
||||
}
|
||||
json.dump(result, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True)
|
||||
sys.stdout.write("\n")
|
||||
return 1 if args.check and stale else 0
|
||||
except (OSError, UnicodeError, json.JSONDecodeError, RuleGenerationError) as exc:
|
||||
json.dump({"schema_version": "rule-adapter-result/v1", "status": "FAIL", "error": str(exc)}, sys.stdout, ensure_ascii=False, indent=2)
|
||||
sys.stdout.write("\n")
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user