@chrono-meta/fh-gate 2.4.0 → 2.5.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.
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ # test_evidence_root_psa_lanes.sh — pre-commit 의 기밀성 스캐너가 **워크트리에서도 오버라이드를
3
+ # 찾는가** (R4, 2026-08-18).
4
+ #
5
+ # WHY
6
+ # `tracks/` 는 `--git-common-dir` 기준 `EVIDENCE_ROOT` 로 옮겼는데, **똑같이 gitignored 인**
7
+ # `.claude/rules/.public-surface-patterns` 는 `$REPO_ROOT` 에 남아 있었다. 그래서 워크트리
8
+ # 커밋이 매번 **defaults-only** 로 돌았다 — 회사명·실명 토큰 클래스가 통째로 UNSCANNED 고,
9
+ # 그 상태는 **비차단 경고**라 조용히 지나간다. 반쪽-픽스의 전형
10
+ # ([[feedback_half_fix_propagation_boundary]]) 이고, EVIDENCE_ROOT 에는 **레인이 하나도
11
+ # 없었다** — 그래서 반쪽인 채로 출하됐다.
12
+ #
13
+ # Lanes
14
+ # E1 워크트리에서 커밋해도 오버라이드를 찾는다 (경고 배너 없음)
15
+ # E1n CONTROL — 오버라이드를 지우면 같은 실행에서 배너가 **뜬다**(계기가 살아 있다는 증거)
16
+ # E2 CONTROL — 메인 트리는 종전 그대로 동작한다 (회귀 없음)
17
+ set -u
18
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
19
+ pass=0; fail=0
20
+ t() { if [ "$2" = "$3" ]; then echo " ✅ $1"; pass=$((pass+1)); else echo " ❌ $1 — got [$3] want [$2]"; fail=$((fail+1)); fi }
21
+
22
+ BANNER='CONFIDENTIALITY COVERAGE REDUCED'
23
+
24
+ setup() { # $1 = base dir
25
+ local b="$1"
26
+ mkdir -p "$b/main"
27
+ ( cd "$b/main" && git init -q . && git config user.email t@t && git config user.name t
28
+ mkdir -p .claude/rules scripts templates/.git-hooks tracks/_meta
29
+ cp "$ROOT/scripts/psa_scan_lib.sh" scripts/
30
+ cp "$ROOT/.claude/rules/.public-surface-patterns.defaults" .claude/rules/ 2>/dev/null
31
+ cp "$ROOT/templates/.git-hooks/pre-commit" templates/.git-hooks/
32
+ printf 'tracks/\n.claude/rules/.public-surface-patterns\n' > .gitignore
33
+ printf 'x\n' > seed.md
34
+ git add -A && git commit -qm base ) >/dev/null 2>&1
35
+ # gitignored 오버라이드 — **메인 트리에만** 둔다 (실제 배치가 그렇다)
36
+ printf 'HIGH\tZZ_R4_OVERRIDE_TOKEN_ZZ\n' > "$b/main/.claude/rules/.public-surface-patterns"
37
+ }
38
+
39
+ # 🟥 고정 `/tmp` 경로 금지 (보안 패스 2026-08-18, MED·재현됨). `>/tmp/.r4out` 은 이름이
40
+ # 완전 고정이라, 공유 `/tmp` 를 쓰는 CI 러너·다중사용자 호스트에서 공격자가 먼저 심볼릭
41
+ # 링크를 걸어두면 `>` 가 링크를 따라가 **호출자 권한으로 임의 파일을 truncate + overwrite**
42
+ # 한다(sticky bit 는 «남의 파일 삭제»만 막지 «먼저 만든 링크»는 못 막는다). 이 스위트는
43
+ # 출하되고 `selfcheck.sh` 에 배선돼 있어 소비자가 셀프체크만 돌려도 발동한다.
44
+ R4OUT="$(mktemp "${TMPDIR:-/tmp}/fh_r4out.XXXXXX")" || exit 1
45
+ trap 'rm -f "$R4OUT"' EXIT
46
+
47
+ run_hook() { # $1 = 실행 디렉터리 → echoes banner|clean
48
+ local out
49
+ ( cd "$1" && printf 'hello\n' > probe.md && git add probe.md \
50
+ && bash templates/.git-hooks/pre-commit ) >"$R4OUT" 2>&1
51
+ if grep -q "$BANNER" "$R4OUT"; then echo banner; else echo clean; fi
52
+ }
53
+
54
+ B=$(mktemp -d); setup "$B"
55
+ ( cd "$B/main" && git worktree add -q "$B/wt" -b wtbranch ) >/dev/null 2>&1
56
+ # 워크트리에는 templates/ 가 체크아웃돼 있다(tracked). 오버라이드는 없다(gitignored).
57
+ t "E1 워크트리에서도 오버라이드를 찾는다" "clean" "$(run_hook "$B/wt")"
58
+ t "E2 control — 메인 트리 회귀 없음" "clean" "$(run_hook "$B/main")"
59
+ mv "$B/main/.claude/rules/.public-surface-patterns" "$B/main/.claude/rules/.psa.bak"
60
+ t "E1n control — 오버라이드가 진짜 없으면 배너가 뜬다" "banner" "$(run_hook "$B/wt")"
61
+
62
+ # ── E3 · «있지만 무효» (cross-family/codex 지적, 2026-08-18) ─────────────────
63
+ # 🟥 지적: E1/E1n 은 **파일 없음**만 재고, EVIDENCE_ROOT 쪽 파일이 «있지만 비었거나 손상»인
64
+ # 경로를 안 잰다. 실측해 보니 기존 기계가 이미 **소리내며** 처리한다 —
65
+ # 빈 파일 → PSA_OVERRIDE_PRESENT=0 → 커버리지 축소 배너
66
+ # 손상 행 → PSA_BAD_ROWS>0 → «gate INACTIVE/INCOMPLETE»
67
+ # 🟥 그리고 **폴백하지 않는 것이 옳은 방향**이다: 워크트리에 있는 다른 파일로 조용히
68
+ # 갈아타면 «어느 패턴셋으로 쟀는지»가 로그에서 사라진다. 시끄럽게 알리는 쪽을 고정한다.
69
+ printf 'HIGH\tZZ_WT_LOCAL_ZZ\n' > "$B/wt/.claude/rules/.public-surface-patterns" # 워크트리 로컬본
70
+ : > "$B/main/.claude/rules/.public-surface-patterns" # EVIDENCE_ROOT = 빈 파일
71
+ t "E3 EVIDENCE_ROOT 가 비었으면 조용히 폴백하지 않고 알린다" "banner" "$(run_hook "$B/wt")"
72
+ printf 'no-tab-garbage-row\n' > "$B/main/.claude/rules/.public-surface-patterns"
73
+ out3=$( ( cd "$B/wt" && printf 'z\n' > p2.md && git add p2.md && bash templates/.git-hooks/pre-commit ) 2>&1 )
74
+ case "$out3" in *"INACTIVE/INCOMPLETE"*) r=announced ;; *) r=silent ;; esac
75
+ t "E3b 손상 행은 «게이트 무효»로 알린다(조용한 통과 아님)" "announced" "$r"
76
+ rm -rf "$B"
77
+
78
+ echo "----"; echo "evidence-root psa: $pass passed, $fail failed"; [ "$fail" -eq 0 ]
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env bash
2
+ # test_satellite_publish_gate_lanes.sh — known-pair anchor for the satellite's publish gate.
3
+ #
4
+ # WHY (2026-08-18, 원정 2차)
5
+ # frontier_digest_daily.sh 는 FH 자신만 대상으로 지어졌고, FH 의 digest 는 gitignored
6
+ # `tracks/` 에 떨어진다 — 그래서 **공개 노출 문제가 없었다**. 위성이 **공개 레포**를 대상으로
7
+ # 돌면 **매 런이 publish** 다(비가역 표면). §Irreversibility Surface-Class Degrade Invariant:
8
+ # 비가역 표면은 fail-CLOSED — 스캐너가 못 재면 «통과»가 아니라 «보류»다.
9
+ #
10
+ # 🟥 이 스위트의 진짜 주장은 «스캔한다»가 아니라 **«세 값을 두 값으로 접지 않는다»** 다.
11
+ # CLEAN(0) · LEAK(1) · NOT-SCANNED(3) 은 처방이 각각 다르다: 각각 게시 / 격리 / 보류.
12
+ # 접으면 «못 쟀다»가 «깨끗하다»로 렌더된다 — [[feedback_not_found_is_not_zero_family]].
13
+ #
14
+ # Lanes
15
+ # P1 CLEAN → rc=0, 파일 그대로 (게시)
16
+ # P2 LEAK → rc=2, 파일이 .QUARANTINE 로 격리 (게시 안 됨)
17
+ # P3 NOT-SCAN → rc=3, 파일이 .UNSCANNED 로 보류 (게시 안 됨) — 스캐너가 죽어도 fail-closed
18
+ # N1 CONTROL — FD_PUBLIC_TARGET 미설정 → 게이트 자체가 안 돈다 (FH 프로덕션 경로 무변경)
19
+ # N2 CONTROL — P2 와 P3 의 rc 가 **다르다**(2 vs 3). 같으면 두 실패가 한 값으로 접힌 것이다
20
+ # P4 착지 증언이 **실제로 로그에 찍힌다** — 직전 digest 가 없으면 «SKIPPED (not a pass)».
21
+ # 배선했다 ≠ 발화한다. 그리고 SKIP 을 통과로 렌더하지 않는 것까지 주장한다
22
+ #
23
+ # R1 착지 증언이 **살아 있다** — 러너가 부르는 형태로 rc=10(계기 사망)이 아니어야 한다
24
+ # R1n CONTROL — 옛 형태(디렉터리를 타깃으로)는 지금도 rc=10 이다. 고친 게 이거라는 증명
25
+ # R2 공개 대상이면 로그가 **OUT_DIR 밖**이다 / R2n CONTROL — 비공개면 종전 자리 그대로
26
+ # R2b LEAK 로그에 **토큰 원문이 없다**(리댁션) — 유출을 격리하며 같은 자리에 복사하지 않는다
27
+ # R3 같은 날 파일이 둘이고 **뒤엣것이 LEAK** 이면 rc=2 — head -1 이면 CLEAN 이 나온다
28
+ #
29
+ # Exit 0 = 전 레인.
30
+ set -u
31
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
32
+ # 🟥 arm 간 전달 파일을 **고정 `/tmp` 이름**으로 쓰면 안 된다 (보안 패스 2026-08-18, MED·재현됨).
33
+ # `/tmp/.lw_clean` · `/tmp/.lg_leak` 처럼 이름이 6개 전부 고정이라 심볼릭 링크 선점으로
34
+ # 임의 파일이 덮인다(`cp` 가 링크를 따라간다). 게다가 정리 코드가 없어 게이트 **로그 사본**이
35
+ # `/tmp` 에 644 로 영구히 남았다 — 기밀성 축의 정반대 방향이다.
36
+ SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/fh_satlane.XXXXXX")" || exit 1
37
+ trap 'rm -rf "$SCRATCH"' EXIT
38
+ pass=0; fail=0
39
+ t() { if [ "$2" = "$3" ]; then echo " ✅ $1"; pass=$((pass+1)); else echo " ❌ $1 — got [$3] want [$2]"; fail=$((fail+1)); fi }
40
+
41
+ run() { # $1 = digest body · $2 = psa_lib override ("dead") · $3 = arm 이름 → echoes "rc|state"
42
+ local td; td=$(mktemp -d)
43
+ mkdir -p "$td/scripts" "$td/out" "$td/.claude/rules"
44
+ cp "$ROOT/scripts/frontier_digest_daily.sh" "$td/scripts/"
45
+ cp "$ROOT/.claude/rules/.public-surface-patterns.defaults" "$td/.claude/rules/" 2>/dev/null
46
+ # 🟥 오버라이드 파일이 없으면 스캐너는 `override_absent(HIGH operator literals unscanned)` 로
47
+ # **NOT SCANNED** 를 낸다 — MED 히트를 찾고도 «부분 커버리지는 판정이 아니다»라며 거부한다.
48
+ # 엄격하게 옳은 동작이고, 그래서 이 픽스처는 **합성 리터럴**로 자립시킨다: 운영자의 실제
49
+ # 리터럴에 의존하면 이 스위트가 그 파일의 존재 여부에 따라 갈린다.
50
+ printf 'HIGH\tZZ_SYNTHETIC_LEAK_TOKEN_ZZ\n' > "$td/.claude/rules/.public-surface-patterns"
51
+ if [ "$2" = "dead" ]; then printf '#!/usr/bin/env bash\npsa_load(){ :; }\npsa_scan_file(){ return 3; }\n' > "$td/scripts/psa_scan_lib.sh"
52
+ else cp "$ROOT/scripts/psa_scan_lib.sh" "$td/scripts/"; fi
53
+ printf '%s\n' "$1" > "$td/out/frontier_digest_$(date +%Y_%m_%d).md"
54
+ # 러너가 digest_ready 로 이미 있는 파일을 보고 조기 종료하지 않도록, 게이트 경로를 직접 태운다:
55
+ # attempt 루프 진입 전에 skip 하므로 여기서는 러너 본체 대신 게이트 분기만 재현할 수 없다.
56
+ # → 러너를 그대로 돌리되 CLAUDE_BIN 을 no-op 스텁으로 줘서 attempt 를 태운다.
57
+ printf '#!/usr/bin/env bash\nexit 0\n' > "$td/scripts/stub-claude"; chmod +x "$td/scripts/stub-claude"
58
+ local out rc
59
+ # R2 이후 공개 대상의 로그는 OUT_DIR 밖이다 — arm 은 자기 위치를 명시한다(레인이 로그를 읽어야 하므로)
60
+ out=$(cd "$td" && FD_FH_DIR="$td" FD_OUT_DIR="out" FD_PUBLIC_TARGET=1 \
61
+ FD_LOG_DIR="$td/logs" \
62
+ FD_CLAUDE_BIN="$td/scripts/stub-claude" FD_MAX_ATTEMPTS=1 FD_ATTEMPT_TIMEOUT_SECS=20 \
63
+ FD_POLL_SECS=1 bash scripts/frontier_digest_daily.sh 2>&1); rc=$?
64
+ local state=none
65
+ ls "$td/out/"*.QUARANTINE >/dev/null 2>&1 && state=quarantine
66
+ ls "$td/out/"*.UNSCANNED >/dev/null 2>&1 && state=unscanned
67
+ ls "$td/out/"*.md >/dev/null 2>&1 && [ "$state" = none ] && state=published
68
+ # 착지 증언이 로그에 남았는지 (P4 가 읽는다)
69
+ # 🟥 arm 마다 따로 남긴다. 초판은 단일 파일에 덮어써서 **마지막 arm(P3, 스캐너 사망)** 것을
70
+ # 잡았는데, 거기선 게이트가 실패해 증언이 안 도는 게 정상이다 — 레인이 정상 동작을 실패로
71
+ # 렌더할 뻔했다. 판정 대상 arm 을 이름으로 고른다.
72
+ grep -h "landing witness" "$td/logs/"*.log 2>/dev/null | head -1 > "$SCRATCH/lw_$3" || :
73
+ # R2 레인이 읽는다: LEAK arm 의 로그에 토큰 원문이 남았나
74
+ cp "$td/logs/"*.log "$SCRATCH/lg_$3" 2>/dev/null || :
75
+ echo "$rc|$state"
76
+ rm -rf "$td"
77
+ }
78
+
79
+ CLEAN_BODY="# digest
80
+ A clean digest with no operator-private tokens. $(head -c 1200 /dev/zero | tr '\0' 'x')"
81
+ LEAK_BODY="# digest
82
+ this line carries ZZ_SYNTHETIC_LEAK_TOKEN_ZZ which the fixture pattern file flags HIGH. $(head -c 1200 /dev/zero | tr '\0' 'x')"
83
+
84
+ r=$(run "$CLEAN_BODY" live clean); t "P1 CLEAN → 게시" "0|published" "$r"
85
+ r=$(run "$LEAK_BODY" live leak); t "P2 LEAK → 격리(.QUARANTINE)" "2|quarantine" "$r"; P2RC=${r%%|*}
86
+ r=$(run "$CLEAN_BODY" dead dead); t "P3 스캐너 사망 → 보류(.UNSCANNED)" "3|unscanned" "$r"; P3RC=${r%%|*}
87
+
88
+ # N1 — 게이트 미활성(FD_PUBLIC_TARGET 없음)이면 LEAK 본문이어도 게시된다 = FH 경로 무변경
89
+ td=$(mktemp -d); mkdir -p "$td/scripts" "$td/out" "$td/.claude/rules"
90
+ cp "$ROOT/scripts/frontier_digest_daily.sh" "$ROOT/scripts/psa_scan_lib.sh" "$td/scripts/"
91
+ cp "$ROOT/.claude/rules/.public-surface-patterns.defaults" "$td/.claude/rules/" 2>/dev/null
92
+ printf '%s\n' "$LEAK_BODY" > "$td/out/frontier_digest_$(date +%Y_%m_%d).md"
93
+ printf '#!/usr/bin/env bash\nexit 0\n' > "$td/scripts/stub-claude"; chmod +x "$td/scripts/stub-claude"
94
+ out=$(cd "$td" && FD_FH_DIR="$td" FD_OUT_DIR="out" FD_CLAUDE_BIN="$td/scripts/stub-claude" \
95
+ FD_MAX_ATTEMPTS=1 FD_POLL_SECS=1 bash scripts/frontier_digest_daily.sh 2>&1); n1rc=$?
96
+ n1state=published; ls "$td/out/"*.QUARANTINE >/dev/null 2>&1 && n1state=quarantine
97
+ t "N1 control — 게이트 미활성이면 안 돈다(FH 경로 무변경)" "0|published" "$n1rc|$n1state"
98
+ rm -rf "$td"
99
+
100
+ lw=$(cat "$SCRATCH/lw_clean" 2>/dev/null) # 게이트가 CLEAN 인 arm 에서만 증언이 돈다
101
+ case "$lw" in *"landing witness"*) r=logged ;; *) r=absent ;; esac
102
+ t "P4 착지 증언이 로그에 찍힌다(배선했다≠발화한다)" "logged" "$r"
103
+ case "$lw" in *"SKIPPED (not a pass)"*) r=honest ;; *"ALL-LANDED"*) r=honest ;; *"SOME-UNLANDED"*) r=honest ;; *"HARNESS-ERROR"*) r=honest ;; *) r=other ;; esac
104
+ t "P4b SKIP 을 통과로 렌더하지 않는다" "honest" "$r"
105
+
106
+ t "N2 control — LEAK 과 NOT-SCANNED 의 rc 가 다르다" "different" "$([ "${P2RC:-x}" != "${P3RC:-y}" ] && echo different || echo collapsed)"
107
+
108
+ # ── FD_PROFILE 갈래 (2026-08-18) ──────────────────────────────────────────
109
+ # 🟥 이 배선은 **한 번 소실됐다.** forge-wiki 워킹 사본에만 넣었다가 PR 정리 때 rm 했고,
110
+ # 그 사실을 몰라 다음 대상의 약한 산출을 «입장 티어가 낮다»·«스킬을 못 불렀다»로 오진했다.
111
+ # 산출물은 커밋됐는데 그걸 만든 코드가 없던 상태 — 그래서 레인으로 못 박는다.
112
+ _prompt() { FH_DIR="$1" OUT_DIR=out HUMAN_DATE=2026-08-18 TODAY=2026_08_18 PROFILE_PATH="$2" \
113
+ bash -c "$(sed -n '/^_build_prompt()/,/^}/p' "$ROOT/scripts/frontier_digest_daily.sh")
114
+ _build_prompt"; }
115
+ pd=$(mktemp -d); printf 'PROFILE_CANARY_BODY
116
+ ' > "$pd/p.md"
117
+ case "$(_prompt "$pd" "p.md")" in *"PROFILE_CANARY_BODY"*) r=in ;; *) r=missing ;; esac
118
+ t "P5 프로필이 프롬프트에 실제로 실린다" "in" "$r"
119
+ case "$(_prompt "$pd" "p.md")" in *"STANDPOINT"*) r=yes ;; *) r=no ;; esac
120
+ t "P5b 입장 지시(대상 소스를 열어라)가 실린다" "yes" "$r"
121
+ case "$(_prompt "$pd" "")" in *"STANDPOINT"*|*"TARGET HARNESS"*) r=leaked ;; *) r=clean ;; esac
122
+ t "N3 control — 프로필 미설정이면 종전 문자열 그대로(FH 경로 무변경)" "clean" "$r"
123
+ case "$(_prompt "$pd" "nonexistent.md")" in *"STANDPOINT"*) r=leaked ;; *) r=clean ;; esac
124
+ t "N4 control — 프로필 경로가 없으면 조용히 종전 경로(존재하지 않는 파일을 실었다고 안 한다)" "clean" "$r"
125
+ rm -rf "$pd"
126
+
127
+
128
+ # ── R1~R3 (2026-08-18) · 압축 후 수리분 앵커 ───────────────────────────────
129
+ # 🟥 세 결함 다 **내가 같은 날 만들었고**, 셋 다 레인이 없어서 「배선 완료」로 보고됐다.
130
+
131
+ # R2 · 공개 대상이면 로그가 OUT_DIR **밖**이다
132
+ _logloc() { # $1 = FD_PUBLIC_TARGET 값 → echoes inside|outside
133
+ local td; td=$(mktemp -d); mkdir -p "$td/scripts" "$td/out" "$td/.claude/rules"
134
+ cp "$ROOT/scripts/frontier_digest_daily.sh" "$td/scripts/"
135
+ cp "$ROOT/scripts/psa_scan_lib.sh" "$td/scripts/"
136
+ cp "$ROOT/.claude/rules/.public-surface-patterns.defaults" "$td/.claude/rules/" 2>/dev/null
137
+ printf 'HIGH\tZZ_SYNTHETIC_LEAK_TOKEN_ZZ\n' > "$td/.claude/rules/.public-surface-patterns"
138
+ printf '#!/usr/bin/env bash\nexit 0\n' > "$td/scripts/stub-claude"; chmod +x "$td/scripts/stub-claude"
139
+ printf 'clean body — 아무 토큰도 없다\n%s\n' "$(head -c 1200 /dev/zero | tr '\0' 'x')" \
140
+ > "$td/out/frontier_digest_$(date +%Y_%m_%d).md"
141
+ ( cd "$td" && FD_FH_DIR="$td" FD_OUT_DIR="out" FD_PUBLIC_TARGET="$1" \
142
+ FD_CLAUDE_BIN="$td/scripts/stub-claude" FD_MAX_ATTEMPTS=1 FD_POLL_SECS=1 \
143
+ bash scripts/frontier_digest_daily.sh ) >/dev/null 2>&1
144
+ local r=outside
145
+ [ -d "$td/out/logs" ] && r=inside
146
+ echo "$r"; rm -rf "$td"
147
+ }
148
+ t "R2 공개 대상이면 로그가 OUT_DIR 밖에 앉는다" "outside" "$(_logloc 1)"
149
+ t "R2n control — 비공개면 종전 자리 그대로(FH 경로 무변경)" "inside" "$(_logloc 0)"
150
+
151
+ # R2b · LEAK 로그에 토큰 원문이 남지 않는다 (run() 이 "$SCRATCH/lg_leak" 에 복사해 둔다)
152
+ if [ -f "$SCRATCH/lg_leak" ]; then
153
+ if grep -q 'ZZ_SYNTHETIC_LEAK_TOKEN_ZZ' "$SCRATCH/lg_leak"; then r=raw
154
+ elif grep -q 'REDACTED' "$SCRATCH/lg_leak"; then r=redacted
155
+ else r=nofinding; fi
156
+ else r=nolog; fi
157
+ t "R2b LEAK 로그가 토큰 원문을 안 적는다(리댁션)" "redacted" "$r"
158
+ # R2c · 리댁션이 **allowlist 줄 형태**(끝이 `)`)도 가린다 — 초판은 줄 끝 앵커라 놓쳤다
159
+ _red() { bash -c "$(sed -n '/^_redact()/,/^}/p' "$ROOT/scripts/frontier_digest_daily.sh")
160
+ _redact \"\$1\"" _ "$1"; }
161
+ case "$(_red " ⚪ allowlisted — f.md: 'ZZ_TOK_ZZ' (row: f.md :: ZZ_TOK_ZZ)")" in
162
+ *ZZ_TOK_ZZ*) r=raw ;; *REDACTED*) r=redacted ;; *) r=other ;;
163
+ esac
164
+ t "R2c 리댁션이 allowlist 줄 형태도 가린다" "redacted" "$r"
165
+ case "$(_red " ❌ HIGH leak — f.md: 'ZZ_TOK_ZZ'")" in
166
+ *ZZ_TOK_ZZ*) r=raw ;; *REDACTED*) r=redacted ;; *) r=other ;;
167
+ esac
168
+ t "R2c-n control — 종전 형태(줄 끝 인용)도 여전히 가린다" "redacted" "$r"
169
+
170
+ # R2d · **경로 쪽** 토큰 (cross-family/codex F2) — 파일명에 토큰이 들어가면 왼쪽이 남았다
171
+ _leaky_name() {
172
+ local td; td=$(mktemp -d); mkdir -p "$td/scripts" "$td/out" "$td/.claude/rules"
173
+ cp "$ROOT/scripts/frontier_digest_daily.sh" "$ROOT/scripts/psa_scan_lib.sh" "$td/scripts/"
174
+ cp "$ROOT/.claude/rules/.public-surface-patterns.defaults" "$td/.claude/rules/" 2>/dev/null
175
+ printf 'HIGH\tZZ_SYNTHETIC_LEAK_TOKEN_ZZ\n' > "$td/.claude/rules/.public-surface-patterns"
176
+ printf '#!/usr/bin/env bash\nexit 0\n' > "$td/scripts/stub-claude"; chmod +x "$td/scripts/stub-claude"
177
+ local d; d=$(date +%Y_%m_%d)
178
+ # 🟥 파일명 자체에 토큰이 들어간 케이스. 본문에도 넣어야 스캐너가 히트를 낸다.
179
+ printf 'ZZ_SYNTHETIC_LEAK_TOKEN_ZZ\n%s\n' "$(head -c 1200 /dev/zero | tr '\0' 'x')" \
180
+ > "$td/out/frontier_digest_${d}-ZZ_SYNTHETIC_LEAK_TOKEN_ZZ.md"
181
+ ( cd "$td" && FD_FH_DIR="$td" FD_OUT_DIR="out" FD_PUBLIC_TARGET=1 FD_LOG_DIR="$td/logs" \
182
+ FD_CLAUDE_BIN="$td/scripts/stub-claude" FD_MAX_ATTEMPTS=1 FD_POLL_SECS=1 \
183
+ bash scripts/frontier_digest_daily.sh ) >/dev/null 2>&1
184
+ if grep -q 'ZZ_SYNTHETIC_LEAK_TOKEN_ZZ' "$td/logs/"*.log 2>/dev/null; then echo raw
185
+ elif grep -q 'FILENAME REDACTED' "$td/logs/"*.log 2>/dev/null; then echo redacted
186
+ else echo other; fi
187
+ rm -rf "$td"
188
+ }
189
+ t "R2d 파일명에 든 토큰도 로그에 안 남는다" "redacted" "$(_leaky_name)"
190
+
191
+ # R3 · 같은 날 파일이 둘이면 **전부** 스캔한다 (head -1 이면 앞엣것만 보고 CLEAN 을 낸다)
192
+ _two_files() {
193
+ local td; td=$(mktemp -d); mkdir -p "$td/scripts" "$td/out" "$td/.claude/rules"
194
+ cp "$ROOT/scripts/frontier_digest_daily.sh" "$td/scripts/"
195
+ cp "$ROOT/scripts/psa_scan_lib.sh" "$td/scripts/"
196
+ cp "$ROOT/.claude/rules/.public-surface-patterns.defaults" "$td/.claude/rules/" 2>/dev/null
197
+ printf 'HIGH\tZZ_SYNTHETIC_LEAK_TOKEN_ZZ\n' > "$td/.claude/rules/.public-surface-patterns"
198
+ printf '#!/usr/bin/env bash\nexit 0\n' > "$td/scripts/stub-claude"; chmod +x "$td/scripts/stub-claude"
199
+ local d; d=$(date +%Y_%m_%d)
200
+ # 🟥 «먼저 걸리는» 쪽을 깨끗하게 둔다 — 초판(head -1)이면 이쪽만 보고 CLEAN 을 냈다.
201
+ printf 'aaa clean\n%s\n' "$(head -c 1200 /dev/zero | tr '\0' 'x')" > "$td/out/frontier_digest_${d}.md"
202
+ printf 'zzz ZZ_SYNTHETIC_LEAK_TOKEN_ZZ\n%s\n' "$(head -c 1200 /dev/zero | tr '\0' 'x')" \
203
+ > "$td/out/frontier_digest_${d}_b.md"
204
+ local rc
205
+ ( cd "$td" && FD_FH_DIR="$td" FD_OUT_DIR="out" FD_PUBLIC_TARGET=1 FD_LOG_DIR="$td/logs" \
206
+ FD_CLAUDE_BIN="$td/scripts/stub-claude" FD_MAX_ATTEMPTS=1 FD_POLL_SECS=1 \
207
+ bash scripts/frontier_digest_daily.sh ) >/dev/null 2>&1; rc=$?
208
+ local q=no; ls "$td/out/"*_b.md.QUARANTINE >/dev/null 2>&1 && q=yes
209
+ echo "$rc|$q"; rm -rf "$td"
210
+ }
211
+ r=$(_two_files)
212
+ t "R3 둘째 파일의 LEAK 을 놓치지 않는다(rc)" "2" "${r%%|*}"
213
+ t "R3b 둘째 파일이 실제로 격리된다" "yes" "${r##*|}"
214
+
215
+ # R3c · 선택자 자기모순(digest_ready 참인데 게이트가 볼 파일 0건) → **보류**, 게시 아님
216
+ _selector_race() {
217
+ local td; td=$(mktemp -d); mkdir -p "$td/scripts" "$td/out" "$td/.claude/rules"
218
+ cp "$ROOT/scripts/frontier_digest_daily.sh" "$ROOT/scripts/psa_scan_lib.sh" "$td/scripts/"
219
+ cp "$ROOT/.claude/rules/.public-surface-patterns.defaults" "$td/.claude/rules/" 2>/dev/null
220
+ printf 'HIGH\tZZ_SYNTHETIC_LEAK_TOKEN_ZZ\n' > "$td/.claude/rules/.public-surface-patterns"
221
+ # digest_files 를 «참이라 말하고 목록은 비게» 만든다 — 레이스의 최소 재현
222
+ cat > "$td/drv.sh" <<'EOD'
223
+ FD_FH_DIR="$PWD"; export FD_FH_DIR
224
+ eval "$(sed -n '/^publish_gate()/,/^}/p' scripts/frontier_digest_daily.sh)"
225
+ eval "$(sed -n '/^_redact()/,/^}/p' scripts/frontier_digest_daily.sh)"
226
+ digest_files() { :; } # 0건
227
+ FH_DIR="$PWD"; OUT_DIR=out; LOG_FILE=/dev/null; FD_PUBLIC_TARGET=1
228
+ publish_gate; echo "rc=$?"
229
+ EOD
230
+ local o; o=$(cd "$td" && bash drv.sh 2>&1); rm -rf "$td"; echo "${o##*rc=}"
231
+ }
232
+ t "R3c 선택자 자기모순은 게시가 아니라 보류(3)" "3" "$(_selector_race)"
233
+
234
+ # R1 · 착지 증언이 **러너를 통해** 살아 있다
235
+ # 🟥 초판 레인은 checker 를 **직접** 불러서, R1 을 원복해도 초록이었다 — 호출부를 우회한
236
+ # 장식 앵커다([[feedback_anchor_can_be_decorative]]). 되돌림 프로브가 그걸 잡았다.
237
+ # 지금은 **러너를 돌리고 그 로그**를 읽는다. 러너가 어떻게 부르는지가 판정 대상이다.
238
+ _runner_witness() { # echoes verdict-word from the log
239
+ local td; td=$(mktemp -d)
240
+ ( cd "$td" && git init -q . && git config user.email t@t && git config user.name t ) >/dev/null 2>&1
241
+ mkdir -p "$td/scripts" "$td/out" "$td/knowledge"
242
+ cp "$ROOT/scripts/frontier_digest_daily.sh" "$ROOT/scripts/digest_landing_check.sh" \
243
+ "$ROOT/scripts/utterance_landing_check.sh" "$td/scripts/"
244
+ chmod +x "$td/scripts/digest_landing_check.sh"
245
+ printf '#!/usr/bin/env bash\nexit 0\n' > "$td/scripts/stub-claude"; chmod +x "$td/scripts/stub-claude"
246
+ local d; d=$(date +%Y_%m_%d)
247
+ cat > "$td/out/frontier_digest_2001_01_01.md" <<'EOF'
248
+ # prev digest
249
+ ## 📌 FH Immediate Application Candidates
250
+
251
+ | # | 티어 | 내용 |
252
+ |---|---|---|
253
+ | **M1** | **M** | `ZZ_LANDED_TOKEN_ZZ` 를 배선한다 |
254
+ EOF
255
+ sleep 1
256
+ printf 'ZZ_LANDED_TOKEN_ZZ 배선함 · ZZ_CTL_ZZ\n' > "$td/out/landed.md"
257
+ printf 'today body\n%s\n' "$(head -c 1200 /dev/zero | tr '\0' 'x')" > "$td/out/frontier_digest_${d}.md"
258
+ ( cd "$td" && FD_FH_DIR="$td" FD_OUT_DIR="out" FD_LOG_DIR="$td/logs" \
259
+ FD_LANDING_TRACKED="knowledge" FD_LANDING_IGNORED="out" FD_LANDING_CONTROLS="ZZ_CTL_ZZ" \
260
+ FD_CLAUDE_BIN="$td/scripts/stub-claude" FD_MAX_ATTEMPTS=1 FD_POLL_SECS=1 \
261
+ bash scripts/frontier_digest_daily.sh ) >/dev/null 2>&1
262
+ local lw; lw=$(grep -h "landing witness" "$td/logs/"*.log 2>/dev/null | tail -1)
263
+ case "$lw" in
264
+ *HARNESS-ERROR*) echo dead ;;
265
+ *ALL-LANDED*) echo landed ;;
266
+ *SOME-UNLANDED*) echo measured ;;
267
+ *SKIPPED*) echo skipped ;;
268
+ *) echo none ;;
269
+ esac
270
+ rm -rf "$td"
271
+ }
272
+ t "R1 러너가 부르는 형태에서 계기가 살아 있다(HARNESS-ERROR 아님)" "landed" "$(_runner_witness)"
273
+
274
+ # R5 · «직전 다이제스트» 선택이 **표기 혼재**에서도 옳은가 (첫 실사용 발견)
275
+ # 🟥 실물 코퍼스에는 `2026-06-02`(대시)와 `2026_08_17`(언더바)가 **섞여 있다.** 로케일 정렬은
276
+ # 대시본을 뒤로 보내 **두 달 전 파일**을 직전으로 골랐고, 그 파일엔 후보 절이 없어 매 런
277
+ # HARNESS-ERROR 였다. 픽스처가 한 표기만 쓰면 레인 21개가 다 초록인 채로 이게 산다.
278
+ _prev_pick() {
279
+ local td; td=$(mktemp -d); mkdir -p "$td/out"
280
+ # 🟥 **같은 연도**여야 재현된다. 첫 픽스처는 `2020-06-02` 를 썼는데 그건 로케일 정렬에서도
281
+ # 올바르게 뒤로 가서 **버그 판본도 초록**이었다(되돌림 프로브가 잡은 세 번째 장식 앵커).
282
+ # 실물 코퍼스의 표기를 그대로 쓴다: `2026-06-02` vs `2026_08_17`.
283
+ : > "$td/out/frontier_digest_2026-06-02.md" # 옛 대시 표기, 날짜상 더 과거
284
+ : > "$td/out/frontier_digest_2026_08_17.md" # 언더바 표기, 더 최근
285
+ local d; d=$(date +%Y_%m_%d)
286
+ : > "$td/out/frontier_digest_${d}.md" # 오늘 것 (제외 대상)
287
+ # 러너의 선택 표현을 그대로 떼어 쓴다 — 우회하면 앵커가 장식이 된다
288
+ local expr; expr=$(sed -n '/prev=\$(find/,/tail -1)/p' "$ROOT/scripts/frontier_digest_daily.sh")
289
+ local got; got=$( cd "$td" && FH_DIR="$td" OUT_DIR=out TODAY="$d" bash -c "$expr; basename \"\$prev\"" )
290
+ rm -rf "$td"; echo "$got"
291
+ }
292
+ t "R5 표기가 섞여도 «직전»은 날짜상 최신이다" "frontier_digest_2026_08_17.md" "$(_prev_pick)"
293
+
294
+ # R6 · **엔진 루트 ≠ 대상 루트** (첫 실사용 발견, 2026-08-18)
295
+ # 🟥 위성은 이 러너를 복사하지 않고 FH 의 것을 그대로 돈다(launchd 가 FH 경로를 가리킨다).
296
+ # 초판은 스캐너·패턴·checker 를 전부 `$FH_DIR`(=대상 레포)에서 찾아서, 대상에 그 파일이
297
+ # 없으면 **publish gate 가 영영 fail-closed** 였다 — 실측으로 `psa_scan_lib.sh 부재` 를 봤다.
298
+ # 게다가 그 분기는 파일을 **격리하지 않아** «게시 안 함» 이 거짓이었다(digest 는 이미 트리에 있다).
299
+ _engine_vs_target() { # echoes "rc|state"
300
+ local eng tgt; eng=$(mktemp -d); tgt=$(mktemp -d)
301
+ mkdir -p "$eng/scripts" "$eng/.claude/rules" "$tgt/out" # 🟥 대상에는 scripts/ 를 안 만든다
302
+ cp "$ROOT/scripts/frontier_digest_daily.sh" "$ROOT/scripts/psa_scan_lib.sh" "$eng/scripts/"
303
+ cp "$ROOT/.claude/rules/.public-surface-patterns.defaults" "$eng/.claude/rules/" 2>/dev/null
304
+ printf 'HIGH\tZZ_SYNTHETIC_LEAK_TOKEN_ZZ\n' > "$eng/.claude/rules/.public-surface-patterns"
305
+ printf '#!/usr/bin/env bash\nexit 0\n' > "$eng/scripts/stub-claude"; chmod +x "$eng/scripts/stub-claude"
306
+ local d; d=$(date +%Y_%m_%d)
307
+ printf 'clean\n%s\n' "$(head -c 1200 /dev/zero | tr '\0' 'x')" > "$tgt/out/frontier_digest_${d}.md"
308
+ local rc
309
+ ( FD_FH_DIR="$tgt" FD_OUT_DIR="out" FD_PUBLIC_TARGET=1 FD_LOG_DIR="$tgt/logs" \
310
+ FD_CLAUDE_BIN="$eng/scripts/stub-claude" FD_MAX_ATTEMPTS=1 FD_POLL_SECS=1 \
311
+ bash "$eng/scripts/frontier_digest_daily.sh" ) >/dev/null 2>&1; rc=$?
312
+ local state=none
313
+ ls "$tgt/out/"*.UNSCANNED >/dev/null 2>&1 && state=unscanned
314
+ ls "$tgt/out/"*.md >/dev/null 2>&1 && [ "$state" = none ] && state=published
315
+ rm -rf "$eng" "$tgt"; echo "$rc|$state"
316
+ }
317
+ r=$(_engine_vs_target)
318
+ t "R6 도구를 엔진 루트에서 찾는다(대상에 없어도 게이트가 돈다)" "0" "${r%%|*}"
319
+ t "R6b 그래서 CLEAN 한 digest 가 실제로 게시된다" "published" "${r##*|}"
320
+
321
+ # R6c · 스캐너가 **진짜로** 없으면 «게시 안 함» 이 말뿐이 아니라 격리까지 간다
322
+ _lib_absent() {
323
+ local eng tgt; eng=$(mktemp -d); tgt=$(mktemp -d)
324
+ mkdir -p "$eng/scripts" "$tgt/out"
325
+ cp "$ROOT/scripts/frontier_digest_daily.sh" "$eng/scripts/" # 🟥 psa_scan_lib.sh 는 안 넣는다
326
+ printf '#!/usr/bin/env bash\nexit 0\n' > "$eng/scripts/stub-claude"; chmod +x "$eng/scripts/stub-claude"
327
+ local d; d=$(date +%Y_%m_%d)
328
+ printf 'body\n%s\n' "$(head -c 1200 /dev/zero | tr '\0' 'x')" > "$tgt/out/frontier_digest_${d}.md"
329
+ local rc
330
+ ( FD_FH_DIR="$tgt" FD_OUT_DIR="out" FD_PUBLIC_TARGET=1 FD_LOG_DIR="$tgt/logs" \
331
+ FD_CLAUDE_BIN="$eng/scripts/stub-claude" FD_MAX_ATTEMPTS=1 FD_POLL_SECS=1 \
332
+ bash "$eng/scripts/frontier_digest_daily.sh" ) >/dev/null 2>&1; rc=$?
333
+ local state=published
334
+ ls "$tgt/out/"*.UNSCANNED >/dev/null 2>&1 && state=quarantined
335
+ rm -rf "$eng" "$tgt"; echo "$state"
336
+ }
337
+ t "R6c 스캐너 부재는 말만이 아니라 실제 격리다" "quarantined" "$(_lib_absent)"
338
+
339
+ echo "----"; echo "satellite publish gate: $pass passed, $fail failed"; [ "$fail" -eq 0 ]
@@ -18,6 +18,42 @@
18
18
  set -uo pipefail
