@chrono-meta/fh-gate 1.4.78 → 1.4.80

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.
@@ -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
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env bash
2
+ # test_pipe_verdict_guard_lanes.sh — known pairs for scripts/pipe_verdict_guard.sh
3
+ #
4
+ # WHY THIS FILE EXISTS, AND WHY IT IS WRITTEN BEFORE THE DETECTOR
5
+ # 2026-07-30 harvest #1: across a 5-round challenger run, three rounds contained a fix that
6
+ # reverted an earlier fix — and the rounds that wrote the lane BEFORE touching the code had
7
+ # zero such regressions. Convergence came from the ORDER, not the patch. So: lanes first.
8
+ #
9
+ # WHAT IS BEING GUARDED
10
+ # Reading a verdict from `$?`/`${PIPESTATUS[…]}` after a pipeline, which yields the status of
11
+ # the LAST stage. When the last stage is a display filter (`tail`, `head`, `cat`), that status
12
+ # is the filter's — a FAILING gate reads as exit 0. Degrade direction: toward PASS.
13
+ #
14
+ # R1 (deterministic, zero-FP): `${PIPESTATUS[...]}` under zsh. zsh spells it `$pipestatus[1]`
15
+ # and 1-indexes it; the bash array expands to the EMPTY STRING. Any verdict read from it is
16
+ # not merely wrong, it is absent. Measured 6× in this project's ad-hoc invocations.
17
+ # R2 (heuristic, narrowed): pipeline whose FINAL stage is a display filter, followed by a read
18
+ # of `$?`. Narrowed to display filters on purpose — see the FP lanes below.
19
+ #
20
+ # WHY A REPO-FILE LINTER IS THE WRONG SURFACE (measured 2026-07-31)
21
+ # The prescription on the session card was "add an S6 class to degrade_direction_scan.sh".
22
+ # Hand-verifying every `pipe + $?` hit in this repo's scripts returned 7 hits, 7 of them correct
23
+ # (the last stage WAS the command under test in all 7). True positives in shipped files: 0.
24
+ # All 6 measured recurrences were in interactively-composed commands, which no file scanner
25
+ # reads. Shipping S6 would have been a 0-true-positive probe — exactly the failure S5's own
26
+ # comment records ("100% FP trains dismissal of the one hit that will matter").
27
+
28
+ set -u
29
+ G="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/pipe_verdict_guard.sh"
30
+ pass=0; fail=0
31
+
32
+ # expect <label> <expected: HIT|CLEAN> <command-string>
33
+ expect() {
34
+ local label="$1" want="$2" cmd="$3" out got
35
+ out=$(printf '%s' "$cmd" | bash "$G" --stdin-raw 2>&1)
36
+ if printf '%s' "$out" | grep -q 'PIPE-VERDICT'; then got=HIT; else got=CLEAN; fi
37
+ if [ "$got" = "$want" ]; then
38
+ printf ' ✅ %-52s %s (expected %s)\n' "$label" "$got" "$want"; pass=$((pass+1))
39
+ else
40
+ printf ' ❌ %-52s %s (expected %s)\n' "$label" "$got" "$want"; fail=$((fail+1))
41
+ printf ' cmd: %s\n out: %s\n' "$cmd" "$out"
42
+ fi
43
+ }
44
+
45
+ echo "[pipe-verdict-guard] known pairs"
46
+ echo "-- R1: PIPESTATUS under zsh (deterministic) --"
47
+ # The exact shape emitted 6× in this project, including twice on 2026-07-31.
48
+ expect "R1 the measured shape" HIT 'bash x.sh | tail -5; echo "exit=${PIPESTATUS[0]}"'
49
+ expect "R1 any index" HIT 'a | b; rc=${PIPESTATUS[1]}'
50
+ expect "R1 inside a larger command" HIT 'cd /r && npm t | tail; E=${PIPESTATUS[0]}; echo $E'
51
+ # zsh's own spelling is correct here and must never be flagged.
52
+ expect "R1 zsh spelling is CLEAN" CLEAN 'a | b; rc=$pipestatus[1]'
53
+
54
+ echo "-- R2: display-filter final stage, then \$? --"
55
+ expect "R2 tail then \$?" HIT 'bash gate.sh | tail -20; echo "exit=$?"'
56
+ expect "R2 head then \$?" HIT 'make test | head -40; rc=$?'
57
+ expect "R2 cat then \$?" HIT 'run.sh | cat; if [ $? -ne 0 ]; then echo bad; fi'
58
+
59
+ echo "-- R2 false-positive lanes: the 7 shapes this repo actually ships --"
60
+ # Every one of these was hand-verified on 2026-07-31 as CORRECT: the final stage IS the command
61
+ # whose status is wanted. A detector that flags these is noise, and noise trains dismissal.
62
+ expect "FP grep -q is the test itself" CLEAN 'echo "$pos" | grep -qE "$re"; rc=$?'
63
+ expect "FP grep compiles the regex" CLEAN "printf '' | grep -E \"\$re\" >/dev/null 2>&1; rc=\$?"
64
+ # Path deliberately generic: naming a real unshipped script here would make this lane a shipped
65
+ # doc pointing at a file the package omits (caught by selfcheck's package-coverage rule).
66
+ expect "FP last stage is the script" CLEAN "printf '%s' \"\$S\" | bash some-filter.sh - ; echo EXIT:\$?"
67
+ expect "FP command substitution assign" CLEAN 'out=$(printf x | ( cd "$r" && bash hook 2>&1 )); rc=$?'
68
+ expect "FP no pipe at all" CLEAN 'bash gate.sh; echo "exit=$?"'
69
+ expect "FP || fallback, not a pipe" CLEAN 'stat -c %Y f || stat -f %m f || echo 0'
70
+ expect "FP pipefail set, explicit" CLEAN 'set -o pipefail; bash gate.sh | tail -5; rc=$?'
71
+
72
+ echo "-- adversarial (found by the Axis-2 pass on this guard, 2026-07-31) --"
73
+ # A: grep is line-oriented, so `.*` never spanned a newline and every MULTI-LINE command missed.
74
+ # This mattered more than the single-line case: the invocations that actually recur in this
75
+ # project are multi-line. Caught by attacking the guard, not by the happy-path lanes.
76
+ expect "A multi-line pipe then \$?" HIT 'bash gate.sh | tail -20
77
+ echo "exit=$?"'
78
+ expect "A multi-line, three statements" HIT 'cd /r
79
+ make test | head -40
80
+ rc=$?'
81
+ expect "A multi-line stays CLEAN if legit" CLEAN 'cd /r
82
+ echo x | grep -q y
83
+ rc=$?'
84
+ # B: zsh accepts `$PIPESTATUS[0]` without braces; the brace-anchored regex missed it.
85
+ expect "B PIPESTATUS without braces" HIT 'a | b; rc=$PIPESTATUS[0]'
86
+ expect "B braced form still caught" HIT 'a | b; rc=${PIPESTATUS[0]}'
87
+
88
+ echo "-- opt-out --"
89
+ expect "noqa suppresses" CLEAN 'bash g.sh | tail; rc=$? # noqa: pipe-verdict'
90
+
91
+ echo
92
+ if [ "$fail" -eq 0 ]; then
93
+ echo "[pipe-verdict-guard] ✅ all $pass known pairs hold"; exit 0
94
+ else
95
+ echo "[pipe-verdict-guard] ❌ $fail/$((pass+fail)) lanes failed"; exit 1
96
+ fi
@@ -143,7 +143,19 @@ if command -v script >/dev/null 2>&1; then
143
143
  # The inner shell writes its rc to a FILE. Parsing `script`'s own stdout fails: it emits ^D and
