@chrono-meta/fh-gate 1.4.92 → 1.4.93

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 (42) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +1 -1
  3. package/CHEATSHEET.md +1 -0
  4. package/CLAUDE.md +9 -1
  5. package/README.md +9 -4
  6. package/docs/codex-compat.md +1 -1
  7. package/knowledge/shared/harness-core/fh_detail_protocols.md +11 -0
  8. package/knowledge/shared/harness-core/gate_locality_principle.md +10 -0
  9. package/knowledge/shared/harness-core/return_path_gate.md +15 -0
  10. package/knowledge/shared/learnings/subagent_invocations_log.yaml +72 -0
  11. package/knowledge/shared/rules/knowledge_layer_seam.md +165 -0
  12. package/package.json +11 -2
  13. package/plugins/fh-commons/.claude-plugin/plugin.json +2 -2
  14. package/plugins/fh-commons/README.md +1 -0
  15. package/plugins/fh-commons/skills/ko-tech-writer/SKILL.md +157 -0
  16. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  17. package/plugins/fh-meta/skills/agent-composer/SKILL.md +3 -1
  18. package/plugins/fh-meta/skills/context-doctor/SKILL.md +4 -0
  19. package/plugins/fh-meta/skills/edit-manifest/SKILL.md +4 -0
  20. package/plugins/fh-meta/skills/harvest-loop/SKILL.md +19 -1
  21. package/plugins/fh-meta/skills/install-wizard/SKILL.md +26 -1
  22. package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +25 -4
  23. package/plugins/fh-meta/skills/phantom-quench/SKILL.md +44 -0
  24. package/plugins/fh-meta/skills/pipeline-conductor/SKILL.md +11 -3
  25. package/plugins/fh-meta/skills/steel-quench/SKILL.md +1 -1
  26. package/scripts/.env_purity_tokens.defaults +8 -0
  27. package/scripts/env_purity_scan.sh +50 -0
  28. package/scripts/fh_env_delta_scan.sh +16 -1
  29. package/scripts/gate_anchor_check.sh +344 -0
  30. package/scripts/knowledge_seam_check.sh +171 -0
  31. package/scripts/rtk_gross_ablation.sh +64 -0
  32. package/scripts/selfcheck.sh +15 -0
  33. package/scripts/session_close_check.sh +3 -0
  34. package/scripts/sidecar_calibrate.sh +32 -1
  35. package/scripts/test_env_purity_lanes.sh +30 -0
  36. package/scripts/test_knowledge_seam_lanes.sh +142 -0
  37. package/scripts/test_marker_axes_run_lanes.sh +115 -0
  38. package/scripts/test_sidecar_calibrate_lanes.sh +15 -1
  39. package/templates/.git-hooks/pre-commit +63 -2
  40. package/templates/CLAUDE.md +7 -0
  41. package/templates/starter_profile.md +2 -2
  42. package/tests/seam_absence_probes.md +31 -0
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env bash
2
+ # gate_anchor_check.sh — FH 게이트 앵커 하네스 (known-pair)
3
+ # provenance: pmh-dev@cbe3932 역수확(2026-08-10) — 규율 3종(문법검사≠계기 · 막힘≠옳게막힘 ·
4
+ # 부재≠통과)은 원래 FH 실측에서 태어나 pmh 에서 스크립트로 벼려진 것을 되가져온 것.
5
+ #
6
+ # 왜 이게 먼저인가:
7
+ # 이 레포는 지금까지 "게이트가 초록이다"를 잴 수단이 없었다. 훅이 설치되어 있는지,
8
+ # 설치돼 있다면 **옳은 이유로** 통과시키는지 — 둘 다 미측정이었다. 훅 본체를 옮기기
9
+ # 전에 이 앵커가 먼저 있어야 이식이 "됐다"가 아니라 "옳게 됐다"로 판정된다.
10
+ #
11
+ # 이 스크립트가 지키는 세 가지 규율 (전부 FH 에서 실측으로 얻어진 것):
12
+ #
13
+ # 1. **문법검사는 계기가 아니다.** `bash -n` 은 런타임 bad substitution 을 통과시킨다.
14
+ # 그 상태의 훅은 중단되면서 exit 0 을 남기고 — 즉 모든 push 가 허용된다.
15
+ # 그래서 여기서는 훅을 **실제로 실행**하고 출력에서 런타임 결함을 함께 본다.
16
+ #
17
+ # 2. **"막혔다" ≠ "옳게 막혔다".** 차단 쌍만 고정하면 과차단 게이트가 초록으로 채점된다.
18
+ # 과차단은 override 를 근육기억으로 만들어 게이트를 무장해제시킨다. 그래서 모든
19
+ # 검사는 known-positive(막혀야 함) 와 known-negative(통과해야 함) 쌍으로 둔다.
20
+ #
21
+ # 3. **부재는 통과가 아니다.** 훅이 없으면 PASS 도 FAIL 도 아닌 `NOT-INSTALLED` 로
22
+ # 보고하고 전용 exit code 를 낸다. 부재를 초록으로 읽는 순간 이 앵커 자체가
23
+ # fail-open 이 된다.
24
+ #
25
+ # Exit: 0 = 모든 쌍 성립 · 1 = 쌍 붕괴(진짜 결함) · 10 = 하네스 오류/미설치(판정 불가)
26
+ #
27
+ # Usage: bash scripts/gate_anchor_check.sh [--quiet]
28
+
29
+ set -uo pipefail
30
+
31
+ REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
32
+
33
+ # ── cwd 가드 ──────────────────────────────────────────────────────────────
34
+ # `git rev-parse` 는 **현재 cwd** 의 레포를 답한다. 다른 레포에서 이 스크립트를
35
+ # 절대경로로 부르면 엉뚱한 대상을 재고, 그 결과가 그럴듯해서 알아채기 어렵다.
36
+ # (2026-07-28 실측: 다른 레포 cwd 에서 호출해 "미설치 0 · 패턴 유효" 라는 거짓 초록을 얻었다.)
37
+ SCRIPT_REPO=$(cd "$(dirname "${BASH_SOURCE[0]}")" && git rev-parse --show-toplevel 2>/dev/null || true)
38
+ if [ -n "$SCRIPT_REPO" ] && [ "$SCRIPT_REPO" != "$REPO_ROOT" ]; then
39
+ echo "❌ harness-error: 스크립트와 cwd 가 서로 다른 레포다 — 측정 대상이 어긋난다."
40
+ echo " 스크립트 레포: $SCRIPT_REPO"
41
+ echo " 현재 cwd 레포: $REPO_ROOT"
42
+ echo " → cd \"$SCRIPT_REPO\" 후 다시 실행할 것."
43
+ exit 10
44
+ fi
45
+
46
+ QUIET=0
47
+ [ "${1:-}" = "--quiet" ] && QUIET=1
48
+
49
+ PAIRS_OK=0
50
+ PAIRS_BROKEN=0
51
+ NOT_INSTALLED=0
52
+
53
+ say() { [ "$QUIET" -eq 1 ] || printf '%s\n' "$*"; }
54
+ pass() { PAIRS_OK=$((PAIRS_OK+1)); say " ✅ $*"; }
55
+ fail() { PAIRS_BROKEN=$((PAIRS_BROKEN+1)); say " ❌ $*"; }
56
+ absent(){ NOT_INSTALLED=$((NOT_INSTALLED+1)); say " ⛔ NOT-INSTALLED — $*"; }
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # 0. 설치 상태 — 판정이 아니라 사실 보고. 여기서 거짓말하면 아래가 전부 무의미하다.
60
+ # ---------------------------------------------------------------------------
61
+ say "── 0. 설치 상태 ──────────────────────────────────────────"
62
+
63
+ HOOKS_PATH=$(git -C "$REPO_ROOT" config core.hooksPath 2>/dev/null || true)
64
+ if [ -n "$HOOKS_PATH" ]; then
65
+ case "$HOOKS_PATH" in
66
+ /*) ACTIVE_DIR="$HOOKS_PATH" ;;
67
+ *) ACTIVE_DIR="$REPO_ROOT/$HOOKS_PATH" ;;
68
+ esac
69
+ say " core.hooksPath = $HOOKS_PATH"
70
+ [ -d "$ACTIVE_DIR" ] || say " ⚠️ 그 경로가 존재하지 않는다 — 훅이 하나도 안 돈다"
71
+ else
72
+ ACTIVE_DIR="$(git -C "$REPO_ROOT" rev-parse --absolute-git-dir)/hooks"
73
+ say " core.hooksPath 미설정 → $ACTIVE_DIR 사용"
74
+ fi
75
+
76
+ for h in pre-commit pre-push; do
77
+ if [ -x "$ACTIVE_DIR/$h" ]; then
78
+ say " 설치됨: $h ($(wc -l < "$ACTIVE_DIR/$h" | tr -d ' ') lines)"
79
+ else
80
+ say " 부재: $h"
81
+ fi
82
+ done
83
+
84
+ # 소스 디렉토리 — FH 는 templates/.git-hooks 를 쓴다.
85
+ # 추측하지 말고 실재하는 것을 고른다: 없는 경로를 이름으로 단정하면 "부재"를 잘못 보고한다.
86
+ SRC_DIR=""
87
+ for cand in "$REPO_ROOT/templates/.git-hooks" "$REPO_ROOT/.git-hooks"; do
88
+ [ -d "$cand" ] && { SRC_DIR="$cand"; break; }
89
+ done
90
+ if [ -n "$SRC_DIR" ]; then
91
+ say " 훅 소스 디렉토리: ${SRC_DIR#$REPO_ROOT/} ($(ls "$SRC_DIR" | tr '\n' ' '))"
92
+ else
93
+ say " 훅 소스 디렉토리 부재 — templates/.git-hooks · .git-hooks 둘 다 없음"
94
+ fi
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # 공통: 일회용 레포에서 훅을 실제로 실행한다. 이 레포의 index/worktree 는 건드리지 않는다.
98
+ # ---------------------------------------------------------------------------
99
+ SANDBOX=$(mktemp -d) || { echo "❌ harness-error: mktemp 실패"; exit 10; }
100
+ trap 'rm -rf "$SANDBOX"' EXIT
101
+
102
+ # run_hook <hook-path> <sandbox> [env assignments...]
103
+ # 훅을 실행하고 "exit코드 + 출력" 을 돌려준다. 런타임 결함 탐지가 목적이므로
104
+ # stderr 를 버리지 않는다 — 과거 실측에서 세 종류의 무음 실패가 전부 stderr 에서 자백했다.
105
+ # 잔여(명명): 훅이 $BASH_SOURCE 기준으로 원레포 파일을 읽으면 sandbox 픽스처가 무의미해진다 —
106
+ # FH 훅 2종은 런타임 rev-parse(REPO_ROOT) 해석임을 실측 확인(2026-08-10). 새 훅 추가 시 재확인.
107
+ run_hook() {
108
+ local hook="$1" dir="$2"; shift 2
109
+ if [ -x "$hook" ]; then ( cd "$dir" && env "$@" "$hook" 2>&1 ) # shebang 존중
110
+ else ( cd "$dir" && env "$@" bash "$hook" 2>&1 ); fi
111
+ return $?
112
+ }
113
+
114
+ # 런타임 결함 문자열 — 이게 나오면 verdict 와 무관하게 계기 고장이다.
115
+ RUNTIME_FAULT='bad substitution|command not found|unbound variable|No such file or directory: .*\.sh|syntax error|Permission denied|grep: invalid|fatal: [^o]'
116
+ # 정책 태그 — 차단이 «옳은 이유»인지 본다. rc!=0 이기만 하면 infra 오류 차단도 초록이 된다
117
+ # (교차 계열 라운드 적발). 태그는 훅 출력 실측(2026-08-10)에서 채집.
118
+ PC_POLICY_TAG='HIGH leak|MED leak|Confidentiality'
119
+ PP_POLICY_TAG='MAIN_PUSH_OK|main.*직|direct.*main|integration branch'
120
+
121
+ # new_repo <name> — 일회용 레포. **초기 커밋을 반드시 만든다.**
122
+ # HEAD 없는 레포에서는 훅의 `git diff --cached … HEAD` 가 fatal 로 죽고, 그 죽음이
123
+ # exit 0 으로 읽혀 "게이트가 통과시켰다"는 거짓 판정이 나온다. 계기가 대상을 모함하는
124
+ # 전형적인 경로라 픽스처 쪽에서 먼저 막는다. (2026-07-28 실측: 이 결함으로 known-positive
125
+ # 2건이 잘못 채점됐다.)
126
+ #
127
+ # 의존 파일 동반 (2026-07-28 이식 실측): pre-commit 훅이 `scripts/psa_scan_lib.sh` 와
128
+ # `.claude/rules/.public-surface-patterns.defaults` 를 참조한다. sandbox 레포에는 이들이
129
+ # 없어서 훅이 "스캐너 부재" 로 FAIL 하고, 그 FAIL 이 평문까지 차단하는 과차단으로 나타난다.
130
+ # known-pair 가 "옳게 통과/옳게 차단" 을 재는 계기라면, 픽스처는 훅이 의존하는 파일을
131
+ # 그대로 제공해야만 계기 자체가 유효하다. 본 레포(psA_scan_lib.sh 가 있는 레포)에서
132
+ # sandbox 로 복사한다.
133
+ _fixture_fail(){ echo "❌ harness-error: 픽스처 준비 실패 — $1"; exit 10; }
134
+ new_repo() {
135
+ local d="$SANDBOX/$1"
136
+ mkdir -p "$d" && git -C "$d" init -q 2>/dev/null || _fixture_fail "git init ($1)"
137
+ git -C "$d" config user.email "anchor@example.invalid"
138
+ git -C "$d" config user.name "anchor"
139
+ # 훅이 참조하는 의존 파일을 sandbox 에 동반 — 부재 시 "스캐너 부재" FAIL 로 과차단.
140
+ if [ -f "$REPO_ROOT/scripts/psa_scan_lib.sh" ]; then
141
+ mkdir -p "$d/scripts" && cp "$REPO_ROOT/scripts/psa_scan_lib.sh" "$d/scripts/" 2>/dev/null
142
+ fi
143
+ if [ -f "$REPO_ROOT/.claude/rules/.public-surface-patterns.defaults" ]; then
144
+ mkdir -p "$d/.claude/rules" \
145
+ && cp "$REPO_ROOT/.claude/rules/.public-surface-patterns.defaults" "$d/.claude/rules/" 2>/dev/null
146
+ fi
147
+ ( cd "$d" && printf 'seed\n' > .anchor_seed && git add .anchor_seed \
148
+ && git -c commit.gpgsign=false commit -qm "anchor seed" ) >/dev/null 2>&1 \
149
+ || _fixture_fail "seed commit ($1)"
150
+ git -C "$d" rev-parse --verify HEAD >/dev/null 2>&1 || _fixture_fail "HEAD 부재 ($1)"
151
+ printf '%s\n' "$d"
152
+ }
153
+
154
+ # new_repo_with_remote <name> — 리모트 tip 이 fetch 된 상태의 레포.
155
+ # pre-push 훅은 리모트 tip 을 모르면 force-push 와 fast-forward 를 구분할 수 없어
156
+ # **옳게 fail-closed** 한다. 그 차단을 "과차단" 으로 채점하지 않으려면 픽스처가
157
+ # 분류 가능한 상태여야 한다.
158
+ new_repo_with_remote() {
159
+ local d="$SANDBOX/$1" bare="$SANDBOX/$1.git"
160
+ git init -q --bare "$bare" 2>/dev/null
161
+ d=$(new_repo "$1")
162
+ git -C "$d" remote add origin "$bare" 2>/dev/null || _fixture_fail "remote add ($1)"
163
+ git -C "$d" push -q origin HEAD:refs/heads/main 2>/dev/null || _fixture_fail "seed push ($1)"
164
+ git -C "$d" fetch -q origin 2>/dev/null || _fixture_fail "fetch ($1)"
165
+ git -C "$d" rev-parse --verify 'origin/main^{commit}' >/dev/null 2>&1 \
166
+ || _fixture_fail "origin/main 미확정 ($1) — 폴백 금지"
167
+ printf '%s\n' "$d"
168
+ }
169
+
170
+ # ---------------------------------------------------------------------------
171
+ # 1. pre-commit — 기밀 스캔 known-pair
172
+ # ---------------------------------------------------------------------------
173
+ say ""
174
+ say "── 1. pre-commit 기밀 스캔 ───────────────────────────────"
175
+
176
+ # 활성 경로의 훅만 측정한다 — 소스 디렉토리 폴백은 «git 이 실제로 돌리지 않는 것»을 재서
177
+ # 미설치를 초록으로 가린다(교차 계열 라운드 적발). 소스는 §0 에서 정보로만 보고.
178
+ PRECOMMIT=""
179
+ [ -x "$ACTIVE_DIR/pre-commit" ] && PRECOMMIT="$ACTIVE_DIR/pre-commit"
180
+
181
+ if [ -z "$PRECOMMIT" ]; then
182
+ absent "pre-commit 훅이 활성 경로에도 .git-hooks/ 에도 없다 — 기밀 스캔 미측정"
183
+ else
184
+ # known-positive: 막혀야 한다
185
+ # AWS access key ID 형태 (AKIA + 16자 영숫자) — .defaults 의 `AKIA[0-9A-Z]{16}` 패턴이 잡는다.
186
+ # 예전 `AWS_SECRET_ACCESS_KEY=...` 형태는 .defaults 패턴이 잡지 못해 fail-open 으로 채점됐다
187
+ # (2026-07-28 이식 실측). 패턴과 픽스처가 일치해야 known-pair 가 유효하다.
188
+ d=$(new_repo pc_block)
189
+ # 픽스처 리터럴은 소스에서 분할 조립 — 이 스크립트 자신이 기밀 스캔(같은 패턴)에 걸리지
190
+ # 않게 하되, 런타임에는 온전한 자격증명 형태를 만든다(스캐너 회피가 아니라 자기-오탐 방지).
191
+ printf 'aws_access_key_id=%s%s\n' 'AKIA' '1234567890ABCDEFGHIJKLMNOP' > "$d/leak.md"
192
+ git -C "$d" add leak.md
193
+ out=$(run_hook "$PRECOMMIT" "$d"); rc=$?
194
+ if printf '%s' "$out" | grep -qE "$RUNTIME_FAULT"; then
195
+ fail "known-positive: 런타임 결함으로 중단 — verdict 신뢰 불가"
196
+ say " $(printf '%s' "$out" | grep -E "$RUNTIME_FAULT" | head -2)"
197
+ elif [ $rc -ne 0 ] && printf '%s' "$out" | grep -qE "$PC_POLICY_TAG"; then
198
+ pass "known-positive: 자격증명 형태를 정책 태그와 함께 차단 (exit=$rc)"
199
+ elif [ $rc -ne 0 ]; then
200
+ fail "known-positive: 차단은 됐으나 정책 태그 미검출 — 이유 불명 차단(infra 오류 가능)"
201
+ else
202
+ fail "known-positive: 자격증명이 통과했다 (exit=0) — fail-open"
203
+ fi
204
+
205
+ # known-negative: 통과해야 한다 (과차단이면 override 가 근육기억이 된다)
206
+ d=$(new_repo pc_pass)
207
+ printf '# 평범한 문서\n\n일반적인 산문입니다.\n' > "$d/ok.md"
208
+ git -C "$d" add ok.md
209
+ out=$(run_hook "$PRECOMMIT" "$d"); rc=$?
210
+ if printf '%s' "$out" | grep -qE "$RUNTIME_FAULT"; then
211
+ fail "known-negative: 런타임 결함으로 중단"
212
+ elif [ $rc -eq 0 ]; then
213
+ pass "known-negative: 평문 문서를 통과 (과차단 아님)"
214
+ else
215
+ fail "known-negative: 평문 문서를 차단했다 (exit=$rc) — 과차단은 게이트를 무장해제시킨다"
216
+ fi
217
+ fi
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # 2. pre-push — main 직행 차단 known-pair
221
+ # ---------------------------------------------------------------------------
222
+ say ""
223
+ say "── 2. pre-push main 직행 차단 ────────────────────────────"
224
+
225
+ PREPUSH=""
226
+ [ -x "$ACTIVE_DIR/pre-push" ] && PREPUSH="$ACTIVE_DIR/pre-push"
227
+
228
+ if [ -z "$PREPUSH" ]; then
229
+ absent "pre-push 훅이 없다 — main 직행/파괴적 op 차단 미측정"
230
+ else
231
+ # known-positive: main 직행은 막혀야 한다
232
+ d=$(new_repo_with_remote pp_main)
233
+ git -C "$d" switch -q -c main 2>/dev/null || git -C "$d" switch -q main 2>/dev/null
234
+ ( cd "$d" && printf 'delta\n' >> .anchor_seed && git add .anchor_seed \
235
+ && git -c commit.gpgsign=false commit -qm "delta" ) >/dev/null 2>&1 \
236
+ || _fixture_fail "main delta commit"
237
+ local_sha=$(git -C "$d" rev-parse HEAD)
238
+ remote_sha=$(git -C "$d" rev-parse --verify 'origin/main^{commit}' 2>/dev/null) \
239
+ || _fixture_fail "origin/main 조회"
240
+ out=$(printf 'refs/heads/main %s refs/heads/main %s\n' "$local_sha" "$remote_sha" \
241
+ | ( cd "$d" && bash "$PREPUSH" origin "$(git -C "$d" remote get-url origin)" 2>&1 )); rc=$?
242
+ if printf '%s' "$out" | grep -qE "$RUNTIME_FAULT"; then
243
+ fail "known-positive: 런타임 결함으로 중단 — 이 상태의 훅은 모든 push 를 허용한다"
244
+ say " $(printf '%s' "$out" | grep -E "$RUNTIME_FAULT" | head -2)"
245
+ elif [ $rc -ne 0 ] && printf '%s' "$out" | grep -qiE "$PP_POLICY_TAG"; then
246
+ pass "known-positive: main 직행을 정책 태그와 함께 차단 (exit=$rc)"
247
+ elif [ $rc -ne 0 ]; then
248
+ fail "known-positive: 차단은 됐으나 정책 태그 미검출 — 이유 불명 차단"
249
+ else
250
+ fail "known-positive: main 직행이 통과했다 — PR-only 규약이 기계적으로 비어있다"
251
+ fi
252
+
253
+ # known-negative: feature 브랜치는 통과해야 한다 (신규 ref = 삭제도 force 도 아님)
254
+ d=$(new_repo_with_remote pp_feat)
255
+ git -C "$d" branch -q feat/x 2>/dev/null || _fixture_fail "feat/x 생성"
256
+ git -C "$d" show-ref -q --verify refs/heads/feat/x || _fixture_fail "feat/x 검증"
257
+ local_sha=$(git -C "$d" rev-parse refs/heads/feat/x)
258
+ out=$(printf 'refs/heads/feat/x %s refs/heads/feat/x %s\n' "$local_sha" "0000000000000000000000000000000000000000" \
259
+ | ( cd "$d" && bash "$PREPUSH" origin "$(git -C "$d" remote get-url origin)" 2>&1 )); rc=$?
260
+ if printf '%s' "$out" | grep -qE "$RUNTIME_FAULT"; then
261
+ fail "known-negative: 런타임 결함으로 중단"
262
+ elif [ $rc -eq 0 ]; then
263
+ pass "known-negative: feature 브랜치 push 통과 (과차단 아님)"
264
+ else
265
+ fail "known-negative: feature 브랜치를 차단했다 (exit=$rc) — 과차단"
266
+ fi
267
+ fi
268
+
269
+ # ---------------------------------------------------------------------------
270
+ # 3. 패턴 소스
271
+ # ---------------------------------------------------------------------------
272
+ say ""
273
+ say "── 3. 기밀 스캔 배선 + 패턴 소스 ─────────────────────────"
274
+
275
+ # 순서가 중요하다: 패턴 파일 부재를 보고하기 전에 **스캔이 존재하는지** 먼저 본다.
276
+ # 스캔이 아예 없는데 "패턴이 없다" 고만 보고하면, 패턴만 채우면 닫힌다는 오독을 만든다.
277
+ if [ -n "$PRECOMMIT" ] && ! grep -qE 'public-surface-patterns|psa_scan|PUBLIC_SURFACE_OK' "$PRECOMMIT" 2>/dev/null; then
278
+ absent "pre-commit 에 기밀 스캔 배선이 없다 — 패턴 파일 문제가 아니라 **스캔 자체가 부재**다.
279
+ (grep: public-surface-patterns · psa_scan · PUBLIC_SURFACE_OK 전부 0건)
280
+ 패턴만 채워도 닫히지 않는다. 스캔 로직 이식이 선행되어야 한다."
281
+ fi
282
+
283
+ # 스캐너 라이브러리 실재 + 진입점 2종 — **실레포** 기준.
284
+ #
285
+ # 왜 별도 레인인가: #27 이 픽스처에 `psa_scan_lib.sh` 를 동반하도록 고친 것은 옳다(없으면 훅이
286
+ # "스캐너 부재" 로 fail-closed 하고, 그 차단이 known-positive 에선 탐지 성공으로 known-negative
287
+ # 에선 과차단으로 채점돼 계기가 대상을 양방향으로 모함한다). 다만 그 수리의 부작용으로
288
+ # **픽스처 런은 이제 실레포의 스캐너 부재를 구조적으로 감지할 수 없다** — 픽스처가 스스로
289
+ # 채워 넣기 때문이다. 그 케이스를 여기서 본다.
290
+ #
291
+ # 부재가 왜 조용한 사고인가: 배선 grep(§3 상단)은 통과한다. 훅은 매 커밋 fail-closed 한다.
292
+ # 즉 "게이트가 빡세다"로 보이지 "스캐너가 없다"로는 안 보인다. 그리고 상시 차단은
293
+ # PUBLIC_SURFACE_OK 를 근육기억으로 만들어, 정작 그 채널에 의존하는 push/publish 게이트까지
294
+ # 같이 무장해제시킨다. 과차단이 그 자체로 결함인 이유가 이거다.
295
+ PSA_LIB_PATH="$REPO_ROOT/scripts/psa_scan_lib.sh"
296
+ if [ ! -r "$PSA_LIB_PATH" ]; then
297
+ absent "scripts/psa_scan_lib.sh 부재 — 배선은 있어도 스캐너가 없다(매 커밋 fail-closed)"
298
+ elif ! grep -q '^psa_load()' "$PSA_LIB_PATH" || ! grep -q '^psa_scan_tagged()' "$PSA_LIB_PATH"; then
299
+ fail "psa_scan_lib.sh 가 진입점을 다 내보내지 않는다 (psa_load / psa_scan_tagged)"
300
+ else
301
+ pass "스캐너 라이브러리 실재 + 진입점 2종"
302
+ fi
303
+
304
+ PAT_LOCAL="$REPO_ROOT/.claude/rules/.public-surface-patterns"
305
+ PAT_DEFAULTS="$REPO_ROOT/.claude/rules/.public-surface-patterns.defaults"
306
+
307
+ if [ -f "$PAT_LOCAL" ] || [ -f "$PAT_DEFAULTS" ]; then
308
+ n=0; m=0
309
+ [ -f "$PAT_LOCAL" ] && n=$(awk 'NF && $0 !~ /^[[:space:]]*#/ {c++} END{print c+0}' "$PAT_LOCAL" 2>/dev/null)
310
+ [ -f "$PAT_DEFAULTS" ] && m=$(awk 'NF && $0 !~ /^[[:space:]]*#/ {c++} END{print c+0}' "$PAT_DEFAULTS" 2>/dev/null)
311
+ say " 로컬 패턴 ${n:-0}행 · 기본 패턴 ${m:-0}행"
312
+ if [ "${n:-0}" -eq 0 ] && [ "${m:-0}" -eq 0 ]; then
313
+ fail "패턴 파일은 있으나 유효 행이 0 — 스캔이 아무것도 안 잡는다(무음 무력화)"
314
+ else
315
+ pass "패턴 소스 유효"
316
+ fi
317
+ else
318
+ absent "기밀 패턴 소스 부재 — 로컬(.public-surface-patterns, gitignored)과 기본(.defaults) 둘 다 없음.
319
+ 그 전까지 기밀 스캔은 fail-closed 하거나(차단만 함) 무의미하다(아무것도 안 잡음)."
320
+ fi
321
+
322
+ # ---------------------------------------------------------------------------
323
+ # 요약
324
+ # ---------------------------------------------------------------------------
325
+ say ""
326
+ say "── 요약 ──────────────────────────────────────────────────"
327
+ say " 성립한 쌍: $PAIRS_OK · 붕괴한 쌍: $PAIRS_BROKEN · 미설치: $NOT_INSTALLED"
328
+
329
+ if [ "$PAIRS_BROKEN" -gt 0 ]; then
330
+ say ""
331
+ say " ❌ 쌍이 붕괴했다 — 게이트가 있지만 옳게 동작하지 않는다."
332
+ exit 1
333
+ fi
334
+
335
+ if [ "$NOT_INSTALLED" -gt 0 ]; then
336
+ say ""
337
+ say " ⛔ 판정 불가 — 게이트 $NOT_INSTALLED 종이 미설치다."
338
+ say " 부재는 통과가 아니다. 이식(C 단계) 후 이 앵커를 다시 돌려라."
339
+ exit 10
340
+ fi
341
+
342
+ say ""
343
+ say " ✅ 모든 known-pair 성립 — 게이트가 옳은 이유로 초록이다."
344
+ exit 0
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bash
2
+ # knowledge_seam_check.sh — 기계 판정: knowledge/{org}/ 이 seam 계약(K1·K2·K3)을 만족하는가.
3
+ # 정본: knowledge/shared/rules/knowledge_layer_seam.md
4
+ # provenance: pmh-dev@cbe3932 역수확(2026-08-10). FH 이식 시 같은 계열 적대 라운드(M4·S5·R5)
5
+ # + 교차 계열 codex 라운드(M6·S3·R2)가 잡은 결함 전건 수리 — 상세는 PR 캡슐.
6
+ #
7
+ # 파서 경계(명명된 잔여): frontmatter 는 제한 awk 파서다 — YAML 전체 의미론(컬렉션 []·{} ·
8
+ # 공백만 든 quoted string · 앵커/별칭)은 범위 밖. find 는 line-based 라 개행 포함 파일명 미지원.
9
+ # 인덱스 폴백은 공백-비포함 파일명 전제(공백 포함 파일명은 개행과 같은 족의 범위 밖 — frontmatter 경로 사용).
10
+ # 수집의 TOCTOU(cd 후 상태 변화)는 stderr 기반 승격까지만. 여기 명명하고 더 조이지 않는다.
11
+ # Exit 계약 (typed — stdout prose 가 아니라 이 코드가 판정이다):
12
+ # 0 = PASS 1 = FAIL(계약 위반) 2 = usage 오류
13
+ # 3 = UNMEASURED 4 = READ-FAIL(경로/권한/수집 실패 — §6 «실패로 보고», 부재로 위장 금지)
14
+ # 호출자는 3·4 를 명시 처리한다 — 0/비0 이분법에서 3·4 는 FAIL 쪽으로 떨어진다(안전 방향).
15
+ # Usage: bash scripts/knowledge_seam_check.sh <org-dir>
16
+ set -uo pipefail
17
+ DIR="${1:-}"
18
+ [ -z "$DIR" ] && { echo "usage: $0 <org-dir>"; exit 2; }
19
+ DIR="${DIR%/}" # trailing slash 로 판정이 갈리지 않게 정규화
20
+
21
+ fail=0
22
+ _no(){ echo "FAIL $1"; fail=1; }
23
+ _ok(){ echo "PASS $1"; }
24
+ _warn(){ echo "WARN $1"; }
25
+ _unm(){ echo "UNMEASURED $1"; }
26
+
27
+ if [ ! -e "$DIR" ]; then
28
+ _unm "K1: $DIR 부재 — '맥락 없음'이지 '위반'이 아니다(§1 빈 슬롯 규약)."
29
+ echo "-- seam check: UNMEASURED --"; exit 3
30
+ fi
31
+ if [ ! -d "$DIR" ] || [ ! -r "$DIR" ] || [ ! -x "$DIR" ]; then
32
+ echo "READ-FAIL $DIR 접근 불가(비디렉터리/권한) — §6: 조용히 빈 결과로 진행하지 않는다."
33
+ echo "-- seam check: READ-FAIL --"; exit 4
34
+ fi
35
+
36
+ # 잔여(정책 선택, R): 숨김 하위 트리는 -prune 하지 않는다 — 그 안의 권한 오류도 READ-FAIL 로
37
+ # 승격된다(무시 대상의 오류가 판정을 멈추는 쪽 = 보수 방향 선택).
38
+ # 파일 수집 — $DIR 안에서 상대경로로 (숨김 조상 경로가 '*/.*' 필터를 오발동시키지 않게),
39
+ # find 의 실패(stderr)는 삼키지 않고 READ-FAIL 로 승격한다(§6).
40
+ FIND_ERR=$(mktemp); trap 'rm -f "$FIND_ERR"' EXIT
41
+ MD_LIST=$(cd "$DIR" && find . -type f -name '*.md' -not -path '*/.*' 2>"$FIND_ERR" | sed 's|^\./||')
42
+ if [ -s "$FIND_ERR" ]; then
43
+ echo "READ-FAIL 파일 수집 실패: $(head -1 "$FIND_ERR") — §6: 실패는 부재가 아니다."
44
+ echo "-- seam check: READ-FAIL --"; exit 4
45
+ fi
46
+ MD_COUNT=$(printf '%s\n' "$MD_LIST" | grep -c . || true)
47
+ if [ "$MD_COUNT" -eq 0 ]; then
48
+ _unm "K1: $DIR 에 마크다운 0건 — 빈 슬롯. K2/K3 는 검사 대상이 없어 UNMEASURED(공허 PASS 금지)."
49
+ echo "-- seam check: UNMEASURED --"; exit 3
50
+ fi
51
+ _ok "K1 마크다운 ${MD_COUNT}건"
52
+
53
+ # 진입 인덱스 — 후보 4종. 이 목록은 계약 §0/K2·Step 2-O 와 동일해야 한다(3표면 정합).
54
+ IDX_BASE=""
55
+ for c in INDEX.md index.md README.md readme.md; do
56
+ [ -f "$DIR/$c" ] && { IDX_BASE="$c"; break; }
57
+ done
58
+ IDX=""
59
+ if [ -z "$IDX_BASE" ]; then
60
+ _no "K2 진입 인덱스 부재 ($DIR/INDEX.md·index.md·README.md·readme.md 중 하나)"
61
+ else
62
+ IDX="$DIR/$IDX_BASE"
63
+ if [ ! -r "$IDX" ]; then
64
+ echo "READ-FAIL 진입 인덱스 읽기 불가: $IDX — §6: 실패는 부재/통과로 위장하지 않는다."
65
+ echo "-- seam check: READ-FAIL --"; exit 4
66
+ fi
67
+ L=$(wc -l < "$IDX" | tr -d ' '); L=${L:-0}
68
+ C=$(wc -c < "$IDX" | tr -d ' '); C=${C:-0}
69
+ _ok "K2 인덱스 존재: $IDX"
70
+ # ⚠️ 크기는 보고만 한다(원본의 '≤200줄'은 유도 없는 임계였다 — UNCALIBRATED 이력 보존).
71
+ # 단위 주의: wc -c = 바이트(자수 아님 — 한글 3바이트/자라 토큰 추정 과대 가능).
72
+ echo "UNCALIBRATED K2-c 인덱스 크기 ${L}줄 / ${C}바이트 (≈$((C/2100))k 토큰 추정, 한글 과대 가능) — 판정 근거 아님."
73
+ fi
74
+
75
+ # frontmatter 파서 — 첫 ---/둘째 --- 사이에서 «키: 비어있지 않은 값» (빈 값은 보유가 아니다)
76
+ _fm_has(){ # $1 파일 $2 키 정규식(alternation 허용) — 닫힌 frontmatter 안의 비어있지 않은 값만 인정
77
+ # ⚠️ awk 의 exit 는 END 로 점프하고 END 의 exit 가 상태를 덮어쓴다 — 그래서 판정은 END 한 곳에서만.
78
+ awk -v key="$2" '
79
+ BEGIN{q=sprintf("%c",39)}
80
+ NR==1 && $0!="---"{bad=1; exit}
81
+ /^---$/{n++; if(n==2){closed=1; exit} next}
82
+ n==1 && $0 ~ "^("key"):"{
83
+ val=$0; sub("^[^:]*:[[:space:]]*","",val)
84
+ sub("[[:space:]]+#.*$","",val) # YAML inline comment 제거 후 판정
85
+ sub("[[:space:]]*$","",val)
86
+ if (val == "") next
87
+ if (val == "\"\"" || val == q q) next # quoted-empty
88
+ if (val == "null" || val == "~") next
89
+ if (val ~ /^[|>][0-9+-]*$/) next # block scalar 머리만 있는 것(|,|-,|2,|+2,>-,>2- 등 indicator 변형 포함)
90
+ found=1
91
+ }
92
+ END{exit (!bad && closed && found)?0:1}' "$1" 2>/dev/null
93
+ }
94
+ # 인덱스 폴백 — 고정문자열 매치 + 경계 문자 + 설명 잔여 요구 (regex 합성 금지: ERE/BRE 혼합 사고 차단)
95
+ _idx_desc(){ # $1 상대경로 — IDX 전역 사용. 성공=0
96
+ [ -n "$IDX" ] || return 1
97
+ local line pre post restc
98
+ while IFS= read -r line; do
99
+ case "$line" in *"$1"*) ;; *) continue;; esac
100
+ pre="${line%%"$1"*}"; post="${line#*"$1"}"
101
+ # "/" 는 경계가 아니다 — docs/a.md 줄이 루트 a.md 로 오인정되는 suffix 매치 차단
102
+ case "${pre: -1}" in ""|" "|"("|'`') ;; *) continue;; esac
103
+ # 후행 경계는 «끝/공백/백틱/닫는괄호»만 — -,:,, 는 파일명 연속 문자일 수 있다(a.md-bak 오인정 차단)
104
+ case "${post:0:1}" in ""|" "|'`'|")") ;; *) continue;; esac
105
+ restc=$(printf '%s' "$post" | tr -d ' `—:,)-' | wc -c | tr -d ' ')
106
+ [ "$restc" -gt 2 ] && return 0
107
+ done < <(grep -F -- "$1" "$IDX" 2>/dev/null)
108
+ return 1
109
+ }
110
+
111
+ MISS_D=0; MISS_DATE=0; CHECKED=0; HAS_RA=0; READ_MISS=0
112
+ while IFS= read -r rel; do
113
+ [ -z "$rel" ] && continue
114
+ # 제외는 «루트의» 진입 인덱스와 SEAM.md 만 — 하위 디렉터리의 동명 파일은 내용 페이지다(K3 대상).
115
+ # 비교는 소문자 정규화: 대소문자 무시 FS(macOS)에선 [ -f INDEX.md ] 가 index.md 에 매치돼
116
+ # IDX_BASE 표기와 find 실명이 갈릴 수 있다(레인 L14 가 잡은 실측).
117
+ rel_lc=$(printf '%s' "$rel" | tr 'A-Z' 'a-z')
118
+ [ -n "$IDX_BASE" ] && [ "$rel_lc" = "$(printf '%s' "$IDX_BASE" | tr 'A-Z' 'a-z')" ] && continue
119
+ [ "$rel_lc" = "seam.md" ] && continue
120
+ f="$DIR/$rel"
121
+ if [ ! -r "$f" ]; then READ_MISS=$((READ_MISS+1)); echo " unreadable: $f"; continue; fi
122
+ CHECKED=$((CHECKED+1))
123
+ has_desc=0
124
+ _fm_has "$f" "description" && has_desc=1
125
+ [ "$has_desc" -eq 0 ] && _idx_desc "$rel" && has_desc=1
126
+ if [ "$has_desc" -eq 0 ]; then
127
+ MISS_D=$((MISS_D+1)); [ "$MISS_D" -le 5 ] && echo " no-desc: $f"
128
+ fi
129
+ _fm_has "$f" "date|updated|last_updated" || MISS_DATE=$((MISS_DATE+1))
130
+ _fm_has "$f" "review_after" && HAS_RA=$((HAS_RA+1))
131
+ done <<EOF
132
+ $MD_LIST
133
+ EOF
134
+ if [ "$READ_MISS" -gt 0 ]; then
135
+ echo "READ-FAIL 읽기 불가 파일 ${READ_MISS}건 — §6: 실패는 부재/통과로 위장하지 않는다."
136
+ echo "-- seam check: READ-FAIL --"; exit 4
137
+ fi
138
+
139
+ if [ "$CHECKED" -eq 0 ]; then
140
+ _unm "K3: 인덱스 외 내용 페이지 0건 — 검사 대상 없음. 보일러플레이트만으로 PASS 를 사지 않는다."
141
+ echo "-- seam check: UNMEASURED --"; exit 3
142
+ fi
143
+ [ "$MISS_D" -eq 0 ] && _ok "K3 설명 ${CHECKED}건 전건 존재" || _no "K3 설명 누락 ${MISS_D}/${CHECKED}건"
144
+ [ "$MISS_DATE" -eq 0 ] && _ok "K3-d 최종갱신일 ${CHECKED}건 전건 존재" \
145
+ || _warn "K3-d 날짜 누락 ${MISS_DATE}/${CHECKED}건 — stale 판정 불가 → '미상'으로 보고(§6 · 권장 조항이라 비차단)"
146
+ echo "INFO K3-f review_after 보유 ${HAS_RA}/${CHECKED}건 — 0건이면 Step 2-O 는 전건 Partial 상한(Grounded 도달 불가)"
147
+
148
+ S="$DIR/SEAM.md"
149
+ if [ -f "$S" ] && [ ! -r "$S" ]; then
150
+ echo "READ-FAIL SEAM.md 읽기 불가: $S — §6."
151
+ echo "-- seam check: READ-FAIL --"; exit 4
152
+ fi
153
+ if [ -f "$S" ]; then
154
+ # Done When 이 «frontmatter 에 기록»이므로 frontmatter 파서로 본다 — 본문/코드블록 기재는 불인정.
155
+ # 닫힘 --- 없는 frontmatter 는 불인정(END 도달 = 미닫힘 = 실패).
156
+ if awk 'NR==1 && $0!="---"{bad=1; exit} /^---$/{n++; if(n==2){closed=1; exit} next} n==1 && $0 ~ "^adoption_path:[[:space:]]*(fork|git-map|export-sync|extract|conform)[[:space:]]*$"{ok=1} END{exit (!bad && closed && ok)?0:1}' "$S"; then
157
+ _ok "SEAM.md adoption_path (frontmatter) 기록됨"
158
+ else
159
+ _no "SEAM.md frontmatter 에 유효한 adoption_path 없음 (본문 기재·미닫힘 frontmatter 불인정)"
160
+ fi
161
+ if awk 'NR==1 && $0!="---"{bad=1; exit} /^---$/{n++; if(n==2){closed=1; exit} next} n==1 && $0 ~ "^synced_at:[[:space:]]*[0-9]{4}-[0-9]{2}-[0-9]{2}[[:space:]]*$"{ok=1} END{exit (!bad && closed && ok)?0:1}' "$S"; then
162
+ _ok "SEAM.md synced_at (YYYY-MM-DD) 기록됨"
163
+ else
164
+ _no "SEAM.md frontmatter 에 synced_at(YYYY-MM-DD 정확 형식) 없음 — Done When mandatory 항목"
165
+ fi
166
+ else
167
+ _no "SEAM.md 부재 — 채택 경로 기록 필요(§Done When)"
168
+ fi
169
+
170
+ echo "-- seam check: $([ "$fail" -eq 0 ] && echo PASS || echo FAIL) --"
171
+ exit "$fail"
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bash
2
+ # rtk_gross_ablation.sh — RTK gross-savings ablation
3
+ # provenance: pmh-dev@cbe3932 역수확(2026-08-10). FH 의 context-doctor 가 벤더 주장(~60-90%)만
4
+ # 인용하던 자리에 실측 계기를 공급 — Step 0 계기검증(필터가 에러로 죽어 스텁을 뱉으면
5
+ # 허위 절감률이 나오는 실측 사례) 내장.
6
+ # RTK gross-savings ablation — run in an env where RTK ACTUALLY filters (rtk gain 이 실절감을 보이는 곳).
7
+ # Measures raw vs rtk-filtered output TOKENS across representative read-only dev commands.
8
+ #
9
+ # ⚠️ INSTRUMENT CHECK included: FH 외부 세션(2026-07-07)서 rtk 필터가 "No such file"로
10
+ # 에러 죽으며 46자 스텁을 뱉어 "82~100% cut" 허위결과가 나왔음. 계측기부터 검증한다.
11
+ # ⚠️ NET(맥락손실)은 이 스크립트가 아님 — graded 품질 프로브(턴수·교정·정답률) 별도.
12
+ # ⚠️ 판정/검증 태스크엔 RTK 미적용(verdict 오염) — 여기 태스크셋은 전부 read-only non-verdict.
13
+ set -u
14
+ # 근사 주의: wc -c / 4 = char 근사(밀도 균일 가정) — 필터 출력은 경로·해시 위주라 절감률이
15
+ # **과대측 방향**으로 치우칠 수 있다. 부호는 안전하나 «밴드 정합» 판정엔 오차 감안.
16
+ # 해석부 상수(~20-30% 슬라이스 비중 · 15-25% 밴드 · RATE)는 원 필드 보고서 유래의 «가정» — 출처: provenance 커밋.
17
+ RTK="${RTK:-$(command -v rtk || echo "$HOME/.headroom/bin/rtk")}"
18
+ REPO="${1:-$PWD}"
19
+ RATE="${RATE:-1.82}" # $/1M tokens — 측정 블렌디드(Token Checker). 필요시 override.
20
+ cd "$REPO" 2>/dev/null || { echo "repo 없음: $REPO"; exit 1; }
21
+ git rev-parse --git-dir >/dev/null 2>&1 || { echo "git 레포 아님: $REPO — git 커맨드 태스크셋이 무의미(과소측 방향 오결론)"; exit 1; }
22
+
23
+ # --- 0. 계측기 검증: rtk가 실제로 FILTER하나 (에러 아님) ---
24
+ praw=$(git status 2>&1); prtk=$("$RTK" git status 2>&1)
25
+ if echo "$prtk" | grep -qiE "no such file|os error|not found|error\]" \
26
+ && ! echo "$praw" | grep -qiE "no such file"; then
27
+ echo "❌ RTK 필터 비작동 (rtk git status 에러): $prtk"
28
+ echo " → rtk 설정/환경 의존. 'rtk gain' 이 실절감을 보이는 env 에서 돌려라."
29
+ exit 2
30
+ fi
31
+ echo "✅ rtk 필터 작동 확인 — 진행."; echo ""
32
+
33
+ # --- 태스크셋 (read-only, non-verdict, RTK가 타깃하는 dev 출력) ---
34
+ # ⚠️ CMDS 에 quote/pipe/glob 든 커맨드 추가 금지 — raw(eval) 와 rtk(word-split) 실행 경로가
35
+ # 비대칭이라 다른 커맨드를 재게 된다.
36
+ CMDS=("git status" "git log --oneline -150" "git diff HEAD~20" "git branch -a" "ls -la")
37
+ tot_r=0; tot_k=0
38
+ printf "%-28s %9s %9s %6s\n" "command" "raw_tok" "rtk_tok" "cut%"
39
+ printf -- "----\n"
40
+ for c in "${CMDS[@]}"; do
41
+ r=$(( $(eval "$c" 2>&1 | wc -c) / 4 )) # ~4 chars/token 근사(헤더 주의 참조)
42
+ kout=$("$RTK" $c 2>&1)
43
+ # per-command 계기검증 — Step 0 은 env 고장만 닫는다. 행 단위 rtk 에러/스텁은 여기서 잡아
44
+ # 총계에서 제외(허위 절감의 역사적 형태: 에러 스텁 → 82~100% 허위 컷).
45
+ if echo "$kout" | grep -qiE "no such file|os error|not found|error\]|panicked at" \
46
+ && ! eval "$c" 2>&1 | grep -qiE "no such file|os error|not found"; then
47
+ printf "%-28s %9d %9s %6s\n" "$c" "$r" "FAIL" "제외"
48
+ continue
49
+ fi
50
+ k=$(( $(printf '%s' "$kout" | wc -c) / 4 ))
51
+ cut=$(awk "BEGIN{if($r>0)printf \"%.0f\",($r-$k)/$r*100;else print 0}")
52
+ printf "%-28s %9d %9d %5s%%\n" "$c" "$r" "$k" "$cut"
53
+ tot_r=$((tot_r+r)); tot_k=$((tot_k+k))
54
+ done
55
+ printf -- "----\n"
56
+ gross=$(awk "BEGIN{if($tot_r>0)printf \"%.1f\",($tot_r-$tot_k)/$tot_r*100;else print 0}")
57
+ saved_tok=$((tot_r-tot_k))
58
+ echo "GROSS (커맨드/툴-출력 슬라이스): raw=$tot_r tok · rtk=$tot_k tok · 절감=$gross%"
59
+ echo ""
60
+ echo "해석:"
61
+ echo " • 이건 커맨드/툴-출력 *슬라이스만*의 절감률 (세션 전체의 ~20-30%)."
62
+ echo " • 청구서 전체 gross ≈ (슬라이스 비중) × $gross% → 보고서 15-25% 밴드와 대조."
63
+ echo " • \$ 환산: 절감 토큰 × \$$RATE/1M = 슬라이스 절감액."
64
+ echo " • NET(맥락손실)은 미측정 — gross만으론 net+ 단정 금지(조용한 오답 리스크)."
@@ -320,6 +320,21 @@ else
320
320
  fail=1
