- C-1: транскрипты сабагентов (<сессия>/subagents/*.jsonl) вливаются в сессию-родителя — ходы, токены, инструменты, codegraph, блокировки, отказы; промпты, роль, стартовый контекст — только из основного. hook_blocks_export.py выгружает их блокировки с сессией-родителем. - I-7: период режется по меткам времени записей; сессия на стыке делится между ретро, стартовый контекст — если первый ход в периоде. - I-1: простые команды Bash — shlex с punctuation_chars, «|» в кавычках команду не делит. I-2: правило src/ ловит и сам каталог src. - I-3: clip() — redact до обрезки (кандидаты, выгрузка), роль — redact. - I-4: без --since период берётся из существующего --out; --from рисует «Цифры» из сохранённого среза. SKILL.md передаёт период явно. - I-5: выгрузка ретро без разметки — сборка корпуса падает. - I-6: сводка компакции — не промпт; «перечитай» — побуждение перечитать. - 2026-10-11.metrics.json: несравнимое помечено (без сабагентов, широкий «перечитай»). Спека и design.md (Decisions) обновлены. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
65 lines
5.2 KiB
Bash
65 lines
5.2 KiB
Bash
# 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")
|