@chrono-meta/fh-gate 1.4.78 → 1.4.79
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-plugin/marketplace.json +2 -2
- package/knowledge/shared/harness-core/loop_engineering.md +1 -1
- package/package.json +9 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/scripts/fh_session_load.sh +42 -3
- package/scripts/halffix_propagation_scan.sh +126 -0
- package/scripts/package_coverage_check.sh +18 -1
- package/scripts/pipe_verdict_guard.sh +93 -0
- package/scripts/selfcheck.sh +84 -6
- package/scripts/session_close_check.sh +15 -1
- package/scripts/sidecar_calibrate.sh +85 -0
- package/scripts/test_card_drift_probe.sh +55 -0
- package/scripts/test_halffix_lanes.sh +170 -0
- package/scripts/test_ollama_panel_lanes.sh +120 -0
- package/scripts/test_package_coverage_lanes.sh +250 -0
- package/scripts/test_pipe_verdict_guard_lanes.sh +96 -0
- package/scripts/test_sidecar_wait_stdin.sh +13 -1
- package/templates/.git-hooks/pre-commit +9 -0
- package/templates/settings.PreToolUse.snippet.json +49 -0
|
@@ -29,6 +29,25 @@ run_fixture() { # $1=name $2=card-content $3=make-artifact(0/1) $4=expect-wa
|
|
|
29
29
|
local out; out=$(bash "$CHECK" "$T" 2>/dev/null)
|
|
30
30
|
local warned=0
|
|
31
31
|
printf '%s\n' "$out" | grep -q "⑤-b card-drift" && warned=1
|
|
32
|
+
# RUN EVERY FIXTURE TWICE — ambient locale AND LC_ALL=C. Added 2026-07-31 after the probe passed
|
|
33
|
+
# 3/3 here for weeks while returning warn=0 for EVERY lane on the ubuntu runner. Cause: the
|
|
34
|
+
# absence regex held a Hangul RANGE `[^가-힣]`, which GNU grep in the C locale rejects with
|
|
35
|
+
# "Invalid collation character" — it exits 2 and prints nothing, which downstream is
|
|
36
|
+
# indistinguishable from "no match", so the probe reported the card clean on every input.
|
|
37
|
+
# BSD grep accepts the range, so no amount of running this suite on macOS could ever have caught
|
|
38
|
+
# it. MEASURED LIMIT, stated because the first version of this comment overclaimed: LC_ALL=C on
|
|
39
|
+
# macOS is NOT a stand-in for GNU grep — reverting the Hangul range with this dual-run in place
|
|
40
|
+
# still passed here. So this leg detects the class only where GNU grep runs, i.e. in CI. It is
|
|
41
|
+
# kept because a locale-divergent verdict is a defect wherever it is observed, and it costs one
|
|
42
|
+
# extra invocation; the platform-independent detector is the source lint below.
|
|
43
|
+
local out_c; out_c=$(LC_ALL=C LANG=C bash "$CHECK" "$T" 2>/dev/null)
|
|
44
|
+
local warned_c=0
|
|
45
|
+
printf '%s\n' "$out_c" | grep -q "⑤-b card-drift" && warned_c=1
|
|
46
|
+
if [ "$warned" != "$warned_c" ]; then
|
|
47
|
+
echo "❌ $name — LOCALE-DIVERGENT: ambient warn=$warned but LC_ALL=C warn=$warned_c"
|
|
48
|
+
echo " 텍스트 계기가 로케일에 따라 다른 판정을 낸다 — 어느 쪽이 맞든 캘리브레이션 실패다."
|
|
49
|
+
rm -rf "$T"; FAILED=1; return
|
|
50
|
+
fi
|
|
32
51
|
rm -rf "$T"
|
|
33
52
|
if [ "$warned" = "$expect" ]; then
|
|
34
53
|
echo "✅ $name (warn=$warned, expected=$expect)"
|
|
@@ -73,5 +92,41 @@ run_fixture "P4 영어 부재주장 (A-4 앵커)" \
|
|
|
73
92
|
run_fixture "N7 인용된 부재키워드+정정 → 무경고 (실카드 07-22 클래스)" \
|
|
74
93
|
"- 🔴 foo-digest 산출 누락. 기존 카드의 \"미가동\" 주장은 오판정이었음" 1 0
|
|
75
94
|
|
|
95
|
+
|
|
96
|
+
# ── LOCALE-RANGE LINT (platform-independent, added 2026-07-31) ─────────────────────────
|
|
97
|
+
# The behavioural dual-run above cannot fire on BSD grep, so the class needs a detector that does
|
|
98
|
+
# not depend on which grep is installed. This one reads the SOURCE: a bracket expression containing
|
|
99
|
+
# a non-ASCII RANGE (`[^가-힣]`, `[ㄱ-ㅎ]`, …) is collation-dependent by construction and will be
|
|
100
|
+
# rejected outright by GNU grep in the C locale — "Invalid collation character", exit 2, no output,
|
|
101
|
+
# which downstream reads as "no match" and therefore as clean.
|
|
102
|
+
# Alternation of non-ASCII literals (`(🔴|🟡)`, `미가동|부재`) is NOT flagged: it carries no
|
|
103
|
+
# collation, and the runner proved it works (the emoji line-selection stage passed there while the
|
|
104
|
+
# range stage errored). Flagging it would push an author to mangle working code.
|
|
105
|
+
echo "-- locale-range lint: 비ASCII 문자 범위를 쓰는 정규식 (collation 의존) --"
|
|
106
|
+
LINT_OUT=$(python3 - "$SCRIPT_DIR" <<'LINTPY'
|
|
107
|
+
import re, sys, glob, os
|
|
108
|
+
root = sys.argv[1]
|
|
109
|
+
# a bracket expression containing <non-ascii> - <non-ascii>
|
|
110
|
+
rng = re.compile(r'\[[^]\n]*[^\x00-\x7F]-[^\x00-\x7F][^]\n]*\]')
|
|
111
|
+
hits = []
|
|
112
|
+
for f in sorted(glob.glob(os.path.join(root, '*.sh'))):
|
|
113
|
+
for i, line in enumerate(open(f, encoding='utf-8', errors='replace'), 1):
|
|
114
|
+
if line.lstrip().startswith('#'):
|
|
115
|
+
continue
|
|
116
|
+
m = rng.search(line)
|
|
117
|
+
if m:
|
|
118
|
+
hits.append(f"{os.path.basename(f)}:{i}: {m.group(0)}")
|
|
119
|
+
print('\n'.join(hits))
|
|
120
|
+
LINTPY
|
|
121
|
+
)
|
|
122
|
+
if [ -n "$LINT_OUT" ]; then
|
|
123
|
+
echo "❌ 비ASCII 범위 발견 — GNU grep(C 로케일)에서 exit 2 로 죽고 결과가 '무매치'로 읽힌다:"
|
|
124
|
+
printf '%s\n' "$LINT_OUT" | sed 's/^/ /'
|
|
125
|
+
FAILED=1
|
|
126
|
+
else
|
|
127
|
+
echo "✅ locale-range lint: 비ASCII 문자 범위 없음"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
|
|
76
131
|
echo "── card-drift probe calibration: $([ "$FAILED" -eq 0 ] && echo "PASS (전 픽스처) — 배선 가능" || echo "FAIL — 계기 불량, 배선 금지") ──"
|
|
77
132
|
exit "$FAILED"
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_halffix_lanes.sh — known pairs for scripts/halffix_propagation_scan.sh
|
|
3
|
+
#
|
|
4
|
+
# WHY, AND WHY LANES FIRST
|
|
5
|
+
# 2026-07-30 harvest #1: rounds that wrote the lane BEFORE touching the code had zero
|
|
6
|
+
# fix-reverts-fix regressions. Order made the convergence, not the patch.
|
|
7
|
+
#
|
|
8
|
+
# THE DEFECT BEING GUARDED — "반쪽-수리" (half-fix)
|
|
9
|
+
# A defect class lives in N sibling copies. The fix lands in ONE. The siblings keep the bug, and
|
|
10
|
+
# nothing says so. Measured 3x in this project, and the shape is worse than it sounds: every one
|
|
11
|
+
# of the three occurred INSIDE an edit that was itself repairing an earlier half-fix.
|
|
12
|
+
# scripts/psa_scan_lib.sh's header records five such divergences found in a single audit — the
|
|
13
|
+
# override-readable check, the fail-closed-on-no-patterns branch, the SPACE-vs-TAB row, the
|
|
14
|
+
# `head -1` shielding, and the LOW allowlist — each repaired where it was found and nowhere else.
|
|
15
|
+
#
|
|
16
|
+
# THE DISCRIMINATOR THAT MAKES THIS USEFUL INSTEAD OF NOISY (lane N3)
|
|
17
|
+
# If BOTH copies are staged, the fix WAS propagated and the scan must stay silent. A detector that
|
|
18
|
+
# fires on correct propagation is a detector that gets ignored — the S5 lesson (9/9 FP, narrowed
|
|
19
|
+
# 2026-07-28) and the S6 lesson (0 true positives on the planned surface, retargeted 2026-07-31)
|
|
20
|
+
# are the two prior claims on this exact mistake in this repo.
|
|
21
|
+
#
|
|
22
|
+
# ADVISORY BY MANDATE, not by preference: the operator's spec for this debt says "표시(차단 아님 —
|
|
23
|
+
# 정당한 복제도 있다)" — mark, do not block, because legitimate duplication exists.
|
|
24
|
+
|
|
25
|
+
set -u
|
|
26
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
27
|
+
SCAN="$ROOT/scripts/halffix_propagation_scan.sh"
|
|
28
|
+
pass=0; fail=0
|
|
29
|
+
|
|
30
|
+
# mkrepo <dir> — a throwaway git repo; caller populates and stages.
|
|
31
|
+
mkrepo() {
|
|
32
|
+
rm -rf "$1"; mkdir -p "$1"; ( cd "$1" && git init -q . && git config user.email t@e && git config user.name t )
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# expect <label> <FLAG|SILENT> <repo> [needle]
|
|
36
|
+
expect() {
|
|
37
|
+
local label="$1" want="$2" repo="$3" needle="${4:-}" out got
|
|
38
|
+
out=$( cd "$repo" && bash "$SCAN" 2>&1 )
|
|
39
|
+
if printf '%s' "$out" | grep -q 'HALF-FIX'; then got=FLAG; else got=SILENT; fi
|
|
40
|
+
if [ "$got" = "$want" ] && { [ -z "$needle" ] || printf '%s' "$out" | grep -q "$needle"; }; then
|
|
41
|
+
printf ' ✅ %-50s %s\n' "$label" "$got"; pass=$((pass+1))
|
|
42
|
+
else
|
|
43
|
+
printf ' ❌ %-50s %s (expected %s%s)\n' "$label" "$got" "$want" \
|
|
44
|
+
"${needle:+, needle '$needle'}"; fail=$((fail+1))
|
|
45
|
+
printf ' out: %s\n' "$(printf '%s' "$out" | head -6)"
|
|
46
|
+
fi
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
|
50
|
+
echo "[halffix] known pairs"
|
|
51
|
+
|
|
52
|
+
# ── P1: the canonical shape — a distinctive symbol duplicated in a sibling, only one staged ──
|
|
53
|
+
R="$T/p1"; mkrepo "$R"
|
|
54
|
+
printf 'psa_low_allowlisted() {\n case "$1" in templates/*) return 0 ;; esac\n}\n' > "$R/a.sh"
|
|
55
|
+
printf 'psa_low_allowlisted() {\n case "$1" in templates/*) return 0 ;; esac\n}\n' > "$R/b.sh"
|
|
56
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
57
|
+
printf 'psa_low_allowlisted() {\n case "$1" in templates/*|docs/*) return 0 ;; esac\n}\n' > "$R/a.sh"
|
|
58
|
+
( cd "$R" && git add a.sh )
|
|
59
|
+
expect "P1 sibling copy left unstaged" FLAG "$R" "b.sh"
|
|
60
|
+
|
|
61
|
+
# ── P2: three copies, one staged — both remaining siblings named ──
|
|
62
|
+
R="$T/p2"; mkrepo "$R"
|
|
63
|
+
for f in a b c; do printf 'validate_marker_fields_strictly() { :; }\n' > "$R/$f.sh"; done
|
|
64
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
65
|
+
printf 'validate_marker_fields_strictly() { echo fixed; }\n' > "$R/a.sh"
|
|
66
|
+
( cd "$R" && git add a.sh )
|
|
67
|
+
expect "P2 two siblings, both named" FLAG "$R" "c.sh"
|
|
68
|
+
|
|
69
|
+
# ── N3: THE DISCRIMINATOR — both copies staged means the fix propagated. Silence. ──
|
|
70
|
+
R="$T/n3"; mkrepo "$R"
|
|
71
|
+
printf 'psa_low_allowlisted() { :; }\n' > "$R/a.sh"; cp "$R/a.sh" "$R/b.sh"
|
|
72
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
73
|
+
printf 'psa_low_allowlisted() { echo fixed; }\n' > "$R/a.sh"; cp "$R/a.sh" "$R/b.sh"
|
|
74
|
+
( cd "$R" && git add a.sh b.sh )
|
|
75
|
+
expect "N3 both staged = propagated" SILENT "$R"
|
|
76
|
+
|
|
77
|
+
# ── N4: unique content — nothing to propagate to ──
|
|
78
|
+
R="$T/n4"; mkrepo "$R"
|
|
79
|
+
printf 'lonely_unique_helper_fn() { :; }\n' > "$R/a.sh"
|
|
80
|
+
printf 'something_entirely_different() { :; }\n' > "$R/b.sh"
|
|
81
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
82
|
+
printf 'lonely_unique_helper_fn() { echo x; }\n' > "$R/a.sh"
|
|
83
|
+
( cd "$R" && git add a.sh )
|
|
84
|
+
expect "N4 no sibling holds the symbol" SILENT "$R"
|
|
85
|
+
|
|
86
|
+
# ── N5: ubiquitous token — a word in 12 files is framework noise, not a half-fix ──
|
|
87
|
+
R="$T/n5"; mkrepo "$R"
|
|
88
|
+
for i in $(seq 1 12); do printf 'REPO_ROOT_DIRECTORY=/x\n' > "$R/f$i.sh"; done
|
|
89
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
90
|
+
printf 'REPO_ROOT_DIRECTORY=/y\n' > "$R/f1.sh"
|
|
91
|
+
( cd "$R" && git add f1.sh )
|
|
92
|
+
expect "N5 ubiquitous token suppressed" SILENT "$R"
|
|
93
|
+
|
|
94
|
+
# ── N6: short/common tokens must not anchor (would flag on `then`, `echo`, `return`) ──
|
|
95
|
+
R="$T/n6"; mkrepo "$R"
|
|
96
|
+
printf 'if [ -f x ]; then echo a; fi\n' > "$R/a.sh"
|
|
97
|
+
printf 'if [ -f y ]; then echo b; fi\n' > "$R/b.sh"
|
|
98
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
99
|
+
printf 'if [ -f x ]; then echo c; fi\n' > "$R/a.sh"
|
|
100
|
+
( cd "$R" && git add a.sh )
|
|
101
|
+
expect "N6 short shell keywords ignored" SILENT "$R"
|
|
102
|
+
|
|
103
|
+
# ── P7: filenames count as symbols too (the spec says "수정 심볼·파일명") ──
|
|
104
|
+
R="$T/p7"; mkrepo "$R"
|
|
105
|
+
printf 'bash scripts/sidecar_calibrate.sh --run\n' > "$R/a.sh"
|
|
106
|
+
printf 'bash scripts/sidecar_calibrate.sh --run\n' > "$R/doc.md"
|
|
107
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
108
|
+
printf 'bash scripts/sidecar_calibrate.sh --run --strict\n' > "$R/a.sh"
|
|
109
|
+
( cd "$R" && git add a.sh )
|
|
110
|
+
expect "P7 filename token propagates" FLAG "$R" "doc.md"
|
|
111
|
+
|
|
112
|
+
# ── N8: nothing staged at all ──
|
|
113
|
+
R="$T/n8"; mkrepo "$R"; printf 'x\n' > "$R/a.sh"; ( cd "$R" && git add . && git commit -qm base )
|
|
114
|
+
expect "N8 empty staging area" SILENT "$R"
|
|
115
|
+
|
|
116
|
+
# ── N9: opt-out ──
|
|
117
|
+
R="$T/n9"; mkrepo "$R"
|
|
118
|
+
printf 'psa_low_allowlisted() { :; }\n' > "$R/a.sh"; cp "$R/a.sh" "$R/b.sh"
|
|
119
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
120
|
+
printf 'psa_low_allowlisted() { echo x; } # noqa: half-fix\n' > "$R/a.sh"
|
|
121
|
+
( cd "$R" && git add a.sh )
|
|
122
|
+
expect "N9 noqa suppresses" SILENT "$R"
|
|
123
|
+
|
|
124
|
+
# ── N10: deleted file must not crash the scan ──
|
|
125
|
+
R="$T/n10"; mkrepo "$R"
|
|
126
|
+
printf 'psa_low_allowlisted() { :; }\n' > "$R/a.sh"; cp "$R/a.sh" "$R/b.sh"
|
|
127
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
128
|
+
( cd "$R" && git rm -q a.sh )
|
|
129
|
+
expect "N10 deletion does not crash" SILENT "$R"
|
|
130
|
+
|
|
131
|
+
echo "-- R2: whole-file copy divergence (found by LIVE calibration, not by the lanes above) --"
|
|
132
|
+
# The token rule alone MISSED this repo's real duplicate pair (scripts/degrade_direction_scan.sh ↔
|
|
133
|
+
# templates/degrade_direction_scan.sh) because a script's NAME appears in 11–18 files here — docs,
|
|
134
|
+
# CATALOG, the manifest, selfcheck references — so the ubiquity filter suppressed it, while an
|
|
135
|
+
# internal symbol like psa_low_allowlisted appears in 2. Measured 2026-07-31: the lanes were 10/10
|
|
136
|
+
# green and the live probe was silent. Lanes are necessary and not sufficient.
|
|
137
|
+
#
|
|
138
|
+
# The predicate is exact rather than heuristic: the sibling was BYTE-IDENTICAL at HEAD and only one
|
|
139
|
+
# side is staged. That is a divergence by construction, so it needs no threshold — and it stays
|
|
140
|
+
# silent for same-basename files that were never copies (CLAUDE.md vs templates/CLAUDE.md, the 40
|
|
141
|
+
# SKILL.md files), which a basename rule would have flooded.
|
|
142
|
+
R="$T/r2p"; mkrepo "$R"; mkdir -p "$R/scripts" "$R/templates"
|
|
143
|
+
printf 'a() { :; }\nb() { :; }\n' > "$R/scripts/x.sh"
|
|
144
|
+
cp "$R/scripts/x.sh" "$R/templates/x.sh"
|
|
145
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
146
|
+
printf 'a() { echo fixed; }\nb() { :; }\n' > "$R/scripts/x.sh"
|
|
147
|
+
( cd "$R" && git add scripts/x.sh )
|
|
148
|
+
expect "R2 byte-identical copy diverges" FLAG "$R" "templates/x.sh"
|
|
149
|
+
|
|
150
|
+
R="$T/r2n"; mkrepo "$R"; mkdir -p "$R/scripts" "$R/templates"
|
|
151
|
+
printf 'hub version, quite different\n' > "$R/scripts/x.sh"
|
|
152
|
+
printf 'template version, also different\n' > "$R/templates/x.sh"
|
|
153
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
154
|
+
printf 'hub version, edited\n' > "$R/scripts/x.sh"
|
|
155
|
+
( cd "$R" && git add scripts/x.sh )
|
|
156
|
+
expect "R2 same name but never a copy" SILENT "$R"
|
|
157
|
+
|
|
158
|
+
R="$T/r2b"; mkrepo "$R"; mkdir -p "$R/scripts" "$R/templates"
|
|
159
|
+
printf 'a() { :; }\n' > "$R/scripts/x.sh"; cp "$R/scripts/x.sh" "$R/templates/x.sh"
|
|
160
|
+
( cd "$R" && git add . && git commit -qm base )
|
|
161
|
+
printf 'a() { echo fixed; }\n' > "$R/scripts/x.sh"; cp "$R/scripts/x.sh" "$R/templates/x.sh"
|
|
162
|
+
( cd "$R" && git add scripts/x.sh templates/x.sh )
|
|
163
|
+
expect "R2 both staged = propagated" SILENT "$R"
|
|
164
|
+
|
|
165
|
+
echo
|
|
166
|
+
if [ "$fail" -eq 0 ]; then
|
|
167
|
+
echo "[halffix] ✅ all $pass known pairs hold"; exit 0
|
|
168
|
+
else
|
|
169
|
+
echo "[halffix] ❌ $fail/$((pass+fail)) lanes failed"; exit 1
|
|
170
|
+
fi
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_ollama_panel_lanes.sh — hermetic known pairs for the ollama panel leg of sidecar_calibrate.sh
|
|
3
|
+
#
|
|
4
|
+
# Hermetic by construction: every lane runs against a stub HTTP server on loopback. No network, no
|
|
5
|
+
# spend, no dependence on a node being powered on — the same property the CLI lanes get from stub
|
|
6
|
+
# binaries. A calibrator whose own tests need the thing it calibrates cannot fail honestly.
|
|
7
|
+
#
|
|
8
|
+
# WHAT MAKES THIS LEG DIFFERENT FROM codex/agy, AND WHY IT NEEDED ITS OWN LANES
|
|
9
|
+
# The CLI legs anchor PIN-OK on the model's SELF-REPORT. That anchor is invalid here and the
|
|
10
|
+
# measurement says so: asked which model it was, `gpt-oss:20b` answered "The underlying model is
|
|
11
|
+
# GPT-4 (likely)" (2026-07-31). Open-weight models do not reliably know their own name, so a
|
|
12
|
+
# self-report anchor would mark an EXACT pin as UNTRUSTED and drop it from the panel — an
|
|
13
|
+
# instrument that cannot separate known-positive from known-negative on its target. The anchor is
|
|
14
|
+
# therefore the SERVER's response envelope: a server-side fact, not a model's claim.
|
|
15
|
+
# Lane S2 is the reason that distinction has teeth — it is the agy failure in this protocol's
|
|
16
|
+
# spelling: the server quietly answers as a different model than the one pinned.
|
|
17
|
+
|
|
18
|
+
set -u
|
|
19
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
20
|
+
CAL="$ROOT/scripts/sidecar_calibrate.sh"
|
|
21
|
+
pass=0; fail=0
|
|
22
|
+
PORT="${FH_LANE_PORT:-18011}"
|
|
23
|
+
SRV_PID=""
|
|
24
|
+
|
|
25
|
+
# `wait` after the kill, so bash reaps the child quietly instead of printing "Terminated: 15" into
|
|
26
|
+
# the lane output — a harness that litters CI logs teaches people to stop reading them.
|
|
27
|
+
stop_stub() {
|
|
28
|
+
[ -n "$SRV_PID" ] || return 0
|
|
29
|
+
kill "$SRV_PID" 2>/dev/null
|
|
30
|
+
wait "$SRV_PID" 2>/dev/null
|
|
31
|
+
SRV_PID=""
|
|
32
|
+
}
|
|
33
|
+
trap 'stop_stub' EXIT
|
|
34
|
+
|
|
35
|
+
# The stub is written to a file so the heredoc cannot collide with the lane script's own quoting.
|
|
36
|
+
STUB="$(mktemp -d)/stub.py"
|
|
37
|
+
cat > "$STUB" <<'PY'
|
|
38
|
+
import json, sys
|
|
39
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
40
|
+
PORT = int(sys.argv[1]); MODE = sys.argv[2]
|
|
41
|
+
|
|
42
|
+
class H(BaseHTTPRequestHandler):
|
|
43
|
+
def log_message(self, *a): pass
|
|
44
|
+
def _send(self, obj, code=200):
|
|
45
|
+
b = json.dumps(obj).encode()
|
|
46
|
+
self.send_response(code); self.send_header("Content-Type", "application/json")
|
|
47
|
+
self.send_header("Content-Length", str(len(b))); self.end_headers(); self.wfile.write(b)
|
|
48
|
+
def do_GET(self):
|
|
49
|
+
if self.path == "/api/version": self._send({"version": "stub"})
|
|
50
|
+
elif self.path == "/api/tags": self._send({"models": [{"name": "stub-model:1b"}]})
|
|
51
|
+
else: self._send({}, 404)
|
|
52
|
+
def do_POST(self):
|
|
53
|
+
n = int(self.headers.get("Content-Length", 0))
|
|
54
|
+
req = json.loads(self.rfile.read(n) or b"{}")
|
|
55
|
+
m = req.get("model", "")
|
|
56
|
+
if m.startswith("fh-calib-nonexistent"):
|
|
57
|
+
# BOGUS control. 'accept' mode is the dangerous server: it serves SOMETHING for a name
|
|
58
|
+
# that cannot exist, so the pin is never validated at all.
|
|
59
|
+
if MODE == "accept": self._send({"model": m, "response": "PASS"})
|
|
60
|
+
else: self._send({"error": f"model '{m}' not found"})
|
|
61
|
+
return
|
|
62
|
+
if MODE == "substitute": # the agy failure, in this protocol
|
|
63
|
+
self._send({"model": "some-other-model:9b", "response": "PASS"}); return
|
|
64
|
+
if MODE == "starved": # reasoning model spent the whole budget thinking
|
|
65
|
+
self._send({"model": m, "response": "", "thinking": "x" * 780}); return
|
|
66
|
+
if MODE == "prose": # cannot carry a machine-read verdict
|
|
67
|
+
self._send({"model": m, "response": "Well, I would say this looks like a PASS overall."}); return
|
|
68
|
+
self._send({"model": m, "response": "PASS"})
|
|
69
|
+
HTTPServer(("127.0.0.1", PORT), H).serve_forever()
|
|
70
|
+
PY
|
|
71
|
+
# shellcheck disable=SC2016
|
|
72
|
+
start_stub() {
|
|
73
|
+
stop_stub
|
|
74
|
+
python3 "$STUB" "$PORT" "$1" >/dev/null 2>&1 &
|
|
75
|
+
SRV_PID=$!
|
|
76
|
+
for _ in $(seq 1 40); do
|
|
77
|
+
curl -sf --max-time 1 "http://127.0.0.1:$PORT/api/version" >/dev/null 2>&1 && return 0
|
|
78
|
+
sleep 0.25
|
|
79
|
+
done
|
|
80
|
+
return 1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# expect <label> <mode> <expect-in-panel: YES|NO> [needle]
|
|
84
|
+
expect() {
|
|
85
|
+
local label="$1" mode="$2" want="$3" needle="${4:-}" out inpanel
|
|
86
|
+
start_stub "$mode" || { printf ' ❌ %-44s stub failed to start\n' "$label"; fail=$((fail+1)); return; }
|
|
87
|
+
out=$( FH_OLLAMA_HOST="http://127.0.0.1:$PORT" FH_OLLAMA_MODELS="stub-model:1b" \
|
|
88
|
+
bash "$CAL" --only ollama --quiet 2>&1 )
|
|
89
|
+
stop_stub
|
|
90
|
+
if printf '%s' "$out" | grep -q 'PANEL:.*ollama:stub-model:1b'; then inpanel=YES; else inpanel=NO; fi
|
|
91
|
+
if [ "$inpanel" = "$want" ] && { [ -z "$needle" ] || printf '%s' "$out" | grep -q "$needle"; }; then
|
|
92
|
+
printf ' ✅ %-44s in-panel=%s\n' "$label" "$inpanel"; pass=$((pass+1))
|
|
93
|
+
else
|
|
94
|
+
printf ' ❌ %-44s in-panel=%s (expected %s%s)\n' "$label" "$inpanel" "$want" "${needle:+, needle '$needle'}"
|
|
95
|
+
printf ' out: %s\n' "$(printf '%s' "$out" | head -4)"; fail=$((fail+1))
|
|
96
|
+
fi
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
echo "[ollama-panel] known pairs (hermetic stub server)"
|
|
100
|
+
expect "S1 envelope matches pin, verdict parses" ok YES "PIN-OK(envelope)"
|
|
101
|
+
expect "S2 server substitutes a DIFFERENT model" substitute NO "UNTRUSTED-PIN"
|
|
102
|
+
expect "S3 empty answer (budget starvation)" starved NO "larger FH_OLLAMA_NUM_PREDICT"
|
|
103
|
+
expect "S4 prose answer cannot carry a verdict" prose NO "VERDICT-UNPARSEABLE"
|
|
104
|
+
expect "S5 server accepts a bogus model name" accept YES "accepts-bogus"
|
|
105
|
+
|
|
106
|
+
# S6 — absence must be MEASURED, never assumed. No server at the host at all.
|
|
107
|
+
out=$( FH_OLLAMA_HOST="http://127.0.0.1:$((PORT+7))" bash "$CAL" --only ollama --quiet 2>&1 )
|
|
108
|
+
if printf '%s' "$out" | grep -q 'ABSENT — no server at the configured host'; then
|
|
109
|
+
printf ' ✅ %-44s reported ABSENT\n' "S6 no server at host"; pass=$((pass+1))
|
|
110
|
+
else
|
|
111
|
+
printf ' ❌ %-44s (expected a measured ABSENT line)\n' "S6 no server at host"
|
|
112
|
+
printf ' out: %s\n' "$(printf '%s' "$out" | head -3)"; fail=$((fail+1))
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
echo
|
|
116
|
+
if [ "$fail" -eq 0 ]; then
|
|
117
|
+
echo "[ollama-panel] ✅ all $pass known pairs hold"; exit 0
|
|
118
|
+
else
|
|
119
|
+
echo "[ollama-panel] ❌ $fail/$((pass+fail)) lanes failed"; exit 1
|
|
120
|
+
fi
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_package_coverage_lanes.sh — regression anchor for scripts/package_coverage_check.sh.
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS EXISTS (measured 2026-07-31):
|
|
5
|
+
# package_coverage_check.sh gated itself on `[ ! -d .git ]`. In a git WORKTREE `.git` is a FILE
|
|
6
|
+
# (a gitdir pointer), so every worktree fell into the "installed package" branch and the check
|
|
7
|
+
# printed `SKIP` and exited 0 without scanning anything. That is a FALSE CLEAN of the worst shape:
|
|
8
|
+
# a worktree is the standard way to approximate a fresh CI checkout, so the surface used to argue
|
|
9
|
+
# "CI would be green" was precisely the surface on which this check silently did not run.
|
|
10
|
+
# It was found by using the instrument, then asking whether the instrument had run at all —
|
|
11
|
+
# CLAUDE.md §Instrument-Calibration, applied to FH's own tooling.
|
|
12
|
+
#
|
|
13
|
+
# Second reason: selfcheck.sh enforces "subject present but anchor missing => FAIL" for eight
|
|
14
|
+
# other scripts, and package_coverage_check.sh was the one subject exempted from its own rule.
|
|
15
|
+
# An unanchored checker is one revert away from being decoration.
|
|
16
|
+
#
|
|
17
|
+
# The lanes pin, in order: the source-checkout predicate across all four tree shapes (worktree /
|
|
18
|
+
# ordinary checkout / package mode / no manifest), and a KNOWN PAIR — a tree that must FAIL and an
|
|
19
|
+
# otherwise identical tree that must PASS. A predicate that cannot separate that pair is not
|
|
20
|
+
# measuring coverage, it is emitting a verdict.
|
|
21
|
+
#
|
|
22
|
+
# Usage: bash scripts/test_package_coverage_lanes.sh
|
|
23
|
+
# Exit: 0 = no lane FAILED. 1 = a regression.
|
|
24
|
+
# NOT "all lanes passed": a lane whose precondition is absent (no git) is counted as
|
|
25
|
+
# UNCALIBRATED and printed in the summary, and exit stays 0. Stated precisely because the
|
|
26
|
+
# earlier wording promised more than the code delivers, and an exit contract that overstates
|
|
27
|
+
# is exactly the false-clean this suite exists to prevent (round-3 review, LOW).
|
|
28
|
+
set -uo pipefail
|
|
29
|
+
|
|
30
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
31
|
+
SUBJECT="$REPO_ROOT/scripts/package_coverage_check.sh"
|
|
32
|
+
[ -f "$SUBJECT" ] || { echo "FAIL: $SUBJECT not found"; exit 1; }
|
|
33
|
+
|
|
34
|
+
TMP="$(mktemp -d)"
|
|
35
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
36
|
+
|
|
37
|
+
pass=0; fail=0; skipped=0
|
|
38
|
+
ok() { printf ' ✅ %s\n' "$1"; pass=$((pass+1)); }
|
|
39
|
+
bad() { printf ' ❌ %s\n' "$1"; fail=$((fail+1)); }
|
|
40
|
+
# A skip is COUNTED and reported. An uncounted skip is how "could not run" becomes indistinguishable
|
|
41
|
+
# from "ran and passed" in the summary line — measured on this very suite in review round 2, where a
|
|
42
|
+
# broken git produced "7 passed, 0 failed" and exit 0 while the real-worktree claim went untested.
|
|
43
|
+
# Degrade direction per CLAUDE.md §Instrument-Calibration: label it UNCALIBRATED, never a bare pass.
|
|
44
|
+
skip() { printf ' ⏭ UNCALIBRATED — %s\n' "$1"; skipped=$((skipped+1)); }
|
|
45
|
+
|
|
46
|
+
# Build a hermetic fake source tree. `git_shape` is one of: file | dir | none.
|
|
47
|
+
# `ship_the_doc` decides whether the referenced path is inside files[] (the known pair).
|
|
48
|
+
make_tree() { # make_tree <dir> <git_shape> <cover_target:yes|no> [omit_manifest]
|
|
49
|
+
local d="$1" git_shape="$2" cover="$3" omit="${4:-}"
|
|
50
|
+
mkdir -p "$d/scripts" "$d/docs"
|
|
51
|
+
cp "$SUBJECT" "$d/scripts/package_coverage_check.sh"
|
|
52
|
+
|
|
53
|
+
# A shipped document that points at scripts/helper.sh, and that file really exists here.
|
|
54
|
+
# "exists here but absent from files[]" is exactly the defect class this check owns.
|
|
55
|
+
printf 'Run `scripts/helper.sh` before the gate.\n' > "$d/docs/guide.md"
|
|
56
|
+
printf '#!/usr/bin/env bash\necho helper\n' > "$d/scripts/helper.sh"
|
|
57
|
+
|
|
58
|
+
if [ "$cover" = "yes" ]; then
|
|
59
|
+
printf '{"files":["docs/guide.md","scripts/helper.sh"]}\n' > "$d/package.json"
|
|
60
|
+
else
|
|
61
|
+
printf '{"files":["docs/guide.md"]}\n' > "$d/package.json"
|
|
62
|
+
fi
|
|
63
|
+
[ -n "$omit" ] && rm -f "$d/package.json"
|
|
64
|
+
|
|
65
|
+
case "$git_shape" in
|
|
66
|
+
dir) mkdir -p "$d/.git" ;;
|
|
67
|
+
file) printf 'gitdir: /somewhere/else/.git/worktrees/x\n' > "$d/.git" ;;
|
|
68
|
+
none) : ;;
|
|
69
|
+
esac
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
run_tree() { bash "$1/scripts/package_coverage_check.sh" 2>&1; }
|
|
73
|
+
rc_tree() { bash "$1/scripts/package_coverage_check.sh" >/dev/null 2>&1; echo $?; }
|
|
74
|
+
|
|
75
|
+
# Assert a POSITIVE outcome, never merely the absence of the SKIP line. Cross-family review caught
|
|
76
|
+
# the first draft here: it checked only that `SKIP package-coverage` was missing, so a checker that
|
|
77
|
+
# exited 1 on every worktree — the opposite defect, equally broken — would have passed the lane that
|
|
78
|
+
# exists to prove the worktree path works. "Did not say the wrong thing" is not "did the right
|
|
79
|
+
# thing"; a lane phrased as a negative can only ever fail one way.
|
|
80
|
+
ran_clean() { # ran_clean <dir> -> 0 iff the check actually ran AND reported a clean scan
|
|
81
|
+
local d="$1" o rc
|
|
82
|
+
o=$(bash "$d/scripts/package_coverage_check.sh" 2>&1); rc=$?
|
|
83
|
+
[ "$rc" -eq 0 ] && printf '%s' "$o" | grep -q 'PASS package-coverage' \
|
|
84
|
+
&& ! printf '%s' "$o" | grep -q 'SKIP package-coverage'
|
|
85
|
+
}
|
|
86
|
+
caught_defect() { # caught_defect <dir> -> 0 iff the check FOUND the planted omission
|
|
87
|
+
local d="$1" o rc
|
|
88
|
+
o=$(bash "$d/scripts/package_coverage_check.sh" 2>&1); rc=$?
|
|
89
|
+
[ "$rc" -eq 1 ] && printf '%s' "$o" | grep -q 'scripts/helper.sh'
|
|
90
|
+
}
|
|
91
|
+
# A CLEAN LANE ALONE PROVES NOTHING ABOUT THE WORKTREE PATH. Cross-family review round 2 demonstrated
|
|
92
|
+
# this by EXECUTION: it replaced the subject with a mutant that printed `PASS package-coverage`
|
|
93
|
+
# whenever `.git` was a file, without scanning anything — and all eight lanes passed, suite exit 0.
|
|
94
|
+
# The lanes proved "returned a PASS-shaped string", not "ran the coverage logic". The known-positive
|
|
95
|
+
# fixtures existed but only under the `.git`-is-a-DIRECTORY shape, so nothing exercised detection in
|
|
96
|
+
# the very shape the fix was about. Every worktree lane below is now a PAIR: the same tree must go
|
|
97
|
+
# clean->PASS and planted-omission->FAIL. A bypassing mutant fails the second half by construction.
|
|
98
|
+
wt_pair() { # wt_pair <label> <dir> <git_shape>
|
|
99
|
+
local label="$1" d="$2" shape="$3"
|
|
100
|
+
make_tree "$d" "$shape" yes
|
|
101
|
+
if ! ran_clean "$d"; then
|
|
102
|
+
bad "$label — clean leg: did not run to a PASS ($(run_tree "$d" | head -1))"; return
|
|
103
|
+
fi
|
|
104
|
+
make_tree "$d" "$shape" no # identical tree, files[] no longer covers the referenced path
|
|
105
|
+
if ! caught_defect "$d"; then
|
|
106
|
+
bad "$label — DEFECT leg: planted omission not caught, so the clean PASS proved nothing"; return
|
|
107
|
+
fi
|
|
108
|
+
ok "$label"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# ── L1 · THE REGRESSION ANCHOR ───────────────────────────────────────────────────────
|
|
112
|
+
wt_pair "L1 worktree (.git is a FILE): scans for real — clean PASSes, planted omission FAILs" \
|
|
113
|
+
"$TMP/l1" file
|
|
114
|
+
|
|
115
|
+
# ── L1-b · a REAL git worktree, not a hand-written pointer ───────────────────────────
|
|
116
|
+
# The synthetic fixture writes a gitdir pointer whose target does not exist, so it proves the
|
|
117
|
+
# predicate accepts "a file named .git" — not that it accepts a genuine worktree. This lane builds
|
|
118
|
+
# an actual repo and an actual `git worktree add`.
|
|
119
|
+
# ISOLATION (review round 2, LOW): the git commands run with signing off, hooks disabled, and both
|
|
120
|
+
# config files pointed at /dev/null, so a configured signing prompt or a global hook cannot hang the
|
|
121
|
+
# suite or write outside $TMP. GIT_TERMINAL_PROMPT=0 turns any credential prompt into an error.
|
|
122
|
+
# SKIP ACCOUNTING (review round 2, MED — confirmed by execution): the first draft printed a skip and
|
|
123
|
+
# incremented nothing, so a machine with a broken git reported "7 passed, 0 failed" and exited 0 —
|
|
124
|
+
# a lane that cannot run must not read as a lane that passed. Now: git ABSENT is a legitimate skip
|
|
125
|
+
# but is counted and surfaced as UNCALIBRATED in the summary; git PRESENT but setup failing is a
|
|
126
|
+
# FAILURE, because there the claim was testable and the test did not run.
|
|
127
|
+
GIT_ISO=(-c commit.gpgsign=false -c core.hooksPath=/dev/null -c user.email=a@b -c user.name=t)
|
|
128
|
+
# CONFIG isolation is not REPOSITORY-ROUTING isolation. Round-3 review reproduced this on Apple Git
|
|
129
|
+
# 2.50.1: with GIT_DIR pointing at an unrelated repo, this suite reported "8 passed, 0 failed" while
|
|
130
|
+
# that external repo's commit count went 0 -> 1. So the lane could pass by exercising — and WRITING
|
|
131
|
+
# TO — a repository that is not its fixture. A test that mutates state outside its own $TMP is not
|
|
132
|
+
# a test, and the green summary is the worst part: it reports calibration it did not perform.
|
|
133
|
+
# Every routing variable is cleared, and the resolved git-dir is then ASSERTED to live under $TMP
|
|
134
|
+
# rather than assumed — the unset list is a denylist and a denylist is never proof; the assertion is.
|
|
135
|
+
# SECOND LEAK CHANNEL, same class, found by the reviewer running the attack rather than reading for
|
|
136
|
+
# it: GIT_CONFIG_PARAMETERS injects config that GIT_CONFIG_GLOBAL/SYSTEM=/dev/null does NOT suppress.
|
|
137
|
+
# Reproduced here: with `init.templateDir` pointed at a directory whose `refs` is a symlink to an
|
|
138
|
+
# external dir, `git init` populated that external dir — and the suite still printed
|
|
139
|
+
# "8 passed, 0 failed". The git-dir assertion cannot see this one, because the git-dir DOES resolve
|
|
140
|
+
# inside $TMP; the escape is through the template, not the routing. So the assertion is necessary and
|
|
141
|
+
# not sufficient, and the config-injection channels have to be closed by name.
|
|
142
|
+
# `--template=` with an empty directory is the belt to that braces: even if a config channel is
|
|
143
|
+
# missed, init has an explicit, empty template to copy from.
|
|
144
|
+
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_OBJECT_DIRECTORY \
|
|
145
|
+
GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_NAMESPACE GIT_CEILING_DIRECTORIES \
|
|
146
|
+
GIT_CONFIG_PARAMETERS GIT_CONFIG_COUNT GIT_TEMPLATE_DIR
|
|
147
|
+
export GIT_TERMINAL_PROMPT=0 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null
|
|
148
|
+
if ! command -v git >/dev/null 2>&1; then
|
|
149
|
+
skip "L1-b real git worktree (git not installed — the on-disk layout under test cannot exist here)"
|
|
150
|
+
else
|
|
151
|
+
mkdir -p "$TMP/realrepo"
|
|
152
|
+
# BOTH SIDES NORMALIZED. First version compared the raw `mktemp -d` path against
|
|
153
|
+
# `rev-parse --absolute-git-dir`, and on macOS that fails for a clean tree: mktemp hands back
|
|
154
|
+
# /var/folders/... while git resolves symlinks and returns /private/var/folders/... . The
|
|
155
|
+
# assertion then rejected its OWN correct fixture — a false positive on a safety check, which is
|
|
156
|
+
# the same defect class as a false negative and would have trained the next reader to delete it.
|
|
157
|
+
# `pwd -P` resolves the $TMP side; the other side is already physical because
|
|
158
|
+
# `rev-parse --absolute-git-dir` resolves symlinks itself. The raw "$TMP"/* branch is kept as a
|
|
159
|
+
# belt for platforms where it does not. (An earlier comment said "both sides normalized via
|
|
160
|
+
# pwd -P" — only one side uses it.)
|
|
161
|
+
_TMP_REAL="$(cd "$TMP" && pwd -P)"
|
|
162
|
+
# NAME THE ORDER HONESTLY: `git init` runs FIRST and this assertion runs immediately after, so it
|
|
163
|
+
# gates every subsequent write (commit, worktree add) — not the init itself. init is bounded
|
|
164
|
+
# separately by the unset list plus the explicit empty --template. An earlier comment claimed
|
|
165
|
+
# "before anything is written", which overstated it by one step.
|
|
166
|
+
_gitdir_ok() { # the fixture repo must resolve INSIDE $TMP before anything FURTHER is written
|
|
167
|
+
local r; r=$(git "${GIT_ISO[@]}" -C "$TMP/realrepo" rev-parse --absolute-git-dir 2>/dev/null) || return 1
|
|
168
|
+
case "$r" in
|
|
169
|
+
"$_TMP_REAL"/*|"$TMP"/*) return 0 ;;
|
|
170
|
+
*) echo " ↳ git-dir resolved OUTSIDE the fixture: $r" >&2; return 1 ;;
|
|
171
|
+
esac
|
|
172
|
+
}
|
|
173
|
+
mkdir -p "$TMP/emptytpl"
|
|
174
|
+
if git "${GIT_ISO[@]}" -C "$TMP/realrepo" init -q --template="$TMP/emptytpl" . >/dev/null 2>&1 \
|
|
175
|
+
&& _gitdir_ok \
|
|
176
|
+
&& git "${GIT_ISO[@]}" -C "$TMP/realrepo" commit -q --allow-empty -m init >/dev/null 2>&1 \
|
|
177
|
+
&& git "${GIT_ISO[@]}" -C "$TMP/realrepo" worktree add -q --detach "$TMP/realwt" >/dev/null 2>&1 \
|
|
178
|
+
&& [ -f "$TMP/realwt/.git" ]; then
|
|
179
|
+
wt_pair "L1-b real \`git worktree add\` (.git is a genuine gitdir pointer): scans for real" \
|
|
180
|
+
"$TMP/realwt" none # the REAL .git file is already in place; do not overwrite it
|
|
181
|
+
else
|
|
182
|
+
bad "L1-b git IS installed but the real-worktree fixture could not be built — the claim was testable and was not tested"
|
|
183
|
+
fi
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# ── L2 · ordinary checkout keeps working ─────────────────────────────────────────────
|
|
187
|
+
wt_pair "L2 ordinary checkout (.git is a DIR): scans for real — both legs" "$TMP/l2" dir
|
|
188
|
+
|
|
189
|
+
# ── L3 · package mode must STILL skip ────────────────────────────────────────────────
|
|
190
|
+
# The widening from -d to -e must not cost the legitimate skip. An installed npm package has no
|
|
191
|
+
# .git of either kind; making it fail there would fire on every consumer running `npm test`.
|
|
192
|
+
make_tree "$TMP/l3" none yes
|
|
193
|
+
out=$(run_tree "$TMP/l3"); rc=$(rc_tree "$TMP/l3")
|
|
194
|
+
if printf '%s' "$out" | grep -q 'SKIP package-coverage' && [ "$rc" -eq 0 ]; then
|
|
195
|
+
ok "L3 package mode (no .git): still skips, exit 0"
|
|
196
|
+
else
|
|
197
|
+
bad "L3 package mode (no .git): expected SKIP+0, got rc=$rc / $(printf '%s' "$out" | head -1)"
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
# ── L4 · a checkout with no manifest is UNMEASURED, not clean ────────────────────────
|
|
201
|
+
# EXPECTATION CORRECTED by cross-family review, 2026-07-31. The first draft asserted SKIP+0 here
|
|
202
|
+
# and would have ANCHORED a fail-open: inside a checkout, a missing package.json means the shipped
|
|
203
|
+
# file list cannot be read, which is "cannot measure", not "nothing to measure". An anchor that
|
|
204
|
+
# pins the wrong direction is worse than no anchor — it makes the hole look deliberate.
|
|
205
|
+
make_tree "$TMP/l4" dir yes omit
|
|
206
|
+
out=$(run_tree "$TMP/l4"); rc=$(rc_tree "$TMP/l4")
|
|
207
|
+
if [ "$rc" -eq 1 ] && printf '%s' "$out" | grep -q 'UNMEASURED, not clean'; then
|
|
208
|
+
ok "L4 .git present but no package.json: FAILS as unmeasured, not a clean skip"
|
|
209
|
+
else
|
|
210
|
+
bad "L4 no package.json: expected exit 1 (unmeasured), got rc=$rc / $(printf '%s' "$out" | head -1)"
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# ── L5/L6 · KNOWN PAIR ───────────────────────────────────────────────────────────────
|
|
214
|
+
# Same tree twice; the ONLY difference is whether files[] covers the referenced path.
|
|
215
|
+
# L5 known-POSITIVE: referenced, exists, not shipped -> must FAIL.
|
|
216
|
+
make_tree "$TMP/l5" dir no
|
|
217
|
+
out=$(run_tree "$TMP/l5"); rc=$(rc_tree "$TMP/l5")
|
|
218
|
+
if [ "$rc" -eq 1 ] && printf '%s' "$out" | grep -q 'scripts/helper.sh'; then
|
|
219
|
+
ok "L5 known-positive (referenced ∧ exists ∧ ¬shipped): FAIL, names the path"
|
|
220
|
+
else
|
|
221
|
+
bad "L5 known-positive: expected exit 1 naming scripts/helper.sh, got rc=$rc"
|
|
222
|
+
fi
|
|
223
|
+
|
|
224
|
+
# L6 known-NEGATIVE: identical, but the path is in files[] -> must PASS.
|
|
225
|
+
make_tree "$TMP/l6" dir yes
|
|
226
|
+
out=$(run_tree "$TMP/l6"); rc=$(rc_tree "$TMP/l6")
|
|
227
|
+
if [ "$rc" -eq 0 ] && printf '%s' "$out" | grep -q 'PASS package-coverage'; then
|
|
228
|
+
ok "L6 known-negative (same tree, path shipped): PASS"
|
|
229
|
+
else
|
|
230
|
+
bad "L6 known-negative: expected exit 0 PASS, got rc=$rc"
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
# ── L7 · the impossible-zero guard is not reachable by an empty files[] ──────────────
|
|
234
|
+
# A manifest with no shipped docs must report the extractor as broken, not print a pass.
|
|
235
|
+
mkdir -p "$TMP/l7/scripts"; cp "$SUBJECT" "$TMP/l7/scripts/"; mkdir -p "$TMP/l7/.git"
|
|
236
|
+
printf '{"files":[]}\n' > "$TMP/l7/package.json"
|
|
237
|
+
out=$(run_tree "$TMP/l7"); rc=$(rc_tree "$TMP/l7")
|
|
238
|
+
if [ "$rc" -eq 1 ] && printf '%s' "$out" | grep -q 'the check broke, it did not pass'; then
|
|
239
|
+
ok "L7 zero shipped docs: reported as broken extractor, not as a pass"
|
|
240
|
+
else
|
|
241
|
+
bad "L7 zero shipped docs: expected exit 1 'check broke', got rc=$rc"
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
echo
|
|
245
|
+
if [ "$skipped" -gt 0 ]; then
|
|
246
|
+
echo "package-coverage lanes: ${pass} passed, ${fail} failed, ${skipped} UNCALIBRATED (not verified here)"
|
|
247
|
+
else
|
|
248
|
+
echo "package-coverage lanes: ${pass} passed, ${fail} failed"
|
|
249
|
+
fi
|
|
250
|
+
[ "$fail" -eq 0 ] || exit 1
|