321
321
  fi
322
322
 
323
+ # marker axes-run lanes — 훅의 4축 자기대조 형식 검사(§CLAUDE.md 3층 자기 대조)의 앵커.
324
+ # subject 부재를 FAIL 로 두는 이유는 branch-claim 블록과 같다: 이 subject 는 pre-commit 훅
325
+ # 자체이고 files[] 에 있으므로 «정당한 부재» 가 없다. 부재 = 삭제다.
326
+ if [ ! -f templates/.git-hooks/pre-commit ]; then
327
+ echo "FAIL templates/.git-hooks/pre-commit absent — the gate itself is missing, not a skip"
328
+ fail=1
329
+ elif [ -f scripts/test_marker_axes_run_lanes.sh ]; then
330
+ if ! bash scripts/test_marker_axes_run_lanes.sh; then
331
+ fail=1
332
+ fi
333
+ else
334
+ echo "FAIL test_marker_axes_run_lanes.sh: pre-commit present but its axes-run anchor is missing"
335
+ fail=1
336
+ fi
337
+
323
338
  # branch-claim lanes — shipped with 27 hand-run lanes and ZERO callers, so CI never ran one of them.
324
339
  # That is the exact defect its own subject guards against (a gate nobody claims against passes
325
340
  # forever), reproduced one layer up in its anchor. Same SKIP/FAIL shape: a missing subject is a
@@ -270,6 +270,9 @@ if [ -n "$LAST_TAG" ] && [ -f "$FH/package.json" ]; then
270
270
  CHANGED_SINCE_TAG=$(git -C "$FH" diff --name-only "$LAST_TAG"..HEAD 2>/dev/null)
271
271
  if printf '%s\n' "$CHANGED_SINCE_TAG" | grep -qE "$SHIP_RE"; then
272
272
  echo "⚠️ ④-b npm-shipped assets changed since $LAST_TAG — propose lockstep republish (never auto)"
273
+ # env-purity advisory (2026-08-10, 운영자 «자체점검기 모듈화»): 큰 업그레이드 시점 = 출하자산
274
+ # 변경 시점 — 공유층의 환경-전용 산문을 세서 포인터화/독해규칙 후보를 표면화한다 (advisory).
275
+ [ -x scripts/env_purity_scan.sh ] && bash scripts/env_purity_scan.sh 2>/dev/null | tail -3
273
276
  fi
274
277
  # ④-b-drift: auto-FIRE a drift-CANDIDATE reminder (not a parity verdict) — **in BOTH directions**.
275
278
  # The two entry points are read by DIFFERENT runtimes (CLAUDE.md → Claude Code · AGENTS.md/codex-compat