#!/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 <