@chrono-meta/fh-gate 3.1.4 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/fh_4axis_gate.md +1 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +6 -0
- package/CLAUDE.md +50 -6
- package/README.ja.md +3 -3
- package/README.ko.md +3 -3
- package/README.md +3 -3
- package/README.zh.md +3 -3
- package/docs/OUTPUT_EVIDENCE.md +1 -1
- package/knowledge/shared/harness-core/claude_md_gate_details.md +26 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +1 -1
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +46 -0
- package/knowledge/shared/harness-core/governance_engineering_definition.md +89 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +68 -0
- package/knowledge/shared/rules/auto_project_mapping.md +1 -1
- package/package.json +11 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/skills/preprep/SKILL.md +23 -0
- package/plugins/fh-commons/skills/preprep/fixtures/font_revert_probe.py +92 -0
- package/plugins/fh-commons/skills/preprep/lane_font.py +462 -0
- package/plugins/fh-commons/skills/preprep/preprep.py +16 -1
- package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +17 -0
- package/plugins/fh-commons/skills/preprep/test_lane_font.py +452 -0
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +69 -0
- package/plugins/fh-qp/.claude-plugin/plugin.json +1 -1
- package/scripts/doc_claim_triad_scan.py +303 -0
- package/scripts/finding_fleet.sh +180 -0
- package/scripts/finding_pipeline.sh +213 -0
- package/scripts/finding_verifier.sh +144 -0
- package/scripts/finding_verify.py +283 -0
- package/scripts/gate_pathspec_check.sh +1 -0
- package/scripts/gate_shape_scan.sh +106 -0
- package/scripts/selfcheck.sh +50 -0
- package/scripts/test_doc_claim_triad_lanes.sh +124 -0
- package/scripts/test_finding_pipeline_lanes.sh +459 -0
- package/scripts/test_gate_shape_scan_lanes.sh +36 -0
- package/scripts/test_heavy_classifier_lanes.sh +13 -3
- package/scripts/test_preprep_font_lanes.sh +87 -0
- package/templates/.git-hooks/pre-commit +6 -5
- package/templates/PRE-PUBLISH-CHECKLIST.md +29 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# gate_shape_scan.sh — is this file «gate-shaped»? The mechanical half of the Field-Harness
|
|
3
|
+
# Load-Bearing Change Gate trigger for files whose project is NOT mapped (CLAUDE.md
|
|
4
|
+
# §Field-Harness Load-Bearing Change Gate · field_verdict_crossfamily_gate.md
|
|
5
|
+
# §Registration is not a precondition, 2026-09-08).
|
|
6
|
+
#
|
|
7
|
+
# IT IS A CLASSIFIER OF THE FILE'S SHAPE, NOT A VERDICT ON THE FILE. A hit means «this file is
|
|
8
|
+
# in the gate's scope — run degrade-lint + cross-family before a merge verdict». A miss means
|
|
9
|
+
# «not in scope by identifier»; the task's own naming of the file as gate/auth/exposure code is a
|
|
10
|
+
# separate MANUAL escalation and is not read here (a prompt is not a property of the file).
|
|
11
|
+
#
|
|
12
|
+
# What counts (three classes — each hit is printed with the class and the matched line):
|
|
13
|
+
# VERDICT word-bounded, case-insensitive: allow(ed) deny denied permit(ted) approve/approved/approval
|
|
14
|
+
# verdict permission(s) auth authn authz authorize(d) authorization authenticat* auth_<x>
|
|
15
|
+
# (🟥 `author`/`authored`/`allowance` do NOT match — the auth branch is a closed list, not `auth.*`)
|
|
16
|
+
# (🟥 `reject` is deliberately absent — it collides with the Promise API `reject(err)`,
|
|
17
|
+
# measured on a real TS mock server: 2 hits, both Promise callbacks. Named residual:
|
|
18
|
+
# a gate that spells its refusal `reject(` is missed by this class; `deny` is not.)
|
|
19
|
+
# permission auth authn authz · plus the UPPERCASE enum literals PASS FAIL BLOCK
|
|
20
|
+
# (uppercase only — lowercase `pass`/`fail`/`block` are ordinary words/keywords)
|
|
21
|
+
# EXPOSURE a socket/server bind or listen: listen( bind( ListenAndServe Serve( 0.0.0.0 INADDR_ANY
|
|
22
|
+
# IRREV an irreversible-op path: publish( delete( unlink( rm -rf force-push --force
|
|
23
|
+
# history-rewrite filter-branch reset --hard
|
|
24
|
+
# Comment/string handling (named residual, crude on purpose): a line whose first non-blank
|
|
25
|
+
# characters are `#`, `//`, `* ` (star+space/end: block-comment body), `/*`, `-- ` (dash-dash+space/end:
|
|
26
|
+
# SQL/Lua), `;`, `"""`, `'''` is skipped; `*allow = 1` and a continued `--force` line are CODE and scanned. Inline trailing comments
|
|
27
|
+
# and string literals are NOT stripped — a false positive here costs one review, a false negative
|
|
28
|
+
# costs a missed gate, so the scan leans toward firing.
|
|
29
|
+
# Supported: any text file. Binary or unreadable → UNSCANNABLE (exit 3), never a silent miss.
|
|
30
|
+
#
|
|
31
|
+
# Usage: bash scripts/gate_shape_scan.sh <file> [file ...]
|
|
32
|
+
# bash scripts/gate_shape_scan.sh --selftest # known-pair calibration
|
|
33
|
+
# Exit: 0 = GATE-SHAPED (≥1 hit, all files scannable) · 1 = NOT-GATE-SHAPED · 3 = UNSCANNABLE/usage
|
|
34
|
+
# (3 dominates 0: a hit next to an unscannable file still exits 3 — that file is unresolved)
|
|
35
|
+
set -uo pipefail
|
|
36
|
+
export LC_ALL=C
|
|
37
|
+
VERDICT_RE='\b(allow(ed)?|deny|denied|permit(ted)?|approv(e|ed|al)|verdict|permissions?|auth(n|z|orize|orized|orization|entic[a-z]*|_[a-z_]+)?)\b'
|
|
38
|
+
ENUM_RE='\b(PASS|FAIL|BLOCK)\b'
|
|
39
|
+
EXPOSURE_RE='(\blisten\(|\bbind\(|ListenAndServe|\bServe\(|0\.0\.0\.0|INADDR_ANY)'
|
|
40
|
+
IRREV_RE='(\bpublish\(|\bdelete\(|\bunlink\(|rm -rf|force-push|--force\b|history-rewrite|filter-branch|reset --hard)'
|
|
41
|
+
COMMENT_RE='^[[:space:]]*(#|//|\*([[:space:]]|$)|/\*|--([[:space:]]|$)|;|"""|'"'''"')'
|
|
42
|
+
|
|
43
|
+
scan_one() { # $1=file → prints hits, returns 0 hit / 1 none / 3 unscannable
|
|
44
|
+
local f="$1" body v e r hits
|
|
45
|
+
[ -r "$f" ] || { echo "UNSCANNABLE $f (unreadable)"; return 3; }
|
|
46
|
+
if /usr/bin/file -b --mime-encoding "$f" 2>/dev/null | /usr/bin/grep -q '^binary$'; then
|
|
47
|
+
echo "UNSCANNABLE $f (binary)"; return 3; fi
|
|
48
|
+
# one pass: drop comment-led lines, then classify (VERDICT wins over EXPOSURE over IRREV per line)
|
|
49
|
+
body=$(/usr/bin/grep -nEv "$COMMENT_RE" "$f" 2>/dev/null || true)
|
|
50
|
+
v=$(printf '%s\n' "$body" | /usr/bin/grep -Ei "$VERDICT_RE" || true)
|
|
51
|
+
v2=$(printf '%s\n' "$body" | /usr/bin/grep -E "$ENUM_RE" || true)
|
|
52
|
+
e=$(printf '%s\n' "$body" | /usr/bin/grep -E "$EXPOSURE_RE" || true)
|
|
53
|
+
r=$(printf '%s\n' "$body" | /usr/bin/grep -E "$IRREV_RE" || true)
|
|
54
|
+
hits=0
|
|
55
|
+
# 🟥 파일명을 sed 프로그램에 넣지 않는다. `s|^|... $f:|` 는 파일명 안의 `|` 로 구분자를 벗어나고,
|
|
56
|
+
# 거기에 `r <path>` 를 이어 붙이면 임의 파일 내용이 스캔 출력에 실리고 `w` 는 덮어쓴다
|
|
57
|
+
# (cross-family security review 2026-09-09, 무해한 프로브로 추가 파일 읽기 실증).
|
|
58
|
+
# awk 의 ENVIRON 은 값을 «프로그램»이 아니라 «데이터»로 받으므로 이 통로가 닫힌다.
|
|
59
|
+
[ -n "$v" ] && { hits=1; printf '%s\n' "$v" | /usr/bin/cut -c1-140 | FH_LABEL="VERDICT $f:" /usr/bin/awk '{print ENVIRON["FH_LABEL"] $0}'; }
|
|
60
|
+
[ -n "$v2" ] && { hits=1; printf '%s\n' "$v2" | /usr/bin/cut -c1-140 | FH_LABEL="VERDICT $f:" /usr/bin/awk '{print ENVIRON["FH_LABEL"] $0}'; }
|
|
61
|
+
[ -n "$e" ] && { hits=1; printf '%s\n' "$e" | /usr/bin/cut -c1-140 | FH_LABEL="EXPOSURE $f:" /usr/bin/awk '{print ENVIRON["FH_LABEL"] $0}'; }
|
|
62
|
+
[ -n "$r" ] && { hits=1; printf '%s\n' "$r" | /usr/bin/cut -c1-140 | FH_LABEL="IRREV $f:" /usr/bin/awk '{print ENVIRON["FH_LABEL"] $0}'; }
|
|
63
|
+
[ "$hits" -gt 0 ] && return 0 || return 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
selftest() { # known pair — positive must hit, negative must not, comment-only must not
|
|
67
|
+
local d rc_pos rc_pos2 rc_neg rc_cmt rc_bin rc_irr rc_bnd rc_star fails=0
|
|
68
|
+
d="$(mktemp -d 2>/dev/null)" || d=""
|
|
69
|
+
[ -n "$d" ] && [ -w "$d" ] || { echo "SELFTEST: ENV-BLOCKED (mktemp -d failed) — result unmeasured, not a pass"; return 3; }
|
|
70
|
+
printf 'export class S {\n start() {\n this.app.listen(this.config.port, () => {});\n }\n}\n' > "$d/pos_exposure.ts"
|
|
71
|
+
printf 'def check(user):\n if user.role == "admin":\n return Verdict.ALLOW\n return Verdict.DENY\n' > "$d/pos_verdict.py"
|
|
72
|
+
printf 'def add(a, b):\n """Return the sum. Pass through ints."""\n return a + b\n\ndef fmt(x):\n return f"{x:.2f}"\n' > "$d/neg_util.py"
|
|
73
|
+
printf '# we allow anything here\n// auth is elsewhere\n/* listen( is not called */\n * allow: block-comment body\n-- deny in a SQL comment\nx = 1\n' > "$d/neg_comments.py"
|
|
74
|
+
printf '\x00\x01\x02binary\x00' > "$d/bin.dat"
|
|
75
|
+
printf 'set -e\ngit push origin main \\\n --force\n' > "$d/pos_irrev.sh"
|
|
76
|
+
printf 'authored_by = "x"\nallowance = 3\nauthor = "y"\n' > "$d/neg_boundary.py"
|
|
77
|
+
printf 'int f(int *allow) {\n *allow = 1;\n return 0;\n}\n' > "$d/pos_star.c"
|
|
78
|
+
scan_one "$d/pos_exposure.ts" >/dev/null; rc_pos=$?
|
|
79
|
+
scan_one "$d/pos_verdict.py" >/dev/null; rc_pos2=$?
|
|
80
|
+
scan_one "$d/neg_util.py" >/dev/null; rc_neg=$?
|
|
81
|
+
scan_one "$d/neg_comments.py" >/dev/null; rc_cmt=$?
|
|
82
|
+
scan_one "$d/bin.dat" >/dev/null; rc_bin=$?
|
|
83
|
+
scan_one "$d/pos_irrev.sh" >/dev/null; rc_irr=$?
|
|
84
|
+
scan_one "$d/neg_boundary.py" >/dev/null; rc_bnd=$?
|
|
85
|
+
scan_one "$d/pos_star.c" >/dev/null; rc_star=$?
|
|
86
|
+
[ "$rc_pos" -eq 0 ] && echo " ✅ known-positive exposure (listen()) → GATE-SHAPED" || { echo " ❌ known-positive exposure rc=$rc_pos"; fails=1; }
|
|
87
|
+
[ "$rc_pos2" -eq 0 ] && echo " ✅ known-positive verdict (ALLOW/DENY) → GATE-SHAPED" || { echo " ❌ known-positive verdict rc=$rc_pos2"; fails=1; }
|
|
88
|
+
[ "$rc_neg" -eq 1 ] && echo " ✅ known-negative util (docstring 'Pass') → NOT" || { echo " ❌ known-negative util rc=$rc_neg"; fails=1; }
|
|
89
|
+
[ "$rc_cmt" -eq 1 ] && echo " ✅ comment-only mentions (#, //, /*, ' * ', '-- ') → NOT" || { echo " ❌ comment-only rc=$rc_cmt"; fails=1; }
|
|
90
|
+
[ "$rc_bin" -eq 3 ] && echo " ✅ binary → UNSCANNABLE (not a silent miss)" || { echo " ❌ binary rc=$rc_bin"; fails=1; }
|
|
91
|
+
[ "$rc_irr" -eq 0 ] && echo " ✅ known-positive irreversible (continued --force line) → GATE-SHAPED" || { echo " ❌ known-positive irrev rc=$rc_irr"; fails=1; }
|
|
92
|
+
[ "$rc_bnd" -eq 1 ] && echo " ✅ boundary: author/authored/allowance → NOT" || { echo " ❌ boundary rc=$rc_bnd"; fails=1; }
|
|
93
|
+
[ "$rc_star" -eq 0 ] && echo " ✅ code line starting with *allow (not a comment) → GATE-SHAPED" || { echo " ❌ star-code rc=$rc_star"; fails=1; }
|
|
94
|
+
/bin/rm -rf "$d"
|
|
95
|
+
[ "$fails" -eq 0 ] && { echo "SELFTEST: PASS"; return 0; } || { echo "SELFTEST: FAIL"; return 3; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
[ $# -ge 1 ] || { echo "usage: $0 <file> [file ...] | --selftest" >&2; exit 3; }
|
|
99
|
+
[ "$1" = "--selftest" ] && { selftest; exit $?; }
|
|
100
|
+
ANY=0; UNS=0
|
|
101
|
+
for f in "$@"; do scan_one "$f"; rc=$?; [ "$rc" -eq 0 ] && ANY=1; [ "$rc" -eq 3 ] && UNS=1; done
|
|
102
|
+
if [ "$UNS" -eq 1 ]; then
|
|
103
|
+
[ "$ANY" -eq 1 ] && echo "GATE-SHAPED in the scannable files —"
|
|
104
|
+
echo "UNSCANNABLE — ≥1 file could not be classified; decide those by hand. Exit 3 dominates any hit (never a silent miss)"; exit 3
|
|
105
|
+
elif [ "$ANY" -eq 1 ]; then echo "GATE-SHAPED — run degrade-lint + cross-family before a merge verdict"; exit 0
|
|
106
|
+
else echo "NOT-GATE-SHAPED (by identifier; the task's own naming is a separate manual escalation)"; exit 1; fi
|
package/scripts/selfcheck.sh
CHANGED
|
@@ -418,6 +418,55 @@ else
|
|
|
418
418
|
fail=1
|
|
419
419
|
fi
|
|
420
420
|
|
|
421
|
+
# doc-claim triad (bridge ①) — a doc that says «A uses B» where A never executes B.
|
|
422
|
+
# Subject = scripts/doc_claim_triad_scan.py (review surface, not a verdict). Wired in the same
|
|
423
|
+
# commit that created its lane, because lane_runner_check.sh rejects an undeclared unwired suite —
|
|
424
|
+
# and adding it to DEBT instead would be the regrowth that file exists to stop.
|
|
425
|
+
if [ ! -f scripts/doc_claim_triad_scan.py ]; then
|
|
426
|
+
_absent_subject_verdict "doc-claim triad lanes" "scripts/doc_claim_triad_scan.py" || fail=1
|
|
427
|
+
elif [ -f scripts/test_doc_claim_triad_lanes.sh ]; then
|
|
428
|
+
if ! bash scripts/test_doc_claim_triad_lanes.sh; then
|
|
429
|
+
fail=1
|
|
430
|
+
fi
|
|
431
|
+
else
|
|
432
|
+
echo "FAIL doc-claim triad lanes: doc_claim_triad_scan.py present but its anchor is missing"
|
|
433
|
+
fail=1
|
|
434
|
+
fi
|
|
435
|
+
|
|
436
|
+
# typed-finding pipeline — fleet (multi-family, parallel) + reject stage (cross-family verdict, code
|
|
437
|
+
# drops false positives). Subject = scripts/finding_fleet.sh + scripts/finding_verify.py.
|
|
438
|
+
if [ ! -f scripts/finding_verify.py ]; then
|
|
439
|
+
_absent_subject_verdict "finding pipeline lanes" "scripts/finding_verify.py" || fail=1
|
|
440
|
+
elif [ -f scripts/test_finding_pipeline_lanes.sh ]; then
|
|
441
|
+
if ! bash scripts/finding_fleet.sh --selftest >/dev/null 2>&1; then
|
|
442
|
+
echo "FAIL finding fleet selftest: known-pair calibration failed"; fail=1
|
|
443
|
+
fi
|
|
444
|
+
if ! bash scripts/test_finding_pipeline_lanes.sh; then
|
|
445
|
+
fail=1
|
|
446
|
+
fi
|
|
447
|
+
else
|
|
448
|
+
echo "FAIL finding pipeline lanes: scripts present but their anchor is missing"
|
|
449
|
+
fail=1
|
|
450
|
+
fi
|
|
451
|
+
|
|
452
|
+
# gate-shape classifier — the mechanical half of the unmapped-file trigger of the Field-Harness
|
|
453
|
+
# Load-Bearing Change Gate (2026-09-08). A classifier of scope, not a verdict; its lane holds the
|
|
454
|
+
# known-pair (exposure/verdict positives · util/comment negatives · Promise reject( FP anchor).
|
|
455
|
+
if [ ! -f scripts/gate_shape_scan.sh ]; then
|
|
456
|
+
_absent_subject_verdict "gate-shape lanes" "scripts/gate_shape_scan.sh" || fail=1
|
|
457
|
+
elif [ -f scripts/test_gate_shape_scan_lanes.sh ]; then
|
|
458
|
+
# direct dispatch of the subject's own selftest (caller surface for the ratchet), then the lane
|
|
459
|
+
if ! bash scripts/gate_shape_scan.sh --selftest >/dev/null 2>&1; then
|
|
460
|
+
echo "FAIL gate-shape selftest: known-pair calibration failed"; fail=1
|
|
461
|
+
fi
|
|
462
|
+
if ! bash scripts/test_gate_shape_scan_lanes.sh; then
|
|
463
|
+
fail=1
|
|
464
|
+
fi
|
|
465
|
+
else
|
|
466
|
+
echo "FAIL gate-shape lanes: gate_shape_scan.sh present but its anchor is missing"
|
|
467
|
+
fail=1
|
|
468
|
+
fi
|
|
469
|
+
|
|
421
470
|
# package-coverage — a shipped doc must not point at a file the tarball omits. Distinct from the
|
|
422
471
|
# ref-path check below: that one asks "does this path exist at all", this one asks "does the
|
|
423
472
|
# CONSUMER get it". Measured 2026-07-28: 35 paths existed, were named by a shipped doc, and were
|
|
@@ -663,6 +712,7 @@ for _pair in \
|
|
|
663
712
|
"plugins/fh-commons/skills/preprep/lane_adjacent_dup.py|scripts/test_preprep_adjacent_dup_lanes.sh" \
|
|
664
713
|
"plugins/fh-commons/skills/preprep/lane_promise.py|scripts/test_preprep_promise_lanes.sh" \
|
|
665
714
|
"plugins/fh-commons/skills/preprep/lane_slide_refs.py|scripts/test_preprep_slide_refs_lanes.sh" \
|
|
715
|
+
"plugins/fh-commons/skills/preprep/lane_font.py|scripts/test_preprep_font_lanes.sh" \
|
|
666
716
|
"plugins/fh-commons/skills/preprep/SKILL.md|scripts/test_preprep_drift_anchor.sh" \
|
|
667
717
|
"scripts/test_preprep_drift_anchor.sh|scripts/test_preprep_drift_anchor_lanes.sh" \
|
|
668
718
|
"scripts/field_canon_preload.sh|scripts/test_skill_canon_preload_lanes.sh" \
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_doc_claim_triad_lanes.sh — scripts/doc_claim_triad_scan.py 회귀 앵커.
|
|
3
|
+
#
|
|
4
|
+
# 🟥 이 레인이 지키는 것은 «숫자»가 아니라 «판별력»이다. 그 계기는 세션 중 세 번 뚫렸고
|
|
5
|
+
# (공존≠관계 · 방향 미해결 · 나열 통과) 세 번 다 손검증이 잡았다. 레인은 그 셋이
|
|
6
|
+
# 되돌아오는지를 본다 — 픽스처는 실제로 뚫렸던 표기를 그대로 쓴다.
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"; ROOT="$(dirname "$HERE")"
|
|
9
|
+
SCAN="$HERE/doc_claim_triad_scan.py"
|
|
10
|
+
PASS=0; FAIL=0
|
|
11
|
+
ok(){ PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; }
|
|
12
|
+
no(){ FAIL=$((FAIL+1)); printf ' ❌ %s — %s\n' "$1" "${2:-}"; }
|
|
13
|
+
|
|
14
|
+
echo "── test_doc_claim_triad_lanes ──"
|
|
15
|
+
|
|
16
|
+
# L1 — 파일 실재 + 실행 가능
|
|
17
|
+
[ -f "$SCAN" ] || { no "L1 파일 실재" "$SCAN 없음"; echo "FAILED=1"; exit 1; }
|
|
18
|
+
python3 -c "import ast,sys;ast.parse(open(sys.argv[1],encoding='utf-8').read())" "$SCAN" >/dev/null 2>&1 \
|
|
19
|
+
&& ok "L1 구문" || no "L1 구문" "ast.parse 실패"
|
|
20
|
+
|
|
21
|
+
# L2 — 계기 보정이 통과한다 (양쪽 절반)
|
|
22
|
+
OUT=$(python3 "$SCAN" --selftest "$ROOT" 2>&1); RC=$?
|
|
23
|
+
[ "$RC" -eq 0 ] && ok "L2 selftest rc=0" || no "L2 selftest" "rc=$RC"
|
|
24
|
+
printf '%s' "$OUT" | grep -q "주장 추출기" && ok "L2b 추출기 보정 실행됨" || no "L2b 추출기 보정" "절이 없다"
|
|
25
|
+
printf '%s' "$OUT" | grep -q "실행 판별기" && ok "L2c 실행판별 보정 실행됨" || no "L2c 실행판별 보정" "절이 없다"
|
|
26
|
+
|
|
27
|
+
# L3 — 🟥 나열을 주장으로 세지 않는다 (v1 이 뚫린 자리, 실제 표기)
|
|
28
|
+
python3 - "$SCAN" <<'PY' && ok "L3 나열 거부" || no "L3 나열 거부" "나열이 주장으로 셰짐"
|
|
29
|
+
import importlib.util,sys
|
|
30
|
+
spec=importlib.util.spec_from_file_location("m",sys.argv[1]); m=importlib.util.module_from_spec(spec)
|
|
31
|
+
spec.loader.exec_module(m)
|
|
32
|
+
A,B="scripts/foo.sh","scripts/bar.sh"
|
|
33
|
+
# 🟥 cross-family codex [B]: 가운뎃점·백틱·쉼표만 막으면 «and/plus/인용부호» 로 뚫린다.
|
|
34
|
+
# 그 표기를 픽스처에 넣는다 — 실제로 CATALOG.md:137 이 `X (pre-commit) and Y (pre-push)` 였다.
|
|
35
|
+
lists=[f"**File:** {A} · {B} · templates/.git-hooks/pre-commit",
|
|
36
|
+
f"Fixtures: `{A}` · `{B}`.", f"Anchors: `{A}`, `{B}`",
|
|
37
|
+
f"Anchors: `{A}` and `{B}`", f"see `{A}` plus `{B}`",
|
|
38
|
+
f'"{A}" "{B}"']
|
|
39
|
+
sys.exit(0 if not any(m.asserts_relation(l,A,B) for l in lists) else 1)
|
|
40
|
+
PY
|
|
41
|
+
|
|
42
|
+
# L4 — 관계 주장은 잡는다 (한/영 둘 다 — 언어 중립 요구)
|
|
43
|
+
python3 - "$SCAN" <<'PY' && ok "L4 관계 포착(한·영)" || no "L4 관계 포착" "관계를 놓쳤다"
|
|
44
|
+
import importlib.util,sys
|
|
45
|
+
spec=importlib.util.spec_from_file_location("m",sys.argv[1]); m=importlib.util.module_from_spec(spec)
|
|
46
|
+
spec.loader.exec_module(m)
|
|
47
|
+
A,B="scripts/foo.sh","scripts/bar.sh"
|
|
48
|
+
rel=[f"{A} 는 {B} 를 호출한다", f"`{A}` is wired into `{B}`", f"the hook {A} blocks by running {B}"]
|
|
49
|
+
sys.exit(0 if all(m.asserts_relation(l,A,B) for l in rel) else 1)
|
|
50
|
+
PY
|
|
51
|
+
|
|
52
|
+
# L5 — 🟥 실행 vs 언급. 실제로 뚫렸던 표기(grep 대안식 안의 `name)\.sh`)를 픽스처로 쓴다
|
|
53
|
+
TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT
|
|
54
|
+
cat > "$TMP/hook_exec.sh" <<'EOF'
|
|
55
|
+
#!/usr/bin/env bash
|
|
56
|
+
bash "$REPO_ROOT/scripts/target_thing.sh" "$REPO_ROOT"
|
|
57
|
+
EOF
|
|
58
|
+
cat > "$TMP/hook_mention.sh" <<'EOF'
|
|
59
|
+
#!/usr/bin/env bash
|
|
60
|
+
# target_thing.sh is only named here, never run
|
|
61
|
+
git diff --name-only | grep -E '(scripts/(other|target_thing)\.sh)' || true
|
|
62
|
+
EOF
|
|
63
|
+
# 🟥 cross-family codex [A]: 출력 구문 안의 인터프리터 호출은 «실행처럼 보이지만» 아무것도 안 돌린다
|
|
64
|
+
cat > "$TMP/hook_echo.sh" <<'EOF'
|
|
65
|
+
#!/usr/bin/env bash
|
|
66
|
+
echo "run it yourself: bash scripts/target_thing.sh"
|
|
67
|
+
EOF
|
|
68
|
+
python3 - "$SCAN" "$TMP" <<'PY' && ok "L5 실행/언급 판별" || no "L5 실행/언급 판별" "둘을 못 가른다"
|
|
69
|
+
import importlib.util,sys,os
|
|
70
|
+
spec=importlib.util.spec_from_file_location("m",sys.argv[1]); m=importlib.util.module_from_spec(spec)
|
|
71
|
+
spec.loader.exec_module(m)
|
|
72
|
+
t=sys.argv[2]
|
|
73
|
+
v1,_=m.executes(os.path.join(t,"hook_exec.sh"),"target_thing.sh")
|
|
74
|
+
v2,_=m.executes(os.path.join(t,"hook_mention.sh"),"target_thing.sh")
|
|
75
|
+
v3,_=m.executes(os.path.join(t,"hook_echo.sh"),"target_thing.sh")
|
|
76
|
+
sys.exit(0 if (v1=="EXECUTES" and v2=="MENTIONS-ONLY" and v3=="MENTIONS-ONLY") else 1)
|
|
77
|
+
PY
|
|
78
|
+
|
|
79
|
+
# L6 — 🟥 보정 실패 시 «스캔을 거부»한다 (fail-closed). 뮤턴트로 확인하고 되돌린다
|
|
80
|
+
CP="$TMP/mutant.py"; cp "$SCAN" "$CP"
|
|
81
|
+
python3 - "$CP" <<'PY'
|
|
82
|
+
import sys
|
|
83
|
+
p=sys.argv[1]; L=open(p,encoding="utf-8").read().split("\n")
|
|
84
|
+
i=[k for k,l in enumerate(L) if l.startswith("def asserts_relation(")][0]
|
|
85
|
+
L.insert(i+1," return True # MUTANT: 모든 것을 주장이라 한다")
|
|
86
|
+
open(p,"w",encoding="utf-8").write("\n".join(L))
|
|
87
|
+
PY
|
|
88
|
+
MOUT=$(python3 "$CP" --selftest "$ROOT" 2>&1); MRC=$?
|
|
89
|
+
[ "$MRC" -eq 2 ] && ok "L6 뮤턴트 → rc=2 거부" || no "L6 뮤턴트 거부" "rc=$MRC (기대 2)"
|
|
90
|
+
MS=$(python3 "$CP" "$ROOT" 2>&1); MSRC=$?
|
|
91
|
+
[ "$MSRC" -eq 2 ] && ok "L6b 뮤턴트는 스캔도 거부" || no "L6b 뮤턴트 스캔거부" "rc=$MSRC (기대 2)"
|
|
92
|
+
|
|
93
|
+
# L8 — 🟥 known-pair 가 «낡으면» 보정이 실패한다 (cross-family codex [S]).
|
|
94
|
+
# B 가 삭제·이동돼도 A 가 이름만 언급하면 통과하던 자리.
|
|
95
|
+
cat > "$TMP/fakeroot_probe.py" <<'PY8'
|
|
96
|
+
import importlib.util,sys,os
|
|
97
|
+
spec=importlib.util.spec_from_file_location("m",sys.argv[1]); m=importlib.util.module_from_spec(spec)
|
|
98
|
+
spec.loader.exec_module(m)
|
|
99
|
+
sys.exit(0 if m.resolve(sys.argv[2], "scripts/zzq_nonexistent_target.sh") is None else 1)
|
|
100
|
+
PY8
|
|
101
|
+
python3 "$TMP/fakeroot_probe.py" "$SCAN" "$ROOT" && ok "L8 부재 대상은 resolve=None" || no "L8 resolve" "없는 파일을 찾았다고 했다"
|
|
102
|
+
|
|
103
|
+
# L7 — 🟥 «진짜» 되돌림: 원본 자리에 뮤턴트를 넣었다 뺀다. 빨강→초록 전이를 본다
|
|
104
|
+
cp "$SCAN" "$TMP/orig_backup.py"
|
|
105
|
+
python3 - "$SCAN" <<'PY7'
|
|
106
|
+
import sys
|
|
107
|
+
p=sys.argv[1]; L=open(p,encoding="utf-8").read().split("\n")
|
|
108
|
+
i=[k for k,l in enumerate(L) if l.startswith("def asserts_relation(")][0]
|
|
109
|
+
L.insert(i+1," return True # MUTANT-INPLACE")
|
|
110
|
+
open(p,"w",encoding="utf-8").write("\n".join(L))
|
|
111
|
+
PY7
|
|
112
|
+
python3 "$SCAN" --selftest "$ROOT" >/dev/null 2>&1; INJ=$?
|
|
113
|
+
cp "$TMP/orig_backup.py" "$SCAN"
|
|
114
|
+
python3 "$SCAN" --selftest "$ROOT" >/dev/null 2>&1; RST=$?
|
|
115
|
+
LEFT=$(grep -c "MUTANT-INPLACE" "$SCAN" || true)
|
|
116
|
+
if [ "$INJ" -eq 2 ] && [ "$RST" -eq 0 ] && [ "$LEFT" -eq 0 ]; then
|
|
117
|
+
ok "L7 되돌림 전이 (주입 rc=2 → 복원 rc=0, 잔존 0)"
|
|
118
|
+
else
|
|
119
|
+
no "L7 되돌림 전이" "주입rc=$INJ 복원rc=$RST 잔존=$LEFT"
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
echo "── PASS=$PASS FAIL=$FAIL ──"
|
|
123
|
+
[ "$FAIL" -eq 0 ] || { echo "FAILED=1"; exit 1; }
|
|
124
|
+
echo "FAILED=0"
|