95 lines
5.6 KiB
Python
95 lines
5.6 KiB
Python
#!/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()
|