@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.
Files changed (45) hide show
  1. package/.claude/rules/fh_4axis_gate.md +38 -0
  2. package/.claude-plugin/marketplace.json +2 -2
  3. package/CLAUDE.md +1 -1
  4. package/README.md +6 -1
  5. package/knowledge/shared/harness-core/fh_three_layer_canon.md +47 -0
  6. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +10 -0
  7. package/knowledge/shared/harness-core/measurement-integrity-checklist.md +15 -1
  8. package/knowledge/shared/harness-core/ship_readiness_gate.md +19 -2
  9. package/knowledge/shared/learnings/subagent_invocations_log.yaml +49 -0
  10. package/package.json +9 -1
  11. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  12. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  13. package/plugins/fh-meta/CHANGELOG.md +25 -0
  14. package/scripts/backtick_guard.sh +194 -0
  15. package/scripts/context_continuity_score.sh +49 -7
  16. package/scripts/fh-gate.sh +3 -3
  17. package/scripts/files_manifest_shipping_check.sh +19 -0
  18. package/scripts/gate_pathspec_check.sh +1 -1
  19. package/scripts/package_coverage_check.sh +24 -2
  20. package/scripts/proposal_hook.sh +89 -0
  21. package/scripts/public_surface_scan_files.sh +11 -2
  22. package/scripts/revert_probe.sh +250 -0
  23. package/scripts/selfcheck.sh +65 -2
  24. package/scripts/sim_isolated_run.sh +97 -7
  25. package/scripts/test_backtick_guard_lanes.sh +115 -0
  26. package/scripts/test_degrade_scan_shell_probes.sh +7 -7
  27. package/scripts/test_files_manifest_shipping_lanes.sh +5 -5
  28. package/scripts/test_heavy_classifier_lanes.sh +1 -1
  29. package/scripts/test_lane_runner_lanes.sh +59 -33
  30. package/scripts/test_mapped_tracks_lanes.sh +1 -1
  31. package/scripts/test_marker_soul_check_lanes.sh +24 -0
  32. package/scripts/test_node_check_lanes.sh +34 -34
  33. package/scripts/test_package_coverage_lanes.sh +53 -27
  34. package/scripts/test_pipe_verdict_guard_lanes.sh +5 -5
  35. package/scripts/test_precommit_pointer_index_lanes.sh +33 -0
  36. package/scripts/test_preprep_drift_anchor.sh +13 -4
  37. package/scripts/test_preprep_drift_anchor_lanes.sh +23 -0
  38. package/scripts/test_proposal_hook_lanes.sh +36 -0
  39. package/scripts/test_revert_probe_lanes.sh +146 -0
  40. package/scripts/test_session_close_lanes.sh +3 -5
  41. package/scripts/test_sim_isolated_run_lanes.sh +17 -0
  42. package/scripts/utterance_landing_check.sh +2 -2
  43. package/templates/.git-hooks/pre-commit +27 -4
  44. package/templates/settings.PreToolUse.snippet.json +37 -1
  45. 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
@@ -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" ] || continue
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 NOT RUN IN A DISPOSABLE CLONE — so this runner cannot measure anything
73
- # that depends on one. Measured 2026-08-30: an arm copied `.claude/settings.json` into its clone
74
- # to make the PreToolUse PriorArt hook live; not one of the three clones grew
75
- # `.claude/.prior_art_events.tsv`, while the live repo's copy carries entries from the same hour.
76
- # The hook never fired, so the "hook vs no-hook" contrast was HOOK ≡ NOHOOK and either verdict
77
- # would have been false. Before claiming a hook-dependent result, check the hook's own
78
- # evidence file INSIDE the clone; absence of that file invalidates the arm, not the hypothesis.
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 ]