@chrono-meta/fh-gate 1.4.89 → 1.4.91

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 (47) hide show
  1. package/.claude/judgment_circuits.txt +14 -0
  2. package/.claude/rules/fh_4axis_gate.md +7 -0
  3. package/.claude-plugin/marketplace.json +2 -2
  4. package/AGENTS.md +25 -0
  5. package/CLAUDE.md +202 -12
  6. package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +105 -0
  7. package/knowledge/shared/harness-core/fh_three_layer_canon.md +307 -0
  8. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +100 -0
  9. package/knowledge/shared/harness-core/onboarding_acceleration_autopilot.md +3 -1
  10. package/knowledge/shared/harness-core/ship_readiness_gate.md +181 -13
  11. package/knowledge/shared/learnings/subagent_invocations_log.yaml +640 -0
  12. package/knowledge/shared/rules/multi_session_close_protocol.md +118 -0
  13. package/package.json +23 -2
  14. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  15. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  16. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +56 -8
  17. package/plugins/fh-meta/skills/install-wizard/SKILL.md +33 -0
  18. package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +104 -15
  19. package/scripts/branch_claim.sh +266 -0
  20. package/scripts/chamber_run.sh +64 -2
  21. package/scripts/chamber_witness.sh +439 -0
  22. package/scripts/compaction_probe.sh +456 -0
  23. package/scripts/digest_landing_check.sh +385 -0
  24. package/scripts/directional_diff_gate.sh +459 -0
  25. package/scripts/fh_env_delta_scan.sh +15 -0
  26. package/scripts/fh_session_load.sh +34 -1
  27. package/scripts/field_canon_preload.sh +129 -0
  28. package/scripts/hook_source_lib.sh +41 -0
  29. package/scripts/judgment_circuit_lint.sh +239 -0
  30. package/scripts/novelty_claim_check.sh +193 -0
  31. package/scripts/relay_channel.sh +645 -0
  32. package/scripts/reviewer_capability_corpus.tsv +124 -0
  33. package/scripts/selfcheck.sh +106 -0
  34. package/scripts/session_close_check.sh +134 -1
  35. package/scripts/test_branch_claim_lanes.sh +231 -0
  36. package/scripts/test_dispatch_log_lanes.sh +35 -1
  37. package/scripts/test_field_canon_lanes.sh +142 -0
  38. package/scripts/test_hook_source_gate_lanes.sh +81 -0
  39. package/scripts/test_marker_crossfamily_lanes.sh +132 -0
  40. package/scripts/test_marker_floor_lanes.sh +9 -8
  41. package/scripts/test_relay_channel_lanes.sh +583 -0
  42. package/scripts/test_reviewer_capability_conformance.sh +173 -0
  43. package/scripts/test_wizard_snippet_merge_lanes.sh +104 -11
  44. package/scripts/utterance_landing_check.sh +209 -0
  45. package/templates/.git-hooks/pre-commit +297 -13
  46. package/templates/settings.Compaction.snippet.json +56 -0
  47. package/templates/settings.FieldCanon.snippet.json +51 -0
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env bash
2
+ # digest_landing_check.sh — frontier digest 가 낸 **적용 후보가 실제로 착지했는지** 검증한다.
3
+ #
4
+ # ─────────────────────────────────────────────────────────────────────────────
5
+ # 무엇을 푸는가 (identity ④: 프런티어→조직 전파)
6
+ # ─────────────────────────────────────────────────────────────────────────────
7
+ # `ship_readiness_gate.md` 의 ④ 는 이렇게 적혀 있었다:
8
+ #
9
+ # "frontier-digest launchd auto + AX submission docs both real,
10
+ # but digest→org **never closed as ONE pipeline**"
11
+ #
12
+ # 두 조각은 실재한다 — digest 는 매일 자동 생성되고, 조직 산출물도 실재한다.
13
+ # 없는 것은 **둘을 잇는 증거**다. 정확히 말하면 *"안 닫힌다"* 가 아니라
14
+ # **"닫히는데 증인이 없다"** 이다(identity ② 의 순서 증인과 같은 얼굴).
15
+ #
16
+ # 실측 근거 — 2026-08-08 digest 의 후보 `M2`(`allow_force_pushes` 잔여 재시도)는 같은 날
17
+ # 저녁 실측으로 다뤄졌고(그 잔여가 **거짓**임이 밝혀짐) 세션 카드에 최상단 항목으로 올라갔다.
18
+ # 즉 **파이프라인은 그날 실제로 관통했다.** 그런데 어디에도 *"이건 그 digest 후보가
19
+ # 발원지다"* 라는 연결이 없다 — 사람 기억으로만 이어졌다. 이 스크립트가 그 연결을 잰다.
20
+ #
21
+ # ─────────────────────────────────────────────────────────────────────────────
22
+ # no-reinvention — 검증 로직은 짓지 않는다
23
+ # ─────────────────────────────────────────────────────────────────────────────
24
+ # 착지 검증기는 이미 있다: `utterance_landing_check.sh` (probes.tsv + **컨트롤 필수** +
25
+ # rc 3분기). 이 스크립트가 새로 하는 일은 **추출뿐**이다 — digest 표를 probes.tsv 로 옮긴다.
26
+ # 검증 로직을 여기에 다시 쓰면 **중복 정규화기**가 되고, 두 벌의 관대함이 갈리는 순간
27
+ # 한쪽만 통과하는 입력이 무음 드롭된다([[feedback_divergent_leniency_duplicate_normalizers]]).
28
+ #
29
+ # ─────────────────────────────────────────────────────────────────────────────
30
+ # 판별자를 어떻게 고르나 — digest 자신이 답을 적어놨다
31
+ # ─────────────────────────────────────────────────────────────────────────────
32
+ # 2026-08-08 digest §Warning Signals: *"히트 수만 보고 판정하면 안 되고 히트를 열어야 한다.
33
+ # 판별자는 **사건 고유 토큰**(도메인·URL)으로 고르는 게 낫다."*
34
+ # 그래서 후보 본문의 **백틱 코드 스팬**과 **`[[위키링크]]`** 만 판별자로 쓴다. 산문 문구를
35
+ # 정규식으로 걸면 문구가 바뀌는 순간 죽는다(착지 검증기 헤더의 명명된 잔여와 같은 함정).
36
+ #
37
+ # ⚠️ **판별 토큰을 못 뽑은 후보는 `UNCHECKABLE` 로 인쇄한다.** 조용히 빼면 착지율이
38
+ # 부풀려진다 — 미검증을 분모에서 지우는 것은 `not found ≠ 0` 위반이다.
39
+ #
40
+ # ─────────────────────────────────────────────────────────────────────────────
41
+ # 스코프 (운영자 결정, 2026-08-09)
42
+ # ─────────────────────────────────────────────────────────────────────────────
43
+ # 착지 대상 = **공개 FH 자산 + `tracks/`**. 비공개 companion store 는 **보지 않는다** —
44
+ # 스캔은 로컬에서 가능하지만 결과를 공개 파일에 쓰면 residency 위반이 된다.
45
+ # 따라서 이 계기가 재는 것은 *"조직 전체 전파"* 가 아니라 **"허브 내부 착지"** 다.
46
+ # 그 차이는 판정에 반드시 붙는다(아래 출력이 매번 스코프를 인쇄한다).
47
+ #
48
+ # ─────────────────────────────────────────────────────────────────────────────
49
+ # 🟡 알려진 한계 — RC 다. 단 **선별기이지 판정기가 아니다** (2026-08-09)
50
+ # ─────────────────────────────────────────────────────────────────────────────
51
+ # **닫힌 것 — mtime 오염.** 초판은 선후를 mtime 하나로 재서, 브랜치 전환이 갱신한 mtime 때문에
52
+ # *내용은 digest 이전인* 파일이 필터를 통과했다(실측: `CLAUDE.md` mtime 21:45 vs 내용은 그 전 →
53
+ # 원래 있던 토큰이 착지로 잡혀 미착지 2건이 초록으로 뒤집힘). 지금은 **두 축을 분리**한다:
54
+ # git 추적 → `git log --since=@<digest mtime>` (커밋 시각)
55
+ # gitignored(`tracks/**`) → mtime (그 축엔 다른 증거가 없다)
56
+ # dirty tracked → **UNMEASURED** 로 셈에 남긴다(커밋 안 된 수정엔 git 시각 증거가 없다)
57
+ # 레인 2종이 그 분리를 고정한다 — mtime 만 새 tracked 는 **안 세고**(a), 커밋만 새 tracked 는
58
+ # mtime 이 옛것이어도 **잡는다**(b). (b)가 없으면 *"tracked 를 전부 버려 negative 만 통과하는
59
+ # 하네스"* 가 된다(cross-family/codex 지목).
60
+ #
61
+ # **안 닫힌 것 — `file-change ≠ token-introduction`.** 파일이 digest 이후 진짜 커밋됐어도
62
+ # 그 토큰은 원래 있던 것일 수 있다(실측: `CLAUDE.md` 는 챔버 실적 수정으로 커밋됐고 후보와
63
+ # 무관하다). 닫으려면 **토큰 수준 diff**(`git log -S`)가 필요한데 후보마다 타깃이 갈려
64
+ # 착지 검증기 인터페이스와 맞지 않는다 — 재설계 사안으로 남긴다.
65
+ #
66
+ # 📌 그래서 이 계기의 출력은 **선별기다.** 히트는 반드시 열어라. 실측 4회 중 4회 손검증이
67
+ # 무언가를 잡았다(자기참조 → 토큰 절단 → mtime 오염 → file-change≠token-introduction).
68
+ #
69
+ # ── 사용 ──
70
+ # digest_landing_check.sh <digest.md> [target ...] # 미지정 시 기본 타깃
71
+ # digest_landing_check.sh --extract <digest.md> # probes.tsv 만 출력
72
+ # digest_landing_check.sh --self-test
73
+ #
74
+ # ── 종료 코드 (착지 검증기의 것을 그대로 물려받는다) ──
75
+ # 0 전건 착지 · 1 미착지 있음 · 10 HARNESS-ERROR(컨트롤 사망/입력 불량)
76
+ # ⚠️ UNCHECKABLE 이 있으면 **0 을 내지 않는다** — 재지 못한 것을 통과로 렌더하지 않는다.
77
+
78
+ set -uo pipefail
79
+
80
+ # ⚠️ `CLAUDE_PROJECT_DIR` 를 우선한다. 초판은 스크립트 위치로만 루트를 잡아서,
81
+ # 격리 픽스처에서 **실제 레포를 봤다**(타깃 279건). 기존 레인은 타깃을 인자로 넘겼기 때문에
82
+ # **기본 타깃 선택 경로가 한 번도 테스트되지 않았다** — 자기 self-test 의 사각이었다.
83
+ SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
84
+ FH="${CLAUDE_PROJECT_DIR:-$(cd "$SELF_DIR/.." && pwd)}"
85
+ # ⚠️ 검증기는 **도구**이고 FH 는 **대상**이다 — 두 축을 같은 변수로 묶으면 격리 픽스처에서
86
+ # 도구까지 임시 트리에서 찾다가 죽는다(실측). 도구는 언제나 이 스크립트 옆에 있다.
87
+ CHECKER="$SELF_DIR/utterance_landing_check.sh"
88
+ SECTION_RE='^## .*Immediate Application Candidates'
89
+
90
+ # 후보 표에서 (id, 판별토큰 OR 정규식) 을 뽑는다. 토큰 없으면 id 만 내고 정규식은 빈 값.
91
+ _extract() { # $1 = digest path
92
+ awk -v sec="$SECTION_RE" '
93
+ $0 ~ sec { inblk=1; next }
94
+ inblk && /^## / { inblk=0 }
95
+ inblk && /^\|/ {
96
+ line=$0
97
+ # 헤더/구분선 제외
98
+ if (line ~ /^\|[[:space:]]*#/) next
99
+ if (line ~ /^\|[-:| ]+\|$/) next
100
+ # 1열 = id (볼드 제거)
101
+ n=split(line, cell, "|")
102
+ if (n < 4) next
103
+ id=cell[2]; gsub(/[*[:space:]]/, "", id)
104
+ if (id == "") next
105
+ body=""
106
+ for (i=4; i<=n; i++) body = body cell[i] "|"
107
+ # 판별자 = 백틱 스팬 + [[위키링크]]
108
+ toks=""
109
+ # 백틱 스팬을 [[위키링크]] 와 **같은 형태로** 모아 한 번에 훑는다.
110
+ # 초판은 헤더에 "백틱 + 위키링크" 라고 적고 **코드는 백틱만** 뽑았다 —
111
+ # 주석이 말하는데 코드가 안 하는 것(cross-family 가 어제 F7 로 지목한 클래스).
112
+ tmp=body
113
+ while (match(tmp, /\[\[[^]]+\]\]/)) {
114
+ w=substr(tmp, RSTART+2, RLENGTH-4)
115
+ tmp=substr(tmp, RSTART+RLENGTH)
116
+ if (length(w) >= 5) toks = (toks == "" ? w : toks "|" w)
117
+ }
118
+ tmp=body
119
+ while (match(tmp, /`[^`]+`/)) {
120
+ t=substr(tmp, RSTART+1, RLENGTH-2)
121
+ tmp=substr(tmp, RSTART+RLENGTH)
122
+ # ⚠️ 콜론/공백 뒤를 자른다. `allow_force_pushes: true` 를 통째로 걸면 실제 착지문
123
+ # (`allow_force_pushes` 만 쓴 카드)을 **놓친다** — 실측으로 확인된 오검출 방향.
124
+ sub(/[: ].*$/, "", t)
125
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", t)
126
+ # 판별력 없는 짧은/일반 토큰은 버린다
127
+ if (length(t) < 5) continue
128
+ # 정규식 메타는 통째로 버린다(escape 실패가 무음 오탐을 만든다)
129
+ if (t ~ /[][(){}.*+?^$\\|]/) {
130
+ # 점·언더스코어만 있는 파일명류는 살린다 (예: plugin.json)
131
+ if (t ~ /^[A-Za-z0-9_.-]+$/) { } else continue
132
+ }
133
+ gsub(/\./, "\\.", t)
134
+ toks = (toks == "" ? t : toks "|" t)
135
+ }
136
+ print id "\t" toks
137
+ }
138
+ ' "$1"
139
+ }
140
+
141
+ do_extract() { # $1 = digest
142
+ local d="$1" id toks n=0 unchk=0
143
+ [ -f "$d" ] || { echo "❌ digest 없음: $d" >&2; return 10; }
144
+ # 컨트롤 — 이 계기가 살아 있는지 증명한다. 대상 트리에 확실히 존재하는 토큰.
145
+ # 컨트롤이 죽으면 착지 검증기가 rc=10 을 내고 타깃 결과를 인쇄하지 않는다.
146
+ printf 'CONTROL\tforge-harness\t컨트롤: 레포명\n'
147
+ printf 'CONTROL\tsession\t컨트롤: 흔한 토큰\n'
148
+ while IFS=$'\t' read -r id toks; do
149
+ [ -n "${id:-}" ] || continue
150
+ n=$((n+1))
151
+ if [ -z "${toks:-}" ]; then
152
+ unchk=$((unchk+1))
153
+ printf '# UNCHECKABLE\t%s\t판별 토큰(백틱/위키링크) 없음 — 재지 못함\n' "$id"
154
+ continue
155
+ fi
156
+ printf 'TARGET\t%s\t후보 %s\n' "$toks" "$id"
157
+ done < <(_extract "$d")
158
+ if [ "$n" -eq 0 ]; then
159
+ echo "❌ 후보 절을 못 찾았다 (섹션 정규식 불일치?) — 0건과 미검출을 가르기 위해 실패로 낸다" >&2
160
+ return 10
161
+ fi
162
+ return 0
163
+ }
164
+
165
+ do_check() {
166
+ local d="${1:-}"; shift || true
167
+ [ -n "$d" ] && [ -f "$d" ] || { echo "❌ usage: digest_landing_check.sh <digest.md> [target ...]" >&2; return 10; }
168
+ [ -x "$CHECKER" ] || [ -f "$CHECKER" ] || {
169
+ echo "❌ 착지 검증기 부재: $CHECKER (이 스크립트는 검증을 자체 구현하지 않는다)" >&2; return 10; }
170
+
171
+ local -a targets
172
+ if [ "$#" -gt 0 ]; then targets=("$@")
173
+ else
174
+ # 기본 스코프 = 공개 FH 자산 + tracks. **비공개 store 제외**(운영자 결정).
175
+ targets=()
176
+ # ⚠️ CLAUDE.md 도 **선후 필터를 거친다.** 초판은 무조건 추가했고, 그래서 이 파일 하나가
177
+ # 필터를 우회해 오탐 원천이 됐다 — 손검증에서 "M1 착지 1파일" 의 그 1파일이 CLAUDE.md
178
+ # 였고, 후보와 무관하게 원래 있던 토큰이었다. 예외 경로 하나가 필터 전체를 무력화한다.
179
+ [ -f "$FH/CLAUDE.md" ] && [ "$FH/CLAUDE.md" -nt "$d" ] && targets+=("$FH/CLAUDE.md")
180
+ # ★ **digest 보다 나중에 수정된 파일만** 본다. grep 은 "토큰이 있다" 만 재고
181
+ # "이 후보 때문에 생겼다" 는 못 잰다 — 이미 있던 문서가 착지로 잡힌다(실측: 후보 하나가
182
+ # 21파일 히트, 전부 무관한 기존 문서였다). 선후 필터가 인과를 주지는 않지만
183
+ # **"그 뒤에 쓰였다"** 는 잡아서 판별력을 크게 올린다.
184
+ # ⚠️ 명명된 잔여: mtime 기반이라 sync/checkout 으로 흔들린다. `tracks/**` 가 gitignored 라
185
+ # git 시각을 못 쓰는 것이 원인이고, 이건 챔버 순서 증인이 만난 것과 같은 제약이다.
186
+ # ★ **두 축을 분리한다** (cross-family/codex 설계, 2026-08-09).
187
+ # git 추적 파일 → **커밋 시각**(`git log --since=@epoch`). mtime 은 브랜치 전환·체크아웃으로
188
+ # 갱신되므로 tracked 에서는 신뢰할 수 없다 — 실측으로 확인된 오탐 원인.
189
+ # gitignored → mtime 밖에 없다(`tracks/**`). 남는 취약면이고 아래에 명명한다.
190
+ local _dts; _dts="$(stat -f %m "$d" 2>/dev/null || stat -c %Y "$d" 2>/dev/null)"
191
+ if [ -z "${_dts:-}" ]; then
192
+ echo "❌ digest mtime 을 못 읽었다 — 선후 판정 불가" >&2; return 10
193
+ fi
194
+ while IFS= read -r f; do
195
+ [ -n "$f" ] || continue
196
+ case "$f" in *.md) ;; *) continue ;; esac
197
+ [ -f "$FH/$f" ] && targets+=("$FH/$f")
198
+ done < <(git -C "$FH" log --since="@$_dts" --name-only --pretty=format: -- knowledge CLAUDE.md 2>/dev/null | sort -u)
199
+ while IFS= read -r f; do targets+=("$f"); done < <(
200
+ find "$FH/tracks/_meta" -name '*.md' -type f -newer "$d" 2>/dev/null | head -400)
201
+ # dirty tracked — 커밋 안 된 수정본엔 git 시각 증거가 없다. 초록으로 만들지 않고 셈에 남긴다.
202
+ DIRTY_TRACKED="$(git -C "$FH" diff --name-only -- knowledge CLAUDE.md 2>/dev/null | grep -c '[.]md$')" || true
203
+ DIRTY_TRACKED="${DIRTY_TRACKED//[^0-9]/}"; DIRTY_TRACKED="${DIRTY_TRACKED:-0}"
204
+ fi
205
+ # ★ 자기참조 차단 — **이 계기의 첫 실물 실행에서 100% 오탐을 낸 원인**이다.
206
+ # digest 자신(과 그 로그)이 타깃에 있으면 모든 후보 토큰이 거기서 발견되므로
207
+ # **전건이 자동으로 "착지"** 가 된다. 손검증 전까지 3/3 초록이었고 실제 착지는 0이었다.
208
+ local _dbase; _dbase="$(basename "$d")"
209
+ local -a _filtered=()
210
+ local _t
211
+ for _t in "${targets[@]}"; do
212
+ case "$_t" in
213
+ *"$_dbase") continue ;; # digest 자신
214
+ */logs/*) continue ;; # 그 생성 로그
215
+ *frontier_digest_*) continue ;; # 다른 날짜 digest (후보가 이월되며 반복 등장한다)
216
+ esac
217
+ _filtered+=("$_t")
218
+ done
219
+ targets=("${_filtered[@]}")
220
+ [ "${#targets[@]}" -gt 0 ] || { echo "❌ 타깃 0건" >&2; return 10; }
221
+
222
+ local probes; probes="$(mktemp -t dlc.XXXXXX)" || return 10
223
+ if ! do_extract "$d" > "$probes"; then rm -f "$probes"; return 10; fi
224
+
225
+ # ⚠️ `grep -c … || echo 0` 은 **매치 0일 때 rc=1** 이라 폴백이 붙어 "0\n0" 을 만든다.
226
+ # 그 두 줄짜리 값이 뒤의 `-gt` 비교를 bash 에러(=거짓)로 만들어 **가드를 무음 통과**시킨다
227
+ # ([[feedback_pipefail_fallback_disarms_guard]]). 파이프를 분해하고 정수로 소독한다.
228
+ local unchk; unchk="$(grep -c '^# UNCHECKABLE' "$probes" 2>/dev/null)" || true
229
+ unchk="${unchk//[^0-9]/}"; unchk="${unchk:-0}"
230
+ echo "── digest 착지 검증: $(basename "$d") ──"
231
+ echo " 스코프: 공개 FH 자산 + tracks/ (비공개 companion store 제외 — '조직 전파'가 아니라 '허브 내부 착지')"
232
+ echo " 타깃 파일 ${#targets[@]}건 (digest 이후 수정분만) · 판별불가 후보 ${unchk}건"
233
+ echo " 축: git추적=커밋시각 · gitignored=mtime · dirty tracked ${DIRTY_TRACKED:-0}건=UNMEASURED"
234
+ echo " ⚠️ 이 계기는 **선후**를 잰다. 인과가 아니다 — 히트는 열어서 확인하라"
235
+ echo
236
+
237
+ bash "$CHECKER" "$probes" "${targets[@]}"
238
+ local rc=$?
239
+ rm -f "$probes"
240
+
241
+ # ⚠️ 재지 못한 후보가 있으면 전건 착지라도 0 을 내지 않는다.
242
+ if [ "$rc" -eq 0 ] && [ "${unchk:-0}" -gt 0 ]; then
243
+ echo
244
+ echo "🟡 착지한 것은 전부 착지했지만 **판별불가 후보 ${unchk}건은 재지 못했다.**"
245
+ echo " 전건 통과로 렌더하지 않는다 (미측정 ≠ 0). rc=1"
246
+ return 1
247
+ fi
248
+ return "$rc"
249
+ }
250
+
251
+ do_self_test() {
252
+ local T; T="$(mktemp -d -t dlc_t.XXXXXX)" || return 10
253
+ local pass=0 fail=0 rc
254
+ _t() { if [ "$2" = "$3" ]; then echo "✅ $1 → rc=$3"; pass=$((pass+1));
255
+ else echo "❌ $1 → rc=$3 (기대 $2)"; fail=$((fail+1)); fi }
256
+
257
+ # known-pair 픽스처. 실물 2026-08-08 digest 의 구조를 그대로 본뜬다.
258
+ cat > "$T/digest.md" <<'EOF'
259
+ # digest
260
+ ## 📌 FH Immediate Application Candidates
261
+
262
+ | # | 티어 | 내용 |
263
+ |---|---|---|
264
+ | **M2** | **M** | `allow_force_pushes` 잔여 재시도 — 실측이 붙었다 |
265
+ | **R2** | R | `prime_agent_sister_asset` 기록 — 코드 직독 필요 |
266
+
267
+ ## ⚠️ Warning Signals
268
+ EOF
269
+ # positive 타깃: M2 토큰이 있다. negative(R2)는 없다.
270
+ mkdir -p "$T/tgt"
271
+ printf 'forge-harness session card\nallow_force_pushes 잔여가 거짓임을 실측했다\n' > "$T/tgt/card.md"
272
+
273
+ # P1 — 착지분과 미착지분이 섞이면 rc=1 (미착지 있음)
274
+ rc=0; bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" "$T/digest.md" "$T/tgt/card.md" >/dev/null 2>&1 || rc=$?
275
+ _t "M2 착지 · R2 미착지 → 미착지 검출(1)" 1 "$rc"
276
+
277
+ # P2 — 둘 다 착지시키면 0
278
+ printf 'prime_agent_sister_asset 도 기록했다\n' >> "$T/tgt/card.md"
279
+ rc=0; bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" "$T/digest.md" "$T/tgt/card.md" >/dev/null 2>&1 || rc=$?
280
+ _t "둘 다 착지 → 전건 착지(0)" 0 "$rc"
281
+
282
+ # N1 — 컨트롤 사망(타깃에 컨트롤 토큰이 없다) → HARNESS-ERROR(10), PASS 도 FAIL 도 아님
283
+ printf 'allow_force_pushes\nprime_agent_sister_asset\n' > "$T/tgt/nocontrol.md"
284
+ rc=0; bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" "$T/digest.md" "$T/tgt/nocontrol.md" >/dev/null 2>&1 || rc=$?
285
+ _t "컨트롤 사망 → HARNESS-ERROR(10), 착지로 안 읽는다" 10 "$rc"
286
+
287
+ # N2 — 판별 토큰 없는 후보는 UNCHECKABLE 이고, 전건 착지라도 0 을 내지 않는다
288
+ cat > "$T/digest_unchk.md" <<'EOF'
289
+ # digest
290
+ ## 📌 FH Immediate Application Candidates
291
+
292
+ | # | 티어 | 내용 |
293
+ |---|---|---|
294
+ | **S9** | S | 판별 토큰이 하나도 없는 산문 후보다 |
295
+
296
+ ## end
297
+ EOF
298
+ # ── N-git ★ 두 축 분리 검증 (cross-family/codex 설계) ──────────────────────
299
+ # 임시 git 레포에서 **mtime 과 커밋시각이 어긋난** 두 상태를 만들어 각각 잰다.
300
+ # 이 두 레인이 없으면 mtime 오염 수리가 회귀해도 초록이 난다.
301
+ local G; G="$(mktemp -d -t dlc_g.XXXXXX)" || return 10
302
+ (
303
+ cd "$G" && git init -q . && git config user.email t@t && git config user.name t
304
+ mkdir -p knowledge tracks/_meta
305
+ printf 'forge-harness session\nstale_token_x\n' > knowledge/old.md
306
+ git add -A && GIT_AUTHOR_DATE="2001-01-01T00:00:00+0000" \
307
+ GIT_COMMITTER_DATE="2001-01-01T00:00:00+0000" git commit -qm base
308
+ printf '# d\n## 📌 FH Immediate Application Candidates\n\n| # | 티어 | 내용 |\n|---|---|---|\n| **A1** | M | `stale_token_x` 후보 |\n| **B1** | M | `fresh_token_y` 후보 |\n\n## end\n' \
309
+ > tracks/_meta/frontier_digest_2001_01_02.md
310
+ touch -t 200101020000 tracks/_meta/frontier_digest_2001_01_02.md
311
+ printf 'forge-harness session\nfresh_token_y\n' > knowledge/new.md
312
+ git add -A && GIT_AUTHOR_DATE="2001-01-03T00:00:00+0000" \
313
+ GIT_COMMITTER_DATE="2001-01-03T00:00:00+0000" git commit -qm later
314
+ touch -t 200101010000 knowledge/new.md
315
+ touch knowledge/old.md
316
+ printf 'forge-harness session control\n' > tracks/_meta/card.md
317
+ ) >/dev/null 2>&1
318
+ out="$( cd "$G" && CLAUDE_PROJECT_DIR="$G" bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" \
319
+ "$G/tracks/_meta/frontier_digest_2001_01_02.md" 2>&1 )"
320
+ # ⚠️ **줄 단위로** 본다. `case *"A1"*"미착지"*` 는 마지막 요약줄("N건 중 M건 미착지")까지
321
+ # 삼켜 오매칭한다 — 문자열 위치로 판정하는 prose-grep 함정([[feedback_typed_verdict_channel]]).
322
+ if printf '%s\n' "$out" | grep -qE '후보 A1[[:space:]]+미착지'; then
323
+ _t "★N-git a) mtime 만 새 tracked 는 착지로 안 센다" 0 0
324
+ else
325
+ echo "❌ N-git a) mtime 오염 파일이 착지로 잡혔다"; fail=$((fail+1))
326
+ fi
327
+ # b) 는 **positive arm** — 없으면 "tracked 를 전부 버려 negative 만 통과" 하는 하네스가 된다.
328
+ if printf '%s\n' "$out" | grep -qE '후보 B1[[:space:]]+[0-9]+파일'; then
329
+ _t "★N-git b) 커밋시각이 새 tracked 는 mtime 이 옛것이어도 잡는다" 0 0
330
+ else
331
+ echo "❌ N-git b) tracked 를 통째로 버렸다(negative 만 통과하는 하네스)"; fail=$((fail+1))
332
+ fi
333
+ rm -rf "$G"
334
+
335
+ # N2a — 후보가 **전부** 판별불가면 TARGET 이 0건이다. 이건 "미착지" 가 아니라
336
+ # **계기 부적용**이고, 착지 검증기가 rc=10(PASS 도 FAIL 도 아님)을 낸다.
337
+ # 초판은 여기 기대값을 1 로 적었는데 **내 기대가 틀렸다** — 전부 못 쟀으면 에러다.
338
+ rc=0; bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" "$T/digest_unchk.md" "$T/tgt/card.md" >/dev/null 2>&1 || rc=$?
339
+ _t "판별불가만 있는 digest → HARNESS-ERROR(10), '착지 0건' 아님" 10 "$rc"
340
+
341
+ # N2b ★ 진짜 중요한 레인 — **혼합**. 잴 수 있는 건 전부 착지했는데 판별불가가 섞였다.
342
+ # 여기서 0 을 내면 "전건 착지" 로 읽히고 **분모에서 미측정이 사라진다.**
343
+ cat > "$T/digest_mixed.md" <<'EOF'
344
+ # digest
345
+ ## 📌 FH Immediate Application Candidates
346
+
347
+ | # | 티어 | 내용 |
348
+ |---|---|---|
349
+ | **M2** | **M** | `allow_force_pushes` 잔여 재시도 |
350
+ | **S9** | S | 판별 토큰이 하나도 없는 산문 후보다 |
351
+
352
+ ## end
353
+ EOF
354
+ rc=0; bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" "$T/digest_mixed.md" "$T/tgt/card.md" >/dev/null 2>&1 || rc=$?
355
+ _t "★혼합(착지 전건 + 판별불가 1) → 0 아님(미측정을 분모에서 안 지운다)" 1 "$rc"
356
+
357
+ # N3 — 후보 절이 없는 digest → 10 (0건과 미검출을 가른다)
358
+ printf '# digest\n## 다른 절\n내용\n' > "$T/nosection.md"
359
+ rc=0; bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" "$T/nosection.md" "$T/tgt/card.md" >/dev/null 2>&1 || rc=$?
360
+ _t "후보 절 부재 → HARNESS-ERROR(10), '0건 착지' 아님" 10 "$rc"
361
+
362
+ # N4 — digest 파일 자체가 없음 → 10
363
+ rc=0; bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" "$T/does-not-exist.md" >/dev/null 2>&1 || rc=$?
364
+ _t "digest 부재 → rc=10" 10 "$rc"
365
+
366
+ # N5 컨트롤 — P2 가 여전히 0 인가 (앞 레인이 상태를 오염시키지 않았는지)
367
+ rc=0; bash "$SELF_DIR/$(basename "${BASH_SOURCE[0]}")" "$T/digest.md" "$T/tgt/card.md" >/dev/null 2>&1 || rc=$?
368
+ _t "컨트롤 — 정상 케이스 재확인" 0 "$rc"
369
+
370
+ rm -rf "$T"
371
+ echo
372
+ if [ "$fail" -eq 0 ]; then
373
+ echo "✅ 캘리브레이션 통과 ($pass 레인) — 착지/미착지/계기사망/미측정을 각각 다른 값으로 가른다."
374
+ return 0
375
+ fi
376
+ echo "🟥 캘리브레이션 실패 ($fail/$((pass+fail)))"
377
+ return 1
378
+ }
379
+
380
+ case "${1:---help}" in
381
+ --self-test) do_self_test ;;
382
+ --extract) shift; do_extract "${1:-}" ;;
383
+ --help|-h) sed -n '1,55p' "${BASH_SOURCE[0]}" | grep -E '^#( |$)' | sed 's/^# \{0,1\}//' ;;
384
+ *) do_check "$@" ;;
385
+ esac