@chrono-meta/fh-gate 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/.claude/regression/probes_live.yaml +137 -0
  2. package/.claude/rules/.residency-patterns.defaults +7 -0
  3. package/.claude/rules/fh_4axis_gate.md +50 -1
  4. package/.claude-plugin/marketplace.json +8 -2
  5. package/AGENTS.md +27 -0
  6. package/CATALOG.md +17 -0
  7. package/CLAUDE.md +12 -2
  8. package/README.ja.md +51 -7
  9. package/README.ko.md +48 -7
  10. package/README.md +37 -5
  11. package/README.zh.md +45 -8
  12. package/docs/STANDARDS_ALIGNMENT.md +61 -0
  13. package/docs/USER_GUIDE.md +3 -0
  14. package/docs/USE_CASES.md +50 -0
  15. package/docs/model_tier_expectations.md +60 -0
  16. package/knowledge/shared/harness-core/fh_three_layer_canon.md +30 -10
  17. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +8 -0
  18. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +9 -0
  19. package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +139 -0
  20. package/knowledge/shared/harness-core/measurement-integrity-checklist.md +20 -0
  21. package/knowledge/shared/learnings/subagent_invocations_log.yaml +451 -4
  22. package/package.json +40 -2
  23. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  24. package/plugins/fh-commons/skills/preprep/README.md +4 -1
  25. package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
  26. package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
  27. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
  28. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
  29. package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
  30. package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
  31. package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
  32. package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
  33. package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
  34. package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
  35. package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
  36. package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
  37. package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
  38. package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
  39. package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
  40. package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
  41. package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
  42. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  43. package/plugins/fh-meta/CHANGELOG.md +76 -1
  44. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
  45. package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
  46. package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
  47. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
  48. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
  49. package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
  50. package/plugins/fh-qp/README.md +71 -0
  51. package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
  52. package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
  53. package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
  54. package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
  55. package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
  56. package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
  57. package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
  58. package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
  59. package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
  60. package/plugins/fh-qp/qp_profile.example.yaml +29 -0
  61. package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
  62. package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
  63. package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
  64. package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
  65. package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
  66. package/scripts/chamber_run.sh +14 -5
  67. package/scripts/com.forge-harness.live-eval.plist +84 -0
  68. package/scripts/compaction_probe.sh +9 -35
  69. package/scripts/directional_diff_gate.sh +14 -2
  70. package/scripts/frontier_digest_autopilot.sh +4 -1
  71. package/scripts/map_postprocess.py +90 -0
  72. package/scripts/outbound_query_guard.sh +131 -0
  73. package/scripts/outbound_query_hook.sh +373 -0
  74. package/scripts/package_coverage_check.sh +55 -16
  75. package/scripts/pipe_verdict_guard.sh +41 -1
  76. package/scripts/probe_live_eval.sh +240 -0
  77. package/scripts/probe_live_eval_lib.py +579 -0
  78. package/scripts/proposal_hook.sh +120 -17
  79. package/scripts/push_zone_check.sh +78 -0
  80. package/scripts/residency_closure_scan.py +252 -0
  81. package/scripts/selfcheck.sh +41 -1
  82. package/scripts/session_close_check.sh +100 -0
  83. package/scripts/sim_isolated_run.sh +98 -2
  84. package/scripts/test_action_yml_lanes.sh +160 -0
  85. package/scripts/test_fh_qp_lanes.sh +105 -0
  86. package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
  87. package/scripts/test_map_postprocess_lanes.sh +143 -0
  88. package/scripts/test_marker_affected_lanes.sh +93 -0
  89. package/scripts/test_marker_crossfamily_lanes.sh +90 -6
  90. package/scripts/test_marker_oracle_lanes.sh +136 -0
  91. package/scripts/test_outbound_query_hook_lanes.sh +433 -0
  92. package/scripts/test_outbound_query_lanes.sh +87 -0
  93. package/scripts/test_pipe_verdict_guard_lanes.sh +26 -0
  94. package/scripts/test_preprep_diagram_lanes.sh +87 -0
  95. package/scripts/test_preprep_drift_anchor.sh +3 -3
  96. package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
  97. package/scripts/test_probe_live_eval_lanes.sh +437 -0
  98. package/scripts/test_proposal_hook_lanes.sh +22 -1
  99. package/scripts/test_push_zone_lanes.sh +304 -0
  100. package/scripts/test_residency_closure_lanes.sh +70 -0
  101. package/scripts/test_sim_isolated_run_lanes.sh +119 -0
  102. package/scripts/test_utterance_intake_lanes.sh +414 -0
  103. package/scripts/test_worktree_reclaim_lanes.sh +70 -0
  104. package/scripts/transcript_utterances.py +222 -0
  105. package/scripts/utterance_intake.sh +424 -0
  106. package/scripts/validate_yaml.sh +27 -0
  107. package/scripts/worktree_reclaim.sh +95 -0
  108. package/templates/.git-hooks/pre-commit +353 -1
  109. package/templates/.git-hooks/pre-push +91 -0
  110. package/templates/RED_TEAM_REPORT.md +49 -0
  111. package/templates/settings.PreToolUse.snippet.json +65 -1
