@chrono-meta/fh-gate 2.15.0 → 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 (48) 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 +20 -3
  9. package/knowledge/shared/learnings/subagent_invocations_log.yaml +111 -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 +49 -0
  14. package/plugins/fh-meta/skills/salience-splitter/SKILL.md +1 -1
  15. package/scripts/backtick_guard.sh +194 -0
  16. package/scripts/capability_effect_probe.sh +14 -1
  17. package/scripts/context_continuity_score.sh +106 -12
  18. package/scripts/fh-gate.sh +3 -3
  19. package/scripts/files_manifest_shipping_check.sh +19 -0
  20. package/scripts/gate_pathspec_check.sh +1 -1
  21. package/scripts/package_coverage_check.sh +25 -2
  22. package/scripts/proposal_hook.sh +89 -0
  23. package/scripts/public_surface_scan_files.sh +11 -2
  24. package/scripts/revert_probe.sh +250 -0
  25. package/scripts/selfcheck.sh +67 -3
  26. package/scripts/sim_isolated_run.sh +97 -7
  27. package/scripts/test_backtick_guard_lanes.sh +115 -0
  28. package/scripts/test_degrade_scan_shell_probes.sh +7 -7
  29. package/scripts/test_files_manifest_shipping_lanes.sh +5 -5
  30. package/scripts/test_heavy_classifier_lanes.sh +1 -1
  31. package/scripts/test_lane_runner_lanes.sh +59 -33
  32. package/scripts/test_mapped_tracks_lanes.sh +1 -1
  33. package/scripts/test_marker_soul_check_lanes.sh +24 -0
  34. package/scripts/test_node_check_lanes.sh +34 -34
  35. package/scripts/test_package_coverage_lanes.sh +53 -27
  36. package/scripts/test_pipe_verdict_guard_lanes.sh +5 -5
  37. package/scripts/test_precommit_pointer_index_lanes.sh +33 -0
  38. package/scripts/test_preprep_drift_anchor.sh +13 -4
  39. package/scripts/test_preprep_drift_anchor_lanes.sh +23 -0
  40. package/scripts/test_proposal_hook_lanes.sh +36 -0
  41. package/scripts/test_revert_probe_lanes.sh +146 -0
  42. package/scripts/test_session_close_lanes.sh +3 -5
  43. package/scripts/test_sim_isolated_run_lanes.sh +17 -0
  44. package/scripts/test_verdict_watermark_lanes.sh +27 -1
  45. package/scripts/utterance_landing_check.sh +2 -2
  46. package/templates/.git-hooks/pre-commit +27 -4
  47. package/templates/settings.PreToolUse.snippet.json +37 -1
  48. package/plugins/fh-commons/README.md +0 -38
