@chrono-meta/fh-gate 2.6.0 → 2.8.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 +26 -3
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +28 -3
- package/CLAUDE.md +146 -168
- package/README.ja.md +144 -24
- package/README.ko.md +135 -22
- package/README.md +204 -27
- package/README.zh.md +126 -21
- package/docs/ETHOS.md +10 -3
- package/knowledge/shared/dialogue/ai_dialogue_playbook.md +131 -0
- package/knowledge/shared/harness-core/claude_md_gate_details.md +90 -0
- package/knowledge/shared/harness-core/dispatch_conditional_prohibition.md +75 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +77 -3
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +92 -5
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +12 -2
- package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +32 -0
- package/knowledge/shared/harness-core/ship_readiness_gate.md +77 -8
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +196 -0
- package/knowledge/shared/rules/knowledge_layer_seam.md +1 -1
- package/knowledge/shared/rules/multi_session_close_protocol.md +7 -3
- package/package.json +15 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +157 -0
- package/plugins/fh-meta/agents/persona-innovator.md +170 -0
- package/plugins/fh-meta/skills/cross-ecosystem-synergy-detection/SKILL.md +37 -0
- package/plugins/fh-meta/skills/public-surface-audit/SKILL_detail.md +16 -1
- package/plugins/fh-meta/skills/steel-quench/SKILL.md +97 -0
- package/scripts/adapters/peer_resolve.sh +58 -6
- package/scripts/branch_claim.sh +3 -1
- package/scripts/cluster_capability_scan.sh +42 -13
- package/scripts/digest_landing_check.sh +142 -2
- package/scripts/fh_hub_identity.sh +83 -0
- package/scripts/fh_session_load.sh +53 -5
- package/scripts/fh_track_resolve.sh +114 -0
- package/scripts/field_canon_preload.sh +50 -5
- package/scripts/listing_watch.sh +187 -0
- package/scripts/package_coverage_check.sh +8 -0
- package/scripts/prior_art_prompt.sh +168 -0
- package/scripts/psa_scan_lib.sh +201 -15
- package/scripts/residency_admission_check.sh +204 -0
- package/scripts/selfcheck.sh +103 -0
- package/scripts/test_adapter_lanes.sh +67 -2
- package/scripts/test_heavy_classifier_lanes.sh +144 -0
- package/scripts/test_listing_watch_lanes.sh +162 -0
- package/scripts/test_marker_defense_lanes.sh +152 -0
- package/scripts/test_marker_soul_check_lanes.sh +211 -0
- package/scripts/test_prior_art_prompt_lanes.sh +128 -0
- package/scripts/test_psa_singlefile_lanes.sh +351 -1
- package/scripts/test_residency_admission_lanes.sh +60 -0
- package/scripts/test_track_resolve_lanes.sh +158 -0
- package/scripts/test_wizard_snippet_merge_lanes.sh +40 -0
- package/templates/.git-hooks/pre-commit +400 -4
- package/templates/.git-hooks/pre-push +17 -2
- package/templates/settings.PriorArt.snippet.json +44 -0
|
@@ -268,6 +268,71 @@ echo
|
|
|
268
268
|
# 부재로 HARNESS_ERROR(3) 를 낸다 — 기대값과 같은 값이다. 즉 R2 는 Q5 를 원리적으로
|
|
269
269
|
# 구분하지 못한다. 그건 Q5 의 결함이 아니라 **이 프로브 축의 사각지대**이고,
|
|
270
270
|
# Q5 는 R1 이 아니라 별도 축(모호 판정 자체를 지우는 중화)으로만 흔들 수 있다.
|
|
271
|
+
# ── P 레인: peer_resolve 자체의 이름 해석 (2026-08-21 신설) ─────────────────────
|
|
272
|
+
# 🟥 이게 왜 필요한가: `peer_resolve.sh` 의 `FH_PROJECTS_HOME` 팔을 `fh_track_resolve.sh` 로
|
|
273
|
+
# 배선하면서 **거짓 AMBIGUOUS 결함이 하나 닫혔는데, 그 변경을 되돌려도 빨개지는 레인이
|
|
274
|
+
# 0개였다**(실측). 즉 기존 31 레인은 이 축에 대해 앵커가 아니다 —
|
|
275
|
+
# [[feedback_anchor_can_be_decorative]]. 픽스처는 **실제로 뚫려 있던 표기**(공백 이름)를 쓴다.
|
|
276
|
+
#
|
|
277
|
+
# 🟥 **초판 P 레인은 거짓 초록이었다 — 계기가 대상을 안 불렀다.** `peer_resolve.sh` 를
|
|
278
|
+
# `bash <파일> <이름>` 으로 «실행» 했는데 이 파일은 **sourced 라이브러리**라 실행하면
|
|
279
|
+
# 아무것도 안 하고 rc=0 으로 끝난다. P1·P3 이 그 rc=0 을 «해소 성공» 으로 읽어 초록이었고,
|
|
280
|
+
# P2 의 빨강도 코드 결함이 아니라 내 하네스 결함이었다. 판별 근거는 **HEAD 에서도 같은
|
|
281
|
+
# 값이 나왔다**는 것이다(변경 전후가 같으면 계기가 대상을 안 보고 있다).
|
|
282
|
+
# [[feedback_instrument_vs_target_and_budget]] — 이상하면 대상보다 계기를 먼저 의심해라.
|
|
283
|
+
_PR="${FH_ADAPTER_DIR:-$REPO_ROOT/scripts/adapters}/peer_resolve.sh"
|
|
284
|
+
if [ ! -f "$_PR" ]; then
|
|
285
|
+
echo " SKIP P 레인 (peer_resolve.sh 없음) — PASS 가 아니라 미검사다"
|
|
286
|
+
else
|
|
287
|
+
# shellcheck source=scripts/adapters/peer_resolve.sh
|
|
288
|
+
. "$_PR"
|
|
289
|
+
if ! type fh_peer_resolve >/dev/null 2>&1; then
|
|
290
|
+
echo " ❌ P 레인 하네스 결함 — source 했는데 fh_peer_resolve 가 없다"
|
|
291
|
+
FAIL=1
|
|
292
|
+
else
|
|
293
|
+
_PW="$(mktemp -d)"
|
|
294
|
+
mkdir -p "$_PW/sp ace" "$_PW/aliased-dev" "$_PW/dual" "$_PW/dual-dev"
|
|
295
|
+
# P0 컨트롤 — 계기가 대상을 실제로 부르는가. 이게 초록이 아니면 아래 셋은 의미가 없다.
|
|
296
|
+
FH_PROJECTS_HOME="$_PW" fh_peer_resolve nosuchpeer >/dev/null 2>&1; _prc=$?
|
|
297
|
+
_lane P0 control "계기 생존 — 없는 peer 는 부재(1)로 온다(rc=0 무음이면 대상을 안 부른 것)" 1 "$_prc"
|
|
298
|
+
# P1 — 공백 이름 1건: 한 후보가 두 단어로 세어져 거짓 AMBIGUOUS(2) 가 나던 자리
|
|
299
|
+
FH_PROJECTS_HOME="$_PW" fh_peer_resolve 'sp ace' >/dev/null 2>&1; _prc=$?
|
|
300
|
+
_lane P1 resolve-guard "공백 이름 1건은 정상 해소 — 거짓 AMBIGUOUS 아님" 0 "$_prc"
|
|
301
|
+
# P2 컨트롤 — 진짜 모호는 여전히 거부해야 한다(P1 의 통과가 모호검출을 죽인 게 아니다)
|
|
302
|
+
FH_PROJECTS_HOME="$_PW" fh_peer_resolve dual >/dev/null 2>&1; _prc=$?
|
|
303
|
+
_lane P2 control "진짜 모호(dual + dual-dev)는 여전히 고르지 않는다" 2 "$_prc"
|
|
304
|
+
# P3 컨트롤 — 별칭 해소가 실제로 살아 있나(P 레인이 통째로 공허하지 않다는 증거)
|
|
305
|
+
FH_PROJECTS_HOME="$_PW" fh_peer_resolve aliased >/dev/null 2>&1; _prc=$?
|
|
306
|
+
_lane P3 resolve-alias "'-dev' 별칭이 해소된다" 0 "$_prc"
|
|
307
|
+
rm -rf "$_PW"
|
|
308
|
+
fi
|
|
309
|
+
fi
|
|
310
|
+
# 🟥 **명시 잔여 — 팔 A 는 안 닫혔다.** `FH_CLUSTER_ROOTS`(임의 절대경로의 basename 조회)는
|
|
311
|
+
# 조회 모델이 달라 라이브러리로 통일하지 않았고, 거기 공백 거짓-AMBIGUOUS 가 **그대로 있다**
|
|
312
|
+
# (실측 확인). 별도 축의 독립 수리이므로 레인을 안 박았다 — 안 잰 것이지 통과한 게 아니다.
|
|
313
|
+
|
|
314
|
+
# 🟥 **2026-08-21 — 사전등록 두 줄을 갱신했다. 이유를 적는 게 갱신 자체보다 중요하다.**
|
|
315
|
+
# (위 규율 그대로: 관측에 맞춰 «조용히» 고치면 프로브의 판별력 증거가 사라진다.)
|
|
316
|
+
# · **R1: `G1 G5 M1 Q1` → `G1 G5 M1 Q1 W1`** — **선재 오류**다. 사전등록이 `qasp_web_rules.sh`
|
|
317
|
+
# (W 레인) 도입 **이전**에 쓰였고, W1 도 부재 분기에 결박된 레인이라 빨간 게 맞다.
|
|
318
|
+
# 내 변경 이전 HEAD 에서도 `--revert-probe` 는 이 차이로 RC=1 이었다(실측). 즉 이건
|
|
319
|
+
# 내가 깬 게 아니라 **이미 틀려 있던 것을 이번에 갚는 것**이다.
|
|
320
|
+
# · **R2: 11개 → `M4 Q4 Q7 Q8`** — 이건 **내 변경이 원인**이고, 기대값을 낮추는 것으로
|
|
321
|
+
# 끝내면 안 된다. `peer_resolve.sh` 의 `FH_PROJECTS_HOME` 팔이 이제 별칭을
|
|
322
|
+
# `fh_track_resolve.sh` 에서 받으므로 **`fh_peer_alias_candidates` 를 안 읽는다** →
|
|
323
|
+
# 그걸 중화해도 안 흔들린다. 남은 넷은 전부 `FH_CLUSTER_ROOTS`(팔 A) 레인이다.
|
|
324
|
+
# 🟥 **명시 잔여 — 커버리지가 줄었다**: R2 는 더 이상 «팔 B 가 별칭을 보는가» 를
|
|
325
|
+
# 흔들지 못한다. 그 축의 앵커는 지금 `scripts/test_track_resolve_lanes.sh`(라이브러리
|
|
326
|
+
# 레인 1~3 + 배선 레인 9)에만 있고, **어댑터 층에는 없다.** 팔 A 는 조회 모델이 달라
|
|
327
|
+
# (임의 절대경로의 basename) 통일하지 않았으므로 R2 는 그대로 유효하다.
|
|
328
|
+
# 이 잔여를 닫으려면 라이브러리를 중화하는 별도 프로브 축(R3)이 필요하고,
|
|
329
|
+
# 지금 프로브는 `scripts/adapters/` 만 사본으로 뜨므로 **그 축은 아직 없다.**
|
|
330
|
+
# · **P 레인 추가분(같은 날, 2차)**: `P0 P1` 이 R1 에, `P1` 이 R2 에 들어간다. 사전등록을
|
|
331
|
+
# P 레인 **추가 전에** 썼기 때문에 빠져 있었다. 기계적 설명: R1 은 부재 분기를 «순진한
|
|
332
|
+
# 추측» 으로 바꾸므로 P0(없는 peer → rc=1 이어야 한다)이 rc=0 을 받아 빨개지고, P1 도
|
|
333
|
+
# 그 조기 반환에 걸린다. R2 는 별칭 후보를 이름 하나로 줄이므로 P1(공백 이름 해소)이
|
|
334
|
+
# 후보 루프를 못 탄다. 🟥 **둘 다 «레인이 그 중화에 실제로 반응한다»는 뜻이라 빨간 게
|
|
335
|
+
# 맞다** — 이 갱신은 기대를 낮추는 게 아니라 **새 앵커를 사전등록에 등록하는 것**이다.
|
|
271
336
|
# ★중화는 **실물 파일이 아니라 사본**에 건다(FH_ADAPTER_DIR). 실물 트리를 건드리지 않고,
|
|
272
337
|
# 사본이 실물과 «중화한 줄 말고는 동일» 함을 적용확인 단계에서 대조한다.
|
|
273
338
|
_revert_probe() {
|
|
@@ -317,14 +382,14 @@ case "${1:-}" in
|
|
|
317
382
|
_revert_probe R1 \
|
|
318
383
|
's|^ printf .peer-resolve: peer| printf "%s" "${FH_PROJECTS_HOME:-$HOME/projects}/$n"; return 0 # NEUTRALIZED_R1\n printf '"'"'peer-resolve: peer|' \
|
|
319
384
|
'NEUTRALIZED_R1' \
|
|
320
|
-
'G1 G5 M1 Q1' \
|
|
385
|
+
'G1 G5 M1 P0 P1 Q1 W1' \
|
|
321
386
|
'«부재» 분기 중화 → 부재 분기에 결박된 레인만 빨개져야 한다' || RP=1
|
|
322
387
|
echo
|
|
323
388
|
# R2: 별칭 후보를 이름 자신 하나로 줄인다 — `-dev` 를 못 찾게 된다.
|
|
324
389
|
_revert_probe R2 \
|
|
325
390
|
's|^ printf .%s\\n. "\$1" "\$1-dev".*$| printf "%s\\n" "$1" # NEUTRALIZED_R2|' \
|
|
326
391
|
'NEUTRALIZED_R2' \
|
|
327
|
-
'
|
|
392
|
+
'M4 P1 Q4 Q7 Q8' \
|
|
328
393
|
'별칭 목록 중화 → mate/qasp 쪽 레인만 빨개져야 한다(gstack 은 별칭이 필요 없다)' || RP=1
|
|
329
394
|
echo
|
|
330
395
|
[ "$RP" -eq 0 ] || FAIL=1
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_heavy_classifier_lanes.sh — known-pair anchor for the pre-commit HEAVY/LIGHT path classifier.
|
|
3
|
+
#
|
|
4
|
+
# WHY: the classifier is three `grep -E` lines, and everything downstream of it — Axes 2+3, the
|
|
5
|
+
# marker, the whole full-gate path — is gated on what they match. A term missing from those lines
|
|
6
|
+
# does not FAIL; it makes the gate **silently not apply**, and the commit goes green. There is no
|
|
7
|
+
# louder symptom. Nothing was watching the watcher: measured 2026-08-20, `scripts/test_*.sh` files
|
|
8
|
+
# testing this classifier = **0**, while 12 test files touch the hook for other reasons.
|
|
9
|
+
#
|
|
10
|
+
# Absorbed from a sibling harness (pmh-dev, 2026-08-08) rather than invented here. The load-bearing
|
|
11
|
+
# idea is the extraction below, not the fixture list.
|
|
12
|
+
#
|
|
13
|
+
# 🟥 FH's classifier is FOUR-WAY, not two-way like the sibling's. The port is not a copy:
|
|
14
|
+
# HEAVY → full gate
|
|
15
|
+
# CARVEOUT → knowledge/ · docs/*.md · AGENTS.md — promoted to HEAVY only if the diff is
|
|
16
|
+
# substantive (adds a fenced block or a citation), else demoted to LIGHT
|
|
17
|
+
# LIGHT → CATALOG.md · tracks/
|
|
18
|
+
# uncovered → matches none of the three. **This class is real and is pinned below** — a path
|
|
19
|
+
# nobody classified is the quietest way for a surface to leave the gate.
|
|
20
|
+
#
|
|
21
|
+
# This lane pins all four directions. Over-matching is a defect too: a gate that fires on every
|
|
22
|
+
# commit is the one people learn to bypass with `--no-verify`, which disarms the Destructive-Op
|
|
23
|
+
# gate living in the same hook.
|
|
24
|
+
#
|
|
25
|
+
# Usage: bash scripts/test_heavy_classifier_lanes.sh → exit 0 if all pairs hold, 1 otherwise.
|
|
26
|
+
set -uo pipefail
|
|
27
|
+
|
|
28
|
+
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
29
|
+
HOOK="$REPO_ROOT/templates/.git-hooks/pre-commit"
|
|
30
|
+
[ -r "$HOOK" ] || { echo "❌ FAIL — $HOOK not readable"; exit 1; }
|
|
31
|
+
|
|
32
|
+
FAILED=0
|
|
33
|
+
|
|
34
|
+
# Extract the LIVE regexes from the hook rather than restating them. A lane that hard-codes its own
|
|
35
|
+
# copy of the pattern passes forever while the real classifier drifts away underneath it — that is
|
|
36
|
+
# testing the fixture instead of the subject, and it is the failure this whole file exists to catch.
|
|
37
|
+
_extract() { # $1 = variable name as it appears at column 0 in the hook
|
|
38
|
+
sed -n "/^$1=\\\$(echo \"\\\$STAGED\"/,/|| true)/p" "$HOOK" \
|
|
39
|
+
| grep -o 'grep -E "[^"]*"' | head -1 | sed 's/^grep -E "//; s/"$//'
|
|
40
|
+
}
|
|
41
|
+
HEAVY_RE=$(_extract HEAVY)
|
|
42
|
+
CARVE_RE=$(_extract CARVEOUT)
|
|
43
|
+
LIGHT_RE=$(_extract LIGHT)
|
|
44
|
+
|
|
45
|
+
# 🟥 Fail CLOSED on extraction failure. An empty regex makes `grep -qE ""` match EVERYTHING, so a
|
|
46
|
+
# silent extraction failure would turn this lane green while classifying every path as heavy.
|
|
47
|
+
for v in HEAVY_RE CARVE_RE LIGHT_RE; do
|
|
48
|
+
if [ -z "${!v}" ]; then
|
|
49
|
+
echo "❌ FAIL — could not extract \$$v from the hook."
|
|
50
|
+
echo " The hook's shape changed. Fix THIS extraction — do not delete the lane, and do not"
|
|
51
|
+
echo " inline a copy of the pattern here (that is how a lane stops covering its subject)."
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
classify() { # <path> → heavy | carveout | light | uncovered
|
|
57
|
+
# Order mirrors the hook: HEAVY wins, then CARVEOUT (which the hook excludes
|
|
58
|
+
# knowledge/shared/rules/ from, since that is already HEAVY), then LIGHT.
|
|
59
|
+
if printf '%s\n' "$1" | grep -qE "$HEAVY_RE"; then echo heavy
|
|
60
|
+
elif printf '%s\n' "$1" | grep -qE "$CARVE_RE"; then echo carveout
|
|
61
|
+
elif printf '%s\n' "$1" | grep -qE "$LIGHT_RE"; then echo light
|
|
62
|
+
else echo uncovered; fi
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
check() { # <expect> <path> <why>
|
|
66
|
+
local expect="$1" path="$2" why="$3" got
|
|
67
|
+
got=$(classify "$path")
|
|
68
|
+
if [ "$got" = "$expect" ]; then
|
|
69
|
+
printf ' ✅ %-9s %-52s %s\n' "$expect" "$path" "$why"
|
|
70
|
+
else
|
|
71
|
+
printf ' ❌ %-9s %-52s expected %s, got %s\n' "$expect" "$path" "$expect" "$got"
|
|
72
|
+
FAILED=1
|
|
73
|
+
fi
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
echo "[heavy-classifier] known-pair anchor (4-way)"
|
|
77
|
+
echo " ── must be HEAVY (full 4-axis gate applies) ──"
|
|
78
|
+
check heavy "plugins/fh-meta/skills/steel-quench/SKILL.md" "skill spec"
|
|
79
|
+
check heavy "plugins/fh-meta/skills/goal-quench/SKILL_detail.md" "detail file — the salience-split half"
|
|
80
|
+
check heavy "plugins/fh-meta/skills/fh/assets/note.md" "any md under a skill dir"
|
|
81
|
+
check heavy "plugins/fh-meta/agents/challenger.md" "agent definition"
|
|
82
|
+
check heavy ".claude/agents/local.md" "repo-local agent definition"
|
|
83
|
+
check heavy ".claude/rules/fh_4axis_gate.md" "gate rule"
|
|
84
|
+
check heavy "knowledge/shared/rules/operations.md" "shared rules — HEAVY, not carveout"
|
|
85
|
+
check heavy "templates/.git-hooks/pre-commit" "the gate machinery itself"
|
|
86
|
+
check heavy "templates/CLAUDE.md" "propagated field asset"
|
|
87
|
+
check heavy "CLAUDE.md" "the resident layer"
|
|
88
|
+
check heavy "scripts/selfcheck.sh" "shipped executable"
|
|
89
|
+
check heavy "scripts/adapters/qasp_web_rules.sh" "nested script still matches"
|
|
90
|
+
|
|
91
|
+
echo " ── CARVEOUT (heavy only if the diff is substantive) ──"
|
|
92
|
+
check carveout "knowledge/shared/harness-core/sonnet_floor_doctrine.md" "doctrine prose"
|
|
93
|
+
check carveout "docs/USER_GUIDE.md" "published guide"
|
|
94
|
+
check carveout "AGENTS.md" "Codex entry point"
|
|
95
|
+
|
|
96
|
+
echo " ── must stay LIGHT (over-firing trains --no-verify) ──"
|
|
97
|
+
check light "CATALOG.md" "index entry"
|
|
98
|
+
check light "tracks/_meta/fh_completed.md" "session record"
|
|
99
|
+
check light "tracks/qasp/NEXT_ACTION.md" "field track record"
|
|
100
|
+
|
|
101
|
+
echo " ── honestly UNCOVERED — pinned so the residual cannot be believed closed ──"
|
|
102
|
+
check uncovered "README.md" "🟥 a README-only commit takes NO 4-axis path"
|
|
103
|
+
check uncovered "package.json" "files[] / version changes are unclassified"
|
|
104
|
+
check uncovered ".github/workflows/validate.yml" "🟥 CI definition — changing what CI runs is unclassified"
|
|
105
|
+
check uncovered "scripts/memory_link_check.py" "🟥 .py is not .sh — shipped python is unclassified"
|
|
106
|
+
|
|
107
|
+
echo " ── controls (the classifier discriminates, it does not just fire) ──"
|
|
108
|
+
# known-negative: a path that resembles a HEAVY one but must not match. This is the arm that
|
|
109
|
+
# separates "the classifier works" from "the classifier fires on everything" — without it, a
|
|
110
|
+
# regex of `.` would pass every HEAVY row above.
|
|
111
|
+
check uncovered "notes/scripts/foo.sh.txt" "control — .sh.txt is not a shell script (\$-anchored)"
|
|
112
|
+
check uncovered "notes/skills-overview.md" "control — 'skills' in a path is not a skill spec"
|
|
113
|
+
# ⚠️ this control was first written as `docs/design/skills-overview.md`, which the lane
|
|
114
|
+
# correctly classified `carveout` (`^docs/.*\.md$`). The FIXTURE was wrong, not the
|
|
115
|
+
# subject — pick a path outside every class when the claim is "matches none of them".
|
|
116
|
+
|
|
117
|
+
# 🟥 KNOWN OVER-MATCH, pinned deliberately as `heavy` rather than "fixed" (2026-08-20).
|
|
118
|
+
# `SKILL(_detail)?\.md` is written WITHOUT a `$` anchor, so any path merely CONTAINING that
|
|
119
|
+
# substring classifies HEAVY — `SKILL.md.bak`, `SKILL.md.orig`, `SKILL.md~`. Recorded, not
|
|
120
|
+
# repaired, for three reasons, and the reasons are the point:
|
|
121
|
+
# ⓐ the error direction is fail-SAFE — it adds gate coverage, never removes it. The classifier's
|
|
122
|
+
# dangerous direction is under-matching (the gate silently not applying), which this is not.
|
|
123
|
+
# ⓑ those paths are not committed in practice, so the over-block the hook warns about
|
|
124
|
+
# ("a gate that always fires is the one people learn to bypass") does not actually fire here.
|
|
125
|
+
# ⓒ anchoring it is a live change to gate routing, and gate routing is exactly what this lane
|
|
126
|
+
# exists to detect changes in. Changing the subject in the same commit that first measures it
|
|
127
|
+
# leaves no before-state.
|
|
128
|
+
# This row is therefore a RECORD, not an endorsement: if someone anchors the pattern later, this
|
|
129
|
+
# line goes red and they will read this comment — which is the whole job of a known-pair.
|
|
130
|
+
check heavy "docs/skills/SKILL.md.bak" "🟥 over-match: pattern is unanchored (see comment)"
|
|
131
|
+
|
|
132
|
+
echo
|
|
133
|
+
echo "🟥 NOT COVERED BY THIS LANE (named, not silently absent):"
|
|
134
|
+
echo " · \`diff_is_substantive\` — whether a CARVEOUT file is promoted depends on diff CONTENT,"
|
|
135
|
+
echo " not path. This lane pins the PATH class only. That test needs its own anchor."
|
|
136
|
+
echo " · the hook's behaviour once classified (marker required, manifest required). Pinned"
|
|
137
|
+
echo " elsewhere (test_marker_*_lanes.sh); this lane covers only the routing decision."
|
|
138
|
+
|
|
139
|
+
if [ "$FAILED" -eq 0 ]; then
|
|
140
|
+
echo "✅ heavy-classifier: all pairs hold"
|
|
141
|
+
exit 0
|
|
142
|
+
fi
|
|
143
|
+
echo "❌ heavy-classifier: at least one pair broke — the gate's routing changed"
|
|
144
|
+
exit 1
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_listing_watch_lanes.sh — listing_watch.sh 의 레인.
|
|
3
|
+
# 🟥 네트워크를 안 쓴다: PATH 앞에 `gh` 스텁을 놓아 응답을 고정한다.
|
|
4
|
+
# (실망 사용 검증은 `--selftest` 가 known-pair 로 따로 한다 — 그건 네트워크가 필요하다)
|
|
5
|
+
HUB="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
6
|
+
SUT="$HUB/scripts/listing_watch.sh"
|
|
7
|
+
PASS=0; FAIL=0
|
|
8
|
+
ok(){ PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; }
|
|
9
|
+
ng(){ FAIL=$((FAIL+1)); printf ' ❌ %s\n 기대: %s\n 실제: %s\n' "$1" "$2" "$3"; }
|
|
10
|
+
|
|
11
|
+
SB=$(mktemp -d); trap 'rm -rf "$SB"' EXIT
|
|
12
|
+
mkdir -p "$SB/tracks/_meta" "$SB/bin"
|
|
13
|
+
|
|
14
|
+
mkstub(){ # $1 = state|merged|badexit
|
|
15
|
+
cat > "$SB/bin/gh" <<STUB
|
|
16
|
+
#!/usr/bin/env bash
|
|
17
|
+
case "\$*" in
|
|
18
|
+
*"contents/README.md"*) printf '%s' "\$(printf 'chrono-meta/forge-harness' | base64)" ;;
|
|
19
|
+
# 🟥 본체 jq 는 4필드다: state|merged|라벨개수|라벨목록. 스텁이 3필드면 계기가 대상과 어긋난다.
|
|
20
|
+
*"issues/33"*) [ "$1" = "merged" ] && printf 'open|2026-08-19T06:48:10Z|0|' || printf 'open|null|1|validation-passed' ;;
|
|
21
|
+
*"issues/999"*) exit 1 ;;
|
|
22
|
+
*) exit 1 ;;
|
|
23
|
+
esac
|
|
24
|
+
STUB
|
|
25
|
+
chmod +x "$SB/bin/gh"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
run(){ CLAUDE_PROJECT_DIR="$SB" PATH="$SB/bin:$PATH" bash "$SUT" "$@" 2>&1; }
|
|
29
|
+
|
|
30
|
+
# ── L1: gh 부재 → SKIPPED (pass 아님) ─────────────────────────────
|
|
31
|
+
printf 'walkinglabs/x\t33\tpr\tchrono-meta/forge-harness\n' > "$SB/tracks/_meta/listing_watch_channels.tsv"
|
|
32
|
+
# 🟥 두 번 틀린 자리다. ① 초판: `PATH=/nonexistent` 가 `bash` 까지 지워 «계기» 를 시험했다.
|
|
33
|
+
# ② 2판: `/bin:/usr/bin` 은 **호스트 의존**이다 — 리눅스에선 거기 진짜 gh 가 있을 수 있고
|
|
34
|
+
# 그러면 이 레인이 «대상» 이 아니라 «그 머신» 을 재게 된다(cross-family 지목).
|
|
35
|
+
# ③ 정본: 본체에 주입 seam(`LISTING_WATCH_GH`)을 두고 존재하지 않는 이름을 넣는다.
|
|
36
|
+
o=$(CLAUDE_PROJECT_DIR="$SB" LISTING_WATCH_GH="gh-does-not-exist-$$" bash "$SUT" --force 2>&1); r=$?
|
|
37
|
+
case "$o" in *SKIPPED*) ok "L1 gh 부재 → SKIPPED (not passed)" ;; *) ng "L1" "SKIPPED" "$o" ;; esac
|
|
38
|
+
[ "$r" = "0" ] && ok "L1b gh 부재여도 exit 0 (훅 무음 방지)" || ng "L1b" "0" "$r"
|
|
39
|
+
|
|
40
|
+
# ── L2: 매니페스트 부재 → DISABLED (실패 아님) ────────────────────
|
|
41
|
+
mkstub state
|
|
42
|
+
mv "$SB/tracks/_meta/listing_watch_channels.tsv" "$SB/m.bak"
|
|
43
|
+
o=$(run --force); case "$o" in *DISABLED*) ok "L2 매니페스트 부재 → DISABLED" ;; *) ng "L2" "DISABLED" "$o" ;; esac
|
|
44
|
+
mv "$SB/m.bak" "$SB/tracks/_meta/listing_watch_channels.tsv"
|
|
45
|
+
|
|
46
|
+
# ── L3: 주석만 있는 매니페스트 → DISABLED (0행을 «정상»으로 안 읽는다) ──
|
|
47
|
+
cp "$SB/tracks/_meta/listing_watch_channels.tsv" "$SB/m.bak"
|
|
48
|
+
printf '# only a comment\n' > "$SB/tracks/_meta/listing_watch_channels.tsv"
|
|
49
|
+
o=$(run --force); case "$o" in *DISABLED*) ok "L3 유효행 0 → DISABLED" ;; *) ng "L3" "DISABLED" "$o" ;; esac
|
|
50
|
+
cp "$SB/m.bak" "$SB/tracks/_meta/listing_watch_channels.tsv"
|
|
51
|
+
|
|
52
|
+
# ── L4: 최초 실행 → 스냅샷 생성 + 변화 «없음» ─────────────────────
|
|
53
|
+
rm -f "$SB/tracks/_meta/.listing_watch_state.tsv"
|
|
54
|
+
o=$(run --force)
|
|
55
|
+
case "$o" in *최초*) ok "L4 최초 실행을 «변화» 로 오보하지 않는다" ;; *) ng "L4" "최초 실행 안내" "$o" ;; esac
|
|
56
|
+
[ -s "$SB/tracks/_meta/.listing_watch_state.tsv" ] && ok "L4b 스냅샷 파일 생성" || ng "L4b" "비어있지 않은 스냅샷" "(없음)"
|
|
57
|
+
|
|
58
|
+
# ── L5: 무변화 → 무출력 (오탐 0) ──────────────────────────────────
|
|
59
|
+
o=$(run --force)
|
|
60
|
+
[ -z "$o" ] && ok "L5 무변화 → 무출력" || ng "L5" "(빈 출력)" "$o"
|
|
61
|
+
|
|
62
|
+
# ── L6: 상태 전이 → 검출 ★ 이 레인이 본체다 ──────────────────────
|
|
63
|
+
mkstub merged
|
|
64
|
+
o=$(run --force)
|
|
65
|
+
# 🟥 배너 문구만 보면 «라벨만 바뀐 버그» 도 통과한다 — **관측된 전이 자체**를 단언한다.
|
|
66
|
+
case "$o" in *"바뀌었다"*) ok "L6 변화 배너 발화" ;; *) ng "L6" "변화 보고" "$o" ;; esac
|
|
67
|
+
case "$o" in *MERGED*) ok "L6b 전이 «내용»이 MERGED 로 찍힌다" ;; *) ng "L6b" "지금: … MERGED" "$o" ;; esac
|
|
68
|
+
case "$o" in *"이전:"*) ok "L6c 이전 상태도 같이 보여준다" ;; *) ng "L6c" "이전: …" "$o" ;; esac
|
|
69
|
+
|
|
70
|
+
# ── L7: 스로틀 — 24h 안이면 조용 ──────────────────────────────────
|
|
71
|
+
o=$(run) # --force 없음
|
|
72
|
+
[ -z "$o" ] && ok "L7 스로틀 동작 (24h 내 재실행 조용)" || ng "L7" "(빈 출력)" "$o"
|
|
73
|
+
|
|
74
|
+
# ── L8: API 실패를 «없음» 이 아니라 FAILED 로 ─────────────────────
|
|
75
|
+
printf 'walkinglabs/x\t999\tpr\t-\n' > "$SB/tracks/_meta/listing_watch_channels.tsv"
|
|
76
|
+
rm -f "$SB/tracks/_meta/.listing_watch_state.tsv"
|
|
77
|
+
run --force >/dev/null
|
|
78
|
+
s=$(cat "$SB/tracks/_meta/.listing_watch_state.tsv" 2>/dev/null)
|
|
79
|
+
case "$s" in *FAILED*) ok "L8 API 실패 → FAILED (0 으로 안 접는다)" ;; *) ng "L8" "FAILED" "$s" ;; esac
|
|
80
|
+
|
|
81
|
+
# ── L9: 🟥 FAILED 반복이 «무출력» 이 되면 안 된다 (자기 헤더 배반 방지) ──
|
|
82
|
+
cat > "$SB/bin/gh" <<'STUB'
|
|
83
|
+
#!/usr/bin/env bash
|
|
84
|
+
exit 1
|
|
85
|
+
STUB
|
|
86
|
+
chmod +x "$SB/bin/gh"
|
|
87
|
+
printf 'r/x\t1\tpr\t-\n' > "$SB/tracks/_meta/listing_watch_channels.tsv"
|
|
88
|
+
rm -f "$SB/tracks/_meta/.listing_watch_state.tsv"
|
|
89
|
+
run --force >/dev/null 2>&1 # 1회차: 스냅샷이 FAILED 로 굳는다
|
|
90
|
+
o=$(run --force) # 2회차: 변화는 «없다» — 그래도 말해야 한다
|
|
91
|
+
case "$o" in *FAILED*|*"읽지 못했다"*) ok "L9 FAILED 반복도 매번 보고 (인증만료≠변화없음)" ;; *) ng "L9" "FAILED 보고" "${o:-(무출력)}" ;; esac
|
|
92
|
+
|
|
93
|
+
# ── L10: 연속 탭(빈 필드)에서 needle 이 밀리지 않는다 ──
|
|
94
|
+
mkstub state
|
|
95
|
+
printf 'walkinglabs/x\t33\t\tchrono-meta/forge-harness\n' > "$SB/tracks/_meta/listing_watch_channels.tsv"
|
|
96
|
+
rm -f "$SB/tracks/_meta/.listing_watch_state.tsv"
|
|
97
|
+
run --force >/dev/null 2>&1
|
|
98
|
+
s9=$(cat "$SB/tracks/_meta/.listing_watch_state.tsv" 2>/dev/null)
|
|
99
|
+
case "$s9" in *LISTED*) ok "L10 빈 필드 있어도 needle 이 제자리 (IFS 탭 축약 방지)" ;; *) ng "L10" "LISTED" "$s9" ;; esac
|
|
100
|
+
|
|
101
|
+
# ── L11: 개행 없는 마지막 행도 읽는다 ──
|
|
102
|
+
printf 'walkinglabs/x\t33\tpr\tchrono-meta/forge-harness\nwalkinglabs/y\t33\tpr\tchrono-meta/forge-harness' > "$SB/tracks/_meta/listing_watch_channels.tsv"
|
|
103
|
+
rm -f "$SB/tracks/_meta/.listing_watch_state.tsv"
|
|
104
|
+
run --force >/dev/null 2>&1
|
|
105
|
+
n=$(grep -c . "$SB/tracks/_meta/.listing_watch_state.tsv" 2>/dev/null || echo 0)
|
|
106
|
+
[ "$n" = "2" ] && ok "L11 개행 없는 마지막 행 누락 안 함 (2/2)" || ng "L11" "2행" "${n}행"
|
|
107
|
+
|
|
108
|
+
# ── L12: 라벨 개수를 실어 ["a,b"] 와 ["a","b"] 를 가른다 ──
|
|
109
|
+
case "$s9" in *" "[0-9]*:*) ok "L12 라벨 필드가 «개수:목록» 형태" ;; *) ng "L12" "N:labels" "$s9" ;; esac
|
|
110
|
+
|
|
111
|
+
# ── L13/L14: 🟥 **반대 플랫폼 흉내** — 이 둘이 이 스위트의 하중선이다.
|
|
112
|
+
# 2026-08-21 실측: 로컬(macOS) 16/16 초록인데 **CI(리눅스)에서 L7·L9 가 죽었다.**
|
|
113
|
+
# 원인 둘 다 «한 플랫폼에서만 나는» 형태였다:
|
|
114
|
+
# ⓐ `stat -f %m` 은 GNU 에서 **실패가 아니라 성공하며 파일시스템 정보를 뱉는다**
|
|
115
|
+
# → `||` 폴백이 안 타고 산술식이 깨진다
|
|
116
|
+
# ⓑ grep 패턴의 `\t` 를 GNU grep 은 **리터럴 t** 로 읽는다 → FAILED 보고가 0건
|
|
117
|
+
# 로컬 초록이 증거가 못 된다는 뜻이므로, **여기서 반대 플랫폼을 스텁으로 만든다.**
|
|
118
|
+
mkstub state
|
|
119
|
+
printf 'walkinglabs/x\t33\tpr\tchrono-meta/forge-harness\n' > "$SB/tracks/_meta/listing_watch_channels.tsv"
|
|
120
|
+
rm -f "$SB/tracks/_meta/.listing_watch_state.tsv"
|
|
121
|
+
|
|
122
|
+
# L13: `stat -f` 가 «성공하면서 쓰레기» 를 내는 플랫폼(=GNU)에서도 스로틀이 산다
|
|
123
|
+
cat > "$SB/bin/stat" <<'STUB'
|
|
124
|
+
#!/usr/bin/env bash
|
|
125
|
+
# GNU 흉내: -f 는 성공하며 파일시스템 정보(비숫자)를 낸다 · -c %Y 만 mtime
|
|
126
|
+
if [ "$1" = "-f" ]; then echo " ID: deadbeef Namelen: 255 Type: ext2/ext3"; exit 0; fi
|
|
127
|
+
if [ "$1" = "-c" ]; then echo 1000000000; exit 0; fi
|
|
128
|
+
exit 1
|
|
129
|
+
STUB
|
|
130
|
+
chmod +x "$SB/bin/stat"
|
|
131
|
+
run --force >/dev/null 2>&1 # 스냅샷 생성
|
|
132
|
+
o=$(run 2>&1) # --force 없음 → 스로틀이 조용해야
|
|
133
|
+
case "$o" in
|
|
134
|
+
*"syntax error"*|*"ext2"*) ng "L13 GNU-stat 흉내에서 스로틀 붕괴" "(빈 출력)" "$o" ;;
|
|
135
|
+
"") ok "L13 GNU-stat 흉내에서도 스로틀 정상 (비숫자 mtime 방어)" ;;
|
|
136
|
+
*) ng "L13" "(빈 출력)" "$o" ;;
|
|
137
|
+
esac
|
|
138
|
+
rm -f "$SB/bin/stat"
|
|
139
|
+
|
|
140
|
+
# L14: grep 패턴의 `\t` 를 리터럴 t 로 읽는 플랫폼(=GNU grep)에서도 FAILED 가 보고된다
|
|
141
|
+
# → 실제 탭을 쓰는지 확인. 스크립트가 `\t` 를 패턴에 쓰면 여기서 죽는다.
|
|
142
|
+
cat > "$SB/bin/gh" <<'STUB'
|
|
143
|
+
#!/usr/bin/env bash
|
|
144
|
+
exit 1
|
|
145
|
+
STUB
|
|
146
|
+
chmod +x "$SB/bin/gh"
|
|
147
|
+
cat > "$SB/bin/grep" <<'STUB'
|
|
148
|
+
#!/usr/bin/env bash
|
|
149
|
+
# GNU 흉내: 패턴 안의 백슬래시-t 는 «리터럴 t» 다 (탭이 아니다)
|
|
150
|
+
args=(); for a in "$@"; do case "$a" in *'\t'*) a="${a//\\t/t}" ;; esac; args+=("$a"); done
|
|
151
|
+
exec /usr/bin/grep "${args[@]}"
|
|
152
|
+
STUB
|
|
153
|
+
chmod +x "$SB/bin/grep"
|
|
154
|
+
printf 'r/x\t1\tpr\t-\n' > "$SB/tracks/_meta/listing_watch_channels.tsv"
|
|
155
|
+
rm -f "$SB/tracks/_meta/.listing_watch_state.tsv"
|
|
156
|
+
run --force >/dev/null 2>&1
|
|
157
|
+
o=$(run --force 2>&1)
|
|
158
|
+
case "$o" in *"읽지 못했다"*|*FAILED*) ok "L14 GNU-grep 흉내에서도 FAILED 보고 (실제 탭 사용)" ;; *) ng "L14 grep \\t 이식성" "FAILED 보고" "${o:-(무출력)}" ;; esac
|
|
159
|
+
rm -f "$SB/bin/grep"
|
|
160
|
+
|
|
161
|
+
printf '\nLISTING_WATCH_LANES: %d passed, %d failed\n' "$PASS" "$FAIL"
|
|
162
|
+
[ "$FAIL" -eq 0 ] || exit 1
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_marker_defense_lanes.sh — regression fixtures for pre-commit's validate_defense_leg
|
|
3
|
+
# (Wave 1-D defense line, 2026-08-20).
|
|
4
|
+
#
|
|
5
|
+
# WHY THIS FILE EXISTS AT ALL: the procedure was absorbed from a sibling harness whose own document
|
|
6
|
+
# asserted "the commit hook reads that line" and "pinned by scripts/test_marker_floor_lanes.sh".
|
|
7
|
+
# 🟥 Measured twice, with a control (`crossfamily` → 21 hits in the same file): BOTH claims were
|
|
8
|
+
# **0 hits** there. The prose was portable; the machine was not. This lane is the machine, built
|
|
9
|
+
# here, so that FH's own claim is true when FH makes it.
|
|
10
|
+
#
|
|
11
|
+
# Fixtures assert BOTH directions (known-pair): every intended shape is admitted, and every hole the
|
|
12
|
+
# leg closes still blocks. A suite that only asserts BLOCK cannot tell "blocked" from "blocked for
|
|
13
|
+
# the wrong reason"; one that only asserts PASS cannot see a leg that admits everything.
|
|
14
|
+
#
|
|
15
|
+
# Usage: bash scripts/test_marker_defense_lanes.sh Exit: 0 = all behave; 1 = regression.
|
|
16
|
+
|
|
17
|
+
set -uo pipefail
|
|
18
|
+
# Script-relative, NOT `git rev-parse --show-toplevel` — same reason as the sibling lanes: in a
|
|
19
|
+
# vendored tree rev-parse answers with the OUTER repo and the suite measures somebody else's files.
|
|
20
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
21
|
+
HOOK="$REPO_ROOT/templates/.git-hooks/pre-commit"
|
|
22
|
+
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
|
23
|
+
|
|
24
|
+
sed -n '/^validate_defense_leg()/,/^}/p' "$HOOK" > "$T/fn.sh"
|
|
25
|
+
|
|
26
|
+
# Instrument calibration BEFORE any fixture runs. An empty extraction would let every BLOCK
|
|
27
|
+
# fixture "pass" against nothing and every PASS fixture fail — and the suite would still print a
|
|
28
|
+
# verdict. Assert the body actually arrived.
|
|
29
|
+
if ! grep -q 'estimation-layer' "$T/fn.sh"; then
|
|
30
|
+
echo "❌ HARNESS-ERROR — validate_defense_leg did not extract from $HOOK."
|
|
31
|
+
echo " Fixtures below would measure an empty function. Aborting rather than reporting green."
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
pass=0; fail=0
|
|
36
|
+
mk() { printf '%s\n' "$2" > "$T/$1"; }
|
|
37
|
+
run() { bash -c "source '$T/fn.sh'; validate_defense_leg '$T/$1'" >/dev/null 2>&1; }
|
|
38
|
+
want() { # $1=fixture $2=expect(pass|block) $3=label
|
|
39
|
+
local rc; run "$1"; rc=$?
|
|
40
|
+
local got; [ "$rc" -eq 0 ] && got=pass || got=block
|
|
41
|
+
if [ "$got" = "$2" ]; then printf ' ✅ %-6s %s\n' "$2" "$3"; pass=$((pass+1))
|
|
42
|
+
else printf ' ❌ %-6s %s — got %s\n' "$2" "$3" "$got"; fail=$((fail+1)); fi
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
GOOD='axis2-defense: reproducibility=bash scripts/test_marker_defense_lanes.sh at 4c1f2ab fairness=both arms reps=3, same fixture dir, same shell estimation-layer=all measured; known-pair separated arm from control'
|
|
46
|
+
|
|
47
|
+
echo "[marker-defense] known-pair fixtures"
|
|
48
|
+
echo " ── admitted (must not over-block; over-blocking teaches --no-verify) ──"
|
|
49
|
+
mk ok.md "floor-status: sonnet-floor
|
|
50
|
+
$GOOD"
|
|
51
|
+
want ok.md pass "all three substantive"
|
|
52
|
+
mk multiline.md "floor-status: sonnet-floor
|
|
53
|
+
axis2-defense: reproducibility=git show 9cd7350 -- scripts/selfcheck.sh
|
|
54
|
+
fairness=arm and control both on 2.6.0 tarball, reps=1 each, named
|
|
55
|
+
estimation-layer=counts measured; the 2,367 b/day figure is a quotation"
|
|
56
|
+
want multiline.md pass "continuation lines are one value"
|
|
57
|
+
mk order.md "floor-status: below-floor
|
|
58
|
+
axis2-defense: estimation-layer=measured via npm pack --dry-run --json fairness=same tarball both arms reproducibility=npm pack @chrono-meta/fh-gate@2.6.0"
|
|
59
|
+
want order.md pass "sub-answers in any order"
|
|
60
|
+
|
|
61
|
+
echo " ── blocked ──"
|
|
62
|
+
mk none.md "floor-status: sonnet-floor"
|
|
63
|
+
want none.md block "no axis2-defense line at all"
|
|
64
|
+
mk empty.md "floor-status: sonnet-floor
|
|
65
|
+
axis2-defense:"
|
|
66
|
+
want empty.md block "key present, value empty (silence is not an answer)"
|
|
67
|
+
mk partial.md "floor-status: sonnet-floor
|
|
68
|
+
axis2-defense: reproducibility=bash scripts/selfcheck.sh completes rc=0 fairness=both arms on same tree"
|
|
69
|
+
want partial.md block "estimation-layer missing — a partial form is not a defense"
|
|
70
|
+
# 🟥 rc IS NOT ENOUGH HERE — measured by a revert probe on 2026-08-20, the day this was written.
|
|
71
|
+
# Deleting the completeness guard leaves `partial.md` STILL blocked, because an absent sub-answer
|
|
72
|
+
# also reads as an empty (vacuous) one and the next guard catches it. Gating on the exit code alone
|
|
73
|
+
# therefore certifies a guard that is doing nothing: the classic decorative anchor. What the
|
|
74
|
+
# completeness guard actually buys is the PRESCRIPTION — "missing sub-answer(s): estimation-layer"
|
|
75
|
+
# tells the author which question they skipped, where "too short" sends them to rewrite the wrong
|
|
76
|
+
# field. A gate's output is half its product, so the lane measures the output.
|
|
77
|
+
want_msg() { # $1=fixture $2=substring $3=label
|
|
78
|
+
local out; out=$(bash -c "source '$T/fn.sh'; validate_defense_leg '$T/$1'" 2>&1)
|
|
79
|
+
if printf '%s' "$out" | grep -qF "$2"; then printf ' ✅ %-6s %s\n' "msg" "$3"; pass=$((pass+1))
|
|
80
|
+
else printf ' ❌ %-6s %s — output did not contain %s\n' "msg" "$3" "$2"; fail=$((fail+1)); fi
|
|
81
|
+
}
|
|
82
|
+
want_msg partial.md "missing sub-answer(s): estimation-layer" \
|
|
83
|
+
"prescription NAMES the skipped question (guard is load-bearing for the message)"
|
|
84
|
+
# (the short.md prescription control lives below, AFTER that fixture is created —
|
|
85
|
+
# 🟥 it was first written here, before `mk short.md`, so it measured a NON-EXISTENT file and
|
|
86
|
+
# read the "no line at all" message. The lane was wrong, not the subject. Fixture order is
|
|
87
|
+
# part of the instrument.)
|
|
88
|
+
mk vacuous.md "floor-status: sonnet-floor
|
|
89
|
+
axis2-defense: reproducibility=ok fairness=yes estimation-layer=n/a"
|
|
90
|
+
want vacuous.md block "filled form, not an answer"
|
|
91
|
+
mk short.md "floor-status: sonnet-floor
|
|
92
|
+
axis2-defense: reproducibility=ran it fairness=same estimation-layer=nums"
|
|
93
|
+
want short.md block "too short to name a command, reps or layer"
|
|
94
|
+
want_msg short.md "vacuous answer(s)" \
|
|
95
|
+
"control — the short case takes the OTHER prescription (the two are distinguishable)"
|
|
96
|
+
mk dup.md "floor-status: sonnet-floor
|
|
97
|
+
$GOOD
|
|
98
|
+
$GOOD"
|
|
99
|
+
want dup.md block "two lines — the reader takes the first, so a correction is shadowed"
|
|
100
|
+
|
|
101
|
+
echo " ── call-site pin (a function nobody calls is prose) ──"
|
|
102
|
+
# 🟥 Found by cross-family review 2026-08-20 (codex/gpt-5.5), self-detection 0: this suite extracts
|
|
103
|
+
# the FUNCTION and calls it directly, so deleting the hook's call site left every lane green while
|
|
104
|
+
# the leg stopped running — and the skill doc claimed "pre-commit runs this". That is the exact
|
|
105
|
+
# defect class this whole file was built to stop importing, reproduced one layer up.
|
|
106
|
+
# The pin is a grep, and its limit is stated rather than hidden: it proves the call EXISTS and sits
|
|
107
|
+
# in the full-gate marker block, not that the block is reached. Reachability is pinned by the hook's
|
|
108
|
+
# own end-to-end lanes, not here.
|
|
109
|
+
cs=$(grep -c 'validate_defense_leg "\$MARKER"' "$HOOK")
|
|
110
|
+
if [ "$cs" -eq 1 ]; then
|
|
111
|
+
printf ' ✅ %-6s %s\n' "call" "hook calls validate_defense_leg exactly once"; pass=$((pass+1))
|
|
112
|
+
else
|
|
113
|
+
printf ' ❌ %-6s %s — found %s call site(s)\n' "call" "hook calls validate_defense_leg exactly once" "$cs"; fail=$((fail+1))
|
|
114
|
+
fi
|
|
115
|
+
# and that the call is gated on the floor tiers the doc claims — not on load-bearing, which is
|
|
116
|
+
# where it was first (wrongly) wired.
|
|
117
|
+
if grep -B4 'validate_defense_leg "\$MARKER"' "$HOOK" | grep -qE 'floor-status:.*sonnet-floor\|below-floor'; then
|
|
118
|
+
printf ' ✅ %-6s %s\n' "call" "call is gated on floor-status (sonnet-floor|below-floor)"; pass=$((pass+1))
|
|
119
|
+
else
|
|
120
|
+
printf ' ❌ %-6s %s\n' "call" "call is NOT gated on floor-status — scope drifted from the doc"; fail=$((fail+1))
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
echo " ── prescriptions (rc alone certifies guards that do nothing) ──"
|
|
124
|
+
# 🟥 Same shadowing as the completeness guard: removing the presence guard leaves none.md/empty.md
|
|
125
|
+
# still blocked by the missing-subanswer guard, so rc cannot see it. What presence buys is the
|
|
126
|
+
# prescription that shows the author the whole field shape.
|
|
127
|
+
want_msg none.md "no 'axis2-defense:' line" \
|
|
128
|
+
"presence guard is load-bearing for its own message"
|
|
129
|
+
want_msg empty.md "no 'axis2-defense:' line" \
|
|
130
|
+
"empty value takes the same prescription as absent (silence is silence)"
|
|
131
|
+
|
|
132
|
+
echo " ── controls (the leg discriminates; it does not just fire) ──"
|
|
133
|
+
# 🟥 known-NEGATIVE for over-blocking: a legitimate answer that merely LOOKS terse per-field but
|
|
134
|
+
# clears the bar. Without this, a leg that blocked everything would pass every BLOCK row above.
|
|
135
|
+
mk borderline.md "floor-status: sonnet-floor
|
|
136
|
+
axis2-defense: reproducibility=see PR #471 CI log job 96296828886 fairness=arms differ: control is v2.5.1 tarball — stated estimation-layer=one measured, one quoted from CHANGELOG"
|
|
137
|
+
want borderline.md pass "control — admits an honest answer that NAMES an asymmetry"
|
|
138
|
+
# 🟥 known-negative on the em-dash: `fairness` here contains a dash, which must NOT truncate parsing
|
|
139
|
+
mk dashes.md "floor-status: sonnet-floor
|
|
140
|
+
axis2-defense: reproducibility=git log main..HEAD --oneline fairness=both arms — same reps, same env estimation-layer=measured; instrument calibrated on a known pair"
|
|
141
|
+
want dashes.md pass "control — dashes inside a value do not truncate it"
|
|
142
|
+
|
|
143
|
+
echo
|
|
144
|
+
if [ "$fail" -eq 0 ]; then
|
|
145
|
+
echo "✅ marker-defense: $pass lanes hold (0 failed)"
|
|
146
|
+
echo " 🟥 SCOPE: this suite pins PRESENCE, COMPLETENESS and NON-VACUITY. It cannot pin whether"
|
|
147
|
+
echo " the answers are TRUE — that is the operator's and the weekly audit's, like every other"
|
|
148
|
+
echo " marker field. A green line here is not a claim that the defense was actually mounted."
|
|
149
|
+
exit 0
|
|
150
|
+
fi
|
|
151
|
+
echo "❌ marker-defense: $fail failed / $pass passed"
|
|
152
|
+
exit 1
|