@chrono-meta/fh-gate 2.2.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 (38) hide show
  1. package/.claude/capabilities/adapters/qasp-new-code-anchor.cap +5 -2
  2. package/.claude/capabilities/adapters/qasp-web-rules.cap +130 -0
  3. package/.claude/rules/fh_4axis_gate.md +55 -0
  4. package/.claude-plugin/marketplace.json +2 -2
  5. package/CLAUDE.md +51 -6
  6. package/README.ja.md +14 -2
  7. package/README.ko.md +14 -1
  8. package/README.md +16 -2
  9. package/README.zh.md +12 -1
  10. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +231 -7
  11. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +215 -1
  12. package/knowledge/shared/harness-core/ship_readiness_gate.md +168 -1
  13. package/knowledge/shared/learnings/subagent_invocations_log.yaml +24 -0
  14. package/package.json +9 -1
  15. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  16. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  17. package/plugins/fh-meta/CHANGELOG.md +110 -0
  18. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +20 -0
  19. package/plugins/fh-meta/skills/steel-quench/SKILL.md +25 -2
  20. package/scripts/adapters/fixtures/qasp_web_rules_known_negative.json +7 -0
  21. package/scripts/adapters/fixtures/qasp_web_rules_known_positive.json +7 -0
  22. package/scripts/adapters/mate_agent_boundary.sh +27 -7
  23. package/scripts/adapters/qasp_web_rules.sh +171 -0
  24. package/scripts/capability_effect_probe.sh +104 -0
  25. package/scripts/capability_registry_check.sh +69 -3
  26. package/scripts/relay_channel.sh +151 -6
  27. package/scripts/selfcheck.sh +18 -0
  28. package/scripts/target_freeze.sh +239 -0
  29. package/scripts/test_adapter_lanes.sh +54 -1
  30. package/scripts/test_marker_axes_run_lanes.sh +19 -0
  31. package/scripts/test_marker_standpoint_lanes.sh +83 -0
  32. package/scripts/test_marker_thirdparty_lanes.sh +73 -0
  33. package/scripts/test_regression_guard_ci_lanes.sh +170 -0
  34. package/scripts/test_relay_channel_lanes.sh +146 -17
  35. package/scripts/test_target_freeze_lanes.sh +203 -0
  36. package/templates/.git-hooks/pre-commit +262 -0
  37. package/templates/PRE-PUBLISH-CHECKLIST.md +22 -0
  38. 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 "$@"
