290 lines
14 KiB
Bash
Executable File
290 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# scripts/check_doc_drift.sh
|
||
#
|
||
# Защита от documentation drift между фактическим кодом и audit package.
|
||
# Закрывает class ошибок обнаруженных внешним аудитом Claude Opus 4.7
|
||
# (отчёт #1 F-1/F-4/F-18, отчёт #2 M2-12/DOC-1/DOC-2): AUDIT.md /
|
||
# audit-checklist.md / security-cards.md повторяют устаревшие числа
|
||
# (line counts, unsafe block count, domain count).
|
||
#
|
||
# Стратегия: вычислить факт из исходного кода (wc / grep) и сверить с
|
||
# заявлениями в .md файлах. При mismatch — exit 1 с понятной diagnostic.
|
||
#
|
||
# Запускается:
|
||
# - Локально перед commit (git pre-commit hook опционально)
|
||
# - В CI как gate doc_drift
|
||
#
|
||
# Использование:
|
||
# ./scripts/check_doc_drift.sh
|
||
#
|
||
# Exit code: 0 (clean) | 1 (drift detected, listing напечатан в stderr)
|
||
|
||
set -euo pipefail
|
||
|
||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
cd "$REPO_ROOT"
|
||
|
||
EXIT=0
|
||
errors=()
|
||
|
||
err() {
|
||
EXIT=1
|
||
errors+=("$1")
|
||
echo "FAIL: $1" >&2
|
||
}
|
||
|
||
ok() {
|
||
echo "ok: $1"
|
||
}
|
||
|
||
# ---------- 1. mt-crypto/src/lib.rs line count ----------
|
||
ACTUAL_MT_CRYPTO_LOC=$(wc -l < crates/mt-crypto/src/lib.rs | tr -d ' ')
|
||
|
||
# AUDIT.md заявляет в строке: | `mt-crypto` | ... | **NNN** | ... |
|
||
CLAIMED_MT_CRYPTO_LOC=$(grep -E '^\| `mt-crypto` \|' AUDIT.md | grep -oE '\*\*[0-9]+\*\*' | head -1 | tr -d '*')
|
||
|
||
if [ -z "$CLAIMED_MT_CRYPTO_LOC" ]; then
|
||
err "AUDIT.md не содержит mt-crypto LOC claim в expected format '**NNN**'"
|
||
elif [ "$ACTUAL_MT_CRYPTO_LOC" != "$CLAIMED_MT_CRYPTO_LOC" ]; then
|
||
err "AUDIT.md mt-crypto LOC drift: claimed $CLAIMED_MT_CRYPTO_LOC, actual $ACTUAL_MT_CRYPTO_LOC"
|
||
else
|
||
ok "AUDIT.md mt-crypto LOC = $ACTUAL_MT_CRYPTO_LOC"
|
||
fi
|
||
|
||
# audit-checklist.md заявляет: Layer 1 Rust shim** **NNN строк** —
|
||
# LOC после "shim** **" чтобы не зацепить "Layer 1" prefix
|
||
CHECKLIST_MT_CRYPTO_LOC=$(grep -oE 'Rust shim\*\* \*\*[0-9]+ строк' docs/audit-checklist.md | grep -oE '\*\*[0-9]+ строк' | grep -oE '[0-9]+')
|
||
if [ -z "$CHECKLIST_MT_CRYPTO_LOC" ]; then
|
||
err "audit-checklist.md не содержит 'Layer 1 Rust shim **NNN строк**' claim"
|
||
elif [ "$ACTUAL_MT_CRYPTO_LOC" != "$CHECKLIST_MT_CRYPTO_LOC" ]; then
|
||
err "audit-checklist.md mt-crypto LOC drift: claimed $CHECKLIST_MT_CRYPTO_LOC, actual $ACTUAL_MT_CRYPTO_LOC"
|
||
else
|
||
ok "audit-checklist.md mt-crypto LOC = $ACTUAL_MT_CRYPTO_LOC"
|
||
fi
|
||
|
||
# ---------- 2. unsafe blocks в mt-crypto ----------
|
||
# Все формы: `unsafe {`, `unsafe fn`, `let _ = unsafe {`, etc.
|
||
# Считаем все unsafe keyword tokens (excluding /// doc comments + // comments)
|
||
ACTUAL_UNSAFE=$(grep -E '\bunsafe\b' crates/mt-crypto/src/lib.rs | grep -vE '^\s*///|^\s*//[^/]' | wc -l | tr -d ' ')
|
||
|
||
# AUDIT.md фразирует "Все **N** `unsafe` blocks"
|
||
CLAIMED_UNSAFE=$(grep -oE 'Все \*\*[0-9]+\*\* `unsafe` blocks' AUDIT.md | grep -oE '[0-9]+' | head -1)
|
||
if [ -z "$CLAIMED_UNSAFE" ]; then
|
||
err "AUDIT.md не содержит 'Все **N** \`unsafe\` blocks' claim"
|
||
elif [ "$ACTUAL_UNSAFE" != "$CLAIMED_UNSAFE" ]; then
|
||
err "AUDIT.md unsafe count drift: claimed $CLAIMED_UNSAFE, actual $ACTUAL_UNSAFE"
|
||
else
|
||
ok "AUDIT.md unsafe count = $ACTUAL_UNSAFE"
|
||
fi
|
||
|
||
# ---------- 3. Domain separators в mt-codec ----------
|
||
ACTUAL_DOMAINS=$(grep -cE '^\s*pub const [A-Z_]+: &\[u8\] = b"mt-' crates/mt-codec/src/lib.rs)
|
||
|
||
# AUDIT.md заявляет "Domain separators registry (NN domains, ..."
|
||
CLAIMED_DOMAINS=$(grep -oE 'Domain separators registry \([0-9]+ domains' AUDIT.md | grep -oE '[0-9]+' | head -1)
|
||
if [ -z "$CLAIMED_DOMAINS" ]; then
|
||
err "AUDIT.md не содержит 'Domain separators registry (NN domains' claim"
|
||
elif [ "$ACTUAL_DOMAINS" != "$CLAIMED_DOMAINS" ]; then
|
||
err "AUDIT.md domain count drift: claimed $CLAIMED_DOMAINS, actual $ACTUAL_DOMAINS"
|
||
else
|
||
ok "AUDIT.md domain count = $ACTUAL_DOMAINS"
|
||
fi
|
||
|
||
# AUDIT.md row "Domain registry sync (NN domains)"
|
||
SECONDARY_DOMAINS=$(grep -oE 'Domain registry sync \([0-9]+ domains\)' AUDIT.md | grep -oE '[0-9]+' | head -1)
|
||
if [ -n "$SECONDARY_DOMAINS" ] && [ "$ACTUAL_DOMAINS" != "$SECONDARY_DOMAINS" ]; then
|
||
err "AUDIT.md secondary domain count drift: claimed $SECONDARY_DOMAINS, actual $ACTUAL_DOMAINS"
|
||
fi
|
||
|
||
# ---------- 4. mt-crypto-native C wrapper LOC ----------
|
||
ACTUAL_C_LOC=$(wc -l < crates/mt-crypto-native/csrc/mt_crypto.c | tr -d ' ')
|
||
ACTUAL_H_LOC=$(wc -l < crates/mt-crypto-native/csrc/mt_crypto.h | tr -d ' ')
|
||
|
||
# AUDIT.md `mt_crypto.c | NNN |` — Layer 2 row
|
||
CLAIMED_C_LOC=$(grep -E 'mt_crypto.c\)\s*\|\s*[0-9]+' AUDIT.md | head -1 | grep -oE '\| [0-9]+ \|' | head -1 | grep -oE '[0-9]+')
|
||
if [ -n "$CLAIMED_C_LOC" ] && [ "$ACTUAL_C_LOC" != "$CLAIMED_C_LOC" ]; then
|
||
err "AUDIT.md mt_crypto.c LOC drift: claimed $CLAIMED_C_LOC, actual $ACTUAL_C_LOC"
|
||
elif [ -n "$CLAIMED_C_LOC" ]; then
|
||
ok "AUDIT.md mt_crypto.c LOC = $ACTUAL_C_LOC"
|
||
fi
|
||
|
||
CLAIMED_H_LOC=$(grep -E 'mt_crypto.h\)\s*\|\s*[0-9]+' AUDIT.md | head -1 | grep -oE '\| [0-9]+ \|' | head -1 | grep -oE '[0-9]+')
|
||
if [ -n "$CLAIMED_H_LOC" ] && [ "$ACTUAL_H_LOC" != "$CLAIMED_H_LOC" ]; then
|
||
err "AUDIT.md mt_crypto.h LOC drift: claimed $CLAIMED_H_LOC, actual $ACTUAL_H_LOC"
|
||
elif [ -n "$CLAIMED_H_LOC" ]; then
|
||
ok "AUDIT.md mt_crypto.h LOC = $ACTUAL_H_LOC"
|
||
fi
|
||
|
||
# ---------- 5. Total audit surface ----------
|
||
RUST_BINDING_LOC=$(wc -l < crates/mt-crypto-native/src/lib.rs | tr -d ' ')
|
||
BUILD_RS_LOC=$(wc -l < crates/mt-crypto-native/build.rs | tr -d ' ')
|
||
ACTUAL_TOTAL=$((ACTUAL_MT_CRYPTO_LOC + RUST_BINDING_LOC + ACTUAL_C_LOC + ACTUAL_H_LOC + BUILD_RS_LOC))
|
||
|
||
# AUDIT.md "Total own audit surface (Layer 1 + Layer 2): NNNN lines"
|
||
CLAIMED_TOTAL=$(grep -oE 'Total own audit surface \(Layer 1 \+ Layer 2\): [0-9]+ lines' AUDIT.md | grep -oE '[0-9]+ lines' | grep -oE '[0-9]+' | head -1)
|
||
if [ -z "$CLAIMED_TOTAL" ]; then
|
||
err "AUDIT.md не содержит 'Total own audit surface (Layer 1 + Layer 2): NNNN lines' claim"
|
||
elif [ "$ACTUAL_TOTAL" != "$CLAIMED_TOTAL" ]; then
|
||
err "AUDIT.md total audit surface drift: claimed $CLAIMED_TOTAL, actual $ACTUAL_TOTAL ($ACTUAL_MT_CRYPTO_LOC + $RUST_BINDING_LOC + $ACTUAL_C_LOC + $ACTUAL_H_LOC + $BUILD_RS_LOC)"
|
||
else
|
||
ok "AUDIT.md total audit surface = $ACTUAL_TOTAL"
|
||
fi
|
||
|
||
# ---------- 6. mt-account LOC (M3 audit scope) ----------
|
||
ACTUAL_MT_ACCOUNT_LOC=$(wc -l < crates/mt-account/src/lib.rs | tr -d ' ')
|
||
|
||
# AUDIT.md row: | `mt-account` | ... | **NNNN** | ... |
|
||
CLAIMED_MT_ACCOUNT_LOC=$(grep -E '^\| `mt-account` \|' AUDIT.md | grep -oE '\*\*[0-9]+\*\*' | head -1 | tr -d '*')
|
||
|
||
if [ -z "$CLAIMED_MT_ACCOUNT_LOC" ]; then
|
||
err "AUDIT.md не содержит mt-account LOC claim в expected format '**NNNN**'"
|
||
elif [ "$ACTUAL_MT_ACCOUNT_LOC" != "$CLAIMED_MT_ACCOUNT_LOC" ]; then
|
||
err "AUDIT.md mt-account LOC drift: claimed $CLAIMED_MT_ACCOUNT_LOC, actual $ACTUAL_MT_ACCOUNT_LOC"
|
||
else
|
||
ok "AUDIT.md mt-account LOC = $ACTUAL_MT_ACCOUNT_LOC"
|
||
fi
|
||
|
||
# AUDIT.md TL;DR table: M3 row "mt-account | NNNN | ..."
|
||
TLDR_MT_ACCOUNT_LOC=$(grep -E 'M3 apply_proposal layer.*mt-account' AUDIT.md | grep -oE '\| [0-9]+ \|' | head -1 | grep -oE '[0-9]+')
|
||
if [ -n "$TLDR_MT_ACCOUNT_LOC" ] && [ "$ACTUAL_MT_ACCOUNT_LOC" != "$TLDR_MT_ACCOUNT_LOC" ]; then
|
||
err "AUDIT.md TL;DR mt-account LOC drift: claimed $TLDR_MT_ACCOUNT_LOC, actual $ACTUAL_MT_ACCOUNT_LOC"
|
||
fi
|
||
|
||
# ---------- 7. mt-account test count ----------
|
||
ACTUAL_MT_ACCOUNT_TESTS=$(grep -cE '#\[test\]' crates/mt-account/src/lib.rs)
|
||
|
||
# AUDIT.md TL;DR заявляет: "NN unit + 35 determinism invariants"
|
||
CLAIMED_MT_ACCOUNT_TESTS=$(grep -oE '[0-9]+ unit \+ [0-9]+ determinism invariants' AUDIT.md | head -1 | grep -oE '^[0-9]+')
|
||
if [ -z "$CLAIMED_MT_ACCOUNT_TESTS" ]; then
|
||
err "AUDIT.md не содержит mt-account 'NN unit + 35 determinism invariants' claim"
|
||
elif [ "$ACTUAL_MT_ACCOUNT_TESTS" != "$CLAIMED_MT_ACCOUNT_TESTS" ]; then
|
||
err "AUDIT.md mt-account unit test count drift: claimed $CLAIMED_MT_ACCOUNT_TESTS, actual $ACTUAL_MT_ACCOUNT_TESTS"
|
||
else
|
||
ok "AUDIT.md mt-account unit test count = $ACTUAL_MT_ACCOUNT_TESTS"
|
||
fi
|
||
|
||
# ---------- 8. mt-account determinism_invariants tests ----------
|
||
ACTUAL_M3_DETERMINISM=$(grep -cE '#\[test\]' crates/mt-account/tests/determinism_invariants.rs)
|
||
|
||
# Check that 29 (M3 number after v34 monetary refactor) appears
|
||
if ! grep -q "29 determinism invariants" AUDIT.md; then
|
||
err "AUDIT.md не упоминает '29 determinism invariants' для M3"
|
||
elif [ "$ACTUAL_M3_DETERMINISM" != "29" ]; then
|
||
err "M3 determinism_invariants count drift: claimed 29, actual $ACTUAL_M3_DETERMINISM"
|
||
else
|
||
ok "AUDIT.md M3 determinism invariants = 29 (matches actual $ACTUAL_M3_DETERMINISM)"
|
||
fi
|
||
|
||
# ---------- 9..12: M4+M5 crate LOC + test counts ----------
|
||
|
||
# M4 mt-lottery
|
||
ACTUAL_MT_LOTTERY_LOC=$(wc -l < crates/mt-lottery/src/lib.rs | tr -d ' ')
|
||
CLAIMED_MT_LOTTERY_LOC=$(grep -E '^\| `mt-lottery` \|' AUDIT.md | grep -oE '\*\*[0-9]+\*\*' | head -1 | tr -d '*')
|
||
if [ -z "$CLAIMED_MT_LOTTERY_LOC" ]; then
|
||
err "AUDIT.md не содержит mt-lottery LOC claim в expected format '**NNNN**'"
|
||
elif [ "$ACTUAL_MT_LOTTERY_LOC" != "$CLAIMED_MT_LOTTERY_LOC" ]; then
|
||
err "AUDIT.md mt-lottery LOC drift: claimed $CLAIMED_MT_LOTTERY_LOC, actual $ACTUAL_MT_LOTTERY_LOC"
|
||
else
|
||
ok "AUDIT.md mt-lottery LOC = $ACTUAL_MT_LOTTERY_LOC"
|
||
fi
|
||
|
||
# M4 mt-consensus
|
||
ACTUAL_MT_CONSENSUS_LOC=$(wc -l < crates/mt-consensus/src/lib.rs | tr -d ' ')
|
||
CLAIMED_MT_CONSENSUS_LOC=$(grep -E '^\| `mt-consensus` \|' AUDIT.md | grep -oE '\*\*[0-9]+\*\*' | head -1 | tr -d '*')
|
||
if [ -z "$CLAIMED_MT_CONSENSUS_LOC" ]; then
|
||
err "AUDIT.md не содержит mt-consensus LOC claim"
|
||
elif [ "$ACTUAL_MT_CONSENSUS_LOC" != "$CLAIMED_MT_CONSENSUS_LOC" ]; then
|
||
err "AUDIT.md mt-consensus LOC drift: claimed $CLAIMED_MT_CONSENSUS_LOC, actual $ACTUAL_MT_CONSENSUS_LOC"
|
||
else
|
||
ok "AUDIT.md mt-consensus LOC = $ACTUAL_MT_CONSENSUS_LOC"
|
||
fi
|
||
|
||
# M4 mt-entry
|
||
ACTUAL_MT_ENTRY_LOC=$(wc -l < crates/mt-entry/src/lib.rs | tr -d ' ')
|
||
CLAIMED_MT_ENTRY_LOC=$(grep -E '^\| `mt-entry` \|' AUDIT.md | grep -oE '\*\*[0-9]+\*\*' | head -1 | tr -d '*')
|
||
if [ -z "$CLAIMED_MT_ENTRY_LOC" ]; then
|
||
err "AUDIT.md не содержит mt-entry LOC claim"
|
||
elif [ "$ACTUAL_MT_ENTRY_LOC" != "$CLAIMED_MT_ENTRY_LOC" ]; then
|
||
err "AUDIT.md mt-entry LOC drift: claimed $CLAIMED_MT_ENTRY_LOC, actual $ACTUAL_MT_ENTRY_LOC"
|
||
else
|
||
ok "AUDIT.md mt-entry LOC = $ACTUAL_MT_ENTRY_LOC"
|
||
fi
|
||
|
||
# M5 mt-store
|
||
ACTUAL_MT_STORE_LOC=$(wc -l < crates/mt-store/src/lib.rs | tr -d ' ')
|
||
CLAIMED_MT_STORE_LOC=$(grep -E '^\| `mt-store` \|' AUDIT.md | grep -oE '\*\*[0-9]+\*\*' | head -1 | tr -d '*')
|
||
if [ -z "$CLAIMED_MT_STORE_LOC" ]; then
|
||
err "AUDIT.md не содержит mt-store LOC claim"
|
||
elif [ "$ACTUAL_MT_STORE_LOC" != "$CLAIMED_MT_STORE_LOC" ]; then
|
||
err "AUDIT.md mt-store LOC drift: claimed $CLAIMED_MT_STORE_LOC, actual $ACTUAL_MT_STORE_LOC"
|
||
else
|
||
ok "AUDIT.md mt-store LOC = $ACTUAL_MT_STORE_LOC"
|
||
fi
|
||
|
||
# M4 total surface — extract число между ":** " и " lines" (skip M4 milestone digit)
|
||
ACTUAL_M4_TOTAL=$((ACTUAL_MT_LOTTERY_LOC + ACTUAL_MT_CONSENSUS_LOC + ACTUAL_MT_ENTRY_LOC))
|
||
CLAIMED_M4_TOTAL=$(grep -oE 'Total M4 audit surface:\*\* [0-9]+ lines' AUDIT.md | awk -F'[*: ]+' '{print $(NF-1)}')
|
||
if [ -n "$CLAIMED_M4_TOTAL" ] && [ "$ACTUAL_M4_TOTAL" != "$CLAIMED_M4_TOTAL" ]; then
|
||
err "AUDIT.md M4 total surface drift: claimed $CLAIMED_M4_TOTAL, actual $ACTUAL_M4_TOTAL"
|
||
elif [ -n "$CLAIMED_M4_TOTAL" ]; then
|
||
ok "AUDIT.md M4 total surface = $ACTUAL_M4_TOTAL"
|
||
fi
|
||
|
||
# M5 total surface
|
||
ACTUAL_M5_TOTAL=$ACTUAL_MT_STORE_LOC
|
||
CLAIMED_M5_TOTAL=$(grep -oE 'Total M5 audit surface:\*\* [0-9]+ lines' AUDIT.md | awk -F'[*: ]+' '{print $(NF-1)}')
|
||
if [ -n "$CLAIMED_M5_TOTAL" ] && [ "$ACTUAL_M5_TOTAL" != "$CLAIMED_M5_TOTAL" ]; then
|
||
err "AUDIT.md M5 total surface drift: claimed $CLAIMED_M5_TOTAL, actual $ACTUAL_M5_TOTAL"
|
||
elif [ -n "$CLAIMED_M5_TOTAL" ]; then
|
||
ok "AUDIT.md M5 total surface = $ACTUAL_M5_TOTAL"
|
||
fi
|
||
|
||
# M4 + M5 determinism invariant counts
|
||
ACTUAL_M4_LOTTERY_DET=$(grep -cE '#\[test\]' crates/mt-lottery/tests/determinism_invariants.rs)
|
||
ACTUAL_M4_CONSENSUS_DET=$(grep -cE '#\[test\]' crates/mt-consensus/tests/determinism_invariants.rs)
|
||
ACTUAL_M4_ENTRY_DET=$(grep -cE '#\[test\]' crates/mt-entry/tests/determinism_invariants.rs)
|
||
ACTUAL_M5_STORE_DET=$(grep -cE '#\[test\]' crates/mt-store/tests/determinism_invariants.rs)
|
||
|
||
if [ "$ACTUAL_M4_LOTTERY_DET" != "34" ]; then
|
||
err "mt-lottery determinism_invariants count drift: expected 34, actual $ACTUAL_M4_LOTTERY_DET"
|
||
else
|
||
ok "mt-lottery determinism_invariants = 34"
|
||
fi
|
||
if [ "$ACTUAL_M4_CONSENSUS_DET" != "27" ]; then
|
||
err "mt-consensus determinism_invariants count drift: expected 27, actual $ACTUAL_M4_CONSENSUS_DET"
|
||
else
|
||
ok "mt-consensus determinism_invariants = 27"
|
||
fi
|
||
if [ "$ACTUAL_M4_ENTRY_DET" != "24" ]; then
|
||
err "mt-entry determinism_invariants count drift: expected 24, actual $ACTUAL_M4_ENTRY_DET"
|
||
else
|
||
ok "mt-entry determinism_invariants = 24"
|
||
fi
|
||
if [ "$ACTUAL_M5_STORE_DET" != "17" ]; then
|
||
err "mt-store determinism_invariants count drift: expected 17 (v34 monetary refactor удалила MonetaryState persistence), actual $ACTUAL_M5_STORE_DET"
|
||
else
|
||
ok "mt-store determinism_invariants = 17"
|
||
fi
|
||
|
||
# ---------- Summary ----------
|
||
echo ""
|
||
if [ $EXIT -eq 0 ]; then
|
||
echo "================================================================"
|
||
echo "✅ Documentation drift check PASS — все числа sync с фактическим кодом"
|
||
echo "================================================================"
|
||
else
|
||
echo ""
|
||
echo "================================================================"
|
||
echo "❌ Documentation drift check FAIL — найдено ${#errors[@]} расхождений:"
|
||
for e in "${errors[@]}"; do
|
||
echo " - $e"
|
||
done
|
||
echo ""
|
||
echo "Действие: обновить AUDIT.md / docs/audit-checklist.md / docs/security-cards.md"
|
||
echo "так чтобы числа совпадали с фактическим кодом. Если числа в коде верны —"
|
||
echo "правьте .md файлы. Если в .md правильно (concept change) — обнови код."
|
||
echo "================================================================"
|
||
fi
|
||
|
||
exit $EXIT
|