scripts/check-roundtrip-gate.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
# Heuristic diff guard for serializer changes in src/.
#
# Fail when this branch adds/changes a serializer without a corresponding
# round-trip test edit. Catches `parse(serialize(x))`-shape bugs that pass
# per-side review by demanding the property test exist.
#
# This is intentionally heuristic. It can false-positive on serializer-looking
# helper functions that have no inverse, and it can false-negative when a
# serializer changes without adding a new matching line. Treat a failure as a
# review prompt, not proof that the code is wrong.
#
# Triggers on added lines (`^+`, not `^+++`) matching any of:
# fn to_raw / fn to_string / fn serialize / fn to_toml / fn to_json
# impl <...> Serialize for
#
# Looks for a corresponding signal in test diffs:
# roundtrip / round_trip / serialize.*parse / parse.*serialize /
# to_raw.*from_raw / from_raw.*to_raw
#
# Override: KEI_SKIP_ROUNDTRIP_GATE=1 (use only with a written reviewer rationale).
set -euo pipefail
BASE_REF="${KEI_ROUNDTRIP_BASE:-origin/main}"
if [ "${KEI_SKIP_ROUNDTRIP_GATE:-0}" = "1" ]; then
echo "roundtrip-gate: skipped via KEI_SKIP_ROUNDTRIP_GATE=1" >&2
exit 0
fi
if ! git rev-parse --verify "$BASE_REF" >/dev/null 2>&1; then
# CI environments always have origin/main fetched; a missing base ref
# there is a real configuration bug and we fail loudly. Locally, we
# warn and skip so a fresh checkout doesn't hard-block on `just gate`
# before the user has had a chance to run `git fetch origin main`.
if [ "${CI:-}" = "true" ] || [ -n "${GITHUB_ACTIONS:-}" ]; then
echo "roundtrip-gate: base ref '$BASE_REF' not found in CI; check checkout config" >&2
exit 1
fi
echo "roundtrip-gate: WARNING base ref '$BASE_REF' not found locally; skipping detector. Run \`git fetch origin main\` to enable the gate." >&2
exit 0
fi
MERGE_BASE=$(git merge-base "$BASE_REF" HEAD)
# Diff body, added lines only, excluding the +++ header.
ADDED_LINES=$(git diff "$MERGE_BASE...HEAD" -- ':(glob)src/**/*.rs' | grep -E '^\+[^+]' || true)
SERIALIZER_HITS=$(printf '%s\n' "$ADDED_LINES" | grep -E '\bfn (to_raw|to_string|serialize|to_toml|to_json)\b|\bimpl[^{]*\bSerialize\b' || true)
if [ -z "$SERIALIZER_HITS" ]; then
exit 0
fi
# Test diff (tests/ tree + any *.rs in src/ since #[cfg(test)] modules live there).
TEST_DIFF=$(git diff "$MERGE_BASE...HEAD" -- ':(glob)tests/**/*.rs' ':(glob)src/**/*.rs' | grep -E '^\+[^+]' || true)
ROUNDTRIP_SIGNAL=$(printf '%s\n' "$TEST_DIFF" | grep -E 'roundtrip|round_trip|serialize.*parse|parse.*serialize|to_raw.*from_raw|from_raw.*to_raw' || true)
if [ -n "$ROUNDTRIP_SIGNAL" ]; then
exit 0
fi
cat >&2 <<EOF
roundtrip-gate: heuristic serializer change detected without a round-trip test edit.
Changed serializers (diff vs $MERGE_BASE):
$(printf '%s\n' "$SERIALIZER_HITS" | sed 's/^/ /')
Add a test that exercises \`parse(serialize(x)) == x\` (or equivalent
inverse) for the type whose serializer changed. Look for an existing
\`*_roundtrip\` / \`round_trip\` test in tests/ or src/ as a pattern.
If this serializer truly has no inverse and no round-trip property,
add the test name with a short reviewer rationale explaining why
(\`fn no_roundtrip_*\`) so the gate sees the signal and the next
reviewer sees the rationale.
To bypass for an emergency hotfix: KEI_SKIP_ROUNDTRIP_GATE=1 just gate
EOF
exit 1