@@ -63,6 +63,7 @@ ACCEPTED_ABSENT=(
63
63
  "scripts/round/target_pin.sh"
64
64
  "scripts/round/instrument_manifest.sh"
65
65
  "scripts/round/eligcheck_qset.sh"
66
+ "scripts/round/gatecheck_qset.sh" # 같은 이유 — 회차 개시 게이트, 소비자 표면 아님 (2026-09-02 짝표 등재로 참조가 생겼다)
66
67
  "scripts/test_round_instruments_lanes.sh"
67
68
  "scripts/fixtures/isolation_assembly_BROKEN_2026-08-30_ccrun7.json" # 역사 산출물(등급표가 증거로 인용)
68
69
  "scripts/outbound_query_guard.sh"
@@ -359,12 +360,34 @@ if ORACLE == 'tarball':
359
360
  if r.returncode != 0:
360
361
  print(f"ORACLE_UNAVAILABLE\tnpm pack exited {r.returncode}")
361
362
  raise SystemExit(2)
362
- packed = {f['path'] for f in json.loads(r.stdout)[0]['files']}
363
+ # Two shapes have been seen for `npm pack --dry-run --json`: the documented one carries
364
+ # `[0]['files'][*]['path']`; inside `npm publish`'s prepublishOnly on the CI runner
365
+ # (Node 22 / npm 10, 2026-09-04, v3.0.0 first OIDC publish) the same call returned JSON
366
+ # WITHOUT that key and this block died with a bare KeyError — fail-closed (correct) but
367
+ # blind (no diagnosis). Parse defensively, and when the JSON does not carry a file list
368
+ # fall back to the text listing (`npm notice <size> <path>` lines), which is what a human
369
+ # reads. The diagnostic line prints the head of stdout so the NEXT failure names its shape.
370
+ parsed = json.loads(r.stdout)
371
+ entry = parsed[0] if isinstance(parsed, list) and parsed else (parsed if isinstance(parsed, dict) else None)
372
+ flist = (entry or {}).get('files') if isinstance(entry, dict) else None
373
+ if flist and all(isinstance(f, dict) and 'path' in f for f in flist):
374
+ packed = {f['path'] for f in flist}
375
+ else:
376
+ t = subprocess.run(['npm', 'pack', '--dry-run'], capture_output=True, text=True, timeout=180)
377
+ lines = (t.stdout + '\n' + t.stderr).splitlines()
378
+ packed = set()
379
+ for ln in lines:
380
+ m = re.match(r'^npm notice\s+[0-9.]+[kMG]?B\s+(\S+)\s*$', ln)
381
+ if m:
382
+ packed.add(m.group(1))
383
+ if not packed:
384
+ print(f"ORACLE_UNAVAILABLE\tnpm pack --json had no files[].path and the text listing had no file lines; json head: {r.stdout[:200]!r}")
385
+ raise SystemExit(2)
363
386
  except FileNotFoundError:
364
387
  print("ORACLE_UNAVAILABLE\tnpm is not on PATH — the tarball cannot be read")
365
388
  raise SystemExit(2)
366
389
  except (json.JSONDecodeError, KeyError, IndexError) as e:
367
- print(f"ORACLE_UNAVAILABLE\tnpm pack --json did not parse ({type(e).__name__})")
390
+ print(f"ORACLE_UNAVAILABLE\tnpm pack --json did not parse ({type(e).__name__}); stdout head: {r.stdout[:200]!r}")
368
391
  raise SystemExit(2)
369
392
  except subprocess.TimeoutExpired:
370
393
  print("ORACLE_UNAVAILABLE\tnpm pack timed out")
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env bash
2
+ # proposal_hook.sh — PreToolUse(Edit|Write|Bash) advisory: a verdict/guard line in scripts/**/*.sh or
3
+ # templates/*.sh is about to change → put ONE proposal instruction into the model's context
4
+ # (additionalContext): «offer the user a known-pair control + degrade_direction_scan.sh in one line».
5
+ #
6
+ # WHY A HOOK (identity ⑤, measured 2026-09-03)
7
+ # r3: a CLAUDE.md table row keyed on this exact file class fired 1/15 at floor tier (that 1 a
8
+ # recitation). r4: this hook, installed in a disposable clone, fired on 9/10 editing reps and the
9
+ # floor session relayed the proposal 9/9; the hard negative (a usage-string edit in a .sh) 0/5.
10
+ # Same tasks, same tier, same prose layer — the row 0, the channel 100%. That is the
11
+ # «explicit instruction 3/3 · advisory 0/3 · framing 0/3» result of 2026-08-21 seen a second time
12
+ # (tracks/_meta/RESULT_2026-09-03_identity5-r4.md · prior_art_prompt.sh header). A channel is
13
+ # built at the channel (§Mechanization Boundary); what the proposal SAYS stays the model's.
14
+ #
15
+ # WHAT IT DOES NOT CLAIM
16
+ # Relaying an injected instruction is not initiative. Of r4's 9 hits, 4 carried task-specific
17
+ # substance beyond the hook's own wording (the K1 «judgment residue» — innovator signal
18
+ # fh_signal_2026-09-03_innovator-identity5-r4.md). This hook opens the window; whether ⑤'s bar
19
+ # («proposes unasked») counts a relayed proposal is the operator's call, recorded there, not here.
20
+ #
21
+ # DISCRIMINATOR (mechanical, quote-aware where it can be)
22
+ # file class : scripts/**/*.sh · templates/*.sh (docs, tracks, tests-as-fixtures: no)
23
+ # edit kind : the touched text carries a verdict/guard token — exit N · return N ·
24
+ # `|| continue|exit|true|return` · `&& continue|exit` · -ne/-eq/-gt/-lt · ==/!= ·
25
+ # `[ -e/-f/-s/-n/-z` · comm/diff/cmp · grep -q — AND for Edit the change is not
26
+ # confined to quoted strings (old/new with quotes stripped must differ). A usage
27
+ # string that happens to contain `exit 2` does not fire (r4 HARD 0/5).
28
+ # Bash path : an edit made through the shell (sed -i · > · >> · tee) — the r4 miss (T2 r5 edited
29
+ # via Bash, hook 0). No old/new here, so the rule is weaker: target file class AND the
30
+ # the RAW command text (quotes included) carries a token. Quotes are NOT stripped on
31
+ # this path — in `sed -i 's/exit 1/exit 2/' x.sh` the token is inside the quotes by
32
+ # construction, and stripping made the path silent on exactly the shape it exists for
33
+ # (found by the Air node 2026-09-03; the original lane's known-positive only fired
34
+ # because its token sat outside the quotes). Named residual now: over-fire when a
35
+ # quoted token elsewhere in the command co-occurs with a script-file edit (advisory).
36
+ #
37
+ # DEGRADE DIRECTION: advisory, exit 0 always, no permissionDecision (same contract as pipe_verdict_guard).
38
+ # Unparseable payload → silent. python3 absent → silent (a dead interpreter must not block edits).
39
+ # Evidence line appended to $CLAUDE_PROJECT_DIR/.claude/.proposal_hook_events.tsv (gitignored dir)
40
+ # so a sim arm can prove the hook fired INSIDE its clone (runner header: absence of that file
41
+ # invalidates the arm, never the hypothesis).
42
+ # Opt out on one call with `# noqa: proposal-hook`.
43
+ # test: printf '%s' '<PreToolUse JSON>' | bash scripts/proposal_hook.sh
44
+ set -u
45
+ RAW=$(cat 2>/dev/null || true)
46
+ printf '%s' "$RAW" | grep -qE '#[[:space:]]*noqa:?[[:space:]]*proposal-hook' && exit 0
47
+ read -r FP FLAG < <(printf '%s' "$RAW" | python3 -c '
48
+ import json,sys,re
49
+ try: d=json.load(sys.stdin)
50
+ except Exception: print("",""); sys.exit(0)
51
+ tn=d.get("tool_name",""); ti=d.get("tool_input",{}) or {}
52
+ TOK=r"exit [0-9]|return [0-9]|\|\| *(continue|exit|true|return)|&& *(continue|exit)|-ne |-eq |-gt |-lt | == | != |\[ -[efsnz] |\bcomm |\bdiff |\bcmp |grep -q"
53
+ def strip(x): return re.sub(r"\"[^\"]*\"|\x27[^\x27]*\x27","",x)
54
+ fp=""; flag="0"
55
+ if tn in ("Edit","Write"):
56
+ fp=ti.get("file_path","") or ""
57
+ old=ti.get("old_string","") or ""; new=(ti.get("new_string","") or ti.get("content","") or "")
58
+ touches=bool(re.search(TOK, old+"\n"+new)); real=strip(old).strip()!=strip(new).strip()
59
+ flag="1" if (touches and real) else "0"
60
+ elif tn=="Bash":
61
+ cmd=(ti.get("command","") or "").replace("\n"," ")
62
+ m=re.search(r"(?:sed\s+-i\S*(?:\s+(?:\x27[^\x27]*\x27|\"[^\"]*\"|\S+)){1,2}\s+|>>?\s*|tee\s+(?:-a\s+)?)[\"\x27]?([^\s\"\x27|;&)<>]+\.sh)\b", cmd)
63
+ if m:
64
+ fp=m.group(1); flag="1" if re.search(TOK, cmd) else "0" # raw cmd, NOT strip(): in a sed -i the token lives INSIDE the quoted expression by construction (Air 2026-09-03: a1 silent, known-positive only fired because its token sat outside the quotes)
65
+ print(fp, flag)
66
+ ' 2>/dev/null) || exit 0
67
+ [ -n "${FP:-}" ] || exit 0
68
+ case "$FP" in *scripts/*.sh|*templates/*.sh|scripts/*.sh|templates/*.sh|*/.git-hooks/*|.git-hooks/*) ;; *) exit 0 ;; esac # .git-hooks/* has no .sh suffix — the gate files themselves were outside the filter (arm C wt2 2026-09-03: pre-commit edit, no FIRE)
69
+ [ "${FLAG:-0}" = 1 ] || exit 0
70
+ _D="${CLAUDE_PROJECT_DIR:-.}/.claude"; mkdir -p "$_D" 2>/dev/null
71
+ printf '%s\t%s\t%s\n' "$(date -u +%FT%TZ)" "FIRE" "$FP" >> "$_D/.proposal_hook_events.tsv" 2>/dev/null
72
+ # ── Fact lines (r8, 2026-09-03): the two preconditions of the proposal are DETERMINISTIC, so the hook checks
73
+ # them itself and carries the result as a «사실» line — agents propose, solvers verify. Measured r8: on the
74
+ # stimulus whose grounds sit in a neighbouring file, wording-only (r7 B) got 1/5 withdraw/amend, fact lines
75
+ # got 4/5 (hand-judged, n=5). Sonnet used the fact as an INPUT (one rep rejected a stale fact against a
76
+ # reproduced bug; one opened the scan file itself) — it did not recite it.
77
+ # Self-lane case: editing `scripts/test_X_lanes.sh` IS the lane — r8's discriminator missed it and emitted
78
+ # a proposal for a lane that already was the file. Fixed here (F2 lane).
79
+ _ROOT="${CLAUDE_PROJECT_DIR:-.}"; _BN=$(basename "$FP" .sh); _FACT=""; _ITEMS=""
80
+ case "$_BN" in
81
+ test_*_lanes) _FACT="$_FACT · 사실: 이 파일 자체가 레인(known-pair 픽스처)이다 — 새 known-pair 컨트롤은 «이 파일 안에» 추가하거나 생략" ;;
82
+ *) if [ -f "$_ROOT/scripts/test_${_BN}_lanes.sh" ]; then _FACT="$_FACT · 사실: 이 파일의 레인 \`scripts/test_${_BN}_lanes.sh\` 가 이미 있다(known-pair 컨트롤은 거기에 붙이거나 생략)"; else _ITEMS="$_ITEMS known-pair(고친 케이스 + 반대 케이스) 컨트롤"; fi ;;
83
+ esac
84
+ _SCAN=$(ls -t "$_ROOT"/scripts/.degrade_scan_last_*.txt 2>/dev/null | head -1)
85
+ if [ -n "$_SCAN" ] && grep -q -- "$(basename "$FP")" "$_SCAN" 2>/dev/null; then _FACT="$_FACT · 사실: 오늘 degrade_direction_scan 결과 \`$(basename "$_SCAN")\` 가 이 파일을 이미 덮었다($(grep -m1 -oE 'findings: [0-9]+' "$_SCAN" 2>/dev/null || echo 'findings: ?')) — 스캐너 통과이지 손 확인이 아니다"; else _ITEMS="$_ITEMS degrade_direction_scan.sh 로 실패가 «통과»로 접히는 방향 확인"; fi
86
+ if [ -z "$_ITEMS" ]; then MSG="🔎 방금 고치는 줄은 판정·가드·종료코드 줄이다(scripts/*.sh · templates/*.sh)${_FACT}. 둘 다 이미 있으니 새 제안은 내지 말고, 작업을 마친 뒤 그 사실을 한 줄로만 말해라(형식: «확인 | basis: <위 사실>»)."
87
+ else MSG="🔎 방금 고치는 줄은 판정·가드·종료코드 줄이다(scripts/*.sh · templates/*.sh)${_FACT}. 없는 것만 사용자에게 한 줄로 제안해라 —${_ITEMS} — 형식은 «제안: … | basis: <네가 확인한 근거 한 구절>». 가능하면 이 파일·이 케이스의 실제 이름으로. 제안이지 실행이 아니다."; fi
88
+ python3 -c 'import json,sys; m=sys.argv[1]; print(json.dumps({"systemMessage":m,"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":m}}, ensure_ascii=False))' "$MSG" 2>/dev/null || exit 0
89
+ exit 0
@@ -124,7 +124,16 @@ fi
124
124
  # content-generating lifecycle is ever added.
125
125
  # ── Resolve the exact npm-published file set (fail-closed if unresolved OR partial) ──
126
126
  FILES=$(npm pack --dry-run --json 2>/dev/null \
127
- | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{JSON.parse(s)[0].files.forEach(f=>console.log(f.path))}catch(e){process.exit(3)}})' 2>/dev/null || true)
127
+ | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{JSON.parse(s)[0].files.forEach(f=>console.log(f.path))}catch(e){process.exit(3)}})' 2>/dev/null)
128
+ # 2026-09-04 (v3.0.0 first OIDC publish): on the CI runner, inside `npm publish`'s prepublishOnly,
129
+ # `npm pack --dry-run --json` returned JSON WITHOUT files[] and this resolution came back EMPTY —
130
+ # fail-closed (correct) but the publish could not proceed at all. Same fallback as the other two
131
+ # tarball readers (package_coverage_check.sh · files_manifest_shipping_check.sh): rebuild the set
132
+ # from the text listing (`npm notice <size> <path>`). Still fail-closed when THAT is empty too.
133
+ if [ -z "$FILES" ]; then
134
+ FILES=$(npm pack --dry-run 2>&1 | sed -nE 's/^npm notice +[0-9.]+[kMG]?B +([^ ]+) *$/\1/p')
135
+ [ -n "$FILES" ] && echo " ⚠️ npm pack --json carried no files[] — file set rebuilt from the text listing ($(printf '%s\n' "$FILES" | wc -l | tr -d ' ') paths)"
136
+ fi
128
137
  if [ -z "$FILES" ]; then
129
138
  echo " ❌ could not resolve the npm-published file set (npm pack --dry-run failed)."
130
139
  [ "${PUBLIC_SURFACE_OK:-0}" = "1" ] && { echo " ⚠️ proceeding by PUBLIC_SURFACE_OK=1"; exit 0; }
@@ -135,7 +144,7 @@ fi
135
144
  # Wrong-set guard (challenger M6): a future npm --json shape change could yield a NON-empty but PARTIAL
136
145
  # file list (forEach iterates a renamed/nested structure without throwing) → files silently unscanned.
137
146
  # npm always ships package.json in the tarball; its absence means the parse got a wrong set → fail-closed.
138
- if ! printf '%s\n' "$FILES" | grep -qx "package.json"; then
147
+ if ! printf '%s\n' "$FILES" | grep -qx "package.json"; then # portability-noqa: checks npm's own packaging invariant (every npm tarball ships package.json), not a repo-specific fixture read from disk — true for any ported npm package
139
148
  echo " ❌ published file set looks wrong — 'package.json' (always shipped) is absent from the parse."
140
149
  [ "${PUBLIC_SURFACE_OK:-0}" = "1" ] && { echo " ⚠️ proceeding by PUBLIC_SURFACE_OK=1"; exit 0; }
141
150
  echo " Fail-closed (possible npm --json shape change). Verify npm pack output or PUBLIC_SURFACE_OK=1."
@@ -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,12 +645,14 @@ 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" \
637
652
  "scripts/round/target_pin.sh|scripts/test_round_instruments_lanes.sh" \
638
653
  "scripts/round/instrument_manifest.sh|scripts/test_round_instruments_lanes.sh" \
639
- "scripts/round/eligcheck_qset.sh|scripts/test_round_instruments_lanes.sh"
654
+ "scripts/round/eligcheck_qset.sh|scripts/test_round_instruments_lanes.sh" \
655
+ "scripts/round/gatecheck_qset.sh|scripts/test_round_instruments_lanes.sh"
640
656
  do
641
657
  _subj="${_pair%%|*}"; _anc="${_pair##*|}"; _lbl="${_anc##*/}"
642
658
  if [ ! -f "$_subj" ]; then
@@ -1055,6 +1071,29 @@ else
1055
1071
  esac
1056
1072
  fi
1057
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
+
1058
1097
  # memory-link-check — the memory store is a GRAPH (memory_intent_recall.md: nodes=files,
1059
1098
  # edges=[[links]], recall walks one hop). Measured 2026-07-28: 50 of 872 edges pointed at a note
1060
1099
  # that existed under a different separator and 22 at nothing — a dead edge returns nothing and is
@@ -1142,6 +1181,20 @@ else
1142
1181
  fail=1
1143
1182
  fi
1144
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
+
1145
1198
  # 무효 워터마크 — 무효 회차의 «숫자 줄»이 자기 무효를 나르는가.
1146
1199
  # 🟥 회차 3 은 자기 게이트가 VOID 를 찍고도 그 숫자만 기록으로 넘어갔다(VOID 낱말은 0회).
1147
1200
  # 판정이 표 «밖»에 있었고 사람은 표를 복사하기 때문이다. 그 채널을 닫은 배선의 앵커다.
@@ -1543,6 +1596,17 @@ else
1543
1596
  fail=1
1544
1597
  fi
1545
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
+
1546
1610
  if [ ! -f scripts/halffix_propagation_scan.sh ]; then
1547
1611
  _absent_subject_verdict "test_halffix_lanes.sh" "scripts/halffix_propagation_scan.sh" || fail=1
1548
1612
  elif [ -f scripts/test_halffix_lanes.sh ]; then