#!/usr/bin/env bash # check-lens-sensors.sh - pin the Lens sensor wiring that must not drift. # # Why this exists: `make verify` echoes VERIFY_SENSORS and runs each name from # the app's sensor loop. A sensor missing from that list is never run, and a # sensor target that can fall back (or skip the tool when it is absent) exits 0 # when it did not check anything - in both cases the pass reports a check it # did not run. AGENTS.md: "A gate that reports a check it did not run is worse # than no gate." # # This script is static and grep-only: no mise, no elixir, no network, no # database. It pins load-bearing text (the VERIFY_SENSORS line and the sobelow # recipe) and the rationale rule for per-finding skips. Renaming the target or # the variable means updating this script in the same commit - that is the # point: the wiring stays a reviewed decision rather than an accident. # # Run standalone, or via root `make check`. set -euo pipefail root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) app_makefile="$root/app/Makefile" skips="$root/app/.sobelow-skips" status=0 # Verified 2026-10-28 against sobelow 0.17.0: # `cd app && mix sobelow` with 3 findings -> exit 0 (findings alone are NOT # fatal); `cd app && mix sobelow --exit` -> exit 1. `--exit` defaults to the # `low` confidence threshold, which is what we want: every finding counts. SOBELOW_FAIL_FLAG='--exit' fail() { echo "FAIL: $*" >&2; status=1; } # --- T1: sobelow is wired and listed first in VERIFY_SENSORS --------------- # `make verify` echoes this list, so a missing name means the pass reports a # sensor it never runs. Cheapest-first ordering is part of the contract. check_verify_sensors() { local line sensors i line=$(grep -E '^VERIFY_SENSORS[[:space:]]*:=' "$app_makefile" || true) if [ -z "$line" ]; then fail "app/Makefile has no VERIFY_SENSORS := line" return fi sensors=$(printf '%s' "${line#*:=}" | tr -s '[:space:]' ' ') local -a order read -r -a order <<<"$sensors" idx() { local n for n in "${!order[@]}"; do if [ "${order[$n]}" = "$1" ]; then echo "$n" return fi done echo 999 } for sensor in sobelow coverage crap; do [ "$(idx "$sensor")" -ne 999 ] || fail "VERIFY_SENSORS does not list '$sensor' (line: $line)" done [ "$(idx sobelow)" -lt "$(idx coverage)" ] || fail "VERIFY_SENSORS puts coverage before sobelow (cheapest first)" [ "$(idx sobelow)" -lt "$(idx crap)" ] || fail "VERIFY_SENSORS puts crap before sobelow (cheapest first)" } # --- T2: the sobelow target fails loudly and has no skip/fallback path ----- # The stickies regression: the user explicitly removed a "not installed, # skipping" fallback because a silently-skipped check is a lying sensor. check_sobelow_recipe() { local recipe recipe=$(awk ' /^sobelow:/ { found = 1; next } found && /^\t/ { print; next } found { exit } ' "$app_makefile") if [ -z "$recipe" ]; then fail "app/Makefile has no sobelow: target with a recipe" return fi case "$recipe" in *"mix sobelow"*) ;; *) fail "sobelow recipe does not invoke 'mix sobelow': $recipe" ;; esac case "$recipe" in *"$SOBELOW_FAIL_FLAG"*) ;; *) fail "sobelow recipe is missing $SOBELOW_FAIL_FLAG, so findings would exit 0: $recipe" ;; esac local banned for banned in '||' 'command -v sobelow' 'skipping' '2>/dev/null'; do case "$recipe" in *"$banned"*) fail "sobelow recipe contains a fallback/skip path ('$banned'): $recipe" ;; esac done } # --- T3: every per-finding skip carries a written rationale ---------------- # Sobelow 0.17 keeps per-finding skips as fingerprinted lines in # `app/.sobelow-skips` (enabled with `--skip`); `.sobelow-conf` holds only # options. A skip entry without a reason is a defect: the next reader cannot # tell a false positive from a debt someone hid. Comments are preserved by # sobelow's skip loader (they sort first and never match a fingerprint). check_skips() { [ -f "$skips" ] || return 0 local prev="" line n=0 while IFS= read -r line || [ -n "$line" ]; do n=$((n + 1)) case "$line" in "" | "#"*) prev="$line" continue ;; esac case "$prev" in "#"*) ;; *) fail "app/.sobelow-skips:$n: skip entry has no rationale comment on the line above: $line" ;; esac prev="$line" done <"$skips" } check_verify_sensors check_sobelow_recipe check_skips if [ $status -eq 0 ]; then echo "lens sensors ok: sobelow wired first in VERIFY_SENSORS, fail-loud recipe, app skips rationalised" else echo >&2 "fix the Lens sensor wiring/skips above, then re-run." fi exit $status