@chrono-meta/fh-gate 2.3.0 → 2.4.0

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 (31) hide show
  1. package/.claude/rules/fh_4axis_gate.md +25 -0
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/CLAUDE.md +51 -6
  4. package/README.ja.md +14 -2
  5. package/README.ko.md +14 -1
  6. package/README.md +16 -2
  7. package/README.zh.md +12 -1
  8. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +231 -7
  9. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +215 -1
  10. package/knowledge/shared/harness-core/ship_readiness_gate.md +168 -1
  11. package/knowledge/shared/learnings/subagent_invocations_log.yaml +24 -0
  12. package/package.json +6 -1
  13. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  14. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  15. package/plugins/fh-meta/CHANGELOG.md +57 -0
  16. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +20 -0
  17. package/plugins/fh-meta/skills/steel-quench/SKILL.md +25 -2
  18. package/scripts/adapters/mate_agent_boundary.sh +27 -7
  19. package/scripts/capability_registry_check.sh +69 -3
  20. package/scripts/relay_channel.sh +151 -6
  21. package/scripts/selfcheck.sh +18 -0
  22. package/scripts/target_freeze.sh +239 -0
  23. package/scripts/test_adapter_lanes.sh +17 -0
  24. package/scripts/test_marker_standpoint_lanes.sh +83 -0
  25. package/scripts/test_marker_thirdparty_lanes.sh +73 -0
  26. package/scripts/test_regression_guard_ci_lanes.sh +170 -0
  27. package/scripts/test_relay_channel_lanes.sh +146 -17
  28. package/scripts/test_target_freeze_lanes.sh +203 -0
  29. package/templates/.git-hooks/pre-commit +242 -0
  30. package/templates/PRE-PUBLISH-CHECKLIST.md +22 -0
  31. package/templates/regression_guard.sh +32 -4
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env bash
2
+ # target_freeze.sh — 발주한 감사의 **대상을 동결**하고, 회수 시 그게 같은 대상인지 대조한다.
3
+ #
4
+ # ## 왜 (N=3, 2026-08-17 운영자 승인)
5
+ #
6
+ # 같은 결함이 하루에 세 번 났다: ① cross-family 리뷰에 **수리 이전** diff 발송 ② 리뷰 출력을
7
+ # `tail` 로 잘라 지적·핀 유실 ③ 스캔 후 파일 편집으로 라인 번호 밀림. 셋 다 「보낸 것/읽은
8
+ # 것이 지금 트리가 맞나」를 안 찍어서 생겼고, **산문 규율은 이미 있었는데 세 번 다 안 걸렸다**
9
+ # (`steel-quench/SKILL.md §PIN THE TARGET`). 그래서 산문이 아니라 스크립트다.
10
+ #
11
+ # ## 🟥 선행 실패 2종을 피한다 — 이건 3번째 시도다
12
+ #
13
+ # 같은 SKILL.md 가 두 번의 기계화 실패를 이미 기록해 뒀다:
14
+ # ① 훅 advisory → 마커 100% 에서 발화 = 배경 소음, 수렴 탐지기가 어휘를 못 맞춤 → 제거
15
+ # ② `base-SHA + diff 줄 수` 지문 → **in-place 편집에 불변**(산문 수리의 기본 형태!)이고
16
+ # untracked 에 구조적으로 맹목 → 제거
17
+ # ⇒ 그래서 여기서는 **줄 수가 아니라 내용을 해시**하고(①의 반대), untracked 를 **따로**
18
+ # 센다(②의 반대). 훅에 안 건다 — 발주자가 명시적으로 부르는 두 커맨드다.
19
+ #
20
+ # ## 설계 제약 (전부 이 레포의 기존 실패에서 나왔다)
21
+ # ⓐ tracked 변경 + untracked 를 **content-address** (steel-quench L450)
22
+ # ⓑ `assume-unchanged`/`skip-worktree` 은폐 검사 필수 (publish_freshness_check.sh:61)
23
+ # ⓒ git 실패 = **UNKNOWN**, 절대 clean 아님 (session_close_check.sh:38-48)
24
+ # ⓓ `.git` 전체를 지문에 넣지 않는다 — `index` 가 정당하게 갱신돼 거짓 불일치
25
+ # (capability_effect_probe.sh:253)
26
+ # ⓔ 판정 키는 트리가 아니라 **세션** (branch_claim.sh:13-20)
27
+ # ⓕ 사이드카는 트리를 못 읽는 경우가 많다 → **발주자 측 대조**여야 한다
28
+ # (auto-decorrelation/SKILL.md:191)
29
+ #
30
+ # ## 사용
31
+ # bash scripts/target_freeze.sh pin <repo> <label> # 발주 직전. 지문 토큰을 stdout 에
32
+ # bash scripts/target_freeze.sh verify <repo> <label> # 회수 직후. 불일치면 WRONG-TARGET
33
+ # bash scripts/target_freeze.sh show <repo> <label>
34
+ #
35
+ # ## 종료 코드
36
+ # 0 MATCH — 대상이 그대로다
37
+ # 1 WRONG-TARGET — 대상이 움직였다. **그 라운드의 판정을 무효화하라**
38
+ # 10 UNKNOWN — 잴 수 없었다(git 실패·해시 도구 부재·핀 없음). 미측정이지 통과 아님
39
+ #
40
+ # 🟥 **이 스크립트는 «내가 보낸 내용»이 아니라 «대상 트리»를 잰다.** 사이드카에 파일을
41
+ # 인라인해 보냈다면 그 인라인 내용의 해시도 따로 결박해야 한다(ⓕ) — 여기서는 안 한다.
42
+ # 범위를 넓게 주장하지 않으려고 명시한다.
43
+
44
+ set -u
45
+
46
+ _die_unknown() { printf 'UNKNOWN — %s\n' "$1" >&2; exit 10; }
47
+
48
+ # 🟥 **`git -C` 는 `GIT_DIR`/`GIT_WORK_TREE` 를 못 이긴다** (cross-family agy, 2026-08-18).
49
+ # 호출자 환경에 그 변수가 남아 있으면 이 스크립트는 **엉뚱한 레포**를 재고도 «정상 경로를
50
+ # 동결했다» 고 보고한다. 지문 계기가 대상을 틀리는 형태라 다른 모든 지적보다 상위다.
51
+ _git() { env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE git "$@"; }
52
+
53
+ _sha() {
54
+ # chamber_witness.sh:84 와 같은 형태(macOS/linux 폴백). 도구 없으면 fail-closed.
55
+ #
56
+ # 🟥 **`| awk` 로 잘라내지 않는다** (cross-family codex, 2026-08-18, 재현 확인).
57
+ # `shasum | awk` 는 shasum 의 rc 를 잃는다 — 해시 도구가 특정 입력에서 실패해도
58
+ # awk 는 rc 0 을 내고 stdout 이 비어, 서로 다른 내용이 **같은 빈 지문**을 받아 MATCH 가
59
+ # 된다. 게이트 안에서 이 형태가 fail-open 이다.
60
+ local out rc
61
+ if command -v shasum >/dev/null 2>&1; then out=$(shasum -a 256); rc=$?
62
+ elif command -v sha256sum >/dev/null 2>&1; then out=$(sha256sum); rc=$?
63
+ else return 3; fi
64
+ [ "$rc" -eq 0 ] || return 3
65
+ out=${out%% *}
66
+ [ -n "$out" ] || return 3
67
+ printf '%s\n' "$out"
68
+ }
69
+
70
+ _label_key() {
71
+ # 🟥 문자 치환으로 파일명을 만들지 않는다 — `a/b` 와 `a_b` 가 **같은 핀 파일**이 되어,
72
+ # 핀한 적 없는 라벨을 verify 하면 MATCH 가 나온다(cross-family codex, 재현 확인).
73
+ # 해시는 충돌하지 않으므로 라벨 공간이 파일명 공간에 안전하게 들어간다.
74
+ printf '%s' "$1" | _sha
75
+ }
76
+
77
+ _fingerprint() {
78
+ # stdout: <64자 해시> · 실패 시 비영 종료 + stdout 은 **빈다**
79
+ # 🟥 명령이 실패하면서 stdout 에 무언가를 뱉는 형태를 만들지 않는다 — 그러면 폴백 없이도
80
+ # 쓰레기 값이 지문이 되고, 대조가 «항상 일치» 로 조용히 무너진다.
81
+ local repo="$1" head diff_patch untracked hidden combined subs files rc
82
+
83
+ # 🟥 `HEAD` 가 아니라 **트리 해시**를 쓴다 (cross-family codex, 재현 확인). 이 지문의
84
+ # 주장은 «감사한 내용이 그대로인가» 이고, 빈 커밋(`--allow-empty`)은 내용을 안 바꾸는데
85
+ # HEAD 만 움직여 **정상 조작이 WRONG-TARGET** 이 됐다. 오탐이 잦으면 이 게이트는
86
+ # override 를 습관화시키고 그건 폐기 경로다.
87
+ head=$(_git -C "$repo" rev-parse "HEAD^{tree}" 2>&1) || { printf '%s\n' "$head" >&2; return 1; }
88
+
89
+ # ⓑ 은폐 비트 — 이게 켜져 있으면 tracked 변경이 diff 에 안 나온다. 지문이 거짓말을 하므로
90
+ # 「일치」를 낼 자격이 없다.
91
+ hidden=$(_git -C "$repo" ls-files -v 2>&1) || { printf '%s\n' "$hidden" >&2; return 1; }
92
+ if printf '%s\n' "$hidden" | grep -qE '^[hS]'; then
93
+ printf 'assume-unchanged/skip-worktree 비트가 켜져 있다 — 지문이 변경을 못 본다\n' >&2
94
+ return 2
95
+ fi
96
+
97
+ # ⓐ-1 tracked 변경: **줄 수가 아니라 패치 내용**. staged 포함(HEAD 기준).
98
+ diff_patch=$(_git -C "$repo" diff HEAD 2>&1) || { printf '%s\n' "$diff_patch" >&2; return 1; }
99
+
100
+ # ⓐ-2 서브모듈: `git diff HEAD` 는 서브모듈 포인터만 보고 **그 안의 dirty 내용은 못 본다**
101
+ # (cross-family codex, 재현 확인 — 서브모듈 파일을 고쳐도 MATCH 였다).
102
+ # `--recursive` 상태 문자열이 dirty 표식(`+`)과 SHA 를 함께 낸다.
103
+ # 🟥 `submodule status` **만으로는 부족하다**: 그 줄은 서브모듈의 HEAD 를 말하므로,
104
+ # 서브모듈 워킹트리를 고치기만 하면(커밋 없이) SHA 가 안 바뀌어 여전히 MATCH 였다
105
+ # (수리 1차본이 그랬고, 재현으로 잡았다). 각 서브모듈 **안으로 내려가** 상태와
106
+ # 패치 내용을 함께 해시에 넣는다.
107
+ subs=$(_git -C "$repo" submodule status --recursive 2>&1) || { printf '%s\n' "$subs" >&2; return 1; }
108
+ if [ -n "$subs" ]; then
109
+ # 🟥 미초기화 서브모듈(`-<sha> path`)은 `foreach` 가 **건너뛴다**(agy 지적). 그 안의
110
+ # 내용은 지문에 한 글자도 안 들어가므로 «같다» 를 낼 자격이 없다.
111
+ if printf '%s\n' "$subs" | grep -q '^-'; then
112
+ printf '미초기화 서브모듈이 있다 — 그 안을 못 잰다(git submodule update --init 후 재시도)\n' >&2
113
+ return 2
114
+ fi
115
+ local subdetail
116
+ subdetail=$(_git -C "$repo" submodule foreach --recursive --quiet \
117
+ 'git status --porcelain --untracked-files=all && git diff HEAD' 2>&1) \
118
+ || { printf '%s\n' "$subdetail" >&2; return 1; }
119
+ subs="${subs}"$'\n'"${subdetail}"
120
+ fi
121
+
122
+ # ⓐ-3 untracked(무시되지 않은 것): 경로 + 내용 해시.
123
+ # 🟥 **파이프 안에서 목록을 만들지 않는다** — `git ls-files -o | while … | sort` 는 git 의
124
+ # rc 를 마지막 `sort` 의 rc 로 덮는다. 목록 취득이 실패해도 «untracked 없음» 으로 접혀
125
+ # MATCH 가 나왔다(재현 확인). 목록을 먼저 변수로 받고 rc 를 본다.
126
+ # ★NUL 구분자는 `$( )` 가 삼킨다 — 목록은 **임시 파일**로 받아야 rc 도 보고 구분자도 산다.
127
+ files=$(mktemp) || return 3
128
+ _git -C "$repo" ls-files -o --exclude-standard -z >"$files" 2>/dev/null; rc=$?
129
+ if [ "$rc" -ne 0 ]; then
130
+ rm -f "$files"
131
+ printf 'ls-files -o 실패(rc=%s) — untracked 를 못 쟀다\n' "$rc" >&2
132
+ return 1
133
+ fi
134
+
135
+ untracked=""
136
+ while IFS= read -r -d '' f; do
137
+ local entry
138
+ if [ -L "$repo/$f" ]; then
139
+ # 🟥 심볼릭 링크는 **가리키는 문자열**이 내용이다. `-f` 로만 보면 깨진 링크의
140
+ # 재지정(retarget)을 통째로 놓친다(재현 확인).
141
+ entry="LINK $(readlink "$repo/$f" 2>/dev/null || printf 'UNREADABLE')"
142
+ elif [ -d "$repo/$f" ]; then
143
+ # 🟥 untracked **디렉토리**는 `ls-files -o` 가 한 항목으로 접는다(중첩 git repo 가
144
+ # 대표 사례). 그 안의 내용 변경을 이 지문은 못 본다 ⇒ «놓쳤다» 를 «같다» 로
145
+ # 렌더하지 않고 **잴 수 없음(UNKNOWN)으로 fail-closed** 한다.
146
+ printf 'untracked 디렉토리를 만났다(%s) — 내용까지 못 잰다\n' "$f" >&2
147
+ rm -f "$files"; return 2
148
+ elif [ -f "$repo/$f" ]; then
149
+ # 🟥 **모드도 내용이다** (agy 지적). tracked 파일은 `git diff` 가 `old mode/new mode`
150
+ # 를 내지만 untracked 는 스트림만 해시해서, `chmod +x` 가 지문에 안 잡혔다.
151
+ # 이 레포는 실행 비트 소실로 훅이 조용히 무장해제된 이력이 있다
152
+ # ([[feedback_exec_bit_loss_disarms_hooks_silently]]).
153
+ local mode; if [ -x "$repo/$f" ]; then mode=x; else mode=-; fi
154
+ entry="$mode $(_sha <"$repo/$f")" || { rm -f "$files"; return 3; }
155
+ case "$entry" in "$mode ") rm -f "$files"; return 3 ;; esac
156
+ else
157
+ entry="NOTAFILE"
158
+ fi
159
+ untracked="${untracked}${f} ${entry}"$'\n'
160
+ done <"$files"
161
+ rm -f "$files"
162
+ untracked=$(printf '%s' "$untracked" | LC_ALL=C sort)
163
+
164
+ # 🟥 **필드를 이어붙이고 한 번 해시하지 않는다** (agy 지적): `---` 는 diff 패치의 표준
165
+ # 헤더(`--- a/file`)라 필드 경계가 내용에 나타날 수 있고, 그러면 서로 다른 상태가 같은
166
+ # 결합 문자열을 만들 여지가 생긴다. 각 필드를 **먼저 해시**해 길이를 고정한 뒤 잇는다.
167
+ local h_head h_diff h_subs h_untracked
168
+ h_head=$(printf '%s' "$head" | _sha) || return 3
169
+ h_diff=$(printf '%s' "$diff_patch" | _sha) || return 3
170
+ h_subs=$(printf '%s' "$subs" | _sha) || return 3
171
+ h_untracked=$(printf '%s' "$untracked" | _sha) || return 3
172
+
173
+ # 🟥 **TOCTOU** (agy 지적): 위 단계들은 원자적 스냅샷이 아니다. 1단계 뒤에 누가
174
+ # `git commit -a` 를 하면 «옛 트리 + 빈 diff» 라는 **한 순간도 존재한 적 없는 상태**가
175
+ # 핀으로 저장되고, 그 뒤 아무 변경이 없어도 verify 가 WRONG-TARGET 을 낸다.
176
+ # ⇒ 끝에서 트리를 다시 읽어 **읽는 동안 움직였으면 지문을 내지 않는다**(fail-closed).
177
+ local head2
178
+ head2=$(_git -C "$repo" rev-parse "HEAD^{tree}" 2>/dev/null) || return 1
179
+ if [ "$head2" != "$head" ]; then
180
+ printf '지문 계산 도중 트리가 움직였다 — 스냅샷이 일관되지 않는다\n' >&2
181
+ return 2
182
+ fi
183
+
184
+ combined=$(printf '%s %s %s %s\n' "$h_head" "$h_diff" "$h_subs" "$h_untracked" | _sha) || return 3
185
+ [ -n "$combined" ] || return 3
186
+ printf '%s\n' "$combined"
187
+ }
188
+
189
+ _pin_path() {
190
+ local repo="$1" label="$2" gitdir sid
191
+ gitdir=$(_git -C "$repo" rev-parse --git-common-dir 2>/dev/null) || return 1
192
+ case "$gitdir" in /*) : ;; *) gitdir="$repo/$gitdir" ;; esac
193
+ # ⓔ 세션 키 — branch_claim.sh:86-91 과 같은 상속 경로.
194
+ sid="${CLAUDE_CODE_SESSION_ID:-${CLAUDE_SESSION_ID:-local}}"
195
+ printf '%s/fh-targets/%s\n' "$gitdir" "$sid"
196
+ }
197
+
198
+ main() {
199
+ local cmd="${1:-}" repo="${2:-}" label="${3:-}"
200
+ [ -n "$cmd" ] && [ -n "$repo" ] && [ -n "$label" ] || {
201
+ printf 'usage: target_freeze.sh {pin|verify|show} <repo> <label>\n' >&2; exit 10; }
202
+ [ -d "$repo" ] || _die_unknown "레포 경로가 없다: $repo"
203
+ _git -C "$repo" rev-parse --git-dir >/dev/null 2>&1 || _die_unknown "git 레포가 아니다: $repo"
204
+
205
+ local dir fp rc
206
+ dir=$(_pin_path "$repo" "$label") || _die_unknown "git-common-dir 를 못 읽었다"
207
+ local lkey; lkey=$(_label_key "$label") || _die_unknown "라벨 키를 못 만들었다(해시 도구 부재)"
208
+ local file="$dir/${lkey}.fp"
209
+
210
+ case "$cmd" in
211
+ pin)
212
+ fp=$(_fingerprint "$repo"); rc=$?
213
+ [ "$rc" -eq 0 ] && [ -n "$fp" ] || _die_unknown "지문을 못 냈다 (rc=$rc)"
214
+ mkdir -p "$dir" || _die_unknown "핀 저장 디렉토리를 못 만들었다"
215
+ printf '%s\n' "$fp" >"$file" || _die_unknown "핀을 못 썼다"
216
+ # 발주 프롬프트에 그대로 박을 한 줄.
217
+ printf 'TARGET-FINGERPRINT %s %s\n' "$label" "${fp:0:16}"
218
+ ;;
219
+ verify)
220
+ [ -f "$file" ] || _die_unknown "핀이 없다 — 발주 전에 pin 을 안 했다 ($label)"
221
+ local pinned; pinned=$(cat "$file") || _die_unknown "핀을 못 읽었다"
222
+ fp=$(_fingerprint "$repo"); rc=$?
223
+ [ "$rc" -eq 0 ] && [ -n "$fp" ] || _die_unknown "지문을 못 냈다 (rc=$rc)"
224
+ if [ "$fp" = "$pinned" ]; then
225
+ printf 'MATCH %s %s\n' "$label" "${fp:0:16}"; exit 0
226
+ fi
227
+ printf 'WRONG-TARGET %s — 핀 %s / 현재 %s\n' "$label" "${pinned:0:16}" "${fp:0:16}" >&2
228
+ printf '🟥 이 라운드의 판정을 **무효화**하라. 대상이 발주 이후 움직였다.\n' >&2
229
+ exit 1
230
+ ;;
231
+ show)
232
+ [ -f "$file" ] || _die_unknown "핀이 없다 ($label)"
233
+ printf 'PINNED %s %s\n' "$label" "$(cat "$file")"
234
+ ;;
235
+ *) _die_unknown "알 수 없는 커맨드: $cmd" ;;
236
+ esac
237
+ }
238
+
239
+ main "$@"
@@ -145,6 +145,23 @@ if [ -n "$MT_ROOT" ]; then
145
145
  else
146
146
  echo " ⏭ M4 [resolve-alias ] mate 실물 루트 없음 — SKIP (통과 아님)"
147
147
  fi
148
+ # 🟥 M2b/M2c — **대상 인자가 실제로 먹히는가** (2026-08-18 신설).
149
+ # 초판은 대상을 peer 진입점에 `$1` 로 넘겼는데 **peer 는 그 인자를 안 읽는다**
150
+ # (`TARGET` 하드코딩). 그래서 양성·음성 두 arm 이 **같은 파일**(peer 실물)을 재고 있었고,
151
+ # 음성의 초록은 판별력이 아니라 **실물이 마침 실패 상태**여서였다 — 캘리브레이션 쌍이
152
+ # 죽어 있었다. M2/M3 만으로는 그 상태를 못 가른다(둘 다 «기대대로» 나올 수 있다).
153
+ # ⇒ **픽스처를 변형해 판정이 따라오는지**를 본다. peer 실물 상태와 무관하게 성립한다.
154
+ if [ -n "$MT_ROOT" ]; then
155
+ _MUT="$T/mutated_positive.md"
156
+ # 양성 픽스처에서 T11(finding_id 어휘 제약) 한 줄만 뺀다.
157
+ grep -v '\^\[a-f0-9\]{6,64}\$' "$(dirname "$M")/fixtures/mate_agent_boundary_known_positive.md" >"$_MUT"
158
+ _lane M2b target-honored "양성에서 한 조항만 빼면 → FAIL (대상 인자가 실제로 먹힌다)" 1 \
159
+ "$(_rc bash "$M" --target "$_MUT")"
160
+ _lane M2c target-honored "원본 양성은 그대로 PASS (변형 레인의 컨트롤)" 0 \
161
+ "$(_rc bash "$M" --known-positive)"
162
+ else
163
+ echo " ⏭ M2b/M2c [target-honored] mate 실물 루트 없음 — SKIP (통과 아님)"
164
+ fi
148
165
  _lane_peer "$MT_ROOT" M5 enum "없는 대상 → HARNESS_ERROR(10). peer 헤더가 «위반 0 이 아니다» 라고 못 박은 값" 10 \
149
166
  "$(_rc bash "$M" --target "$T/does_not_exist.md")"
150
167
  _lane_peer "$MT_ROOT" M6 enum "알 수 없는 모드 → HARNESS_ERROR(10)" 10 "$(_rc bash "$M" --bogus-mode)"
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bash
2
+ # test_marker_standpoint_lanes.sh — regression fixtures for pre-commit's
3
+ # validate_standpoint_leg (typed standpoint verdict, 2026-08-17).
4
+ #
5
+ # WHY: `standpoint:` shipped 2026-08-14 with its value DELIBERATELY unvalidated — the doctrine
6
+ # said "mechanize on the first recorded false value, not before". That value has now been
7
+ # recorded (three, in fact): a `release_2.3.0` marker wrote `not-applicable` on a delta whose own
8
+ # grounds line concedes "소비자 install 의 게이트 수용은 바뀐다 (BREAKING 2건)", and two
9
+ # 2026-08-14 deltas that altered shipped gate scripts / a shipped SKILL.md carried no line at all.
10
+ # The threshold the doctrine set for this field is met; this lane is that mechanization.
11
+ #
12
+ # SCOPE — channel, not judgment (CLAUDE.md §Mechanization Boundary). These lanes assert the
13
+ # RECORD's properties: present · single · a member of the closed enum · not vacuous · not wearing
14
+ # the OTHER axis's tokens. They never assert the value is CORRECT.
15
+ #
16
+ # 🟥 NAMED RESIDUAL — the execution claim is WARN, not BLOCK. `tier2`/`tier2b`/`tier3` assert that
17
+ # something was EXECUTED in the target, and the doctrine calls that "the load-bearing half". A
18
+ # blocking check for it needs a vocabulary grep, and on first contact with the real corpus that
19
+ # grep over-blocked a legitimate marker whose grounds read "그 레포에서 실제로 호출해 양·음 arm 을
20
+ # 확인했다" — it simply did not know 「호출」. Over-blocking trains `--no-verify`, which would
21
+ # disarm the Destructive-Op gate living in the same hook, so the check warns. The cost is stated
22
+ # rather than hidden: a fabricated `tier2` with a fluent reason PASSES this lane. That hole is
23
+ # what §4-b (cross-family reads the marker) is for — it is not closed here.
24
+ #
25
+ # Fixtures assert BOTH directions (known-pair): every intended shape is admitted, and every hole
26
+ # the lane closes still blocks. Asserting only BLOCK cannot tell "blocked" from "blocked for the
27
+ # wrong reason"; asserting only PASS cannot see a lane that admits everything.
28
+ #
29
+ # Usage: bash scripts/test_marker_standpoint_lanes.sh Exit: 0 = all behave; 1 = regression.
30
+
31
+ set -uo pipefail
32
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
33
+ HOOK="$REPO_ROOT/templates/.git-hooks/pre-commit"
34
+ T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
35
+
36
+ sed -n '/^validate_standpoint_leg()/,/^}/p' "$HOOK" > "$T/fn.sh"
37
+
38
+ # Instrument calibration — an empty extraction would let every fixture "pass" against nothing.
39
+ if ! grep -q 'DEGRADED_NO_TARGET_ACCESS' "$T/fn.sh"; then
40
+ echo "❌ HARNESS-ERROR — validate_standpoint_leg did not extract from $HOOK."
41
+ echo " Fixtures below would measure an empty function. Aborting rather than reporting green."
42
+ exit 1
43
+ fi
44
+
45
+ FAIL=0
46
+ lane() { # $1 = id $2 = expect(BLOCK|PASS) $3 = marker body
47
+ local id="$1" expect="$2" body="$3" rc out
48
+ printf '%s\n' "$body" > "$T/m.marker"
49
+ out=$( bash -c 'set -uo pipefail; . "$1"; validate_standpoint_leg "$2"' _ "$T/fn.sh" "$T/m.marker" 2>&1 ); rc=$?
50
+ local got=PASS; [ $rc -ne 0 ] && got=BLOCK
51
+ if [ "$got" = "$expect" ]; then
52
+ printf ' ✅ %-34s %s\n' "$id" "$got"
53
+ else
54
+ printf ' ❌ %-34s expected %s, got %s\n %s\n' "$id" "$expect" "$got" "$(printf '%s' "$out" | head -2)"
55
+ FAIL=1
56
+ fi
57
+ }
58
+
59
+ echo "== standpoint lanes — BLOCK side (holes that must stay closed) =="
60
+ lane S1-missing BLOCK 'axis2-model: opus'
61
+ lane S2-duplicate BLOCK 'standpoint: tier1
62
+ standpoint: not-applicable — 나중에 고침'
63
+ lane S3-bare-na BLOCK 'standpoint: not-applicable'
64
+ lane S4-unknown-value BLOCK 'standpoint: tier9(qasp) — 새 등급을 지어냈다'
65
+ lane S5-crossfamily BLOCK 'standpoint: DEGRADED_SINGLE_FAMILY — codex 안 붙었다'
66
+ lane S6-panel-token BLOCK 'standpoint: panel(codex) — 축을 헷갈렸다'
67
+ lane S7-ungrounded-unk BLOCK 'standpoint: UNKNOWN'
68
+ lane S8-ungrounded-deg BLOCK 'standpoint: DEGRADED_NOT_RUN — 안 했음'
69
+
70
+ echo "== standpoint lanes — PASS side (legitimate shapes must not over-block) =="
71
+ lane N1-tier1 PASS 'standpoint: tier1'
72
+ lane N2-na-grounded PASS 'standpoint: not-applicable — FH 내부 문서이고 출하 표면·peer 계약을 안 건드린다(확인함)'
73
+ lane N3-tier1b PASS 'standpoint: tier1b(pmh-dev) — pmh-dev 정본 사본 직독 + 마커 42건 grep, 재분류 대상 0'
74
+ lane N4-tier2-exec PASS 'standpoint: tier2(qasp-dev) — 그 레포에서 스위트 실행, rc=0 / 3176 passed 출력 확인'
75
+ lane N5-deg-grounded PASS 'standpoint: DEGRADED_NOT_RUN — 표적 접근 가능했으나 안 돌렸다, 다음 라운드 이월'
76
+ lane N6-deg-targets PASS 'standpoint: DEGRADED_NOT_RUN(qasp-dev · pmh-dev) — 전파 자산이라 해당되나 대상 레포에서 아무것도 실행 안 했다'
77
+ lane N7-quoted-value PASS 'standpoint: "tier2(pmh-dev) — 그 레포 로컬 클론에서 실제로 실행, 양·음 arm 확인"'
78
+
79
+ echo "== WARN side — recorded, not blocking (the named residual above) =="
80
+ lane N8-tier2-vague PASS 'standpoint: tier2(pmh-dev) — 그쪽 레포 기준으로 판단했다'
81
+
82
+ [ $FAIL -eq 0 ] && { echo "✅ all standpoint lanes behave"; exit 0; }
83
+ echo "❌ standpoint lane regression"; exit 1
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ # test_marker_thirdparty_lanes.sh — regression fixtures for pre-commit's validate_thirdparty_leg.
3
+ #
4
+ # WHY: `thirdparty:` shipped 2026-08-17 with presence checked and the VALUE unvalidated. Within the
5
+ # same day the author wrote it wrong TWICE — free prose about peer-session contact — while a peer
6
+ # session used the enum correctly 4/4. ⓓ3자대면 is confronting third-party PRIOR ART (does this
7
+ # claimed-new thing already exist outside the repo), not "I talked to another session". Nothing
8
+ # caught the substitution because nothing read the value.
9
+ #
10
+ # 🟥 THE AXIS HAS TWO HALVES and the field originally encoded only one — ① prior art
11
+ # (checked/none-found) and ② harness-level adversarial review (peer-review): put a model in a THIRD
12
+ # harness, wearing that harness's persona, and have it review the target's change. FH is the
13
+ # governor — it creates the situation, observes, judges; the 4-axis gate verifies the OPINION that
14
+ # came back, locally. Because ② had no value, records that meant ② leaked into free prose. That is
15
+ # the very defect this field was created to fix, recurring one level in.
16
+ #
17
+ # SCOPE — channel, not judgment. These lanes assert the record is an enum member with non-vacuous
18
+ # grounds. They never assert the search was thorough, and never assert that what a third party
19
+ # said is TRUE: the axis exists to surface information you could not predict, and adjudicating what
20
+ # comes back belongs to the other axes. That separation is the point of the 4-axis gate.
21
+ #
22
+ # Fixtures assert BOTH directions. The real corpus supplied both arms on its own — that is unusual
23
+ # and worth stating: peer-authored markers were the known-negatives, the author's own malformed
24
+ # ones were the known-positives.
25
+ #
26
+ # Usage: bash scripts/test_marker_thirdparty_lanes.sh Exit: 0 = all behave; 1 = regression.
27
+
28
+ set -uo pipefail
29
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
30
+ HOOK="$REPO_ROOT/templates/.git-hooks/pre-commit"
31
+ T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
32
+ sed -n '/^validate_thirdparty_leg()/,/^}/p' "$HOOK" > "$T/fn.sh"
33
+ if ! grep -q 'DEGRADED_NO_ACCESS' "$T/fn.sh"; then
34
+ echo "❌ HARNESS-ERROR — validate_thirdparty_leg did not extract from $HOOK."
35
+ echo " Fixtures below would measure an empty function. Aborting rather than reporting green."
36
+ exit 1
37
+ fi
38
+ FAIL=0
39
+ lane() { # $1=id $2=expect $3=body
40
+ local rc out got
41
+ printf '%s\n' "$3" > "$T/m.marker"
42
+ out=$( bash -c 'set -uo pipefail; . "$1"; validate_thirdparty_leg "$2"' _ "$T/fn.sh" "$T/m.marker" 2>&1 ); rc=$?
43
+ got=PASS; [ $rc -ne 0 ] && got=BLOCK
44
+ if [ "$got" = "$2" ]; then printf ' ✅ %-32s %s\n' "$1" "$got"
45
+ else printf ' ❌ %-32s expected %s, got %s\n %s\n' "$1" "$2" "$got" "$(printf '%s' "$out"|head -1)"; FAIL=1; fi
46
+ }
47
+
48
+ echo "== BLOCK side =="
49
+ lane T1-wrong-axis-semantics BLOCK 'thirdparty: 병렬 챔버 세션과 실교신 — 상호 정정 3건'
50
+ lane T2-bare-checked BLOCK 'thirdparty: checked'
51
+ lane T3-vacuous-grounds BLOCK 'thirdparty: none-found(없음)'
52
+ lane T4-standpoint-token BLOCK 'thirdparty: tier1b(pmh-dev) — 축을 헷갈렸다'
53
+ lane T5-crossfamily-token BLOCK 'thirdparty: panel(codex) — 축을 헷갈렸다'
54
+ lane T6-not-in-enum BLOCK 'thirdparty: yes'
55
+ lane T7-duplicate BLOCK 'thirdparty: UNKNOWN
56
+ thirdparty: none-found(나중에 고침)'
57
+
58
+ echo "== PASS side =="
59
+ lane N1-unknown-bare PASS 'thirdparty: UNKNOWN'
60
+ lane N2-checked-grounded PASS 'thirdparty: checked(promptfoo·DeepEval·garak 훑음 — 겹치는 선행 없음)'
61
+ lane N3-none-found PASS 'thirdparty: none-found(「등록 시점 검사기를 호출 시점에 배선」 조합의 외부 선행)'
62
+ lane N4-degraded-not-run PASS 'thirdparty: DEGRADED_NOT_RUN(외부 선행 미조사 — 필드 설계가 먼저였다)'
63
+ lane N5-not-applicable PASS 'thirdparty: not-applicable(이 저장소 고유 taxonomy 의 내부 규정 변경)'
64
+ lane N6-multiline-parenthetical PASS 'thirdparty: none-found(「정체성 승급 기준의 residency-제약 하 측정」이라는
65
+ 조합으로 외부 선행을 찾았고 겹치는 것이 없었다)'
66
+ # ② 하네스 단위 적대검증 — 이 축의 «다른 반쪽». 규격에 값이 없어서 산문으로 새던 자리다.
67
+ lane N8-peer-review PASS 'thirdparty: peer-review(gstack/harness-persona → qasp, 경계 가정 2건을 그쪽 입장에서 반증)'
68
+ lane N9-peer-review-degraded PASS 'thirdparty: DEGRADED_NO_ACCESS(gstack 로컬 클론 없음 — 제3 하네스 페르소나를 세울 수 없었다)'
69
+ lane T8-peer-review-vacuous BLOCK 'thirdparty: peer-review(했음)'
70
+ lane N7-absent PASS 'axis2-model: opus'
71
+
72
+ [ $FAIL -eq 0 ] && { echo "✅ all thirdparty lanes behave"; exit 0; }
73
+ echo "❌ thirdparty lane regression"; exit 1
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env bash
2
+ # test_regression_guard_ci_lanes.sh — regression fixtures for Axis 1's CI path.
3
+ #
4
+ # WHY (measured 2026-08-17, and this is a production fail-open that ran silently):
5
+ # `Axis 1 — Backward Regression Check` reported a GREEN check on every PR it triggered on, while
6
+ # never once comparing anything. Two independent faults, both required:
7
+ # ① templates/regression_guard.sh --pr took a bare BRANCH NAME and ran `git merge-base main
8
+ # <branch>`. In a GitHub Actions PR checkout neither name resolves — actions/checkout lands
9
+ # on a DETACHED HEAD and creates refs/remotes/origin/*, not local branches. `fetch-depth: 0`
10
+ # was already set and is NOT the cause: the history was there, the NAMES were not. → exit 3.
11
+ # ② the workflow's Evaluate step was `2 -> block · 1 -> warn · else -> "PASS"`, so exit 3
12
+ # ("could not run") fell into `else` and printed a green PASS.
13
+ # Sample: 5 of 5 runs inspected (4 historical + the one that surfaced it) hit ① and rendered ②.
14
+ # Scope stated honestly: 5/5 SAMPLED, not an exhaustive audit of every run ever.
15
+ #
16
+ # ② is the load-bearing half. Fixing ① alone leaves the next instrument error just as green,
17
+ # because `else` swallows every code nobody enumerated.
18
+ #
19
+ # These lanes assert BOTH directions, and include a REVERT PROBE: the fail-closed branch is
20
+ # neutralized and the suite must go red at exactly that lane — an anchor nobody has watched fail
21
+ # is an anchor nobody knows is alive.
22
+ #
23
+ # Usage: bash scripts/test_regression_guard_ci_lanes.sh Exit: 0 = all behave; 1 = regression.
24
+
25
+ set -uo pipefail
26
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
27
+ WF="$REPO_ROOT/.github/workflows/regression-guard.yml"
28
+ GUARD="$REPO_ROOT/templates/regression_guard.sh"
29
+ T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
30
+ FAIL=0
31
+
32
+ # ── instrument calibration ───────────────────────────────────────────────────
33
+ # Extract the workflow's verdict logic and PROVE it arrived. An empty extraction would let every
34
+ # lane "pass" against nothing — the exact class this suite exists to close.
35
+ # Extracted with stdlib only. PyYAML is NOT a declared dependency of this repo, and this suite
36
+ # runs inside selfcheck.sh — a missing module here would block unrelated legitimate commits with
37
+ # an error about the wrong thing (cross-family finding, 2026-08-17).
38
+ python3 - "$WF" > "$T/eval.sh" <<'PYX'
39
+ import sys, re
40
+ lines = open(sys.argv[1], encoding="utf-8").read().split("\n")
41
+ try:
42
+ i = next(k for k, l in enumerate(lines) if l.strip() == "- name: Evaluate result")
43
+ except StopIteration:
44
+ sys.exit(0) # emit nothing -> the calibration check below aborts loudly
45
+ j = next(k for k in range(i, len(lines)) if lines[k].strip().startswith("run:"))
46
+ indent = len(lines[j + 1]) - len(lines[j + 1].lstrip())
47
+ body = []
48
+ for l in lines[j + 1:]:
49
+ if l.strip() and (len(l) - len(l.lstrip())) < indent:
50
+ break
51
+ body.append(l[indent:])
52
+ run = "\n".join(body)
53
+ run = run.replace("${{ steps.guard.outputs.exit_code }}", "$EXIT_IN")
54
+ run = run.replace("${{ steps.guard.outputs.scanned }}", "$SCANNED_IN")
55
+ sys.stdout.write('EXIT="$EXIT_IN"\n' + re.sub(r"^EXIT=.*\n", "", run, count=1, flags=re.M))
56
+ PYX
57
+ if ! grep -q 'could NOT RUN' "$T/eval.sh"; then
58
+ echo "❌ HARNESS-ERROR — the workflow's Evaluate logic did not extract from $WF."
59
+ echo " Lanes below would measure an empty script. Aborting rather than reporting green."
60
+ exit 1
61
+ fi
62
+
63
+ verdict() { # $1=exit_code $2=scanned ; echoes rc + first line
64
+ local out rc
65
+ out=$(EXIT_IN="$1" SCANNED_IN="$2" bash "$T/eval.sh" 2>&1); rc=$?
66
+ printf '%s|%s' "$rc" "$(printf '%s' "$out" | head -1)"
67
+ }
68
+ lane() { # $1=id $2=exit $3=scanned $4=expect(ok|fail) $5=substring
69
+ local r; r=$(verdict "$2" "$3"); local rc="${r%%|*}" msg="${r#*|}"
70
+ local got=ok; [ "$rc" -ne 0 ] && got=fail
71
+ if [ "$got" = "$4" ] && printf '%s' "$msg" | grep -q "$5"; then
72
+ printf ' ✅ %-30s rc=%s %s\n' "$1" "$rc" "$(printf '%s' "$msg" | cut -c1-58)"
73
+ else
74
+ printf ' ❌ %-30s expected %s+/%s/, got rc=%s %s\n' "$1" "$4" "$5" "$rc" "$msg"; FAIL=1
75
+ fi
76
+ }
77
+
78
+ echo "== Evaluate-step lanes =="
79
+ lane L1-clean-scanned 0 yes ok "PASS — scanned"
80
+ lane L2-skip-not-a-pass 0 no ok "NOT a pass"
81
+ lane L2b-unknown-typed 0 unknown fail "wrote no typed result"
82
+ lane L3-s-tier-warn 1 yes ok "warning"
83
+ lane L4-m-tier-block 2 yes fail "BLOCK"
84
+ lane L5-instrument-3 3 yes fail "could NOT RUN"
85
+ lane L6-unknown-99 99 yes fail "could NOT RUN"
86
+ lane L7-empty-output "" yes fail "could NOT RUN"
87
+
88
+ echo "== script lane — ref resolution under a CI-shaped checkout =="
89
+ # Real reproduction: a detached-HEAD clone with no local branch refs, which is what broke ①.
90
+ CL="$T/clone"
91
+ if git -C "$REPO_ROOT" clone -q --no-local "$REPO_ROOT" "$CL" 2>/dev/null; then
92
+ git -C "$CL" checkout -q --detach HEAD
93
+ # 🟥 The first version of this lane claimed "no local branch refs" and did NOT create that
94
+ # state — `git clone` makes one, so the resolver matched the FIRST candidate and the lane never
95
+ # exercised the CI shape it advertised (cross-family finding, 2026-08-17). Delete every local
96
+ # branch so only refs/remotes/* remain — what actions/checkout actually leaves behind.
97
+ for _b in $(git -C "$CL" for-each-ref --format='%(refname:short)' refs/heads); do
98
+ git -C "$CL" branch -q -D "$_b" >/dev/null 2>&1 || true # noqa: destructive-op (throwaway clone)
99
+ done
100
+ _left=$(git -C "$CL" for-each-ref --format='%(refname:short)' refs/heads | wc -l | tr -d ' ')
101
+ cp "$GUARD" "$CL/templates/regression_guard.sh"
102
+ # The head fixture is the VULNERABILITY ITSELF, not an arbitrary name: `main`. In this clone
103
+ # `origin/main` exists and local `main` does not — exactly a fork PR whose branch is called
104
+ # `main`. The old resolver guessed `main -> origin/main` and compared the base repo to itself.
105
+ # (An earlier fixture used `rev-parse --abbrev-ref HEAD`, which returns the literal string
106
+ # "HEAD" on a detached checkout — a ref that DOES resolve, so the lane asserted nothing. Caught
107
+ # by reproducing the CI shape locally rather than trusting the developer-machine green.)
108
+ BR=main
109
+ # Take every SHA from INSIDE the clone. A SHA read from the source repo is not guaranteed to
110
+ # exist in the clone: when the source is a detached checkout with no local branches (which is
111
+ # exactly what actions/checkout leaves), `git clone` has few refs to transfer from, so an
112
+ # arbitrary source SHA may simply be absent. Reading from the clone removes that assumption
113
+ # instead of hoping it holds — the previous version of this lane hoped, and CI disagreed.
114
+ SHA=$(git -C "$CL" rev-parse HEAD)
115
+ # 🟥 The BASE side must be a SHA too, and this cost a red CI to learn.
116
+ # The first version passed `origin/main` as the base. That resolves on a developer machine and
117
+ # NOT in CI: a GitHub Actions checkout is detached with no local `main`, so a clone of it has no
118
+ # `origin/main` either. Consequences, both measured on the same run:
119
+ # L8b failed — base unresolvable, so the SHA lane could never reach its assertion
120
+ # L8a "passed" FOR THE WRONG REASON — it expects exit 3, and got exit 3 from the BASE failing,
121
+ # not from the head NAME being refused. A green lane measuring something else.
122
+ # This file's own header warns that asserting only BLOCK cannot tell "blocked" from "blocked for
123
+ # the wrong reason". Holding exactly one variable is the fix: base is always a resolvable SHA,
124
+ # so any refusal is attributable to the head, and the assertion checks the head half by name.
125
+ # 🟥 `git rev-parse HEAD~1` prints the literal string "HEAD~1" ON STDOUT when it fails, so
126
+ # `$(... || fallback)` captures that string alongside the fallback. Measured in CI: the clone of
127
+ # a detached, branchless checkout has a single reachable commit, HEAD~1 does not exist, and
128
+ # BASE_SHA became the literal "HEAD~1" — which then failed to resolve and turned this lane red
129
+ # for a reason unrelated to what it tests. Same family as `|| echo 0` emitting "0\n0".
130
+ # Use --verify --quiet (silent on failure) and branch on emptiness.
131
+ BASE_SHA=$(git -C "$CL" rev-parse --verify --quiet 'HEAD~1^{commit}') || BASE_SHA=""
132
+ [ -n "$BASE_SHA" ] || BASE_SHA=$(git -C "$CL" rev-parse HEAD)
133
+
134
+ # L8a — a bare NAME must be REFUSED. Name-guessing is what let a fork PR branch called `main`
135
+ # resolve to this repo's main and compare it to itself.
136
+ out=$(cd "$CL" && bash templates/regression_guard.sh --pr "$BR" "$BASE_SHA" 2>&1); rc=$?
137
+ if [ "$rc" -eq 3 ] && printf '%s' "$out" | grep -q "head='$BR'->'<none>'"; then
138
+ printf ' ✅ %-30s fork-collision name '"'"'%s'"'"' refused, attributed to HEAD (local refs: %s)\n' "L8a-name-refused" "$BR" "$_left"
139
+ else
140
+ printf ' ❌ %-30s expected exit 3 naming head=%s, got rc=%s: %s\n' "L8a-name-refused" "$BR" "$rc" "$(printf '%s' "$out" | head -1)"; FAIL=1
141
+ fi
142
+
143
+ # L8b — unambiguous SHAs on both sides must resolve and compute a merge-base.
144
+ out=$(cd "$CL" && bash templates/regression_guard.sh --pr "$SHA" "$BASE_SHA" 2>&1); rc=$?
145
+ if printf '%s' "$out" | grep -q 'PR MODE: merge-base='; then
146
+ printf ' ✅ %-30s %s\n' "L8b-sha-resolves" "$(printf '%s' "$out" | grep -o 'merge-base=[^ ]*')"
147
+ else
148
+ printf ' ❌ %-30s SHA did not resolve (rc=%s): %s\n' "L8b-sha-resolves" "$rc" "$(printf '%s' "$out" | head -1)"; FAIL=1
149
+ fi
150
+
151
+ else
152
+ printf ' ⚠️ %-30s clone unavailable — lanes SKIPPED (not passed)\n' "L8-ci-shape"
153
+ fi
154
+
155
+ echo "== revert probe — is the fail-closed branch load-bearing, or decoration? =="
156
+ # Neutralize ONLY the catch-all failure branch; L5/L6/L7 must go red and nothing else may move.
157
+ sed 's/exit 1 ;;/: ;;/' "$T/eval.sh" > "$T/eval_neutered.sh"
158
+ if ! diff -q "$T/eval.sh" "$T/eval_neutered.sh" >/dev/null; then
159
+ r=$(EXIT_IN=3 SCANNED_IN=yes bash "$T/eval_neutered.sh" >/dev/null 2>&1; echo $?)
160
+ if [ "$r" -eq 0 ]; then
161
+ echo " ✅ revert probe neutralizing the catch-all makes exit 3 pass again → anchor is alive"
162
+ else
163
+ echo " ❌ revert probe neutralized branch STILL fails — the lane is not testing what it claims"; FAIL=1
164
+ fi
165
+ else
166
+ echo " ❌ revert probe neutralization was a no-op — the probe measured nothing"; FAIL=1
167
+ fi
168
+
169
+ [ $FAIL -eq 0 ] && { echo "✅ all regression-guard CI lanes behave"; exit 0; }
170
+ echo "❌ regression-guard CI lane regression"; exit 1