#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
BIN="$REPO_ROOT/target/debug/codelattice"
TMP_DIR="$(mktemp -d /tmp/codelattice-precommit-XXXXXX)"
REPORT="${CODELATTICE_PRECOMMIT_REPORT:-/tmp/codelattice-precommit-detect-changes.json}"
LANGUAGE="rust"
SCOPE="all"
RUN_FULL_TEST=0
FAIL_ON_HIGH_RISK=0
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
usage() {
cat <<'USAGE'
Usage: scripts/codelattice-precommit-check.sh [options]
Options:
--language <lang> Language for native detect-changes (default: rust)
--scope <scope> Change scope: all/staged/unstaged/head (default: all)
--full Also run full `cargo test`
--fail-on-high-risk Exit non-zero when native detect-changes reports high/critical
-h, --help Show this help
This script runs:
1. cargo fmt --check
2. git diff --check
3. cargo test --test productization_commands
4. cargo test --test mcp_server
5. scripts/codelattice-mcp-concurrency-smoke.sh
6. scripts/codelattice-detect-changes-smoke.sh
7. codelattice detect-changes --scope <scope>
It does not call GitNexus-Tool by default.
Set CODELATTICE_PRECOMMIT_REPORT to override the output report path.
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--language)
LANGUAGE="${2:-}"
shift 2
;;
--scope)
SCOPE="${2:-}"
shift 2
;;
--full)
RUN_FULL_TEST=1
shift
;;
--fail-on-high-risk)
FAIL_ON_HIGH_RISK=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 2
;;
esac
done
if [[ -z "$LANGUAGE" || -z "$SCOPE" ]]; then
echo "language and scope must be non-empty" >&2
exit 2
fi
step() {
echo
echo "==> $*"
}
step "CodeLattice native precommit check"
echo "Repo: $REPO_ROOT"
echo "Language: $LANGUAGE"
echo "Scope: $SCOPE"
cd "$REPO_ROOT"
step "cargo fmt --check"
cargo fmt --check
step "git diff --check"
git diff --check
step "cargo test --test productization_commands"
cargo test --test productization_commands
step "cargo test --test mcp_server"
cargo test --test mcp_server
step "CodeLattice MCP concurrency smoke"
bash "$REPO_ROOT/scripts/codelattice-mcp-concurrency-smoke.sh"
step "CodeLattice detect-changes smoke"
bash "$REPO_ROOT/scripts/codelattice-detect-changes-smoke.sh"
if [[ "$RUN_FULL_TEST" -eq 1 ]]; then
step "cargo test"
cargo test
fi
step "Build debug codelattice binary"
cargo build -p gitnexus-rust-core-cli --bin codelattice --quiet
step "Native detect-changes"
"$BIN" detect-changes \
--root "$REPO_ROOT" \
--language "$LANGUAGE" \
--scope "$SCOPE" \
--format json \
>"$REPORT"
CHANGE_REVIEW="/tmp/codelattice-change-review.json"
cp "$REPORT" "$CHANGE_REVIEW"
python3 - "$REPORT" "$FAIL_ON_HIGH_RISK" <<'PY'
import json
import sys
path = sys.argv[1]
fail_on_high = sys.argv[2] == "1"
data = json.load(open(path))
summary = data.get("summary", {})
risk = summary.get("riskLevel", "unknown")
ws = data.get("workspaceContext", {}) or {}
affected_projects = data.get("affectedProjects", []) or []
unsupported_hits = data.get("unsupportedBoundaryHits", []) or []
cross_risk = data.get("crossProjectRisk")
file_owners = data.get("fileOwners", []) or []
followups = data.get("recommendedFollowups", []) or []
print(f"schema: {data.get('schemaVersion')}")
print(f"risk: {risk}")
print(f"tracked files: {summary.get('changedFileCount')}")
print(f"untracked files: {summary.get('untrackedFileCount')}")
print(f"total files: {summary.get('totalFileChangeCount')}")
print(f"changed symbols: {summary.get('changedSymbolCount')}")
print(f"unknown hunks: {summary.get('unknownHunkCount')}")
affected_process_count = summary.get("affectedProcessCount")
print(f"affectedProcessCount: {affected_process_count if affected_process_count is not None else 'null'}")
print()
print("── workspace ──")
print(f"workspace: {ws.get('workspaceRoot', 'N/A')}")
print(f"isWorkspace: {ws.get('isWorkspace', False)}")
print(f"projects: {ws.get('projectCount', 0)} ({ws.get('supportedProjectCount', 0)} supported, {ws.get('unsupportedProjectCount', 0)} unsupported)")
print(f"graph available: {ws.get('workspaceGraphAvailable', False)}")
print(f"affected projects: {len(affected_projects)}")
print(f"workspace edges: {len(data.get('affectedWorkspaceEdges', []) or [])}")
print(f"unsupported boundary hits: {len(unsupported_hits)}")
print(f"cross-project risk: {cross_risk or 'N/A'}")
print()
print("── 变更智能摘要 ──")
print(f"风险等级: {risk}")
print(f"变更文件数: {summary.get('changedFileCount', 0)}")
print(f"变更符号数: {summary.get('changedSymbolCount', 0)}")
print(f"受影响项目数: {len(affected_projects)}")
print(f"跨项目风险: {cross_risk or '无'}")
print(f"不支持语言边界: {len(unsupported_hits)} 个")
if unsupported_hits:
for hit in unsupported_hits[:3]:
print(f" ⚠ {hit.get('label', '?')} ({hit.get('language', '?')}) — {hit.get('reason', '')}")
if followups:
print("推荐跟进:")
for f in followups[:5]:
print(f" → {f}")
if data.get("schemaVersion") != "codelattice.detectChanges.v1":
raise SystemExit("unexpected detect-changes schema")
if data.get("generatedFrom", {}).get("nativeCodeLattice") is not True:
raise SystemExit("detect-changes report is not native CodeLattice output")
if risk in {"high", "critical"}:
print(f"\n⚠️ 风险等级为 {risk},请在提交前审查变更内容")
if fail_on_high:
raise SystemExit(1)
PY
echo
echo "Native report: $REPORT"
echo "Change review: $CHANGE_REVIEW"
echo "All CodeLattice native precommit checks completed."