@chrono-meta/fh-gate 2.15.1 → 3.1.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 (134) hide show
  1. package/.claude/regression/probes_live.yaml +137 -0
  2. package/.claude/rules/.residency-patterns.defaults +7 -0
  3. package/.claude/rules/fh_4axis_gate.md +88 -1
  4. package/.claude-plugin/marketplace.json +8 -2
  5. package/AGENTS.md +27 -0
  6. package/CATALOG.md +17 -0
  7. package/CLAUDE.md +13 -3
  8. package/README.ja.md +51 -7
  9. package/README.ko.md +48 -7
  10. package/README.md +43 -6
  11. package/README.zh.md +45 -8
  12. package/docs/STANDARDS_ALIGNMENT.md +61 -0
  13. package/docs/USER_GUIDE.md +3 -0
  14. package/docs/USE_CASES.md +50 -0
  15. package/docs/model_tier_expectations.md +60 -0
  16. package/knowledge/shared/harness-core/fh_three_layer_canon.md +77 -10
  17. package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +18 -0
  18. package/knowledge/shared/harness-core/harness_incubator_doctrine.md +9 -0
  19. package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +139 -0
  20. package/knowledge/shared/harness-core/measurement-integrity-checklist.md +35 -1
  21. package/knowledge/shared/harness-core/ship_readiness_gate.md +19 -2
  22. package/knowledge/shared/learnings/subagent_invocations_log.yaml +489 -3
  23. package/package.json +48 -2
  24. package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
  25. package/plugins/fh-commons/skills/preprep/README.md +4 -1
  26. package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
  27. package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
  28. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
  29. package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
  30. package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
  31. package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
  32. package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
  33. package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
  34. package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
  35. package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
  36. package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
  37. package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
  38. package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
  39. package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
  40. package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
  41. package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
  42. package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
  43. package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
  44. package/plugins/fh-meta/CHANGELOG.md +67 -0
  45. package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
  46. package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
  47. package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
  48. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
  49. package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
  50. package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
  51. package/plugins/fh-qp/README.md +71 -0
  52. package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
  53. package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
  54. package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
  55. package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
  56. package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
  57. package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
  58. package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
  59. package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
  60. package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
  61. package/plugins/fh-qp/qp_profile.example.yaml +29 -0
  62. package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
  63. package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
  64. package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
  65. package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
  66. package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
  67. package/scripts/backtick_guard.sh +194 -0
  68. package/scripts/chamber_run.sh +14 -5
  69. package/scripts/com.forge-harness.live-eval.plist +84 -0
  70. package/scripts/compaction_probe.sh +9 -35
  71. package/scripts/context_continuity_score.sh +49 -7
  72. package/scripts/directional_diff_gate.sh +14 -2
  73. package/scripts/fh-gate.sh +3 -3
  74. package/scripts/files_manifest_shipping_check.sh +19 -0
  75. package/scripts/frontier_digest_autopilot.sh +4 -1
  76. package/scripts/gate_pathspec_check.sh +1 -1
  77. package/scripts/map_postprocess.py +90 -0
  78. package/scripts/outbound_query_guard.sh +131 -0
  79. package/scripts/outbound_query_hook.sh +373 -0
  80. package/scripts/package_coverage_check.sh +79 -18
  81. package/scripts/pipe_verdict_guard.sh +41 -1
  82. package/scripts/probe_live_eval.sh +240 -0
  83. package/scripts/probe_live_eval_lib.py +579 -0
  84. package/scripts/proposal_hook.sh +192 -0
  85. package/scripts/public_surface_scan_files.sh +11 -2
  86. package/scripts/push_zone_check.sh +78 -0
  87. package/scripts/residency_closure_scan.py +252 -0
  88. package/scripts/revert_probe.sh +250 -0
  89. package/scripts/selfcheck.sh +106 -3
  90. package/scripts/session_close_check.sh +100 -0
  91. package/scripts/sim_isolated_run.sh +195 -9
  92. package/scripts/test_action_yml_lanes.sh +97 -0
  93. package/scripts/test_backtick_guard_lanes.sh +115 -0
  94. package/scripts/test_degrade_scan_shell_probes.sh +7 -7
  95. package/scripts/test_fh_qp_lanes.sh +105 -0
  96. package/scripts/test_files_manifest_shipping_lanes.sh +5 -5
  97. package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
  98. package/scripts/test_heavy_classifier_lanes.sh +1 -1
  99. package/scripts/test_lane_runner_lanes.sh +59 -33
  100. package/scripts/test_map_postprocess_lanes.sh +143 -0
  101. package/scripts/test_mapped_tracks_lanes.sh +1 -1
  102. package/scripts/test_marker_affected_lanes.sh +93 -0
  103. package/scripts/test_marker_crossfamily_lanes.sh +90 -6
  104. package/scripts/test_marker_oracle_lanes.sh +136 -0
  105. package/scripts/test_marker_soul_check_lanes.sh +24 -0
  106. package/scripts/test_node_check_lanes.sh +34 -34
  107. package/scripts/test_outbound_query_hook_lanes.sh +433 -0
  108. package/scripts/test_outbound_query_lanes.sh +87 -0
  109. package/scripts/test_package_coverage_lanes.sh +53 -27
  110. package/scripts/test_pipe_verdict_guard_lanes.sh +31 -5
  111. package/scripts/test_precommit_pointer_index_lanes.sh +33 -0
  112. package/scripts/test_preprep_diagram_lanes.sh +87 -0
  113. package/scripts/test_preprep_drift_anchor.sh +16 -7
  114. package/scripts/test_preprep_drift_anchor_lanes.sh +23 -0
  115. package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
  116. package/scripts/test_probe_live_eval_lanes.sh +437 -0
  117. package/scripts/test_proposal_hook_lanes.sh +57 -0
  118. package/scripts/test_push_zone_lanes.sh +304 -0
  119. package/scripts/test_residency_closure_lanes.sh +70 -0
  120. package/scripts/test_revert_probe_lanes.sh +146 -0
  121. package/scripts/test_session_close_lanes.sh +3 -5
  122. package/scripts/test_sim_isolated_run_lanes.sh +136 -0
  123. package/scripts/test_utterance_intake_lanes.sh +414 -0
  124. package/scripts/test_worktree_reclaim_lanes.sh +70 -0
  125. package/scripts/transcript_utterances.py +222 -0
  126. package/scripts/utterance_intake.sh +424 -0
  127. package/scripts/utterance_landing_check.sh +2 -2
  128. package/scripts/validate_yaml.sh +27 -0
  129. package/scripts/worktree_reclaim.sh +95 -0
  130. package/templates/.git-hooks/pre-commit +380 -5
  131. package/templates/.git-hooks/pre-push +91 -0
  132. package/templates/RED_TEAM_REPORT.md +49 -0
  133. package/templates/settings.PreToolUse.snippet.json +101 -1
  134. package/plugins/fh-commons/README.md +0 -38
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # test_proposal_hook_lanes.sh — known pairs for scripts/proposal_hook.sh (written before shipping; r4 KP + Bash path)
3
+ set -u; HDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"; T=$(mktemp -d); pass=0; fail=0
4
+ export CLAUDE_PROJECT_DIR="$T"
5
+ exp(){ local label="$1" want="$2" payload="$3" got; got=$(printf '%s' "$payload" | bash "$HDIR/proposal_hook.sh" 2>/dev/null | wc -c | tr -d ' '); [ "$got" -gt 0 ] && got=HIT || got=CLEAN
6
+ if [ "$got" = "$want" ]; then printf ' ✅ %-52s %s\n' "$label" "$got"; pass=$((pass+1)); else printf ' ❌ %-52s %s (expected %s)\n' "$label" "$got" "$want"; fail=$((fail+1)); fi; }
7
+ echo "[proposal-hook] known pairs"
8
+ exp "T1 Edit comm+LC_ALL (verdict compare)" HIT '{"tool_name":"Edit","tool_input":{"file_path":"/x/scripts/sim_isolated_run.sh","old_string":" comm -13 <(sort a) <(sort b) \\","new_string":" comm -13 <(LC_ALL=C sort a) <(LC_ALL=C sort b) \\"}}'
9
+ exp "T2 Edit adds fail-open guard" HIT '{"tool_name":"Edit","tool_input":{"file_path":"/x/scripts/capability_effect_probe.sh","old_string":" h=$(ls -A \"$HOME\" | shasum)","new_string":" h=$(ls -A \"$HOME\" | shasum) || { echo LS_FAILED; exit 10; }"}}'
10
+ exp "T3 Edit continue guard" HIT '{"tool_name":"Edit","tool_input":{"file_path":"/x/templates/regression_guard.sh","old_string":" [ -e \"$blk\" ] && [ -s \"$blk\" ] || continue","new_string":" [ -e \"$blk\" ] || { echo missing >&2; continue; }"}}'
11
+ exp "HARD Edit usage string only (r4 0/5)" CLEAN '{"tool_name":"Edit","tool_input":{"file_path":"/x/scripts/daily_report.sh","old_string":" *) echo \"usage: d.sh [run]\" >&2; exit 2 ;;","new_string":" *) echo \"usage: d.sh [run|--self-test] (DR_DATE)\" >&2; exit 2 ;;"}}'
12
+ exp "docs file with tokens" CLEAN '{"tool_name":"Edit","tool_input":{"file_path":"/x/docs/a.md","old_string":"a","new_string":"exit 1 || continue"}}'
13
+ exp "Write new scripts/*.sh with verdict" HIT '{"tool_name":"Write","tool_input":{"file_path":"/x/scripts/new_check.sh","content":"#!/bin/bash\n[ -f x ] || exit 1"}}'
14
+ exp "Bash sed -i on scripts/*.sh with token" HIT '{"tool_name":"Bash","tool_input":{"command":"sed -i \"\" \"s/comm -13/LC_ALL=C comm -13/\" scripts/sim_isolated_run.sh && [ -s out ] || exit 1"}}'
15
+ exp "Bash sed -i token ONLY inside quotes (a1)" HIT '{"tool_name":"Bash","tool_input":{"command":"sed -i \"\" \"s/exit 1/exit 2/\" scripts/target.sh"}}'
16
+ exp "Bash sed -i single-quoted token (a1b)" HIT '{"tool_name":"Bash","tool_input":{"command":"sed -i '"'"''"'"' '"'"'s/|| continue/|| { echo x; continue; }/'"'"' scripts/target.sh"}}'
17
+ exp "Bash sed -i no token anywhere (a3)" CLEAN '{"tool_name":"Bash","tool_input":{"command":"sed -i \"\" s/foo/bar/ scripts/target.sh"}}'
18
+ exp "Bash redirect into scripts/*.sh w/ token" HIT '{"tool_name":"Bash","tool_input":{"command":"printf \"[ -f x ] || exit 1\\n\" >> scripts/x.sh; grep -q y scripts/x.sh"}}'
19
+ exp "Bash redirect into docs (no)" CLEAN '{"tool_name":"Bash","tool_input":{"command":"echo \"exit 1\" >> docs/a.md || exit 1"}}'
20
+ exp "Bash ls only (no target)" CLEAN '{"tool_name":"Bash","tool_input":{"command":"ls scripts/ && [ -d scripts ] || exit 1"}}'
21
+ # ── 2026-09-04 계기 교체 lanes — fixtures are the SHAPES the first independent grading quoted
22
+ # (tracks/_meta/RESULT_2026-09-04_identity5-armC-live-count.md §계기 결함), not easier spellings.
23
+ # Fail-before: the HEAD~ hook (whole-command regex) is HIT on R2/R3/R4-ctrl/D3-*/D4 and CLEAN on D2-* (recorded in
24
+ # proposal_hook_repair_lanes.txt for the patch); the scanner hook inverts exactly those.
25
+ exp "R2 marker heredoc QUOTES sed -i … scripts/target.sh (row 2)" CLEAN '{"tool_name":"Bash","tool_input":{"command":"cat > tracks/_meta/.axes_23_2026-09-03.marker <<'"'"'MK'"'"'\naxes-run: ⓐ ⓑ\na1 `sed -i '"'"''"'"' '"'"'s/exit 1/exit 2/'"'"' scripts/target.sh` → HIT · lanes 16/16 · [ -s x ] || exit 1\nMK"}}'
26
+ exp "R3 gh pr --body QUOTES sed -i … scripts/target.sh (row 3)" CLEAN '{"tool_name":"Bash","tool_input":{"command":"gh pr create --title \"fix(hook): a1\" --body \"Lanes: a1 `sed -i '"'"''"'"' '"'"'s/exit 1/exit 2/'"'"' scripts/target.sh` now HIT; 16/16 · revert 14/16 || exit 1\""}}'
27
+ exp "R4-ctrl : > \"\$T/scripts/…\" fixture root (var path)" CLEAN '{"tool_name":"Bash","tool_input":{"command":"mkdir -p \"$T/scripts\"; : > \"$T/scripts/test_has_lane_lanes.sh\"; [ -f x ] || exit 1"}}'
28
+ exp "D2-P python heredoc open(p,\"w\") verdict edit (8746)" HIT '{"tool_name":"Bash","tool_input":{"command":"python3 - <<'"'"'PY'"'"'\np=\"scripts/package_coverage_check.sh\"\ns=open(p).read()\nassert s.count(\" exit 2\") == 1\ns=s.replace(\" exit 2\", \" echo \\\"PKG_ORACLE_MISSING: npm pack gave no files[]\\\" >&2; exit 2\")\nopen(p,\"w\").write(s)\nPY"}}'
29
+ exp "D2-P2 python heredoc q=Path(…); q.write_text (verdict)" HIT '{"tool_name":"Bash","tool_input":{"command":"python3 - <<'"'"'PY'"'"'\nfrom pathlib import Path\nq=Path(\"scripts/selfcheck.sh\")\nq.write_text(q.read_text().replace(\"run_lane x\", \"run_lane x || exit 1\"))\nPY"}}'
30
+ exp "D2-ctrl python heredoc comment-word replace (== in code only)" CLEAN '{"tool_name":"Bash","tool_input":{"command":"python3 - <<'"'"'PY'"'"'\np=\"scripts/sim_isolated_run.sh\"\ns=open(p).read()\nassert s.count(\"# 옛말\") == 1\ns=s.replace(\"# 옛말\", \"# 조직\")\nopen(p,\"w\").write(s)\nPY"}}'
31
+ exp "D3 row 6: sed comment word + || exit 1 in another segment" CLEAN '{"tool_name":"Bash","tool_input":{"command":"sed -i '"'"''"'"' '"'"'s/옛말/조직/g'"'"' scripts/sim_isolated_run.sh && bash scripts/test_sim_isolated_run_lanes.sh; rc=$?; [ $rc = 0 ] || exit 1"}}'
32
+ exp "D3 row 1: self-probe sed (no token in expr) + [ -s ] || exit 1" CLEAN '{"tool_name":"Bash","tool_input":{"command":"sed -i '"'"''"'"' '"'"'s/^#NOOP-PROBE-LINE$//'"'"' scripts/proposal_hook.sh && [ -s scripts/proposal_hook.sh ] || exit 1; tail -1 .claude/.proposal_hook_events.tsv"}}'
33
+ exp "D4 old shape: payload w/o token, check in next segment" CLEAN '{"tool_name":"Bash","tool_input":{"command":"printf \"%s\\n\" x >> scripts/x.sh; grep -q y scripts/x.sh || exit 3"}}'
34
+ exp "H1 cat > scripts/new.sh <<EOF body carries token" HIT '{"tool_name":"Bash","tool_input":{"command":"cat > scripts/new_lane.sh <<'"'"'EOF'"'"'\n#!/usr/bin/env bash\n[ -f x ] || exit 1\nEOF"}}'
35
+ exp "H2 cat <<EOF > \"scripts/q.sh\" (quoted target after >)" HIT '{"tool_name":"Bash","tool_input":{"command":"cat <<'"'"'EOF'"'"' > \"scripts/q.sh\"\nexit 1\nEOF"}}'
36
+ exp "H3 printf | tee -a scripts/t.sh" HIT '{"tool_name":"Bash","tool_input":{"command":"printf \"exit 1\\n\" | tee -a scripts/t.sh"}}'
37
+ # R4 proper: the real edit is a python heredoc whose CONTENT quotes `: > "$T/scripts/test_has_lane_lanes.sh"` — the hook
38
+ # must record the file the python writes, not the fixture root inside the string (row 4 recorded `$T/scripts/…`).
39
+ R4='{"tool_name":"Bash","tool_input":{"command":"python3 - <<'"'"'PY'"'"'\np=\"scripts/test_proposal_hook_lanes.sh\"\ns=open(p).read()\ns=s.replace(\"exp \\\"noqa\", \"mkdir -p \\\"$T/scripts\\\"; : > \\\"$T/scripts/test_has_lane_lanes.sh\\\"; [ -f x ] || exit 1; exp \\\"noqa\")\nopen(p,\"w\").write(s)\nPY"}}'
40
+ printf '%s' "$R4" | bash "$HDIR/proposal_hook.sh" >/dev/null 2>&1; r4fp=$(tail -1 "$T/.claude/.proposal_hook_events.tsv" 2>/dev/null | cut -f3)
41
+ if [ "$r4fp" = "scripts/test_proposal_hook_lanes.sh" ]; then printf ' ✅ %-52s fp=%s\n' "R4 python heredoc → recorded fp is the WRITTEN file" "$r4fp"; pass=$((pass+1)); else printf ' ❌ %-52s fp=%s (expected scripts/test_proposal_hook_lanes.sh)\n' "R4 python heredoc → recorded fp is the WRITTEN file" "${r4fp:-<none>}"; fail=$((fail+1)); fi
42
+ exp "G1 Edit templates/.git-hooks/pre-commit (no .sh)" HIT '{"tool_name":"Edit","tool_input":{"file_path":"/x/templates/.git-hooks/pre-commit","old_string":" [ -f x ] || continue","new_string":" [ -f x ] || { echo missing; PTR_FAIL=1; continue; }"}}'
43
+ exp "G1-ctrl Edit .git-hooks docs-ish no token" CLEAN '{"tool_name":"Edit","tool_input":{"file_path":"/x/templates/.git-hooks/pre-commit","old_string":"# note a","new_string":"# note b"}}'
44
+ exp "noqa exempts" CLEAN '{"tool_name":"Edit","tool_input":{"file_path":"/x/scripts/a.sh","old_string":"a","new_string":"exit 1 # noqa: proposal-hook"}}'
45
+ exp "unparseable payload silent" CLEAN 'not json'
46
+ msg(){ printf '%s' "$2" | bash "$HDIR/proposal_hook.sh" 2>/dev/null; }
47
+ fact(){ local label="$1" want="$2" payload="$3" got; got=$(msg x "$payload"); if printf '%s' "$got" | grep -q -- "$want"; then printf ' ✅ %-52s carries «%s»\n' "$label" "$want"; pass=$((pass+1)); else printf ' ❌ %-52s missing «%s»\n' "$label" "$want"; fail=$((fail+1)); fi; }
48
+ mkdir -p "$T/scripts"; : > "$T/scripts/test_has_lane_lanes.sh"; printf 'scan of scripts/covered.sh\nfindings: 0\n' > "$T/scripts/.degrade_scan_last_2026-09-03.txt"
49
+ E='{"tool_name":"Edit","tool_input":{"file_path":"/x/scripts/%s","old_string":"a","new_string":"[ -f x ] || exit 1"}}'
50
+ fact "F1 lane exists → fact line, no known-pair item" "이미 있다" "$(printf "$E" has_lane.sh)"
51
+ fact "F1b lane exists → scan item still proposed" "degrade_direction_scan.sh 로" "$(printf "$E" has_lane.sh)"
52
+ fact "F2 self-lane (test_*_lanes.sh) → fact line" "이 파일 자체가 레인" "$(printf "$E" test_has_lane_lanes.sh)"
53
+ fact "F3 scan covers file → fact line w/ findings" "findings: 0" "$(printf "$E" covered.sh)"
54
+ fact "F4 neither → both items proposed" "known-pair(고친 케이스 + 반대 케이스) 컨트롤 degrade_direction_scan.sh" "$(printf "$E" bare.sh)"
55
+ got=$(msg x "$(printf "$E" bare.sh)"); if printf '%s' "$got" | grep -q "사실:"; then echo " ❌ F4-ctrl bare file must carry NO fact line"; fail=$((fail+1)); else echo " ✅ F4-ctrl bare file carries no fact line (known-negative)"; pass=$((pass+1)); fi
56
+ [ -f "$T/.claude/.proposal_hook_events.tsv" ] && [ "$(grep -c FIRE "$T/.claude/.proposal_hook_events.tsv")" -ge 5 ]; r=$?; [ $r = 0 ] && { echo " ✅ evidence file carries a FIRE row per hit"; pass=$((pass+1)); } || { echo " ❌ evidence file missing/short"; fail=$((fail+1)); }
57
+ rm -rf "$T"; echo "[proposal-hook] $pass passed, $fail failed"; [ "$fail" -eq 0 ]
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env bash
2
+ # test_push_zone_lanes.sh — known-pair anchor for the pre-push hook's Push-Zone verdict, and for
3
+ # the session-close ①-f advisory that mirrors it.
4
+ #
5
+ # ── WHY THIS FILE EXISTS ───────────────────────────────────────────────────────────────────────
6
+ # 2026-09-05: templates/.git-hooks/pre-push gained a new verdict path and scripts/session_close_
7
+ # check.sh gained a new ①-f advisory, both implementing CLAUDE.local.md §REST API push 계정규칙
8
+ # mechanically — a push to a github.com remote whose owner is NOT on the operator's owner-account
9
+ # list needs the REST Contents API channel, not plain git push. Before this suite the new code had
10
+ # no known-pair anchor at all. Per CLAUDE.md §Instrument Calibration, a gate is not trusted until
11
+ # calibrated on at least one known-positive and one known-negative, hand-verified.
12
+ #
13
+ # ── DISCIPLINE INHERITED FROM THE SIBLING SUITE (test_prepush_destructive_lanes.sh) ────────────
14
+ # (a) Run the hook FOR REAL — `bash -n` is not an instrument (a runtime fault can parse clean,
15
+ # abort the hook, and still exit 0).
16
+ # (b) A runtime fault is checked BEFORE the verdict, else an aborted hook scores as a clean pass.
17
+ # (c) "It blocked" is NOT "it blocked correctly" — a block must be ATTRIBUTED to a named FH gate.
18
+ # (d) NEVER read or assert on the operator's real owner-account list
19
+ # (.claude/rules/.push-zone-owners). Every lane below builds its own throwaway fixture list
20
+ # via the PUSH_ZONE_OWNERS override, with fictitious account names. The real file, if present
21
+ # on this machine, is never opened by this suite in either direction.
22
+ #
23
+ # Runs entirely in mktemp throwaway repos: never touches this repo's index, worktree, refs, or its
24
+ # real .claude/rules/.push-zone-owners.
25
+ #
26
+ # Usage: bash scripts/test_push_zone_lanes.sh
27
+ # FH_PUSH_ZONE_HOOK_SRC=<path> → test a DIFFERENT copy of the pre-push hook (fail-before proof).
28
+ # FH_PUSH_ZONE_SCC_SRC=<path> → same, for session_close_check.sh.
29
+ set -uo pipefail
30
+ . "$(dirname "${BASH_SOURCE[0]}")/fixture_guard_lib.sh"
31
+
32
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
33
+ HOOK_SRC="${FH_PUSH_ZONE_HOOK_SRC:-$REPO_ROOT/templates/.git-hooks/pre-push}"
34
+ SCC_SRC="${FH_PUSH_ZONE_SCC_SRC:-$REPO_ROOT/scripts/session_close_check.sh}"
35
+ DEF_SRC="$REPO_ROOT/.claude/rules/.public-surface-patterns.defaults"
36
+ ZERO="0000000000000000000000000000000000000000"
37
+
38
+ if [ ! -f "$HOOK_SRC" ]; then
39
+ echo "HARNESS_ERROR — pre-push hook not found at $HOOK_SRC (subject absent; NOT a pass)"; exit 1
40
+ fi
41
+
42
+ WORK="$(fh_fixture_root "$(mktemp -d)")" || exit 1
43
+ : "${WORK:?fixture root unset — refusing to run git in cwd}"
44
+ trap 'rm -rf "$WORK"' EXIT
45
+ cp "$HOOK_SRC" "$WORK/hook" || exit 1
46
+ HOOK="$WORK/hook"
47
+
48
+ # Fictitious owner-account fixture — NEVER the operator's real list. The second entry
49
+ # (acme-shared-org) is asserted to never appear in any captured output — it is a decoy for the
50
+ # "list content is never printed" requirement, not used by any URL in this suite.
51
+ OWNERS_OK="$WORK/owners-ok.txt"
52
+ printf '# fixture owner list — test only, not the operator real list\nknown-owner\nacme-shared-org\n' > "$OWNERS_OK"
53
+
54
+ PASSED=0; FAILED=0; ALL_OUT=""
55
+
56
+ # ── fixture: same shape as test_prepush_destructive_lanes.sh's newrepo() ───────────────────────
57
+ newrepo() {
58
+ local d; d="$(fh_fixture_root "$(mktemp -d)")" || return 1
59
+ : "${d:?fixture root unset — refusing to run git in cwd}"
60
+ mkdir -p "$d/.claude/rules" "$d/templates/.git-hooks" "$d/tracks/_meta" "$d/scripts" || return 1
61
+ [ -f "$DEF_SRC" ] && cp "$DEF_SRC" "$d/.claude/rules/.public-surface-patterns.defaults"
62
+ [ -f "$REPO_ROOT/scripts/psa_scan_lib.sh" ] && cp "$REPO_ROOT/scripts/psa_scan_lib.sh" "$d/scripts/psa_scan_lib.sh"
63
+ printf '.claude/rules/.public-surface-patterns\n' > "$d/.gitignore"
64
+ cp "$HOOK" "$d/templates/.git-hooks/pre-push" || return 1
65
+ printf 'base\n' > "$d/a.txt"
66
+ (
67
+ cd "$d" || exit 1
68
+ git init -q || exit 1
69
+ git symbolic-ref HEAD refs/heads/main || exit 1
70
+ git config user.email t@example.com || exit 1
71
+ git config user.name t || exit 1
72
+ git add -A >/dev/null 2>&1 || exit 1
73
+ git commit -qm base >/dev/null 2>&1 || exit 1
74
+ git update-ref refs/remotes/origin/main HEAD || exit 1
75
+ ) || return 1
76
+ printf '%s' "$d"
77
+ }
78
+
79
+ require_repo() { # $1 = candidate repo path, $2 = lane name
80
+ if [ -z "${1:-}" ] || [ ! -d "$1/.git" ]; then
81
+ echo "HARNESS_ERROR — fixture repo not built for $2 (setup failed; NOT a pass)"
82
+ exit 1
83
+ fi
84
+ }
85
+
86
+ # check_zone <name> <repo> <owners-file|""> <url> <push_zone_ok:0|1> <expect:block|pass> <cause-regex|""> <refline>
87
+ check_zone() {
88
+ local name="$1" repo="$2" owners="$3" url="$4" pzok="$5" expect="$6" cause="$7"; shift 7
89
+ local out rc got _pzsrc
90
+ require_repo "$repo" "$name"
91
+ _pzsrc="${owners:-$WORK/nonexistent-owners-file-for-zone-test}"
92
+ out=$(printf '%s\n' "$@" | ( cd "$repo" && PUSH_ZONE_OWNERS="$_pzsrc" PUSH_ZONE_OK="$pzok" \
93
+ bash templates/.git-hooks/pre-push origin "$url" 2>&1 )); rc=$?
94
+ ALL_OUT="$ALL_OUT
95
+ $out"
96
+
97
+ if printf '%s' "$out" | grep -qiE 'bad substitution|unbound variable|syntax error|command not found'; then
98
+ echo " ❌ $name — RUNTIME FAULT in the hook (aborted, not passed)"
99
+ printf '%s\n' "$out" | grep -iE 'bad substitution|unbound|syntax error|command not found' | sed 's/^/ /' | head -3
100
+ FAILED=$((FAILED+1)); return
101
+ fi
102
+
103
+ if [ "$rc" -ne 0 ]; then
104
+ if printf '%s' "$out" | grep -qE 'FH (Push-Zone Gate|Destructive-Op Gate)'; then
105
+ got=block
106
+ else
107
+ echo " ❌ $name — blocked, but NOT by a named FH gate (harness reason ≠ finding)"
108
+ printf '%s\n' "$out" | sed 's/^/ | /' | head -8
109
+ FAILED=$((FAILED+1)); return
110
+ fi
111
+ else
112
+ got=pass
113
+ fi
114
+
115
+ if [ "$got" != "$expect" ]; then
116
+ echo " ❌ $name — expected $expect, got $got"
117
+ printf '%s\n' "$out" | sed 's/^/ | /' | head -10
118
+ FAILED=$((FAILED+1)); return
119
+ fi
120
+
121
+ if [ -n "$cause" ] && ! printf '%s' "$out" | grep -qE "$cause"; then
122
+ echo " ❌ $name — verdict $got is correct but UNATTRIBUTED (missing: $cause)"
123
+ printf '%s\n' "$out" | sed 's/^/ | /' | head -10
124
+ FAILED=$((FAILED+1)); return
125
+ fi
126
+
127
+ echo " ✅ $name (expected $expect)"
128
+ PASSED=$((PASSED+1))
129
+ }
130
+
131
+ echo "[push-zone] known-pair anchor for the pre-push Push-Zone verdict (hook: $HOOK_SRC)"
132
+ echo ""
133
+
134
+ # Z0 — no owner-account list at all → UNCALIBRATED, plain push allowed.
135
+ R=$(newrepo); require_repo "$R" "Z0"
136
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
137
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
138
+ check_zone "Z0 no owner-account list → PASS " "$R" "" \
139
+ "https://github.com/known-owner/repo.git" 0 pass 'push-zone: UNCALIBRATED' \
140
+ "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
141
+ rm -rf "$R"
142
+
143
+ # Z1 — owner IN the list, https → silent pass; the rest of the hook (confidentiality) still runs.
144
+ R=$(newrepo); require_repo "$R" "Z1"
145
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
146
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
147
+ check_zone "Z1 owner IN list, https → PASS " "$R" "$OWNERS_OK" \
148
+ "https://github.com/known-owner/repo.git" 0 pass 'FH Pre-Publish' \
149
+ "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
150
+ rm -rf "$R"
151
+
152
+ # Z2 — owner OUT of the list, https → BLOCK, attributed, names the URL's (fictitious) owner.
153
+ R=$(newrepo); require_repo "$R" "Z2"
154
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
155
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
156
+ check_zone "Z2 owner OUT of list, https → BLOCK" "$R" "$OWNERS_OK" \
157
+ "https://github.com/someone-else/repo.git" 0 block 'FH Push-Zone Gate' \
158
+ "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
159
+ if printf '%s' "$ALL_OUT" | grep -qF 'github.com/someone-else'; then
160
+ echo " ✅ Z2b block message names the URL's owner"; PASSED=$((PASSED+1))
161
+ else
162
+ echo " ❌ Z2b — block message did not name the URL's owner (someone-else)"; FAILED=$((FAILED+1))
163
+ fi
164
+ rm -rf "$R"
165
+
166
+ # Z3 — owner OUT of the list, scp-like ssh (git@github.com:owner/repo) → BLOCK.
167
+ R=$(newrepo); require_repo "$R" "Z3"
168
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
169
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
170
+ check_zone "Z3 owner OUT of list, ssh scp → BLOCK" "$R" "$OWNERS_OK" \
171
+ "git@github.com:someone-else/repo.git" 0 block 'FH Push-Zone Gate' \
172
+ "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
173
+ rm -rf "$R"
174
+
175
+ # Z4 — remote host is not github.com → out of scope for this axis, pass allowed, says so.
176
+ R=$(newrepo); require_repo "$R" "Z4"
177
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
178
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
179
+ check_zone "Z4 non-github host → PASS " "$R" "$OWNERS_OK" \
180
+ "https://ghe.example.com/x/y" 0 pass 'not github.com' \
181
+ "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
182
+ rm -rf "$R"
183
+
184
+ # Z5 — PUSH_ZONE_OK=1 override on an out-of-list push → allowed, conscious, LOGGED.
185
+ R=$(newrepo); require_repo "$R" "Z5"
186
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
187
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
188
+ check_zone "Z5 PUSH_ZONE_OK=1 override → PASS " "$R" "$OWNERS_OK" \
189
+ "https://github.com/someone-else/repo.git" 1 pass 'PUSH_ZONE_OK=1' \
190
+ "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
191
+ LOG="$R/tracks/_meta/.push_zone_override_log"
192
+ if [ -f "$LOG" ] && grep -qF 'owner:someone-else' "$LOG"; then
193
+ echo " ✅ Z5b override logged to tracks/_meta/.push_zone_override_log"; PASSED=$((PASSED+1))
194
+ else
195
+ echo " ❌ Z5b override NOT logged (wanted a line inside $LOG)"; FAILED=$((FAILED+1))
196
+ fi
197
+ rm -rf "$R"
198
+
199
+ # Z6 — existing destructive verdict (branch DELETE, unique paths → REVIEW → BLOCK) still stands
200
+ # AFTER a silent zone PASS. Same shape as the sibling suite's P2, through an owner-matching
201
+ # github.com URL — proves the zone block, inserted near the TOP of the hook, does not shadow
202
+ # or reorder the destructive checks further down.
203
+ R=$(newrepo); require_repo "$R" "Z6"
204
+ ( cd "$R" && git checkout -q -b unique-work && printf 'only here\n' > unique.txt \
205
+ && git add unique.txt && git commit -qm unique >/dev/null && git checkout -q main )
206
+ TIP=$(cd "$R" && git rev-parse unique-work)
207
+ check_zone "Z6 zone PASS, then BRANCH DELETE → BLOCK" "$R" "$OWNERS_OK" \
208
+ "https://github.com/known-owner/repo.git" 0 block 'REVIEW: .* unique path' \
209
+ "(delete) $ZERO refs/heads/unique-work $TIP"
210
+ rm -rf "$R"
211
+
212
+ # ── Residency: the owner-list's OTHER entries must never appear in any captured output ─────────
213
+ if printf '%s' "$ALL_OUT" | grep -qF 'acme-shared-org'; then
214
+ echo " ❌ RESIDENCY — the fixture owner-list's second entry leaked into hook output"
215
+ FAILED=$((FAILED+1))
216
+ else
217
+ echo " ✅ RESIDENCY — owner-list content never appears in any captured hook output"
218
+ PASSED=$((PASSED+1))
219
+ fi
220
+
221
+ echo ""
222
+ # ── Z7–Z14 (cross-family codex gpt-5.5, 2026-09-05): the first draft accepted three literal URL
223
+ # prefixes and read every other github.com shape as "unrecognized → allow" — four real shapes were
224
+ # failing OPEN; an inline comment in the owner list authorised pushes; CRLF / trailing slash blocked
225
+ # the operator's own remotes. Each lane below is one of those exact inputs.
226
+ OWNERS_CRLF="$WORK/owners-crlf.txt"; printf 'known-owner\r\nacme-shared-org\r\n' > "$OWNERS_CRLF"
227
+ OWNERS_COMMENT="$WORK/owners-comment.txt"; printf 'known-owner # someone-else is NOT allowed\nacme-shared-org\n' > "$OWNERS_COMMENT"
228
+ OWNERS_SLASH="$WORK/owners-slash.txt"; printf 'known-owner/\nacme-shared-org\n' > "$OWNERS_SLASH"
229
+
230
+ _zone_ff_lane() { # $1=id $2=desc $3=owners $4=url $5=expect $6=cause
231
+ local R BASESHA NEWSHA
232
+ R=$(newrepo); require_repo "$R" "$1"
233
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
234
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
235
+ check_zone "$1 $2" "$R" "$3" "$4" 0 "$5" "$6" "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
236
+ rm -rf "$R"
237
+ }
238
+ _zone_ff_lane Z7 "userinfo https (user@github.com), owner OUT → BLOCK" "$OWNERS_OK" "https://user@github.com/someone-else/repo.git" block 'FH Push-Zone Gate'
239
+ _zone_ff_lane Z8 "https with :443 port, owner OUT → BLOCK" "$OWNERS_OK" "https://github.com:443/someone-else/repo.git" block 'FH Push-Zone Gate'
240
+ _zone_ff_lane Z9 "ssh:// with :22 port, owner OUT → BLOCK" "$OWNERS_OK" "ssh://git@github.com:22/someone-else/repo.git" block 'FH Push-Zone Gate'
241
+ _zone_ff_lane Z10 "scp form with leading slash (git@github.com:/o/r), owner OUT → BLOCK" "$OWNERS_OK" "git@github.com:/someone-else/repo.git" block 'FH Push-Zone Gate'
242
+ _zone_ff_lane Z11 "inline comment in owner list must NOT authorise the commented name → BLOCK" "$OWNERS_COMMENT" "https://github.com/someone-else/repo.git" block 'FH Push-Zone Gate'
243
+ _zone_ff_lane Z12 "CRLF owner list, owner IN → pass (no over-block)" "$OWNERS_CRLF" "https://github.com/known-owner/repo.git" pass ''
244
+ _zone_ff_lane Z13 "trailing-slash owner entry, owner IN → pass" "$OWNERS_SLASH" "https://github.com/known-owner/repo.git" pass ''
245
+ _zone_ff_lane Z13b "uppercase host/owner in URL, owner IN → pass (case-insensitive)" "$OWNERS_OK" "https://GitHub.com/Known-Owner/repo.git" pass ''
246
+ # Z14 — $2 is a bare remote NAME (alias/insteadOf edge): the hook must resolve it, not read it as a host.
247
+ R=$(newrepo); require_repo "$R" "Z14"
248
+ ( cd "$R" && git remote add origin https://github.com/someone-else/repo.git ) 2>/dev/null
249
+ BASESHA=$(cd "$R" && git rev-parse HEAD)
250
+ NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
251
+ check_zone "Z14 \$2 is the remote NAME → resolved via git remote get-url → BLOCK" "$R" "$OWNERS_OK" \
252
+ "origin" 0 block 'FH Push-Zone Gate' "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
253
+ rm -rf "$R"
254
+
255
+ echo "── session-close ①-f wiring (scripts/session_close_check.sh) ──"
256
+ if [ ! -f "$SCC_SRC" ]; then
257
+ echo "HARNESS_ERROR — session_close_check.sh not found at $SCC_SRC"; FAILED=$((FAILED+1))
258
+ else
259
+ SR="$(fh_fixture_root "$(mktemp -d)")" || exit 1
260
+ mkdir -p "$SR/tracks/_meta" "$SR/scripts" "$SR/.claude/rules"
261
+ cp "$REPO_ROOT/scripts/push_zone_check.sh" "$SR/scripts/push_zone_check.sh" 2>/dev/null
262
+ chmod +x "$SR/scripts/push_zone_check.sh" 2>/dev/null
263
+ ( cd "$SR" && git init -q && git symbolic-ref HEAD refs/heads/main \
264
+ && git config user.email t@example.com && git config user.name t \
265
+ && git commit -q --allow-empty -m base >/dev/null ) >/dev/null 2>&1
266
+
267
+ if [ -x "$SR/scripts/push_zone_check.sh" ]; then
268
+ SC_OUT=$(PUSH_ZONE_OWNERS="$OWNERS_OK" bash "$SCC_SRC" "$SR" 2>&1)
269
+ if printf '%s' "$SC_OUT" | grep -qE '①-f push-zone: '; then
270
+ echo " ✅ ①-f prints advisory-prefixed push_zone_check.sh output when both files exist"
271
+ PASSED=$((PASSED+1))
272
+ else
273
+ echo " ❌ ①-f did not print the expected advisory prefix"; FAILED=$((FAILED+1))
274
+ fi
275
+ # RESIDENCY (codex #9, 2026-09-05): the enumerator prints owner-account names, and a close's
276
+ # output lands in transcripts and session cards. ①-f therefore prints COUNTS only — the decoy
277
+ # list entry must never appear, and the summary line must.
278
+ if printf '%s' "$SC_OUT" | grep -qF 'acme-shared-org'; then
279
+ echo " ❌ ①-f leaked an owner-account list entry into the close output"; FAILED=$((FAILED+1))
280
+ else
281
+ echo " ✅ ①-f withholds owner-account names (counts only)"; PASSED=$((PASSED+1))
282
+ fi
283
+ if printf '%s' "$SC_OUT" | grep -qE '①-f push-zone: [0-9]+ repo\(s\) enumerated'; then
284
+ echo " ✅ ①-f prints the enumeration summary (repo count · outside-list count)"; PASSED=$((PASSED+1))
285
+ else
286
+ echo " ❌ ①-f summary line missing"; FAILED=$((FAILED+1))
287
+ fi
288
+ else
289
+ echo " ❌ Z-SC setup — fixture push_zone_check.sh not executable"; FAILED=$((FAILED+1))
290
+ fi
291
+
292
+ # Owner-account list absent → UNCALIBRATED, one line, never silent.
293
+ SC_OUT2=$(PUSH_ZONE_OWNERS="$SR/no-such-owners-file" bash "$SCC_SRC" "$SR" 2>&1)
294
+ if printf '%s' "$SC_OUT2" | grep -qE '①-f push-zone UNCALIBRATED'; then
295
+ echo " ✅ ①-f UNCALIBRATED when the owner-account list is absent"; PASSED=$((PASSED+1))
296
+ else
297
+ echo " ❌ ①-f did not report UNCALIBRATED with no owner-account list"; FAILED=$((FAILED+1))
298
+ fi
299
+ rm -rf "$SR"
300
+ fi
301
+
302
+ echo ""
303
+ echo "[push-zone] $PASSED passed, $FAILED failed"
304
+ [ "$FAILED" -eq 0 ]
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # residency_closure_scan.py 레인 테스트 — known-pair 교정.
3
+ #
4
+ # 이 파일이 있는 이유: 초판이 cross-family 적대검증(gpt-5.5, 2026-08-09)에서 **false-CLEAN
5
+ # 다수**로 반증됐다. 아래 L1~L3 은 그때 재현한 실제 우회 경로이고, 되돌리면 이 레인들이
6
+ # 적색이 된다. 「고쳤다」를 주장으로 두지 않고 실행으로 남긴다.
7
+ #
8
+ # 각 레인 = (입력, 기대 rc). rc 계약: 0=CLEAN(스크리닝) · 1=TAINTED · 10=HARNESS(fail-closed)
9
+ set -uo pipefail
10
+
11
+ HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
12
+ SCAN="$HERE/residency_closure_scan.py"
13
+ TMP=$(mktemp -d) || exit 10
14
+ trap 'rm -rf "$TMP"' EXIT
15
+
16
+ PASS=0; FAIL=0
17
+ lane() { # lane <이름> <기대rc> <모듈> [패턴파일]
18
+ local name=$1 want=$2 mod=$3 pat=${4:-$TMP/pat}
19
+ RESIDENCY_PATTERNS="$pat" python3 "$SCAN" --repo "$TMP/repo" "$mod" >/dev/null 2>&1
20
+ local got=$?
21
+ if [ "$got" = "$want" ]; then PASS=$((PASS+1)); printf ' ✅ %-42s rc=%s\n' "$name" "$got"
22
+ else FAIL=$((FAIL+1)); printf ' ❌ %-42s rc=%s (기대 %s)\n' "$name" "$got" "$want"; fi
23
+ }
24
+
25
+ R="$TMP/repo"
26
+ mkdir -p "$R"/{src/pkg,myapp,app,clean,deep/sub}
27
+ : > "$R/src/pkg/__init__.py"; : > "$R/app/__init__.py"; : > "$R/deep/__init__.py"
28
+ : > "$R/deep/sub/__init__.py"
29
+
30
+ # L1 상대 임포트 — 초판이 `level == 0` 으로 통째로 버렸다
31
+ printf 'from .secrets import HOST\n' > "$R/src/pkg/main.py"
32
+ printf 'HOST = "api.acme.corp"\n' > "$R/src/pkg/secrets.py"
33
+ # L2 루트 화이트리스트 밖 레이아웃 — 초판은 ("src","scripts",…) 만 따라갔다
34
+ printf 'import myapp.config\n' > "$R/myapp/main.py"
35
+ printf 'HOST = "api.acme.corp"\n' > "$R/myapp/config.py"
36
+ # L3 `from pkg import sub` — 초판은 pkg/__init__.py 만 훑었다
37
+ printf 'from app import secrets\n' > "$R/app/main.py"
38
+ printf 'HOST = "api.acme.corp"\n' > "$R/app/secrets.py"
39
+ # L4 부모 패키지 __init__.py 오염 — payload 에 같이 나간다
40
+ printf 'HOST = "api.acme.corp"\n' > "$R/deep/__init__.py"
41
+ printf 'X = 1\n' > "$R/deep/sub/leaf.py"
42
+ # 과교정 컨트롤 — 진짜 깨끗한 모듈
43
+ printf 'X = 1\n' > "$R/clean/ok.py"
44
+
45
+ printf 'acme\n' > "$TMP/pat"
46
+ : > "$TMP/pat_empty"
47
+ printf '# 주석뿐\n' > "$TMP/pat_comment"
48
+
49
+ echo "== residency 폐포 스캔 레인 =="
50
+ lane "L1 상대 임포트 → TAINTED" 1 src.pkg.main
51
+ lane "L2 루트 화이트리스트 밖 → TAINTED" 1 myapp.main
52
+ lane "L3 from pkg import sub → TAINTED" 1 app.main
53
+ lane "L4 부모 __init__ 오염 → TAINTED" 1 deep.sub.leaf
54
+ lane "★컨트롤: 깨끗한 모듈 → CLEAN" 0 clean.ok
55
+ lane "빈 운영자 패턴 → HARNESS" 10 clean.ok "$TMP/pat_empty"
56
+ lane "주석뿐 운영자 패턴 → HARNESS" 10 clean.ok "$TMP/pat_comment"
57
+ lane "운영자 패턴 부재 → HARNESS" 10 clean.ok "$TMP/nonexistent"
58
+ lane "모듈 부재 → HARNESS" 10 no.such.module
59
+
60
+ # --files 모드: 실제 송신 목록만 잰다
61
+ RESIDENCY_PATTERNS="$TMP/pat" python3 "$SCAN" --repo "$R" --files src/pkg/secrets.py >/dev/null 2>&1
62
+ [ $? = 1 ] && { PASS=$((PASS+1)); echo " ✅ --files 오염 파일 → TAINTED rc=1"; } \
63
+ || { FAIL=$((FAIL+1)); echo " ❌ --files 오염 파일"; }
64
+ RESIDENCY_PATTERNS="$TMP/pat" python3 "$SCAN" --repo "$R" --files clean/ok.py >/dev/null 2>&1
65
+ [ $? = 0 ] && { PASS=$((PASS+1)); echo " ✅ --files 깨끗한 파일 → CLEAN rc=0"; } \
66
+ || { FAIL=$((FAIL+1)); echo " ❌ --files 깨끗한 파일"; }
67
+
68
+ echo "── PASS $PASS · FAIL $FAIL"
69
+ [ "$FAIL" -eq 0 ] || exit 1
70
+ exit 0
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env bash
2
+ # test_revert_probe_lanes.sh — known-pair calibration for scripts/revert_probe.sh
3
+ #
4
+ # WHY THESE LANES. revert_probe.sh exists to replace 15+ hand-written revert-and-observe scripts
5
+ # with one generic tool. The two failure modes that would make it worse than the hand-rolled
6
+ # scripts it replaces are exactly the two known-pair lanes below:
7
+ # L1 DECORATIVE anchor — a suite whose lanes never actually depend on the target file's content
8
+ # must be reported as decorative (exit 1), not silently scored as "passed".
9
+ # L2 REAL anchor — the mirror-image known-positive: a suite that DOES catch the revert
10
+ # must report the exact flipped lane and exit 0. Without this half, L1 proves nothing
11
+ # ([[feedback_control_presence_is_not_discrimination]]).
12
+ # L3 RESTORE GUARANTEED — the target file must come back byte-identical after the probe, in
13
+ # BOTH the normal-completion path and the harness-error path (suite crashes / produces no
14
+ # parseable output). A revert tool that leaves the baseline content sitting in the working
15
+ # tree on the failure path is strictly worse than not having the tool.
16
+ # L4+ usage guards, harness-error detection, and the git-show (not git-checkout) restore path.
17
+ #
18
+ # Fixtures are throwaway git repos + throwaway suite scripts, never this repo's own assets — a
19
+ # lane suite that references a repo-specific path fails on a fresh clone (portability_lint P9).
20
+ #
21
+ # Usage: bash scripts/test_revert_probe_lanes.sh
22
+
23
+ set -uo pipefail
24
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
25
+ SUT="${FH_REVERT_PROBE_BIN:-$ROOT/scripts/revert_probe.sh}"
26
+ pass=0; fail=0
27
+ ok() { printf ' ✅ %s\n' "$1"; pass=$((pass+1)); }
28
+ no() { printf ' ❌ %s\n' "$1"; fail=$((fail+1)); }
29
+
30
+ if [ ! -f "$SUT" ]; then
31
+ echo "FAIL test_revert_probe_lanes.sh: subject absent ($SUT) — skipped is not passed"
32
+ exit 1
33
+ fi
34
+
35
+ WORKROOT="$(mktemp -d "${TMPDIR:-/tmp}/revertlane-XXXXXX")"
36
+ trap 'rm -rf "$WORKROOT"' EXIT
37
+
38
+ # ── throwaway repo: commit 1 = baseline (missing the fix) · commit 2/HEAD = current (has the fix)
39
+ SRC="$WORKROOT/src"; mkdir -p "$SRC"
40
+ (
41
+ cd "$SRC" && git init -q . && git config user.email l@l && git config user.name l
42
+ printf 'value = old_and_broken\n' > guard.txt
43
+ git add guard.txt && git commit -qm "baseline (no fix)"
44
+ printf 'value = MARKER_OK\n' > guard.txt
45
+ git add guard.txt && git commit -qm "current (has the fix)"
46
+ ) >/dev/null 2>&1
47
+ TARGET="$SRC/guard.txt"
48
+ TARGET_SHA_BEFORE() { shasum -a 256 "$TARGET" 2>/dev/null | awk '{print $1}'; }
49
+
50
+ # ── fixture suites ──────────────────────────────────────────────────────────────────────────
51
+ # REAL anchor: the lane's pass/fail genuinely depends on the target file's content.
52
+ REAL_SUITE="$WORKROOT/real_suite.sh"
53
+ cat > "$REAL_SUITE" <<'EOF'
54
+ #!/usr/bin/env bash
55
+ if grep -q MARKER_OK "$FIXTURE_TARGET" 2>/dev/null; then
56
+ printf ' %s %s\n' "✅" "marker present in guard.txt"
57
+ else
58
+ printf ' %s %s\n' "❌" "marker present in guard.txt"
59
+ fi
60
+ exit 0
61
+ EOF
62
+ chmod +x "$REAL_SUITE"
63
+
64
+ # DECORATIVE anchor: always ✅, never actually reads the target file.
65
+ DECO_SUITE="$WORKROOT/deco_suite.sh"
66
+ cat > "$DECO_SUITE" <<'EOF'
67
+ #!/usr/bin/env bash
68
+ printf ' %s %s\n' "✅" "unrelated check that never reads guard.txt"
69
+ exit 0
70
+ EOF
71
+ chmod +x "$DECO_SUITE"
72
+
73
+ # HARNESS-ERROR suite: crashes before printing any ✅/❌ line at all.
74
+ CRASH_SUITE="$WORKROOT/crash_suite.sh"
75
+ cat > "$CRASH_SUITE" <<'EOF'
76
+ #!/usr/bin/env bash
77
+ echo "no verdict lines here, just noise" >&2
78
+ exit 1
79
+ EOF
80
+ chmod +x "$CRASH_SUITE"
81
+
82
+ echo "── revert_probe known-pair lanes ──────────────────────────────────"
83
+
84
+ # L1 — DECORATIVE anchor → exit 1, 0 flipped lanes
85
+ BEFORE1="$(TARGET_SHA_BEFORE)"
86
+ OUT1=$(FIXTURE_TARGET="$TARGET" bash "$SUT" "$TARGET" "$DECO_SUITE" --baseline HEAD~1 2>&1); RC1=$?
87
+ [ "$RC1" -eq 1 ] && ok "L1 decorative anchor → exit 1" || no "L1 decorative anchor: rc=$RC1 (want 1)"
88
+ printf '%s' "$OUT1" | grep -q "되돌린 레인 (✅→❌, 앵커가 실제로 잡은 것): 0개" \
89
+ && ok "L1b report states 0 flipped lanes" || no "L1b report did not state 0 flipped lanes"
90
+ AFTER1="$(TARGET_SHA_BEFORE)"
91
+ [ "$BEFORE1" = "$AFTER1" ] && ok "L1c target file restored after decorative run" \
92
+ || no "L1c target file NOT restored (before=$BEFORE1 after=$AFTER1)"
93
+
94
+ # L2 — REAL anchor → exit 0, exactly 1 flipped lane, names it
95
+ BEFORE2="$(TARGET_SHA_BEFORE)"
96
+ OUT2=$(FIXTURE_TARGET="$TARGET" bash "$SUT" "$TARGET" "$REAL_SUITE" --baseline HEAD~1 2>&1); RC2=$?
97
+ [ "$RC2" -eq 0 ] && ok "L2 real anchor → exit 0" || no "L2 real anchor: rc=$RC2 (want 0)"
98
+ printf '%s' "$OUT2" | grep -q "되돌린 레인 (✅→❌, 앵커가 실제로 잡은 것): 1개" \
99
+ && ok "L2b report states exactly 1 flipped lane" || no "L2b report did not state 1 flipped lane"
100
+ printf '%s' "$OUT2" | grep -q "marker present in guard.txt" \
101
+ && ok "L2c report names the exact flipped label" || no "L2c flipped label not named in report"
102
+ AFTER2="$(TARGET_SHA_BEFORE)"
103
+ [ "$BEFORE2" = "$AFTER2" ] && ok "L2d target file restored after real-anchor run" \
104
+ || no "L2d target file NOT restored (before=$BEFORE2 after=$AFTER2)"
105
+
106
+ # L3 — frontier warning header always present (both suites above already exercised it; assert once)
107
+ printf '%s' "$OUT2" | grep -q "2607.22880" && ok "L3 frontier-warning citation present in output" \
108
+ || no "L3 frontier-warning citation missing"
109
+ printf '%s' "$OUT2" | grep -q "되돌린 파일(뮤턴트) 수: 1" \
110
+ && ok "L3b mutant-count line present (n=1 caveat)" || no "L3b mutant-count line missing"
111
+
112
+ # L4 — HARNESS ERROR: suite produces zero ✅/❌ lines → exit 10, and restore STILL happens.
113
+ # This is the lane that matters most: a crash on the baseline run is exactly the moment a naive
114
+ # implementation would leave the baseline content sitting in the working tree.
115
+ BEFORE4="$(TARGET_SHA_BEFORE)"
116
+ OUT4=$(FIXTURE_TARGET="$TARGET" bash "$SUT" "$TARGET" "$CRASH_SUITE" --baseline HEAD~1 2>&1); RC4=$?
117
+ [ "$RC4" -eq 10 ] && ok "L4 zero-label suite → exit 10 (harness error, not a silent pass)" \
118
+ || no "L4 zero-label suite: rc=$RC4 (want 10)"
119
+ AFTER4="$(TARGET_SHA_BEFORE)"
120
+ [ "$BEFORE4" = "$AFTER4" ] && ok "L4b target file restored even on the harness-error path" \
121
+ || no "L4b RESTORE FAILED ON HARNESS-ERROR PATH (before=$BEFORE4 after=$AFTER4) — this is the defect class this suite exists to catch"
122
+
123
+ # L5 — usage guards
124
+ OUT5=$(bash "$SUT" 2>&1); RC5=$?
125
+ [ "$RC5" -eq 2 ] && ok "L5 no args → exit 2" || no "L5 no args: rc=$RC5"
126
+ OUT5b=$(bash "$SUT" "$TARGET" 2>&1); RC5b=$?
127
+ [ "$RC5b" -eq 2 ] && ok "L5b missing suite arg → exit 2" || no "L5b missing suite arg: rc=$RC5b"
128
+ OUT5c=$(bash "$SUT" "$WORKROOT/does_not_exist.txt" "$REAL_SUITE" 2>&1); RC5c=$?
129
+ [ "$RC5c" -eq 2 ] && ok "L5c missing target file → exit 2" || no "L5c missing target file: rc=$RC5c"
130
+ OUT5d=$(bash "$SUT" "$TARGET" "$WORKROOT/does_not_exist.sh" 2>&1); RC5d=$?
131
+ [ "$RC5d" -eq 2 ] && ok "L5d missing suite file → exit 2" || no "L5d missing suite file: rc=$RC5d"
132
+ OUT5e=$(bash "$SUT" "$TARGET" "$REAL_SUITE" --baseline totally-bogus-ref 2>&1); RC5e=$?
133
+ [ "$RC5e" -eq 2 ] && ok "L5e bad --baseline ref → exit 2" || no "L5e bad baseline ref: rc=$RC5e"
134
+
135
+ # L6 — the restore path uses `git show`, never `git checkout <ref> -- <path>` (the latter STAGES
136
+ # the revert into the index — [[feedback_git_checkout_path_stages_the_revert]]). Assert the repo's
137
+ # index carries no staged change after a probe run, both directions.
138
+ ( cd "$SRC" && git diff --cached --quiet ) \
139
+ && ok "L6 no staged changes left in the index after any probe run" \
140
+ || no "L6 the index has staged changes — a checkout-based restore would do this"
141
+ ( cd "$SRC" && git status --porcelain ) | grep -q . \
142
+ && no "L6b working tree not clean after probes (git status is non-empty)" \
143
+ || ok "L6b working tree clean after all probes (file content == committed HEAD)"
144
+
145
+ echo "revert_probe lanes: $pass passed, $fail failed"
146
+ [ "$fail" -eq 0 ] || exit 1
@@ -138,9 +138,7 @@ PREMISE_FAILED=0
138
138
  # Human-readable on both platforms: GNU prints a date for %y, BSD's %Fm prints a raw epoch, so the
139
139
  # BSD branch asks for a formatted date instead — the whole point of this line is eyeballing.
140
140
  _mtime_h() {
141
- stat -c %y "$1" 2>/dev/null \
142
- || stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$1" 2>/dev/null \
143
- || echo "?"
141
+ stat -c %y "$1" 2>/dev/null || stat -f "%Sm" -t "%Y-%m-%d %H:%M:%S" "$1" 2>/dev/null || echo "?"
144
142
  }
145
143
 
146
144
  _assert_newer() { # $1=expected-newer $2=reference $3=lane label — returns 1 if premise fails
@@ -298,7 +296,7 @@ _carry_fixture() { # $1=오늘카드 본문 $2=fh_completed 본문(빈 문자
298
296
  printf '%s\n' "$card_body" > "$T/tracks/_meta/reference_next_session_starter.md"
299
297
  [ -n "$done_body" ] && printf '%s\n' "$done_body" > "$T/tracks/_meta/fh_completed_${TODAY}.md"
300
298
  # companion store: 어제 날짜로 커밋된 «이전 카드» 미러 하나
301
- FUT=$(date -v+2d '+%m-%d' 2>/dev/null || date -d '+2 days' '+%m-%d')
299
+ FUT=$(date -d '+2 days' '+%m-%d' 2>/dev/null || date -v+2d '+%m-%d')
302
300
  ( cd "$S" && git init -q . && git config user.email a@l && git config user.name a \
303
301
  && mkdir -p tracks-meta \
304
302
  && printf '기한 %s 미팅 안건 [B]\n' "$FUT" > tracks-meta/reference_next_session_starter.md \
@@ -307,7 +305,7 @@ _carry_fixture() { # $1=오늘카드 본문 $2=fh_completed 본문(빈 문자
307
305
  git commit -qm prior ) >/dev/null 2>&1
308
306
  printf '%s|%s' "$T" "$S"
309
307
  }
310
- _carry_future() { date -v+2d '+%m-%d' 2>/dev/null || date -d '+2 days' '+%m-%d'; }
308
+ _carry_future() { date -d '+2 days' '+%m-%d' 2>/dev/null || date -v+2d '+%m-%d'; }
311
309
 
312
310
  _carry_lane() { # $1=name $2=pattern $3=expect $4="REPO|STORE" $5=extra-env(옵션)
313
311
  local name="$1" pat="$2" expect="$3" pair="$4" env5="${5:-}" T S out hit