@chrono-meta/fh-gate 1.4.97 → 1.4.98
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/CATALOG.md +19 -0
- package/CHEATSHEET.md +9 -1
- package/CLAUDE.md +28 -2
- package/README.ja.md +10 -0
- package/README.ko.md +10 -0
- package/README.md +11 -0
- package/README.zh.md +10 -0
- package/docs/pillars.svg +3 -7
- package/knowledge/shared/harness-core/fh_ecosystem_positioning.md +2 -0
- package/knowledge/shared/harness-core/fh_global_positioning_and_distribution_roadmap.md +136 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +20 -0
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +215 -2
- package/knowledge/shared/harness-core/ship_readiness_gate.md +112 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +55 -0
- package/package.json +5 -1
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/skills/ko-tech-writer/SKILL.md +63 -12
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +65 -0
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +30 -0
- package/scripts/degrade_direction_scan.sh +10 -1
- package/scripts/digest_landing_check.sh +20 -4
- package/scripts/fh_node_check.sh +60 -1
- package/scripts/fh_session_load.sh +22 -2
- package/scripts/frontier_digest_autopilot.sh +229 -0
- package/scripts/lane_runner_check.sh +237 -25
- package/scripts/package_coverage_check.sh +17 -0
- package/scripts/postinstall_notice.js +34 -0
- package/scripts/selfcheck.sh +111 -5
- package/scripts/test_degrade_scan_shell_probes.sh +75 -0
- package/scripts/test_field_canon_lanes.sh +29 -5
- package/scripts/test_lane_runner_lanes.sh +221 -0
- package/scripts/test_stale_clone_guard_lanes.sh +21 -7
- package/scripts/test_version_lockstep_lanes.sh +62 -0
- package/scripts/version_lockstep_check.sh +143 -1
- package/templates/.git-hooks/pre-commit +22 -1
- package/templates/degrade_direction_scan.sh +10 -1
|
@@ -74,6 +74,68 @@ T=$(_fixture he4 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89); printf '{"name":"fh-meta"}
|
|
|
74
74
|
_expect "HE-4 manifest with no version → exit 2, NOT 0" 2 "$T"
|
|
75
75
|
_says "HE-4 → says which file carries none" "carries no version string" 1
|
|
76
76
|
|
|
77
|
+
# ── Self-restatement — found→extend into this lens (2026-08-14) ──────────────────────────────
|
|
78
|
+
# RS-KN pins the exact false positive the naive first draft threw against its OWN calibration
|
|
79
|
+
# target (scripts/lane_runner_check.sh): a loose \D{0,12} gap matched across an unrelated
|
|
80
|
+
# exit-code legend entry ("declared DEBT · 1 =") and across a before→after transition narration
|
|
81
|
+
# ("DEBT 2 → 0"). Both shapes are known-negatives — they describe history/enumeration, not a
|
|
82
|
+
# live contradiction — and must stay silent.
|
|
83
|
+
|
|
84
|
+
T=$(_fixture rs1 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89)
|
|
85
|
+
mkdir -p "$T/scripts"
|
|
86
|
+
cat > "$T/scripts/some_lane.sh" <<'EOF'
|
|
87
|
+
#!/usr/bin/env bash
|
|
88
|
+
# Exit: 0 = every suite is WIRED, EXEMPT, or declared DEBT · 1 = an undeclared suite has no runner
|
|
89
|
+
# THE TWO THAT WERE HERE ARE RE-WIRED (2026-08-14) — DEBT 2 → 0.
|
|
90
|
+
# WHAT `DEBT: 0` DOES NOT MEAN — read before quoting the number anywhere.
|
|
91
|
+
DEBT=()
|
|
92
|
+
EOF
|
|
93
|
+
_expect "RS-KN historical DEBT narration (2→0, exit-code legend) → exit 0" 0 "$T"
|
|
94
|
+
_says "RS-KN → does not fire on the transition/legend shapes" "self-restatement drift" 0
|
|
95
|
+
|
|
96
|
+
T=$(_fixture rs2 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89)
|
|
97
|
+
mkdir -p "$T/scripts"
|
|
98
|
+
cat > "$T/scripts/broken_lane.sh" <<'EOF'
|
|
99
|
+
#!/usr/bin/env bash
|
|
100
|
+
# DEBT: 12 unwired suites remain, tracked below.
|
|
101
|
+
# ... (later, uncorrected after a partial fix) ...
|
|
102
|
+
# still DEBT 9 by last count.
|
|
103
|
+
DEBT=()
|
|
104
|
+
EOF
|
|
105
|
+
_expect "RS-KP shell comment restates DEBT with two different numbers → exit 0 (advisory)" 0 "$T"
|
|
106
|
+
_says "RS-KP → names the file and the disagreeing values" \
|
|
107
|
+
'broken_lane.sh :: comments state DEBT as \[9, 12\]' 1
|
|
108
|
+
|
|
109
|
+
T=$(_fixture rs3 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89)
|
|
110
|
+
mkdir -p "$T/plugins/fh-commons/skills/fake-skill"
|
|
111
|
+
cat > "$T/plugins/fh-commons/skills/fake-skill/SKILL.md" <<'EOF'
|
|
112
|
+
---
|
|
113
|
+
name: fake-skill
|
|
114
|
+
description: uses v1.4.95 of the reference build
|
|
115
|
+
---
|
|
116
|
+
# fake-skill
|
|
117
|
+
|
|
118
|
+
Body text never restates a version number, so there is nothing to cross-check against.
|
|
119
|
+
EOF
|
|
120
|
+
_expect "RS-KN2 SKILL.md frontmatter version, body silent → exit 0, no drift" 0 "$T"
|
|
121
|
+
_says "RS-KN2 → does not fire when body has zero version mentions" "self-restatement drift" 0
|
|
122
|
+
|
|
123
|
+
T=$(_fixture rs4 1.4.89 1.4.89 1.4.89 1.4.89 1.4.89)
|
|
124
|
+
mkdir -p "$T/plugins/fh-commons/skills/fake-skill2"
|
|
125
|
+
cat > "$T/plugins/fh-commons/skills/fake-skill2/SKILL.md" <<'EOF'
|
|
126
|
+
---
|
|
127
|
+
name: fake-skill2
|
|
128
|
+
description: ships as part of v1.4.95
|
|
129
|
+
---
|
|
130
|
+
# fake-skill2
|
|
131
|
+
|
|
132
|
+
Elsewhere in this doc we note the shipped build is v1.4.92, which is the value users should
|
|
133
|
+
actually expect to see when they check their installed version.
|
|
134
|
+
EOF
|
|
135
|
+
_expect "RS-KP2 SKILL.md frontmatter v1.4.95 vs body v1.4.92 → exit 0 (advisory)" 0 "$T"
|
|
136
|
+
_says "RS-KP2 → names the file and the drifted version" \
|
|
137
|
+
'fake-skill2/SKILL.md :: frontmatter states v{1.4.95}' 1
|
|
138
|
+
|
|
77
139
|
echo
|
|
78
140
|
echo "──────────────────────────────────────────────"
|
|
79
141
|
if [ "$FAIL" -gt 0 ]; then
|
|
@@ -22,7 +22,7 @@ ROOT="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
|
22
22
|
command -v python3 >/dev/null 2>&1 || { echo "LOCKSTEP: HARNESS-ERROR — python3 unavailable"; exit 2; }
|
|
23
23
|
|
|
24
24
|
python3 - "$ROOT" <<'PY'
|
|
25
|
-
import json, sys, glob, os
|
|
25
|
+
import json, sys, glob, os, re
|
|
26
26
|
root = sys.argv[1]
|
|
27
27
|
pkg_path = os.path.join(root, 'package.json')
|
|
28
28
|
try:
|
|
@@ -69,6 +69,39 @@ for p in targets:
|
|
|
69
69
|
if got != want:
|
|
70
70
|
drift.append(f" {rel} :: {label} = {got} (package.json = {want})")
|
|
71
71
|
|
|
72
|
+
# ── CHANGELOG is a shipped version surface too, and it was outside this check ─────────────────
|
|
73
|
+
# Measured 2026-08-13, by a peer session, AFTER 1.4.97 was already published: the four JSON
|
|
74
|
+
# manifests all read 1.4.97 and plugins/fh-meta/CHANGELOG.md's newest entry was still [1.4.96].
|
|
75
|
+
# So the tarball a consumer downloads carries a changelog with no entry for the version they just
|
|
76
|
+
# installed — six merged PRs invisible on the record surface. This check reported PASS while that
|
|
77
|
+
# was true, because its target list was "manifests carrying a JSON version field" rather than
|
|
78
|
+
# "surfaces that state which version this is".
|
|
79
|
+
# ★ That is the half-fix propagation boundary in its purest form: the version was updated
|
|
80
|
+
# everywhere the CHECK looked, which is not the same set as everywhere it MATTERS.
|
|
81
|
+
# Non-blocking is deliberate and narrow: a missing entry is a documentation gap, not a broken
|
|
82
|
+
# package, and turning a publish red on prose would train the override this repo has already
|
|
83
|
+
# measured people reaching for. It is LOUD, it names the file, and it is impossible to miss in the
|
|
84
|
+
# publish output — which is what the JSON drift arms could not have been, since those genuinely
|
|
85
|
+
# break installs.
|
|
86
|
+
CHANGELOGS = sorted(glob.glob(os.path.join(root, 'plugins', '*', 'CHANGELOG.md')))
|
|
87
|
+
for p in CHANGELOGS:
|
|
88
|
+
rel = os.path.relpath(p, root)
|
|
89
|
+
try:
|
|
90
|
+
txt = open(p, encoding='utf-8', errors='replace').read()
|
|
91
|
+
except OSError as e:
|
|
92
|
+
print(f"LOCKSTEP: HARNESS-ERROR — cannot read {rel}: {e}")
|
|
93
|
+
sys.exit(2)
|
|
94
|
+
# The heading form this repo uses: `### [x.y.z] — DATE`. Absence of ANY such heading means the
|
|
95
|
+
# extractor stopped matching the file's real shape — report that rather than a clean run.
|
|
96
|
+
heads = re.findall(r'^#+\s*\[(\d+\.\d+\.\d+)\]', txt, re.M)
|
|
97
|
+
if not heads:
|
|
98
|
+
print(f"LOCKSTEP: HARNESS-ERROR — {rel} has no `[x.y.z]` heading; the extractor is blind, "
|
|
99
|
+
f"which is not the same as the changelog being current")
|
|
100
|
+
sys.exit(2)
|
|
101
|
+
if want not in heads:
|
|
102
|
+
print(f"LOCKSTEP: ⚠️ {rel} has no entry for {want} (newest is {heads[0]}) — the published "
|
|
103
|
+
f"tarball would carry a changelog that does not mention the version it ships")
|
|
104
|
+
|
|
72
105
|
if drift:
|
|
73
106
|
print(f"LOCKSTEP: DRIFT — {len(drift)} of {checked} shipped version string(s) do not match package.json")
|
|
74
107
|
print("\n".join(drift))
|
|
@@ -76,5 +109,114 @@ if drift:
|
|
|
76
109
|
print("so a stale entry ships as 'already installed' on the runtime that cannot report it.")
|
|
77
110
|
sys.exit(1)
|
|
78
111
|
|
|
112
|
+
# ── Self-restatement — a file that states its own count/version twice can drift from itself ──
|
|
113
|
+
# Measured 2026-08-13 (peer session, reship axis): N=2 on two DIFFERENT surfaces, both real —
|
|
114
|
+
# (a) a memory file's YAML frontmatter `description:` stated an npm version differently from
|
|
115
|
+
# its own body (1.4.95 vs 1.4.92 — a lockstep drift, just in prose instead of JSON)
|
|
116
|
+
# (b) lane_runner_check.sh's own header comment stated "11/8" while a later comment said the
|
|
117
|
+
# DEBT count was 12 — caught by cross-family review, not by re-reading (see that file's own
|
|
118
|
+
# header for the incident write-up; it has since self-corrected into a historical note).
|
|
119
|
+
# «타표면 재발 = 기계화 의무» (recurrence on a second, different surface obligates mechanizing) —
|
|
120
|
+
# found→extend into THIS lens rather than a new scanner: same idiom (extract candidate values,
|
|
121
|
+
# compare, collect drift, report), new target surfaces.
|
|
122
|
+
#
|
|
123
|
+
# 🟥 (a) IS NOT CLOSED BY THIS ARM, AND THAT IS DELIBERATE — cross-family review (2026-08-14)
|
|
124
|
+
# caught an earlier draft of this comment reading as though citing incident (a) meant this scan
|
|
125
|
+
# reaches it. It does not: the memory store lives under the operator's home directory
|
|
126
|
+
# (~/.claude/projects/.../memory/), outside this repo's `root` entirely, so the candidate glob
|
|
127
|
+
# below structurally cannot see it — this script runs at `npm publish` time (package.json
|
|
128
|
+
# prepublishOnly), where only the git-tracked repo exists (CI has no access to a contributor's
|
|
129
|
+
# local home directory, and different operators have different paths). Incident (a) is the
|
|
130
|
+
# MOTIVATING PRECEDENT for the general pattern, not a case this specific scope closes. Closing it
|
|
131
|
+
# for real needs a separate, local-only check over the memory store — a different surface with
|
|
132
|
+
# different constraints, not a widening of this one.
|
|
133
|
+
#
|
|
134
|
+
# Advisory (like the CHANGELOG check above), same reasoning: a stale self-count is a documentation
|
|
135
|
+
# defect, not a broken package, and turning publish red on prose trains the override this repo has
|
|
136
|
+
# already measured people reaching for.
|
|
137
|
+
#
|
|
138
|
+
# Scope, deliberately narrow — the two false-positive traps that would have made this noise instead
|
|
139
|
+
# of signal, both avoided on purpose:
|
|
140
|
+
# 1. tracks/**/*.md and this repo's session cards are EXCLUDED. Those are running logs that
|
|
141
|
+
# legitimately cite dozens of historical version numbers across dated sessions — scanning them
|
|
142
|
+
# for "two differing v-numbers in one file" would fire on nearly every one. The mission here is
|
|
143
|
+
# "does a single-snapshot doc contradict itself", not "every version ever mentioned".
|
|
144
|
+
# 2. The DEBT counter only fires on 2+ DISTINCT values for the SAME literal label ("DEBT") within
|
|
145
|
+
# comment lines of one file — not on any two numbers that happen to appear near each other. A
|
|
146
|
+
# one-off historical narration ("this said 11/8 until it was fixed") does not by itself carry
|
|
147
|
+
# two live DEBT-labeled claims, so it does not false-positive as an active contradiction.
|
|
148
|
+
def _self_restate_md():
|
|
149
|
+
hits = []
|
|
150
|
+
candidates = [os.path.join(root, 'CLAUDE.md'), os.path.join(root, 'AGENTS.md')]
|
|
151
|
+
candidates += sorted(glob.glob(os.path.join(root, 'plugins', '*', 'skills', '*', 'SKILL.md')))
|
|
152
|
+
# v-prefix optional — cross-family review found the real incident that motivated this arm
|
|
153
|
+
# (a memory file's frontmatter `latest v1.4.97` vs body `latest **1.4.97**`) does not carry the
|
|
154
|
+
# prefix on the body side, so the mandatory-v form never fires on the actual shape it exists
|
|
155
|
+
# to catch. Bare N.N.N is noisier (could match an unrelated dependency version), but the
|
|
156
|
+
# candidate list above is narrow enough (CLAUDE.md/AGENTS.md/SKILL.md only) that this stays
|
|
157
|
+
# advisory-tolerable rather than explosive.
|
|
158
|
+
VERPAT = re.compile(r'\bv?(\d+\.\d+\.\d+)\b')
|
|
159
|
+
for p in candidates:
|
|
160
|
+
if not os.path.exists(p):
|
|
161
|
+
continue
|
|
162
|
+
try:
|
|
163
|
+
txt = open(p, encoding='utf-8', errors='replace').read()
|
|
164
|
+
except OSError:
|
|
165
|
+
continue
|
|
166
|
+
m = re.match(r'^---\n(.*?)\n---\n(.*)$', txt, re.S)
|
|
167
|
+
if not m:
|
|
168
|
+
continue
|
|
169
|
+
fm, body = m.group(1), m.group(2)
|
|
170
|
+
fm_vers = set(VERPAT.findall(fm))
|
|
171
|
+
body_vers = set(VERPAT.findall(body))
|
|
172
|
+
drifted = fm_vers - body_vers
|
|
173
|
+
if fm_vers and body_vers and drifted:
|
|
174
|
+
rel = os.path.relpath(p, root)
|
|
175
|
+
hits.append(f" {rel} :: frontmatter states v{{{','.join(sorted(drifted))}}} not "
|
|
176
|
+
f"found anywhere in body (body has v{{{','.join(sorted(body_vers))}}})")
|
|
177
|
+
return hits
|
|
178
|
+
|
|
179
|
+
def _self_restate_sh():
|
|
180
|
+
hits = []
|
|
181
|
+
# Tight: DEBT immediately followed by (optional :/=, whitespace) a number — not "any digit
|
|
182
|
+
# within 12 chars" (that matched an unrelated exit-code legend entry across a bullet, «DEBT ·
|
|
183
|
+
# 1»). A number immediately followed by an arrow (→ / ->) is a before→after transition
|
|
184
|
+
# narration ("DEBT 2 → 0"), not a live claim about the current count — excluded on purpose,
|
|
185
|
+
# calibrated against this repo's own lane_runner_check.sh (known-negative: 3 raw matches with
|
|
186
|
+
# the loose form, 1 real match with this form — verified by hand, 2026-08-14).
|
|
187
|
+
DEBTPAT = re.compile(r'\bDEBT\b\s*[:=]?\s*(\d+)\b(?!\s*(?:→|->))')
|
|
188
|
+
# test_*.sh / *_lanes.sh are excluded — measured 2026-08-14 on this check's own test file:
|
|
189
|
+
# a known-pair fixture legitimately embeds two differing DEBT numbers as heredoc TEST DATA
|
|
190
|
+
# (proving the detector can tell them apart), and that heredoc text is also literal source in
|
|
191
|
+
# the test file itself. Those are fixtures, not a claim about the test file's own state — same
|
|
192
|
+
# distinction the `suites` glob elsewhere in this repo already draws between subject scripts
|
|
193
|
+
# and the test scripts that exercise them.
|
|
194
|
+
for p in (sorted(f for f in glob.glob(os.path.join(root, 'scripts', '*.sh'))
|
|
195
|
+
if not re.match(r'^(test_.*|.*_lanes)\.sh$', os.path.basename(f)))
|
|
196
|
+
+ sorted(f for f in glob.glob(os.path.join(root, 'templates', '*.sh'))
|
|
197
|
+
if not re.match(r'^(test_.*|.*_lanes)\.sh$', os.path.basename(f)))):
|
|
198
|
+
try:
|
|
199
|
+
txt = open(p, encoding='utf-8', errors='replace').read()
|
|
200
|
+
except OSError:
|
|
201
|
+
continue
|
|
202
|
+
# Comment lines only — a live `DEBT=()` array literal is code computing the fact, not a
|
|
203
|
+
# restated claim about it, and must not be treated as a second (possibly stale) copy.
|
|
204
|
+
vals = set()
|
|
205
|
+
for ln in txt.splitlines():
|
|
206
|
+
if not re.match(r'^\s*#', ln):
|
|
207
|
+
continue
|
|
208
|
+
vals |= {int(m) for m in DEBTPAT.findall(ln)}
|
|
209
|
+
if len(vals) > 1:
|
|
210
|
+
rel = os.path.relpath(p, root)
|
|
211
|
+
hits.append(f" {rel} :: comments state DEBT as {sorted(vals)} — pick one number, "
|
|
212
|
+
f"or point the stale comment at the live count instead of a literal")
|
|
213
|
+
return hits
|
|
214
|
+
|
|
215
|
+
restate_hits = _self_restate_md() + _self_restate_sh()
|
|
216
|
+
if restate_hits:
|
|
217
|
+
print(f"LOCKSTEP: ⚠️ self-restatement drift — {len(restate_hits)} file(s) state their own "
|
|
218
|
+
f"version/count more than once and the copies disagree:")
|
|
219
|
+
print("\n".join(restate_hits))
|
|
220
|
+
|
|
79
221
|
print(f"LOCKSTEP: PASS — {checked} shipped version string(s) all at {want}")
|
|
80
222
|
PY
|
|
@@ -1072,7 +1072,28 @@ if [ -n "$LOADBEARING" ]; then
|
|
|
1072
1072
|
echo "[Gate] load-bearing change — degrade lint (advisory) + cross-family acknowledgment..."
|
|
1073
1073
|
DDSCAN="$REPO_ROOT/scripts/degrade_direction_scan.sh"
|
|
1074
1074
|
if [ -f "$DDSCAN" ]; then
|
|
1075
|
-
|
|
1075
|
+
# SCOPE THE SCAN TO THE STAGED FILES. This used to call `bash "$DDSCAN"` with NO ARGUMENTS,
|
|
1076
|
+
# which makes the scanner walk the WHOLE REPOSITORY recursively — and then the very next pipe
|
|
1077
|
+
# threw almost all of it away, keeping only the lines naming files we already had in hand.
|
|
1078
|
+
# Measured on this repo 2026-08-13: whole-repo 42s / 260 lines · one staged file 0s / 2 lines.
|
|
1079
|
+
# A sibling harness measured the same call under load at 2m07s inside a commit that took
|
|
1080
|
+
# 7m54s–9m40s, one of which hit a 10-minute tool ceiling and could not complete at all.
|
|
1081
|
+
# ★ The structure was inverted: it held the target list, scanned everything, then filtered.
|
|
1082
|
+
# WHY THIS IS WORSE THAN A SLOW CHECK: this lane declares itself ADVISORY (see the doctrine line
|
|
1083
|
+
# above — FP-tolerant, never a solo block). So a lane that cannot block was consuming most of
|
|
1084
|
+
# every load-bearing commit. That trains `--no-verify`, and the SAME hook carries the
|
|
1085
|
+
# Destructive-Op gate — one learned bypass disarms an irreversible-surface gate too. CLAUDE.md
|
|
1086
|
+
# warns repeatedly that over-blocking trains the override; over-DELAYING gets there just as well.
|
|
1087
|
+
# Read into an array rather than relying on word-splitting: $LOADBEARING is newline-separated and
|
|
1088
|
+
# an unquoted expansion would also split on spaces. `while read` and not `mapfile`, because
|
|
1089
|
+
# mapfile does not exist on bash 3.2 (stock macOS), which this hook still has to run on.
|
|
1090
|
+
_dd_targets=()
|
|
1091
|
+
while IFS= read -r _p; do
|
|
1092
|
+
[ -n "$_p" ] && _dd_targets+=("$_p")
|
|
1093
|
+
done <<< "$LOADBEARING"
|
|
1094
|
+
# The grep filter is KEPT even though the scan is now scoped: the scanner emits summary lines
|
|
1095
|
+
# ("degrade-scan: N smell(s) …") that name no file, and those must not read as findings.
|
|
1096
|
+
_dd=$(bash "$DDSCAN" "${_dd_targets[@]}" 2>/dev/null \
|
|
1076
1097
|
| grep -Ff <(printf '%s\n' "$LOADBEARING") 2>/dev/null | head -5 || true)
|
|
1077
1098
|
if [ -n "$_dd" ]; then
|
|
1078
1099
|
echo " ⚠️ degrade-direction smell(s) in the staged load-bearing files (advisory):"
|
|
@@ -500,11 +500,20 @@ for f in "${FILES[@]}"; do
|
|
|
500
500
|
# `tok in text` (paid⊂prepaid) when the variables aren't named verdict/state (M#4, steel-quench).
|
|
501
501
|
# C2 closes that: simple var-in-var (not a collection literal / range / for) = a likely
|
|
502
502
|
# containment check that should be exact/word-boundary if it grounds a verdict. Higher noise; advisory.
|
|
503
|
+
# 2026-08-14: pinned an ALL-CAPS-constant exclusion here that Probe C already carries a few lines
|
|
504
|
+
# up (`in [A-Z_]+\b`) — C2 had drifted out of sync with it. Measured on this repo's own scripts/:
|
|
505
|
+
# 6 raw C2 hits, all `if VAR in {seen,by_name,accepted,packed,PLACEHOLDERS}:` — genuine collection
|
|
506
|
+
# membership, not the substring-containment smell this probe exists to catch. Do NOT also add
|
|
507
|
+
# lowercase collection-name heuristics (`seen`/`by_name`/...) here — that class still needs a
|
|
508
|
+
# human to read the assignment and confirm it is a set/dict/list, and a naming guess would just
|
|
509
|
+
# move the false-negative risk instead of removing it. This narrows one already-established class,
|
|
510
|
+
# it does not invent a new one.
|
|
503
511
|
while IFS= read -r m; do
|
|
504
512
|
emit "$f" "${m%%:*}" "C2:substring-boolean" "bare 'X in Y' in if/return/assert — if this grounds a presence/verdict check, use exact/word-boundary match, not containment"
|
|
505
513
|
done < <(grep -nE '^[[:space:]]*(if|elif|return|assert|while)[[:space:]]+[A-Za-z_][A-Za-z0-9_]*[[:space:]]+in[[:space:]]+[A-Za-z_][A-Za-z0-9_.]*[[:space:]]*[:)]?[[:space:]]*$' "$f" 2>/dev/null \
|
|
506
514
|
| grep -vE '\bfor\b|in range|in enumerate|not in' \
|
|
507
|
-
| grep -vE '\b(verdict|present|ground|state|match|expected)\w*\b'
|
|
515
|
+
| grep -vE '\b(verdict|present|ground|state|match|expected)\w*\b' \
|
|
516
|
+
| grep -vE 'in [A-Z_]+[[:space:]]*[:)]?[[:space:]]*$')
|
|
508
517
|
|
|
509
518
|
# Probe E — negated-falsy guard returning permissive (dominance-benchmark round-2 f2 class): an error
|
|
510
519
|
# SENTINEL (None / {} / "" / []) is falsy, so `if not X: return <PASS>` treats "the check errored / never
|