# hook_corpus.py: регрессионный корпус хуков (B2) через источники hooks/ — assert на запись. R="$ROOT/skills/harness-doctor/scripts/hook_corpus.py" F="$ROOT/skills/harness-doctor/fixtures/hook-cases.jsonl" assert_file "hook_corpus.py существует" "$R" assert_file "корпус hook-cases.jsonl" "$F" assert_eq "корпус совпадает со сборкой из выгрузки" "$(python3 "$ROOT/docs/retro/scripts/hook_cases_build.py" | cmp -s - "$F" && echo same)" "same" while IFS=$'\t' read -r id why exp got st cmd; do case "$id" in total*) continue ;; esac case "$st" in ok|KNOWN) ok "корпус $id ($why)" ;; *) fail "корпус $id ($why): $cmd" "ожидалось $exp, получено $got ($st)" ;; esac done < <(python3 "$R" "$ROOT/hooks") # сегменты и known (#29): синтетический корпус из двух сегментов с разными home S="$TMP/seg"; mkdir -p "$S" cat > "$S/c.jsonl" <<'EOF' {"home": "/h1", "layout": [{"path": "m", "branch": "main"}]} {"id": "a1", "why": "main", "tool": "Write", "file_path": "/h1/m/a.md", "cwd": "/h1/m", "expected": "block"} {"id": "a2", "why": "known-x", "tool": "Write", "file_path": "/h1/m/b.md", "cwd": "/h1/m", "expected": "allow", "known": "RT-21"} {"home": "/h2", "layout": [{"path": "r", "branch": "feat/x"}]} {"id": "b1", "why": "branch", "tool": "Write", "file_path": "/h2/r/a.md", "cwd": "/h2/r", "expected": "allow"} EOF out=$(python3 "$R" "$ROOT/hooks" --summary --fixtures "$S/c.jsonl"); rc=$? assert_eq "два сегмента и известная ошибка: exit 0" "$rc" "0" assert_eq "итог с известными ошибками" "$out" "корпус хуков: 3/3, известных ошибок 1" sed 's/"expected": "allow", "known"/"expected": "block", "known"/' "$S/c.jsonl" > "$S/f.jsonl" out=$(python3 "$R" "$ROOT/hooks" --summary --fixtures "$S/f.jsonl"); rc=$? assert_eq "known совпал с вердиктом: exit 1" "$rc" "1" assert_grep "KNOWN-FIXED с id записи" '^KNOWN-FIXED a2' <(printf '%s' "$out") # разметка ретро (#29): hook-labels-<дата>.json + hook-blocks-<дата>.jsonl → новый сегмент корпуса B="$ROOT/docs/retro/scripts/hook_cases_build.py" FX="$TMP/fx"; mkdir -p "$FX"; cp "$ROOT/docs/retro/fixtures/hook-blocks.jsonl" "$FX/" cat > "$FX/hook-blocks-2026-11-01.jsonl" <<'EOF' {"id": "2026-11-01-001", "session": "s", "ts": "2026-11-02T10:00:00Z", "cwd": "/h3/r", "branch": "main", "tool": "Bash", "command": "ls x 2>/dev/null", "file_path": null, "hook": "guard-main(Bash)", "message": "BLOCKED", "expected": null} {"id": "2026-11-01-002", "session": "s", "ts": "2026-11-02T10:01:00Z", "cwd": "/h3/r", "branch": "main", "tool": "Bash", "command": "echo x > f", "file_path": null, "hook": "guard-main(Bash)", "message": "BLOCKED", "expected": null} {"id": "2026-11-01-003", "session": "s", "ts": "2026-11-02T10:02:00Z", "cwd": "/h3/r", "branch": "main", "tool": "Bash", "command": "cat $X", "file_path": null, "hook": "guard-main(Bash)", "message": "BLOCKED", "expected": null} EOF cat > "$FX/hook-labels-2026-11-01.json" <<'EOF' {"home": "/h3", "layout": [{"path": "r", "branch": "main"}], "cases": {"2026-11-01-001": {"expected": "allow", "why": "devnull"}, "2026-11-01-002": {"expected": "block", "why": "main-redirect"}, "2026-11-01-003": {"skip": "цель в переменной, состояние неизвестно"}}} EOF out=$(python3 "$B" --fixtures-dir "$FX"); rc=$? assert_eq "сборка с разметкой ретро: exit 0" "$rc" "0" assert_eq "в корпусе два сегмента" "$(printf '%s\n' "$out" | grep -c '"layout"')" "2" assert_eq "сегмент ретро: две записи" "$(printf '%s\n' "$out" | grep -c '"2026-11-01-00[12]"')" "2" assert_eq "запись со skip не в корпусе" "$(printf '%s\n' "$out" | grep -c '2026-11-01-003')" "0" printf '%s\n' "$out" > "$FX/cases.jsonl" assert_eq "корпус с сегментом ретро проходит хуки" "$(python3 "$R" "$ROOT/hooks" --summary --fixtures "$FX/cases.jsonl" | tail -1)" "корпус хуков: 131/131" python3 - "$FX/hook-labels-2026-11-01.json" <<'PY' import json, sys p = sys.argv[1]; d = json.load(open(p)); del d["cases"]["2026-11-01-002"]; d["cases"]["2026-11-01-009"] = {"skip": "x"} json.dump(d, open(p, "w")) PY out=$(python3 "$B" --fixtures-dir "$FX" 2>&1 >/dev/null); rc=$? assert_eq "разметка не совпадает с выгрузкой: exit 1" "$rc" "1" assert_grep "сообщение называет недостающий и лишний id" '2026-11-01-002.*2026-11-01-009' <(printf '%s' "$out") # I-5: выгрузка ретро без парной разметки — сборка падает, а не пропускает её молча FX2="$TMP/fx2"; mkdir -p "$FX2"; cp "$ROOT/docs/retro/fixtures/hook-blocks.jsonl" "$FX2/" head -1 "$FX/hook-blocks-2026-11-01.jsonl" > "$FX2/hook-blocks-2026-11-07.jsonl" out=$(python3 "$B" --fixtures-dir "$FX2" 2>&1 >/dev/null); rc=$? assert_eq "выгрузка без разметки: exit 1" "$rc" "1" assert_grep "сообщение называет файл выгрузки" 'hook-blocks-2026-11-07\.jsonl' <(printf '%s' "$out")