144
144
  # CRLF, and the first version of this lane read rc as empty and reported a defect that did not
145
145
  # exist — the target was fine, the instrument was not.
146
- script -q /dev/null bash -c "SIDECAR_POLL=1 timeout 12 bash '$SW' '$TD/p9.out' 3 -- cat >'$TD/p9.txt' 2>&1; echo \$? > '$TD/p9rc'" >/dev/null 2>&1 < /dev/null
146
+ # `script(1)` HAS TWO INCOMPATIBLE CLIs and this lane only ever spoke one of them. BSD/macOS takes
147
+ # `script -q <file> <cmd> [args...]`; util-linux (every Linux runner) takes `script -q -c "<cmd>"
148
+ # <file>`. Given the BSD form, util-linux treats `bash` and `-c` as stray operands, never runs the
149
+ # inner command, and writes no rc file — so the lane read rc='<unread>' and reported a defect in
150
+ # sidecar_wait that did not exist. Measured on the first CI run, 2026-07-31; it had been invisible
151
+ # because this suite had only ever executed on macOS. Same shape as the Hangul-range collation bug
152
+ # found in the same run: a TOOL INTERFACE that differs by platform, silently.
153
+ _P9CMD="SIDECAR_POLL=1 timeout 12 bash '$SW' '$TD/p9.out' 3 -- cat >'$TD/p9.txt' 2>&1; echo \$? > '$TD/p9rc'"
154
+ if script --version 2>&1 | grep -qi util-linux; then
155
+ script -q -c "$_P9CMD" /dev/null >/dev/null 2>&1 < /dev/null
156
+ else
157
+ script -q /dev/null bash -c "$_P9CMD" >/dev/null 2>&1 < /dev/null
158
+ fi
147
159
  rc9=$(tr -dc '0-9' < "$TD/p9rc" 2>/dev/null)
