aboutsummaryrefslogtreecommitdiff
path: root/scripts/sync-check.sh
blob: cc7bc62171c487eccdae441613fd7cbf6227094d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#!/usr/bin/env bash
#
# sync-check.sh — verify canonical claude-templates/.ai/ matches the .ai/ mirror.
#
# Usage:
#   scripts/sync-check.sh           # exit 0 if synced, 1 if drift
#   scripts/sync-check.sh --fix     # rsync canonical → mirror, then re-check
#
# Runs as a pre-commit hook (githooks/pre-commit) to catch drift before
# commit, and as a manual check via `make sync-check`.
#
# The three synced paths are:
#   claude-templates/.ai/protocols.org  ↔  .ai/protocols.org
#   claude-templates/.ai/workflows/     ↔  .ai/workflows/
#   claude-templates/.ai/scripts/       ↔  .ai/scripts/
#
# Source of truth is the canonical (claude-templates/) side. The mirror
# exists so rulesets-as-a-project has a working copy. Drift in either
# direction is a defect — both have to land in the same commit.

set -euo pipefail

if ! repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || [ -z "$repo_root" ]; then
    echo "sync-check: not inside a git checkout" >&2
    exit 2
fi

canonical="$repo_root/claude-templates/.ai"
mirror="$repo_root/.ai"

if [ ! -d "$canonical" ] || [ ! -d "$mirror" ]; then
    echo "sync-check: not a rulesets-shaped repo (missing claude-templates/.ai or .ai)" >&2
    exit 2
fi

paths=(protocols.org workflows scripts)

# Generated artifacts that should never count as drift. These appear on the
# mirror side when tests run (pytest writes .pyc and .pytest_cache; emacs
# writes .elc) but aren't part of the canonical source.
exclude_args=(
    --exclude='__pycache__'
    --exclude='*.pyc'
    --exclude='*.pyo'
    --exclude='.pytest_cache'
    --exclude='*.elc'
)

check_drift() {
    local drift=0
    for relpath in "${paths[@]}"; do
        if ! diff -rq "${exclude_args[@]}" "$canonical/$relpath" "$mirror/$relpath" >/dev/null 2>&1; then
            echo "drift: claude-templates/.ai/$relpath ↔ .ai/$relpath" >&2
            diff -rq "${exclude_args[@]}" "$canonical/$relpath" "$mirror/$relpath" 2>&1 | head -20 >&2
            drift=1
        fi
    done
    return "$drift"
}

if check_drift; then
    exit 0
fi

if [ "${1:-}" = "--fix" ]; then
    echo "" >&2
    echo "sync-check --fix: syncing canonical → mirror..." >&2
    # Same exclude patterns as the diff so generated files in the mirror
    # aren't wiped by --delete when canonical has no counterpart.
    rsync_excludes=(
        --exclude='__pycache__'
        --exclude='*.pyc'
        --exclude='*.pyo'
        --exclude='.pytest_cache'
        --exclude='*.elc'
    )
    rsync -a "$canonical/protocols.org" "$mirror/protocols.org"
    rsync -a --delete "${rsync_excludes[@]}" "$canonical/workflows/" "$mirror/workflows/"
    rsync -a --delete "${rsync_excludes[@]}" "$canonical/scripts/" "$mirror/scripts/"
    if check_drift; then
        echo "sync-check --fix: resolved." >&2
        echo "Re-stage the synced files and retry the commit." >&2
        exit 0
    else
        echo "sync-check --fix: drift persists after sync. Inspect manually." >&2
        exit 1
    fi
fi

echo "" >&2
echo "Run 'scripts/sync-check.sh --fix' (or 'make sync-check FIX=1') to resolve." >&2
exit 1