@chrono-meta/fh-gate 1.4.89 → 1.4.90
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/judgment_circuits.txt +14 -0
- package/.claude/rules/fh_4axis_gate.md +7 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +25 -0
- package/CLAUDE.md +179 -12
- package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +105 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +165 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +100 -0
- package/knowledge/shared/harness-core/onboarding_acceleration_autopilot.md +3 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +181 -13
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +528 -0
- package/package.json +16 -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/skills/auto-decorrelation/SKILL.md +56 -8
- package/plugins/fh-meta/skills/install-wizard/SKILL.md +33 -0
- package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +104 -15
- package/scripts/chamber_run.sh +64 -2
- package/scripts/chamber_witness.sh +439 -0
- package/scripts/compaction_probe.sh +456 -0
- package/scripts/digest_landing_check.sh +385 -0
- package/scripts/directional_diff_gate.sh +459 -0
- package/scripts/judgment_circuit_lint.sh +239 -0
- package/scripts/novelty_claim_check.sh +193 -0
- package/scripts/relay_channel.sh +645 -0
- package/scripts/reviewer_capability_corpus.tsv +124 -0
- package/scripts/selfcheck.sh +66 -0
- package/scripts/test_marker_crossfamily_lanes.sh +132 -0
- package/scripts/test_marker_floor_lanes.sh +9 -8
- package/scripts/test_relay_channel_lanes.sh +583 -0
- package/scripts/test_reviewer_capability_conformance.sh +173 -0
- package/scripts/test_wizard_snippet_merge_lanes.sh +104 -11
- package/scripts/utterance_landing_check.sh +209 -0
- package/templates/.git-hooks/pre-commit +253 -13
- package/templates/settings.Compaction.snippet.json +56 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# chamber_witness.sh — 챔버 런의 **순서 증인**. 내용은 비공개로 두고 순서만 증언한다.
|
|
3
|
+
#
|
|
4
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
# 무엇을 푸는가
|
|
6
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
# `ship_readiness_gate.md §② promotion criteria` 의 P1 은 이렇게 적혀 있었다:
|
|
8
|
+
#
|
|
9
|
+
# "An EMIT run leaves an ordering record that does not depend on trusting the
|
|
10
|
+
# author — the intent/budget/sim record committed, hashed, or otherwise
|
|
11
|
+
# witnessed OUTSIDE the gitignored workspace, before the verdict"
|
|
12
|
+
# 상태: **not buildable today** — no such channel exists for `tracks/**`
|
|
13
|
+
#
|
|
14
|
+
# 못 짓는다고 적힌 이유는 `tracks/**` 가 `.gitignore` 로 통째 무시라 챔버
|
|
15
|
+
# 아티팩트가 버전관리 밖이고, 남는 순서 증거가 **mtime 뿐인데 위조가 자명**하다는
|
|
16
|
+
# 것이었다. 그래서 *"게이트가 결과를 알기 전에 돌았는가"* 를 **정직한 런조차
|
|
17
|
+
# 증명할 수 없었다.**
|
|
18
|
+
#
|
|
19
|
+
# 이 스크립트가 여는 채널은 P1 문구가 이미 허용한 두 번째 형태다 — **hashed**.
|
|
20
|
+
# 내용(INTENT/BUDGET/SIM_NOTES)은 비공개 워크스페이스에 그대로 두고,
|
|
21
|
+
# **SHA-256 만** 추적되는 원장에 적어 커밋한다. 그러면:
|
|
22
|
+
#
|
|
23
|
+
# · 본문 유출 0 — 해시는 내용을 복원하지 못한다(비공개 유지 요구 충족)
|
|
24
|
+
# · 순서 증인 O — 커밋 그래프가 "이 해시가 verdict 해시보다 먼저 들어왔다"를 증언
|
|
25
|
+
#
|
|
26
|
+
# ⚠️ **"유출 0" 이라고 쓰면 과장이다**(cross-family F9). 원장에는 `run` slug · 아티팩트
|
|
27
|
+
# 파일명 · 기록 시각 · 해시가 남는다 — 즉 *"어떤 이름의 런이 언제 돌았는가"* 는 공개된다.
|
|
28
|
+
# 그리고 INTENT/BUDGET 템플릿처럼 **저엔트로피 아티팩트는 SHA-256 도 사전 공격 대상**이다
|
|
29
|
+
# (후보 문자열을 만들어 해시를 맞춰보면 내용을 알아낼 수 있다). 숨는 것은 **본문**이지
|
|
30
|
+
# 런의 존재나 형식이 아니다.
|
|
31
|
+
#
|
|
32
|
+
# ★ 이 증인이 **실제로** 성립하는 조건(이 레포 한정):
|
|
33
|
+
# `main` 에 `non_fast_forward` ruleset 이 active 라 **push 된 히스토리를 되쓸 수
|
|
34
|
+
# 없다**. 즉 커밋 순서는 저자가 사후에 바꿀 수 없다. ruleset 이 꺼지면 이 스크립트의
|
|
35
|
+
# 증인력도 같이 약해진다 — 배선이 아니라 **전제**라서 여기 적는다.
|
|
36
|
+
#
|
|
37
|
+
# ⚠️ **그 전제를 이 스크립트가 검사하지는 않는다**(cross-family F7). `verify` 가 보는 것은
|
|
38
|
+
# **로컬 커밋 그래프**뿐이다: 브랜치가 `main` 인지 · 그 커밋이 push 됐는지 · ruleset 이
|
|
39
|
+
# 지금도 켜져 있는지 · ancestry 가 맞는지를 **확인하지 않는다.** 로컬에만 있는 커밋은
|
|
40
|
+
# 여전히 되쓸 수 있으므로, `WITNESSED` 는 *"로컬 이력상 먼저였다"* 까지가 정확한 뜻이고
|
|
41
|
+
# 되쓰기 불가는 **push 이후에** 서버 설정이 준다. 두 문장을 붙여 읽지 마라.
|
|
42
|
+
#
|
|
43
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
# 이 계기가 증명하지 않는 것 (과잉주장 금지 — 명명된 잔여)
|
|
45
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
# 커밋먼트는 **"그 시점에 이 내용이 고정됐다"** 를 증명한다.
|
|
47
|
+
# **"정직하게 썼다"** 는 증명하지 않는다. 저자는 여전히 결론을 먼저 정하고 INTENT 를
|
|
48
|
+
# 그에 맞춰 쓴 뒤 커밋할 수 있다. 그것은 이 채널이 겨누는 실패가 아니다 —
|
|
49
|
+
# 겨누는 것은 **사후 재작성**(verdict 를 보고 나서 INTENT 를 고쳐 "screened" 라고
|
|
50
|
+
# 주장하는 것)이고, 그건 닫힌다.
|
|
51
|
+
#
|
|
52
|
+
# 두 번째 잔여: 원장 자체가 공개 표면이라 **run slug 이 노출**된다. restricted-env·비공개
|
|
53
|
+
# 후보를 챔버에 넣을 때는 slug 을 익명화해서 넣어라(`--anon` 없음 — 호출자가
|
|
54
|
+
# 익명 slug 을 그냥 넘기면 된다). pre-commit 기밀 스캔이 뒤를 받치지만 그건
|
|
55
|
+
# 패턴 denylist 지 보편 스캐너가 아니다.
|
|
56
|
+
#
|
|
57
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
# 사용법
|
|
59
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
# chamber_witness.sh record <slug> <artifact-path> # 해시를 원장에 append
|
|
61
|
+
# chamber_witness.sh verify <slug> # 순서/무결 판정
|
|
62
|
+
# chamber_witness.sh --self-test # known-pair 캘리브레이션
|
|
63
|
+
#
|
|
64
|
+
# verify 판정 (4-state — `not found ≠ 0` 을 지킨다):
|
|
65
|
+
# WITNESSED verdict 보다 먼저 커밋된 pre-verdict 아티팩트가 1건 이상 있다
|
|
66
|
+
# UNWITNESSED 원장에 기록이 없다 (= 미측정. 위반이 아니다)
|
|
67
|
+
# PENDING 기록은 있으나 아직 커밋 안 됨 (증인 미성립 — 아직은 아니다)
|
|
68
|
+
# TAMPERED 기록된 해시와 현재 파일이 불일치
|
|
69
|
+
# UNORDERED verdict 가 pre-verdict 아티팩트보다 먼저 커밋됐다
|
|
70
|
+
#
|
|
71
|
+
# exit: 0 WITNESSED · 1 UNORDERED/TAMPERED · 2 UNWITNESSED/PENDING · 10 입력·환경 불량
|
|
72
|
+
# ⚠️ **2 는 통과가 아니다.** 호출자는 0 만 통과로 읽어라. 2 를 통과로 읽으면
|
|
73
|
+
# "기록이 없다" 가 "문제 없다" 로 렌더되고, 그게 이 파일이 막으려는 바로 그 결함이다.
|
|
74
|
+
|
|
75
|
+
set -u
|
|
76
|
+
|
|
77
|
+
REPO_ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
78
|
+
LEDGER_REL="knowledge/shared/learnings/chamber_ordering_witness.yaml"
|
|
79
|
+
LEDGER="$REPO_ROOT/$LEDGER_REL"
|
|
80
|
+
|
|
81
|
+
# verdict 파일명 — 이 이름만 "결과" 로 취급하고 나머지는 pre-verdict 로 본다.
|
|
82
|
+
VERDICT_NAME="EMISSION_VERDICT.md"
|
|
83
|
+
|
|
84
|
+
_sha() { # $1=path → sha256 hex, 실패 시 빈 문자열
|
|
85
|
+
[ -f "$1" ] || { echo ""; return 1; }
|
|
86
|
+
if command -v shasum >/dev/null 2>&1; then
|
|
87
|
+
shasum -a 256 "$1" 2>/dev/null | awk '{print $1}'
|
|
88
|
+
elif command -v sha256sum >/dev/null 2>&1; then
|
|
89
|
+
sha256sum "$1" 2>/dev/null | awk '{print $1}'
|
|
90
|
+
else
|
|
91
|
+
echo ""; return 1
|
|
92
|
+
fi
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
# record — 해시를 원장에 append
|
|
97
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
do_record() {
|
|
99
|
+
local slug="$1" art="$2"
|
|
100
|
+
[ -n "$slug" ] && [ -n "$art" ] || { echo "❌ usage: record <slug> <artifact-path>" >&2; return 10; }
|
|
101
|
+
if [ ! -f "$art" ]; then
|
|
102
|
+
echo "❌ 아티팩트 없음: $art" >&2; return 10
|
|
103
|
+
fi
|
|
104
|
+
local h; h="$(_sha "$art")"
|
|
105
|
+
if [ -z "$h" ]; then
|
|
106
|
+
# 해시 도구가 없으면 **조용히 넘어가지 않는다** — 증인 채널이 죽은 것이다.
|
|
107
|
+
echo "❌ sha256 도구 없음 (shasum/sha256sum) — 증인 기록 불가" >&2; return 10
|
|
108
|
+
fi
|
|
109
|
+
local base; base="$(basename "$art")"
|
|
110
|
+
# ⚠️ 원장은 공백 구분 positional 로 파싱된다. 공백·개행·콜론이 든 slug/파일명을 그대로
|
|
111
|
+
# 박으면 **기록은 되고 파싱은 깨진다** — 즉 증인이 조용히 사라진다(cross-family F10).
|
|
112
|
+
# 그래서 여기서 fail-closed 로 막는다. 러너는 이미 slug charset 을 제한하지만
|
|
113
|
+
# 이 스크립트는 직접 호출도 가능하므로 러너의 검증에 기대지 않는다.
|
|
114
|
+
case "$slug" in
|
|
115
|
+
""|*[!A-Za-z0-9._-]*) echo "❌ slug 문자셋 불량 '$slug' (허용: 영숫자 . _ -)" >&2; return 10 ;;
|
|
116
|
+
esac
|
|
117
|
+
case "$base" in
|
|
118
|
+
""|*[!A-Za-z0-9._-]*) echo "❌ 아티팩트 파일명 불량 '$base' (허용: 영숫자 . _ -)" >&2; return 10 ;;
|
|
119
|
+
esac
|
|
120
|
+
# ⚠️ mkdir/쓰기 실패를 **검사한다.** 초판은 `2>/dev/null` 로 삼키고 그대로 "witnessed" 를
|
|
121
|
+
# 인쇄해 rc=0 을 냈다 — cross-family 가 `CLAUDE_PROJECT_DIR` 를 일반 파일로 걸어 실제로
|
|
122
|
+
# 재현했다(원장 경로가 "Not a directory" 인데 witnessed + rc=0). 증인 채널이 아무것도
|
|
123
|
+
# 기록하지 못한 채 성공을 보고하는 것은 이 파일이 막으려는 바로 그 형태다.
|
|
124
|
+
if ! mkdir -p "$(dirname "$LEDGER")" 2>/dev/null; then
|
|
125
|
+
echo "❌ 원장 디렉토리 생성 실패: $(dirname "$LEDGER")" >&2; return 10
|
|
126
|
+
fi
|
|
127
|
+
if [ ! -f "$LEDGER" ]; then
|
|
128
|
+
if ! cat > "$LEDGER" 2>/dev/null <<'HDR'
|
|
129
|
+
# Chamber ordering witness — HASHES ONLY, never content.
|
|
130
|
+
#
|
|
131
|
+
# 이 파일은 챔버 런 아티팩트의 **순서 증인**이다. 내용은 gitignored
|
|
132
|
+
# `tracks/_chamber/<slug>/` 에 남고 여기에는 SHA-256 만 들어온다.
|
|
133
|
+
# 커밋 그래프가 "무엇이 verdict 보다 먼저 고정됐는가" 를 증언한다.
|
|
134
|
+
#
|
|
135
|
+
# 생성/검증: scripts/chamber_witness.sh (record | verify | --self-test)
|
|
136
|
+
# ⚠️ 손으로 고치지 마라 — 손편집은 증인을 무효화한다(해시 불일치 = TAMPERED).
|
|
137
|
+
# ⚠️ 비공개 후보는 slug 을 익명화해서 넣어라. 이 파일은 공개 표면이다.
|
|
138
|
+
HDR
|
|
139
|
+
then
|
|
140
|
+
echo "❌ 원장 헤더 쓰기 실패: $LEDGER" >&2; return 10
|
|
141
|
+
fi
|
|
142
|
+
fi
|
|
143
|
+
if ! printf -- '- run: %s\n artifact: %s\n sha256: %s\n recorded: %s\n' \
|
|
144
|
+
"$slug" "$base" "$h" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$LEDGER" 2>/dev/null; then
|
|
145
|
+
echo "❌ 원장 append 실패: $LEDGER" >&2; return 10
|
|
146
|
+
fi
|
|
147
|
+
# **쓴 것을 다시 읽어 확인한다.** append 가 rc=0 이어도 파일에 없으면 증인은 없다.
|
|
148
|
+
if ! grep -q " sha256: $h" "$LEDGER" 2>/dev/null; then
|
|
149
|
+
echo "❌ 원장에 기록이 확인되지 않는다 (쓰기는 성공했다고 보고됨): $LEDGER" >&2; return 10
|
|
150
|
+
fi
|
|
151
|
+
echo "witnessed: $slug/$base sha256=${h%"${h#??????????}"}…"
|
|
152
|
+
return 0
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# 이 (run, artifact, sha) **엔트리**가 처음 커밋된 시각(epoch). 미커밋이면 빈 문자열.
|
|
156
|
+
#
|
|
157
|
+
# ⚠️ 초판은 `git log -S "$sha"` 로 **해시 문자열**만 찾았다. 해시는 내용 기반이라
|
|
158
|
+
# 서로 다른 run 이 같은 내용(템플릿 INTENT 등)을 기록하면 **같은 sha** 가 되고, 과거 run 의
|
|
159
|
+
# 커밋이 현재 run 의 **미커밋** 엔트리를 커밋된 것처럼 보이게 했다 — cross-family 가
|
|
160
|
+
# 실제로 재현(dup_pending_rc=0). 그래서 커밋된 원장을 실제로 열어 **세 값의 조합**을 본다.
|
|
161
|
+
_commit_ts_of_entry() { # $1=slug $2=artifact $3=sha
|
|
162
|
+
local slug="$1" art="$2" sha="$3" h ts
|
|
163
|
+
git -C "$REPO_ROOT" log --format='%H %ct' --reverse -- "$LEDGER_REL" 2>/dev/null |
|
|
164
|
+
while read -r h ts; do
|
|
165
|
+
if git -C "$REPO_ROOT" show "$h:$LEDGER_REL" 2>/dev/null |
|
|
166
|
+
awk -v s="$slug" -v a="$art" -v x="$sha" '
|
|
167
|
+
/^- run:/ { cur=$3; art=""; next }
|
|
168
|
+
/^ artifact:/ { art=$2; next }
|
|
169
|
+
/^ sha256:/ { if (cur==s && art==a && $2==x) { found=1; exit } }
|
|
170
|
+
END { exit (found?0:1) }' ; then
|
|
171
|
+
printf '%s' "$ts"; break # 파이프 서브셸이라 `return` 은 함수를 빠져나가지 못한다
|
|
172
|
+
fi
|
|
173
|
+
done
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
177
|
+
# verify — 순서/무결 판정
|
|
178
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
179
|
+
do_verify() {
|
|
180
|
+
# ⚠️ 한 `local` 문에서 앞 변수를 뒤 기본값이 참조하면 `set -u` 하에서 unbound 로 죽는다.
|
|
181
|
+
# self-test 가 항상 2인자로 불러서 **1인자 경로를 한 번도 안 덮었고**, 첫 실사용에서
|
|
182
|
+
# 터졌다. 아래 N7 레인이 그 경로를 고정한다.
|
|
183
|
+
local slug="$1"
|
|
184
|
+
local wsdir="${2:-}"
|
|
185
|
+
[ -n "$wsdir" ] || wsdir="$REPO_ROOT/tracks/_chamber/$slug"
|
|
186
|
+
[ -n "$slug" ] || { echo "❌ usage: verify <slug> [workspace-dir]" >&2; return 10; }
|
|
187
|
+
if [ ! -f "$LEDGER" ]; then
|
|
188
|
+
echo "UNWITNESSED — 원장 자체가 없다 ($LEDGER_REL). 미측정이지 위반이 아니다."
|
|
189
|
+
return 2
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
# 이 slug 의 엔트리 수집 (bash 3.2 호환: 배열 대신 임시파일)
|
|
193
|
+
local tmp; tmp="$(mktemp -t chwit.XXXXXX 2>/dev/null)" || { echo "❌ mktemp 실패" >&2; return 10; }
|
|
194
|
+
awk -v s="$slug" '
|
|
195
|
+
/^- run:/ { cur=$3; art=""; sha="" ; next }
|
|
196
|
+
/^ artifact:/ { art=$2; next }
|
|
197
|
+
/^ sha256:/ { sha=$2; if (cur==s && art!="" && sha!="") print art"\t"sha; next }
|
|
198
|
+
' "$LEDGER" > "$tmp" 2>/dev/null
|
|
199
|
+
|
|
200
|
+
if [ ! -s "$tmp" ]; then
|
|
201
|
+
rm -f "$tmp"
|
|
202
|
+
echo "UNWITNESSED — 원장에 '$slug' 기록이 없다. 미측정이지 위반이 아니다."
|
|
203
|
+
return 2
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
# 판정 상태. cross-family 지적 F1·F3·F4·F6 을 반영해 재작성했다.
|
|
207
|
+
local tampered=0 pending=0 unverif=0 verdict_ts="" pre_max_ts="" pre_count=0 art sha ts cur
|
|
208
|
+
local have_intent=0 have_budget=0 have_sim=0
|
|
209
|
+
while IFS=$'\t' read -r art sha; do
|
|
210
|
+
[ -n "$art" ] || continue
|
|
211
|
+
# ① 무결 — 기록된 해시와 현재 파일이 같은가.
|
|
212
|
+
if [ -f "$wsdir/$art" ]; then
|
|
213
|
+
cur="$(_sha "$wsdir/$art")"
|
|
214
|
+
if [ -z "$cur" ]; then
|
|
215
|
+
# 해시 도구 장애를 "변조 아님" 으로 읽으면 fail-open 이다(F6). 비가역 주장을
|
|
216
|
+
# 받치는 계기이므로 판정을 포기하지 **정상으로 넘기지 않는다.**
|
|
217
|
+
echo " ❌ 해시 재계산 실패: $art — 무결 판정 불가 (계기 장애)" >&2
|
|
218
|
+
rm -f "$tmp"; return 10
|
|
219
|
+
fi
|
|
220
|
+
if [ "$cur" != "$sha" ]; then
|
|
221
|
+
echo " 🟥 TAMPERED: $art — 기록된 해시와 현재 파일이 다르다"
|
|
222
|
+
tampered=1
|
|
223
|
+
fi
|
|
224
|
+
else
|
|
225
|
+
# 파일 부재는 변조가 아니다(다른 머신에서 검증할 수 있다). 다만 **무결을 못 봤다는
|
|
226
|
+
# 사실을 말한다** — 침묵하면 "검증됨" 으로 읽힌다.
|
|
227
|
+
unverif=$((unverif + 1))
|
|
228
|
+
echo " 🟡 무결 미검증: $art — 워크스페이스에 파일이 없다(순서만 판정)"
|
|
229
|
+
fi
|
|
230
|
+
# ② 이 엔트리가 커밋됐는가 (sha 문자열이 아니라 run+artifact+sha 조합으로 — F5)
|
|
231
|
+
ts="$(_commit_ts_of_entry "$slug" "$art" "$sha")"
|
|
232
|
+
if [ -z "$ts" ]; then
|
|
233
|
+
echo " 🟡 PENDING: $art — 원장에 적혔지만 아직 커밋 안 됨 (증인 미성립)"
|
|
234
|
+
pending=1
|
|
235
|
+
continue
|
|
236
|
+
fi
|
|
237
|
+
if [ "$art" = "$VERDICT_NAME" ]; then
|
|
238
|
+
verdict_ts="$ts"
|
|
239
|
+
else
|
|
240
|
+
pre_count=$((pre_count + 1))
|
|
241
|
+
# ⚠️ **최댓값**을 쓴다. 초판은 최솟값을 써서 "아무 pre 하나만 verdict 보다 앞서면"
|
|
242
|
+
# 통과했다 — INTENT 만 먼저 커밋하고 BUDGET/SIM_NOTES 를 verdict 뒤에 커밋해도
|
|
243
|
+
# WITNESSED 였다(F4, cross-family 가 mixed_rc=0 으로 재현).
|
|
244
|
+
if [ -z "$pre_max_ts" ] || [ "$ts" -gt "$pre_max_ts" ]; then pre_max_ts="$ts"; fi
|
|
245
|
+
case "$art" in
|
|
246
|
+
INTENT.md) have_intent=1 ;;
|
|
247
|
+
BUDGET.md) have_budget=1 ;;
|
|
248
|
+
SIM_NOTES.md) have_sim=1 ;;
|
|
249
|
+
esac
|
|
250
|
+
fi
|
|
251
|
+
done < "$tmp"
|
|
252
|
+
rm -f "$tmp"
|
|
253
|
+
|
|
254
|
+
[ "$tampered" -eq 1 ] && { echo "TAMPERED — 증인 무효."; return 1; }
|
|
255
|
+
|
|
256
|
+
if [ "$pending" -eq 1 ]; then
|
|
257
|
+
echo "PENDING — 원장에 적혔으나 커밋되지 않은 엔트리가 있다. 아직 증인이 아니다."
|
|
258
|
+
return 2
|
|
259
|
+
fi
|
|
260
|
+
|
|
261
|
+
if [ "$pre_count" -eq 0 ]; then
|
|
262
|
+
echo "UNWITNESSED — pre-verdict 아티팩트 기록이 없다. 미측정."
|
|
263
|
+
return 2
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
# ★ verdict 미기록은 **통과가 아니다**(F1). 초판은 여기서 rc=0 을 냈고, 러너가 그걸
|
|
267
|
+
# "② 승격 근거로 사용 가능" 으로 렌더했다 — 증인 채널이 증인 없이 초록을 낸 것이다.
|
|
268
|
+
if [ -z "$verdict_ts" ]; then
|
|
269
|
+
echo "INCOMPLETE — pre-verdict $pre_count 건은 고정됐으나 **verdict 해시가 원장에 없다**."
|
|
270
|
+
echo " 순서를 비교할 대상이 없으므로 증인이 성립하지 않는다. verdict 를 record 후 재실행."
|
|
271
|
+
return 2
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# 챔버는 세 게이트를 모두 거친다. 하나라도 증인이 없으면 "게이트들이 결과 전에 돌았다" 를
|
|
275
|
+
# 주장할 수 없다 — 부분 증인을 완전 증인으로 렌더하지 않는다(F4).
|
|
276
|
+
if [ "$have_intent" -eq 0 ] || [ "$have_budget" -eq 0 ] || [ "$have_sim" -eq 0 ]; then
|
|
277
|
+
echo "INCOMPLETE — 게이트 아티팩트 증인이 일부만 있다"
|
|
278
|
+
echo " INTENT=$have_intent BUDGET=$have_budget SIM_NOTES=$have_sim (셋 다 필요)"
|
|
279
|
+
return 2
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
# ⚠️ `-le` 다. 같은 커밋(또는 같은 초)에 들어온 verdict 는 "먼저" 를 증명하지 못한다 —
|
|
283
|
+
# 초판은 `-lt` 만 봐서 same-second 역순을 통과시켰다(F3, same_commit_rc=0 으로 재현).
|
|
284
|
+
if [ "$verdict_ts" -le "$pre_max_ts" ]; then
|
|
285
|
+
echo "UNORDERED — verdict 가 pre-verdict 아티팩트보다 먼저이거나 같은 시점에 커밋됐다."
|
|
286
|
+
echo " verdict_commit=$verdict_ts latest_pre=$pre_max_ts"
|
|
287
|
+
return 1
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
echo "WITNESSED — 게이트 아티팩트 $pre_count 건이 전부 verdict 보다 먼저 커밋으로 고정됐다."
|
|
291
|
+
echo " latest_pre=$pre_max_ts verdict=$verdict_ts"
|
|
292
|
+
[ "$unverif" -gt 0 ] && echo " ⚠️ 그중 $unverif 건은 무결 미검증(파일 부재) — 순서만 증명됨"
|
|
293
|
+
# 명명된 잔여: 이 판정은 **로컬 커밋 그래프**만 본다. 브랜치가 main 인지, push 됐는지,
|
|
294
|
+
# ruleset 이 살아 있는지는 검사하지 않는다(F7). 되쓰기 방지는 서버 설정에 의존한다.
|
|
295
|
+
return 0
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
299
|
+
# self-test — known-pair 캘리브레이션
|
|
300
|
+
# 계기가 **positive 와 negative 를 실제로 가르는지** 본다. 한쪽만 맞히는 계기는
|
|
301
|
+
# 재는 게 아니라 생성하는 것이다.
|
|
302
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
303
|
+
do_self_test() {
|
|
304
|
+
local T; T="$(mktemp -d -t chwit_t.XXXXXX)" || { echo "❌ mktemp -d 실패" >&2; return 10; }
|
|
305
|
+
local pass=0 fail=0
|
|
306
|
+
_t() { # $1=label $2=expected-rc $3=actual-rc
|
|
307
|
+
if [ "$2" = "$3" ]; then echo "✅ $1 → rc=$3"; pass=$((pass+1));
|
|
308
|
+
else echo "❌ $1 → rc=$3 (기대 $2)"; fail=$((fail+1)); fi
|
|
309
|
+
}
|
|
310
|
+
_tgrep() { # $1=label $2=needle $3=output
|
|
311
|
+
case "$3" in *"$2"*) echo "✅ $1"; pass=$((pass+1)) ;;
|
|
312
|
+
*) echo "❌ $1 — 출력에 '$2' 없음"; fail=$((fail+1)) ;; esac
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# 격리된 임시 레포에서 **진짜 커밋 순서**로 잰다.
|
|
316
|
+
( cd "$T" && git init -q . && git config user.email t@t && git config user.name t ) || {
|
|
317
|
+
echo "❌ 임시 레포 생성 실패" >&2; rm -rf "$T"; return 10; }
|
|
318
|
+
mkdir -p "$T/knowledge/shared/learnings"
|
|
319
|
+
|
|
320
|
+
# ⚠️ 절대경로여야 한다. 상대경로면 아래 `cd "$T"` 서브셸에서 스크립트를 못 찾아
|
|
321
|
+
# **전 레인이 rc=127 로 죽는다**(첫 실행 실측). 그 방향은 fail-closed 라 7/7 실패로 드러났다.
|
|
322
|
+
local SELF; SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
|
323
|
+
local rc out
|
|
324
|
+
|
|
325
|
+
# 한 run 의 게이트 3종을 만들고 record+커밋 하는 헬퍼.
|
|
326
|
+
_mkrun() { # $1=slug $2=문구접미사
|
|
327
|
+
mkdir -p "$T/ws/$1"
|
|
328
|
+
printf 'intent %s\n' "$2" > "$T/ws/$1/INTENT.md"
|
|
329
|
+
printf 'budget %s\n' "$2" > "$T/ws/$1/BUDGET.md"
|
|
330
|
+
printf 'sim %s\n' "$2" > "$T/ws/$1/SIM_NOTES.md"
|
|
331
|
+
printf 'verdict %s\n' "$2" > "$T/ws/$1/EMISSION_VERDICT.md"
|
|
332
|
+
}
|
|
333
|
+
_rec() { ( cd "$T" && CLAUDE_PROJECT_DIR="$T" bash "$SELF" record "$1" "$T/ws/$1/$2" >/dev/null 2>&1 ); }
|
|
334
|
+
_commit() { ( cd "$T" && git add -A && git commit -qm "$1" ) >/dev/null 2>&1; }
|
|
335
|
+
_verify() { ( cd "$T" && CLAUDE_PROJECT_DIR="$T" bash "$SELF" verify "$1" "$T/ws/$1" 2>&1 ); }
|
|
336
|
+
|
|
337
|
+
# ── P1 정상: 게이트 3종 커밋 → (1초 후) verdict 커밋 → WITNESSED(0)
|
|
338
|
+
_mkrun good g; _rec good INTENT.md; _rec good BUDGET.md; _rec good SIM_NOTES.md; _commit "gates"
|
|
339
|
+
sleep 1
|
|
340
|
+
_rec good EMISSION_VERDICT.md; _commit "verdict"
|
|
341
|
+
out="$(_verify good)"; rc=$?
|
|
342
|
+
_t "정상(게이트3 → verdict) → WITNESSED" 0 "$rc"
|
|
343
|
+
|
|
344
|
+
# ── N1 역순: verdict 먼저 커밋 → UNORDERED(1)
|
|
345
|
+
_mkrun bad b; _rec bad EMISSION_VERDICT.md; _commit "bad verdict first"
|
|
346
|
+
sleep 1
|
|
347
|
+
_rec bad INTENT.md; _rec bad BUDGET.md; _rec bad SIM_NOTES.md; _commit "bad gates later"
|
|
348
|
+
out="$(_verify bad)"; rc=$?
|
|
349
|
+
_t "역순(verdict 먼저) → UNORDERED" 1 "$rc"
|
|
350
|
+
|
|
351
|
+
# ── N2 변조: 커밋 후 파일을 고침 → TAMPERED(1)
|
|
352
|
+
_mkrun tamper t; _rec tamper INTENT.md; _rec tamper BUDGET.md; _rec tamper SIM_NOTES.md; _commit "tamper gates"
|
|
353
|
+
sleep 1; _rec tamper EMISSION_VERDICT.md; _commit "tamper verdict"
|
|
354
|
+
printf 'rewritten after the fact\n' > "$T/ws/tamper/INTENT.md"
|
|
355
|
+
out="$(_verify tamper)"; rc=$?
|
|
356
|
+
_t "사후 재작성 → TAMPERED" 1 "$rc"
|
|
357
|
+
|
|
358
|
+
# ── N3 미기록 slug → UNWITNESSED(2). 0 아님.
|
|
359
|
+
mkdir -p "$T/ws/nonexistent"
|
|
360
|
+
out="$(_verify nonexistent)"; rc=$?
|
|
361
|
+
_t "미기록 slug → UNWITNESSED(2, 통과 아님)" 2 "$rc"
|
|
362
|
+
|
|
363
|
+
# ── N4 미커밋 → PENDING(2)
|
|
364
|
+
_mkrun pend p; _rec pend INTENT.md
|
|
365
|
+
out="$(_verify pend)"; rc=$?
|
|
366
|
+
_t "기록만 하고 미커밋 → PENDING(2)" 2 "$rc"
|
|
367
|
+
|
|
368
|
+
# ── N5 없는 아티팩트 record → 입력 불량(10)
|
|
369
|
+
( cd "$T" && CLAUDE_PROJECT_DIR="$T" bash "$SELF" record x "$T/ws/does-not-exist.md" >/dev/null 2>&1 ); rc=$?
|
|
370
|
+
_t "없는 아티팩트 record → rc=10" 10 "$rc"
|
|
371
|
+
|
|
372
|
+
# ── N6 1인자 호출(wsdir 생략) — 죽지 않고 판정. 워크스페이스 부재라 무결은 미검증.
|
|
373
|
+
( cd "$T" && CLAUDE_PROJECT_DIR="$T" bash "$SELF" verify good >/dev/null 2>&1 ); rc=$?
|
|
374
|
+
_t "1인자 호출(wsdir 생략) → 죽지 않고 WITNESSED" 0 "$rc"
|
|
375
|
+
|
|
376
|
+
# ── N7 ★ verdict 미기록 → INCOMPLETE(2). 초판은 여기서 rc=0 을 냈다 (cross-family F1).
|
|
377
|
+
_mkrun noverd n; _rec noverd INTENT.md; _rec noverd BUDGET.md; _rec noverd SIM_NOTES.md; _commit "noverd gates"
|
|
378
|
+
out="$(_verify noverd)"; rc=$?
|
|
379
|
+
_t "★verdict 미기록 → INCOMPLETE(2), 통과 아님" 2 "$rc"
|
|
380
|
+
_tgrep " └ 사유가 'verdict 해시가 원장에 없다' 로 인쇄된다" "verdict 해시가 원장에 없다" "$out"
|
|
381
|
+
|
|
382
|
+
# ── N8 ★ same-commit: 게이트와 verdict 가 같은 커밋 → UNORDERED(1). 초판은 통과시켰다 (F3).
|
|
383
|
+
_mkrun same s
|
|
384
|
+
_rec same INTENT.md; _rec same BUDGET.md; _rec same SIM_NOTES.md; _rec same EMISSION_VERDICT.md
|
|
385
|
+
_commit "same commit both"
|
|
386
|
+
out="$(_verify same)"; rc=$?
|
|
387
|
+
_t "★같은 커밋(pre==verdict 시각) → UNORDERED" 1 "$rc"
|
|
388
|
+
|
|
389
|
+
# ── N9 ★ 부분 게이트: INTENT 만 먼저, BUDGET/SIM 은 verdict 뒤 → INCOMPLETE 또는 UNORDERED (F4).
|
|
390
|
+
# 초판은 pre 최솟값만 봐서 WITNESSED 였다.
|
|
391
|
+
_mkrun mixed m; _rec mixed INTENT.md; _commit "mixed intent"
|
|
392
|
+
sleep 1; _rec mixed EMISSION_VERDICT.md; _commit "mixed verdict"
|
|
393
|
+
sleep 1; _rec mixed BUDGET.md; _rec mixed SIM_NOTES.md; _commit "mixed rest"
|
|
394
|
+
out="$(_verify mixed)"; rc=$?
|
|
395
|
+
_t "★부분 게이트(BUDGET/SIM 이 verdict 뒤) → 통과 아님" 1 "$rc"
|
|
396
|
+
|
|
397
|
+
# ── N10 ★ 중복 해시: 다른 run 이 **같은 내용**을 먼저 커밋해도 현재 run 의 미커밋을
|
|
398
|
+
# 커밋된 것으로 읽지 않는다 (F5). 초판은 sha 문자열만 봐서 숨겼다.
|
|
399
|
+
mkdir -p "$T/ws/dupA" "$T/ws/dupB"
|
|
400
|
+
printf 'identical body\n' > "$T/ws/dupA/INTENT.md"
|
|
401
|
+
printf 'identical body\n' > "$T/ws/dupB/INTENT.md"
|
|
402
|
+
_rec dupA INTENT.md; _commit "dupA committed"
|
|
403
|
+
_rec dupB INTENT.md # 미커밋
|
|
404
|
+
out="$(_verify dupB)"; rc=$?
|
|
405
|
+
_t "★중복 해시가 PENDING 을 숨기지 않는다" 2 "$rc"
|
|
406
|
+
_tgrep " └ dupB 가 PENDING 으로 인쇄된다" "PENDING" "$out"
|
|
407
|
+
|
|
408
|
+
# ── N11 ★ 원장 쓰기 실패 → rc=10 (F2). 초판은 witnessed + rc=0 이었다.
|
|
409
|
+
printf 'not a dir\n' > "$T/rootfile"
|
|
410
|
+
printf 'artifact\n' > "$T/art.md"
|
|
411
|
+
( CLAUDE_PROJECT_DIR="$T/rootfile" bash "$SELF" record wf "$T/art.md" >/dev/null 2>&1 ); rc=$?
|
|
412
|
+
_t "★원장 쓰기 실패 → rc=10 (성공 렌더 금지)" 10 "$rc"
|
|
413
|
+
|
|
414
|
+
# ── N12 ★ slug 문자셋 불량 → rc=10 (F10, 원장 파싱 파괴 방지)
|
|
415
|
+
( cd "$T" && CLAUDE_PROJECT_DIR="$T" bash "$SELF" record "bad slug:x" "$T/art.md" >/dev/null 2>&1 ); rc=$?
|
|
416
|
+
_t "★slug 에 공백/콜론 → rc=10" 10 "$rc"
|
|
417
|
+
|
|
418
|
+
# ── N13 컨트롤: 정상 케이스가 여전히 0 인가 (앞 레인이 상태를 오염시키지 않았는지)
|
|
419
|
+
out="$(_verify good)"; rc=$?
|
|
420
|
+
_t "컨트롤 — 정상 케이스 재확인" 0 "$rc"
|
|
421
|
+
|
|
422
|
+
rm -rf "$T"
|
|
423
|
+
echo
|
|
424
|
+
if [ "$fail" -eq 0 ]; then
|
|
425
|
+
echo "✅ 캘리브레이션 통과 ($pass 레인) — positive/negative 를 실제로 가른다."
|
|
426
|
+
return 0
|
|
427
|
+
fi
|
|
428
|
+
echo "🟥 캘리브레이션 실패 ($fail/$((pass+fail)))"
|
|
429
|
+
return 1
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
case "${1:---help}" in
|
|
433
|
+
record) shift; do_record "${1:-}" "${2:-}" ;;
|
|
434
|
+
verify) shift; do_verify "${1:-}" "${2:-}" ;;
|
|
435
|
+
--self-test) do_self_test ;;
|
|
436
|
+
*)
|
|
437
|
+
sed -n '1,60p' "${BASH_SOURCE[0]}" | grep -E '^#( |$)' | sed 's/^# \{0,1\}//'
|
|
438
|
+
exit 0 ;;
|
|
439
|
+
esac
|