@chrono-meta/fh-gate 3.1.4 → 3.4.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/registry/agent_cards.json +1 -1
- package/.claude/rules/fh_4axis_gate.md +26 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/AGENTS.md +8 -2
- package/CATALOG.md +4 -4
- package/CHEATSHEET.md +1 -1
- package/CLAUDE.md +54 -10
- 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 +2 -2
- package/docs/STANDARDS_ALIGNMENT.md +1 -1
- package/docs/codex-compat.md +1 -1
- package/knowledge/shared/harness-core/agents_md_runtime_details.md +2 -2
- 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 +76 -3
- package/knowledge/shared/harness-core/governance_engineering_definition.md +89 -0
- package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +1 -1
- package/knowledge/shared/harness-core/skill_quality_rubric.md +1 -1
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +99 -0
- package/knowledge/shared/rules/auto_project_mapping.md +1 -1
- package/knowledge/shared/rules/modes_and_value.md +2 -2
- package/package.json +12 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/README.md +38 -0
- 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 +164 -0
- package/plugins/fh-meta/skills/agent-composer/SKILL.md +2 -2
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +40 -0
- package/plugins/fh-meta/skills/frontier-digest/SKILL.md +1 -1
- package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +43 -5
- package/plugins/fh-meta/skills/{hub-cc-pr-reviewer → harness-pr-reviewer}/SKILL.md +75 -3
- package/plugins/fh-meta/skills/{hub-cc-pr-reviewer → harness-pr-reviewer}/SKILL_detail.md +2 -2
- package/plugins/fh-meta/skills/harvest-loop/SKILL_detail.md +2 -2
- package/plugins/fh-meta/skills/install-doctor/SKILL.md +1 -1
- package/plugins/fh-meta/skills/install-wizard/SKILL.md +1 -1
- package/plugins/fh-meta/skills/meta-prompt-builder/SKILL.md +1 -1
- package/plugins/fh-meta/skills/pipeline-conductor/SKILL.md +1 -1
- package/plugins/fh-meta/skills/plugin-recommender/SKILL.md +1 -1
- package/plugins/fh-meta/skills/sim-conductor/SKILL.md +1 -1
- package/plugins/fh-qp/.claude-plugin/plugin.json +1 -1
- package/scripts/doc_claim_triad_scan.py +303 -0
- package/scripts/finding_fleet.sh +558 -0
- package/scripts/finding_pipeline.sh +572 -0
- package/scripts/finding_verifier.sh +173 -0
- package/scripts/finding_verify.py +468 -0
- package/scripts/frontier_digest_autopilot.sh +3 -3
- package/scripts/gate_pathspec_check.sh +1 -0
- package/scripts/gate_shape_scan.sh +120 -0
- package/scripts/selfcheck.sh +50 -0
- package/scripts/test_doc_claim_triad_lanes.sh +124 -0
- package/scripts/test_finding_pipeline_lanes.sh +2011 -0
- package/scripts/test_gate_shape_scan_lanes.sh +47 -0
- package/scripts/test_heavy_classifier_lanes.sh +13 -3
- package/scripts/test_marker_crossfamily_lanes.sh +75 -3
- package/scripts/test_marker_standpoint_lanes.sh +31 -6
- package/scripts/test_preprep_font_lanes.sh +87 -0
- package/templates/.git-hooks/pre-commit +158 -11
- package/templates/PRE-PUBLISH-CHECKLIST.md +29 -0
- package/templates/local_fh_context.md +1 -1
- package/templates/regression_guard.sh +1 -1
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
# BACK-END SHIPPING DOCTRINE (operator, 2026-08-15): "개발의 앞단-영혼심기, 중간단-탈상관 가속화,
|
|
23
23
|
# 뒷단 출하전-4단검증 및 하네스오너 리뷰" — the back end of shipping is 4-axis verification AND a
|
|
24
24
|
# standpoint-axis review, not 4-axis alone. CORRECTED same session (an earlier draft of this
|
|
25
|
-
# header named fh-meta:
|
|
25
|
+
# header named fh-meta:harness-pr-reviewer here — wrong skill, caught by the operator: "내가 말한건
|
|
26
26
|
# 하네스오너(너) 아니라 그 하네스에 에이전트가 들어가서 그 입장에서 리뷰한다는거야"). "하네스오너
|
|
27
27
|
# 리뷰" is NOT the human operator (that gate is merge, unconditionally human, unchanged) NOR
|
|
28
|
-
#
|
|
28
|
+
# harness-pr-reviewer (which checks FH's diff against FH's OWN conventions — same-repo
|
|
29
29
|
# self-consistency, a different question). It is the STANDPOINT AXIS
|
|
30
30
|
# (`knowledge/shared/harness-core/field_verdict_crossfamily_gate.md §7`, the mechanism behind the
|
|
31
31
|
# if(kakao)26 keynote's p15 "(c) 탈상관의 확장" slide — "계열을 늘려도 못 잡는 결함이 있습니다,
|
|
@@ -197,7 +197,7 @@ BACK-END CHECKPOINTS (operator instruction, 2026-08-15 - required steps on this
|
|
|
197
197
|
2. STANDPOINT AXIS (knowledge/shared/harness-core/field_verdict_crossfamily_gate.md §7 - read it before applying this step, this summary is not the full spec): does the diff alter ANOTHER harness's actual behavior, gate outcome, or interaction contract (not merely touch a path that happens to be synced elsewhere - the trigger is behavioral, not file-class)? Most ordinary FH self-improvement from a digest signal will correctly land on standpoint: not-applicable - that is a correct, expected answer, not a shortfall to fix. This is a genuinely different axis from "harness-owner reviewing FH's own conventions" - it means an agent actually running the diff's effect FROM the standpoint of the OTHER harness's own repo (family diversity alone does not catch this: the mechanism behind the if(kakao)26 keynote's p15 slide "(c) 탈상관의 확장" - "계열을 늘려도 못 잡는 결함이 있습니다, 입장을 바꾸면 보입니다" - §7 is built on three field incidents where full cross-family review missed a defect that only one execution-from-the-target's-own-repo caught).
|
|
198
198
|
- If NOT applicable (the common case): record standpoint: not-applicable in the marker/signal, state briefly what was checked (per the spec's own discipline - asserting non-applicability without naming what was checked is indistinguishable from not having looked at all), and move on.
|
|
199
199
|
- If applicable: check whether a local clone of the target harness exists on this machine (e.g. under the parent of ${FH_DIR} - sibling directories like pmh-dev, qasp-dev, or similar). If one exists, run the change against THAT repo's own content/rules from its own standpoint (tier2) - this must happen BEFORE any push or gh pr create for THIS diff, same non-negotiable pre-push timing as the irreversibility check above and for the identical reason (PR #370, 2026-08-14: a post-PR standpoint review still caught 2 residency leaks that had already sat in public view before the fix - public exposure is effectively irreversible, so this cannot run after the diff is visible). If it finds anything, fix it in the local diff and re-run until clean - only then push and open the PR (or, if step 1 already routed to hold, fold the finding into that signal file instead). If no local clone of the target harness is reachable, record standpoint: DEGRADED_NO_TARGET_ACCESS (could not, not did not) and proceed - do not block indefinitely on a target you structurally cannot reach, but do not silently claim not-applicable either when it actually is applicable and merely unreachable.
|
|
200
|
-
Do NOT conflate this with fh-meta:challenger (family/adversarial-correctness axis, already required by the 4-axis gate above) or with fh-meta:
|
|
200
|
+
Do NOT conflate this with fh-meta:challenger (family/adversarial-correctness axis, already required by the 4-axis gate above) or with fh-meta:harness-pr-reviewer (checks FH's own diff against FH's OWN baseline conventions - a same-repo self-consistency check, not a standpoint-axis review at all). All three are different lenses; running one is not a substitute for another.
|
|
201
201
|
|
|
202
202
|
3. If the diff is neither irreversible/load-bearing (step 1: NO) nor standpoint-applicable (step 2: NO or DEGRADED) - the common case, ordinary small reversible self-improvement: skip straight to branch/commit/push/PR below.
|
|
203
203
|
|
|
@@ -230,6 +230,7 @@ else
|
|
|
230
230
|
'CLAUDE.md|CLAUDE\.md' \
|
|
231
231
|
'AGENTS.md|AGENTS\.md' \
|
|
232
232
|
'scripts/**/*.sh|scripts/\*\*/\*\.sh' \
|
|
233
|
+
'scripts/**/*.py|scripts/\*\*/\*\.py' \
|
|
233
234
|
'agent definitions (plugins/*/agents)|plugins/\*/agents/' \
|
|
234
235
|
'agent definitions (.claude/agents)|\.claude/agents/'
|
|
235
236
|
do
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
# 🟥 pmh-dev #76 (2026-09-11): 여러 줄 docstring 의 «안쪽» 줄은 줄머리가 따옴표가 아니라 COMMENT_RE 를 통과했다 —
|
|
50
|
+
# 실물: 파이썬 테스트의 docstring 안 «fail-safe … 봉인» 문장이 GATE-SHAPED 로 지목됨. 줄 단위 grep 앞에
|
|
51
|
+
# """ / ''' 짝 상태를 awk 로 추적해 열린 동안의 줄을 전부 버린다(여는 줄·닫는 줄 포함). 한 줄 docstring 은
|
|
52
|
+
# 짝수라 상태가 안 바뀌고 종전 COMMENT_RE 가 그대로 거른다. 잔여(이름으로): JS 템플릿 리터럴(`) · 일반 문자열 안의 """.
|
|
53
|
+
body=$(/usr/bin/awk -v q="'''" -v t='"""' '
|
|
54
|
+
{ line=$0; n=gsub(t, "&", line); n+=gsub(q, "&", line)
|
|
55
|
+
if (indoc) { if (n % 2 == 1) indoc=0; next }
|
|
56
|
+
if (n % 2 == 1) { indoc=1; next }
|
|
57
|
+
print NR ":" $0 }' "$f" 2>/dev/null | /usr/bin/grep -Ev "^[0-9]+:${COMMENT_RE#^}" || true)
|
|
58
|
+
v=$(printf '%s\n' "$body" | /usr/bin/grep -Ei "$VERDICT_RE" || true)
|
|
59
|
+
v2=$(printf '%s\n' "$body" | /usr/bin/grep -E "$ENUM_RE" || true)
|
|
60
|
+
e=$(printf '%s\n' "$body" | /usr/bin/grep -E "$EXPOSURE_RE" || true)
|
|
61
|
+
r=$(printf '%s\n' "$body" | /usr/bin/grep -E "$IRREV_RE" || true)
|
|
62
|
+
hits=0
|
|
63
|
+
# 🟥 파일명을 sed 프로그램에 넣지 않는다. `s|^|... $f:|` 는 파일명 안의 `|` 로 구분자를 벗어나고,
|
|
64
|
+
# 거기에 `r <path>` 를 이어 붙이면 임의 파일 내용이 스캔 출력에 실리고 `w` 는 덮어쓴다
|
|
65
|
+
# (cross-family security review 2026-09-09, 무해한 프로브로 추가 파일 읽기 실증).
|
|
66
|
+
# awk 의 ENVIRON 은 값을 «프로그램»이 아니라 «데이터»로 받으므로 이 통로가 닫힌다.
|
|
67
|
+
[ -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}'; }
|
|
68
|
+
[ -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}'; }
|
|
69
|
+
[ -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}'; }
|
|
70
|
+
[ -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}'; }
|
|
71
|
+
[ "$hits" -gt 0 ] && return 0 || return 1
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
selftest() { # known pair — positive must hit, negative must not, comment-only must not
|
|
75
|
+
local d rc_pos rc_pos2 rc_neg rc_cmt rc_bin rc_irr rc_bnd rc_star rc_doc rc_doc2 fails=0
|
|
76
|
+
d="$(mktemp -d 2>/dev/null)" || d=""
|
|
77
|
+
[ -n "$d" ] && [ -w "$d" ] || { echo "SELFTEST: ENV-BLOCKED (mktemp -d failed) — result unmeasured, not a pass"; return 3; }
|
|
78
|
+
printf 'export class S {\n start() {\n this.app.listen(this.config.port, () => {});\n }\n}\n' > "$d/pos_exposure.ts"
|
|
79
|
+
printf 'def check(user):\n if user.role == "admin":\n return Verdict.ALLOW\n return Verdict.DENY\n' > "$d/pos_verdict.py"
|
|
80
|
+
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"
|
|
81
|
+
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"
|
|
82
|
+
printf '\x00\x01\x02binary\x00' > "$d/bin.dat"
|
|
83
|
+
printf 'set -e\ngit push origin main \\\n --force\n' > "$d/pos_irrev.sh"
|
|
84
|
+
printf 'authored_by = "x"\nallowance = 3\nauthor = "y"\n' > "$d/neg_boundary.py"
|
|
85
|
+
printf 'int f(int *allow) {\n *allow = 1;\n return 0;\n}\n' > "$d/pos_star.c"
|
|
86
|
+
printf 'def t():\n """docstring\n PASS allow true — fail-safe 계통은 봉인\n """\n return 1\n' > "$d/neg_docstring.py" # pmh-dev #76
|
|
87
|
+
printf 'def t():\n """docstring\n PASS allow true\n """\n return Verdict.ALLOW\n' > "$d/pos_after_docstring.py"
|
|
88
|
+
scan_one "$d/pos_exposure.ts" >/dev/null; rc_pos=$?
|
|
89
|
+
scan_one "$d/pos_verdict.py" >/dev/null; rc_pos2=$?
|
|
90
|
+
scan_one "$d/neg_util.py" >/dev/null; rc_neg=$?
|
|
91
|
+
scan_one "$d/neg_comments.py" >/dev/null; rc_cmt=$?
|
|
92
|
+
scan_one "$d/bin.dat" >/dev/null; rc_bin=$?
|
|
93
|
+
scan_one "$d/pos_irrev.sh" >/dev/null; rc_irr=$?
|
|
94
|
+
scan_one "$d/neg_boundary.py" >/dev/null; rc_bnd=$?
|
|
95
|
+
scan_one "$d/pos_star.c" >/dev/null; rc_star=$?
|
|
96
|
+
scan_one "$d/neg_docstring.py" >/dev/null; rc_doc=$?
|
|
97
|
+
scan_one "$d/pos_after_docstring.py" >/dev/null; rc_doc2=$?
|
|
98
|
+
[ "$rc_pos" -eq 0 ] && echo " ✅ known-positive exposure (listen()) → GATE-SHAPED" || { echo " ❌ known-positive exposure rc=$rc_pos"; fails=1; }
|
|
99
|
+
[ "$rc_pos2" -eq 0 ] && echo " ✅ known-positive verdict (ALLOW/DENY) → GATE-SHAPED" || { echo " ❌ known-positive verdict rc=$rc_pos2"; fails=1; }
|
|
100
|
+
[ "$rc_neg" -eq 1 ] && echo " ✅ known-negative util (docstring 'Pass') → NOT" || { echo " ❌ known-negative util rc=$rc_neg"; fails=1; }
|
|
101
|
+
[ "$rc_cmt" -eq 1 ] && echo " ✅ comment-only mentions (#, //, /*, ' * ', '-- ') → NOT" || { echo " ❌ comment-only rc=$rc_cmt"; fails=1; }
|
|
102
|
+
[ "$rc_bin" -eq 3 ] && echo " ✅ binary → UNSCANNABLE (not a silent miss)" || { echo " ❌ binary rc=$rc_bin"; fails=1; }
|
|
103
|
+
[ "$rc_irr" -eq 0 ] && echo " ✅ known-positive irreversible (continued --force line) → GATE-SHAPED" || { echo " ❌ known-positive irrev rc=$rc_irr"; fails=1; }
|
|
104
|
+
[ "$rc_bnd" -eq 1 ] && echo " ✅ boundary: author/authored/allowance → NOT" || { echo " ❌ boundary rc=$rc_bnd"; fails=1; }
|
|
105
|
+
[ "$rc_star" -eq 0 ] && echo " ✅ code line starting with *allow (not a comment) → GATE-SHAPED" || { echo " ❌ star-code rc=$rc_star"; fails=1; }
|
|
106
|
+
[ "$rc_doc" -eq 1 ] && echo " ✅ multi-line docstring interior ('PASS allow') → NOT (pmh-dev #76)" || { echo " ❌ docstring interior rc=$rc_doc"; fails=1; }
|
|
107
|
+
[ "$rc_doc2" -eq 0 ] && echo " ✅ control: real verdict line AFTER the docstring → GATE-SHAPED" || { echo " ❌ after-docstring rc=$rc_doc2"; fails=1; }
|
|
108
|
+
/bin/rm -rf "$d"
|
|
109
|
+
[ "$fails" -eq 0 ] && { echo "SELFTEST: PASS"; return 0; } || { echo "SELFTEST: FAIL"; return 3; }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
[ $# -ge 1 ] || { echo "usage: $0 <file> [file ...] | --selftest" >&2; exit 3; }
|
|
113
|
+
[ "$1" = "--selftest" ] && { selftest; exit $?; }
|
|
114
|
+
ANY=0; UNS=0
|
|
115
|
+
for f in "$@"; do scan_one "$f"; rc=$?; [ "$rc" -eq 0 ] && ANY=1; [ "$rc" -eq 3 ] && UNS=1; done
|
|
116
|
+
if [ "$UNS" -eq 1 ]; then
|
|
117
|
+
[ "$ANY" -eq 1 ] && echo "GATE-SHAPED in the scannable files —"
|
|
118
|
+
echo "UNSCANNABLE — ≥1 file could not be classified; decide those by hand. Exit 3 dominates any hit (never a silent miss)"; exit 3
|
|
119
|
+
elif [ "$ANY" -eq 1 ]; then echo "GATE-SHAPED — run degrade-lint + cross-family before a merge verdict"; exit 0
|
|
120
|
+
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"
|