@@ -67,13 +67,14 @@ _lane() { # $1=id $2=class $3=설명 $4=expected_rc $5=actual_rc
67
67
 
68
68
  # 계기 전제 — 어댑터 3종이 **실제로 거기 있는지** 먼저 본다. 없으면 이 레인 묶음은
69
69
  # «전부 초록» 이 아니라 하네스 오류다(빈 대상에 대고 초록을 내는 것이 여기서 막으려는 형태).
70
- for a in gstack_content_safety mate_agent_boundary qasp_new_code_anchor peer_resolve; do
70
+ for a in gstack_content_safety mate_agent_boundary qasp_new_code_anchor qasp_web_rules peer_resolve; do
71
71
  [ -r "$ADAPTER_DIR/$a.sh" ] || { echo "❌ HARNESS-ERROR — $ADAPTER_DIR/$a.sh 부재. 빈 대상에 초록을 내지 않는다"; exit 10; }
72
72
  done
73
73
 
74
74
  G="$ADAPTER_DIR/gstack_content_safety.sh"
75
75
  M="$ADAPTER_DIR/mate_agent_boundary.sh"
76
76
  Q="$ADAPTER_DIR/qasp_new_code_anchor.sh"
77
+ W="$ADAPTER_DIR/qasp_web_rules.sh"
77
78
 
78
79
  # 「peer 가 안 보이는 세계」 = 잘 형성된 루트 목록인데 그 안에 이 peer 가 없는 것.
79
80
  # (빈 **문자열**이 아니다 — 그건 「어디를 볼지 자체가 없다」는 다른 명제이고 전제 파손이다.)
@@ -144,6 +145,23 @@ if [ -n "$MT_ROOT" ]; then
144
145
  else
145
146
  echo " ⏭ M4 [resolve-alias ] mate 실물 루트 없음 — SKIP (통과 아님)"
146
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
147
165
  _lane_peer "$MT_ROOT" M5 enum "없는 대상 → HARNESS_ERROR(10). peer 헤더가 «위반 0 이 아니다» 라고 못 박은 값" 10 \
148
166
  "$(_rc bash "$M" --target "$T/does_not_exist.md")"
149
167
  _lane_peer "$MT_ROOT" M6 enum "알 수 없는 모드 → HARNESS_ERROR(10)" 10 "$(_rc bash "$M" --bogus-mode)"
@@ -196,6 +214,41 @@ else
196
214
  fi
197
215
  echo
198
216
 
217
+ # ═══════════════════════════════════════════════════════════════════════════
218
+ # ④ qasp-web-rules (자매 노드 — 같은 peer, 다른 능력)
219
+ # ═══════════════════════════════════════════════════════════════════════════
220
+ # 🟥 ③ 과 **같은 peer 인데 다른 enum 이다**. 값을 옮겨 쓰면 안 된다 —
221
+ # ③ 의 `3=HARNESS_ERROR`·`4=OUT_OF_SCOPE` 와 여기의 `3=ENGINE_ERROR` 는 다른 사건이고,
222
+ # 여기엔 `4` 가 아예 없다. 두 노드를 한 눈에 보는 이 파일이 그 혼동의 1순위 발생지다.
223
+ echo "── qasp-web-rules (enum 0=CLEAN 1=FINDINGS 2=ARGS 3=ENGINE_ERROR 20=PEER_ABSENT)"
224
+ _lane W1 resolve-absent "peer 불가시 → 전용값 PEER_ABSENT(CLEAN 으로도 ENGINE_ERROR 로도 안 접힌다)" 20 \
225
+ "$(FH_CLUSTER_ROOTS="$EMPTY_WORLD" _rc bash "$W" --known-positive)"
226
+ _lane_peer "$QS_ROOT" W2 control "peer 가시 → 양성 arm 은 FINDINGS" 1 "$(_rc bash "$W" --known-positive)"
227
+ _lane_peer "$QS_ROOT" W3 control "peer 가시 → 음성 arm 은 CLEAN (판별력의 증거)" 0 "$(_rc bash "$W" --known-negative)"
228
+ _lane W4 enum "모드 미지정 → ARGS(2). 안 돈 실행을 판정으로 렌더하지 않는다" 2 "$(_rc bash "$W")"
229
+ _lane W5 enum "없는 diff-json → ARGS(2). 호출 오류지 계기 고장이 아니다" 2 \
230
+ "$(_rc bash "$W" --diff-json /nonexistent/nope.json)"
231
+ # 🟥 W6 — 픽스처 부재는 **ARGS 가 아니라 HARNESS_ERROR** 다. 캘리브레이션 쌍이 사라진 것은
232
+ # 「호출을 잘못했다」가 아니라 **「이 계기가 자기 판별력을 증명할 수 없다」**는 뜻이고,
233
+ # 그 둘을 같은 값으로 접으면 «쌍이 없는 계기»가 «인자 실수»로 보고된다.
234
+ if [ -n "$QS_ROOT" ]; then
235
+ FIXBK="$T/fixbk"; mkdir -p "$FIXBK"
236
+ FIXPOS="$ADAPTER_DIR/fixtures/qasp_web_rules_known_positive.json"
237
+ if [ -r "$FIXPOS" ]; then
238
+ mv "$FIXPOS" "$FIXBK/pos.json"
239
+ _lane W6 enum "캘리브레이션 픽스처 부재 → HARNESS_ERROR(10), ARGS 아님" 10 \
240
+ "$(_rc bash "$W" --known-positive)"
241
+ mv "$FIXBK/pos.json" "$FIXPOS"
242
+ # 복원 확인 — 레인이 자기가 옮긴 것을 되돌렸는지 **잰다**(안 하면 다음 레인이 거짓 적색)
243
+ [ -r "$FIXPOS" ] || { echo "❌ HARNESS-ERROR — W6 가 픽스처를 복원하지 못했다"; exit 10; }
244
+ else
245
+ echo " ⏭ W6 [enum ] 양성 픽스처 부재 — SKIP (통과 아님)"
246
+ fi
247
+ else
248
+ echo " ⏭ W6 [enum ] qasp 실물 루트 없음 — SKIP (통과 아님)"
249
+ fi
250
+ echo
251
+
199
252
  # ═══════════════════════════════════════════════════════════════════════════
200
253
  # 되돌림 프로브 — 앵커가 장식이 아님을 «되돌려 보고» 잰다 (적용확인 → 실행 → 복원)
201
254
  # ═══════════════════════════════════════════════════════════════════════════
@@ -168,6 +168,25 @@ controls: n/a — 측정 없음
168
168
  standpoint: tier1b(qasp)')
169
169
  check "$S7c" PASS "★컨트롤: 포인터에 대상이 붙어도 standpoint: 줄이 있으면 통과 (과차단 방지)"
170
170
 
171
+ # ── ⓓ3자대면 자기 필드 `thirdparty:` (2026-08-17 신설) ─────────────────────────
172
+ # ⓑ 와 같은 형태이므로 **ⓑ가 실측으로 겪은 fail-open 을 미리 막았는지**를 같이 고정한다.
173
+ S7d=$(mk "$D_SIX" 'axes-run: ⓐ=none ⓑ=none ⓒ=none ⓓ=→thirdparty ⓔ=none ⓕ=none
174
+ controls: n/a — 측정 없음')
175
+ check "$S7d" BLOCK "★ⓓ=→thirdparty 인데 thirdparty: 줄이 없음 → 차단 (죽은 포인터)"
176
+
177
+ S7e=$(mk "$D_SIX" 'axes-run: ⓐ=none ⓑ=none ⓒ=none ⓓ=thirdparty ⓔ=none ⓕ=none
178
+ controls: n/a — 측정 없음')
179
+ check "$S7e" BLOCK "★화살표 없는 ⓓ=thirdparty 도 차단 (ⓑ가 뚫렸던 그 표기 fail-open)"
180
+
181
+ S7f=$(mk "$D_SIX" 'axes-run: ⓐ=none ⓑ=none ⓒ=none ⓓ=→thirdparty(promptfoo·DeepEval) ⓔ=none ⓕ=none
182
+ controls: n/a — 측정 없음
183
+ thirdparty: none-found(promptfoo·DeepEval·mutation testing 훑음)')
184
+ check "$S7f" PASS "★컨트롤: 포인터에 대상이 붙어도 thirdparty: 줄이 있으면 통과 (과차단 방지)"
185
+
186
+ S7g=$(mk "$D_SIX" 'axes-run: ⓐ=none ⓑ=none ⓒ=none ⓓ=none ⓔ=none ⓕ=none
187
+ controls: n/a — 측정 없음')
188
+ check "$S7g" PASS "★컨트롤: ⓓ=none 이면 thirdparty: 줄이 없어도 통과 (포인터를 안 쓴 경우)"
189
+
171
190
  S8=$(mk "$D_SIX" 'axes-run: ⓐ=codex ⓑ=→standpoint ⓒ=none ⓓ=none ⓔ=실사용 ⓕ=되돌림
172
191
  axes-run: ⓐ=거짓말 ⓑ=x ⓒ=x ⓓ=x ⓔ=x ⓕ=x
173
192
  controls: alive — ok
@@ -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