@chrono-meta/fh-gate 2.15.1 → 3.0.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 +38 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/CLAUDE.md +1 -1
- package/README.md +6 -1
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +47 -0
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +10 -0
- package/knowledge/shared/harness-core/measurement-integrity-checklist.md +15 -1
- package/knowledge/shared/harness-core/ship_readiness_gate.md +19 -2
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +49 -0
- package/package.json +9 -1
- 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 +25 -0
- package/scripts/backtick_guard.sh +194 -0
- package/scripts/context_continuity_score.sh +49 -7
- package/scripts/fh-gate.sh +3 -3
- package/scripts/files_manifest_shipping_check.sh +19 -0
- package/scripts/gate_pathspec_check.sh +1 -1
- package/scripts/package_coverage_check.sh +24 -2
- package/scripts/proposal_hook.sh +89 -0
- package/scripts/public_surface_scan_files.sh +11 -2
- package/scripts/revert_probe.sh +250 -0
- package/scripts/selfcheck.sh +65 -2
- package/scripts/sim_isolated_run.sh +97 -7
- package/scripts/test_backtick_guard_lanes.sh +115 -0
- package/scripts/test_degrade_scan_shell_probes.sh +7 -7
- package/scripts/test_files_manifest_shipping_lanes.sh +5 -5
- package/scripts/test_heavy_classifier_lanes.sh +1 -1
- package/scripts/test_lane_runner_lanes.sh +59 -33
- package/scripts/test_mapped_tracks_lanes.sh +1 -1
- package/scripts/test_marker_soul_check_lanes.sh +24 -0
- package/scripts/test_node_check_lanes.sh +34 -34
- package/scripts/test_package_coverage_lanes.sh +53 -27
- package/scripts/test_pipe_verdict_guard_lanes.sh +5 -5
- package/scripts/test_precommit_pointer_index_lanes.sh +33 -0
- package/scripts/test_preprep_drift_anchor.sh +13 -4
- package/scripts/test_preprep_drift_anchor_lanes.sh +23 -0
- package/scripts/test_proposal_hook_lanes.sh +36 -0
- package/scripts/test_revert_probe_lanes.sh +146 -0
- package/scripts/test_session_close_lanes.sh +3 -5
- package/scripts/test_sim_isolated_run_lanes.sh +17 -0
- package/scripts/utterance_landing_check.sh +2 -2
- package/templates/.git-hooks/pre-commit +27 -4
- package/templates/settings.PreToolUse.snippet.json +37 -1
- package/plugins/fh-commons/README.md +0 -38
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# revert_probe.sh — general-purpose ⓕ revert-and-observe probe (rung 강화 #2, six_axis_review_2026-09-04).
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS EXISTS. `되돌림`(revert-and-observe) has been done by hand 15+ times in this repo's own
|
|
5
|
+
# history (see 6축 실측: ⓕ none 비율 57.4%, «정확히 4레인 적색» 급 실물은 매번 손으로 짠 1회성
|
|
6
|
+
# 스크립트였다). Hand-rolled revert probes are the exact shape §Mechanize-at-repetition names —
|
|
7
|
+
# N≥3 recurrence on the SAME operation (swap a file to an older version, rerun a suite, read which
|
|
8
|
+
# lines changed color) is a mechanization trigger, not a one-off. This is that mechanization.
|
|
9
|
+
#
|
|
10
|
+
# 🟥 FRONTIER WARNING — read before citing this tool's PASS as "the suite has detection power".
|
|
11
|
+
# A single revert of a single file is ONE mutant. Mutation-testing research (arXiv 2607.22880,
|
|
12
|
+
# and the companion Meta engineering write-up cited alongside it in
|
|
13
|
+
# frontier_verification_map_2026-09-04.md §ⓕ) found that coverage/mutation SCORE loses its
|
|
14
|
+
# correlation with real fault-detection effectiveness once suite size is controlled for — a
|
|
15
|
+
# single kill is evidence the ANCHOR under test is load-bearing for THIS ONE reverted file,
|
|
16
|
+
# never a general claim that the suite "has good mutation coverage" or "catches regressions".
|
|
17
|
+
# Run this against every file you actually care about; do not average or extrapolate from one.
|
|
18
|
+
#
|
|
19
|
+
# WHAT IT DOES
|
|
20
|
+
# 1. Runs the lane suite AS-IS against the file's CURRENT (working-tree) content — the "수리 후"
|
|
21
|
+
# run.
|
|
22
|
+
# 2. Swaps ONLY the target file to its content at --baseline (default HEAD), backing up the
|
|
23
|
+
# current content first.
|
|
24
|
+
# 3. Reruns the same lane suite against that swapped-in baseline content — the "되돌린" run.
|
|
25
|
+
# 4. Restores the target file to its exact pre-probe content — ALWAYS, even if either suite run
|
|
26
|
+
# crashes, hangs past its timeout, or this script itself errors. Restore is attempted from an
|
|
27
|
+
# EXIT trap (safety net) in addition to the normal-path restore, so a `kill`-free abnormal
|
|
28
|
+
# exit still restores. Physical restore, never `git checkout <ref> -- <path>` — that stages
|
|
29
|
+
# the revert into the index ([[feedback_git_checkout_path_stages_the_revert]]); this tool
|
|
30
|
+
# writes bytes to the file only, with `git show <ref>:<path>` (never `git checkout`), and
|
|
31
|
+
# never touches the index.
|
|
32
|
+
# 5. Diffs the two runs' ✅/❌ label lines (this repo's universal `ok()`/`no()` convention —
|
|
33
|
+
# every test_*.sh / *_lanes.sh in scripts/ prints ` ✅ <label>` / ` ❌ <label>`) and reports
|
|
34
|
+
# EXACTLY which labels flipped from ✅ (current) to ❌ (baseline) — i.e. which lane actually
|
|
35
|
+
# went red when the fix was undone.
|
|
36
|
+
#
|
|
37
|
+
# WHAT IT ASSUMES (named, not hidden): the lane suite's ✅/❌ label text is STABLE across the two
|
|
38
|
+
# runs for a given lane (the suite script itself does not change between the two invocations —
|
|
39
|
+
# only the target file's content does). A suite whose pass/fail label text is dynamically built
|
|
40
|
+
# from data that changes with the target file (e.g. embeds a byte count in the label) will show
|
|
41
|
+
# as "label only in one run" rather than a flip — reported honestly as UNMATCHED, not silently
|
|
42
|
+
# dropped, and not counted toward the flip total.
|
|
43
|
+
#
|
|
44
|
+
# USAGE
|
|
45
|
+
# bash scripts/revert_probe.sh <target-file> <lane-suite-script> [--baseline <ref>] [--timeout <sec>]
|
|
46
|
+
# bash scripts/revert_probe.sh scripts/foo.sh scripts/test_foo_lanes.sh
|
|
47
|
+
# bash scripts/revert_probe.sh scripts/foo.sh scripts/test_foo_lanes.sh --baseline HEAD~1
|
|
48
|
+
#
|
|
49
|
+
# EXIT CODES (fail-closed, per CLAUDE.md §Irreversibility Surface-Class Degrade Invariant — this
|
|
50
|
+
# is a REVERSIBLE, read-then-restore surface, so the floor here is "never mis-score", not
|
|
51
|
+
# "never run"):
|
|
52
|
+
# 0 = exactly ≥1 lane flipped ✅→❌ when reverted (anchor is alive — it caught the mutant)
|
|
53
|
+
# 1 = 0 lanes flipped (anchor is DECORATIVE for this file — nothing depended on the fix)
|
|
54
|
+
# 2 = usage error (bad args, target/suite/ref not found)
|
|
55
|
+
# 10 = the probe itself is unreliable for this run — either suite run produced ZERO parseable
|
|
56
|
+
# ✅/❌ lines (harness error, not "0 lanes exist"), OR the restore step failed (the target
|
|
57
|
+
# file may still hold BASELINE content — treated as the more severe failure and reported
|
|
58
|
+
# loudly, never silently folded into a verdict)
|
|
59
|
+
#
|
|
60
|
+
# Usage in a lane test: see scripts/test_revert_probe_lanes.sh for the known-pair calibration
|
|
61
|
+
# (decorative anchor → 1, real anchor → 0, restore-guaranteed-on-suite-crash).
|
|
62
|
+
|
|
63
|
+
set -uo pipefail
|
|
64
|
+
|
|
65
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
66
|
+
|
|
67
|
+
TARGET=""; SUITE=""; BASELINE="HEAD"; TIMEOUT="120"
|
|
68
|
+
_POS=()
|
|
69
|
+
while [ $# -gt 0 ]; do
|
|
70
|
+
case "$1" in
|
|
71
|
+
--baseline) BASELINE="${2:-HEAD}"; shift 2 ;;
|
|
72
|
+
--timeout) TIMEOUT="${2:-120}"; shift 2 ;;
|
|
73
|
+
-*) echo "FAIL: unknown flag: $1" >&2; exit 2 ;;
|
|
74
|
+
*) _POS+=("$1"); shift ;;
|
|
75
|
+
esac
|
|
76
|
+
done
|
|
77
|
+
TARGET="${_POS[0]:-}"; SUITE="${_POS[1]:-}"
|
|
78
|
+
|
|
79
|
+
[ -n "$TARGET" ] || { echo "FAIL: usage: revert_probe.sh <target-file> <lane-suite-script> [--baseline <ref>]" >&2; exit 2; }
|
|
80
|
+
[ -n "$SUITE" ] || { echo "FAIL: usage: revert_probe.sh <target-file> <lane-suite-script> [--baseline <ref>]" >&2; exit 2; }
|
|
81
|
+
[ -f "$TARGET" ] || { echo "FAIL: target file not found: $TARGET" >&2; exit 2; }
|
|
82
|
+
[ -f "$SUITE" ] || { echo "FAIL: lane suite script not found: $SUITE" >&2; exit 2; }
|
|
83
|
+
|
|
84
|
+
# 🟥 physical path (`pwd -P`), not logical — macOS `/tmp` and `$TMPDIR` are symlinks into
|
|
85
|
+
# `/private/...`, and `git rev-parse --show-toplevel` always answers with the PHYSICAL path.
|
|
86
|
+
# A logical `pwd` here would make every fixture under a temp dir fail the prefix-strip below
|
|
87
|
+
# (same class of defect `sim_isolated_run.sh` already names for its own path isolation).
|
|
88
|
+
TARGET_DIR="$(cd "$(dirname "$TARGET")" && pwd -P)"
|
|
89
|
+
TARGET_ABS="$TARGET_DIR/$(basename "$TARGET")"
|
|
90
|
+
GIT_ROOT="$(cd "$TARGET_DIR" && git rev-parse --show-toplevel 2>/dev/null)"
|
|
91
|
+
[ -n "$GIT_ROOT" ] || { echo "FAIL: target file is not inside a git repository: $TARGET" >&2; exit 2; }
|
|
92
|
+
case "$TARGET_ABS" in
|
|
93
|
+
"$GIT_ROOT"/*) REL_PATH="${TARGET_ABS#"$GIT_ROOT"/}" ;;
|
|
94
|
+
*) echo "FAIL: could not compute a repo-relative path for $TARGET" >&2; exit 2 ;;
|
|
95
|
+
esac
|
|
96
|
+
|
|
97
|
+
if ! git -C "$GIT_ROOT" cat-file -e "${BASELINE}:${REL_PATH}" 2>/dev/null; then
|
|
98
|
+
echo "FAIL: $REL_PATH not found at baseline '$BASELINE' (bad ref, or the file did not exist there)" >&2
|
|
99
|
+
exit 2
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
echo "── revert_probe ────────────────────────────────────────────────────"
|
|
103
|
+
echo "target: $REL_PATH"
|
|
104
|
+
echo "suite: $SUITE"
|
|
105
|
+
echo "baseline: $BASELINE"
|
|
106
|
+
echo ""
|
|
107
|
+
echo "🟥 FRONTIER WARNING (arXiv 2607.22880): this run reverts EXACTLY ONE file — ONE mutant."
|
|
108
|
+
echo " A ✅ verdict here means the anchor caught THIS mutant, not that the suite has general"
|
|
109
|
+
echo " mutation-detection power. Coverage/mutation score decorrelates from real effectiveness"
|
|
110
|
+
echo " once suite size is controlled for — do not average or extrapolate from a single run."
|
|
111
|
+
echo "되돌린 파일(뮤턴트) 수: 1 — $REL_PATH"
|
|
112
|
+
echo ""
|
|
113
|
+
|
|
114
|
+
WORKDIR="$(mktemp -d "${TMPDIR:-/tmp}/revert_probe.XXXXXX")" || { echo "FAIL: mktemp -d failed" >&2; exit 10; }
|
|
115
|
+
BACKUP="$WORKDIR/backup"
|
|
116
|
+
RUN_A="$WORKDIR/run_current.txt"
|
|
117
|
+
RUN_B="$WORKDIR/run_baseline.txt"
|
|
118
|
+
|
|
119
|
+
cp -p "$TARGET_ABS" "$BACKUP" || { echo "FAIL: could not back up $TARGET_ABS — refusing to touch it" >&2; rm -rf "$WORKDIR"; exit 10; }
|
|
120
|
+
|
|
121
|
+
RESTORED=0
|
|
122
|
+
_restore() {
|
|
123
|
+
[ "$RESTORED" = 1 ] && return 0
|
|
124
|
+
if cp -p "$BACKUP" "$TARGET_ABS" 2>/dev/null; then
|
|
125
|
+
RESTORED=1
|
|
126
|
+
else
|
|
127
|
+
echo "🟥🟥🟥 RESTORE FAILED — $TARGET_ABS may still hold BASELINE ($BASELINE) content." >&2
|
|
128
|
+
echo " Backup of the pre-probe content is kept at: $BACKUP" >&2
|
|
129
|
+
echo " Restore it by hand: cp \"$BACKUP\" \"$TARGET_ABS\"" >&2
|
|
130
|
+
fi
|
|
131
|
+
}
|
|
132
|
+
# Safety net — fires on ANY exit path (normal, error, unbound-var under set -u), so a crash
|
|
133
|
+
# mid-probe still restores. The normal path below also calls _restore explicitly and checks its
|
|
134
|
+
# result directly, because a trap cannot hand its own success/failure back to the exit-code logic.
|
|
135
|
+
trap '_restore' EXIT
|
|
136
|
+
|
|
137
|
+
_run_suite() { # $1=output-file
|
|
138
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
139
|
+
timeout "$TIMEOUT" bash "$SUITE" > "$1" 2>&1
|
|
140
|
+
else
|
|
141
|
+
bash "$SUITE" > "$1" 2>&1
|
|
142
|
+
fi
|
|
143
|
+
return 0 # the suite's own exit code (pass/fail count) is not this function's concern
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
echo "── run 1/2: 현재(수리 후) 판 ──"
|
|
147
|
+
_run_suite "$RUN_A"
|
|
148
|
+
echo " captured $(wc -l < "$RUN_A" | tr -d ' ') lines"
|
|
149
|
+
|
|
150
|
+
if ! git -C "$GIT_ROOT" show "${BASELINE}:${REL_PATH}" > "$TARGET_ABS.new" 2>"$WORKDIR/show.err"; then
|
|
151
|
+
echo "FAIL: git show ${BASELINE}:${REL_PATH} failed:" >&2
|
|
152
|
+
cat "$WORKDIR/show.err" >&2
|
|
153
|
+
rm -f "$TARGET_ABS.new"
|
|
154
|
+
# trap restores (no-op here, file was never swapped) and exits
|
|
155
|
+
exit 10
|
|
156
|
+
fi
|
|
157
|
+
mv "$TARGET_ABS.new" "$TARGET_ABS"
|
|
158
|
+
|
|
159
|
+
echo "── run 2/2: 기준($BASELINE) 판 (파일만 되돌림, 인덱스는 안 건드림) ──"
|
|
160
|
+
_run_suite "$RUN_B"
|
|
161
|
+
echo " captured $(wc -l < "$RUN_B" | tr -d ' ') lines"
|
|
162
|
+
|
|
163
|
+
_restore
|
|
164
|
+
trap - EXIT # explicit restore already ran; the safety net has nothing left to do
|
|
165
|
+
if [ "$RESTORED" != 1 ]; then
|
|
166
|
+
echo "" >&2
|
|
167
|
+
echo "RESULT: RESTORE-FAILED — do not trust the file on disk, see backup path above" >&2
|
|
168
|
+
rm -rf "$WORKDIR"
|
|
169
|
+
exit 10
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# ── ✅/❌ 라벨 추출 — 심볼\tSPACE-트림한 설명 ────────────────────────────────────────────
|
|
173
|
+
_extract_labels() { # $1=source-file → writes "P|F<TAB>desc"
|
|
174
|
+
grep -E '(✅|❌)' "$1" 2>/dev/null | while IFS= read -r line; do
|
|
175
|
+
case "$line" in
|
|
176
|
+
*✅*) sym=P; rest="${line#*✅}" ;;
|
|
177
|
+
*) sym=F; rest="${line#*❌}" ;;
|
|
178
|
+
esac
|
|
179
|
+
rest="$(printf '%s' "$rest" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
|
180
|
+
[ -n "$rest" ] && printf '%s\t%s\n' "$sym" "$rest"
|
|
181
|
+
done
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
LABELS_A="$WORKDIR/labels_a.tsv"; LABELS_B="$WORKDIR/labels_b.tsv"
|
|
185
|
+
_extract_labels "$RUN_A" > "$LABELS_A"
|
|
186
|
+
_extract_labels "$RUN_B" > "$LABELS_B"
|
|
187
|
+
NA=$(wc -l < "$LABELS_A" | tr -d ' ')
|
|
188
|
+
NB=$(wc -l < "$LABELS_B" | tr -d ' ')
|
|
189
|
+
|
|
190
|
+
if [ "$NA" -eq 0 ] || [ "$NB" -eq 0 ]; then
|
|
191
|
+
echo ""
|
|
192
|
+
echo "🟥 HARNESS ERROR — one of the two runs produced ZERO ✅/❌ lines (current=$NA, baseline=$NB)."
|
|
193
|
+
echo " That is not \"0 lanes exist\" — it means this probe cannot see the suite's verdicts at"
|
|
194
|
+
echo " all for that run (crash, missing labels, wrong suite path). Read the raw output:"
|
|
195
|
+
echo " current: $RUN_A"
|
|
196
|
+
echo " baseline: $RUN_B"
|
|
197
|
+
rm -rf "$WORKDIR"
|
|
198
|
+
exit 10
|
|
199
|
+
fi
|
|
200
|
+
|
|
201
|
+
# DESC\tSYM, sorted by DESC — LC_ALL=C throughout so non-ASCII label text (this repo's labels are
|
|
202
|
+
# routinely Korean) never collapses under a locale-dependent string-equality/sort
|
|
203
|
+
# ([[feedback_locale_string_equality_breaks_nonascii]]).
|
|
204
|
+
DESC_A="$WORKDIR/desc_a.tsv"; DESC_B="$WORKDIR/desc_b.tsv"
|
|
205
|
+
awk -F'\t' '{print $2"\t"$1}' "$LABELS_A" | LC_ALL=C sort -t"$(printf '\t')" -k1,1 -k2,2 > "$DESC_A"
|
|
206
|
+
awk -F'\t' '{print $2"\t"$1}' "$LABELS_B" | LC_ALL=C sort -t"$(printf '\t')" -k1,1 -k2,2 > "$DESC_B"
|
|
207
|
+
|
|
208
|
+
JOINED="$WORKDIR/joined.tsv"
|
|
209
|
+
LC_ALL=C join -t "$(printf '\t')" -j 1 -o 1.1,1.2,2.2 "$DESC_A" "$DESC_B" > "$JOINED" 2>/dev/null || : > "$JOINED"
|
|
210
|
+
|
|
211
|
+
FLIPPED_RED="$WORKDIR/flipped_red.txt" # ✅(현재) → ❌(기준) — 앵커가 실제로 잡은 것
|
|
212
|
+
FLIPPED_GREEN="$WORKDIR/flipped_green.txt" # ❌(현재) → ✅(기준) — 이상 신호, 참고용
|
|
213
|
+
awk -F'\t' '$2=="P" && $3=="F" {print $1}' "$JOINED" > "$FLIPPED_RED"
|
|
214
|
+
awk -F'\t' '$2=="F" && $3=="P" {print $1}' "$JOINED" > "$FLIPPED_GREEN"
|
|
215
|
+
K=$(wc -l < "$FLIPPED_RED" | tr -d ' ')
|
|
216
|
+
J=$(wc -l < "$FLIPPED_GREEN" | tr -d ' ')
|
|
217
|
+
|
|
218
|
+
# 라벨 텍스트가 두 실행에서 안 겹치는 경우 — 조용히 버리지 않고 이름으로 남긴다(가정 위반 알림).
|
|
219
|
+
ONLY_A="$WORKDIR/only_a.txt"; ONLY_B="$WORKDIR/only_b.txt"
|
|
220
|
+
LC_ALL=C comm -23 <(cut -f1 "$DESC_A" | LC_ALL=C sort -u) <(cut -f1 "$DESC_B" | LC_ALL=C sort -u) > "$ONLY_A"
|
|
221
|
+
LC_ALL=C comm -13 <(cut -f1 "$DESC_A" | LC_ALL=C sort -u) <(cut -f1 "$DESC_B" | LC_ALL=C sort -u) > "$ONLY_B"
|
|
222
|
+
NUM_ONLY_A=$(wc -l < "$ONLY_A" | tr -d ' '); NUM_ONLY_B=$(wc -l < "$ONLY_B" | tr -d ' ')
|
|
223
|
+
|
|
224
|
+
echo ""
|
|
225
|
+
echo "── 결과 ──────────────────────────────────────────────────────────"
|
|
226
|
+
echo "현재(수리 후) 라벨: ${NA}줄 · 기준($BASELINE) 라벨: ${NB}줄"
|
|
227
|
+
echo ""
|
|
228
|
+
echo "되돌린 레인 (✅→❌, 앵커가 실제로 잡은 것): ${K}개"
|
|
229
|
+
if [ "$K" -gt 0 ]; then sed 's/^/ ❌ /' "$FLIPPED_RED"; fi
|
|
230
|
+
echo ""
|
|
231
|
+
echo "(참고) 반대방향 (❌→✅, 이상 신호): ${J}개"
|
|
232
|
+
if [ "$J" -gt 0 ]; then sed 's/^/ ⚠️ /' "$FLIPPED_GREEN"; fi
|
|
233
|
+
if [ "$NUM_ONLY_A" -gt 0 ] || [ "$NUM_ONLY_B" -gt 0 ]; then
|
|
234
|
+
echo ""
|
|
235
|
+
echo "🟥 라벨 텍스트가 두 실행에서 완전히 겹치지 않는다 — 이 도구의 가정(라벨 텍스트가 안정적)"
|
|
236
|
+
echo " 이 이 스위트에서는 안 맞을 수 있다. 아래는 매칭 대상에서 빠진 라벨(플립 집계에 미포함):"
|
|
237
|
+
[ "$NUM_ONLY_A" -gt 0 ] && sed 's/^/ 현재에만: /' "$ONLY_A"
|
|
238
|
+
[ "$NUM_ONLY_B" -gt 0 ] && sed 's/^/ 기준에만: /' "$ONLY_B"
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
echo ""
|
|
242
|
+
if [ "$K" -ge 1 ]; then
|
|
243
|
+
echo "판정: 앵커 살아있음 — 되돌리면 정확히 ${K}개 레인이 빨개진다"
|
|
244
|
+
rm -rf "$WORKDIR"
|
|
245
|
+
exit 0
|
|
246
|
+
else
|
|
247
|
+
echo "판정: 앵커 장식 — 되돌려도 빨개지는 레인이 0개다"
|
|
248
|
+
rm -rf "$WORKDIR"
|
|
249
|
+
exit 1
|
|
250
|
+
fi
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -187,10 +187,22 @@ else
|
|
|
187
187
|
fail=1
|
|
188
188
|
fi
|
|
189
189
|
|
|
190
|
-
# Bash surface: npm-shipped scripts + local bin wrappers + gate-chain infra
|
|
190
|
+
# Bash surface: npm-shipped scripts + local bin wrappers + gate-chain infra.
|
|
191
|
+
# `bin/fh-gate` · `bin/fh-run` · `bin/fh-goal` are named EXPLICITLY here (not a glob) and, unlike
|
|
192
|
+
# most of this list, are not covered by files_manifest_shipping_check.sh either (only their
|
|
193
|
+
# `.js` counterparts are declared in package.json files[]) — so a plain `[ -f "$f" ] || continue`
|
|
194
|
+
# made their disappearance invisible to BOTH checks at once. Reproduced 2026-09-03: deleting
|
|
195
|
+
# bin/fh-gate from a fixture tree left fail=0, no FAIL line, nothing. `scripts/*.sh` staying a
|
|
196
|
+
# silent skip on a genuinely-empty glob is correct (that arm still has no non-glob name); the
|
|
197
|
+
# named gate-chain-infra paths must not degrade the same way.
|
|
191
198
|
for f in scripts/*.sh bin/fh-gate bin/fh-run bin/fh-goal \
|
|
192
199
|
templates/regression_guard.sh templates/temper_check.sh templates/predelete_check.sh templates/.git-hooks/pre-commit; do
|
|
193
|
-
[ -f "$f" ]
|
|
200
|
+
if [ ! -f "$f" ]; then
|
|
201
|
+
case "$f" in
|
|
202
|
+
*'*'*) continue ;; # unmatched glob (nullglob off) — not a real path, legitimate skip
|
|
203
|
+
*) echo "FAIL bash -n coverage: gate-chain infra file missing: $f"; fail=1; continue ;;
|
|
204
|
+
esac
|
|
205
|
+
fi
|
|
194
206
|
check "bash -n $f" bash -n "$f"
|
|
195
207
|
done
|
|
196
208
|
|
|
@@ -615,10 +627,12 @@ for _pair in \
|
|
|
615
627
|
".claude/soul_tenets.txt|scripts/test_marker_soul_tenet_lanes.sh" \
|
|
616
628
|
"templates/.git-hooks/pre-commit|scripts/test_precommit_staged_drift_lanes.sh" \
|
|
617
629
|
"templates/.git-hooks/pre-commit|scripts/test_marker_address_lanes.sh" \
|
|
630
|
+
"templates/.git-hooks/pre-commit|scripts/test_precommit_pointer_index_lanes.sh" \
|
|
618
631
|
"scripts/residency_closure_scan.py|scripts/test_residency_closure_lanes.sh" \
|
|
619
632
|
"scripts/reviewer_capability_corpus.tsv|scripts/test_reviewer_capability_conformance.sh" \
|
|
620
633
|
"scripts/field_canon_preload.sh|scripts/test_field_canon_lanes.sh" \
|
|
621
634
|
"scripts/stale_clone_guard.sh|scripts/test_stale_clone_guard_lanes.sh" \
|
|
635
|
+
"scripts/proposal_hook.sh|scripts/test_proposal_hook_lanes.sh" \
|
|
622
636
|
"plugins/fh-commons/skills/ko-tech-writer/SKILL.md|scripts/test_ko_tech_writer_lanes.sh" \
|
|
623
637
|
"scripts/script_caller_ratchet.sh|scripts/test_script_caller_ratchet_lanes.sh" \
|
|
624
638
|
"scripts/script_caller_ratchet.sh|scripts/test_runner_surface_index_lanes.sh" \
|
|
@@ -631,6 +645,7 @@ for _pair in \
|
|
|
631
645
|
"plugins/fh-commons/skills/preprep/lane_adjacent_dup.py|scripts/test_preprep_adjacent_dup_lanes.sh" \
|
|
632
646
|
"plugins/fh-commons/skills/preprep/lane_promise.py|scripts/test_preprep_promise_lanes.sh" \
|
|
633
647
|
"plugins/fh-commons/skills/preprep/SKILL.md|scripts/test_preprep_drift_anchor.sh" \
|
|
648
|
+
"scripts/test_preprep_drift_anchor.sh|scripts/test_preprep_drift_anchor_lanes.sh" \
|
|
634
649
|
"scripts/field_canon_preload.sh|scripts/test_skill_canon_preload_lanes.sh" \
|
|
635
650
|
`# ── round/ 회차 계기 4종(2026-09-01). 넷 다 한 스위트가 잡는다 — 주체별로 행을 둔다 ──` \
|
|
636
651
|
"scripts/round/delta_guard.sh|scripts/test_round_instruments_lanes.sh" \
|
|
@@ -1056,6 +1071,29 @@ else
|
|
|
1056
1071
|
esac
|
|
1057
1072
|
fi
|
|
1058
1073
|
|
|
1074
|
+
# prepublish_scope_note — an embedded --self-test subject lane_runner_check.sh flagged as having
|
|
1075
|
+
# no dispatcher anywhere (2026-09-03): its own 7-lane known-pair (does validate.yml still call
|
|
1076
|
+
# selfcheck.sh — known-positive/negative, missing-workflow, commented-out call, real call beside a
|
|
1077
|
+
# stale commented one, the real `run: |` block-scalar shape, and echo-mention-is-not-a-call) lives
|
|
1078
|
+
# behind `--self-test`, and nothing runs it. It IS invoked at publish time (package.json
|
|
1079
|
+
# `prepublishOnly`) — but that is `check()`, the gate's default argument-less mode, running for
|
|
1080
|
+
# real; it never exercises the gate's OWN calibration. Not in the `for _subj in ...` loop above:
|
|
1081
|
+
# its terminal line is `── N pass / M fail`, never 캘리브레이션, same reason capability_registry_check
|
|
1082
|
+
# and capability_effect_probe were pulled out of that loop. Direct dispatch instead, same shape as
|
|
1083
|
+
# capability_effect_probe.sh above — whole-line terminal verdict with a non-zero PASS count, so an
|
|
1084
|
+
# emptied suite cannot certify itself. Ships via package.json files[], so absence is FAIL, not SKIP.
|
|
1085
|
+
if [ ! -f scripts/prepublish_scope_note.sh ]; then
|
|
1086
|
+
echo "FAIL prepublish_scope_note.sh: missing — it ships via package.json files[], so absence is deletion, not package mode"
|
|
1087
|
+
fail=1
|
|
1088
|
+
elif _out=$(bash scripts/prepublish_scope_note.sh --self-test < /dev/null 2>&1) \
|
|
1089
|
+
&& printf '%s\n' "$_out" | grep -qE '^ ── [1-9][0-9]* pass / 0 fail$'; then
|
|
1090
|
+
echo "PASS prepublish_scope_note.sh --self-test ($(printf '%s\n' "$_out" | grep -oE '[0-9]+ pass / [0-9]+ fail' | tail -1))"
|
|
1091
|
+
else
|
|
1092
|
+
echo "FAIL prepublish_scope_note.sh: --self-test failed or produced no terminal verdict line"
|
|
1093
|
+
_show_failure "$_out"
|
|
1094
|
+
fail=1
|
|
1095
|
+
fi
|
|
1096
|
+
|
|
1059
1097
|
# memory-link-check — the memory store is a GRAPH (memory_intent_recall.md: nodes=files,
|
|
1060
1098
|
# edges=[[links]], recall walks one hop). Measured 2026-07-28: 50 of 872 edges pointed at a note
|
|
1061
1099
|
# that existed under a different separator and 22 at nothing — a dead edge returns nothing and is
|
|
@@ -1143,6 +1181,20 @@ else
|
|
|
1143
1181
|
fail=1
|
|
1144
1182
|
fi
|
|
1145
1183
|
|
|
1184
|
+
# ⓕ 되돌림 범용 프로브 (six_axis_review_2026-09-04 강화 #2) — 15+ 손짜기 되돌림 스크립트를
|
|
1185
|
+
# 대체하는 계기다. 자기 자신을 known-pair 로 검증한다(장식 앵커→1, 실물 앵커→0, 복원 보장) —
|
|
1186
|
+
# 앵커가 아니라 그 앵커를 검증하는 계기이므로 반드시 실행돼야 한다.
|
|
1187
|
+
if [ ! -f scripts/revert_probe.sh ]; then
|
|
1188
|
+
_absent_subject_verdict "test_revert_probe_lanes.sh" "scripts/revert_probe.sh" || fail=1
|
|
1189
|
+
elif [ -f scripts/test_revert_probe_lanes.sh ]; then
|
|
1190
|
+
if ! bash scripts/test_revert_probe_lanes.sh; then
|
|
1191
|
+
fail=1
|
|
1192
|
+
fi
|
|
1193
|
+
else
|
|
1194
|
+
echo "FAIL test_revert_probe_lanes.sh: revert_probe.sh present but its anchor is missing"
|
|
1195
|
+
fail=1
|
|
1196
|
+
fi
|
|
1197
|
+
|
|
1146
1198
|
# 무효 워터마크 — 무효 회차의 «숫자 줄»이 자기 무효를 나르는가.
|
|
1147
1199
|
# 🟥 회차 3 은 자기 게이트가 VOID 를 찍고도 그 숫자만 기록으로 넘어갔다(VOID 낱말은 0회).
|
|
1148
1200
|
# 판정이 표 «밖»에 있었고 사람은 표를 복사하기 때문이다. 그 채널을 닫은 배선의 앵커다.
|
|
@@ -1544,6 +1596,17 @@ else
|
|
|
1544
1596
|
fail=1
|
|
1545
1597
|
fi
|
|
1546
1598
|
|
|
1599
|
+
if [ ! -f scripts/backtick_guard.sh ]; then
|
|
1600
|
+
_absent_subject_verdict "test_backtick_guard_lanes.sh" "scripts/backtick_guard.sh" || fail=1
|
|
1601
|
+
elif [ -f scripts/test_backtick_guard_lanes.sh ]; then
|
|
1602
|
+
if ! bash scripts/test_backtick_guard_lanes.sh; then
|
|
1603
|
+
fail=1
|
|
1604
|
+
fi
|
|
1605
|
+
else
|
|
1606
|
+
echo "FAIL test_backtick_guard_lanes.sh: backtick_guard.sh present but its anchor is missing"
|
|
1607
|
+
fail=1
|
|
1608
|
+
fi
|
|
1609
|
+
|
|
1547
1610
|
if [ ! -f scripts/halffix_propagation_scan.sh ]; then
|
|
1548
1611
|
_absent_subject_verdict "test_halffix_lanes.sh" "scripts/halffix_propagation_scan.sh" || fail=1
|
|
1549
1612
|
elif [ -f scripts/test_halffix_lanes.sh ]; then
|
|
@@ -46,6 +46,18 @@
|
|
|
46
46
|
# same clone, --tools "", no --restricted → "🐿️" (memory PRESENT)
|
|
47
47
|
# One variable, opposite answers. ([[feedback_instrument_cannot_discriminate_hypotheses]])
|
|
48
48
|
#
|
|
49
|
+
# 🟥 THE SAME FLAG ALSO KILLS `act` MODE'S WRITE PATH — measured 2026-09-03, known-pair.
|
|
50
|
+
# `--restricted` ignores settings for PERMISSIONS too, not just memory, and `-p` has no TTY to
|
|
51
|
+
# ask a human — so `act`+`--no-harness` cannot Write/Edit AT ALL, regardless of what the model
|
|
52
|
+
# decides. Confirmed with one variable (`--restricted` on/off, same prompt, same --tools): Edit
|
|
53
|
+
# succeeds without it; with it every rep says "The edit was blocked — permission to write to
|
|
54
|
+
# file.txt wasn't granted." Deterministic, not a race — this is why a real run reads 5/5.
|
|
55
|
+
# ⇒ A "did not edit" result from `act --no-harness` is NOT evidence the base model chose not to
|
|
56
|
+
# write. Use `--no-harness` as an observe-mode control (memory presence, per the pair above)
|
|
57
|
+
# only — never to ask whether a write would have happened. Workaround not built: `--restricted`
|
|
58
|
+
# still honors an explicit `--settings <file>` (per `claude --help`), so a minimal file carrying
|
|
59
|
+
# only Write/Edit approval could restore act-mode fidelity here — filed, not implemented.
|
|
60
|
+
#
|
|
49
61
|
# 🟢 AND THE DEFECT IS REUSABLE AS AN INSTRUMENT. `--no-harness` answers a question this repo
|
|
50
62
|
# asks constantly and usually by eye: **does this behaviour come from FH, or would the base
|
|
51
63
|
# model have done it anyway?** Run the same prompt with and without the flag; a behaviour that
|
|
@@ -69,17 +81,74 @@
|
|
|
69
81
|
# FH defect (a normal session has Bash and can `ls`) — but scoring an arm without knowing it
|
|
70
82
|
# produces a confident zero from a fixture that was never observable.
|
|
71
83
|
#
|
|
72
|
-
# 🟥 PROJECT HOOKS DO
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
84
|
+
# 🟥 PROJECT HOOKS DO RUN IN A DISPOSABLE CLONE — an earlier version of this paragraph said the
|
|
85
|
+
# opposite, and it was wrong (RETRACTED 2026-09-03). It read: "PROJECT HOOKS DO NOT RUN IN A
|
|
86
|
+
# DISPOSABLE CLONE … measured 2026-08-30: an arm copied `.claude/settings.json` into its clone to
|
|
87
|
+
# make the PreToolUse PriorArt hook live; not one of the three clones grew
|
|
88
|
+
# `.claude/.prior_art_events.tsv`". That absence was read as "hooks never fire here" with NO
|
|
89
|
+
# control arm whose hook was known to fire — and it narrowed two rounds of the identity-⑤
|
|
90
|
+
# design (the hook-layer arm was declared unmeasurable). Known pair, 2026-09-03: a PreToolUse
|
|
91
|
+
# Bash hook that appends to `$CLAUDE_PROJECT_DIR/_hook_evidence.txt`, installed via
|
|
92
|
+
# `--setup 'cp <settings.json> .claude/settings.json'`, FIRED inside this runner's own clone
|
|
93
|
+
# (act mode, deny settings.local.json present) and in a bare `claude -p` clone, trusted or not.
|
|
94
|
+
# Why the 2026-08-30 arm saw nothing — CONFIRMED the same day, one more known pair: with the
|
|
95
|
+
# real project hook set copied into a clone, an arm asked to build a new mechanism script FOUND
|
|
96
|
+
# PRIOR ART and never called Write (identity-④ behaviour), so the Write-matched PriorArt hook had
|
|
97
|
+
# nothing to fire on; an arm forced to Write a new scripts/*.sh produced `.prior_art_events.tsv`
|
|
98
|
+
# with a FIRE row inside the clone. The stimulus never reached the hook — a hook that is never
|
|
99
|
+
# triggered looks identical to a hook that cannot run, which is why a control arm whose hook is
|
|
100
|
+
# KNOWN to fire is mandatory. Signal: tracks/_meta/fh_signal_2026-09-03_runner-hooks-do-fire.md
|
|
101
|
+
# What SURVIVES from the old paragraph: before claiming a hook-dependent result, check the
|
|
102
|
+
# hook's own evidence file INSIDE the clone; absence of that file invalidates the ARM, never the
|
|
103
|
+
# hypothesis. And `--setup` writing an UNTRACKED file (.claude/settings.json is gitignored) is
|
|
104
|
+
# not contamination — the tree baseline is taken after setup, and git status never sees it.
|
|
79
105
|
#
|
|
80
106
|
# 🟥 CONTROL IS NOT OPTIONAL. Always run at least one arm whose correct answer is "the thing
|
|
81
107
|
# being measured should NOT fire". An instrument that fires on everything measures nothing
|
|
82
108
|
# ([[feedback_control_presence_is_not_discrimination]]).
|
|
109
|
+
#
|
|
110
|
+
# ── ⓒ 격리 그라운딩 — 날짜 오염 통제 (six_axis_review_2026-09-04 강화 #3, RECORD ONLY) ──────────
|
|
111
|
+
# WHY: LiveCodeBench (arXiv 2403.07974) and the contamination-taxonomy literature
|
|
112
|
+
# (frontier_verification_map_2026-09-04.md §ⓒ) filter benchmark items by RELEASE DATE precisely
|
|
113
|
+
# because a model can have memorized — not reasoned about — anything published before its
|
|
114
|
+
# training cutoff. This runner has the same exposure: a sim arm's "correct" answer about a
|
|
115
|
+
# doc/rule/gate can be recall of that doc's OWN CONTENT (if the doc predates the model's cutoff
|
|
116
|
+
# and the model happened to see this repo, or a near-identical one, during training) rather than
|
|
117
|
+
# evidence the harness under test actually delivers the behaviour to a cold session.
|
|
118
|
+
# THIS IS A RECORDING, NOT A GATE — per CLAUDE.md §Mechanization Boundary, "what the right value
|
|
119
|
+
# IS" stays judgment; only "was it recorded" is a channel. Below writes two fields per rep to
|
|
120
|
+
# `<out>/<arm>_r<rep>.meta.tsv` and does NOT compare them or fail a run on their basis:
|
|
121
|
+
# corpus_head_date — the clone's HEAD commit date (ISO 8601). PROXY, named honestly: this
|
|
122
|
+
# runner cannot resolve which files a given PROMPT actually cites, so it
|
|
123
|
+
# records the whole corpus's most recent commit as an upper bound on
|
|
124
|
+
# "how fresh could the cited material be", not a per-file citation date.
|
|
125
|
+
# sim_model / sim_model_cutoff — the `--model` value and a best-effort known cutoff via
|
|
126
|
+
# `_model_known_cutoff()` below, `UNKNOWN` when not recorded there. This
|
|
127
|
+
# table WILL go stale as new models ship — it is deliberately not treated
|
|
128
|
+
# as authoritative; a scoring session should verify the cutoff it cites
|
|
129
|
+
# rather than trust this file's guess (§Instrument-Calibration).
|
|
130
|
+
#
|
|
131
|
+
# ── ⓒ 격리 그라운딩 — 일회용 샌드박스 표준 (six_axis_review_2026-09-04 강화 #3) ─────────────────
|
|
132
|
+
# 프런티어 대조표가 "ephemeral wipe-and-reseed 샌드박스(E2B/Modal/Runloop)"를 도입 후보로 든다.
|
|
133
|
+
# 조직 제약상 외부 도구는 보류하고(§도입 후보 3), 이 러너가 "이미 그렇게 동작하는 부분"과
|
|
134
|
+
# "아닌 부분"을 정직하게 갈라 적는다 — 매번 재발견하지 않도록.
|
|
135
|
+
#
|
|
136
|
+
# WIPE (매 rep 마다 새로 만든다 — 재사용 없음):
|
|
137
|
+
# ✅ 이미 참 — 매 rep 이 독립된 `git clone` (§위 "A disposable clone per REP") + 독립된 부모
|
|
138
|
+
# 디렉터리(§L10). 오늘 실행한 실측 (`test_sim_isolated_run_lanes.sh`):
|
|
139
|
+
# ✅ L10 no clone shares a parent with another (2 clones, 0 co-parented)
|
|
140
|
+
# ✅ L10b control — 2 clones found (L10 did not pass on an empty set)
|
|
141
|
+
# (재현: `bash scripts/test_sim_isolated_run_lanes.sh` — 위 두 줄이 그대로 나온다.)
|
|
142
|
+
# RESEED (팔이 못 보면 안 되는 자산을 클론에서 지운다):
|
|
143
|
+
# ✅ 이미 참 — ARM_BLIND_PATHS 제거(§아래) + 그 자체가 known-pair 로 검정됨:
|
|
144
|
+
# ✅ L24 팔 눈가림 자산이 클론에서 제거된다 (히트 >0 → 0)
|
|
145
|
+
# 기계-표면 리셋 (LaunchAgents/crontab/settings — VM 리셋과 달리 "지우고 다시 만들지" 않는다):
|
|
146
|
+
# 🟥 아직 부분 — observe 모드는 도구 자체가 없어 못 건드리고(구조적 예방), act 모드는 실제
|
|
147
|
+
# 변경을 만들고 나서 **탐지**만 한다(§L7). "매 rep 전 기계 상태를 초기화"는 하지 않는다 —
|
|
148
|
+
# E2B/Modal 급 VM wipe-and-reseed와의 실제 델타는 이 한 줄이다. 진짜 OS 샌드박스가 필요한
|
|
149
|
+
# 이유는 헤더 상단 "WHAT THIS GIVES YOU" 절이 이미 말한다 — 여기서 되풀이하지 않는다.
|
|
150
|
+
# 요약: 파일시스템 코퍼스 축(WIPE+RESEED)은 클론 단위로 이미 wipe-and-reseed다. 기계 표면 축은
|
|
151
|
+
# 아니다 — 탐지기이지 리셋기가 아니다. 이 두 문장이 "샌드박스 표준"의 정직한 전부다.
|
|
83
152
|
|
|
84
153
|
set -uo pipefail
|
|
85
154
|
|
|
@@ -274,6 +343,16 @@ snapshot() {
|
|
|
274
343
|
} > "$f"
|
|
275
344
|
}
|
|
276
345
|
|
|
346
|
+
# ⓒ 날짜 오염 통제 — 기록 전용 헬퍼(§헤더 참조). 판정 아님, 있으면 값·없으면 UNKNOWN.
|
|
347
|
+
# 🟥 이 표는 부패한다(stale) — 새 모델이 나올 때마다 손으로 갱신해야 한다. 여기 적힌 값을
|
|
348
|
+
# «검증됨»으로 인용하지 마라: 이 스크립트 자신이 그렇게 적어두라고 요구한다(§Instrument-Calibration).
|
|
349
|
+
_model_known_cutoff() { # $1=--model 값 → ISO 월 또는 UNKNOWN
|
|
350
|
+
case "$1" in
|
|
351
|
+
*sonnet-5*|sonnet) printf '2026-01' ;; # 이 세션의 시스템 프롬프트가 스스로 적은 값
|
|
352
|
+
*) printf 'UNKNOWN' ;;
|
|
353
|
+
esac
|
|
354
|
+
}
|
|
355
|
+
|
|
277
356
|
echo "── sim_isolated_run ──────────────────────────────────────────────"
|
|
278
357
|
echo "arm=$ARM mode=$MODE model=$MODEL reps=$REPS timeout=${TIMEOUT}s"
|
|
279
358
|
echo "out=$OUTDIR"
|
|
@@ -318,6 +397,17 @@ for r in $(seq 1 "$REPS"); do
|
|
|
318
397
|
continue
|
|
319
398
|
fi
|
|
320
399
|
|
|
400
|
+
# ⓒ 날짜 오염 통제 — 기록만, 판정 아님(§헤더). corpus_head_date 는 "프롬프트가 인용하는
|
|
401
|
+
# 파일들"의 날짜가 아니라 그 상한(코퍼스 전체 HEAD)이다 — 러너는 프롬프트가 무엇을 인용하는지
|
|
402
|
+
# 모르므로 더 좁게 잴 수 없다. 그렇게 좁혀 적는다.
|
|
403
|
+
_corpus_date=$(git -C "$WORK" log -1 --format=%cI 2>/dev/null); [ -n "$_corpus_date" ] || _corpus_date="UNKNOWN"
|
|
404
|
+
_cutoff=$(_model_known_cutoff "$MODEL")
|
|
405
|
+
{
|
|
406
|
+
printf 'corpus_head_date\t%s\n' "$_corpus_date"
|
|
407
|
+
printf 'sim_model\t%s\n' "$MODEL"
|
|
408
|
+
printf 'sim_model_cutoff\t%s\n' "$_cutoff"
|
|
409
|
+
} > "$OUTDIR/${ARM}_r${r}.meta.tsv"
|
|
410
|
+
|
|
321
411
|
# ── 🟥 팔이 읽으면 안 되는 tracked 자산을 «클론 안에서» 제거한다 (2026-09-01) ──────
|
|
322
412
|
# 왜: 얼린 정답지(`scripts/fixtures/knownpair_refusal_48_*`)가 tracked 가 되면서
|
|
323
413
|
# **모든 팔의 클론에 들어갔다.** 실측 — negative 문항의 핵심 명사구가 그 안에 있다
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_backtick_guard_lanes.sh — known pairs for scripts/backtick_guard.sh. Written BEFORE the detector.
|
|
3
|
+
#
|
|
4
|
+
# WHAT IS BEING GUARDED
|
|
5
|
+
# A backtick inside a shell DOUBLE-QUOTING CONTEXT — an unquoted heredoc body (`<<EOF`) or a
|
|
6
|
+
# "double-quoted string" — is command substitution: the text between the backticks is REPLACED by
|
|
7
|
+
# the command's output. With no such command the output is empty, so the text is DELETED; with one,
|
|
8
|
+
# foreign content is INSERTED. The sentence stays grammatical (only its subject is gone), the only
|
|
9
|
+
# signal is one `command not found` line at the top of the output, and every marker/record hook
|
|
10
|
+
# checks a field's presence, not its completeness.
|
|
11
|
+
# Measured 7× (2026-08-10 · 2026-09-01 ×3 · 2026-09-02 ×4 — marker, RESULT doc, fh_completed echo ×2)
|
|
12
|
+
# with a resident memory rule that failed each time because the actor's task had a different NAME
|
|
13
|
+
# (writing a marker · a failure message · a seal). N≥3 → mechanize (weekly_audit_2026-09-02 HIGH #1).
|
|
14
|
+
#
|
|
15
|
+
# BT1 — unquoted heredoc body (`<<TAG`, `<<-TAG`; NOT `<<'TAG'` / `<<"TAG"` / `<<\TAG`) containing `
|
|
16
|
+
# BT2 — double-quoted string containing ` (single-quoted text and `\`` are literal → CLEAN)
|
|
17
|
+
#
|
|
18
|
+
# Surface = the Bash tool call itself (interactively-composed commands), same reasoning as
|
|
19
|
+
# pipe_verdict_guard: every recurrence was in a composed command, none in a shipped file.
|
|
20
|
+
|
|
21
|
+
set -u
|
|
22
|
+
G="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/backtick_guard.sh"
|
|
23
|
+
pass=0; fail=0
|
|
24
|
+
|
|
25
|
+
# expect <label> <HIT|CLEAN> <command-string> (the command text is passed raw via --stdin-raw)
|
|
26
|
+
expect() {
|
|
27
|
+
local label="$1" want="$2" cmd="$3" out got
|
|
28
|
+
out=$(printf '%s' "$cmd" | bash "$G" --stdin-raw 2>&1)
|
|
29
|
+
# want may be HIT · CLEAN · HIT:BT1@2 (rule and line asserted — a hit on the WRONG line is a miss)
|
|
30
|
+
if printf '%s' "$out" | grep -q 'BACKTICK'; then got=HIT; else got=CLEAN; fi
|
|
31
|
+
case "$want" in HIT:*) printf '%s' "$out" | grep -q "${want#HIT:BT}" 2>/dev/null; :; esac
|
|
32
|
+
if [ "$got" = HIT ] && [ "${want%%:*}" = HIT ] && [ "$want" != HIT ]; then
|
|
33
|
+
local r="${want#HIT:}"; r="${r%@*}"; local l="${want##*@}"
|
|
34
|
+
printf '%s' "$out" | grep -q "$r L$l " || got="HIT-WRONG($(printf '%s' "$out" | grep -oE 'BT[12] L[0-9]+' | head -1))"
|
|
35
|
+
[ "$got" = HIT ] && got="$want"
|
|
36
|
+
fi
|
|
37
|
+
if [ "$got" = "$want" ]; then
|
|
38
|
+
printf ' ✅ %-56s %s (expected %s)\n' "$label" "$got" "$want"; pass=$((pass+1))
|
|
39
|
+
else
|
|
40
|
+
printf ' ❌ %-56s %s (expected %s)\n' "$label" "$got" "$want"; fail=$((fail+1))
|
|
41
|
+
printf ' cmd: %s\n out: %s\n' "$cmd" "$out"
|
|
42
|
+
fi
|
|
43
|
+
}
|
|
44
|
+
BT='`' # one backtick, spelled once so the lanes below never carry a live one in double quotes
|
|
45
|
+
NL=$'\n'
|
|
46
|
+
|
|
47
|
+
echo "[backtick-guard] known pairs"
|
|
48
|
+
echo "-- BT1: unquoted heredoc body --"
|
|
49
|
+
# 2026-09-02 ⓐ — the measured shape: a marker written through an unquoted heredoc so a $VAR expands.
|
|
50
|
+
expect "BT1 measured: marker heredoc" HIT:BT1@3 "cat > m.marker <<EOF${NL}date: \$TODAY${NL}axis2-evidence: 러너가 ${BT}--keep-blind-paths${BT} 를 삼켰다${NL}EOF"
|
|
51
|
+
expect "BT1 <<-TAG (tab-stripped) form" HIT "cat <<-EOF${NL} note ${BT}x${BT}${NL} EOF"
|
|
52
|
+
expect "BT1 markdown fence in heredoc" HIT "cat > r.md <<EOF${NL}\`\`\`${NL}out${NL}\`\`\`${NL}EOF"
|
|
53
|
+
expect "BT1 second of two heredocs on a line" HIT:BT1@4 "diff <(cat <<'A') <(cat <<B)${NL}${BT}a${BT}${NL}A${NL}${BT}b${BT}${NL}B"
|
|
54
|
+
expect "BT1 quoted <<'EOF' is CLEAN" CLEAN "cat > m.marker <<'EOF'${NL}evidence: ${BT}--keep-blind-paths${BT} 삼킴${NL}EOF"
|
|
55
|
+
expect "BT1 quoted <<\"EOF\" is CLEAN" CLEAN "cat <<\"EOF\"${NL}${BT}x${BT}${NL}EOF"
|
|
56
|
+
expect "BT1 escaped <<\\EOF is CLEAN" CLEAN "cat <<\\EOF${NL}${BT}x${BT}${NL}EOF"
|
|
57
|
+
expect "BT1 backslash-escaped backtick CLEAN" CLEAN "cat <<EOF${NL}see \\${BT}x\\${BT}${NL}EOF"
|
|
58
|
+
expect "BT1 backtick AFTER the body ends" CLEAN "cat <<EOF${NL}plain${NL}EOF${NL}echo '${BT}later${BT}'"
|
|
59
|
+
expect "BT1 <<< herestring is not a heredoc" CLEAN "grep -c x <<< 'a ${BT}b${BT}'"
|
|
60
|
+
|
|
61
|
+
echo "-- BT2: double-quoted string --"
|
|
62
|
+
# 2026-09-02 ⓑ — the measured shape: a completion-log append through echo "…".
|
|
63
|
+
expect "BT2 measured: echo append" HIT:BT2@1 "echo \"- ✅ 러너 ${BT}sim_isolated_run.sh${BT} 헤더 경고\" >> tracks/_meta/fh_completed.md"
|
|
64
|
+
expect "BT2 failure-message string (09-01)" HIT "printf '%s\\n' \"(${BT}nameleak_check.sh gen${BT} 을 써라)\""
|
|
65
|
+
expect "BT2 single quote inside dq is inert" HIT "echo \"don't ${BT}x${BT}\""
|
|
66
|
+
expect "BT2 single-quoted is CLEAN" CLEAN "printf '%s\\n' '- ✅ 러너 ${BT}sim_isolated_run.sh${BT} 헤더' >> log.md"
|
|
67
|
+
expect "BT2 escaped \\\` is CLEAN" CLEAN "echo \"see \\${BT}x\\${BT}\""
|
|
68
|
+
expect "BT2 dq inside single quotes is CLEAN" CLEAN "echo '\"${BT}x${BT}\"'"
|
|
69
|
+
expect "BT2 sq inside \$( ) inside dq CLEAN" CLEAN "echo \"\$(printf '%s' '${BT}x${BT}')\""
|
|
70
|
+
expect "BT2 no backtick at all" CLEAN "echo \"\$(git log -1) done\" && cat <<EOF${NL}plain \$X${NL}EOF"
|
|
71
|
+
expect "BT2 bare backtick outside quotes" CLEAN "V=${BT}date${BT}; echo ok" # live command substitution on purpose, not a text context
|
|
72
|
+
|
|
73
|
+
echo "-- Axis-2 pass 2026-09-03 (challenger, repros executed by the governor) --"
|
|
74
|
+
# A1: the first build stripped single-quoted spans BEFORE matching heredoc operators, so <<'EOF' was
|
|
75
|
+
# never a heredoc — three symptoms from one cause. Each pinned in its real shape.
|
|
76
|
+
expect "A1a quoted body with dq+backtick CLEAN" CLEAN "cat <<'EOF'${NL}axis2-evidence: 메시지 \"use ${BT}x${BT}\" 가 떴다${NL}EOF"
|
|
77
|
+
expect "A1b quoted A then unquoted B: only B" CLEAN "diff <(cat <<'A') <(cat <<B)${NL}${BT}a${BT}${NL}A${NL}plain${NL}B"
|
|
78
|
+
expect "A1c apostrophe in quoted body, then echo" HIT:BT2@4 "cat <<'EOF'${NL}don't${NL}EOF${NL}echo \"${BT}x${BT}\" >> log"
|
|
79
|
+
# A2: a comment's apostrophe must not open a single-quote context that swallows the next line.
|
|
80
|
+
expect "A2 comment apostrophe then echo" HIT:BT2@2 "# don't re-run this${NL}echo \"${BT}x${BT}\" >> f.md"
|
|
81
|
+
expect "A2 url fragment is not a comment" HIT:BT2@1 "curl \"https://x/a#frag ${BT}x${BT}\""
|
|
82
|
+
# B3: <<TAG inside a double-quoted string (commit message) opens no heredoc.
|
|
83
|
+
expect "B3 <<EOF in commit message is CLEAN" CLEAN "git commit -m \"docs: prefer <<EOF for markers\"${NL}V=${BT}date${BT}; echo ok"
|
|
84
|
+
# B5: escaped backslash + LIVE backtick.
|
|
85
|
+
expect "B5 \\\\ then live backtick HITs" HIT:BT1@2 "cat <<EOF${NL}path\\\\${BT}x${BT}${NL}EOF"
|
|
86
|
+
# B2: ANSI-C $'…' with an escaped apostrophe does not end early.
|
|
87
|
+
expect "B2 \$'don\\'t' then dq backtick" HIT:BT2@1 "echo \$'don\\'t' \"${BT}x${BT}\""
|
|
88
|
+
# B1: backtick inside \$( ) re-entered from dq is live substitution — deliberately NOT flagged.
|
|
89
|
+
expect "B1 backtick inside \$( ) in dq CLEAN" CLEAN "echo \"\$(echo ${BT}x${BT})\""
|
|
90
|
+
|
|
91
|
+
echo "-- hook mode: JSON payload in → JSON out (A4: detection is worthless if delivery is 0) --"
|
|
92
|
+
jexp() { # <label> <expect-substring-in-additionalContext|SILENT> <env> <payload>
|
|
93
|
+
local label="$1" want="$2" env_="$3" payload="$4" out ctx
|
|
94
|
+
out=$(printf '%s' "$payload" | env $env_ bash "$G" 2>/dev/null)
|
|
95
|
+
if [ "$want" = SILENT ]; then
|
|
96
|
+
if [ -z "$out" ]; then printf ' ✅ %-56s SILENT\n' "$label"; pass=$((pass+1)); else printf ' ❌ %-56s expected SILENT, got: %s\n' "$label" "${out:0:80}"; fail=$((fail+1)); fi
|
|
97
|
+
return
|
|
98
|
+
fi
|
|
99
|
+
ctx=$(printf '%s' "$out" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["hookSpecificOutput"]["additionalContext"])' 2>/dev/null)
|
|
100
|
+
if printf '%s' "$ctx" | grep -q "$want"; then printf ' ✅ %-56s JSON additionalContext carries %s\n' "$label" "$want"; pass=$((pass+1))
|
|
101
|
+
else printf ' ❌ %-56s no JSON/context (%s)\n' "$label" "${out:0:80}"; fail=$((fail+1)); fi
|
|
102
|
+
}
|
|
103
|
+
P='{"tool_name":"Bash","tool_input":{"command":"echo \"- done `x.sh` ok\""}}'
|
|
104
|
+
jexp "JSON payload → additionalContext" "BT2 L1" "X=1" "$P"
|
|
105
|
+
jexp "ascii PYTHONIOENCODING still emits" "BT2 L1" "PYTHONIOENCODING=ascii" "$P"
|
|
106
|
+
jexp "non-Bash tool is SILENT" SILENT "X=1" '{"tool_name":"Write","tool_input":{"content":"`x`"}}'
|
|
107
|
+
jexp "unparseable payload is SILENT" SILENT "X=1" 'not json'
|
|
108
|
+
|
|
109
|
+
echo "-- opt-out / payload --"
|
|
110
|
+
expect "noqa exempts" CLEAN "echo \"${BT}x${BT}\" # noqa: backtick"
|
|
111
|
+
expect "empty payload" CLEAN ""
|
|
112
|
+
|
|
113
|
+
echo
|
|
114
|
+
echo "[backtick-guard] $pass passed, $fail failed"
|
|
115
|
+
[ "$fail" -eq 0 ]
|