@chrono-meta/fh-gate 2.6.0 → 2.8.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.
- package/.claude/rules/fh_4axis_gate.md +26 -3
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +28 -3
- package/CLAUDE.md +146 -168
- package/README.ja.md +144 -24
- package/README.ko.md +135 -22
- package/README.md +204 -27
- package/README.zh.md +126 -21
- package/docs/ETHOS.md +10 -3
- package/knowledge/shared/dialogue/ai_dialogue_playbook.md +131 -0
- package/knowledge/shared/harness-core/claude_md_gate_details.md +90 -0
- package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +75 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +77 -3
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +92 -5
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +12 -2
- package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +32 -0
- package/knowledge/shared/harness-core/ship_readiness_gate.md +77 -8
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +196 -0
- package/knowledge/shared/rules/knowledge_layer_seam.md +1 -1
- package/knowledge/shared/rules/multi_session_close_protocol.md +7 -3
- package/package.json +15 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +157 -0
- package/plugins/fh-meta/agents/persona-innovator.md +170 -0
- package/plugins/fh-meta/skills/cross-ecosystem-synergy-detection/SKILL.md +37 -0
- package/plugins/fh-meta/skills/public-surface-audit/SKILL_detail.md +16 -1
- package/plugins/fh-meta/skills/steel-quench/SKILL.md +97 -0
- package/scripts/adapters/peer_resolve.sh +58 -6
- package/scripts/branch_claim.sh +3 -1
- package/scripts/cluster_capability_scan.sh +42 -13
- package/scripts/digest_landing_check.sh +142 -2
- package/scripts/fh_hub_identity.sh +83 -0
- package/scripts/fh_session_load.sh +53 -5
- package/scripts/fh_track_resolve.sh +114 -0
- package/scripts/field_canon_preload.sh +50 -5
- package/scripts/listing_watch.sh +187 -0
- package/scripts/package_coverage_check.sh +8 -0
- package/scripts/prior_art_prompt.sh +168 -0
- package/scripts/psa_scan_lib.sh +201 -15
- package/scripts/residency_admission_check.sh +204 -0
- package/scripts/selfcheck.sh +103 -0
- package/scripts/test_adapter_lanes.sh +67 -2
- package/scripts/test_heavy_classifier_lanes.sh +144 -0
- package/scripts/test_listing_watch_lanes.sh +162 -0
- package/scripts/test_marker_defense_lanes.sh +152 -0
- package/scripts/test_marker_soul_check_lanes.sh +211 -0
- package/scripts/test_prior_art_prompt_lanes.sh +128 -0
- package/scripts/test_psa_singlefile_lanes.sh +351 -1
- package/scripts/test_residency_admission_lanes.sh +60 -0
- package/scripts/test_track_resolve_lanes.sh +158 -0
- package/scripts/test_wizard_snippet_merge_lanes.sh +40 -0
- package/templates/.git-hooks/pre-commit +400 -4
- package/templates/.git-hooks/pre-push +17 -2
- package/templates/settings.PriorArt.snippet.json +44 -0
package/scripts/psa_scan_lib.sh
CHANGED
|
@@ -112,7 +112,19 @@ psa_low_allowlisted() {
|
|
|
112
112
|
# Prints diagnostics for every degraded state; sets the flags above. NEVER exits — the caller owns
|
|
113
113
|
# the degrade direction, because it differs by surface (see the header).
|
|
114
114
|
psa_load() {
|
|
115
|
-
local defaults="$1" override="$2" raw ov
|
|
115
|
+
local defaults="$1" override="$2" raw ov _tabtest
|
|
116
|
+
# 🟥 R5-3: 이 라이브러리는 `$'\t'` (ANSI-C quoting) 에 의존한다 — **POSIX 가 아니다.**
|
|
117
|
+
# dash 에서는 리터럴 `$\t` 로 읽혀 정상 행이 «TAB 없음» 으로 떨어지고, 스트림이 통째로
|
|
118
|
+
# 비면서 **bad=1 · stream=[] 로 조용히 무력화**됐다(cross-family 실측). 그건 미측정이지
|
|
119
|
+
# 「패턴이 없다」가 아니다. 지원 범위는 bash/zsh 이고, 그걸 산문이 아니라 여기서 확인한다.
|
|
120
|
+
# 판별은 길이다 — 실측: bash/zsh `len=1`(진짜 TAB) · dash `len=3`(리터럴 `$\t`).
|
|
121
|
+
_tabtest=$'\t'
|
|
122
|
+
if [ "${#_tabtest}" -ne 1 ]; then
|
|
123
|
+
echo " ❌ INSTRUMENT DEAD — this shell does not support \$'\\t' (ANSI-C quoting)." >&2
|
|
124
|
+
echo " psa_scan_lib.sh requires bash or zsh. Re-run under one of them; NOT SCANNED." >&2
|
|
125
|
+
PSA_STREAM=""; PSA_DEFAULTS_OK=0; PSA_OVERRIDE_PRESENT=0; PSA_BAD_ROWS=1
|
|
126
|
+
return 3
|
|
127
|
+
fi
|
|
116
128
|
PSA_STREAM=""; PSA_DEFAULTS_OK=0; PSA_OVERRIDE_PRESENT=0; PSA_BAD_ROWS=0
|
|
117
129
|
|
|
118
130
|
# Layer 1 — committed defaults. This file ships with the repo, so missing/unreadable/empty is a
|
|
@@ -156,11 +168,23 @@ $ov"; PSA_OVERRIDE_PRESENT=1
|
|
|
156
168
|
echo " ❌ unusable pattern row (no TAB between severity and regex): ${line%%$'\t'*}"
|
|
157
169
|
PSA_BAD_ROWS=$((PSA_BAD_ROWS+1)); continue
|
|
158
170
|
fi
|
|
159
|
-
|
|
171
|
+
# 🟥 R4-1: 여기도 `cmd; rc=$?` 였다 — P3 를 스캔 경로에서만 고치고 **로더 경로로 전파를 안 봤다**.
|
|
172
|
+
# 정상 패턴은 빈 입력에 rc=1 을 내므로 `set -e` 호출자는 psa_load 에서 죽는다(선재, 양 셸 실측).
|
|
173
|
+
if printf '' | grep -E "$re" >/dev/null 2>&1; then rc=0; else rc=$?; fi # >=2 = regex error
|
|
160
174
|
if [ "$rc" -ge 2 ]; then
|
|
161
175
|
echo " ❌ unusable pattern (invalid regex) — this detector would match nothing: [${line%%$'\t'*}]"
|
|
162
176
|
PSA_BAD_ROWS=$((PSA_BAD_ROWS+1)); continue
|
|
163
177
|
fi
|
|
178
|
+
# 🟥 P4 (선재 결함): 판별력 없는 패턴을 거부한다 — 빈 패턴 · `^` · `a*` · `SECRET|^$`.
|
|
179
|
+
# 모든 입력에 매치하므로 검출기로 **쓸 수 없는데** 초판 로더는 유효로 받아들였다.
|
|
180
|
+
# 🟥 그리고 이 검사는 **위의 컴파일 검사로는 못 한다**: `printf ''` 는 줄을 0개 주므로
|
|
181
|
+
# grep 이 매치할 대상 자체가 없어 **어떤 패턴이든 rc=1** 이다(실측). 위 주석의
|
|
182
|
+
# 「0 = 컴파일됨」은 도달 불가능한 값을 서술한 것이었다. 빈 «줄» 을 따로 먹인다.
|
|
183
|
+
if printf '\n' | grep -E "$re" >/dev/null 2>&1; then rc=0; else rc=$?; fi
|
|
184
|
+
if [ "$rc" -eq 0 ]; then
|
|
185
|
+
echo " ❌ unusable pattern (matches the EMPTY string — no discrimination): [${line%%$'\t'*}]"
|
|
186
|
+
PSA_BAD_ROWS=$((PSA_BAD_ROWS+1)); continue
|
|
187
|
+
fi
|
|
164
188
|
valid="$valid
|
|
165
189
|
$line"
|
|
166
190
|
done <<PSA_ROWS
|
|
@@ -208,31 +232,128 @@ psa_detect_operator_context() {
|
|
|
208
232
|
# Why path-tagged input: the LOW allowlist is per FILE, so a scanner that flattens content loses the
|
|
209
233
|
# only information needed to apply it. The push copy did exactly that and blocked its own first real
|
|
210
234
|
# push on a companion-store name inside the script whose job is to sync to the companion store.
|
|
235
|
+
# _psa_in_selftest — «지금 psa_require_live 안에서 불린 것인가»를 **호출 스택**으로 판별한다.
|
|
236
|
+
# 🟥 변수 표식을 쓰면 안 된다: 외부가 `_PSA_IN_SELFTEST=1` 을 미리 export 하는 것만으로
|
|
237
|
+
# 자가검사가 통째로 생략된다(cross-family 반례 1b, 실행 재현). 전역 상태를 권한 표식처럼
|
|
238
|
+
# 믿는 형태다. 호출 스택은 **호출자가 미리 심을 수 없다.**
|
|
239
|
+
# bash=FUNCNAME · zsh=funcstack. 둘 다 없으면 판별 불가이므로 **재진입이 아니라고 답한다** —
|
|
240
|
+
# 그러면 자가검사가 한 번 더 돌 뿐(무한재귀는 psa_require_live 가 이 함수를 한 겹만 부르므로
|
|
241
|
+
# 발생하지 않는다), 반대로 «재진입이다」로 접으면 그게 fail-open 이다.
|
|
242
|
+
_psa_in_selftest() {
|
|
243
|
+
local _st=""
|
|
244
|
+
# shellcheck disable=SC2154
|
|
245
|
+
if [ -n "${FUNCNAME[*]:-}" ]; then _st=" ${FUNCNAME[*]} "
|
|
246
|
+
elif [ -n "${funcstack[*]:-}" ]; then _st=" ${funcstack[*]} "
|
|
247
|
+
else return 1; fi
|
|
248
|
+
case "$_st" in *" psa_require_live "*) return 0 ;; esac
|
|
249
|
+
return 1
|
|
250
|
+
}
|
|
251
|
+
|
|
211
252
|
psa_scan_tagged() {
|
|
212
|
-
|
|
213
|
-
|
|
253
|
+
# 🟥 `path` 를 지역선언하지 마라 — zsh 에서 `path` 는 `PATH` 와 **tied 된 특수 배열**이라
|
|
254
|
+
# `local path` 하는 순간 그 스코프의 `PATH` 가 빈 문자열이 되고, 바로 다음 줄의
|
|
255
|
+
# `$(cat)` 부터 `command not found: cat` 으로 죽는다. 그런데 이 함수의 계약이
|
|
256
|
+
# 「빈 입력 = 신고할 것 없음 = return 0」이라 **계기 사망이 «깨끗한 스캔»과 바이트 단위로
|
|
257
|
+
# 같은 출력**이 된다 — 이 저장소가 이름 붙인 «미측정을 0으로 렌더» 의 가장 조용한 얼굴이다.
|
|
258
|
+
# 실측 2026-08-21: zsh 에서 known-positive·known-negative 가 **둘 다 rc=0** 이었다.
|
|
259
|
+
# 형제 특수변수(`fpath` `status` `argv` `cdpath` …)도 같은 함정이다.
|
|
260
|
+
local input hit=0 row sev re _psa_path body tok _line_hits _tok_hits _lg _tg
|
|
261
|
+
|
|
262
|
+
# ── E: 진입 생존성 가드 (2026-08-21, cross-family FAIL 판정 후 재작성) ─────────────
|
|
263
|
+
# 🟥 **초판 E 는 fail-open 을 못 닫았다.** codex/gpt-5.6-sol 이 반례 6개를 전부 실행으로
|
|
264
|
+
# 재현했고 판정은 FAIL 이었다. 초판의 구조적 맹점: **«자가검사가 한 번 통과했다»를
|
|
265
|
+
# «그 뒤의 실제 스캔도 실행됐다»로 확장**한 것. 그 사이엔 시간·전역상태·파이프라인
|
|
266
|
+
# 상태라는 단절점이 셋 있다. 재작성이 닫는 것과 못 닫는 것을 아래에 이름으로 적는다.
|
|
267
|
+
#
|
|
268
|
+
# 🟥 **캐시 제거.** 초판은 `_PSA_LIVE_OK` 로 프로세스당 1회만 검사했다 — 첫 양성 뒤
|
|
269
|
+
# grep 을 죽이면 같은 양성이 rc=0 이 됐다(codex 재현). 그리고 **캐시는 뚫린 쪽(긴
|
|
270
|
+
# 스킬 세션)에서만 위험하고 안전한 쪽(짧은 훅)에서만 이득**이라 방향이 반대였다.
|
|
271
|
+
# 비용은 **실측했다: psa_require_live 1회 ≈ 64ms** (훅 경로 최대 4회 = 0.26초).
|
|
272
|
+
# 초판은 이 비용을 **안 재고** 캐시를 정당화했다 — 그게 진짜 결함이었다.
|
|
273
|
+
#
|
|
274
|
+
# 🟥 **재진입 표식은 `local` 로 «동적 스코프»에 둔다.** 초판은 전역 변수를 권한 표식처럼
|
|
275
|
+
# 믿어서, 외부가 `_PSA_IN_SELFTEST=1` 을 미리 export 하면 자가검사가 **통째로 생략**됐다
|
|
276
|
+
# (codex 재현: `grep(){ return 127; }` 상태에서 known-positive 가 rc=0 무출력).
|
|
277
|
+
# `local` 선언은 외부 값을 **가리고**, 함수가 끝나면 **자동 복구**된다 — unset 실패나
|
|
278
|
+
# 시그널·트랩으로 값이 남는 경로가 없어진다.
|
|
279
|
+
if ! _psa_in_selftest; then
|
|
280
|
+
# 🟥 실제 패턴 스트림을 여기서 본다. 자가검사는 PSA_STREAM 을 카나리아로 **바꿔치기**
|
|
281
|
+
# 하므로 «진짜 패턴이 비었거나 깨졌는지»를 구조적으로 못 본다(codex 반례 3).
|
|
282
|
+
if [ -z "${PSA_STREAM:-}" ]; then
|
|
283
|
+
echo " ❌ INSTRUMENT DEAD — PSA_STREAM is empty; nothing to match against. NOT SCANNED."
|
|
284
|
+
echo " ❌ INSTRUMENT DEAD — PSA_STREAM is empty; NOT SCANNED (call psa_load first)." >&2
|
|
285
|
+
return 3
|
|
286
|
+
fi
|
|
287
|
+
# 🟥 R4-6: **부분 로드된 패턴 집합으로 «깨끗»을 말하지 않는다.** 초판은 이것을 «호출자
|
|
288
|
+
# 계약»으로 두었고(헤더가 그렇게 적어뒀다), cross-family 가 그 계약을 안 지키는 호출자를
|
|
289
|
+
# 실측 재현했다 — `SECRET|^$` 한 행이 떨어져 나간 집합으로 `SECRET` 입력이 rc=0 «깨끗».
|
|
290
|
+
# 산문 계약을 라이브러리 층으로 내린다: 안 실린 검출기가 있으면 그건 미측정이다.
|
|
291
|
+
# (psa_load 를 안 부른 호출자는 PSA_BAD_ROWS 가 미설정이고, 그 경로는 위 빈-스트림
|
|
292
|
+
# 검사가 이미 잡는다. `case` 로 보는 이유는 비수치 오염에서 fail-open 하지 않기 위해서다.)
|
|
293
|
+
case "${PSA_BAD_ROWS:-0}" in
|
|
294
|
+
0) ;;
|
|
295
|
+
*) echo " ❌ INSTRUMENT DEAD — ${PSA_BAD_ROWS} pattern row(s) were dropped; the detector set is PARTIAL. NOT SCANNED."
|
|
296
|
+
echo " ❌ INSTRUMENT DEAD — ${PSA_BAD_ROWS} pattern row(s) dropped — a partial set cannot report clean." >&2
|
|
297
|
+
return 3 ;;
|
|
298
|
+
esac
|
|
299
|
+
if ! psa_require_live >/dev/null 2>&1; then
|
|
300
|
+
echo " ❌ INSTRUMENT DEAD — psa_scan_tagged self-test failed. NOT SCANNED."
|
|
301
|
+
echo " ❌ INSTRUMENT DEAD — psa_scan_tagged self-test failed. NOT SCANNED." >&2
|
|
302
|
+
echo " A 0 from this scanner is UNMEASURED, not clean. Re-run under bash." >&2
|
|
303
|
+
return 3
|
|
304
|
+
fi
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
# 🟥 `cat` 의 상태를 버리지 마라 (codex 반례 1). 초판은 `input=$(cat)` 로 대입만 하고
|
|
308
|
+
# rc 를 버려서, cat 이 죽어 입력이 비면 그게 곧 «신고할 것 없음»이 됐다.
|
|
309
|
+
if ! input=$(cat); then
|
|
310
|
+
echo " ❌ INSTRUMENT DEAD — could not read stdin. NOT SCANNED."
|
|
311
|
+
echo " ❌ INSTRUMENT DEAD — could not read stdin. NOT SCANNED." >&2
|
|
312
|
+
return 3
|
|
313
|
+
fi
|
|
214
314
|
[ -n "$input" ] || return 0
|
|
215
315
|
while IFS= read -r row; do
|
|
216
316
|
case "$row" in ''|\#*) continue;; esac
|
|
217
317
|
re="${row#*$'\t'}"; [ "$re" = "$row" ] && continue
|
|
218
318
|
sev="${row%%$'\t'*}"
|
|
319
|
+
# 🟥 grep 상태를 «매치 없음» 으로 접지 마라 (cross-family 반례 2, 2026-08-21).
|
|
320
|
+
# `2>/dev/null || true` 는 rc 0(매치)·1(없음)·**2 이상(계기 오류)**을 하나로 뭉갠다.
|
|
321
|
+
# 실측 도달 경로: 패턴 파일에 **깨진 정규식**이 한 줄 있으면 그 행만 조용히 아무것도
|
|
322
|
+
# 안 잡고 스캔은 rc=0 «깨끗» 을 낸다. 자가검사는 자기 카나리아 패턴을 쓰므로
|
|
323
|
+
# **진짜 패턴의 깨짐을 구조적으로 못 본다** — 그래서 여기서 봐야 한다.
|
|
324
|
+
# 🟥 P3 동형: grep 의 정상 «미매치»(rc=1)가 `set -e` 호출자를 죽인다.
|
|
325
|
+
if _line_hits=$(printf '%s\n' "$input" | grep -iE "$re" 2>/dev/null); then _lg=0; else _lg=$?; fi
|
|
326
|
+
if [ "$_lg" -gt 1 ]; then
|
|
327
|
+
echo " ❌ INSTRUMENT DEAD — grep failed (rc=$_lg) on pattern: $re"
|
|
328
|
+
echo " ❌ INSTRUMENT DEAD — grep failed (rc=$_lg) on pattern: $re" >&2
|
|
329
|
+
echo " A broken pattern row scans NOTHING and reads as clean. NOT SCANNED." >&2
|
|
330
|
+
return 3
|
|
331
|
+
fi
|
|
219
332
|
while IFS= read -r line; do
|
|
220
333
|
[ -z "$line" ] && continue
|
|
221
|
-
|
|
334
|
+
_psa_path="${line%%$'\t'*}"; body="${line#*$'\t'}"
|
|
222
335
|
# EVERY match on the line. Taking only the first let a documented placeholder earlier on the
|
|
223
336
|
# line shield a real token later on it.
|
|
337
|
+
if _tok_hits=$(printf '%s' "$body" | grep -oiE "$re" 2>/dev/null); then _tg=0; else _tg=$?; fi
|
|
338
|
+
if [ "$_tg" -gt 1 ]; then
|
|
339
|
+
echo " ❌ INSTRUMENT DEAD — grep -o failed (rc=$_tg) on pattern: $re"
|
|
340
|
+
echo " ❌ INSTRUMENT DEAD — grep -o failed (rc=$_tg) on pattern: $re" >&2
|
|
341
|
+
return 3
|
|
342
|
+
fi
|
|
224
343
|
while IFS= read -r tok; do
|
|
225
344
|
[ -z "$tok" ] && continue
|
|
226
345
|
printf '%s' "$tok" | grep -qiE "$PSA_PLACEHOLDER" && continue
|
|
227
|
-
if [ "$sev" = "LOW" ] && psa_low_allowlisted "$
|
|
228
|
-
if psa_pair_allowlisted "$
|
|
229
|
-
echo
|
|
346
|
+
if [ "$sev" = "LOW" ] && psa_low_allowlisted "$_psa_path"; then continue; fi
|
|
347
|
+
if psa_pair_allowlisted "$_psa_path" "$tok"; then continue; fi
|
|
348
|
+
# 🟥 P5: zsh 의 `echo` 는 백슬래시를 해석한다 — 경로 `dir\two` 가 탭으로 변해
|
|
349
|
+
# 증거 문자열이 손상됐다(실측). fail-open 은 아니나 증거는 증거여야 한다.
|
|
350
|
+
printf ' ❌ %s leak — %s: \x27%s\x27\n' "$sev" "${_psa_path}" "$tok"
|
|
230
351
|
hit=1
|
|
231
352
|
done <<PSA_TOK
|
|
232
|
-
$
|
|
353
|
+
$_tok_hits
|
|
233
354
|
PSA_TOK
|
|
234
355
|
done <<PSA_LINES
|
|
235
|
-
$
|
|
356
|
+
$_line_hits
|
|
236
357
|
PSA_LINES
|
|
237
358
|
done <<PSA_PAT
|
|
238
359
|
$PSA_STREAM
|
|
@@ -285,15 +406,42 @@ _psa_can_assign() {
|
|
|
285
406
|
# the caller's shell before the restore line. Status capture is now inside an `if`.
|
|
286
407
|
psa_require_live() {
|
|
287
408
|
local saved_stream saved_allow saved_stream_set saved_allow_set tmpd out rc _canary
|
|
409
|
+
local _nonce _tag_rc
|
|
288
410
|
saved_stream="${PSA_STREAM-}"; saved_allow="${PSA_ALLOWLIST-}"
|
|
289
411
|
saved_stream_set=""; saved_allow_set=""
|
|
290
412
|
[ "${PSA_STREAM+x}" = "x" ] && saved_stream_set=1
|
|
291
413
|
[ "${PSA_ALLOWLIST+x}" = "x" ] && saved_allow_set=1
|
|
292
414
|
tmpd=$(mktemp -d 2>/dev/null) || { echo " ❌ INSTRUMENT DEAD — mktemp unavailable." >&2; return 1; }
|
|
415
|
+
# 🟥 P1 (cross-family 3라운드): 카나리아가 **고정 문자열**이면, 입력을 읽지도 않고
|
|
416
|
+
# 그 문자열을 rc=0 으로 뱉는 위조 생산자가 자가검사를 통과한다. 세 라운드가 공유한
|
|
417
|
+
# 얼굴 —「상태(rc=0)를 의미(생산이 유효하다)로 확장」— 의 마지막 잔재다.
|
|
418
|
+
# 실행마다 새 nonce 를 만들면 **하드코딩된 위조는 그 값을 알 수 없다.**
|
|
419
|
+
_nonce=$(head -c 12 /dev/urandom 2>/dev/null | od -An -tx1 2>/dev/null | tr -d ' \n')
|
|
420
|
+
# 🟥 **신뢰 경계 — 여기서 라운드를 멈추는 이유(2026-08-21, 6라운드 후 판정).**
|
|
421
|
+
# 이 자가검사가 막는 것: 생산자가 **깨졌거나**(awk 부재·crash·출력 훼손), **PATH 로
|
|
422
|
+
# 가려졌거나**(외부 바이너리 shadowing), 입력을 **안 읽고** 고정/유도값을 뱉는 경우.
|
|
423
|
+
# 이 자가검사가 **못** 막는 것: 이 라이브러리를 **소싱한 셸 안에서 함수를 정의할 수 있는**
|
|
424
|
+
# 상대. 그 상대는 `psa_scan_tagged(){ return 0; }` 한 줄이면 끝이라 nonce·cmp·심링크를
|
|
425
|
+
# 아무리 조여도 소용이 없다 — **이미 신뢰 안쪽**이다. 6라운드 반례 둘이 정확히 그 부류였고
|
|
426
|
+
# (`cmp(){ return 0; }` · `ln -sf` 로 tagged 바꿔치기), 값싼 두 수리(`command cmp` ·
|
|
427
|
+
# 심링크 거부)는 했지만 **그 축을 닫았다고 주장하지 않는다.**
|
|
428
|
+
# 🟥 R5-1: **fallback 을 없앴다.** 5라운드 실측 —
|
|
429
|
+
# `${tmpd##*/}` 파생 → 생산자가 경로를 인자로 받으므로 유도 가능 (4라운드에서 교체)
|
|
430
|
+
# `${RANDOM}${RANDOM}$$${SECONDS}` → **생산자가 같은 셸에서 그대로 재계산해 통과**(zsh 실측 rc=0),
|
|
431
|
+
# `RANDOM`/`SECONDS` 가 unset 이면 사실상 `$$` 하나라 `ps` 로도 읽힌다(bash 실측 rc=0)
|
|
432
|
+
# ⇒ 「예측 불가한 값」은 **커널 엔트로피에서만** 나온다. 못 얻으면 그건 약한 nonce 가 아니라
|
|
433
|
+
# **미측정**이다. 두 번째 출처(openssl)까지 시도하고, 그것도 없으면 fail-closed.
|
|
434
|
+
[ -n "$_nonce" ] || _nonce=$(openssl rand -hex 12 2>/dev/null | tr -cd 'A-Za-z0-9')
|
|
435
|
+
if [ -z "$_nonce" ]; then
|
|
436
|
+
echo " ❌ INSTRUMENT DEAD — no kernel entropy source (/dev/urandom, openssl); cannot mint an unguessable nonce." >&2
|
|
437
|
+
echo " A guessable nonce is not a weaker probe, it is NO probe — a producer can replay it." >&2
|
|
438
|
+
rm -rf "$tmpd" 2>/dev/null || :
|
|
439
|
+
return 1
|
|
440
|
+
fi
|
|
293
441
|
# Round 4 refuted the round-3 fix: `|| :` does NOT rescue an assignment to a readonly variable —
|
|
294
442
|
# bash aborts the shell before the `||` is ever considered. So the only safe move is to REFUSE
|
|
295
443
|
# before mutating anything. Detected, reported as DEAD, and the caller survives.
|
|
296
|
-
_canary=$(printf 'HIGH\
|
|
444
|
+
_canary=$(printf 'HIGH\tPSA_SELFTEST_CANARY_%s' "$_nonce")
|
|
297
445
|
if ! _psa_can_assign PSA_STREAM "$_canary" || ! _psa_can_assign PSA_ALLOWLIST /dev/null; then
|
|
298
446
|
echo " ❌ INSTRUMENT DEAD — PSA_STREAM/PSA_ALLOWLIST cannot take the values the self-test needs." >&2
|
|
299
447
|
echo " (readonly, or an attribute such as \`declare -i\` that rejects the value)" >&2
|
|
@@ -303,9 +451,47 @@ psa_require_live() {
|
|
|
303
451
|
fi
|
|
304
452
|
PSA_STREAM="$_canary"
|
|
305
453
|
PSA_ALLOWLIST=/dev/null
|
|
306
|
-
printf '
|
|
307
|
-
#
|
|
308
|
-
|
|
454
|
+
printf 'PSA_SELFTEST_CANARY_%s\n' "$_nonce" > "$tmpd/canary" 2>/dev/null
|
|
455
|
+
# 🟥 생산자와 매처를 **분리해서** 상태를 각각 본다 (cross-family 반례 4, 2026-08-21).
|
|
456
|
+
# 초판은 `awk … | psa_scan_tagged` 한 파이프의 최종 rc 만 봤는데, 그러면
|
|
457
|
+
# **«생산자 실패의 rc=1»** 과 **«스캐너가 유출을 찾음의 rc=1»** 이 같은 값으로 합쳐진다.
|
|
458
|
+
# 실측 위조: 실패하는 `awk` 가 카나리아 행을 인쇄하고 `return 1` 하게 만들면
|
|
459
|
+
# 양 셸에서 `psa_require_live` 가 **rc=0(살아있음)** 을 냈다. 즉 자가검사 판정 자체가
|
|
460
|
+
# 위조 가능했다. 태깅 결과를 먼저 **물질화**하고 그 rc 를 따로 확인한다.
|
|
461
|
+
# 🟥 P3: `x=$(cmd); rc=$?` 는 `set -e` 아래서 **셸을 즉시 죽인다** — 아래 복원문과
|
|
462
|
+
# `rm -rf` 에 도달하지 못하고 PSA_STREAM 이 **카나리아인 채로 남는다**(실측, bash).
|
|
463
|
+
# 조건문 안의 대입은 errexit 면제다.
|
|
464
|
+
# 🟥 R4-3: `$(...)` 는 **후행 개행을 지운다** — 정답 줄 뒤에 빈 줄을 아무리 붙여도
|
|
465
|
+
# 문자열 비교가 통과했다(실측 rc=0). 「정확한 문자열 대조」라고 적어놓고 정확하지 않았다.
|
|
466
|
+
# 생산자 출력을 **파일로 물질화**해서 `cmp` 로 바이트 비교한다. stderr 는 따로 받는다.
|
|
467
|
+
if awk -v P="psa/selftest" '{print P "\t" $0}' "$tmpd/canary" > "$tmpd/tagged" 2>"$tmpd/err"; then _tag_rc=0; else _tag_rc=$?; fi
|
|
468
|
+
# 🟥 P1: 상태(rc)만이 아니라 **내용**을 대조한다. 생산자가 «입력을 읽어서 태깅했는가» 는
|
|
469
|
+
# rc 로 알 수 없고, nonce 를 포함한 정확한 문자열 일치로만 알 수 있다.
|
|
470
|
+
# 🟥 R5-2: 초판은 기대값도 `$tmpd/expect` 파일에 뒀는데, **생산자는 `$tmpd/canary` 를 인자로
|
|
471
|
+
# 받으므로 `$tmpd` 를 알고 expect 를 덮어쓸 수 있다** — 틀린 바이트를 양쪽에 써넣으면
|
|
472
|
+
# `cmp` 가 통과했다(양 셸 실측 rc=0). 기대값은 **프로세스 안에만** 두고 파이프로 먹인다.
|
|
473
|
+
# (`cmp file -` 는 stdin 을 두 번째 피연산자로 읽는다. cmp 부재는 rc≠0 → fail-closed, 실측.)
|
|
474
|
+
# 🟥 R6-1: `cmp` 를 셸이 해석하면 **함수로 가려진다** — `cmp(){ return 0; }` 하나로 대조가
|
|
475
|
+
# 통째로 무력화됐다(양 셸 실측 rc=0). `command` 는 함수를 건너뛴다.
|
|
476
|
+
# 🟥 R6-2: 생산자는 `$tmpd` 를 알므로 stdout 에는 틀린 바이트를 쓰고 `tagged` 를 **정답 파일로
|
|
477
|
+
# 가는 심링크로 바꿔치기**할 수 있다(양 셸 실측 rc=0). 비교 대상이 «awk 가 실제로 쓴 파일»
|
|
478
|
+
# 인지 먼저 확인한다 — 심링크가 아닌 보통 파일이어야 한다.
|
|
479
|
+
if [ -L "$tmpd/tagged" ] || [ ! -f "$tmpd/tagged" ]; then
|
|
480
|
+
rm -rf "$tmpd" 2>/dev/null || :
|
|
481
|
+
if [ -n "${saved_stream_set:-}" ]; then PSA_STREAM="$saved_stream"; else unset PSA_STREAM; fi
|
|
482
|
+
if [ -n "${saved_allow_set:-}" ]; then PSA_ALLOWLIST="$saved_allow"; else unset PSA_ALLOWLIST; fi
|
|
483
|
+
echo " ❌ INSTRUMENT DEAD — the self-test output was replaced (symlink or non-regular file)." >&2
|
|
484
|
+
return 1
|
|
485
|
+
fi
|
|
486
|
+
if [ "$_tag_rc" -ne 0 ] || ! printf 'psa/selftest\tPSA_SELFTEST_CANARY_%s\n' "$_nonce" | command cmp -s "$tmpd/tagged" -; then
|
|
487
|
+
rm -rf "$tmpd" 2>/dev/null || :
|
|
488
|
+
if [ -n "${saved_stream_set:-}" ]; then PSA_STREAM="$saved_stream"; else unset PSA_STREAM; fi
|
|
489
|
+
if [ -n "${saved_allow_set:-}" ]; then PSA_ALLOWLIST="$saved_allow"; else unset PSA_ALLOWLIST; fi
|
|
490
|
+
echo " ❌ INSTRUMENT DEAD — the self-test PRODUCER did not tag this run's nonce (rc=$_tag_rc)." >&2
|
|
491
|
+
echo " A canary printed by a failing — or input-ignoring — producer is not evidence the matcher ran." >&2
|
|
492
|
+
return 1
|
|
493
|
+
fi
|
|
494
|
+
if out=$(psa_scan_tagged < "$tmpd/tagged" 2>&1); then
|
|
309
495
|
rc=0
|
|
310
496
|
else
|
|
311
497
|
rc=$?
|
|
@@ -317,7 +503,7 @@ psa_require_live() {
|
|
|
317
503
|
if [ -n "${saved_stream_set:-}" ]; then PSA_STREAM="$saved_stream"; else unset PSA_STREAM; fi
|
|
318
504
|
if [ -n "${saved_allow_set:-}" ]; then PSA_ALLOWLIST="$saved_allow"; else unset PSA_ALLOWLIST; fi
|
|
319
505
|
case "$out" in
|
|
320
|
-
*
|
|
506
|
+
*"PSA_SELFTEST_CANARY_$_nonce"*) [ "$rc" -eq 1 ] && return 0 ;;
|
|
321
507
|
esac
|
|
322
508
|
echo " ❌ INSTRUMENT DEAD — the scanner did not report a synthetic known-positive." >&2
|
|
323
509
|
echo " self-test rc=$rc out=[$out]" >&2
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# residency_admission_check.sh — 상주 텍스트 **유입** 게이트.
|
|
3
|
+
#
|
|
4
|
+
# 왜 있나 (2026-08-20 실측). 감량은 유출 문제가 아니라 유입 문제였다:
|
|
5
|
+
# CLAUDE.md 23,839(05-26) → 70,758(07-20 «11.9% 감량» 직후) → 144,138(08-20)
|
|
6
|
+
# 07-20 이후 31일간 +73,380 = 약 2,367 bytes/일 (최근 5일은 약 7,000/일, 가속)
|
|
7
|
+
# 같은 기간 어블레이션 CUT 판정 = **0건** (4개 절 시도 → KEEP 3 · UNCALIBRATED 1)
|
|
8
|
+
# 즉 배수구를 넓히는 쪽엔 레버가 거의 없고, 수도꼭지는 아무도 안 보고 있었다.
|
|
9
|
+
# 가장 큰 감량 후보 절(22k)조차 **유입 9일치**다 — 완벽히 잘라도 한 달이면 원위치이고,
|
|
10
|
+
# 그건 가설이 아니라 07-20 에 실제로 일어난 일이다.
|
|
11
|
+
#
|
|
12
|
+
# 무엇을 검사하나 — 🟥 **결론이 아니라 기록의 속성**이다 (§Mechanization Boundary).
|
|
13
|
+
# 「이 문단이 상주할 가치가 있나」는 판정하지 않는다. 「저자가 유형을 선언했나」만 본다.
|
|
14
|
+
# `crossfamily:` · `standpoint:` 와 같은 형태이고 같은 마커에 얹힌다.
|
|
15
|
+
#
|
|
16
|
+
# 🟥 명시 잔여 — 이 게이트가 잡는 것은 **침묵**이지 **거짓 선언**이 아니다.
|
|
17
|
+
# `detail-belongs-elsewhere` 는 자기 거절 값이라 압박받는 세션은 절대 고르지 않는다.
|
|
18
|
+
# 형식은 채널이고 진위는 사람이다. 위 두 필드와 같은 이유로 받아들인다.
|
|
19
|
+
# 보완: 순증마다 **총량과 베이스라인 대비 델타를 출력**한다 — 그건 자기신고가 아니라
|
|
20
|
+
# 측정이라 게임이 안 되고, 추가하는 그 순간에 보인다.
|
|
21
|
+
#
|
|
22
|
+
# ── 🔒 사전등록 (이 게이트가 값을 냈는지 나중에 채점하기 위해, 배선 **전에** 적는다) ──
|
|
23
|
+
# 가설: 이 게이트는 상주 유입을 줄인다.
|
|
24
|
+
# 채점: 배선일로부터 30일간 CLAUDE.md 순증 bytes/day 를 직전 31일(2026-07-20~08-20)의
|
|
25
|
+
# **2,367 bytes/day** 와 대조한다. 베이스라인 144,138 @ 2026-08-20.
|
|
26
|
+
# 🟥 사전등록 없이 나중에 «줄어든 것 같다» 로 채점하면 07-20 의 「11.9% 감량」과 같은 종류의
|
|
27
|
+
# 숫자가 하나 더 생긴다 — 그 숫자는 한 달 뒤 2.1배로 반증됐다.
|
|
28
|
+
# 🟥 **정직한 기대값은 «유입 차단» 이 아니라 «유입에 이름이 붙는다» 이다** (peer 재프레이밍).
|
|
29
|
+
# 형식 검사는 침묵을 잡지 위조를 못 잡고, 저자는 압박받으면 `soul` 을 고른다
|
|
30
|
+
# (`crossfamily:` 가 이미 거짓 값이 레인을 통과한 실측을 갖고 있다).
|
|
31
|
+
# 이름이 붙는 것만으로도 값이 있다 — **다음 감량이 무엇을 지울지 고를 수 있게 된다.**
|
|
32
|
+
#
|
|
33
|
+
# ── 손 캘리브레이션 (enum 이 실물에서 실제로 가르는가 · 2026-08-20) ──
|
|
34
|
+
# 게이트는 기록을 보지 내용을 판정하지 않으므로, enum 의 판별력은 **손으로** 잰다.
|
|
35
|
+
# 최근 5일 최대 유입 둘에 적용했더니 **서로 다른 답**이 나왔다:
|
|
36
|
+
# §Mechanization Boundary (10,157 B) → `soul` — 「기계는 비가역 경계에만, 판단은
|
|
37
|
+
# 진화에」는 판단 좌표계다. 상주 정당
|
|
38
|
+
# §Expedition (5,748 B) → `relocatable(knowledge/shared/rules/operations.md)`
|
|
39
|
+
# — 캐던스 절차 + 실측 이력. §Cadence Rules 가 이미 거기 있다
|
|
40
|
+
# ⇒ 실물 두 건이 갈렸다. 같은 답만 나왔으면 이 enum 은 판별력이 0 이다.
|
|
41
|
+
#
|
|
42
|
+
# exit 0 = 통과(감량·무변경 포함) · 1 = 차단 · 2 = 하네스 오류(PASS 아님)
|
|
43
|
+
|
|
44
|
+
set -uo pipefail
|
|
45
|
+
|
|
46
|
+
RESIDENT_FILE="CLAUDE.md"
|
|
47
|
+
BASELINE_BYTES=144138
|
|
48
|
+
BASELINE_DATE="2026-08-20"
|
|
49
|
+
# 🟥 사람이 읽는 출력은 enum 을 뱉지 않는다. 기계 어휘는 기계 안에만 둔다 —
|
|
50
|
+
# 사용자가 우리 용어를 배우게 만들면 그 게이트는 이식 후 통과 불가가 된다
|
|
51
|
+
# (`count_check.sh:94-106` 이 같은 법칙을 README 렌더링에서 이미 실측했다).
|
|
52
|
+
VALID="soul irreversible-gate disambiguator pointer relocatable"
|
|
53
|
+
|
|
54
|
+
# 🟥 루트는 **스크립트 위치가 아니라 검사 대상 레포**다. 초판은 `dirname $0/..` 를 썼는데,
|
|
55
|
+
# 소비자 설치에서 그건 `node_modules/@chrono-meta/fh-gate` 라 **소비자 레포의 CLAUDE.md 가
|
|
56
|
+
# 아니라 패키지 안의 우리 CLAUDE.md** 를 검사한다. 정적으로 읽을 땐 안 보였고,
|
|
57
|
+
# 팩을 풀어 돌려서 나왔다(§Local Execution First — 실행이 하중선인 이유).
|
|
58
|
+
# 호출자(pre-commit)가 $1 로 레포 루트를 준다. 없으면 git 에게 묻는다.
|
|
59
|
+
ROOT="${1:-}"
|
|
60
|
+
if [ -z "$ROOT" ]; then
|
|
61
|
+
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || ROOT=""
|
|
62
|
+
fi
|
|
63
|
+
# ── 적용 가능성 판정 (🟥 «구조적으로 해당 없음» 과 «도구가 없음» 을 같은 값으로 접지 않는다) ──
|
|
64
|
+
if [ -z "$ROOT" ]; then
|
|
65
|
+
echo " SKIP 상주 유입 게이트 — git 레포가 아니다 (적용 대상 없음, 통과가 아니라 해당 없음)"
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
if [ ! -f "$ROOT/$RESIDENT_FILE" ]; then
|
|
69
|
+
echo " SKIP 상주 유입 게이트 — 이 레포에 $RESIDENT_FILE 이 없다 (적용 대상 없음)"
|
|
70
|
+
exit 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# ── 순증 계산: 스테이징된 변경만 본다 ────────────────────────────────────────
|
|
74
|
+
diffstat=$(git -C "$ROOT" diff --cached --numstat -- "$RESIDENT_FILE" 2>/dev/null)
|
|
75
|
+
rc=$?
|
|
76
|
+
[ "$rc" -ne 0 ] && { echo "❌ HARNESS ERROR: git diff 실패 (rc=$rc) — 미측정이지 통과 아님" >&2; exit 2; }
|
|
77
|
+
[ -z "$diffstat" ] && exit 0 # 이 커밋은 상주를 안 건드린다
|
|
78
|
+
|
|
79
|
+
ins=$(printf '%s' "$diffstat" | awk '{print $1}')
|
|
80
|
+
del=$(printf '%s' "$diffstat" | awk '{print $2}')
|
|
81
|
+
case "$ins$del" in *-*) echo "ℹ️ $RESIDENT_FILE 이 바이너리로 보고됨 — 판정 불가"; exit 2 ;; esac
|
|
82
|
+
net=$(( ins - del ))
|
|
83
|
+
|
|
84
|
+
cur=$(wc -c < "$ROOT/$RESIDENT_FILE" | tr -d ' ')
|
|
85
|
+
# 🟥 베이스라인은 **FH 자신의** CLAUDE.md 기준이다. 다른 레포에서 델타를 찍으면 남의 파일을
|
|
86
|
+
# 우리 숫자로 재는 것이라 무의미하다 — 소비 레포에선 총량만 말한다.
|
|
87
|
+
if [ "$cur" -lt $(( BASELINE_BYTES / 4 )) ] || [ "$cur" -gt $(( BASELINE_BYTES * 4 )) ]; then
|
|
88
|
+
printf ' 상주 규모: %s bytes (이 레포 기준 · FH 베이스라인과 스케일이 달라 델타는 생략)\n' "$cur"
|
|
89
|
+
BASELINE_BYTES=0
|
|
90
|
+
fi
|
|
91
|
+
delta=$(( cur - BASELINE_BYTES ))
|
|
92
|
+
if [ "$BASELINE_BYTES" -gt 0 ]; then pct=$(( delta * 1000 / BASELINE_BYTES )); else pct=0; fi
|
|
93
|
+
if [ "$BASELINE_BYTES" -gt 0 ]; then
|
|
94
|
+
printf ' 상주 규모: %s bytes (기준 %s @ %s · %+d, %+d.%d%%)\n' \
|
|
95
|
+
"$cur" "$BASELINE_BYTES" "$BASELINE_DATE" "$delta" "$((pct/10))" "$((pct%10<0?-pct%10:pct%10))"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
if [ "$net" -le 0 ]; then
|
|
99
|
+
echo " ✅ 상주 순증 없음 (+$ins/-$del 줄) — 유입 게이트 해당 없음"
|
|
100
|
+
exit 0
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# ── 순증이다. 마커에 유형 선언이 있어야 한다 ─────────────────────────────────
|
|
104
|
+
# 🟥 단, **이 레포가 4축 마커 체제를 돌릴 때만** 그렇다. 마커 디렉터리 자체가 없으면
|
|
105
|
+
# 그건 소비 레포이고, 거기서 마커를 요구하면 **이식 이후 한 번도 통과 못 하는 검사**가 된다
|
|
106
|
+
# — 통과 불가능한 게이트는 게이트가 아니라 `--no-verify` 훈련기다
|
|
107
|
+
# (`count_check.sh:94-106` 이 README 렌더링에서 이미 실측한 법칙).
|
|
108
|
+
# 소비자 트리 ARM 3 실행이 이걸 냈다: 소비자가 자기 CLAUDE.md 를 늘리자 rc=2 로 막혔다.
|
|
109
|
+
if [ ! -d "$ROOT/tracks/_meta" ]; then
|
|
110
|
+
echo " SKIP 상주 유입 게이트 — 이 레포는 4축 마커 체제를 안 돌린다 (적용 대상 없음)"
|
|
111
|
+
echo " (순증 +$net 줄은 봤다. 판정하지 않는 이유는 마커 디렉터리 부재다)"
|
|
112
|
+
exit 0
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
today=$(date +%Y-%m-%d)
|
|
116
|
+
marker=$(ls -t "$ROOT"/tracks/_meta/.axes_23_passed_*_"$today".marker 2>/dev/null | head -1)
|
|
117
|
+
if [ -z "$marker" ]; then
|
|
118
|
+
cat >&2 <<MSG
|
|
119
|
+
❌ 상주 순증(+$net 줄)인데 오늘자 마커가 없다.
|
|
120
|
+
이 검사는 마커 위에 얹힌다 — 4축 마커를 먼저 쓴 다음 다시 커밋해라.
|
|
121
|
+
MSG
|
|
122
|
+
exit 2
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
line=$(grep -E '^residency:' "$marker" | head -1)
|
|
126
|
+
if [ -z "$line" ]; then
|
|
127
|
+
cat >&2 <<MSG
|
|
128
|
+
❌ $RESIDENT_FILE 이 +$net 줄 늘었는데, 그게 왜 **항상 로드되어야 하는지**가 안 적혀 있다.
|
|
129
|
+
|
|
130
|
+
늘어난 내용이 다음 중 무엇인가? 마커에 한 줄 적어라 — 근거를 같이.
|
|
131
|
+
· 판단 회로다 (무엇이 성공 · 어디로 기움 · 절대 안 함) → residency: soul — <근거>
|
|
132
|
+
· 비가역 경계에서 발화해야 한다 (출하·삭제·이력 재작성) → residency: irreversible-gate — <근거>
|
|
133
|
+
· 특정 오독 하나를 막는 줄이다 → residency: disambiguator — <무엇을 막나>
|
|
134
|
+
· 상세로 보내는 포인터다 → residency: pointer — <어디로>
|
|
135
|
+
· 🟥 사실 다른 파일로 갈 수 있는 내용이다 → residency: relocatable(<경로>)
|
|
136
|
+
(목적지를 적으면 이 커밋은 막힌다 — 거기로 옮기고 여기엔 포인터만 남겨라)
|
|
137
|
+
|
|
138
|
+
왜 묻나: 07-20 감량 이후 31일간 +73,380 bytes (약 2,367/일). 같은 기간 어블레이션이
|
|
139
|
+
낸 CUT 판정은 0건이다. 나가는 문은 사실상 닫혀 있으므로 들어오는 문에서 정한다.
|
|
140
|
+
MSG
|
|
141
|
+
exit 1
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# 🟥 구분자는 «em-dash» 또는 «공백-하이픈-공백» 이다. 맨하이픈으로 자르면
|
|
145
|
+
# `detail-belongs-elsewhere` 가 `detail` 로 잘린다 — 되돌림 프로브가 이걸 잡았다
|
|
146
|
+
# (그 전까지 그 레인은 «못 알아듣는 값» 으로 **우연히** 초록이었다).
|
|
147
|
+
val=$(printf '%s' "$line" | sed -E 's/^residency:[[:space:]]*//; s/[[:space:]]*—.*$//; s/[[:space:]]+-[[:space:]].*$//' | tr -d ' ')
|
|
148
|
+
grounds=$(printf '%s' "$line" | sed -E 's/^residency:[^—]*—[[:space:]]*//; s/^residency:[^-]*[[:space:]]-[[:space:]]//')
|
|
149
|
+
|
|
150
|
+
# 🟥 `relocatable(<경로>)` 는 **매개변수를 갖는다** — 정확 일치로만 보면 정상 사용이
|
|
151
|
+
# «못 알아듣는 값» 으로 막히고 자기 브랜치는 죽은 코드가 된다(되돌림 프로브가 지목).
|
|
152
|
+
ok=0; for v in $VALID; do
|
|
153
|
+
case "$val" in "$v") ok=1 ;; "$v"'('*')') ok=1 ;; esac
|
|
154
|
+
done
|
|
155
|
+
if [ "$ok" -ne 1 ]; then
|
|
156
|
+
echo "❌ 마커의 residency 값 '$val' 을 못 알아듣는다. 위 다섯 중 하나로 적어라." >&2
|
|
157
|
+
exit 1
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
case "$val" in
|
|
161
|
+
relocatable*)
|
|
162
|
+
dest=$(printf '%s' "$val" | sed -E 's/^relocatable\(?//; s/\)$//')
|
|
163
|
+
if [ -z "$dest" ] || [ "$dest" = "relocatable" ]; then
|
|
164
|
+
echo "❌ 옮길 곳을 안 적었다. 어느 파일로 가야 하나 — 경로를 괄호 안에 적어라." >&2
|
|
165
|
+
exit 1
|
|
166
|
+
fi
|
|
167
|
+
cat >&2 <<MSG
|
|
168
|
+
🟥 저자가 «이건 $dest 로 갈 수 있다» 고 적었는데 상주가 +$net 줄 늘었다. 둘이 안 맞는다.
|
|
169
|
+
$dest 로 옮기고 $RESIDENT_FILE 엔 **명령형 포인터 한 줄**만 남겨라.
|
|
170
|
+
MSG
|
|
171
|
+
exit 1 ;;
|
|
172
|
+
esac
|
|
173
|
+
|
|
174
|
+
if [ "${#grounds}" -lt 12 ] || [ "$grounds" = "$line" ]; then
|
|
175
|
+
echo "❌ residency: $val 인데 근거가 비었다. '— ' 뒤에 왜 상주여야 하는지 한 줄." >&2
|
|
176
|
+
exit 1
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# ── soul 흡수구 완충 (advisory · 절대 차단하지 않는다) ──────────────────────
|
|
180
|
+
# peer 지적: 「판단 좌표계」는 저자가 자기 델타에 거의 항상 붙일 수 있는 라벨이라
|
|
181
|
+
# `soul` 이 기본 흡수구가 된다. 🟥 게이트는 마커를 읽지 내용을 안 읽으므로 이건
|
|
182
|
+
# **기계로 판정할 수 없다** — 픽스처를 만들어도 그냥 통과한다.
|
|
183
|
+
# 대신 **재서 보여준다**: 추가된 줄의 «근거 표지»(날짜·실측·n=·반증·런 #) 밀도.
|
|
184
|
+
# 캘리브레이션 실물 2점 (.claude/regression/fixtures/):
|
|
185
|
+
# §Mechanization Boundary → soul 근거표지 3%
|
|
186
|
+
# §Expedition → relocatable 근거표지 16%
|
|
187
|
+
# 🟥 **n=2 다. 판정에 쓰지 않는다** — 중점 10% 를 넘으면 한 줄 묻기만 한다.
|
|
188
|
+
if [ "$val" = "soul" ]; then
|
|
189
|
+
addl=$(git -C "$ROOT" diff --cached -- "$RESIDENT_FILE" | grep -c '^+[^+]' 2>/dev/null)
|
|
190
|
+
provl=$(git -C "$ROOT" diff --cached -- "$RESIDENT_FILE" \
|
|
191
|
+
| grep '^+[^+]' \
|
|
192
|
+
| grep -cE '[0-9]{4}-[0-9]{2}-[0-9]{2}|실측|측정|n=[0-9]|반증|런 #[0-9]|PR #[0-9]' 2>/dev/null)
|
|
193
|
+
if [ "${addl:-0}" -gt 0 ]; then
|
|
194
|
+
d=$(( provl * 100 / addl ))
|
|
195
|
+
if [ "$d" -ge 10 ]; then
|
|
196
|
+
echo " ⚠️ soul 로 선언했는데 추가분의 ${d}% 가 근거 서술이다(날짜·실측·n=)."
|
|
197
|
+
echo " 판단 좌표계가 맞나, 아니면 근거는 signal 로 보내고 결론 한 줄만 남길 수 있나?"
|
|
198
|
+
echo " (참고 2점: 판단 좌표계 3%% · 이관 대상 16%%. n=2 — 판정 아니라 물음이다)"
|
|
199
|
+
fi
|
|
200
|
+
fi
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
echo " ✅ 상주 순증 +$net 줄 · 유형 선언됨 ($val)"
|
|
204
|
+
exit 0
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -416,6 +416,20 @@ else
|
|
|
416
416
|
_absent_subject_verdict "test_adapter_lanes.sh" "scripts/test_adapter_lanes.sh" || fail=1
|
|
417
417
|
fi
|
|
418
418
|
|
|
419
|
+
# track→repo 해석기 단일화 레인 (2026-08-21 harness-doctor F-1).
|
|
420
|
+
# 🟥 배선이 없으면 이 레인은 «만들고 배선 안 함» 이 된다. 그리고 이 수리는 특히 그 위험이 크다 —
|
|
421
|
+
# 세 소비자가 전부 npm 으로 나가므로, 라이브러리가 files[] 나 selfcheck 어느 한쪽에서 빠지면
|
|
422
|
+
# 소비자 install 에서만 조용히 수리 이전 동작으로 강등된다(로컬은 초록).
|
|
423
|
+
if [ ! -f scripts/fh_track_resolve.sh ]; then
|
|
424
|
+
_absent_subject_verdict "test_track_resolve_lanes.sh" "scripts/fh_track_resolve.sh" || fail=1
|
|
425
|
+
elif [ -f scripts/test_track_resolve_lanes.sh ]; then
|
|
426
|
+
if ! bash scripts/test_track_resolve_lanes.sh; then
|
|
427
|
+
fail=1
|
|
428
|
+
fi
|
|
429
|
+
else
|
|
430
|
+
_absent_subject_verdict "test_track_resolve_lanes.sh" "scripts/test_track_resolve_lanes.sh" || fail=1
|
|
431
|
+
fi
|
|
432
|
+
|
|
419
433
|
if [ ! -f scripts/package_coverage_check.sh ]; then
|
|
420
434
|
_absent_subject_verdict "test_package_coverage_lanes.sh" "scripts/package_coverage_check.sh" || fail=1
|
|
421
435
|
elif [ -f scripts/test_package_coverage_lanes.sh ]; then
|
|
@@ -530,6 +544,7 @@ for _pair in \
|
|
|
530
544
|
".github/workflows/regression-guard.yml|scripts/test_regression_guard_ci_lanes.sh" \
|
|
531
545
|
"templates/.git-hooks/pre-commit|scripts/test_marker_standpoint_lanes.sh" \
|
|
532
546
|
"templates/.git-hooks/pre-commit|scripts/test_marker_thirdparty_lanes.sh" \
|
|
547
|
+
"templates/.git-hooks/pre-commit|scripts/test_marker_soul_check_lanes.sh" \
|
|
533
548
|
"scripts/residency_closure_scan.py|scripts/test_residency_closure_lanes.sh" \
|
|
534
549
|
"scripts/reviewer_capability_corpus.tsv|scripts/test_reviewer_capability_conformance.sh" \
|
|
535
550
|
"scripts/field_canon_preload.sh|scripts/test_field_canon_lanes.sh" \
|
|
@@ -703,6 +718,37 @@ else
|
|
|
703
718
|
fail=1
|
|
704
719
|
fi
|
|
705
720
|
|
|
721
|
+
# test_marker_defense_lanes — Wave 1-D 방어줄(`axis2-defense:`)의 known-pair (2026-08-20).
|
|
722
|
+
# 🟥 이 레인이 존재하는 이유 자체가 기록이다: 절차를 흡수해온 sibling 문서가 «훅이 이 줄을 읽는다»·
|
|
723
|
+
# «marker_floor 레인이 핀한다» 고 적었는데 **둘 다 0 히트**였다(컨트롤 crossfamily 21).
|
|
724
|
+
# 산문은 이식됐고 기계는 안 왔다. 그래서 FH 가 짓는다 — 우리가 그 주장을 할 땐 참이도록.
|
|
725
|
+
if [ ! -f scripts/test_marker_defense_lanes.sh ]; then
|
|
726
|
+
echo "FAIL test_marker_defense_lanes.sh: missing — axis2-defense 레인에 앵커 없음"
|
|
727
|
+
fail=1
|
|
728
|
+
elif _out=$(bash scripts/test_marker_defense_lanes.sh < /dev/null 2>&1); then
|
|
729
|
+
echo "PASS test_marker_defense_lanes.sh ($(printf '%s\n' "$_out" | grep -oE '[0-9]+ lanes hold' | tail -1))"
|
|
730
|
+
else
|
|
731
|
+
echo "FAIL test_marker_defense_lanes.sh: 방어줄 검증이 바뀌었다"
|
|
732
|
+
_show_failure "$_out"
|
|
733
|
+
fail=1
|
|
734
|
+
fi
|
|
735
|
+
|
|
736
|
+
# test_heavy_classifier_lanes — 4축 게이트의 **라우팅 판별자**에 대한 known-pair (2026-08-20).
|
|
737
|
+
# 🟥 이 분류기가 놓치는 경로는 FAIL 하지 않는다 — 게이트가 **조용히 적용되지 않고** 커밋이
|
|
738
|
+
# 초록으로 나간다. 가장 조용한 실패 형태이고, 실측상 이걸 시험하는 레인이 **0개**였다
|
|
739
|
+
# (컨트롤: 훅을 언급하는 test_*.sh 는 12개). sibling harness 에서 흡수했고, 핵심은
|
|
740
|
+
# 픽스처 목록이 아니라 **훅에서 정규식을 실시간 추출**하는 설계다(픽스처가 대상에서 안 떨어진다).
|
|
741
|
+
if [ ! -f scripts/test_heavy_classifier_lanes.sh ]; then
|
|
742
|
+
echo "FAIL test_heavy_classifier_lanes.sh: missing — 게이트 라우팅 판별자에 앵커 없음"
|
|
743
|
+
fail=1
|
|
744
|
+
elif _out=$(bash scripts/test_heavy_classifier_lanes.sh < /dev/null 2>&1); then
|
|
745
|
+
echo "PASS test_heavy_classifier_lanes.sh (4-way routing known-pair + fail-closed extraction)"
|
|
746
|
+
else
|
|
747
|
+
echo "FAIL test_heavy_classifier_lanes.sh: 게이트 라우팅이 바뀌었다"
|
|
748
|
+
_show_failure "$_out"
|
|
749
|
+
fail=1
|
|
750
|
+
fi
|
|
751
|
+
|
|
706
752
|
# test_satellite_publish_gate_lanes — 위성 공개표면 게이트(2026-08-18, 원정 2차).
|
|
707
753
|
# 🟥 이 배선이 없어서 CI 가 `lane-runner: lane suite(s) with no runner and no declaration` 로
|
|
708
754
|
# 빨갰다. 스위트를 신설하고 아무 데서도 안 돌린 것 — 검사기의 표현대로
|
|
@@ -1018,6 +1064,37 @@ else
|
|
|
1018
1064
|
fail=1
|
|
1019
1065
|
fi
|
|
1020
1066
|
|
|
1067
|
+
# listing_watch — 같은 형태. 🟥 이 블록을 «레인을 짓는 같은 커밋에서» 붙인다: 바로 위 주석이
|
|
1068
|
+
# 기록한 「레인은 지었는데 selfcheck 배선을 안 했다」 재발이 최소 세 번이고, 그 셋 다 CI 나
|
|
1069
|
+
# lane-runner 가 뒤늦게 잡았다. 순서를 바꾸는 것이 유일한 처방이라 여기서 그렇게 한다.
|
|
1070
|
+
if [ ! -f scripts/listing_watch.sh ]; then
|
|
1071
|
+
echo "FAIL scripts/listing_watch.sh is in package.json files[] but absent — a deleted subject, not a skip"
|
|
1072
|
+
fail=1
|
|
1073
|
+
elif [ -f scripts/test_listing_watch_lanes.sh ]; then
|
|
1074
|
+
if ! bash scripts/test_listing_watch_lanes.sh; then
|
|
1075
|
+
fail=1
|
|
1076
|
+
fi
|
|
1077
|
+
else
|
|
1078
|
+
echo "FAIL test_listing_watch_lanes.sh: listing_watch.sh present but its anchor is missing"
|
|
1079
|
+
fail=1
|
|
1080
|
+
fi
|
|
1081
|
+
|
|
1082
|
+
# residency_admission — 같은 형태. 🟥 그리고 **또 같은 얼굴이었다**: 2026-08-20 에 이 레인을
|
|
1083
|
+
# 지으면서 여기 배선을 안 했고, `lane-runner` 가 «돌리는 게 없는 스위트는 산문이다» 로 CI 에서
|
|
1084
|
+
# 잡았다. 아래 target_freeze 주석이 기록한 그 형태의 **최소 세 번째**다 — 즉 이건 개인 부주의가
|
|
1085
|
+
# 아니라 «레인을 짓는 사람이 배선 지점을 안 본다» 는 구조적 순서 문제다.
|
|
1086
|
+
if [ ! -f scripts/residency_admission_check.sh ]; then
|
|
1087
|
+
echo "FAIL scripts/residency_admission_check.sh is wired into pre-commit but absent — a deleted subject, not a skip"
|
|
1088
|
+
fail=1
|
|
1089
|
+
elif [ -f scripts/test_residency_admission_lanes.sh ]; then
|
|
1090
|
+
if ! bash scripts/test_residency_admission_lanes.sh; then
|
|
1091
|
+
fail=1
|
|
1092
|
+
fi
|
|
1093
|
+
else
|
|
1094
|
+
echo "FAIL test_residency_admission_lanes.sh: residency_admission_check.sh present but its anchor is missing"
|
|
1095
|
+
fail=1
|
|
1096
|
+
fi
|
|
1097
|
+
|
|
1021
1098
|
# target_freeze — 같은 형태(shipped subject + 자기 앵커). 🟥 이 블록이 없어서 `lane-runner`
|
|
1022
1099
|
# 검사가 «돌리는 게 없는 스위트는 산문이다» 로 정확히 걸었다. 지은 사람이 자기 레인을 안
|
|
1023
1100
|
# 배선한 것이고, 오늘 이 세션이 남의 코드에서 네 번 잡은 그 얼굴이다.
|
|
@@ -1200,6 +1277,32 @@ else
|
|
|
1200
1277
|
fail=1
|
|
1201
1278
|
fi
|
|
1202
1279
|
|
|
1280
|
+
# prior-art prompt (T2) — 새 메커니즘을 쓰기 직전 «내부·외부·숙고» 를 모델 컨텍스트에 넣는 훅.
|
|
1281
|
+
# 레인의 다수가 **음성**이다: 이 훅의 사활은 막는 것이 아니라 소음이 아닌 것이다.
|
|
1282
|
+
if [ ! -f scripts/prior_art_prompt.sh ]; then
|
|
1283
|
+
_absent_subject_verdict "test_prior_art_prompt_lanes.sh" "scripts/prior_art_prompt.sh" || fail=1
|
|
1284
|
+
elif [ -f scripts/test_prior_art_prompt_lanes.sh ]; then
|
|
1285
|
+
if ! bash scripts/test_prior_art_prompt_lanes.sh; then
|
|
1286
|
+
fail=1
|
|
1287
|
+
fi
|
|
1288
|
+
else
|
|
1289
|
+
echo "FAIL test_prior_art_prompt_lanes.sh: prior_art_prompt.sh present but its anchor is missing"
|
|
1290
|
+
fail=1
|
|
1291
|
+
fi
|
|
1292
|
+
|
|
1293
|
+
# outbound-query 위생 린트 — 나가는 질의에 내부 토큰이 실렸는지. 레인이 이 가드의 fail-closed
|
|
1294
|
+
# 분기 4개(라이브러리 부재·defaults 부재·override 부재·상태값 오염)에 각각 짝을 갖는다.
|
|
1295
|
+
if [ ! -f scripts/outbound_query_guard.sh ]; then
|
|
1296
|
+
_absent_subject_verdict "test_outbound_query_lanes.sh" "scripts/outbound_query_guard.sh" || fail=1
|
|
1297
|
+
elif [ -f scripts/test_outbound_query_lanes.sh ]; then
|
|
1298
|
+
if ! bash scripts/test_outbound_query_lanes.sh; then
|
|
1299
|
+
fail=1
|
|
1300
|
+
fi
|
|
1301
|
+
else
|
|
1302
|
+
echo "FAIL test_outbound_query_lanes.sh: outbound_query_guard.sh present but its anchor is missing"
|
|
1303
|
+
fail=1
|
|
1304
|
+
fi
|
|
1305
|
+
|
|
1203
1306
|
if [ ! -f scripts/pipe_verdict_guard.sh ]; then
|
|
1204
1307
|
_absent_subject_verdict "test_pipe_verdict_guard_lanes.sh" "scripts/pipe_verdict_guard.sh" || fail=1
|
|
1205
1308
|
elif [ -f scripts/test_pipe_verdict_guard_lanes.sh ]; then
|