#!/usr/bin/env bash # ratchet.sh - enforce that the dead-field scanner baseline only shrinks. # # Called from `make dead-field-scan-ratchet` (and from CI). Runs the scanner, # diffs the current output against scan-baseline.json, and fails if any NEW # dead field was introduced. A shrinking baseline (dead field wired up) is # fine -- the script just prints a reminder to commit the new baseline. # # Why ratchet instead of "gate on zero": the existing 26-row baseline is # tracked implementation debt (see core/TODO.md "实现债务" section), not # code to delete. Gating CI on zero findings would push developers to # delete fields that carry design intent -- the opposite of what we want. # Ratchet lets the debt drain naturally without letting new debt sneak in. # # Why whitelist was rejected: a per-field whitelist becomes a silent # dumping ground ("add it to the whitelist to pass CI"). A single baseline # JSON that the scanner regenerates keeps every change visible in the diff. # # ratchet.sh - 保证 dead-field scanner 的 baseline 只减不增. # # 由 `make dead-field-scan-ratchet` (和 CI) 调用. 跑 scanner, 对比当前 # 输出和 scan-baseline.json, 一旦有新增 dead field 就 fail. baseline # 变小 (debt 被 wire 上) 可以, 脚本只提示 commit 新 baseline. # # 为啥用 ratchet 而非 "卡 0": 当前 26 条 baseline 是可追踪的实现债务 # (见 core/TODO.md "实现债务" section), 不是要删的代码. 卡 0 会逼开发者 # 删有设计意图的字段, 和本意相反. Ratchet 让债务自然流出, 同时挡新增. # # 为啥不用 whitelist: 逐字段白名单会变成无声垃圾堆 ("加白过 CI"). 单 # 文件 baseline JSON 每次 scanner 重跑就整体刷新, 任何改动都在 diff 里 # 显式可见. set -euo pipefail # Resolve paths relative to this script so callers can invoke from any cwd. # 相对本脚本路径解析, 调用方 cwd 无关. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CORE_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" BASELINE="${SCRIPT_DIR}/scan-baseline.json" if [ ! -f "${BASELINE}" ]; then echo "ratchet: baseline not found at ${BASELINE}" >&2 exit 2 fi # Produce current scan output as JSON. # 生成当前扫描输出 (JSON). CURRENT="$(mktemp)" trap 'rm -f "${CURRENT}" "${BASE_KEYS}" "${NOW_KEYS}"' EXIT cd "${SCRIPT_DIR}" GOWORK=off go run . -root="${CORE_DIR}" -json > "${CURRENT}" # Reduce both JSON arrays to a sorted key set of "pkg.Struct.Field". Ignoring # line/type/tag churn keeps the ratchet stable under refactors that move a # struct definition by a few lines without changing field identity. # # 把两份 JSON 归约成排序后的 "pkg.Struct.Field" key 集合. 忽略 line/type/tag # 的抖动, 让 ratchet 在搬动 struct 定义几行的 refactor 下保持稳定. BASE_KEYS="$(mktemp)" NOW_KEYS="$(mktemp)" jq -r '.[] | "\(.pkg).\(.struct).\(.field)"' "${BASELINE}" | sort > "${BASE_KEYS}" jq -r '.[] | "\(.pkg).\(.struct).\(.field)"' "${CURRENT}" | sort > "${NOW_KEYS}" BASE_COUNT=$(wc -l < "${BASE_KEYS}") NOW_COUNT=$(wc -l < "${NOW_KEYS}") ADDED="$(comm -13 "${BASE_KEYS}" "${NOW_KEYS}")" REMOVED="$(comm -23 "${BASE_KEYS}" "${NOW_KEYS}")" ADDED_N=$(printf "%s" "${ADDED}" | grep -c . || true) REMOVED_N=$(printf "%s" "${REMOVED}" | grep -c . || true) echo "ratchet baseline: ${BASE_COUNT} fields" echo "ratchet current: ${NOW_COUNT} fields" echo "ratchet added: ${ADDED_N}" echo "ratchet removed: ${REMOVED_N}" FAIL=0 if [ "${ADDED_N}" -gt 0 ]; then echo echo "NEW dead fields introduced (ratchet fail):" printf " %s\n" ${ADDED} echo echo "Options:" echo " 1. Wire the field (preferred -- if a field exists someone intended it)." echo " 2. Delete the field if it has no design intent." echo " 3. Regenerate baseline to explicitly accept as tracked debt:" echo " cd tools/dead-field-scan && GOWORK=off go run . -root=../.. -json > scan-baseline.json" echo " ... and add a TODO.md entry under '实现债务'." FAIL=1 fi if [ "${REMOVED_N}" -gt 0 ]; then echo echo "Dead fields fixed (baseline should shrink):" printf " %s\n" ${REMOVED} echo echo "Regenerate and commit the new baseline:" echo " cd tools/dead-field-scan && GOWORK=off go run . -root=../.. -json > scan-baseline.json" fi exit ${FAIL}