claude-harness/docs/retro/scripts/hook_cases_build.py

95 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Сборка регрессионного корпуса хуков из сырой выгрузки блокировок (B2, #6).
Запуск из корня репозитория:
python3 docs/retro/scripts/hook_cases_build.py > skills/harness-doctor/fixtures/hook-cases.jsonl
Вход: docs/retro/fixtures/hook-blocks.jsonl (hook_blocks_export.py, 12.10.2026, 126 записей).
Выход: первая строка — {"home": …, "layout": […]} (раскладка песочницы), дальше по записи на случай:
поля выгрузки + id, expected (allow|block), why (класс). Вердикт размечен вручную по спеке
openspec/changes/hooks-write-target/specs/hook-guards/spec.md, а не прогоном хука.
"""
import json
import os
import re
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
RAW = os.path.join(HERE, "..", "fixtures", "hook-blocks.jsonl")
HOME = "/Users/andrey.epifancev"
AW, CH = "projects/awp-ca", "projects/cc-skills/claude-harness"
LAYOUT = [{"path": AW}, {"path": AW + "/reference-old-arm"}]
LAYOUT += [{"path": "%s/%s" % (AW, r), "branch": "main"}
for r in ("arm-core", "docs", "tabular-parser", "scan-gateway", "arm-web", "fixtures")]
LAYOUT += [{"path": "%s/.wt/%s/%s" % (AW, n, r), "worktree_of": "%s/%s" % (AW, r), "branch": "feat/" + n}
for n, r in (("web-proto", "arm-core"), ("web-proto", "arm-web"), ("ocr-q1", "fixtures"),
("ocr-q1", "scan-gateway"), ("stmt-template", "arm-web"), ("qa-guide-web", "docs"),
("slice-0", "scan-gateway"), ("slice-3b", "arm-core"))]
LAYOUT += [{"path": CH, "branch": "main"}]
LAYOUT += [{"path": "%s/.wt/%s" % (CH, n), "worktree_of": CH, "branch": b}
for n, b in (("retro", "retro/2026-10-11"), ("roles", "feat/2-roles-and-memory"),
("guard", "feat/6-guard-main-target"))]
LAYOUT += [{"path": ".claude/projects/-Users-andrey-epifancev-projects-awp-ca/memory"},
{"path": ".claude/projects/-Users-andrey-epifancev-projects-cc-skills-claude-harness/memory"},
{"path": ".claude/hooks"}, {"path": ".claude/jobs"}]
# Блок по спеке: архивный коммит openspec прямо в main (решение владельца 1 — архив отдельным PR).
BLOCK = {"c031", "c069", "c070", "c103"}
# Класс для записей, где он не выводится общим правилом ниже.
WHY = {
"c002": "unparseable", "c008": "var-target", "c023": "outside-repo", "c026": "outside-repo",
"c029": "worktree", "c030": "outside-repo", "c040": "worktree-setup", "c044": "worktree", "c045": "worktree",
"c055": "outside-repo", "c064": "worktree-setup", "c072": "switch-then-commit", "c073": "worktree-setup",
"c078": "outside-repo", "c080": "heredoc", "c081": "heredoc", "c083": "outside-repo", "c091": "heredoc",
"c099": "worktree-setup", "c100": "worktree-setup", "c101": "worktree", "c102": "worktree",
"c104": "worktree-setup", "c106": "worktree-setup", "c119": "heredoc",
}
# Ложные блоки этой сессии (15.10.2026), до перехода в worktree: cwd — основной checkout на main.
LIVE_RE = ('git show origin/feat/6-guard-main-target:docs/retro/fixtures/hook-blocks.jsonl | python3 -c \'\n'
'import sys,json,re,collections\n'
'rs=[json.loads(l) for l in sys.stdin if l.strip()]\n'
'c=collections.Counter()\n'
'for r in rs:\n'
' cmd=r.get("command") or ""\n'
' if re.search(r"\\d>{1,2}\\s*/dev/null|\\d>&\\d",cmd): c["bash: N>/dev/null или N>&M"]+=1\n'
' if re.search(r"mkdir\\s+-p\\s+\\S*\\.wt/",cmd): c["bash: mkdir -p .wt/"]+=1\n'
' if re.search(r"\\bsed\\s+-i",cmd): c["bash: sed -i"]+=1\n'
'for k,v in c.most_common(): print(f"{v:4} {k}")\'')
LIVE = [
("l001", "grep-pattern", 'grep -rn "npm i\\|npm install\\|brew install\\|plugin install\\|go install" '
'--include="*.md" --include="*.sh" . | grep -v "^./docs/retro/2026" | head -20'),
("l002", "devnull", 'grep -rn "plugin add\\|marketplace\\|caveman@\\|superpowers@" --include="*.md" '
'--include="*.sh" README.md docs global install.sh 2>/dev/null | head -20'),
("l003", "regex-text", LIVE_RE),
]
DEVNULL = re.compile(r"\d>{1,2}\s*/dev/null|\d>&\d")
def why_of(cid, r):
if cid in BLOCK:
return "main-git"
if cid in WHY:
return WHY[cid]
if "hook error" not in (r.get("message") or ""):
return "not-a-block" # вывод успешной команды, где встретилось слово BLOCKED
if r["tool"] != "Bash":
return "outside-repo"
return "devnull" if DEVNULL.search(r.get("command") or "") else "read-only"
def main():
print(json.dumps({"home": HOME, "layout": LAYOUT}, ensure_ascii=False))
for n, line in enumerate(l for l in open(RAW, encoding="utf-8") if l.strip()):
r = json.loads(line)
cid = "c%03d" % (n + 1)
r.update({"id": cid, "expected": "block" if cid in BLOCK else "allow", "why": why_of(cid, r)})
print(json.dumps(r, ensure_ascii=False))
for cid, why, cmd in LIVE:
print(json.dumps({"id": cid, "session": "live-1015", "cwd": HOME + "/" + CH, "branch": "main",
"tool": "Bash", "command": cmd, "file_path": None, "hook": "guard-main(Bash)",
"message": "", "expected": "allow", "why": why}, ensure_ascii=False))
if __name__ == "__main__":
main()