148
160
  if [ -n "$rc9" ] && [ "$rc9" != "124" ] && grep -q 'SIDECAR_VERDICT=' "$TD/p9.txt" 2>/dev/null; then
149
161
  ok "P9 the tty path runs and emits a verdict (UNCALIBRATED — see note; does NOT bind the branch)"
@@ -362,6 +362,15 @@ if [ -n "$EXEC_STAGED" ] && [ -z "$DOC_STAGED" ]; then
362
362
  echo ""
363
363
  fi
364
364
 
365
+ # ── Half-fix propagation (advisory — MARK, never block) ──────────────────────
366
+ # The operator's spec for this debt is explicit: 표시(차단 아님 — 정당한 복제도 있다).
367
+ # templates/ ships deliberate copies of scripts/, so blocking on duplication would block correct
368
+ # work; and a gate that fires when the author did the right thing is a gate that gets disabled.
369
+ # Failing to RUN is likewise never a finding: a missing/erroring scan is silent here, because this
370
+ # surface is a commit (reversible) — the fail-closed direction belongs to publish and delete.
371
+ HALFFIX="$REPO_ROOT/scripts/halffix_propagation_scan.sh"
372
+ [ -f "$HALFFIX" ] && bash "$HALFFIX" 2>&1 || true
373
+
365
374
  # ── Axis 1 — Regression Guard (always required) ──────────────────────────────
366
375
  echo "[Axis 1] Regression Guard..."
367
376
  GUARD="$REPO_ROOT/templates/regression_guard.sh"
@@ -608,12 +617,71 @@ if [ ! -f "$MANIFEST" ]; then
608
617
  echo " ❌ FAIL — tracks/_meta/edit_manifest.yaml not found"
609
618
  echo " Run /edit-manifest RECORD or create the file manually."
610
619
  FAILED=1
611
- elif grep -q "date: $TODAY" "$MANIFEST" 2>/dev/null; then
612
- echo " ✅ PASS (entry for $TODAY found in edit_manifest.yaml)"
613
620
  else