19
19
 
20
20
  REPO_ROOT=$(git rev-parse --show-toplevel)
21
+ # ── EVIDENCE_ROOT — 증거(tracks/)가 사는 곳. **소스 경로와 다르다.** ──────────────
22
+ # 🟥 WHY: 워크트리에서 `--show-toplevel` 은 **그 워크트리**를 준다. `tracks/` 는 gitignored 라
23
+ # 워크트리에 안 따라오므로, 마커와 매니페스트를 거기서 찾으면 **항상 부재**다. 그래서 게이트가
24
+ # 스스로 권하는 «세션당 worktree» 를 게이트 자신이 봉쇄하고 있었다(2026-08-18 실측: 워크트리
25
+ # FH-자산 커밋 → Axis 2+3 「NOT CONFIRMED」 + Axis 4 「edit_manifest.yaml not found」).
26
+ # 「구조적으로 불가능」이 아니라 **경로 해석 한 줄**이었다.
27
+ # `--git-common-dir` 은 워크트리에서도 **메인 트리의 .git** 을 준다. 메인 트리에서는 `.git` 이라
28
+ # 같은 값이 나온다 ⇒ **하위호환**(3트리 대조 실측: 메인/워크트리/FH 전부 기대대로).
29
+ # ⚠️ 상대 경로로 나올 수 있으므로 절대화한다. 실패하면 REPO_ROOT 로 **degrade** — 증거를 못 찾는
30
+ # 쪽이지 통과시키는 쪽이 아니다(fail-closed 방향 유지).
31
+ #
32
+ # 🟥 **명시된 잔여 — 이 수리가 «두 번째 위험»을 처음 도달 가능하게 만든다** (2026-08-18,
33
+ # 타계열 리뷰 지적, 실측으로 확인):
34
+ # ⓐ 증거 부재 = 이 블록이 닫는 것
35
+ # ⓑ 훅 자기무력화 = **안 닫힌다.** `core.hooksPath` 가 **상대 경로**(`templates/.git-hooks`
36
+ # — FH 문서가 지시하는 바로 그 형태)면 워크트리는 **자기 사본**의 훅을 돌린다.
37
+ # 그 사본을 워크트리 안에서 고치면 그 워크트리의 게이트가 죽고,
38
+ # 메인 트리의 `git status` 에는 안 보인다.
39
+ # 종전엔 ⓐ 가 **사실상의 장벽**이라 ⓑ 가 도달 불가였다. ⓐ 를 없애면 ⓑ 가 **처음으로 실재**한다.
40
+ # ⇒ 워크트리를 쓸 거면 `core.hooksPath` 를 **절대 경로**로 설정해라:
41
+ # git config core.hooksPath "$(git rev-parse --show-toplevel)/templates/.git-hooks"
42
+ # 그러면 모든 워크트리가 **메인 트리의 훅 한 벌**을 돈다.
43
+ # 🟥 정정 (2026-08-18) — 초판은 여기에 «그러면 자기무력화가 **구조적으로 막힌다**» 라고
44
+ # 적었고 그건 **과대주장**이다. 절대 경로가 막는 것은 **훅 파일 자체의 교체**뿐이다:
45
+ # 이 훅은 `$REPO_ROOT/scripts/*` 를 10곳 넘게 **source** 하므로(psa_scan_lib ·
46
+ # branch_claim · regression_guard …), 워크트리에서 그 헬퍼 하나를 고치면 게이트는
47
+ # 그대로 초록으로 통과한다. 「막힌다」가 아니라 **「한 겹 좁아진다」**가 정확하다.
48
+ # 🟥 이 훅은 그것을 **강제하지 않는다** — 자기 자신의 설치 형태를 자기가 검사하는 것은
49
+ # 이미 무력화된 뒤엔 안 돌기 때문이다(검사기가 검사 대상). 문서·설치 안내가 그 층이다.
50
+ _gcd=$(git rev-parse --git-common-dir 2>/dev/null || echo "")
51
+ case "$_gcd" in
52
+ "") EVIDENCE_ROOT="$REPO_ROOT" ;;
53
+ /*) EVIDENCE_ROOT=$(dirname "$_gcd") ;;
54
+ *) EVIDENCE_ROOT=$(dirname "$REPO_ROOT/$_gcd") ;;
55
+ esac
56
+ [ -d "$EVIDENCE_ROOT" ] || EVIDENCE_ROOT="$REPO_ROOT"
21
57
  BRANCH=$(git rev-parse --abbrev-ref HEAD)
22
58
  TODAY=$(date +%Y-%m-%d)
23
59
  BRANCH_SLUG="${BRANCH//\//_}"
@@ -281,8 +317,17 @@ if [ ! -r "$PSA_LIB" ]; then
281
317
  FAILED=1
282
318
  else
283
319
  . "$PSA_LIB"
320
+ # ── R4 (2026-08-18) · 오버라이드는 **EVIDENCE_ROOT** 에서 찾는다 ──────────────
321
+ # `tracks/` 는 `--git-common-dir` 로 옮겼는데 **똑같이 gitignored 인** 이 패턴 파일은
322
+ # `$REPO_ROOT` 에 남아 있었다. 그래서 워크트리 커밋이 매번 **defaults-only** 로 기밀성
323
+ # 스캔을 돌았다(회사명·실명 클래스가 통째로 UNSCANNED, 게다가 비차단 경고라 조용하다).
324
+ # 반쪽-픽스의 전형이다([[feedback_half_fix_propagation_boundary]]).
325
+ # 🟥 `defaults` 는 **tracked** 라 `REPO_ROOT` 가 맞다 — 같이 옮기면 안 된다. 옮기는 것은
326
+ # gitignored 인 오버라이드 쪽 하나뿐이고, 없으면 REPO_ROOT 로 폴백한다.
327
+ PSA_OVR="$EVIDENCE_ROOT/.claude/rules/.public-surface-patterns"
328
+ [ -r "$PSA_OVR" ] || PSA_OVR="$REPO_ROOT/.claude/rules/.public-surface-patterns"
284
329
  psa_load "$REPO_ROOT/.claude/rules/.public-surface-patterns.defaults" \
285
- "${PSA_PATTERNS:-$REPO_ROOT/.claude/rules/.public-surface-patterns}"
330
+ "${PSA_PATTERNS:-$PSA_OVR}"
286
331
  if [ "$PSA_OVERRIDE_PRESENT" -eq 0 ]; then
287
332
  # 2026-08-16 weekly-audit residual — operator decision: LOUDER WARNING, not fail-closed.
288
333
  # A hard block here would fail every fresh clone's / CI's / worktree's first commit (the
@@ -305,7 +350,7 @@ else
305
350
  if [ "${PUBLIC_SURFACE_OK:-0}" = "1" ]; then
306
351
  echo " ⚠️ proceeding with an incomplete gate by PUBLIC_SURFACE_OK=1 (conscious)"
307
352
  echo "$(date +%Y-%m-%dT%H:%M:%S) PUBLIC_SURFACE_OK override — branch $BRANCH — gate inactive/incomplete" \
308
- >> "$REPO_ROOT/tracks/_meta/.psa_override_log" 2>/dev/null || true
353
+ >> "$EVIDENCE_ROOT/tracks/_meta/.psa_override_log" 2>/dev/null || true
309
354
  else
310
355
  FAILED=1
311
356
  fi
@@ -342,7 +387,7 @@ else
342
387
  if [ "${PUBLIC_SURFACE_OK:-0}" = "1" ]; then
343
388
  echo " ⚠️ public-surface hit(s) allowed by PUBLIC_SURFACE_OK=1 (conscious, reviewed intent)"
344
389
  echo "$(date +%Y-%m-%dT%H:%M:%S) PUBLIC_SURFACE_OK override — branch $BRANCH — review suppressed hit(s) above" \
345
- >> "$REPO_ROOT/tracks/_meta/.psa_override_log" 2>/dev/null || true
390
+ >> "$EVIDENCE_ROOT/tracks/_meta/.psa_override_log" 2>/dev/null || true
346
391
  else
347
392
  echo " Operator-private token reached the public surface. Generalize it (companion-store name"
348
393
  echo " → 'a private companion store'; corp-context → 'restricted/corp env'; absolute home path"
@@ -1243,7 +1288,7 @@ validate_marker_floor() {
1243
1288
  # ── Axes 2+3 — steel-quench + phantom-quench (full gate only) ─────────
1244
1289
  if [ "$GATE_MODE" = "full" ]; then
1245
1290
  echo "[Axis 2+3] Adversarial + Source-Grounding..."
1246
- MARKER_DIR="$REPO_ROOT/tracks/_meta"
1291
+ MARKER_DIR="$EVIDENCE_ROOT/tracks/_meta"
1247
1292
  MARKER="$MARKER_DIR/.axes_23_passed_${BRANCH_SLUG}_${TODAY}.marker"
1248
1293
 
1249
1294
  if [ -f "$MARKER" ]; then
@@ -1270,7 +1315,7 @@ fi
1270
1315
 
1271
1316
  # ── Axis 4 — Edit Manifest entry (always required) ────────────────────────────
1272
1317
  echo "[Axis 4] Edit Manifest..."
1273
- MANIFEST="$REPO_ROOT/tracks/_meta/edit_manifest.yaml"
1318
+ MANIFEST="$EVIDENCE_ROOT/tracks/_meta/edit_manifest.yaml"
1274
1319
  if [ ! -f "$MANIFEST" ]; then
1275
1320
  echo " ❌ FAIL — tracks/_meta/edit_manifest.yaml not found"
1276
1321
  echo " Run /edit-manifest RECORD or create the file manually."
@@ -1311,6 +1356,19 @@ for i, entry in enumerate(doc):
1311
1356
  if not isinstance(entry, dict):
1312
1357
  print("entry #%d is a %s, not a mapping — a list item may have drifted outside its parent"
1313
1358
  % (i, type(entry).__name__)); sys.exit(1)
1359
+ # 🟥 인터리브 지문 — 동시 append 는 **YAML 을 깨지 않고** 내용을 뒤섞는다.
1360
+ # 2026-08-18 실측(적대적 재현): 워커 4개가 한 엔트리를 여러 write 로 쪼개 append 하면
1361
+ # 파싱은 OK 인데 필드가 서로 섞이고(w3 의 branch 에 w2 의 impact) 나머지는 `date` 만 남는다.
1362
+ # ⇒ **형식 검사가 통과시키는 무음 손상.** 이 축이 없애려는 병을 이 축 자신이 통과시키고 있었다.
1363
+ # 지문은 스키마가 아니다 — 실물 407 엔트리의 키 분포가 느슨해서(date 100% · branch 38%)
1364
+ # 필수키를 걸면 과거를 과차단한다. known-pair 로 고른 지문은 **«date 만 있는 엔트리»**:
1365
+ # known-negative 실물 407건 → 0건 · known-positive 재현본 → 3/5.
1366
+ # 워크트리 병렬 커밋이 열리기 전에는 이 조건 자체가 안 생겼다 — #448 이 문을 여니 같이 닫는다.
1367
+ orphan = [i for i, e in enumerate(doc) if set(e.keys()) == {"date"}]
1368
+ if orphan:
1369
+ print("entry #%s has only `date` — concurrent-append interleaving signature "
1370
+ "(fields lost/mixed). Re-check the last appended entries by hand." % orphan[0])
1371
+ sys.exit(1)
1314
1372
  def norm(v):
1315
1373
  return v.isoformat() if hasattr(v, "isoformat") else str(v).strip()
1316
1374
  if not any(norm(e.get("date")) == sys.argv[2] for e in doc):
@@ -1512,7 +1570,7 @@ if [ -n "$LOADBEARING" ]; then
1512
1570
  else
1513
1571
  echo " ⚠️ degrade lint unavailable (scripts/degrade_direction_scan.sh missing) — advisory leg skipped"
1514
1572
  fi
1515
- MARKER_LB="$REPO_ROOT/tracks/_meta/.axes_23_passed_${BRANCH_SLUG}_${TODAY}.marker"
1573
+ MARKER_LB="$EVIDENCE_ROOT/tracks/_meta/.axes_23_passed_${BRANCH_SLUG}_${TODAY}.marker"
1516
1574
  if validate_crossfamily_leg "$MARKER_LB"; then :; else FAILED=1; fi
1517
1575
  if validate_standpoint_leg "$MARKER_LB"; then :; else FAILED=1; fi
1518
1576
  if validate_thirdparty_leg "$MARKER_LB"; then :; else FAILED=1; fi