116 lines
3.3 KiB
Bash
Executable File
116 lines
3.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# check-contract-drift.sh — Enforce Constitution Principle V (contracts stay in lock-step).
|
|
#
|
|
# Fails when boundary-signature changes are detected under internal layers without
|
|
# any update under contracts/*.md in the same diff range.
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
|
|
log() {
|
|
printf '[contract-drift] %s\n' "$*"
|
|
}
|
|
|
|
err() {
|
|
printf '[contract-drift] ERROR: %s\n' "$*" >&2
|
|
}
|
|
|
|
resolve_range() {
|
|
if [[ -n "${AUGUR_CONTRACT_DRIFT_RANGE:-}" ]]; then
|
|
printf '%s' "$AUGUR_CONTRACT_DRIFT_RANGE"
|
|
return 0
|
|
fi
|
|
|
|
if [[ -n "${GITHUB_BASE_REF:-}" ]]; then
|
|
git fetch --no-tags --depth=1 origin "$GITHUB_BASE_REF" >/dev/null 2>&1 || true
|
|
printf 'origin/%s...HEAD' "$GITHUB_BASE_REF"
|
|
return 0
|
|
fi
|
|
|
|
if [[ -n "${GITHUB_EVENT_BEFORE:-}" ]] && [[ -n "${GITHUB_SHA:-}" ]] && [[ "$GITHUB_EVENT_BEFORE" != "0000000000000000000000000000000000000000" ]]; then
|
|
printf '%s...%s' "$GITHUB_EVENT_BEFORE" "$GITHUB_SHA"
|
|
return 0
|
|
fi
|
|
|
|
if git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
|
|
printf 'HEAD~1...HEAD'
|
|
return 0
|
|
fi
|
|
|
|
printf ''
|
|
}
|
|
|
|
USE_WORKTREE="${AUGUR_CONTRACT_DRIFT_USE_WORKTREE:-0}"
|
|
RANGE=""
|
|
if [[ "$USE_WORKTREE" == "1" ]]; then
|
|
log "Diff source: working tree (HEAD -> working tree)"
|
|
changed_files="$(git diff --name-only)"
|
|
else
|
|
RANGE="$(resolve_range)"
|
|
if [[ -z "$RANGE" ]]; then
|
|
log "No diff range could be resolved; skipping contract drift check."
|
|
exit 0
|
|
fi
|
|
log "Diff range: $RANGE"
|
|
changed_files="$(git diff --name-only "$RANGE")"
|
|
fi
|
|
|
|
if [[ -z "$changed_files" ]]; then
|
|
log "No changed files in range; skipping."
|
|
exit 0
|
|
fi
|
|
|
|
if printf '%s\n' "$changed_files" | grep -Eq '^contracts/.*\.md$'; then
|
|
log "Contract files changed in range; check passed."
|
|
exit 0
|
|
fi
|
|
|
|
# Boundary-sensitive files that define cross-layer contracts.
|
|
boundary_files="$(printf '%s\n' "$changed_files" | grep -E '^internal/(cli|service|provider|storage|sync|model)/.*\.go$' || true)"
|
|
|
|
if [[ -z "$boundary_files" ]]; then
|
|
log "No boundary-sensitive Go files changed; check passed."
|
|
exit 0
|
|
fi
|
|
|
|
violations=()
|
|
|
|
while IFS= read -r file; do
|
|
[[ -z "$file" ]] && continue
|
|
|
|
# Canonical model and provider interface are always contract-relevant.
|
|
if [[ "$file" == "internal/model/conversation.go" ]] || [[ "$file" == "internal/provider/provider.go" ]]; then
|
|
violations+=("$file")
|
|
continue
|
|
fi
|
|
|
|
# Heuristic: exported symbol signature/shape changes in boundary layers are contract-relevant.
|
|
# Matches exported funcs, exported interfaces, and exported struct fields with JSON tags.
|
|
diff_output=""
|
|
if [[ "$USE_WORKTREE" == "1" ]]; then
|
|
diff_output="$(git diff -U0 -- "$file")"
|
|
else
|
|
diff_output="$(git diff -U0 "$RANGE" -- "$file")"
|
|
fi
|
|
|
|
if printf '%s\n' "$diff_output" | grep -Eq '^[+-](func (\([^)]*\) )?[A-Z][A-Za-z0-9_]*\(|type [A-Z][A-Za-z0-9_]* interface|[[:space:]]+[A-Z][A-Za-z0-9_]*[[:space:]].*`json:"[^"]+"`)'; then
|
|
violations+=("$file")
|
|
fi
|
|
done <<< "$boundary_files"
|
|
|
|
if [[ "${#violations[@]}" -eq 0 ]]; then
|
|
log "No contract-relevant signature drift detected; check passed."
|
|
exit 0
|
|
fi
|
|
|
|
err "Contract drift detected: contract-relevant files changed without contracts/*.md updates."
|
|
err "Update the applicable contract file(s) in contracts/ in the same change."
|
|
err "Impacted files:"
|
|
for file in "${violations[@]}"; do
|
|
err " - $file"
|
|
done
|
|
|
|
exit 1
|