@@ -0,0 +1,414 @@
1
+ #!/usr/bin/env bash
2
+ # test_utterance_intake_lanes.sh — known pair for the «받는 절반»(operator utterance → record) channel.
3
+ #
4
+ # WHAT THIS GUARDS. FH 의 «질문하기» 엔진은 묻는 절반만 배선돼 있고 **받는 절반이 산문이다**
5
+ # (`fh_three_layer_canon.md §2`: *"이 엔진의 수용 절반이 비어 있다"*). 계기는 둘 다 이미 있었는데
6
+ # 서로 안 이어져 있었다:
7
+ # · `compaction_probe.sh seal` 전사본 → 운영자 발화 축자 원장 (있음)
8
+ # · `utterance_landing_check.sh` 손으로 쓴 probes.tsv → 기록 파일 grep (있음, **호출부 0**)
9
+ # probes.tsv 를 만드는 기계가 없어서 두 번째는 사람이 손으로 짜야 했고, 그래서 안 돌았다.
10
+ # `utterance_intake.sh` 가 그 사이를 잇고 `session_close_check.sh ①-f` 가 그걸 부른다.
11
+ #
12
+ # 🟥 이 스위트가 지키는 불변식 넷 — 넷 다 «미측정을 0 으로 접지 마라» 의 사례다:
13
+ # ⓐ 미착지는 rc=1 · 계기 사망은 rc=10 — **뭉치면 「기록하라」와 「프로브를 고쳐라」가 안 갈린다**
14
+ # ⓑ 전사본 부재 → UNMEASURED(rc≠0). 「전부 착지」가 **아니다**
15
+ # ⓒ 추출 발화 0 건 → UNMEASURED. 빈 프로브 세트는 합격이 아니다
16
+ # ⓓ seal 출력은 리팩터 전후로 **바이트 동일**해야 한다 — 공용화가 기존 원장을 조용히 바꾸면
17
+ # 그건 「미측정을 0 으로」의 사촌인 «재생성이 커밋된 의도를 지운다» 다
18
+ #
19
+ # 🟥 음성 컨트롤이 왜 필요한가. `utterance_landing_check.sh` 의 CONTROL 은 **양성만** 있다
20
+ # ("착지가 확실한 것이 안 잡히면 계기 사망"). 그건 「계기가 죽었나」만 재고 「계기가 아무거나
21
+ # 다 잡나」는 못 잰다 — `[[feedback_control_presence_is_not_discrimination]]`. 그래서 이 래퍼가
22
+ # **없어야 마땅한 무작위 토큰**을 넣고, 그게 잡히면 rc=10 을 낸다. L5/L5b 가 그 known pair 다.
23
+ #
24
+ # HERMETIC: 픽스처는 전부 mktemp 아래. 실 전사본도 실 tracks/ 도 읽지 않는다.
25
+ #
26
+ # 종료코드는 selfcheck.sh 규약: 0 pass · 2 주체 부재 · 10 이 스위트 자신의 setup 실패.
27
+
28
+ set -uo pipefail
29
+ . "$(dirname "${BASH_SOURCE[0]}")/fixture_guard_lib.sh"
30
+ cd "$(dirname "$0")/.." || exit 1
31
+ ROOT="$(pwd -P)"
32
+ SUT="$ROOT/scripts/utterance_intake.sh"
33
+ EXTRACT="$ROOT/scripts/transcript_utterances.py"
34
+ PROBE="$ROOT/scripts/compaction_probe.sh"
35
+ CLOSE="$ROOT/scripts/session_close_check.sh"
36
+ PASS=0; FAIL=0
37
+ ok() { echo "✅ $1"; PASS=$((PASS+1)); }
38
+ ng() { echo "❌ $1"; FAIL=$((FAIL+1)); }
39
+
40
+ # 주체 부재는 «통과» 가 아니다 — skipped is not passed.
41
+ for _s in "$SUT" "$EXTRACT"; do
42
+ [ -f "$_s" ] || { echo "ⓘ ${_s##*/} absent — subject missing when we looked (NOT a pass)"; exit 2; }
43
+ done
44
+ [ -f "$PROBE" ] || { echo "ⓘ compaction_probe.sh absent (NOT a pass)"; exit 2; }
45
+
46
+ T="$(fh_fixture_root "$(mktemp -d 2>/dev/null)")" || { echo "ⓘ mktemp failed — this suite's own setup broke (NOT a pass)"; exit 10; }
47
+ : "${T:?fixture root unset}"
48
+ trap 'rm -rf "$T"' EXIT INT TERM
49
+
50
+ TODAY="$(date +%Y-%m-%d)"
51
+
52
+ # ── 픽스처 전사본 ────────────────────────────────────────────────────────────
53
+ # 발화 4 (착지 2 · 미착지 1 · 짧음 1) + tool_result 1 + 슬래시 커맨드 1 + assistant 1
54
+ mk_transcript() { # $1=경로
55
+ cat > "$1" <<'JSONL'
56
+ {"type":"user","timestamp":"2026-09-05T01:00:00.000Z","message":{"content":"앞으로 무인 잡은 plist 에 모델을 핀하는 걸 원칙으로 하자"}}
57
+ {"type":"user","timestamp":"2026-09-05T01:01:00.000Z","message":{"content":[{"type":"tool_result","content":"툴출력본문 재규어호루라기"}]}}
58
+ {"type":"user","timestamp":"2026-09-05T01:02:00.000Z","message":{"content":"/clear 슬래시커맨드본문 도롱뇽부표"}}
59
+ {"type":"assistant","timestamp":"2026-09-05T01:03:00.000Z","message":{"content":"응답"}}
60
+ {"type":"user","timestamp":"2026-09-05T01:04:00.000Z","message":{"content":"ㄱㄱ"}}
61
+ {"type":"user","timestamp":"2026-09-05T01:05:00.000Z","message":{"content":[{"type":"text","text":"등급 기댓값은 분기마다 다시 세우자 이번 분기는 인큐베이터를 과녁으로"},{"type":"image","source":{"type":"base64"}}]}}
62
+ {"type":"user","timestamp":"2026-09-05T01:06:00.000Z","message":{"content":"이폴트 추천은 세션 시작에 한 번만 띄우고 반복하지 마라 두번은 소음이다"}}
63
+ JSONL
64
+ }
65
+
66
+ # 기록 픽스처: 1번(plist 모델 핀)과 3번(등급 기댓값)만 착지. 4번(이폴트 추천)은 미착지.
67
+ # 양성 컨트롤(오늘 날짜)이 반드시 들어 있어야 한다.
68
+ mk_record() { # $1=경로
69
+ cat > "$1" <<REC
70
+ # fh_completed_${TODAY}.md
71
+
72
+ - ✅ 무인 잡 plist 에 모델을 핀했다 — launchd 3종 전부
73
+ - ✅ 등급 기댓값을 분기 단위로 재정의 — 인큐베이터 과녁 선언
74
+ REC
75
+ }
76
+
77
+ mk_transcript "$T/tr.jsonl"
78
+ mk_record "$T/rec.md"
79
+
80
+ # ── L1 착지 2 · 미착지 1 · rc=1 ──────────────────────────────────────────────
81
+ OUT1="$(bash "$SUT" "$T/tr.jsonl" "$T/rec.md" 2>&1)"; RC1=$?
82
+ case "$RC1" in 1) ok "L1 미착지가 있으면 rc=1" ;; *) ng "L1 rc=$RC1 (기대 1)"; echo "$OUT1" | sed 's/^/ /' ;; esac
83
+ case "$OUT1" in *"착지 2"*) ok "L1b 착지 2 건으로 센다" ;; *) ng "L1b 착지 수가 2 가 아니다"; echo "$OUT1" | sed 's/^/ /' ;; esac
84
+ case "$OUT1" in *"미착지 1"*) ok "L1c 미착지 1 건으로 센다" ;; *) ng "L1c 미착지 수가 1 이 아니다" ;; esac
85
+
86
+ # ── L2 미착지 목록이 **정확히 그 발화**를 가리킨다 ───────────────────────────
87
+ case "$OUT1" in *"이폴트 추천"*) ok "L2 미착지 목록에 그 발화가 나온다" ;; *) ng "L2 미착지 발화가 목록에 없다" ;; esac
88
+ # known-negative: 착지한 발화는 미착지 목록에 없어야 한다. 목록 절만 잘라서 본다.
89
+ LIST1="$(printf '%s\n' "$OUT1" | sed -n '/미착지 목록/,$p')"
90
+ case "$LIST1" in *"등급 기댓값"*) ng "L2b 착지한 발화가 미착지 목록에 섞였다" ;; *) ok "L2b 착지한 발화는 목록에 없다 (known-negative)" ;; esac
91
+
92
+ # ── L3 구조 제외: tool_result 본문·슬래시 커맨드는 프로브가 되지 않는다 ──────
93
+ PT="$T/probes_dump.tsv"
94
+ FH_INTAKE_DUMP_PROBES="$PT" bash "$SUT" "$T/tr.jsonl" "$T/rec.md" >/dev/null 2>&1
95
+ if [ -f "$PT" ]; then
96
+ grep -q "재규어호루라기" "$PT" && ng "L3 tool_result 본문이 프로브가 됐다" || ok "L3 tool_result 제외 (known-negative)"
97
+ grep -q "도롱뇽부표" "$PT" && ng "L3b 슬래시 커맨드가 프로브가 됐다" || ok "L3b 슬래시 커맨드 제외 (known-negative)"
98
+ # ── L4 짧은 발화 제외 ──
99
+ grep -qE $'^TARGET\t.*ㄱㄱ' "$PT" && ng "L4 짧은 발화가 프로브가 됐다" || ok "L4 짧은 발화(<20자) 제외"
100
+ # known-positive: 덤프가 비어 있지 않다 — 위 세 줄이 «빈 파일이라 통과» 가 아님을 증명한다
101
+ # 🟥 `grep -c ... || echo 0` 은 무매치일 때 0 을 두 번 낸다 — 계기 자신에게 난 같은 결함이다.
102
+ # TARGET 은 토큰마다 한 행이다(#idx.k/n) — 발화 수는 고유 idx 로 센다
103
+ NT=$(grep $'^TARGET\t' "$PT" 2>/dev/null | sed -n 's/.*#\([0-9][0-9]*\)\.[0-9]*\/[0-9]* .*/\1/p' | sort -u | grep -c .); case "$NT" in ''|*[!0-9]*) NT=0 ;; esac
104
+ [ "${NT:-0}" -eq 3 ] && ok "L4b TARGET 발화 3 건 (덤프가 공허하지 않다 — known-positive)" \
105
+ || ng "L4b TARGET 발화 $NT 건 (기대 3) — 위 known-negative 셋이 빈 집합 통과일 수 있다"
106
+ # known-positive: 음성 컨트롤 행이 실제로 생성된다
107
+ grep -q "^NEGATIVE" "$PT" && ok "L4c 음성 컨트롤 행이 생성된다" || ng "L4c 음성 컨트롤 행이 없다"
108
+ grep -q "^CONTROL" "$PT" && ok "L4d 양성 컨트롤 행이 생성된다" || ng "L4d 양성 컨트롤 행이 없다"
109
+ else
110
+ ng "L3/L4 프로브 덤프가 안 나왔다 (FH_INTAKE_DUMP_PROBES 미지원)"
111
+ fi
112
+
113
+ # ── L5 음성 컨트롤이 기록에 있으면 rc=10 (계기 사망) ─────────────────────────
114
+ cp "$T/rec.md" "$T/rec_neg.md"
115
+ printf '\n무작위토큰 FHNEGZZTEST9 가 기록에 섞여 있다\n' >> "$T/rec_neg.md"
116
+ OUT5="$(FH_INTAKE_NEG_TOKEN=FHNEGZZTEST9 bash "$SUT" "$T/tr.jsonl" "$T/rec_neg.md" 2>&1)"; RC5=$?
117
+ case "$RC5" in 10) ok "L5 음성 컨트롤 적발 → rc=10 (계기 사망)" ;; *) ng "L5 rc=$RC5 (기대 10)"; echo "$OUT5" | sed 's/^/ /' ;; esac
118
+ # 🟥 known-negative — 같은 토큰을 주입해도 기록에 **없으면** 10 이 아니어야 한다.
119
+ # 이게 없으면 L5 는 「무조건 10 을 내는 계기」와 구분되지 않는다.
120
+ OUT5B="$(FH_INTAKE_NEG_TOKEN=FHNEGZZTEST9 bash "$SUT" "$T/tr.jsonl" "$T/rec.md" 2>&1)"; RC5B=$?
121
+ [ "$RC5B" != "10" ] && ok "L5b 토큰이 기록에 없으면 rc≠10 (known-negative)" || { ng "L5b 토큰 부재인데도 rc=10"; echo "$OUT5B" | sed 's/^/ /'; }
122
+
123
+ # ── L6 양성 컨트롤 — 「날짜가 내용에 없다」로 죽으면 안 된다 (sim 이 잡은 넷째) ─────
124
+ # 🟥 초판 컨트롤은 «오늘 날짜» 였고 «오늘자 기록엔 오늘 날짜가 있다» 를 가정했다. 거짓이다 —
125
+ # 날짜는 파일명에 있지 내용에 있을 의무가 없다. 실측(2026-09-05 sim, WRITECTL 팔): 플로어
126
+ # 티어가 만든 `fh_completed_2026-09-05.md` 내용은 한 줄이고 날짜 문자열이 없다.
127
+ # 그러면 정상 마감마다 rc=10 거짓 경보가 뜨고, 그 소음이 ❌ 줄을 스킵하게 만든다.
128
+ # ⇒ 컨트롤을 기록 «내용»에서 뽑는다. 아래가 그 known pair 다.
129
+ printf '오늘 날짜 문자열이 없는 기록 파일이다 이폴트 추천 등급 기댓값 무인 잡 인큐베이터\n' > "$T/rec_nodate.md"
130
+ OUT6="$(bash "$SUT" "$T/tr.jsonl" "$T/rec_nodate.md" 2>&1)"; RC6=$?
131
+ [ "$RC6" != "10" ] && ok "L6 날짜가 내용에 없어도 계기가 안 죽는다 (rc=$RC6, 거짓 경보 방지)" \
132
+ || { ng "L6 날짜 부재만으로 rc=10 — 정상 마감에 거짓 경보"; echo "$OUT6" | sed 's/^/ /'; }
133
+ # known-negative 짝: 기록에서 토큰을 못 뽑으면(사실상 빈 기록) **반드시** rc=10 이어야 한다.
134
+ printf '\n' > "$T/rec_empty.md"
135
+ OUT6B="$(bash "$SUT" "$T/tr.jsonl" "$T/rec_empty.md" 2>&1)"; RC6B=$?
136
+ case "$RC6B" in 10) ok "L6b 컨트롤을 못 뽑으면 rc=10 (계기 사망)" ;; *) ng "L6b rc=$RC6B (기대 10)"; echo "$OUT6B" | sed 's/^/ /' ;; esac
137
+ case "$OUT6B" in *"전부 착지"*) ng "L6c 계기 사망인데 «전부 착지» 를 인쇄했다" ;; *) ok "L6c 계기 사망일 때 착지 판정을 인쇄하지 않는다" ;; esac
138
+ # 파생 컨트롤이 **실제로 생성**되는지 — 이름표만 있고 값이 없으면 위 셋이 공허해진다
139
+ FH_INTAKE_DUMP_PROBES="$T/p6.tsv" bash "$SUT" "$T/tr.jsonl" "$T/rec.md" >/dev/null 2>&1
140
+ grep -q "기록파생" "$T/p6.tsv" 2>/dev/null && ok "L6d 파생 양성 컨트롤 행이 생성된다" || ng "L6d 파생 컨트롤 행이 없다"
141
+
142
+ # ── L7 전사본 부재 → UNMEASURED, rc≠0, 「전부 착지」 금지 ────────────────────
143
+ OUT7="$(bash "$SUT" "$T/NOPE.jsonl" "$T/rec.md" 2>&1)"; RC7=$?
144
+ [ "$RC7" -ne 0 ] && ok "L7 전사본 부재 → rc=$RC7 (0 아님)" || ng "L7 전사본 부재인데 rc=0"
145
+ case "$OUT7" in *UNMEASURED*) ok "L7b 전사본 부재가 UNMEASURED 로 라벨된다" ;; *) ng "L7b UNMEASURED 라벨이 없다"; echo "$OUT7" | sed 's/^/ /' ;; esac
146
+ case "$OUT7" in *"전부 착지"*) ng "L7c 부재를 «전부 착지» 로 접었다" ;; *) ok "L7c 부재를 0 으로 접지 않는다" ;; esac
147
+
148
+ # ── L8 추출 발화 0 건 → UNMEASURED (빈 프로브 세트는 합격이 아니다) ─────────
149
+ cat > "$T/tr_short.jsonl" <<'JSONL'
150
+ {"type":"user","timestamp":"2026-09-05T01:00:00.000Z","message":{"content":"ㄱㄱ"}}
151
+ {"type":"user","timestamp":"2026-09-05T01:01:00.000Z","message":{"content":"응 좋아"}}
152
+ JSONL
153
+ OUT8="$(bash "$SUT" "$T/tr_short.jsonl" "$T/rec.md" 2>&1)"; RC8=$?
154
+ [ "$RC8" -ne 0 ] && ok "L8 발화 0 건 → rc=$RC8 (0 아님)" || ng "L8 발화 0 건인데 rc=0"
155
+ case "$OUT8" in *UNMEASURED*) ok "L8b 발화 0 건이 UNMEASURED 로 라벨된다" ;; *) ng "L8b UNMEASURED 라벨이 없다"; echo "$OUT8" | sed 's/^/ /' ;; esac
156
+
157
+ # ── L9 transcript_utterances.py --min-chars known pair ──────────────────────
158
+ # 🟥 문턱은 실측으로 골랐다 — 설계안의 40 이 아니라 **20** 이다. 이 레포 전사본 전수
159
+ # (2026-09-05, 봉투/툴결과 제외 후 3191 발화)에서 30–39 자 구간에 하중 지는 발화가 산다:
160
+ # «그리고 입장리뷰는 정적 동적 모두 수행해야해» · «4는 초록으로 올리도록 하자» — 한글은
161
+ # 자당 정보량이 커서 40 자 바닥은 **교리 발화를 조용히 버린다**(위험한 방향).
162
+ # 20 = 853 건 제외 · 2338 건 보존. 아래 두 줄이 그 문턱의 known pair 다.
163
+ N0=$(python3 "$EXTRACT" "$T/tr.jsonl" --min-chars 0 2>/dev/null | grep -c .); case "$N0" in ''|*[!0-9]*) N0=0 ;; esac
164
+ N20=$(python3 "$EXTRACT" "$T/tr.jsonl" --min-chars 20 2>/dev/null | grep -c .); case "$N20" in ''|*[!0-9]*) N20=0 ;; esac
165
+ [ "${N0:-0}" -eq 4 ] && ok "L9 --min-chars 0 → 4 발화 (known-positive)" || ng "L9 --min-chars 0 → $N0 (기대 4)"
166
+ [ "${N20:-0}" -eq 3 ] && ok "L9b --min-chars 20 → 3 발화 (필터가 실제로 거른다)" || ng "L9b --min-chars 20 → $N20 (기대 3)"
167
+ # 출력 형식: n<TAB>ts<TAB>text
168
+ python3 "$EXTRACT" "$T/tr.jsonl" --min-chars 0 2>/dev/null | head -1 | grep -qE $'^1\t2026-09-05T01:00:00' \
169
+ && ok "L9c tsv 형식이 n<TAB>ts<TAB>text" || ng "L9c tsv 형식이 어긋난다"
170
+
171
+ # ── L10 seal 출력 골든 — 공용화가 기존 원장을 바꾸지 않았다 ─────────────────
172
+ bash "$PROBE" seal --transcript "$T/tr.jsonl" --dir "$T/seal" >/dev/null 2>&1
173
+ SF="$(ls "$T/seal"/seal_*.md 2>/dev/null | head -1)"
174
+ if [ -n "$SF" ]; then
175
+ sed -n '/^## 운영자 발화/,/^## 이 세션이 건드린/p' "$SF" | sed '$d' > "$T/seal_block.txt"
176
+ cat > "$T/seal_expect.txt" <<'GOLD'
177
+ ## 운영자 발화 (이 세션)
178
+ 1. 앞으로 무인 잡은 plist 에 모델을 핀하는 걸 원칙으로 하자
179
+ 2. ㄱㄱ
180
+ 3. 등급 기댓값은 분기마다 다시 세우자 이번 분기는 인큐베이터를 과녁으로
181
+ 4. 이폴트 추천은 세션 시작에 한 번만 띄우고 반복하지 마라 두번은 소음이다
182
+
183
+ 합계: 4건
184
+ 제외: tool_result 1 · 메타/커맨드 1 · 텍스트없음 0
185
+
186
+ GOLD
187
+ if diff -u "$T/seal_expect.txt" "$T/seal_block.txt" >"$T/seal_diff.txt" 2>&1; then
188
+ ok "L10 seal 발화 블록이 골든과 바이트 동일"
189
+ else
190
+ ng "L10 seal 발화 블록이 골든과 다르다"; sed 's/^/ /' "$T/seal_diff.txt" | head -20
191
+ fi
192
+ else
193
+ ng "L10 seal 파일이 안 생겼다"
194
+ fi
195
+
196
+ # ── L11 compaction_probe.sh --self-test 초록 유지 ───────────────────────────
197
+ if bash "$PROBE" --self-test >/dev/null 2>&1; then ok "L11 compaction_probe --self-test 초록"; else ng "L11 compaction_probe --self-test 실패"; fi
198
+
199
+ # ── L12 되돌림 컨트롤 — 배선이 장식이 아니다 ────────────────────────────────
200
+ # 실레포를 안 건드리고, compaction_probe.sh 만 스크래치 트리로 복사해 REPO_ROOT 를 옮긴다.
201
+ # 추출기가 **없으면** seal 이 파싱 실패로 떨어져야 한다 = seal 이 정말 그 파일을 통해 돈다.
202
+ mkdir -p "$T/mirror/scripts"
203
+ cp "$PROBE" "$T/mirror/scripts/compaction_probe.sh"
204
+ bash "$T/mirror/scripts/compaction_probe.sh" seal --transcript "$T/tr.jsonl" --dir "$T/seal_rv" >/dev/null 2>&1
205
+ SFR="$(ls "$T/seal_rv"/seal_*.md 2>/dev/null | head -1)"
206
+ if [ -n "$SFR" ] && grep -q "앞으로 무인 잡은 plist" "$SFR"; then
207
+ ng "L12 추출기가 없는데도 발화가 실렸다 — seal 이 그 파일을 통해 돌지 않는다(배선 장식)"
208
+ else
209
+ ok "L12 추출기 제거 → seal 발화 블록이 죽는다 (되돌림 컨트롤)"
210
+ fi
211
+ # known-positive 짝: 같은 미러에 추출기를 넣으면 살아난다
212
+ cp "$EXTRACT" "$T/mirror/scripts/transcript_utterances.py"
213
+ bash "$T/mirror/scripts/compaction_probe.sh" seal --transcript "$T/tr.jsonl" --dir "$T/seal_rv2" >/dev/null 2>&1
214
+ SFR2="$(ls "$T/seal_rv2"/seal_*.md 2>/dev/null | head -1)"
215
+ if [ -n "$SFR2" ] && grep -q "앞으로 무인 잡은 plist" "$SFR2"; then
216
+ ok "L12b 추출기 복원 → 발화가 다시 실린다 (복원 확인 — L12 가 미러 자체의 사망이 아님)"
217
+ else
218
+ ng "L12b 추출기를 복원했는데도 발화가 안 실린다 — L12 는 미러 결함이지 되돌림 증거가 아니다"
219
+ fi
220
+
221
+ # ── L13 session_close_check.sh 가 ①-f 를 **실행**한다 (mention 아님) ────────
222
+ # 🟥 ①-e 가 아니다 — 그 라벨은 stray-path 가 이미 쓰고 있다(session_close_check.sh:690).
223
+ # 임무 진단이 «①-e 는 없다» 고 적었는데 거짓이었고, 이 레인이 «배선 전인데 초록» 으로 잡았다.
224
+ if [ -f "$CLOSE" ]; then
225
+ # 🟥 도구를 **설치해서** 돌린다 — 없으면 «skipped, not passed» 로 빠져 배선 자체를 못 잰다.
226
+ # fakerepo 는 실 전사본 slug 와 안 겹치므로 결과는 결정적으로 UNMEASURED(전사본 부재)다.
227
+ mkdir -p "$T/fakerepo/scripts" && ( cd "$T/fakerepo" && git init -q . 2>/dev/null && git config user.email l@example.invalid && git config user.name lane )
228
+ cp "$SUT" "$EXTRACT" "$ROOT/scripts/utterance_landing_check.sh" "$ROOT/scripts/branch_claim.sh" "$T/fakerepo/scripts/" 2>/dev/null
229
+ OUT13="$(FH_PEER_SOCK_DIR="$T/nosock" bash "$CLOSE" "$T/fakerepo" 2>&1)"
230
+ case "$OUT13" in *"①-f"*) ok "L13 close check 가 ①-f 를 인쇄한다 (실행 확인)" ;; *) ng "L13 close check 출력에 ①-f 가 없다" ;; esac
231
+ # 🟥 «utterance_intake.sh 없음 … UNMEASURED» 분기도 ①-f+UNMEASURED 를 찍는다 — 그 분기로 통과하면
232
+ # «설치돼서 돌았다» 가 아니라 «없어서 건너뛰었다» 다(codex #9). 설치 분기의 문구를 요구한다.
233
+ case "$OUT13" in
234
+ *"utterance_intake.sh 없음"*) ng "L13b 스크립트를 설치했는데 «없음» 분기로 갔다 (실행이 아니라 skip)" ;;
235
+ *"①-f UNMEASURED"*) ok "L13b 설치 분기에서 ①-f 가 UNMEASURED 로 뜬다 (전사본 부재 / 세션 미상)" ;;
236
+ *) ng "L13b ①-f 가 설치 분기의 UNMEASURED 로 라벨되지 않는다" ;;
237
+ esac
238
+ else
239
+ ng "L13 session_close_check.sh 부재"
240
+ fi
241
+
242
+ # ── L14/L15 첫 실사용이 잡은 둘 (2026-09-05) ────────────────────────────────
243
+ # 실 전사본에 처음 돌리자 두 결함이 즉시 나왔다 — 레인 29개도 cross-family 도 못 잡던 것들이다
244
+ # ([[feedback_adversarial_review_not_substitute_for_first_use]]):
245
+ # ⓐ peer 알림 · 무인 실행 · 이미지 마커가 «운영자 발화» 로 세어졌다
246
+ # ⓑ 라벨에 머신 고유 절대경로(`/var/folders/3h/…`)가 그대로 찍혔다 = 사설 토큰 유출 채널
247
+ cat > "$T/tr_sys.jsonl" <<'JSONL'
248
+ {"type":"user","timestamp":"2026-09-05T02:00:00.000Z","message":{"content":"[Cross-session delivery notice] peer 세션이 보낸 알림이지 운영자 발화가 아니다"}}
249
+ {"type":"user","timestamp":"2026-09-05T02:01:00.000Z","message":{"content":"[automated-run: launchd] 무인 실행 프리앰블이지 운영자 발화가 아니다"}}
250
+ {"type":"user","timestamp":"2026-09-05T02:02:00.000Z","message":{"content":"[Image: source: /var/folders/3h/deadbeefcafe/T/shot.png]"}}
251
+ {"type":"user","timestamp":"2026-09-05T02:03:00.000Z","message":{"content":"[Image #3] 레드팀 블루팀을 나눠서 단계별로 돌리는 기법을 우리도 쓰는지 궁금하다"}}
252
+ JSONL
253
+ OUT14="$(FH_INTAKE_DUMP_PROBES="$T/p14.tsv" bash "$SUT" "$T/tr_sys.jsonl" "$T/rec.md" 2>&1)"
254
+ if [ -f "$T/p14.tsv" ]; then
255
+ N14=$(grep $'^TARGET\t' "$T/p14.tsv" 2>/dev/null | sed -n 's/.*#\([0-9][0-9]*\)\.[0-9]*\/[0-9]* .*/\1/p' | sort -u | grep -c .); case "$N14" in ''|*[!0-9]*) N14=0 ;; esac
256
+ [ "$N14" -eq 1 ] && ok "L14 시스템 마커 3 건 제외 · 실발화 1 건만 프로브가 된다" \
257
+ || ng "L14 TARGET $N14 건 (기대 1)"
258
+ grep -q "Cross-session" "$T/p14.tsv" && ng "L14b peer 알림이 프로브가 됐다" || ok "L14b peer 알림 제외 (known-negative)"
259
+ grep -q "automated-run\|launchd" "$T/p14.tsv" && ng "L14c 무인 실행 프리앰블이 프로브가 됐다" || ok "L14c 무인 실행 제외 (known-negative)"
260
+ # known-positive: 마커만 벗기고 **뒤의 실발화는 살린다** — 통째로 버리는 것과 다르다
261
+ grep -q "레드팀" "$T/p14.tsv" && ok "L14d [Image #N] 뒤의 실발화는 살아남는다 (known-positive)" \
262
+ || ng "L14d 마커 벗기기가 실발화까지 버렸다"
263
+ # ── L15 사설 토큰 마스킹 ──
264
+ grep -q "var/folders\|deadbeefcafe" "$T/p14.tsv" && ng "L15 머신 고유 임시경로가 프로브에 남았다" \
265
+ || ok "L15 임시경로가 마스킹된다"
266
+ else
267
+ ng "L14/L15 프로브 덤프가 안 나왔다"
268
+ fi
269
+ # 🟥 컨트롤: 플래그를 **안 주면** 안 걸러져야 한다. 안 그러면 «원래부터 안 잡히는 입력» 을
270
+ # 필터의 공로로 오귀속한다. seal 이 바이트 동일한 이유가 정확히 이것이다.
271
+ NRAW=$(python3 "$EXTRACT" "$T/tr_sys.jsonl" --min-chars 20 2>/dev/null | grep -c .); case "$NRAW" in ''|*[!0-9]*) NRAW=0 ;; esac
272
+ NSTR=$(python3 "$EXTRACT" "$T/tr_sys.jsonl" --min-chars 20 --strip-system-markers 2>/dev/null | grep -c .); case "$NSTR" in ''|*[!0-9]*) NSTR=0 ;; esac
273
+ [ "$NRAW" -eq 4 ] && ok "L15b 플래그 없으면 4 건 전부 통과 (known-negative — 필터가 기본 OFF)" \
274
+ || ng "L15b 플래그 없이 $NRAW 건 (기대 4) — seal 경로가 이미 오염됐다"
275
+ [ "$NSTR" -eq 1 ] && ok "L15c 플래그를 주면 1 건 (필터가 실제로 거른다)" || ng "L15c 플래그 켜고 $NSTR 건 (기대 1)"
276
+ # 🟥 홈 경로 마스킹 — HOME 은 러너마다 다르니 값이 아니라 **부재**를 잰다.
277
+ printf '%s\n' '{"type":"user","timestamp":"2026-09-05T03:00:00.000Z","message":{"content":"'"$HOME"'/projects/secretrepo 아래 파일을 손봐야 하는데 어떻게 할까 고민이다"}}' > "$T/tr_home.jsonl"
278
+ FH_INTAKE_DUMP_PROBES="$T/p15.tsv" bash "$SUT" "$T/tr_home.jsonl" "$T/rec.md" >/dev/null 2>&1
279
+ if [ -f "$T/p15.tsv" ]; then
280
+ grep -qF -- "$HOME/projects/secretrepo" "$T/p15.tsv" && ng "L15d 홈 절대경로가 프로브에 남았다" \
281
+ || ok "L15d 홈 절대경로가 마스킹된다"
282
+ grep -q "secretrepo" "$T/p15.tsv" && ok "L15e known-positive: 그 발화 자체는 잡혔다 (빈 파일 통과 아님)" \
283
+ || ng "L15e 그 발화가 아예 안 잡혔다 — L15d 는 공허한 통과다"
284
+ else
285
+ ng "L15d/e 프로브 덤프가 안 나왔다"
286
+ fi
287
+
288
+ # ── L16 슬래시 커맨드 vs 절대경로 발화 (첫 실사용이 잡은 셋째) ──────────────
289
+ # 옛 규칙 `startswith('/')` 은 «/Users/…/OT.pptx.pdf 이건 발표자 ot자료인데…» 를 커맨드로
290
+ # 오분류해 **무음 삭제**했다. 실측: `/` 접두 28 건 중 13 건(46%)이 경로형 실발화.
291
+ cat > "$T/tr_slash.jsonl" <<'JSONL'
292
+ {"type":"user","timestamp":"2026-09-05T04:00:00.000Z","message":{"content":"/compact"}}
293
+ {"type":"user","timestamp":"2026-09-05T04:01:00.000Z","message":{"content":"/install-wizard --dry-run 으로 먼저 돌려보고 결과를 보자 그 다음에 결정하면 된다"}}
294
+ {"type":"user","timestamp":"2026-09-05T04:02:00.000Z","message":{"content":"/srv/tester/Downloads/발표자료.pdf 이건 발표자 오티 자료인데 이걸 기준으로 초안을 잡자"}}
295
+ JSONL
296
+ NS_OFF=$(python3 "$EXTRACT" "$T/tr_slash.jsonl" --min-chars 20 2>/dev/null | grep -c .); case "$NS_OFF" in ''|*[!0-9]*) NS_OFF=0 ;; esac
297
+ NS_ON=$(python3 "$EXTRACT" "$T/tr_slash.jsonl" --min-chars 20 --strip-system-markers 2>/dev/null | grep -c .); case "$NS_ON" in ''|*[!0-9]*) NS_ON=0 ;; esac
298
+ [ "$NS_OFF" -eq 0 ] && ok "L16 플래그 없으면 셋 다 커맨드로 제외 (옛 규칙 = seal 경로, 컨트롤)" \
299
+ || ng "L16 플래그 없이 $NS_OFF 건 남았다 (기대 0) — seal 행동이 바뀌었다"
300
+ [ "$NS_ON" -eq 1 ] && ok "L16b 플래그를 주면 경로형 실발화 1 건만 살아난다" || ng "L16b 플래그 켜고 $NS_ON 건 (기대 1)"
301
+ python3 "$EXTRACT" "$T/tr_slash.jsonl" --min-chars 20 --strip-system-markers 2>/dev/null | grep -q "발표자 오티" \
302
+ && ok "L16c 살아난 것이 **경로형 실발화** 다 (known-positive)" || ng "L16c 엉뚱한 것이 살아났다"
303
+ python3 "$EXTRACT" "$T/tr_slash.jsonl" --min-chars 20 --strip-system-markers 2>/dev/null | grep -q "install-wizard" \
304
+ && ng "L16d 진짜 슬래시 커맨드가 살아남았다" || ok "L16d 진짜 슬래시 커맨드는 여전히 제외 (known-negative)"
305
+
306
+ # ── L17 압축 이어붙임 프리앰블 (첫 실사용이 잡은 다섯째) ────────────────────
307
+ # 대괄호로 시작하지 않아 L14 규칙이 **구조적으로** 못 잡는다. 실측 21 건, 실전 미착지 목록에 등장.
308
+ cat > "$T/tr_cont.jsonl" <<'JSONL'
309
+ {"type":"user","timestamp":"2026-09-05T05:00:00.000Z","message":{"content":"This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion."}}
310
+ {"type":"user","timestamp":"2026-09-05T05:01:00.000Z","message":{"content":"이건 진짜 운영자 발화다 등급 기댓값을 분기마다 다시 세우자고 했던 그 이야기"}}
311
+ JSONL
312
+ NC_ON=$(python3 "$EXTRACT" "$T/tr_cont.jsonl" --min-chars 20 --strip-system-markers 2>/dev/null | grep -c .); case "$NC_ON" in ''|*[!0-9]*) NC_ON=0 ;; esac
313
+ NC_OFF=$(python3 "$EXTRACT" "$T/tr_cont.jsonl" --min-chars 20 2>/dev/null | grep -c .); case "$NC_OFF" in ''|*[!0-9]*) NC_OFF=0 ;; esac
314
+ [ "$NC_ON" -eq 1 ] && ok "L17 압축 프리앰블 제외 · 실발화 1 건만 남는다" || ng "L17 플래그 켜고 $NC_ON 건 (기대 1)"
315
+ [ "$NC_OFF" -eq 2 ] && ok "L17b 플래그 없으면 둘 다 통과 (컨트롤 — seal 행동 불변)" || ng "L17b 플래그 없이 $NC_OFF 건 (기대 2)"
316
+ python3 "$EXTRACT" "$T/tr_cont.jsonl" --min-chars 20 --strip-system-markers 2>/dev/null | grep -q "등급 기댓값" \
317
+ && ok "L17c 남은 것이 실발화다 (known-positive)" || ng "L17c 실발화까지 버렸다"
318
+
319
+ # ── L18 ①-f **행복 경로** — 전사본과 기록이 다 있을 때 실제로 판정까지 간다 ────
320
+ # 🟥 L13/L13b 는 UNMEASURED 가지만 잰다. 그것만 있으면 «①-f 가 늘 UNMEASURED 를 내는 블록» 과
321
+ # 구분이 안 된다 — 존재 확인이지 판별력 확인이 아니다
322
+ # (`[[feedback_control_presence_is_not_discrimination]]`).
323
+ if [ -f "$CLOSE" ] && [ -d "$T/fakerepo" ]; then
324
+ mkdir -p "$T/fakerepo/tracks/_meta" "$T/trdir"
325
+ # 오늘자 기록 — 착지 1 · 미착지 1 이 되게 만든다
326
+ { echo "# fh_completed_$TODAY.md"; echo "- ✅ 무인 잡 plist 에 모델을 핀했다 — launchd 3종 전부"; } \
327
+ > "$T/fakerepo/tracks/_meta/fh_completed_$TODAY.md"
328
+ mk_transcript "$T/trdir/LANESESSION.jsonl"
329
+ OUT18="$(FH_PEER_SOCK_DIR="$T/nosock" FH_CLAIM_TEST=1 FH_CLAIM_SESSION=LANESESSION \
330
+ FH_TRANSCRIPT_DIR="$T/trdir" bash "$CLOSE" "$T/fakerepo" 2>&1)"; RC18=$?
331
+ case "$OUT18" in *"①-f 미착지 발화가 있다"*) ok "L18 ①-f 가 실제로 판정까지 간다 (미착지 분기)" ;;
332
+ *) ng "L18 ①-f 가 판정 분기에 도달하지 못했다"; printf '%s\n' "$OUT18" | grep -A4 "①-f" | sed 's/^/ /' ;; esac
333
+ case "$OUT18" in *"이폴트 추천"*) ok "L18b 미착지 발화가 마감 출력에 그대로 뜬다" ;; *) ng "L18b 미착지 목록이 마감 출력에 없다" ;; esac
334
+ # 🟥 advisory 다 — ①-f 때문에 close check 가 «VIOLATIONS» 로 뒤집히면 안 된다.
335
+ # 그러면 정상 마감이 막히고 그게 --no-verify 를 훈련시킨다.
336
+ case "$OUT18" in *"①-f"*) _seen=1 ;; *) _seen=0 ;; esac
337
+ OUT18B="$(FH_PEER_SOCK_DIR="$T/nosock" FH_CLAIM_TEST=1 FH_CLAIM_SESSION=NOSUCHSESSION \
338
+ FH_TRANSCRIPT_DIR="$T/trdir" bash "$CLOSE" "$T/fakerepo" 2>&1)"; RC18B=$?
339
+ V18=$(printf '%s\n' "$OUT18" | grep -c '^❌'); case "$V18" in ''|*[!0-9]*) V18=0 ;; esac
340
+ V18B=$(printf '%s\n' "$OUT18B" | grep -c '^❌'); case "$V18B" in ''|*[!0-9]*) V18B=0 ;; esac
341
+ [ "$V18" -eq "$V18B" ] && ok "L18c ①-f 는 ❌ 개수를 늘리지 않는다 (advisory — 미착지 유무로 불변)" \
342
+ || ng "L18c ①-f 가 close check 판정을 바꿨다 (미착지 $V18 vs 전사본없음 $V18B)"
343
+ [ "$_seen" -eq 1 ] && ok "L18d known-positive: 그 실행에 ①-f 가 실제로 있었다" || ng "L18d ①-f 자체가 안 떴다 — L18c 는 공허하다"
344
+ # ❌ 개수만 보면 «같은 출력 + exit 1» 회귀를 못 잡는다(codex #8) — exit code 도 같아야 advisory 다
345
+ [ "${RC18:-x}" = "${RC18B:-y}" ] && ok "L18e ①-f 는 close check exit code 도 바꾸지 않는다 (rc $RC18 = $RC18B)" \
346
+ || ng "L18e exit code 가 달라졌다 (미착지 $RC18 vs 전사본없음 $RC18B)"
347
+ # L13c — 스크립트가 없으면 «없음 — skipped, not passed» 로 표시된다 (L13b 의 반대쪽)
348
+ rm -f "$T/fakerepo/scripts/utterance_intake.sh"
349
+ OUT13C="$(FH_PEER_SOCK_DIR="$T/nosock" FH_CLAIM_TEST=1 FH_CLAIM_SESSION=LANESESSION \
350
+ FH_TRANSCRIPT_DIR="$T/trdir" bash "$CLOSE" "$T/fakerepo" 2>&1)"
351
+ case "$OUT13C" in *"utterance_intake.sh 없음"*) ok "L13c 스크립트 부재 → «없음 — skipped, not passed» (known-negative)" ;; *) ng "L13c 부재를 표시하지 않는다" ;; esac
352
+ else
353
+ ng "L18 fakerepo 준비 안 됨"
354
+ fi
355
+
356
+ # ── L19 발화 판정은 토큰 과반 — 주제어 하나가 겹친다고 착지가 아니다 (codex #1) ──
357
+ cat > "$T/tr19.jsonl" <<'JSONL'
358
+ {"type":"user","timestamp":"2026-09-05T05:00:00.000Z","message":{"content":"쿠버네티스 배포는 승인자두명 카나리지표 장애예산 셋이 모두 맞을 때만 열자"}}
359
+ JSONL
360
+ printf '%s\n' '- 오늘 쿠버네티스 버전만 확인했다 (주제어 하나만 겹친다 — 정책은 안 적혔다)' > "$T/rec19a.md"
361
+ OUT19="$(bash "$SUT" "$T/tr19.jsonl" "$T/rec19a.md" 2>&1)"; RC19=$?
362
+ [ "$RC19" -eq 1 ] && ok "L19 주제어 하나만 겹치면 미착지 (rc=1)" || { ng "L19 rc=$RC19 (기대 1) — 낱말 하나로 착지 처리"; echo "$OUT19" | sed 's/^/ /'; }
363
+ case "$OUT19" in *"쿠버네티스 배포는"*) ok "L19b 그 발화가 미착지 목록에 있다" ;; *) ng "L19b 미착지 목록에 없다" ;; esac
364
+ printf '%s\n' '- 배포 정책: 승인자두명 확인 + 카나리지표 통과 + 장애예산 잔여 — 셋 다 맞을 때만' > "$T/rec19b.md"
365
+ OUT19B="$(bash "$SUT" "$T/tr19.jsonl" "$T/rec19b.md" 2>&1)"; RC19B=$?
366
+ [ "$RC19B" -eq 0 ] && ok "L19c CONTROL 토큰 3 중 2 이상 겹치면 착지 (rc=0)" || { ng "L19c rc=$RC19B (기대 0)"; echo "$OUT19B" | sed 's/^/ /'; }
367
+
368
+ # ── L20 프로브불가 발화는 «못 쟀다» — 미착지 0 이어도 rc=0 이 아니다 (codex #2) ──
369
+ cat > "$T/tr20.jsonl" <<'JSONL'
370
+ {"type":"user","timestamp":"2026-09-05T05:10:00.000Z","message":{"content":"그리고 그러니까 그래서 그러면 그런데 이렇게 그렇게 어떻게 하지만 그러나 마찬가지 이야기"}}
371
+ {"type":"user","timestamp":"2026-09-05T05:11:00.000Z","message":{"content":"앞으로 무인 잡은 plist 에 모델을 핀하는 걸 원칙으로 하자"}}
372
+ JSONL
373
+ OUT20="$(bash "$SUT" "$T/tr20.jsonl" "$T/rec.md" 2>&1)"; RC20=$?
374
+ [ "$RC20" -eq 1 ] && ok "L20 프로브불가 1 + 착지 1 → rc=1 (미측정은 착지가 아니다)" || { ng "L20 rc=$RC20 (기대 1)"; echo "$OUT20" | sed 's/^/ /'; }
375
+ case "$OUT20" in *"프로브불가 1 건"*) ok "L20b 프로브불가 발화가 목록으로 뜬다" ;; *) ng "L20b 프로브불가 목록이 없다" ;; esac
376
+ case "$OUT20" in *"착지 1 · 미착지 0"*) ok "L20c CONTROL 다른 발화는 착지로 센다 (rc=1 이 미착지 탓이 아님)" ;; *) ng "L20c 착지/미착지 계수가 기대와 다르다"; echo "$OUT20" | sed 's/^/ /' ;; esac
377
+
378
+ # ── L21 라벨·프로브의 사설 토큰 마스킹은 $HOME 만이 아니다 (codex #5) ──
379
+ cat > "$T/tr21.jsonl" <<'JSONL'
380
+ {"type":"user","timestamp":"2026-09-05T05:20:00.000Z","message":{"content":"/srv/bob/projects/acme-secret/prod.env 에 있는 DB_HOST=db01.internal 값을 기록하지 말고 비교만 해줘"}}
381
+ JSONL
382
+ OUT21="$(FH_INTAKE_DUMP_PROBES="$T/p21.tsv" bash "$SUT" "$T/tr21.jsonl" "$T/rec.md" 2>&1)"
383
+ if [ -f "$T/p21.tsv" ]; then
384
+ for _tok in bob acme-secret prod.env db01 internal; do
385
+ grep -q -- "$_tok" "$T/p21.tsv" && ng "L21 사설 토큰 '$_tok' 이 프로브/라벨에 남았다" || ok "L21 '$_tok' 마스킹"
386
+ done
387
+ printf '%s\n' "$OUT21" | grep -q -- "acme-secret\|db01" && ng "L21b 출력(미착지 목록)에 사설 토큰이 찍혔다" || ok "L21b 출력에도 없다"
388
+ grep -q "기록하지" "$T/p21.tsv" && ok "L21c known-positive: 그 발화 자체는 프로브가 됐다" || ng "L21c 발화가 아예 안 잡혔다 — L21 은 공허하다"
389
+ else
390
+ ng "L21 프로브 덤프가 안 나왔다"
391
+ fi
392
+
393
+ # ── L22 기록이 짧아 양성 컨트롤을 못 만들면 UNMEASURED(rc=10), 착지 판정 없음 (codex #7) ──
394
+ printf '%s\n' '하네스 배선 착지' > "$T/rec22.md"
395
+ OUT22="$(bash "$SUT" "$T/tr.jsonl" "$T/rec22.md" 2>&1)"; RC22=$?
396
+ [ "$RC22" -eq 10 ] && ok "L22 컨트롤 재료 없음 → rc=10" || { ng "L22 rc=$RC22 (기대 10)"; echo "$OUT22" | sed 's/^/ /'; }
397
+ case "$OUT22" in *"양성 컨트롤 토큰을 못 뽑았다"*) ok "L22b 사유가 «컨트롤 재료 없음» 으로 명명된다 (계기 사망과 구분)" ;; *) ng "L22b 사유 문구 없음" ;; esac
398
+ case "$OUT22" in *"전부 착지"*) ng "L22c 컨트롤 없이 «전부 착지» 를 찍었다" ;; *) ok "L22c 착지 판정을 내지 않는다" ;; esac
399
+
400
+ # ── L23 런타임이 isMeta/isSidechain 으로 표시한 레코드는 발화가 아니다 (codex #4) ──
401
+ cat > "$T/tr23.jsonl" <<'JSONL'
402
+ {"type":"user","isMeta":true,"timestamp":"2026-09-05T05:30:00.000Z","message":{"content":"작업 큐 알림 재시도 필요 메타표시레코드 본문이다 발화가 아니다"}}
403
+ {"type":"user","isSidechain":true,"timestamp":"2026-09-05T05:31:00.000Z","message":{"content":"사이드체인 서브에이전트 대화 본문이다 운영자 발화가 아니다"}}
404
+ {"type":"user","timestamp":"2026-09-05T05:32:00.000Z","message":{"content":"같은 문장인데 표시가 없는 이 줄은 운영자 발화라 남아야 한다"}}
405
+ JSONL
406
+ N23=$(python3 "$EXTRACT" "$T/tr23.jsonl" --min-chars 20 2>/dev/null | grep -c .); case "$N23" in ''|*[!0-9]*) N23=0 ;; esac
407
+ [ "$N23" -eq 1 ] && ok "L23 isMeta·isSidechain 레코드 제외 · 표시 없는 1 건만 남는다" || ng "L23 $N23 건 (기대 1)"
408
+ python3 "$EXTRACT" "$T/tr23.jsonl" --min-chars 20 2>/dev/null | grep -q "표시가 없는" && ok "L23b known-positive: 남은 것이 표시 없는 발화다" || ng "L23b 표시 없는 발화까지 버렸다"
409
+ python3 "$EXTRACT" "$T/tr23.jsonl" --min-chars 20 2>/dev/null | grep -q "메타표시레코드\|사이드체인" && ng "L23c 표시된 레코드가 살아남았다" || ok "L23c 표시된 레코드는 없다 (known-negative)"
410
+
411
+ echo
412
+ echo "── utterance-intake lanes: PASS=$PASS FAIL=$FAIL ──"
413
+ [ "$FAIL" -eq 0 ] || exit 1
414
+ exit 0
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # test_worktree_reclaim_lanes.sh — known pairs for scripts/worktree_reclaim.sh (2026-09-05).
3
+ # Fixture: a real `git init` main checkout + a real `git worktree add`, gitignored tracks/ on both sides.
4
+ # The lanes assert the ORDER (list file before copy) as well as the verdicts — the list is the evidence
5
+ # that survives the copy, which is the whole reason the script exists.
6
+ set -uo pipefail
7
+ cd "$(dirname "$0")/.." || exit 1
8
+ ROOT="$(pwd -P)"
9
+ SUT="$ROOT/scripts/worktree_reclaim.sh" # own line: new_code_anchor_check resolves the variable per line
10
+ PASS=0; FAIL=0
11
+ ok() { echo " ✅ $1"; PASS=$((PASS+1)); }
12
+ ng() { echo " ❌ $1"; FAIL=$((FAIL+1)); }
13
+ [ -f "$SUT" ] || { echo "ⓘ worktree_reclaim.sh absent — subject missing when we looked (NOT a pass)"; exit 2; }
14
+ T="$(mktemp -d 2>/dev/null)" || { echo "ⓘ mktemp failed (NOT a pass)"; exit 10; }
15
+ trap 'rm -rf "$T"' EXIT INT TERM
16
+
17
+ mk_pair() { # $1=root → creates $1/main (checkout) + $1/wt (worktree); echoes nothing
18
+ mkdir -p "$1" && ( cd "$1" && git init -q main && cd main \
19
+ && git config user.email l@example.invalid && git config user.name lane \
20
+ && mkdir -p tracks/_meta && printf 'tracks/\n' > .gitignore && echo x > README.md \
21
+ && git add .gitignore README.md && git commit -qm init && git worktree add -q ../wt -b wt-branch ) 2>/dev/null
22
+ }
23
+
24
+ echo "── W1 enumerate: only-in files are listed, list file written, rc=1, nothing copied ──"
25
+ mk_pair "$T/a"; M="$T/a/main"; W="$T/a/wt"
26
+ mkdir -p "$W/tracks/_meta/sub"; echo "signal body" > "$W/tracks/_meta/fh_signal_lost.md"; echo nested > "$W/tracks/_meta/sub/deep.yaml"
27
+ OUT="$(bash "$SUT" "$W" 2>&1)"; RC=$?
28
+ [ "$RC" -eq 1 ] && ok "W1 rc=1 (only-in exists, not yet reclaimed)" || { ng "W1 rc=$RC (기대 1)"; printf '%s\n' "$OUT" | sed 's/^/ /'; }
29
+ N=$(printf '%s\n' "$OUT" | grep -c ' ONLY '); [ "$N" -eq 2 ] && ok "W1b 2 only-in files listed (nested dir walked)" || ng "W1b listed $N (기대 2)"
30
+ L=$(ls "$M"/tracks/_meta/dispatch/reclaim_wt_*.txt 2>/dev/null | wc -l | tr -d ' '); [ "$L" -eq 1 ] && ok "W1c list file written in MAIN tracks/_meta/dispatch" || ng "W1c list files: $L (기대 1)"
31
+ grep -q $'^ONLY\t_meta/sub/deep.yaml' "$M"/tracks/_meta/dispatch/reclaim_wt_*.txt && ok "W1d list carries the relative path" || ng "W1d list lacks the nested path"
32
+ [ ! -e "$M/tracks/_meta/fh_signal_lost.md" ] && ok "W1e enumerate mode copies nothing (known-negative)" || ng "W1e enumerate mode copied a file"
33
+
34
+ echo "── W2 --apply: copied, byte-verified, rc=0; re-run says nothing left ──"
35
+ OUT="$(bash "$SUT" "$W" --apply 2>&1)"; RC=$?
36
+ [ "$RC" -eq 0 ] && ok "W2 --apply rc=0" || { ng "W2 rc=$RC (기대 0)"; printf '%s\n' "$OUT" | sed 's/^/ /'; }
37
+ cmp -s "$W/tracks/_meta/fh_signal_lost.md" "$M/tracks/_meta/fh_signal_lost.md" && cmp -s "$W/tracks/_meta/sub/deep.yaml" "$M/tracks/_meta/sub/deep.yaml" \
38
+ && ok "W2b both files byte-identical in main" || ng "W2b copies differ or missing"
39
+ OUT="$(bash "$SUT" "$W" 2>&1)"; RC=$?
40
+ [ "$RC" -eq 0 ] && ok "W2c re-run after reclaim → rc=0 (idempotent)" || ng "W2c re-run rc=$RC"
41
+ case "$OUT" in *"safe to: git worktree remove"*) ok "W2d prints the next (human) step, never runs it" ;; *) ng "W2d no next-step line" ;; esac
42
+ [ -d "$W" ] && ok "W2e worktree still exists (script never removes)" || ng "W2e worktree vanished"
43
+
44
+ echo "── W3 differ-both-sides: listed, NOT copied, rc=1 even with --apply ──"
45
+ echo same > "$M/tracks/_meta/shared.md"; echo "changed in wt" > "$W/tracks/_meta/shared.md"
46
+ OUT="$(bash "$SUT" "$W" --apply 2>&1)"; RC=$?
47
+ [ "$RC" -eq 1 ] && ok "W3 rc=1 (needs a human)" || ng "W3 rc=$RC (기대 1)"
48
+ case "$OUT" in *"DIFF _meta/shared.md"*) ok "W3b the differing file is named" ;; *) ng "W3b DIFF not listed" ;; esac
49
+ grep -q '^same$' "$M/tracks/_meta/shared.md" && ok "W3c main copy untouched (known-negative: no overwrite)" || ng "W3c main copy was overwritten"
50
+
51
+ echo "── W4 list-before-copy: unwritable list dir → rc=10 and NO copy happened ──"
52
+ mk_pair "$T/b"; M="$T/b/main"; W="$T/b/wt"
53
+ mkdir -p "$W/tracks/_meta" && echo body > "$W/tracks/_meta/only.md"; rm -rf "$M/tracks/_meta/dispatch"; touch "$M/tracks/_meta/dispatch" # a FILE where the dir must be
54
+ [ -f "$W/tracks/_meta/only.md" ] && ok "W4-FIXTURE the only-in file exists in the worktree (potency)" || ng "W4-FIXTURE fixture file missing — W4b would pass vacuously"
55
+ OUT="$(bash "$SUT" "$W" --apply 2>&1)"; RC=$?
56
+ [ "$RC" -eq 10 ] && ok "W4 rc=10 when the evidence file cannot be written" || ng "W4 rc=$RC (기대 10)"
57
+ [ ! -e "$M/tracks/_meta/only.md" ] && ok "W4b nothing was copied before the list failed (order holds)" || ng "W4b copy happened without a list"
58
+
59
+ echo "── W5 argument guards ──"
60
+ OUT="$(bash "$SUT" "$M" 2>&1)"; RC=$?; [ "$RC" -eq 2 ] && ok "W5 main checkout as argument → rc=2" || ng "W5 rc=$RC (기대 2)"
61
+ OUT="$(bash "$SUT" "$T" 2>&1)"; RC=$?; [ "$RC" -eq 2 ] && ok "W5b non-repo dir → rc=2" || ng "W5b rc=$RC (기대 2)"
62
+ OUT="$(bash "$SUT" "$W" --bogus 2>&1)"; RC=$?; [ "$RC" -eq 2 ] && ok "W5c unknown flag → rc=2" || ng "W5c rc=$RC (기대 2)"
63
+ OUT="$(bash "$SUT" 2>&1)"; RC=$?; [ "$RC" -eq 2 ] && ok "W5d no argument → rc=2" || ng "W5d rc=$RC (기대 2)"
64
+ mk_pair "$T/c"; rm -rf "$T/c/wt/tracks"; OUT="$(bash "$SUT" "$T/c/wt" 2>&1)"; RC=$?
65
+ [ "$RC" -eq 0 ] && ok "W5e worktree without tracks/ → rc=0 nothing to reclaim" || ng "W5e rc=$RC (기대 0)"
66
+
67
+ echo
68
+ echo "── worktree-reclaim lanes: PASS=$PASS FAIL=$FAIL ──"
69
+ [ "$FAIL" -eq 0 ] || exit 1
70
+ exit 0