mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-30 06:16:37 +08:00
chore(autoresearch): re-baseline harness on pinned real-risk yardstick
The committed golangci gate now reports 0 issues, so the previous lint_issues metric was saturated and could no longer measure progress. Measure debt against an immutable .auto/lint.ref.yaml snapshot that adds analyzers for genuine defects (panics, error unwrapping, dead stores, missing enum cases, method ordering, suppression hygiene) while excluding cosmetic churn (tagliatelle, wrapcheck). Guard enforces build, vet, tests, the Cordis architecture gate, and anti-cheat floors: the yardstick cannot be edited, the project gate may only be strengthened, nolint directives may only shrink, and no test may disappear.
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
# Autoresearch baseline — captured at iteration #0 (2026-08-29).
|
||||
# The Guard compares live values against these floors; the PRIMARY metric is debt.
|
||||
BASE_DEBT=102
|
||||
BASE_NOLINT=96
|
||||
BASE_TESTS_PASSED=46
|
||||
BASE_TEST_FUNCS=232
|
||||
BASE_TEST_FILES=76
|
||||
BASE_ARCH_VIOL=0
|
||||
BASE_COVERAGE=34.09
|
||||
|
||||
# SHA-256 of the pinned yardstick config. Guard aborts if it changes.
|
||||
REF_SHA=e881bda167bd688489f1356b7cd4056b8a6960f48b6778b095bdd2e44f627b82
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Anti-cheat: prove .golangci.yml was only ever strengthened, never weakened.
|
||||
|
||||
Compares the live gate against the immutable snapshot taken at run start.
|
||||
Exits non-zero with a reason if any hardening rule is violated.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
BASELINE = os.path.join(ROOT, ".auto", "gate.baseline.yml")
|
||||
LIVE = os.path.join(ROOT, ".golangci.yml")
|
||||
|
||||
# threshold-like knobs: (path, direction) where direction "max" means the value
|
||||
# is an upper bound (smaller == stricter), "min" means a lower bound.
|
||||
STRICTNESS = [
|
||||
(("linters", "settings", "dupl", "threshold"), "max"),
|
||||
(("linters", "settings", "cyclop", "max-complexity"), "max"),
|
||||
(("linters", "settings", "cyclop", "package-average"), "max"),
|
||||
(("linters", "settings", "nestif", "min-complexity"), "min"),
|
||||
(("linters", "settings", "funlen", "lines"), "max"),
|
||||
(("linters", "settings", "funlen", "statements"), "max"),
|
||||
(("linters", "settings", "gocyclo", "min-complexity"), "min"),
|
||||
(("linters", "settings", "lll", "line-length"), "max"),
|
||||
]
|
||||
|
||||
|
||||
def load(path):
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
return yaml.safe_load(fh) or {}
|
||||
|
||||
|
||||
def dig(doc, path):
|
||||
node = doc
|
||||
for key in path:
|
||||
if not isinstance(node, dict) or key not in node:
|
||||
return None
|
||||
node = node[key]
|
||||
return node
|
||||
|
||||
|
||||
def enabled_linters(doc):
|
||||
lint = doc.get("linters") or {}
|
||||
if lint.get("enable-presets"):
|
||||
return None # preset based; fall back to "any removal is suspicious"
|
||||
return set(lint.get("enable") or [])
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
base, live = load(BASELINE), load(LIVE)
|
||||
except OSError as exc:
|
||||
print(f"gate snapshot unreadable: {exc}")
|
||||
return 1
|
||||
except yaml.YAMLError as exc:
|
||||
print(f".golangci.yml is not parseable: {exc}")
|
||||
return 1
|
||||
|
||||
problems = []
|
||||
|
||||
base_lint, live_lint = base.get("linters") or {}, live.get("linters") or {}
|
||||
if (base_lint.get("default") or "none") != (live_lint.get("default") or "none"):
|
||||
problems.append("linters.default changed")
|
||||
|
||||
base_set, live_set = enabled_linters(base), enabled_linters(live)
|
||||
if base_set is None or live_set is None:
|
||||
if set((base.get("linters") or {}).get("enable-presets") or []) - set(
|
||||
(live.get("linters") or {}).get("enable-presets") or []
|
||||
):
|
||||
problems.append("an enable-preset was removed")
|
||||
elif dropped := base_set - live_set:
|
||||
problems.append(f"linters disabled: {sorted(dropped)}")
|
||||
|
||||
for path, direction in STRICTNESS:
|
||||
old, new = dig(base, path), dig(live, path)
|
||||
if old is None or new is None:
|
||||
continue
|
||||
try:
|
||||
old_f, new_f = float(old), float(new)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if direction == "max" and new_f > old_f:
|
||||
problems.append(f"{'.'.join(path)} loosened {old} -> {new}")
|
||||
if direction == "min" and new_f < old_f:
|
||||
problems.append(f"{'.'.join(path)} loosened {old} -> {new}")
|
||||
|
||||
base_mnd = set(dig(base, ("linters", "settings", "mnd", "checks")) or [])
|
||||
live_mnd = set(dig(live, ("linters", "settings", "mnd", "checks")) or [])
|
||||
if base_mnd - live_mnd:
|
||||
problems.append(f"mnd checks dropped: {sorted(base_mnd - live_mnd)}")
|
||||
|
||||
issues_live = live.get("issues") or {}
|
||||
for key in ("exclude-rules", "exclude-patterns"):
|
||||
if issues_live.get(key) and not (base.get("issues") or {}).get(key):
|
||||
problems.append(f"issues.{key} added (suppresses reporting)")
|
||||
|
||||
for key in ("max-issues-per-linter", "max-same-issues"):
|
||||
old = (base.get("issues") or {}).get(key)
|
||||
new = issues_live.get(key)
|
||||
if old == 0 and new != 0:
|
||||
problems.append(f"issues.{key} no longer 0 — findings would be truncated")
|
||||
|
||||
# Exclusions expressed through the newer 'linters.exclusions' block.
|
||||
if (live_lint.get("exclusions") or {}) and not (base_lint.get("exclusions") or {}):
|
||||
problems.append("linters.exclusions added")
|
||||
|
||||
if problems:
|
||||
print("\n".join(f" - {p}" for p in problems))
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+48
-9
@@ -1,11 +1,50 @@
|
||||
#!/bin/bash
|
||||
# Correctness gate: build + full tests + Cordis architecture checker (errors only)
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/../backend"
|
||||
# Autoresearch GUARD — hard veto. Every line here protects an invariant that is
|
||||
# unrelated to the primary metric, plus the anti-cheat red lines.
|
||||
set -uo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "${ROOT}/backend"
|
||||
STATUS=0
|
||||
fail() { echo "GUARD FAIL: $1"; STATUS=1; }
|
||||
|
||||
go build ./... 2>&1 | head -20
|
||||
go test ./... 2>&1 | grep -vE '^(ok|---|\?|PASS)' | grep -v 'no test files' | head -40 || true
|
||||
FAILS=$(go test ./... 2>&1 | grep -cE '^(FAIL|--- FAIL)' || true)
|
||||
if [ "${FAILS}" != "0" ]; then echo "TESTS FAILED (${FAILS})"; exit 1; fi
|
||||
"$(dirname "$0")/../scripts/check_cordis_architecture.sh" >/dev/null 2>&1 || { echo "CORDIS ARCH CHECK FAILED"; exit 1; }
|
||||
echo "CHECKS OK"
|
||||
source "${ROOT}/.auto/baseline.env"
|
||||
|
||||
# --- 1. Correctness -----------------------------------------------------------
|
||||
go build ./... || fail "go build failed"
|
||||
go vet ./... || fail "go vet failed"
|
||||
|
||||
go test ./... > /tmp/ar_guard_test.txt 2>&1 || true
|
||||
FAILS=$(grep -cE '^(FAIL|--- FAIL)' /tmp/ar_guard_test.txt || true)
|
||||
[ "${FAILS}" = "0" ] || { grep -E '^(FAIL|--- FAIL)' /tmp/ar_guard_test.txt | head -20; fail "tests failing (${FAILS})"; }
|
||||
|
||||
# --- 2. Cordis architecture gate ---------------------------------------------
|
||||
"${ROOT}/scripts/check_cordis_architecture.sh" > /dev/null 2>&1 || fail "cordis architecture check failed"
|
||||
|
||||
# --- 3. Project lint gate must stay clean ------------------------------------
|
||||
PROJECT_LINT=$(golangci-lint run 2>&1 | grep -cE '\.go:[0-9]+:[0-9]+: ' || true)
|
||||
[ "${PROJECT_LINT}" = "0" ] || { fail "project golangci-lint reports ${PROJECT_LINT} issues"; }
|
||||
|
||||
# --- 4. Anti-cheat: the yardstick itself is immutable ------------------------
|
||||
REF_SHA_NOW=$(shasum -a 256 "${ROOT}/.auto/lint.ref.yaml" | awk '{print $1}')
|
||||
[ "${REF_SHA_NOW}" = "${REF_SHA}" ] || fail "pinned yardstick .auto/lint.ref.yaml was modified"
|
||||
|
||||
# --- 5. Anti-cheat: the project gate may only ever be STRENGTHENED ------------
|
||||
WEAK=$(python3 "${ROOT}/.auto/check_gate_weaken.py" 2>&1) || { echo "${WEAK}"; fail "project gate weakened"; }
|
||||
|
||||
# --- 6. Anti-cheat: no new suppressions --------------------------------------
|
||||
NOLINT=$(rg '//\s*nolint' --glob '*.go' 2>/dev/null | wc -l | tr -d ' ')
|
||||
[ "${NOLINT}" -le "${BASE_NOLINT}" ] || fail "nolint directives grew (${NOLINT} > ${BASE_NOLINT})"
|
||||
|
||||
# --- 7. Anti-cheat: no tests deleted, no packages lost -----------------------
|
||||
TEST_FUNCS=$(rg -c '^(func Test|func Benchmark)' --glob '*_test.go' 2>/dev/null | awk -F: '{s+=$2} END {print s+0}')
|
||||
[ "${TEST_FUNCS}" -ge "${BASE_TEST_FUNCS}" ] || fail "test funcs shrank (${TEST_FUNCS} < ${BASE_TEST_FUNCS})"
|
||||
TEST_FILES=$(rg --files --glob '*_test.go' 2>/dev/null | wc -l | tr -d ' ')
|
||||
[ "${TEST_FILES}" -ge "${BASE_TEST_FILES}" ] || fail "test files deleted (${TEST_FILES} < ${BASE_TEST_FILES})"
|
||||
PASSED=$(grep -c '^ok' /tmp/ar_guard_test.txt || true)
|
||||
[ "${PASSED}" -ge "${BASE_TESTS_PASSED}" ] || fail "passing packages shrank (${PASSED} < ${BASE_TESTS_PASSED})"
|
||||
|
||||
# --- 8. License headers on Go sources (CI gate) ------------------------------
|
||||
"${ROOT}/scripts/update_go_license.sh" --check > /dev/null 2>&1 || fail "license header check failed"
|
||||
|
||||
if [ "${STATUS}" = "0" ]; then echo "CHECKS OK"; fi
|
||||
exit ${STATUS}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
tests: false
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
# 基础检查
|
||||
- govet
|
||||
- staticcheck
|
||||
- errcheck
|
||||
- ineffassign
|
||||
- unused
|
||||
|
||||
# 代码坏味道
|
||||
- dupl # 重复代码
|
||||
- mnd # 魔法数字
|
||||
- goconst # 不必要的字符串常量
|
||||
- cyclop # 包/函数复杂度
|
||||
- nestif # if 嵌套太深
|
||||
- maintidx # 维护性指数
|
||||
- revive # 风格/命名/坏味道
|
||||
- gocritic # 各类代码问题
|
||||
- funlen # 函数过长
|
||||
|
||||
- gosec # 安全问题检查
|
||||
- bodyclose # HTTP response body 没有正确关闭
|
||||
- noctx # 没有传递 context.Context
|
||||
- contextcheck # 其他检查
|
||||
- sqlclosecheck # SQL rows 没有正确关闭
|
||||
- unconvert # 不必要的类型转换
|
||||
- nilerr # 函数返回 nil 错误
|
||||
|
||||
settings:
|
||||
dupl:
|
||||
threshold: 80
|
||||
|
||||
cyclop:
|
||||
max-complexity: 20
|
||||
package-average: 10
|
||||
|
||||
nestif:
|
||||
min-complexity: 5
|
||||
|
||||
funlen:
|
||||
lines: 200
|
||||
statements: 100
|
||||
|
||||
mnd:
|
||||
checks:
|
||||
- argument
|
||||
- condition
|
||||
- return
|
||||
|
||||
|
||||
# 完整上报所有问题(取消 golangci 默认 50/3 截断,保证 code-check 与度量真实)
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
@@ -0,0 +1,78 @@
|
||||
version: "2"
|
||||
|
||||
# Pinned autoresearch yardstick. IMMUTABLE for the duration of a run.
|
||||
# Snapshot of the committed .golangci.yml plus the extra analyzers that report
|
||||
# genuine defects (correctness / panics / dead code) rather than cosmetics.
|
||||
# Keeping this separate from .golangci.yml means strengthening the project gate
|
||||
# can never silently lower the measured debt.
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
tests: false
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
# --- from the committed project gate ---
|
||||
- govet
|
||||
- staticcheck
|
||||
- errcheck
|
||||
- ineffassign
|
||||
- unused
|
||||
- dupl
|
||||
- mnd
|
||||
- goconst
|
||||
- cyclop
|
||||
- nestif
|
||||
- maintidx
|
||||
- revive
|
||||
- gocritic
|
||||
- funlen
|
||||
- gosec
|
||||
- bodyclose
|
||||
- noctx
|
||||
- contextcheck
|
||||
- sqlclosecheck
|
||||
- unconvert
|
||||
- nilerr
|
||||
# --- extra real-risk analyzers (defects, not cosmetics) ---
|
||||
- errorlint # err == / %v instead of errors.Is/As and %w
|
||||
- forcetypeassert # unchecked type assertions can panic
|
||||
- nilnil # (value, nil) breaks the nil-check contract
|
||||
- predeclared # shadowing builtins
|
||||
- unparam # dead params/results
|
||||
- wastedassign # dead stores
|
||||
- exhaustive # enum switches missing cases
|
||||
- makezero # append to preallocated slice
|
||||
- rowserrcheck # sql.Rows error after iteration
|
||||
- durationcheck # multiplied time.Duration
|
||||
- prealloc # slice growth in loops
|
||||
- copyloopvar # loop-var capture
|
||||
- nonamedreturns
|
||||
- funcorder # struct methods scattered across files
|
||||
- nolintlint # suppression audit (must stay 0)
|
||||
|
||||
settings:
|
||||
dupl:
|
||||
threshold: 80
|
||||
|
||||
cyclop:
|
||||
max-complexity: 20
|
||||
package-average: 10
|
||||
|
||||
nestif:
|
||||
min-complexity: 5
|
||||
|
||||
funlen:
|
||||
lines: 200
|
||||
statements: 100
|
||||
|
||||
mnd:
|
||||
checks:
|
||||
- argument
|
||||
- condition
|
||||
- return
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
+31
-11
@@ -1,18 +1,38 @@
|
||||
#!/bin/bash
|
||||
# Autoresearch measure: golangci-lint issue count (primary) + dupl subset + test packages
|
||||
set -euo pipefail
|
||||
# Autoresearch measure — pinned yardstick.
|
||||
# debt : findings under .auto/lint.ref.yaml (lower is better) [PRIMARY]
|
||||
# nolint_dirs : raw //nolint directive count (lower is better, floor 0)
|
||||
# tests_passed : go test packages passing (floor, must never drop)
|
||||
# test_funcs : total Test*/Benchmark* funcs (floor, must never drop)
|
||||
# arch_viol : Cordis architecture script violations (floor 0)
|
||||
# coverage : backend statement coverage % (informational)
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/../backend"
|
||||
|
||||
# Primary metric: total golangci-lint issues (non-test code)
|
||||
golangci-lint run > /tmp/ar_lint.txt 2>&1 || true
|
||||
LINT_ISSUES=$(grep -cE '(^|[/\\])[^/\\:]+\.go:[0-9]+:' /tmp/ar_lint.txt || true)
|
||||
REF_CFG="$(cd .. && pwd)/.auto/lint.ref.yaml"
|
||||
|
||||
# Secondary: dupl-specific issues
|
||||
DUP_ISSUES=$(grep -cE '\(dupl\)$' /tmp/ar_lint.txt || true)
|
||||
# Primary: pinned yardstick findings (never the mutable project config).
|
||||
golangci-lint run -c "${REF_CFG}" > /tmp/ar_debt.txt 2>&1 || true
|
||||
DEBT=$(grep -cE '\.go:[0-9]+:[0-9]+: ' /tmp/ar_debt.txt || true)
|
||||
|
||||
# Secondary: passing test packages (regression guard)
|
||||
TESTS_PASSED=$(go test ./... 2>&1 | grep -c '^ok' || true)
|
||||
# Suppression reliance — anti-cheat signal.
|
||||
NOLINT=$(rg '//\s*nolint' --glob '*.go' 2>/dev/null | wc -l | tr -d ' ')
|
||||
|
||||
echo "METRIC lint_issues=${LINT_ISSUES}"
|
||||
echo "METRIC dup_issues=${DUP_ISSUES}"
|
||||
# Regression floors. One covered run feeds both the pass floor and coverage.
|
||||
go test -cover ./... > /tmp/ar_test.txt 2>&1 || true
|
||||
TESTS_PASSED=$(grep -c '^ok' /tmp/ar_test.txt || true)
|
||||
FAILS=$(grep -cE '^(FAIL|--- FAIL)' /tmp/ar_test.txt || true)
|
||||
TEST_FUNCS=$(rg -c '^(func Test|func Benchmark)' --glob '*_test.go' 2>/dev/null | awk -F: '{s+=$2} END {print s+0}')
|
||||
|
||||
# Cordis architecture violations (count of FAIL lines emitted by the gate script).
|
||||
ARCH_VIOL=$("../scripts/check_cordis_architecture.sh" 2>&1 | grep -c 'FAIL' || true)
|
||||
|
||||
COVERAGE=$(grep -oE 'coverage: [0-9.]+%' /tmp/ar_test.txt | awk '{gsub("%","",$2); s+=$2; n++} END {if(n>0) printf "%.2f", s/n; else print "0"}')
|
||||
|
||||
echo "METRIC debt=${DEBT}"
|
||||
echo "METRIC nolint_dirs=${NOLINT}"
|
||||
echo "METRIC tests_passed=${TESTS_PASSED}"
|
||||
echo "METRIC test_funcs=${TEST_FUNCS}"
|
||||
echo "METRIC arch_viol=${ARCH_VIOL}"
|
||||
echo "METRIC coverage=${COVERAGE}"
|
||||
echo "INFO test_failures=${FAILS}"
|
||||
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
# Mechanically prove a FIX iteration is load-bearing.
|
||||
#
|
||||
# Usage: .auto/prove_fix.sh <package> <changed source file> [<more files>...]
|
||||
#
|
||||
# Run immediately AFTER committing the fix, with a clean worktree. It reverts
|
||||
# only the non-test source files to their pre-fix state (keeping the new test),
|
||||
# runs the package tests, and requires them to FAIL. Then it restores HEAD.
|
||||
# A fix nobody can break with a revert is not a fix.
|
||||
set -uo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
if [ ! -z "$(git -C "${ROOT}" status --porcelain)" ]; then
|
||||
echo "PROVE ABORT: worktree must be clean (commit the change first)"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
PKG="$1"; shift
|
||||
SRC_FILES=("$@")
|
||||
if [ "${#SRC_FILES[@]}" -eq 0 ]; then
|
||||
echo "PROVE ABORT: no source files given"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cd "${ROOT}/backend" || exit 2
|
||||
|
||||
restore() {
|
||||
git -C "${ROOT}" checkout HEAD -- "${SRC_FILES[@]}" 2>/dev/null
|
||||
}
|
||||
trap restore EXIT
|
||||
|
||||
for f in "${SRC_FILES[@]}"; do
|
||||
if git -C "${ROOT}" cat-file -e "HEAD^:${f}" 2>/dev/null; then
|
||||
git -C "${ROOT}" checkout "HEAD^" -- "${f}" || { echo "PROVE ABORT: cannot revert ${f}"; exit 2; }
|
||||
else
|
||||
# File did not exist before this commit — removing it is the revert.
|
||||
rm -f "${ROOT}/${f}"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "--- tests against pre-fix source ---"
|
||||
OUT=$(go test -count=1 "${PKG}" 2>&1)
|
||||
RC=$?
|
||||
echo "${OUT}" | tail -15
|
||||
if [ "${RC}" -eq 0 ]; then
|
||||
echo "PROVE FAILED: tests still pass without the fix — this is not a real bug fix"
|
||||
exit 1
|
||||
fi
|
||||
if echo "${OUT}" | grep -qE '^(FAIL|--- FAIL)'; then
|
||||
KIND="assertion"
|
||||
else
|
||||
KIND="compile"
|
||||
fi
|
||||
echo "PROVED: test fails without the fix (${KIND})"
|
||||
exit 0
|
||||
@@ -0,0 +1,2 @@
|
||||
iteration commit metric delta status guard description
|
||||
0 - 102 0.0 baseline pass initial measurement (pinned yardstick: repo gate + real-risk analyzers)
|
||||
|
Reference in New Issue
Block a user