@chrono-meta/fh-gate 2.6.0 → 2.7.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 +24 -3
- package/CLAUDE.md +39 -12
- package/README.ja.md +32 -8
- package/README.ko.md +32 -8
- package/README.md +112 -15
- package/README.zh.md +28 -8
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +19 -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 +90 -0
- package/knowledge/shared/rules/knowledge_layer_seam.md +1 -1
- package/package.json +13 -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 +111 -0
- package/plugins/fh-meta/agents/persona-innovator.md +170 -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/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/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 +88 -0
- package/scripts/test_adapter_lanes.sh +67 -2
- package/scripts/test_heavy_classifier_lanes.sh +144 -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/templates/.git-hooks/pre-commit +400 -4
- package/templates/.git-hooks/pre-push +17 -2
- package/templates/settings.PriorArt.snippet.json +15 -0
|
@@ -282,7 +282,357 @@ out=$(bash -c "
|
|
|
282
282
|
check "L21 declare -i (attribute rejects the value) → REFUSED, caller survives" 0 "$rc" "REFUSED" "$out"
|
|
283
283
|
case "$out" in *SURVIVED*) ok "L21b caller survives the attribute case" ;; *) bad "L21b caller died on the attribute case" ;; esac
|
|
284
284
|
|
|
285
|
+
# ── Z 레인 : 셸 이식성 (2026-08-21) ──────────────────────────────────────────────────────
|
|
286
|
+
# 🟥 왜 [실측]: psa_scan_tagged 가 `local path` 를 선언했는데 **zsh 에서 `path` 는 `PATH` 와
|
|
287
|
+
# tied 된 특수 배열**이라 그 스코프의 PATH 가 비어 `cat` 부터 죽었다. 이 함수 계약이
|
|
288
|
+
# 「빈 입력 = return 0」이라 **계기 사망이 «깨끗한 스캔»과 바이트 단위로 같은 출력**이 됐다 —
|
|
289
|
+
# known-positive·known-negative 가 zsh 에서 **둘 다 rc=0**.
|
|
290
|
+
# 🟥 이 머신만의 문제가 아니었다: psa_scan_lib.sh 와 public-surface-audit/SKILL_detail.md 가
|
|
291
|
+
# **둘 다 출하되고**(npm pack 산출물로 확인), 그 스킬 본문은 무가드 psa_scan_tagged 직접
|
|
292
|
+
# 호출을 지시하며, macOS 기본 셸은 zsh 다. ⇒ 소비자 스킬 경로가 같은 상태였다.
|
|
293
|
+
# 🟥 `bash -n`/`zsh -n` 은 못 잡는다 — 문법이 아니라 런타임 의미론이다. 실행으로만 성립한다.
|
|
294
|
+
# 🟥 조건부 레인 수를 세어 둔다. 플로어를 상수로 박으면 zsh 없는 러너에서 «INSTRUMENT ERROR»
|
|
295
|
+
# 가 난다 — 실제로 CI(ubuntu-latest, zsh 미설치)에서 그렇게 났다. 스킵은 실패가 아니다.
|
|
296
|
+
_cond_lanes=0
|
|
297
|
+
if command -v zsh >/dev/null 2>&1; then
|
|
298
|
+
_cond_lanes=$((_cond_lanes + 4))
|
|
299
|
+
_ztmp=$(mktemp -d)
|
|
300
|
+
printf 'contains LANECANARY here\n' > "$_ztmp/pos.md"
|
|
301
|
+
printf 'clean prose\n' > "$_ztmp/neg.md"
|
|
302
|
+
_zrun() {
|
|
303
|
+
"$1" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\\tLANECANARY') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1; cat '$2' | psa_scan_tagged '$2' >/dev/null 2>&1; echo \$?"
|
|
304
|
+
}
|
|
305
|
+
_zp=$(_zrun zsh "$_ztmp/pos.md"); _zn=$(_zrun zsh "$_ztmp/neg.md")
|
|
306
|
+
_bp=$(_zrun bash "$_ztmp/pos.md"); _bn=$(_zrun bash "$_ztmp/neg.md")
|
|
307
|
+
[ "$_zp" = "1" ] && ok "Z1 zsh known-positive → 1 (계기가 zsh 에서 살아 있다)" || bad "Z1 zsh 양성이 1 이 아니다: got $_zp"
|
|
308
|
+
[ "$_zn" = "0" ] && ok "Z2 zsh known-negative → 0 (오탐 없음)" || bad "Z2 zsh 음성이 0 이 아니다: got $_zn"
|
|
309
|
+
# ★컨트롤 — 두 팔이 갈리면 이식성 결함 잔존, 두 팔이 같이 죽으면 Z1/Z2 통과가 공허하다
|
|
310
|
+
[ "$_bp" = "$_zp" ] && ok "Z3 ★컨트롤 bash 양성 = zsh 양성 (두 팔 일치, $_bp)" || bad "Z3 두 팔 불일치: bash=$_bp zsh=$_zp"
|
|
311
|
+
[ "$_bn" = "$_zn" ] && ok "Z4 ★컨트롤 bash 음성 = zsh 음성 ($_bn)" || bad "Z4 두 팔 불일치: bash=$_bn zsh=$_zn"
|
|
312
|
+
rm -rf "$_ztmp"
|
|
313
|
+
else
|
|
314
|
+
echo " SKIP Z 레인 (zsh 없음) — 🟥 PASS 가 아니라 미검사다"
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
# ── E 레인 : 계기 사망은 «깨끗함»이 아니라 NOT SCANNED(3) 로 ──────────────────────────────
|
|
318
|
+
# 무가드 직접 호출부(pre-commit ×3 · pre-push · outbound-guard)는 전부 **비영 = 차단**으로
|
|
319
|
+
# 읽으므로 3 은 셋 모두에서 fail-closed 다. 0 이면 셋 다 «깨끗»으로 통과한다.
|
|
320
|
+
# 🟥 **초판 E1 은 두 번 결함이었다** (cross-family + 되돌림 프로브가 각각 하나씩 잡았다):
|
|
321
|
+
# ⓐ 기본 분기가 `ok` 여서 rc=3 이 아닌 «모든» 출력(빈 출력 포함)을 PASS 로 셌다
|
|
322
|
+
# ⓑ 판정을 **종료코드가 아니라 문자열 `*rc=3*`** 로 했는데, 가드가 stdout 에 찍는 문구
|
|
323
|
+
# 자체에 `(rc=3)` 이 들어 있다 → **페이로드가 자기 판정 문자열을 인쇄**한다. 가드의
|
|
324
|
+
# `return 3` 을 `return 0` 으로 사보타주해도 레인이 초록이었다. 앵커가 장식이었다.
|
|
325
|
+
# ⇒ 판정은 **숫자 rc 하나**로만. 메시지는 증거지 판정이 아니다.
|
|
326
|
+
_erc=$(bash -c "
|
|
327
|
+
. '$LIB'
|
|
328
|
+
psa_load '$PWD/.claude/rules/.public-surface-patterns.defaults' '$PWD/.claude/rules/.public-surface-patterns' >/dev/null 2>&1
|
|
329
|
+
PATH=/nonexistent-psa-probe
|
|
330
|
+
printf 'p\tLANECANARY\n' | psa_scan_tagged >/dev/null 2>&1
|
|
331
|
+
echo \$?
|
|
332
|
+
" 2>/dev/null | tail -1)
|
|
333
|
+
[ "$_erc" = "3" ] && ok "E1 계기 사망 → rc=3 NOT SCANNED (숫자로 판정, 0 으로 안 접힌다)" \
|
|
334
|
+
|| bad "E1 계기 사망이 rc=3 이 아니다 — got rc=[$_erc]"
|
|
335
|
+
|
|
336
|
+
# ── E2~E4 : cross-family(codex/gpt-5.6-sol) 가 실행으로 재현한 반례들. 판정 FAIL 이었다.
|
|
337
|
+
# 전부 «자가검사가 한 번 통과했다 → 그 뒤 실제 스캔도 실행됐다» 로 확장한 초판의 맹점이다.
|
|
338
|
+
_erun() { # $1=shell $2=prelude → rc
|
|
339
|
+
"$1" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\\tLANECANARY') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1; $2 printf 'p\tLANECANARY\n' | psa_scan_tagged >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1
|
|
340
|
+
}
|
|
341
|
+
for _sh in bash zsh; do
|
|
342
|
+
command -v "$_sh" >/dev/null 2>&1 || { echo " SKIP E2~E5 ($_sh 없음) — PASS 아님, 미검사다"; continue; }
|
|
343
|
+
[ "$_sh" = "zsh" ] && _cond_lanes=$((_cond_lanes + 4))
|
|
344
|
+
# E2 — `cat` 이 죽으면 빈 입력이 «신고할 것 없음»이 됐다 (반례 1)
|
|
345
|
+
_r=$(_erun "$_sh" 'cat(){ return 7; };')
|
|
346
|
+
[ "$_r" = "3" ] && ok "E2/$_sh cat 사망 → rc=3 (빈 입력이 «깨끗»으로 안 접힌다)" || bad "E2/$_sh cat 사망 rc=[$_r], 3 이어야"
|
|
347
|
+
# E3 — 🟥 외부가 가드 표식을 미리 export 하면 자가검사가 통째로 생략됐다 (반례 1b)
|
|
348
|
+
# 수리: 재진입 판별을 **호출 스택**으로 바꿨다 — 호출자가 미리 심을 수 없다
|
|
349
|
+
_r=$(_erun "$_sh" 'export _PSA_IN_SELFTEST=1; export _PSA_LIVE_OK=1; grep(){ return 127; };')
|
|
350
|
+
[ "$_r" = "3" ] && ok "E3/$_sh 가드 표식 선설정 → 여전히 rc=3 (전역 상태가 권한이 아니다)" || bad "E3/$_sh 선설정으로 우회됨 rc=[$_r]"
|
|
351
|
+
# E4 — 자가검사는 PSA_STREAM 을 카나리아로 바꿔치기하므로 «진짜 패턴이 비었는지»를 못 본다 (반례 3)
|
|
352
|
+
_r=$(_erun "$_sh" "PSA_STREAM='';")
|
|
353
|
+
[ "$_r" = "3" ] && ok "E4/$_sh 빈 PSA_STREAM → rc=3 (자가검사 통과해도 실제 패턴을 본다)" || bad "E4/$_sh 빈 STREAM rc=[$_r], 3 이어야"
|
|
354
|
+
# ★컨트롤 — 위 셋의 통과가 «전부 3 을 내서» 인지 확인. 정상 입력은 1/0 이어야 한다
|
|
355
|
+
_r=$(_erun "$_sh" '')
|
|
356
|
+
[ "$_r" = "1" ] && ok "E5/$_sh ★컨트롤 정상 양성 → rc=1 (E2~E4 가 «항상 3» 이 아니다)" || bad "E5/$_sh 컨트롤 rc=[$_r], 1 이어야"
|
|
357
|
+
done
|
|
358
|
+
|
|
359
|
+
# ── G/F 레인 : cross-family 반례 2·4 (2026-08-21, 2차) ──────────────────────────────────
|
|
360
|
+
# 🟥 둘 다 «자가검사가 통과했으니 그 뒤 스캔도 실행됐다» 는 확장의 잔재다.
|
|
361
|
+
# G = 진짜 패턴이 깨졌을 때. 자가검사는 **자기 카나리아 패턴**을 쓰므로 구조적으로 못 본다.
|
|
362
|
+
# 실측 도달: 패턴 파일에 깨진 정규식 한 줄 → 그 행만 조용히 아무것도 안 잡고 rc=0 «깨끗».
|
|
363
|
+
# F = 자가검사 판정 자체의 위조. 초판은 파이프 최종 rc 하나만 봐서 «생산자 실패의 1» 과
|
|
364
|
+
# «스캐너가 유출을 찾음의 1» 이 합쳐졌다 — 실패하는 awk 가 카나리아를 뱉으면 통과했다.
|
|
365
|
+
for _sh in bash zsh; do
|
|
366
|
+
command -v "$_sh" >/dev/null 2>&1 || { echo " SKIP G/F ($_sh 없음) — PASS 아님, 미검사다"; continue; }
|
|
367
|
+
_cond_lanes=$((_cond_lanes + 0))
|
|
368
|
+
_gp() { "$1" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\\t%s') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1; printf 'p\\t%s\\n' | psa_scan_tagged >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1; }
|
|
369
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\t[unclosed') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1; printf 'p\t[unclosed here\n' | psa_scan_tagged >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
370
|
+
[ "$_r" = "3" ] && ok "G1/$_sh 깨진 정규식 → rc=3 (조용한 미스캔이 «깨끗»으로 안 접힌다)" || bad "G1/$_sh 깨진 정규식 rc=[$_r], 3 이어야"
|
|
371
|
+
# ★컨트롤 — 정상 패턴은 여전히 1/0 을 낸다(G1 이 «항상 3» 이 아니다)
|
|
372
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\tOKTOK') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1; printf 'p\tOKTOK\n' | psa_scan_tagged >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
373
|
+
[ "$_r" = "1" ] && ok "G2/$_sh ★컨트롤 정상 패턴 → rc=1" || bad "G2/$_sh 컨트롤 rc=[$_r], 1 이어야"
|
|
374
|
+
# F — 실패하는 생산자가 카나리아를 뱉어도 «살아있음» 이 아니어야
|
|
375
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\tX') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1; awk(){ printf 'PSA_SELFTEST_CANARY\n'; return 1; }; psa_require_live >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
376
|
+
[ "$_r" != "0" ] && ok "F1/$_sh 실패한 생산자의 카나리아 → 자가검사 «살아있음» 아님(rc=$_r)" || bad "F1/$_sh 자가검사 판정이 위조됐다 rc=0"
|
|
377
|
+
# ★컨트롤 — 정상 환경에서 psa_require_live 는 살아있다고 해야 한다
|
|
378
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\tX') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1; psa_require_live >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
379
|
+
[ "$_r" = "0" ] && ok "F2/$_sh ★컨트롤 정상 자가검사 → rc=0 (F1 이 «항상 실패» 가 아니다)" || bad "F2/$_sh 컨트롤 rc=[$_r], 0 이어야"
|
|
380
|
+
_cond_lanes=$((_cond_lanes + 0))
|
|
381
|
+
[ "$_sh" = "zsh" ] && _cond_lanes=$((_cond_lanes + 4))
|
|
382
|
+
done
|
|
383
|
+
|
|
384
|
+
# ── H 레인 : 훅이 «계기 사망(3)» 과 «유출(1)» 을 가르는가 (2026-08-21) ────────────────────
|
|
385
|
+
# 🟥 왜 [실행 확인]: 초판 훅은 `if ! … | psa_scan_tagged` 로 **비영을 전부 유출로 뭉갰다.**
|
|
386
|
+
# 그래서 `PUBLIC_SURFACE_OK=1` 이 **계기 사망까지 «승인된 유출»로 통과**시켰다 — 비가역
|
|
387
|
+
# 표면에서 가장 나쁜 조합이다. override 는 «알고 있는 언급»을 승인하는 것이지 «안 잰
|
|
388
|
+
# 표면»을 승인하는 게 아니다. 되돌림 실측: 옛 훅 rc=0 · 새 훅 rc=1.
|
|
389
|
+
_HOOK="$PWD/templates/.git-hooks/pre-commit"
|
|
390
|
+
if [ ! -f "$_HOOK" ] || ! command -v git >/dev/null 2>&1; then
|
|
391
|
+
echo " SKIP H 레인 (훅 또는 git 없음) — PASS 아님, 미검사"
|
|
392
|
+
else
|
|
393
|
+
_hrun() { # $1=hook path → rc (계기를 죽인 픽스처에서 PUBLIC_SURFACE_OK=1 로 커밋 시도)
|
|
394
|
+
local T; T=$(mktemp -d); git init -q "$T"
|
|
395
|
+
( cd "$T"
|
|
396
|
+
git config user.email t@t; git config user.name t; git config commit.gpgsign false
|
|
397
|
+
mkdir -p .githooks .claude/rules tracks/_meta scripts
|
|
398
|
+
cp "$1" .githooks/pre-commit; chmod +x .githooks/pre-commit
|
|
399
|
+
git config core.hooksPath .githooks
|
|
400
|
+
cp "$OLDPWD/.claude/rules/.public-surface-patterns.defaults" .claude/rules/ 2>/dev/null
|
|
401
|
+
cp "$OLDPWD/.claude/rules/.public-surface-patterns" .claude/rules/ 2>/dev/null
|
|
402
|
+
cp "$OLDPWD/scripts/psa_scan_lib.sh" scripts/
|
|
403
|
+
# 계기를 죽인다 — 원인은 무엇이든 좋다. 중요한 건 «안 쟀다» 가 «깨끗» 으로 안 읽히는 것
|
|
404
|
+
sed -i.bak 's/^psa_require_live() {/psa_require_live() { return 1/' scripts/psa_scan_lib.sh && rm -f scripts/psa_scan_lib.sh.bak
|
|
405
|
+
printf 'x\n' > seed.md; git add seed.md; git commit -qm seed --no-verify >/dev/null 2>&1
|
|
406
|
+
printf 'just prose\n' > probe.md; git add probe.md
|
|
407
|
+
PUBLIC_SURFACE_OK=1 git commit -qm probe >/dev/null 2>&1; echo $? )
|
|
408
|
+
rm -rf "$T"
|
|
409
|
+
}
|
|
410
|
+
_h=$(_hrun "$_HOOK")
|
|
411
|
+
[ "$_h" = "1" ] && ok "H1 계기 사망 + PUBLIC_SURFACE_OK=1 → **차단**(override 가 미측정을 안 덮는다)" \
|
|
412
|
+
|| bad "H1 계기 사망이 override 로 통과했다 rc=[$_h] — 이 수리가 되돌아갔다"
|
|
413
|
+
# ★컨트롤 — 계기가 살아 있으면 같은 픽스처가 통과해야 한다. 아니면 H1 은 «항상 막힌다» 일 뿐이다
|
|
414
|
+
_hrun_live() {
|
|
415
|
+
local T; T=$(mktemp -d); git init -q "$T"
|
|
416
|
+
( cd "$T"
|
|
417
|
+
git config user.email t@t; git config user.name t; git config commit.gpgsign false
|
|
418
|
+
mkdir -p .githooks .claude/rules tracks/_meta scripts
|
|
419
|
+
cp "$_HOOK" .githooks/pre-commit; chmod +x .githooks/pre-commit
|
|
420
|
+
git config core.hooksPath .githooks
|
|
421
|
+
cp "$OLDPWD/.claude/rules/.public-surface-patterns.defaults" .claude/rules/ 2>/dev/null
|
|
422
|
+
cp "$OLDPWD/.claude/rules/.public-surface-patterns" .claude/rules/ 2>/dev/null
|
|
423
|
+
cp "$OLDPWD/scripts/psa_scan_lib.sh" scripts/
|
|
424
|
+
printf 'x\n' > seed.md; git add seed.md; git commit -qm seed --no-verify >/dev/null 2>&1
|
|
425
|
+
printf 'just prose\n' > probe.md; git add probe.md
|
|
426
|
+
git commit -qm probe >/dev/null 2>&1; echo $? )
|
|
427
|
+
rm -rf "$T"
|
|
428
|
+
}
|
|
429
|
+
_hl=$(_hrun_live)
|
|
430
|
+
[ "$_hl" = "0" ] && ok "H2 ★컨트롤 계기 생존 + 깨끗한 내용 → 통과 (H1 이 «항상 막힘» 이 아니다)" \
|
|
431
|
+
|| bad "H2 컨트롤 실패 rc=[$_hl] — 픽스처가 다른 이유로 막힌다, H1 판정 무효"
|
|
432
|
+
fi
|
|
433
|
+
|
|
434
|
+
# ── N 레인 : cross-family 3라운드 반례 P1~P5 (2026-08-21) ────────────────────────────────
|
|
435
|
+
# 🟥 세 라운드가 같은 얼굴이었다 — **상태를 의미로 확장**. rc=0 을 «유효», «분리했다» 를
|
|
436
|
+
# «생산이 옳다», «자가검사 통과» 를 «그 뒤 스캔도 실행» 으로 읽었다. N 레인은 그 축을 친다.
|
|
437
|
+
for _sh in bash zsh; do
|
|
438
|
+
command -v "$_sh" >/dev/null 2>&1 || { echo " SKIP N1/N2/N4/N5 ($_sh 없음) — PASS 아님, 미검사다"; continue; }
|
|
439
|
+
# N1 — P1: 입력을 읽지도 않고 rc=0 으로 «고정» 카나리아를 뱉는 위조 생산자
|
|
440
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\tX') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1;
|
|
441
|
+
awk(){ printf 'psa/selftest\tPSA_SELFTEST_CANARY\n'; return 0; };
|
|
442
|
+
psa_require_live >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
443
|
+
[ "$_r" != "0" ] && ok "N1/$_sh rc=0 위조 생산자 → «살아있음» 아님 (nonce 불일치, rc=$_r)" \
|
|
444
|
+
|| bad "N1/$_sh 자가검사가 rc=0 위조에 뚫렸다 — P1 이 안 닫혔다"
|
|
445
|
+
# N1b — P1 의 **문자열 대조** 를 따로 친다. N1(고정 카나리아)은 nonce 만으로도 막히므로
|
|
446
|
+
# 대조문이 없어도 초록이었다(되돌림 실측 → 적색 0). 이 레인은 «입력은 읽었는데
|
|
447
|
+
# 태그가 틀린» 생산자를 쓴다 — nonce 는 통과하고 대조문만이 잡는다.
|
|
448
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\tX') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1;
|
|
449
|
+
awk(){ for _a; do :; done; printf 'WRONGPREFIX\t%s\n' \"\$(cat \"\$_a\")\"; return 0; };
|
|
450
|
+
psa_require_live >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
451
|
+
[ "$_r" != "0" ] && ok "N1b/$_sh 입력은 읽되 태그가 틀린 생산자 → «살아있음» 아님 (rc=$_r)" \
|
|
452
|
+
|| bad "N1b/$_sh 태그 대조가 없다 — 생산자가 무엇을 뱉든 통과한다"
|
|
453
|
+
# N2 ★컨트롤 — 정상 환경에서는 여전히 살아있다고 해야 한다 (N1 이 «항상 실패» 가 아니다)
|
|
454
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\tX') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1;
|
|
455
|
+
psa_require_live >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
456
|
+
[ "$_r" = "0" ] && ok "N2/$_sh ★컨트롤 정상 자가검사 → rc=0" || bad "N2/$_sh 컨트롤 rc=[$_r], 0 이어야"
|
|
457
|
+
# N4 — P4: 판별력 없는 패턴(빈·^·a*)은 거부되고 정상 패턴만 남는다
|
|
458
|
+
_T=$(mktemp -d); printf 'HIGH\t\nHIGH\t^\nHIGH\ta*\nHIGH\tREALSECRET\n' > "$_T/def"; : > "$_T/ovr"
|
|
459
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_DEFAULTS_OK=1; psa_load '$_T/def' '$_T/ovr' >/dev/null 2>&1;
|
|
460
|
+
printf '%s/%s\n' \"\$(printf '%s' \"\$PSA_STREAM\" | grep -c .)\" \"\$PSA_BAD_ROWS\"" 2>/dev/null | tail -1)
|
|
461
|
+
[ "$_r" = "1/3" ] && ok "N4a/$_sh 무판별 패턴 3행 거부, 정상 1행 생존 ($_r)" \
|
|
462
|
+
|| bad "N4a/$_sh 유효행/거부행=[$_r], 1/3 이어야 — P4 가 안 닫혔다"
|
|
463
|
+
# N4b ★컨트롤 — 거부가 «전부 버리기» 가 아니다: **깨끗한** 패턴 파일이면 정상 검출한다.
|
|
464
|
+
# 🟥 초판은 위 «혼합» 파일로 rc=1 을 기대했는데, R4-6 이후 그 파일은 bad_rows>0 이라
|
|
465
|
+
# rc=3(부분집합=미측정)이 정답이다. 컨트롤은 깨끗한 파일로 분리한다.
|
|
466
|
+
printf 'HIGH\tREALSECRET\n' > "$_T/clean"
|
|
467
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_DEFAULTS_OK=1; PSA_ALLOWLIST=/dev/null; psa_load '$_T/clean' '$_T/ovr' >/dev/null 2>&1;
|
|
468
|
+
printf 'p\tREALSECRET here\n' | psa_scan_tagged >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
469
|
+
[ "$_r" = "1" ] && ok "N4b/$_sh ★컨트롤 깨끗한 패턴 파일 → 정상 검출 rc=1" \
|
|
470
|
+
|| bad "N4b/$_sh 컨트롤 rc=[$_r], 1 이어야 — 거부가 과했다"
|
|
471
|
+
# N4c — R4-6: **부분 로드된 집합은 «깨끗»도 «유출»도 말할 수 없다.** cross-family 4라운드가
|
|
472
|
+
# `SECRET|^$` 한 행이 떨어진 집합에서 `SECRET` 입력이 rc=0 «깨끗» 을 내는 것을 실측했다.
|
|
473
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_DEFAULTS_OK=1; PSA_ALLOWLIST=/dev/null; psa_load '$_T/def' '$_T/ovr' >/dev/null 2>&1;
|
|
474
|
+
printf 'p\tREALSECRET here\n' | psa_scan_tagged >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
475
|
+
[ "$_r" = "3" ] && ok "N4c/$_sh 행이 떨어진 부분집합 → rc=3 (부분 로드는 미측정)" \
|
|
476
|
+
|| bad "N4c/$_sh 부분집합 rc=[$_r], 3 이어야 — R4-6 이 안 닫혔다"
|
|
477
|
+
rm -rf "$_T"
|
|
478
|
+
# N5 — P5: 백슬래시가 든 경로가 증거에 그대로 남는다 (zsh 의 echo 가 해석하던 자리)
|
|
479
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\tSEEKRET') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1;
|
|
480
|
+
printf 'dir\\\\two\tSEEKRET\n' | psa_scan_tagged 2>&1 | grep -c 'dir\\\\two'" 2>/dev/null | tail -1)
|
|
481
|
+
[ "$_r" = "1" ] && ok "N5/$_sh 백슬래시 경로가 증거에 보존된다" \
|
|
482
|
+
|| bad "N5/$_sh 증거 손상 (일치=[$_r]) — P5 가 안 닫혔다"
|
|
483
|
+
[ "$_sh" = "zsh" ] && _cond_lanes=$((_cond_lanes + 7))
|
|
484
|
+
done
|
|
485
|
+
|
|
486
|
+
# R 레인 : cross-family 4라운드 반례 (2026-08-21, 수리에 대한 재검)
|
|
487
|
+
# 🟥 이 라운드의 교훈은 **「수리가 새 결함의 주된 출처」**다 — P3 를 스캔 경로에서만 고치고
|
|
488
|
+
# 로더 경로로 전파를 안 봤고(R4-1), 「정확한 문자열 대조」가 후행 개행에 뚫렸다(R4-3).
|
|
489
|
+
for _sh in bash zsh; do
|
|
490
|
+
command -v "$_sh" >/dev/null 2>&1 || { echo " SKIP R1/R2/R3 ($_sh 없음) — PASS 아님, 미검사다"; continue; }
|
|
491
|
+
_RT=$(mktemp -d); printf 'HIGH\tREALSECRET\n' > "$_RT/def"; : > "$_RT/ovr"
|
|
492
|
+
# R1 — psa_load 가 `set -e` 호출자를 죽이지 않는다 (정상 패턴은 빈 입력에 rc=1 을 낸다)
|
|
493
|
+
_r=$("$_sh" -c "set -e; . '$LIB'; psa_load '$_RT/def' '$_RT/ovr' >/dev/null 2>&1; echo SURVIVED" 2>/dev/null | tail -1)
|
|
494
|
+
[ "$_r" = "SURVIVED" ] && ok "R1/$_sh set -e 호출자가 psa_load 를 살아서 통과" \
|
|
495
|
+
|| bad "R1/$_sh psa_load 가 errexit 호출자를 죽인다 [$_r] — P3 전파 미완"
|
|
496
|
+
# R2 — fallback nonce 가 생산자 인자에서 유도되지 않는다
|
|
497
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\tX') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1;
|
|
498
|
+
head(){ return 1; };
|
|
499
|
+
awk(){ for _a; do :; done; _d=\${_a%/*}; _d=\${_d##*/}; _d=\$(printf %s \"\$_d\" | tr -cd 'A-Za-z0-9');
|
|
500
|
+
printf 'psa/selftest\tPSA_SELFTEST_CANARY_%s\n' \"\$_d\"; return 0; };
|
|
501
|
+
psa_require_live >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
502
|
+
[ "$_r" != "0" ] && ok "R2/$_sh urandom 부재 fallback 이 경로에서 유도 안 됨 (rc=$_r)" \
|
|
503
|
+
|| bad "R2/$_sh fallback nonce 가 인자에서 유도된다 — 위조 통과"
|
|
504
|
+
# R3 — 정답 줄 + 후행 빈 줄은 «바이트 동일» 이 아니다 ($(...) 가 후행 개행을 지운다)
|
|
505
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\tX') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1;
|
|
506
|
+
awk(){ for _a; do :; done; printf 'psa/selftest\t%s\n\n\n' \"\$(cat \"\$_a\")\"; return 0; };
|
|
507
|
+
psa_require_live >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
508
|
+
[ "$_r" != "0" ] && ok "R3/$_sh 후행 빈 줄이 붙은 산출 → 통과 안 됨 (rc=$_r)" \
|
|
509
|
+
|| bad "R3/$_sh 후행 개행이 대조를 뚫었다 — 「바이트 동일」이 아니다"
|
|
510
|
+
rm -rf "$_RT"
|
|
511
|
+
[ "$_sh" = "zsh" ] && _cond_lanes=$((_cond_lanes + 3))
|
|
512
|
+
done
|
|
513
|
+
|
|
514
|
+
# S 레인 : cross-family 5라운드 반례 (2026-08-21). 🟥 **4라운드 수리 자체가 낸 결함들**이다.
|
|
515
|
+
for _sh in bash zsh; do
|
|
516
|
+
command -v "$_sh" >/dev/null 2>&1 || { echo " SKIP S1/S2 ($_sh 없음) — PASS 아님, 미검사다"; continue; }
|
|
517
|
+
# S1 — 엔트로피 출처가 둘 다 없으면 «약한 nonce» 가 아니라 **미측정**이다.
|
|
518
|
+
# 4라운드 fallback(`$RANDOM$RANDOM$$$SECONDS`)은 생산자가 같은 셸에서 재계산해 통과했다.
|
|
519
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\tX') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1;
|
|
520
|
+
head(){ return 1; }; openssl(){ return 1; };
|
|
521
|
+
awk(){ for _a; do :; done; _n=\$(printf '%s%s%s%s' \"\${RANDOM:-}\" \"\${RANDOM:-}\" \"\$\$\" \"\${SECONDS:-}\" | tr -cd 'A-Za-z0-9');
|
|
522
|
+
printf 'psa/selftest\tPSA_SELFTEST_CANARY_%s\n' \"\$_n\"; return 0; };
|
|
523
|
+
psa_require_live >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
524
|
+
[ "$_r" != "0" ] && ok "S1/$_sh 엔트로피 부재 → 예측가능 nonce 로 통과 안 됨 (rc=$_r)" \
|
|
525
|
+
|| bad "S1/$_sh 생산자가 재계산한 nonce 로 통과했다 — 추측 가능한 nonce 는 nonce 가 아니다"
|
|
526
|
+
# S2 — 생산자는 `$tmpd/canary` 를 인자로 받으므로 `$tmpd` 를 안다. 기대값이 그 안에 있으면 덮인다.
|
|
527
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\tX') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1;
|
|
528
|
+
awk(){ for _a; do :; done; _d=\${_a%/*}; _n=\$(cat \"\$_a\");
|
|
529
|
+
printf 'wrong/path\t%s\n' \"\$_n\" > \"\$_d/expect\";
|
|
530
|
+
printf 'wrong/path\t%s\n' \"\$_n\"; return 0; };
|
|
531
|
+
psa_require_live >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
532
|
+
[ "$_r" != "0" ] && ok "S2/$_sh 생산자가 기대값 파일을 덮어써도 통과 안 됨 (rc=$_r)" \
|
|
533
|
+
|| bad "S2/$_sh 기대값이 생산자 사정권에 있다 — cmp 가 자기 자신과 비교했다"
|
|
534
|
+
# T1 — R6-1: `cmp` 가 함수로 가려져도 대조가 무력화되지 않는다 (`command cmp`)
|
|
535
|
+
_r=$("$_sh" -c ". '$LIB'; export PSA_STREAM=\$(printf 'HIGH\tX') PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1 PSA_BAD_ROWS=0;
|
|
536
|
+
cmp(){ return 0; };
|
|
537
|
+
awk(){ for _a; do :; done; printf 'WRONGPREFIX\t%s\n' \"\$(cat \"\$_a\")\"; return 0; };
|
|
538
|
+
psa_require_live >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
539
|
+
[ "$_r" != "0" ] && ok "T1/$_sh cmp 함수 가림 → 통과 안 됨 (rc=$_r)" \
|
|
540
|
+
|| bad "T1/$_sh 대조가 셸 함수로 무력화됐다 — command 우회가 없다"
|
|
541
|
+
# T2 — R6-2: 생산자가 stdout 은 틀리게 쓰고 tagged 를 정답 파일 심링크로 바꿔치기
|
|
542
|
+
_r=$("$_sh" -c ". '$LIB'; PSA_STREAM=\$(printf 'HIGH\tX'); export PSA_STREAM PSA_ALLOWLIST=/dev/null PSA_DEFAULTS_OK=1 PSA_BAD_ROWS=0;
|
|
543
|
+
awk(){ for _a; do :; done; _d=\${_a%/*}; _n=\$(cat \"\$_a\");
|
|
544
|
+
printf 'WRONGPREFIX\t%s\n' \"\$_n\";
|
|
545
|
+
printf 'psa/selftest\t%s\n' \"\$_n\" > \"\$_d/alt\"; ln -sf alt \"\$_d/tagged\"; return 0; };
|
|
546
|
+
psa_require_live >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
547
|
+
[ "$_r" != "0" ] && ok "T2/$_sh tagged 심링크 스왑 → 통과 안 됨 (rc=$_r)" \
|
|
548
|
+
|| bad "T2/$_sh 비교 대상이 생산자가 실제로 쓴 파일이 아니다"
|
|
549
|
+
[ "$_sh" = "zsh" ] && _cond_lanes=$((_cond_lanes + 4))
|
|
550
|
+
done
|
|
551
|
+
# S3 — dash 는 `$'"'"'\t'"'"'` 를 리터럴로 읽어 정상 행이 통째로 떨어졌다(bad=1 stream=[]).
|
|
552
|
+
# 그건 «패턴이 없다» 가 아니라 **미측정**이므로 로드 자체를 거부한다.
|
|
553
|
+
if command -v dash >/dev/null 2>&1; then
|
|
554
|
+
_ST=$(mktemp -d); printf 'HIGH\tREALSECRET\n' > "$_ST/def"; : > "$_ST/ovr"
|
|
555
|
+
_r=$(dash -c ". '$LIB'; psa_load '$_ST/def' '$_ST/ovr' >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
556
|
+
[ "$_r" = "3" ] && ok "S3 dash → psa_load rc=3 (조용한 빈 스트림이 아니라 명시적 미측정)" \
|
|
557
|
+
|| bad "S3 dash psa_load rc=[$_r], 3 이어야 — 무능한 셸이 «패턴 없음» 으로 접힌다"
|
|
558
|
+
# S3b ★컨트롤 — 같은 파일로 bash 는 정상 로드 (S3 가 «항상 거부» 가 아니다)
|
|
559
|
+
_r=$(bash -c ". '$LIB'; psa_load '$_ST/def' '$_ST/ovr' >/dev/null 2>&1; echo \$?" 2>/dev/null | tail -1)
|
|
560
|
+
[ "$_r" = "0" ] && ok "S3b ★컨트롤 bash 는 같은 파일을 정상 로드 → rc=0" \
|
|
561
|
+
|| bad "S3b 컨트롤 rc=[$_r], 0 이어야 — 셸 검사가 과차단이다"
|
|
562
|
+
rm -rf "$_ST"
|
|
563
|
+
_cond_lanes=$((_cond_lanes + 2))
|
|
564
|
+
else
|
|
565
|
+
echo " SKIP S3/S3b (dash 없음) — PASS 아님, 미검사다"
|
|
566
|
+
fi
|
|
567
|
+
|
|
568
|
+
# N3 — 🟥 **교차조건 레인**: `set -e` × 실패 생산자. codex 가 「세 라운드가 공유한 갭」으로 지목한 것.
|
|
569
|
+
# 한 조건씩 보면 둘 다 통과하는데, 겹치면 복원문에 **도달하지 못하고** PSA_STREAM 이
|
|
570
|
+
# 카나리아인 채로 남는다. EXIT trap 으로 «중단돼도» 상태를 회수한다 —
|
|
571
|
+
# ⚠️ `|| true` 를 붙이면 errexit 가 꺼져서 계기 자체가 죽는다(첫 판이 그랬다).
|
|
572
|
+
# bash 전용: zsh 는 `-c` + errexit 중단에서 EXIT trap 이 발화하지 않아 회수 채널이 없다(미검사).
|
|
573
|
+
_N3=$(mktemp -d)
|
|
574
|
+
bash -c "set -e; . '$LIB'; PSA_STREAM=ORIGINAL; PSA_ALLOWLIST=/dev/null; PSA_DEFAULTS_OK=1;
|
|
575
|
+
trap 'printf \"%s\" \"\$PSA_STREAM\" > '$_N3'/v' EXIT
|
|
576
|
+
awk(){ return 1; }
|
|
577
|
+
psa_require_live >/dev/null 2>&1" >/dev/null 2>&1
|
|
578
|
+
# portability-noqa: 이 파일은 set -e 가 아니다 — 린트가 잡은 `set -e` 는 위 `bash -c` 문자열 «안»의 것이고
|
|
579
|
+
# 그건 자식 셸의 설정이다. 파일 스코프 판별의 오탐이며, 그 오탐 자체가 N3 가 재현하는 결함이다.
|
|
580
|
+
_r=$(cat "$_N3/v" 2>/dev/null); rm -rf "$_N3"
|
|
581
|
+
[ "$_r" = "ORIGINAL" ] && ok "N3 ★교차조건 set -e × 실패 생산자 → PSA_STREAM 복원됨" \
|
|
582
|
+
|| bad "N3 교차조건에서 복원 미도달 — PSA_STREAM=[$_r] (카나리아 잔류 = P3 미해결)"
|
|
583
|
+
|
|
584
|
+
# N6 — P2: outbound guard 의 override 가 «미측정» 까지 통과시키면 안 된다.
|
|
585
|
+
# 🟥 첫 판 픽스처는 `FH=` 로 뿌리를 바꾸려 했는데 그 변수는 스크립트가 자기 것으로 계산한다
|
|
586
|
+
# → 가드가 **실제 레포를 스캔하고 깨끗하다고 통과했다**. 죽은 픽스처였다.
|
|
587
|
+
# 가드가 이미 제공하는 주입점(PSA_LIB_FILE / PSA_DEFAULTS_FILE / PSA_OVERRIDE_FILE)을 쓴다.
|
|
588
|
+
# 🟥 이 블록은 **조건부**다 — `outbound_query_guard.sh` 는 npm `files[]` 에 없어서
|
|
589
|
+
# **소비자 설치본에는 존재하지 않는다.** 초판은 조건부인데 `_cond_lanes` 에 안 세서,
|
|
590
|
+
# 포장본에서 `pass=80 < floor 84` 로 **거짓 INSTRUMENT ERROR** 를 냈다(실측 rc=3).
|
|
591
|
+
# 정적으로는 안 보였고 **포장본에서 실행하자마자** 나왔다 — standpoint 실행 팔이 잡은 것.
|
|
592
|
+
_GUARD="$REPO_ROOT/scripts/outbound_query_guard.sh"
|
|
593
|
+
if [ -f "$_GUARD" ]; then
|
|
594
|
+
_cond_lanes=$((_cond_lanes + 4))
|
|
595
|
+
_S6=$(mktemp -d)
|
|
596
|
+
# 스텁 라이브러리: 앞단 게이트는 전부 통과시키고 **스캔만 rc=3(미측정)** 을 내게 한다.
|
|
597
|
+
# 이래야 P2 가 고친 «그 분기»(RC 판정 직후의 override)에 실제로 도달한다.
|
|
598
|
+
cat > "$_S6/lib_unmeasured.sh" <<'STUB'
|
|
599
|
+
psa_load() { PSA_DEFAULTS_OK=1; PSA_BAD_ROWS=0; PSA_OVERRIDE_PRESENT=1; return 0; }
|
|
600
|
+
psa_require_live() { return 0; }
|
|
601
|
+
psa_scan_tagged() { return 3; }
|
|
602
|
+
STUB
|
|
603
|
+
# 대조 스텁: 같은 모양인데 스캔이 rc=1(유출을 «봤다») 을 낸다
|
|
604
|
+
sed 's/return 3/return 1/' "$_S6/lib_unmeasured.sh" > "$_S6/lib_leak.sh"
|
|
605
|
+
_n6() { OUTBOUND_QUERY_OK=1 PSA_LIB_FILE="$1" bash "$_GUARD" "some outbound question" >/dev/null 2>&1; echo $?; }
|
|
606
|
+
_r=$(_n6 "$_S6/lib_unmeasured.sh")
|
|
607
|
+
[ "$_r" = "3" ] && ok "N6a override + 스캔 미측정(rc=3) → 여전히 차단 (rc=3)" \
|
|
608
|
+
|| bad "N6a OUTBOUND_QUERY_OK=1 이 미측정을 승인했다 rc=[$_r] — P2 가 안 닫혔다"
|
|
609
|
+
# N6b ★컨트롤 — 같은 override 가 «본» 히트(rc=1)에서는 여전히 통과해야 한다
|
|
610
|
+
# (N6a 가 «override 를 통째로 죽였다» 가 아님을 보인다)
|
|
611
|
+
_r=$(_n6 "$_S6/lib_leak.sh")
|
|
612
|
+
[ "$_r" = "0" ] && ok "N6b ★컨트롤 override + 히트(rc=1) → 통과 (override 가 죽지 않았다)" \
|
|
613
|
+
|| bad "N6b 컨트롤 rc=[$_r], 0 이어야 — 과차단이다"
|
|
614
|
+
# R5 — rc=2 는 «유출 발견» 이 아니라 **미측정** 이다. 초판은 exit 1 로 오분류했다.
|
|
615
|
+
printf '%s\n' 'psa_load(){ PSA_DEFAULTS_OK=1; PSA_BAD_ROWS=0; PSA_OVERRIDE_PRESENT=1; return 0; }' \
|
|
616
|
+
'psa_require_live(){ return 0; }' 'psa_scan_tagged(){ return 2; }' > "$_S6/lib_rc2.sh"
|
|
617
|
+
_r=$(_n6 "$_S6/lib_rc2.sh")
|
|
618
|
+
[ "$_r" = "3" ] && ok "R5 스캐너 rc=2 → 미측정(3) 으로 분류 (유출 1 로 접히지 않는다)" \
|
|
619
|
+
|| bad "R5 rc=2 가 [$_r] 로 분류됐다, 3 이어야"
|
|
620
|
+
# N6c ★컨트롤 — override 없이 미측정이면 당연히 차단 (N6a 가 override 덕이 아님을 가른다)
|
|
621
|
+
_r=$(PSA_LIB_FILE="$_S6/lib_unmeasured.sh" bash "$_GUARD" "some outbound question" >/dev/null 2>&1; echo $?)
|
|
622
|
+
[ "$_r" = "3" ] && ok "N6c ★컨트롤 override 없는 미측정 → 차단 (rc=3)" \
|
|
623
|
+
|| bad "N6c 컨트롤 rc=[$_r], 3 이어야"
|
|
624
|
+
rm -rf "$_S6"
|
|
625
|
+
else
|
|
626
|
+
echo " SKIP N6a/N6b/N6c/R5 (outbound_query_guard.sh 부재 — 배포 산출물) — PASS 아님, 미검사다"
|
|
627
|
+
fi
|
|
628
|
+
|
|
285
629
|
echo "[psa single-file lanes] pass=$pass fail=$fail"
|
|
286
630
|
[ "$fail" -eq 0 ] || exit 1
|
|
287
|
-
|
|
631
|
+
# 🟥 플로어는 **조건부 레인을 반영한 값**이다. 33 = zsh 없는 러너의 기저(Z1~Z4 · E2~E5/zsh 스킵).
|
|
632
|
+
# zsh 가 있으면 +8. 상수 하나로 박으면 «스킵 = 계기 오류» 가 되어 러너를 거짓 적색으로 만든다.
|
|
633
|
+
# ⚠️ 그리고 이 수식이 말하는 진짜 사실을 잊지 마라: **zsh 축은 zsh 가 있는 곳에서만 검증된다.**
|
|
634
|
+
# CI 에 zsh 를 설치한 이유가 그것이고(.github/workflows/validate.yml), 그게 없으면 이 PR 이
|
|
635
|
+
# 고친 결함의 축이 CI 에서 **한 번도** 안 돌아간다.
|
|
636
|
+
_floor=$((52 + _cond_lanes))
|
|
637
|
+
[ "$pass" -ge "$_floor" ] || { echo " ❌ INSTRUMENT ERROR — only $pass lanes ran; expected >=$_floor (zsh 조건부 +$_cond_lanes)"; exit 3; }
|
|
288
638
|
exit 0
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_residency_admission_lanes.sh — residency_admission_check.sh 의 known-pair 레인.
|
|
3
|
+
# 🟥 컨트롤(감량·무변경)이 없으면 «오탐이 있나» 를 못 잰다. 차단 레인만 있는 스위트는
|
|
4
|
+
# 「막혔다」만 증명하고 「옳게 막혔다」는 못 증명한다(`feedback_gate_verification_must_execute`).
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
CHECK="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/residency_admission_check.sh}"
|
|
7
|
+
# 🟥 절대경로로 고정한다. 각 레인은 임시 디렉터리로 `cd` 하므로 상대경로를 받으면
|
|
8
|
+
# 거기서 깨진다 — 스크래치패드에서 10/10 이던 것이 레포로 옮기자마자 **0/10** 이 됐다
|
|
9
|
+
# (`feedback_parallel_isolation_unit_is_checkout_not_file`: cwd 상대경로는 이동에 안 산다).
|
|
10
|
+
case "$CHECK" in /*) ;; *) CHECK="$(cd "$(dirname "$CHECK")" && pwd)/$(basename "$CHECK")" ;; esac
|
|
11
|
+
[ -f "$CHECK" ] || { echo "❌ HARNESS ERROR: 검사 스크립트를 못 찾겠다: $CHECK" >&2; exit 2; }
|
|
12
|
+
PASS=0; FAIL=0
|
|
13
|
+
# 🟥 rc 만 보면 **처방을 안 본다.** 이 게이트의 산출물은 판정이 아니라 «무엇을 하라» 이고,
|
|
14
|
+
# 필드-부재 레인은 rc 만으로는 enum 가드에 가려져 격리가 안 된다(되돌림 프로브가 지목).
|
|
15
|
+
run() { # run <lane> <expected-rc> <setup-fn> [expected-substring]
|
|
16
|
+
local lane="$1" want="$2" fn="$3" want_msg="${4:-}"
|
|
17
|
+
W=$(mktemp -d); ( cd "$W" && git init -q . && mkdir -p scripts tracks/_meta && cp "$CHECK" scripts/ \
|
|
18
|
+
&& git config user.email t@t && git config user.name t && "$fn" ) >/dev/null 2>&1
|
|
19
|
+
out=$(cd "$W" && bash scripts/"$(basename "$CHECK")" 2>&1); rc=$?
|
|
20
|
+
local msg_ok=1
|
|
21
|
+
if [ -n "$want_msg" ] && ! printf '%s' "$out" | grep -q "$want_msg"; then msg_ok=0; fi
|
|
22
|
+
if [ "$rc" -eq "$want" ] && [ "$msg_ok" -eq 1 ]; then PASS=$((PASS+1)); printf ' ✅ %-34s rc=%s\n' "$lane" "$rc"
|
|
23
|
+
else FAIL=$((FAIL+1)); printf ' ❌ %-34s rc=%s (기대 %s)\n %s\n' "$lane" "$rc" "$want" "$(printf '%s' "$out" | head -2 | tr '\n' ' ')"; fi
|
|
24
|
+
rm -rf "$W"
|
|
25
|
+
}
|
|
26
|
+
mk_base() { printf 'x\n%.0s' $(seq 1 50) > CLAUDE.md; git add CLAUDE.md; git commit -qm base; }
|
|
27
|
+
mk_marker() { printf '%s\n' "$1" > "tracks/_meta/.axes_23_passed_test_$(date +%Y-%m-%d).marker"; }
|
|
28
|
+
grow() { printf 'y\n%.0s' $(seq 1 20) >> CLAUDE.md; git add CLAUDE.md; }
|
|
29
|
+
|
|
30
|
+
L_nochange() { mk_base; } # 컨트롤
|
|
31
|
+
L_shrink() { mk_base; sed -i '' '1,20d' CLAUDE.md 2>/dev/null || sed -i '1,20d' CLAUDE.md; git add CLAUDE.md; } # 컨트롤: 감량
|
|
32
|
+
L_nofield() { mk_base; grow; mk_marker "axes-run: ⓐⓑ"; }
|
|
33
|
+
# 🟥 근거를 **일부러 길게** 준다. 짧으면 근거-공허 검사에 먼저 걸려서 이 레인이
|
|
34
|
+
# enum 가드를 격리하지 못한다 — 되돌림 프로브가 그걸 잡았다(가드를 지워도 8/8 초록이었다).
|
|
35
|
+
L_badenum() { mk_base; grow; mk_marker "residency: whatever — 근거는 충분히 길게 적어서 이 레인이 오직 enum 검사에만 걸리도록 만든다"; }
|
|
36
|
+
L_selfreject(){ mk_base; grow; mk_marker "residency: relocatable(knowledge/shared/rules/operations.md) — 근거를 길게 적어 오직 자기거절 가드에만 걸리게 한다"; }
|
|
37
|
+
L_nodest() { mk_base; grow; mk_marker "residency: relocatable — 목적지를 안 적었다 그래도 근거는 충분히 길게 적는다"; }
|
|
38
|
+
L_novoid() { mk_base; grow; mk_marker "residency: soul —"; }
|
|
39
|
+
L_valid() { mk_base; grow; mk_marker "residency: soul — 절대 안 함 목록이라 판단 회로다"; }
|
|
40
|
+
# soul 인데 추가분이 근거 서술 → advisory 가 떠야 한다(차단은 아니다: rc=0)
|
|
41
|
+
L_soulprov() { mk_base; for i in 1 2 3 4 5; do echo "2026-08-$i 실측: n=3 반증됨" >> CLAUDE.md; done; git add CLAUDE.md; mk_marker "residency: soul — 판단 회로라고 주장한다 근거는 길게"; }
|
|
42
|
+
L_nomarker() { mk_base; grow; } # tracks/_meta 는 있는데 오늘자 마커가 없다 → HARNESS ERROR
|
|
43
|
+
# 🟥 소비 레포: 마커 체제 자체가 없다 → **SKIP(적용 대상 없음)** 이지 차단이 아니다.
|
|
44
|
+
# 이 레인이 없으면 「이식 후 통과 불가」가 조용히 살아 있다(소비자 트리 실행이 잡았다).
|
|
45
|
+
L_consumer() { mk_base; grow; rm -rf tracks/_meta; }
|
|
46
|
+
|
|
47
|
+
echo "── residency admission lanes ──"
|
|
48
|
+
run "컨트롤: 상주 무변경" 0 L_nochange
|
|
49
|
+
run "컨트롤: 감량(순감)" 0 L_shrink
|
|
50
|
+
run "순증 + 유형 선언 정상" 0 L_valid
|
|
51
|
+
run "soul 인데 근거서술(advisory)" 0 L_soulprov "근거 서술이다"
|
|
52
|
+
run "순증인데 필드 없음(+처방)" 1 L_nofield "판단 회로다"
|
|
53
|
+
run "순증 + enum 밖 값" 1 L_badenum
|
|
54
|
+
run "순증 + 자기 거절(목적지 있음)" 1 L_selfreject
|
|
55
|
+
run "순증 + 자기 거절(목적지 없음)" 1 L_nodest "옮길 곳을 안 적었다"
|
|
56
|
+
run "순증 + 근거 공허" 1 L_novoid
|
|
57
|
+
run "순증인데 마커 자체 부재" 2 L_nomarker
|
|
58
|
+
run "소비 레포(마커 체제 없음)" 0 L_consumer "적용 대상 없음"
|
|
59
|
+
echo "── PASS $PASS · FAIL $FAIL ──"
|
|
60
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_track_resolve_lanes.sh — scripts/fh_track_resolve.sh 의 known-pair 레인.
|
|
3
|
+
#
|
|
4
|
+
# 이 파일이 있는 이유: 2026-08-21 harness-doctor F-1. track→repo 해석이 세 소비자에 각각
|
|
5
|
+
# 다르게 구현돼 `tracks/the_bible` 이 두 소비자에서 **무음 드롭**되고 있었다. 레인은 별칭
|
|
6
|
+
# 목록이 닫혀 있는지 · 모호를 고르지 않는지 · UNRESOLVED 를 0 으로 접지 않는지를 잰다.
|
|
7
|
+
#
|
|
8
|
+
# 🟥 픽스처는 «가장 쉬운 표기» 가 아니라 **실제로 뚫렸던 표기**를 쓴다 — 밑줄 이름(the_bible)이
|
|
9
|
+
# 그것이다. 하이픈 이름만 테스트하면 뚫린 축을 안 건드린다.
|
|
10
|
+
set -u
|
|
11
|
+
LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/fh_track_resolve.sh"
|
|
12
|
+
[ -f "$LIB" ] || { echo "❌ lib 없음: $LIB"; exit 1; }
|
|
13
|
+
. "$LIB"
|
|
14
|
+
|
|
15
|
+
PASS=0; FAIL=0
|
|
16
|
+
ok() { PASS=$((PASS+1)); echo " ✅ $1"; }
|
|
17
|
+
no() { FAIL=$((FAIL+1)); echo " ❌ $1 — got: [$2]"; }
|
|
18
|
+
want() { # want <label> <expected> <actual>
|
|
19
|
+
if [ "$2" = "$3" ]; then ok "$1"; else no "$1 (expected [$2])" "$3"; fi
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
|
23
|
+
P="$TMP/projects"; mkdir -p "$P"
|
|
24
|
+
|
|
25
|
+
# ── 픽스처 ────────────────────────────────────────────────────────────────
|
|
26
|
+
mkdir -p "$P/plainname/.git" # 이름 그대로 + git
|
|
27
|
+
mkdir -p "$P/withdev-dev/.git" # `-dev` 별칭만 존재
|
|
28
|
+
mkdir -p "$P/under-score/.git" # 밑줄→하이픈 별칭만 존재 (track 이름은 under_score)
|
|
29
|
+
mkdir -p "$P/nogit" # 디렉토리는 있고 .git 없음 — 술어 분기용
|
|
30
|
+
mkdir -p "$P/ambi" "$P/ambi-dev" # 둘 다 존재 → 모호
|
|
31
|
+
|
|
32
|
+
echo "── 1. 별칭 3종이 각각 맞는가 (닫힌 목록) ──"
|
|
33
|
+
want "plain: 이름 그대로, note 비어야" "$P/plainname|" "$(fh_resolve_track_root plainname "$P" dir)"
|
|
34
|
+
want "alias -dev" "$P/withdev-dev|alias:withdev-dev" "$(fh_resolve_track_root withdev "$P" dir)"
|
|
35
|
+
want "alias _→- (실제로 뚫렸던 표기)" "$P/under-score|alias:under-score" "$(fh_resolve_track_root under_score "$P" dir)"
|
|
36
|
+
|
|
37
|
+
echo "── 2. 모호는 «고르지 않는다» ──"
|
|
38
|
+
got=$(fh_resolve_track_root ambi "$P" dir)
|
|
39
|
+
case "$got" in
|
|
40
|
+
*"|AMBIGUOUS:ambi,ambi-dev") ok "둘 다 맞으면 AMBIGUOUS, 조용히 첫째를 고르지 않음" ;;
|
|
41
|
+
*) no "모호를 고르지 않아야 한다" "$got" ;;
|
|
42
|
+
esac
|
|
43
|
+
|
|
44
|
+
echo "── 3. 부재는 UNRESOLVED — «0» 이나 빈 문자열이 아니다 ──"
|
|
45
|
+
got=$(fh_resolve_track_root ghost "$P" dir)
|
|
46
|
+
case "$got" in
|
|
47
|
+
*"|UNRESOLVED") ok "미해소를 UNRESOLVED 로 표기 (부재 ≠ 0)" ;;
|
|
48
|
+
*) no "UNRESOLVED 여야 한다" "$got" ;;
|
|
49
|
+
esac
|
|
50
|
+
|
|
51
|
+
echo "── 4. 존재 술어는 통일하지 않는다 (소비자별로 다른 게 의도) ──"
|
|
52
|
+
want "pred=dir 면 .git 없어도 맞음" "$P/nogit|" "$(fh_resolve_track_root nogit "$P" dir)"
|
|
53
|
+
got=$(fh_resolve_track_root nogit "$P" git)
|
|
54
|
+
case "$got" in
|
|
55
|
+
*"|UNRESOLVED") ok "pred=git 면 .git 없는 디렉토리는 안 맞음" ;;
|
|
56
|
+
*) no "pred=git 은 .git 을 요구해야 한다" "$got" ;;
|
|
57
|
+
esac
|
|
58
|
+
|
|
59
|
+
echo "── 5. known-NEGATIVE — 별칭 목록이 닫혀 있나 (임의 접미를 주워오면 안 된다) ──"
|
|
60
|
+
mkdir -p "$P/closed-suffix-test-prod"
|
|
61
|
+
got=$(fh_resolve_track_root closed-suffix-test "$P" dir)
|
|
62
|
+
case "$got" in
|
|
63
|
+
*"|UNRESOLVED") ok "비등재 접미(-prod)는 안 주워온다 — 목록이 닫혀 있음" ;;
|
|
64
|
+
*) no "닫힌 목록이어야 한다" "$got" ;;
|
|
65
|
+
esac
|
|
66
|
+
|
|
67
|
+
echo "── 6. 종료코드도 판정을 싣는다 (0 해소 · 1 UNRESOLVED · 2 AMBIGUOUS · 3 ARGS) ──"
|
|
68
|
+
# 🟥 초판 계약은 «항상 rc=0, 판정은 note 로» 였다. 2026-08-21 cross-family(codex/gpt-5.6-sol)가
|
|
69
|
+
# **실행해서** 반증했다: 종료코드만 읽는 호출자에게 AMBIGUOUS 와 UNRESOLVED 가 **둘 다 성공**
|
|
70
|
+
# 으로 보인다(`if fh_resolve_track_root … >/dev/null; then consume; fi`). note 는 그대로 두고
|
|
71
|
+
# rc 를 타입화했다 — 두 채널이 같은 판정을 실으면 어느 쪽만 읽어도 fail-closed 다.
|
|
72
|
+
fh_resolve_track_root plainname "$P" dir >/dev/null; want "해소 → rc=0" "0" "$?"
|
|
73
|
+
fh_resolve_track_root ghost "$P" dir >/dev/null; want "UNRESOLVED → rc=1 (성공으로 안 접힌다)" "1" "$?"
|
|
74
|
+
fh_resolve_track_root ambi "$P" dir >/dev/null; want "AMBIGUOUS → rc=2 (성공으로 안 접힌다)" "2" "$?"
|
|
75
|
+
|
|
76
|
+
echo "── 6-b. 🟥 전제 파손은 «못 찾음» 이 아니다 — ARGS(rc=3). 전부 cross-family 가 실행으로 찾았다 ──"
|
|
77
|
+
# ⓐ n="" → 첫 후보가 "" 라 `[ -d "$root/" ]` = `[ -d "/" ]` 가 참이 되던 자리
|
|
78
|
+
fh_resolve_track_root "" "$P" dir >/dev/null; want "빈 이름 → rc=3" "3" "$?"
|
|
79
|
+
# ⓑ root="" → `$root/$n` 이 `/foo` 가 되어 **의도한 루트가 아닌 파일시스템 루트**를 봤다
|
|
80
|
+
fh_resolve_track_root plainname "" dir >/dev/null; want "빈 루트 → rc=3" "3" "$?"
|
|
81
|
+
# ⓒ 경로 탈출 — `-d` 만 참이면 루트 바깥도 해소됐다. dedup 의 `/` 불변식도 여기서 지켜진다
|
|
82
|
+
fh_resolve_track_root "../etc" "$P" dir >/dev/null; want "상위 탈출 → rc=3" "3" "$?"
|
|
83
|
+
fh_resolve_track_root "a/b" "$P" dir >/dev/null; want "경로 구분자 → rc=3" "3" "$?"
|
|
84
|
+
# ⓓ `|` → 직렬화가 깨져 **손상된 결과가 정상 성공으로 승격**되던 자리
|
|
85
|
+
fh_resolve_track_root "a|b" "$P" dir >/dev/null; want "직렬화 구분자 → rc=3" "3" "$?"
|
|
86
|
+
# ⓔ pred 오타가 `*)` 로 떨어져 조용히 dir 모드가 됐다 — **오타가 fail-open**
|
|
87
|
+
fh_resolve_track_root plainname "$P" gti >/dev/null; want "알 수 없는 술어 → rc=3 (조용히 dir 로 안 떨어진다)" "3" "$?"
|
|
88
|
+
# 컨트롤 — 정상 입력이 이 검증들에 안 걸린다(6-b 가 통째로 «전부 거부» 가 아니라는 증거)
|
|
89
|
+
# (픽스처 plainname 은 `.git` 을 갖고 만들어졌으므로 pred=git 에서도 해소된다 → rc=0.
|
|
90
|
+
# 초판은 여기 기대값을 1 로 썼다가 빨개졌는데, **틀린 것은 대상이 아니라 내 기대**였다.)
|
|
91
|
+
fh_resolve_track_root plainname "$P" git >/dev/null; want "컨트롤: pred=git 은 유효한 값이라 거부되지 않는다" "0" "$?"
|
|
92
|
+
fh_resolve_track_root nogit "$P" git >/dev/null; want "컨트롤: pred=git 은 실제로 .git 을 요구한다(ARGS 가 아니라 UNRESOLVED)" "1" "$?"
|
|
93
|
+
|
|
94
|
+
echo "── 7. fail-before 컨트롤 — 옛 «-dev 접미만» 로직은 밑줄 이름에서 실패했어야 한다 ──"
|
|
95
|
+
_old_resolve() { local n="$1" r="$2" c; for c in "$r/$n" "$r/${n}-dev"; do [ -d "$c" ] && { printf '%s' "$c"; return; }; done; printf ''; }
|
|
96
|
+
old=$(_old_resolve under_score "$P")
|
|
97
|
+
new=$(fh_resolve_track_root under_score "$P" dir)
|
|
98
|
+
if [ -z "$old" ] && [ "${new%|*}" = "$P/under-score" ]; then
|
|
99
|
+
ok "옛 로직=무음 드롭(빈 값) / 새 로직=해소 — 레인이 실제 회귀를 잡는다"
|
|
100
|
+
else
|
|
101
|
+
no "fail-before 가 성립해야 한다 (옛=빈 값, 새=해소)" "old=[$old] new=[$new]"
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
echo "── 8. 🟥 회귀 앵커 — 이름에 공백이 있어도 거짓 AMBIGUOUS 가 나면 안 된다 ──"
|
|
105
|
+
# 2026-08-21 실측 결함: 구분자가 공백이고 개수를 `wc -w` 로 세면, 실재하는 `a b` 한 건이
|
|
106
|
+
# 두 단어로 세어져 AMBIGUOUS 가 났다. 원본 cluster_capability_scan.sh 에서 물려받은 형태다.
|
|
107
|
+
# 이 레인이 죽으면 구분자가 공백으로 되돌아간 것이다.
|
|
108
|
+
mkdir -p "$P/spaced name"
|
|
109
|
+
want "공백 이름 1건은 정상 해소 (거짓 AMBIGUOUS 아님)" "$P/spaced name|" "$(fh_resolve_track_root 'spaced name' "$P" dir)"
|
|
110
|
+
# 컨트롤: 진짜 모호는 여전히 발화해야 한다 — 위 레인의 침묵이 공허하지 않다는 증거
|
|
111
|
+
got=$(fh_resolve_track_root ambi "$P" dir)
|
|
112
|
+
case "$got" in
|
|
113
|
+
*"|AMBIGUOUS:"*) ok "컨트롤: 진짜 모호는 여전히 발화(레인 8 이 모호 검출을 죽인 게 아님)" ;;
|
|
114
|
+
*) no "컨트롤 실패 — 모호 검출이 죽었다" "$got" ;;
|
|
115
|
+
esac
|
|
116
|
+
|
|
117
|
+
echo "── 9. 🟥 배선 레인 — 소비자 스크립트를 실제로 태운다 (레인 1~8 은 함수만 잰다) ──"
|
|
118
|
+
# 2026-08-21 적대검증 HIGH-1, **실행으로 확정**: 배선만 되돌리고 라이브러리·레인을 그대로 두면
|
|
119
|
+
# 레인 8건 + field_canon 레인 19건이 **전량 초록**이었다. 즉 이 수리가 실제로 고친 것
|
|
120
|
+
# («소비자가 밑줄→하이픈 별칭을 본다»)에 대한 앵커가 0개였다. 같은 레포의 선례가 이미
|
|
121
|
+
# 경고한 형태다 — cluster_capability_scan.sh 가 "함수를 떼어내 재지 않고 실물을 그대로 태운다"
|
|
122
|
+
# 고 적어놨는데 이 수리는 정확히 거꾸로 갔다. [[feedback_anchor_can_be_decorative]]
|
|
123
|
+
#
|
|
124
|
+
# 🟥 픽스처는 **뚫려 있던 표기**를 쓴다: 트랙 `the_bible` ↔ 레포 `the-bible`.
|
|
125
|
+
# 기존 field_canon 픽스처는 `qasp`→`qasp-dev` 인데 그건 **옛 코드도 이미 풀던** 별칭이라
|
|
126
|
+
# 이 축을 한 번도 안 태운다.
|
|
127
|
+
HOOK="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/field_canon_preload.sh"
|
|
128
|
+
if [ ! -f "$HOOK" ]; then
|
|
129
|
+
echo " ⏭️ SKIP (field_canon_preload.sh 없음) — 🟥 PASS 가 아니다, 미검사다"
|
|
130
|
+
else
|
|
131
|
+
W=$(mktemp -d)
|
|
132
|
+
FHUB="$W/hub"; mkdir -p "$FHUB/tracks/the_bible"
|
|
133
|
+
FPROJ="$W/proj"; mkdir -p "$FPROJ/the-bible/.git"
|
|
134
|
+
printf '# fixture\n' > "$FPROJ/the-bible/README.md"
|
|
135
|
+
wout=$(printf '%s' '{"prompt":"the_bible 이어서 가자","session_id":"WIRE1"}' \
|
|
136
|
+
| env CLAUDE_PROJECT_DIR="$FHUB" FIELD_CANON_PROJECT_ROOT="$FPROJ" \
|
|
137
|
+
FIELD_CANON_SENTINEL_DIR="$W/sent" bash "$HOOK" 2>&1)
|
|
138
|
+
case "$wout" in
|
|
139
|
+
*the-bible*README.md*) ok "소비자(field_canon)가 밑줄→하이픈 트랙의 정본을 실제로 낸다" ;;
|
|
140
|
+
*) no "소비자가 the_bible→the-bible 을 해소해야 한다" "$wout" ;;
|
|
141
|
+
esac
|
|
142
|
+
# 컨트롤: 옛 코드도 풀던 -dev 별칭은 **원래** 되던 것이므로, 위 레인의 통과가
|
|
143
|
+
# 「아무 트랙이나 되는 것」이 아님을 이 컨트롤이 보증한다.
|
|
144
|
+
mkdir -p "$FHUB/tracks/ghosttrack"
|
|
145
|
+
gout=$(printf '%s' '{"prompt":"ghosttrack 가자","session_id":"WIRE2"}' \
|
|
146
|
+
| env CLAUDE_PROJECT_DIR="$FHUB" FIELD_CANON_PROJECT_ROOT="$FPROJ" \
|
|
147
|
+
FIELD_CANON_SENTINEL_DIR="$W/sent2" bash "$HOOK" 2>&1)
|
|
148
|
+
case "$gout" in
|
|
149
|
+
*ghosttrack*) no "컨트롤: 레포가 없는 트랙은 안 나와야 한다" "$gout" ;;
|
|
150
|
+
*) ok "컨트롤: 레포 실물이 없는 트랙은 침묵 (레인 9 의 통과가 공허하지 않다)" ;;
|
|
151
|
+
esac
|
|
152
|
+
rm -rf "$W"
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
echo
|
|
156
|
+
echo "PASS=$PASS FAIL=$FAIL"
|
|
157
|
+
[ "$FAIL" -eq 0 ] || exit 1
|
|
158
|
+
exit 0
|