614
- echo " ❌ FAIL no entry dated $TODAY in edit_manifest.yaml"
615
- echo " Run /edit-manifest RECORD to log predicted impact for today's changes."
616
- FAILED=1
621
+ # PRESENCE **AND** VALIDITY, decided by the canonical loader rather than by grep.
622
+ #
623
+ # This axis had been a pure substring grep, so it never asked whether the file it gates is the
624
+ # thing its name promises. It was not: 12 of 147 entries were invalid YAML for months, silently,
625
+ # because every consumer reads it line-wise (this hook, activity_log.sh awk, sync-to-be.sh cp).
626
+ #
627
+ # The FIRST fix for that was itself defective, and cross-family review measured all three holes —
628
+ # which is why the verdict now travels on the EXIT CODE and one Python pass owns every predicate:
629
+ # - `$(python3 ...) || echo unchecked` SWALLOWED the exit code, so any interpreter failure, on a
630
+ # manifest with a confirmed syntax error, degraded to PASS. A checker whose crash means "fine"
631
+ # is not a checker.
632
+ # - it discarded the parse RESULT, so a duplicate `date:` key (YAML is last-wins) could delete
633
+ # today's entry while still reporting valid, and a list item drifted outside its parent list
634
+ # still passed — the exact class the same commit had just repaired, left undetected.
635
+ # - the grep was substring-based: a COMMENTED `# date: <today>` counted as an entry, while a
636
+ # quoted `date: "<today>"` or extra spacing did not. Fail-open and over-block in one predicate.
637
+ # Free-form stdout is never the verdict channel either — a python3 shim printing one banner line
638
+ # would otherwise fail a healthy manifest.
639
+ MANIFEST_MSG=$(python3 - "$MANIFEST" "$TODAY" <<'PYEOF'
640
+ import sys
641
+ try:
642
+ import yaml
643
+ except Exception:
644
+ sys.exit(3) # 3 = checker unavailable (env gap, not a defect in the change)
645
+ try:
646
+ doc = yaml.safe_load(open(sys.argv[1]))
647
+ except Exception as e:
648
+ mk = getattr(e, "problem_mark", None)
649
+ print("invalid YAML at line %s" % (mk.line + 1 if mk else "?")); sys.exit(1)
650
+ if not isinstance(doc, list):
651
+ print("top level is %s, expected a list of entries" % type(doc).__name__); sys.exit(1)
652
+ for i, entry in enumerate(doc):
653
+ if not isinstance(entry, dict):
654
+ print("entry #%d is a %s, not a mapping — a list item may have drifted outside its parent"
655
+ % (i, type(entry).__name__)); sys.exit(1)
656
+ def norm(v):
657
+ return v.isoformat() if hasattr(v, "isoformat") else str(v).strip()
658
+ if not any(norm(e.get("date")) == sys.argv[2] for e in doc):
659
+ sys.exit(2) # 2 = parses fine, but no entry for today
660
+ sys.exit(0)
661
+ PYEOF
662
+ )
663
+ MANIFEST_RC=$?
664
+ case "$MANIFEST_RC" in
665
+ 0) echo " ✅ PASS (entry for $TODAY present; manifest parses and its shape is intact)" ;;
666
+ 3) # Degrade-to-advisory on purpose: a commit is a REVERSIBLE surface (CLAUDE.md
667
+ # §Irreversibility Surface-Class Degrade Invariant), and a missing PyYAML is an environment
668
+ # gap rather than a defect in the change being committed. Loud, never silent.
669
+ if grep -q "date: $TODAY" "$MANIFEST" 2>/dev/null; then
670
+ echo " ✅ PASS (entry for $TODAY found by fallback grep; validity UNCHECKED — PyYAML unavailable)"
671
+ else
672
+ echo " ❌ FAIL — no entry dated $TODAY in edit_manifest.yaml (fallback grep; PyYAML unavailable)"
673
+ echo " Run /edit-manifest RECORD to log predicted impact for today's changes."
674
+ FAILED=1
675
+ fi ;;
676
+ 2) echo " ❌ FAIL — manifest is valid YAML but has no entry dated $TODAY"
677
+ echo " Run /edit-manifest RECORD to log predicted impact for today's changes."
678
+ FAILED=1 ;;
679
+ *) echo " ❌ FAIL — edit_manifest.yaml did not validate: ${MANIFEST_MSG:-checker exited $MANIFEST_RC}"
680
+ echo " Usual causes: an unquoted value containing ': ' (quote it), or a list item indented"
681
+ echo " outside its parent list. Check:"
682
+ echo " python3 -c \"import yaml;yaml.safe_load(open('tracks/_meta/edit_manifest.yaml'))\""
683
+ FAILED=1 ;;
684
+ esac
617
685
  fi
618
686
 
619
687
  # Universal guards (defined above) — confidentiality/privacy boundary, every commit.