diff --git a/.code-review-graphignore b/.code-review-graphignore new file mode 100644 index 000000000..98d798175 --- /dev/null +++ b/.code-review-graphignore @@ -0,0 +1,31 @@ +# BEGIN AI-HUB CRG GLOBAL POLICY +build/** +.worktrees/** +worktrees/** +.agents/worktrees/** +.code-review-graph/wiki/** +*.db +*.db-shm +*.db-wal +*.sqlite +*.sqlite3 +*.min.js +*.min.css +coverage/** +.coverage/** +.pytest_cache/** +.ruff_cache/** +.mypy_cache/** +.tox/** +.cache/** +tmp/** +temp/** +.claude/worktrees/** +.codex/worktrees/** +.gemini/worktrees/** +.kimi-code/worktrees/** +.cursor/worktrees/** +.config/worktrees/** +.vscode/worktrees/** +.aider/worktrees/** +# END AI-HUB CRG GLOBAL POLICY diff --git a/Makefile b/Makefile index 8e08bbf63..7d65ad5bc 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ export RUST_2024_LINTS := -D unsafe_op_in_unsafe_fn -D rust_2024_compatibility - gate = [ "$(APPLY)" = "Y" ] || { printf "DRY-RUN: would %s; set APPLY=Y to execute\n" "$(1)" >&2; exit 0; } # --- WHATS_ phase SSOT (drives sub-help + error arms) ------------------- -WHATS_check := fmt lint validate audit udeps coverage qlty all +WHATS_check := fmt lint staged validate audit udeps coverage qlty hooks guard all WHATS_fix := fmt lint docs all WHATS_dev := run docker-up docker-down docker-logs docker-test WHATS_docs := build serve lint validate sync rust check setup adr adr-new diagrams diff --git a/makefiles/dispatch.mk b/makefiles/dispatch.mk index cd8ccc943..8b7ea7f69 100644 --- a/makefiles/dispatch.mk +++ b/makefiles/dispatch.mk @@ -70,11 +70,14 @@ define DISPATCH_CHECK @case "$(WHAT)" in \ fmt) cargo fmt --all -- --check ;; \ lint) cargo fmt --all -- --check && cargo clippy --all-targets -- -D warnings ;; \ + staged) bash $(MCB_SH) check-staged ;; \ validate) bash $(MCB_SH) validate $(if $(filter 1,$(QUICK)),quick,full) ;; \ audit) cargo audit $(foreach i,$(MCB_AUDIT_IGNORES),--ignore $(i)) && $(MAKE) check WHAT=udeps ;; \ udeps) command -v cargo-udeps >/dev/null 2>&1 || cargo install cargo-udeps; cargo +nightly udeps --workspace ;; \ coverage) cargo tarpaulin --out Lcov --output-dir coverage --exclude-files 'crates/*/tests/integration/*' --exclude-files 'crates/*/tests/admin/*' --timeout 300 ;; \ qlty) mkdir -p docs/reports; ./scripts/analyze_qlty.py --scan --check --summary --markdown docs/reports/qlty-check-REPORTS.md; ./scripts/analyze_qlty.py --scan --smells --summary --markdown docs/reports/qlty-smells-REPORTS.md ;; \ + hooks) bash scripts/lib/tests/test-hooks.sh ;; \ + guard) bash $(MCB_SH) guard ;; \ ""|all) cargo fmt --all -- --check && $(MAKE) lint-impl && $(MAKE) test && bash $(MCB_SH) validate $(if $(filter 1,$(QUICK)),quick,full) ;; \ *) printf "ERRO: WHAT '%s' invalido. Validos: $(WHATS_check)\n" "$(WHAT)" >&2; exit 2 ;; \ esac @@ -202,7 +205,7 @@ define DISPATCH_GIT log) git log --oneline -$(or $(LOG_N),10) ;; \ show) git show --stat $(or $(REF),HEAD) ;; \ add) bash $(MCB_SH) files-safe "$(FILES)"; $(call require_var,FILES); git add $(FILES) ;; \ - commit) $(call require_var,MSG); bash $(MCB_SH) files-safe "$(FILES)"; [ -n "$(FILES)" ] && git add $(FILES) || true; $(call gate,commit); git commit -m "$(MSG)" ;; \ + commit) $(call require_var,MSG); bash $(MCB_SH) files-safe "$(FILES)"; $(call gate,commit); [ -n "$(FILES)" ] && git add $(FILES) || true; git commit -m "$(MSG)" ;; \ push) $(call gate,push $(BRANCH)); git push origin $(BRANCH) ;; \ pull) git pull origin $(BRANCH) ;; \ branch) [ -z "$(REF)" ] && git branch -a || git branch $(REF) $(BASE) ;; \ @@ -235,7 +238,7 @@ endef define DISPATCH_SUB @case "$(WHAT)" in \ ""|status) git submodule status ;; \ - sync) git submodule sync --recursive; git submodule update --init --recursive ;; \ + sync) bash $(MCB_SH) sync-submodules ;; \ diff) git submodule foreach --quiet 'D=$$(git diff); [ -n "$$D" ] && { echo "=== $$name ==="; git diff; } || true' ;; \ commit) $(call require_var,SUB); $(call require_var,MSG); $(call gate,commit in submodule $(SUB)); (cd third-party/$(SUB) && git add -A && git commit -m "$(MSG)") ;; \ push) $(call require_var,SUB); $(call gate,push submodule $(SUB)); (cd third-party/$(SUB) && git push) ;; \ @@ -247,10 +250,10 @@ endef # --- setup ------------------------------------------------------------------- define DISPATCH_SETUP @case "$(WHAT)" in \ - hooks) cp scripts/hooks/pre-commit .git/hooks/pre-commit; chmod +x .git/hooks/pre-commit; echo "✓ pre-commit hook installed" ;; \ + hooks) bash $(MCB_SH) install-hooks ;; \ tools) cargo install cargo-udeps cargo-audit cargo-tarpaulin 2>/dev/null || true; echo "✓ tools installed" ;; \ adr) ./scripts/setup/install-adr-tools.sh ;; \ - ""|all) cp scripts/hooks/pre-commit .git/hooks/pre-commit; chmod +x .git/hooks/pre-commit; echo "✓ pre-commit hook installed"; cargo install cargo-udeps cargo-audit cargo-tarpaulin 2>/dev/null || true; ./scripts/setup/install-adr-tools.sh 2>/dev/null || true; echo "✓ setup complete" ;; \ + ""|all) bash $(MCB_SH) install-hooks; cargo install cargo-udeps cargo-audit cargo-tarpaulin 2>/dev/null || true; ./scripts/setup/install-adr-tools.sh 2>/dev/null || true; echo "✓ setup complete" ;; \ *) printf "ERRO: WHAT '%s' invalido. Validos: $(WHATS_setup)\n" "$(WHAT)" >&2; exit 2 ;; \ esac endef diff --git a/scripts/hooks/pre-commit b/scripts/hooks/pre-commit index 0b5685a06..5faebd7df 100755 --- a/scripts/hooks/pre-commit +++ b/scripts/hooks/pre-commit @@ -1,11 +1,11 @@ #!/usr/bin/env bash -# Installed by `make setup WHAT=hooks`. Runs the same gates as CI, via the -# canonical monopoly. No bypass — fix the cause, not the gate (AGENTS.md §3). +# Installed by `make setup WHAT=hooks`. Runs the canonical commit gates without +# bypassing hooks or calling removed Make targets. # guard runs in --staged mode so it blocks NEW violations in this commit, not # the retroactive baseline; `make guard` (full tree) is the CI/manual scan. set -euo pipefail cd "$(git rev-parse --show-toplevel)" +[ -f scripts/lib/mcb.sh ] || exit 0 echo "→ guard (staged)…"; bash scripts/lib/mcb.sh guard --staged -echo "→ lint…"; make check WHAT=lint -echo "→ validate (quick)…"; make check WHAT=validate QUICK=1 +echo "→ staged check…"; make check WHAT=staged echo "✓ pre-commit passed" diff --git a/scripts/hooks/pre-push b/scripts/hooks/pre-push new file mode 100644 index 000000000..b39b43c45 --- /dev/null +++ b/scripts/hooks/pre-push @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Installed by `make setup WHAT=hooks`. Runs the canonical full gate before +# push, then preserves Beads hook processing when available. +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" +[ -f scripts/lib/mcb.sh ] || exit 0 +make check WHAT=all +if command -v bd >/dev/null 2>&1; then + exec bd hooks run pre-push "$@" +fi diff --git a/scripts/lib/mcb.sh b/scripts/lib/mcb.sh index f0e61c6d6..994a1d082 100755 --- a/scripts/lib/mcb.sh +++ b/scripts/lib/mcb.sh @@ -51,6 +51,54 @@ mcb_retry() { local n="$1" s="$2"; shift 2; local t=1; while ! "$@"; do [ "$t" - # --- SSOT readers ------------------------------------------------------------ mcb_version() { grep -m1 '^version =' "$MCB_ROOT/Cargo.toml" | sed 's/.*"\([^"]*\)".*/\1/'; } +mcb_git_hooks_dir() { + local repo="${1:-$MCB_ROOT}" + local common_dir + common_dir="$(git -C "$repo" rev-parse --path-format=absolute --git-common-dir)" \ + || mcb_die "$EX_PREREQ" "nao foi possivel resolver o diretorio Git comum de '$repo'" + printf '%s/hooks\n' "$common_dir" +} + +mcb_install_hooks() { + local repo="${1:-$MCB_ROOT}" + local hooks_dir + hooks_dir="$(mcb_git_hooks_dir "$repo")" + mkdir -p "$hooks_dir" + cp "$MCB_ROOT/scripts/hooks/pre-commit" "$MCB_ROOT/scripts/hooks/pre-push" "$hooks_dir/" + chmod +x "$hooks_dir/pre-commit" "$hooks_dir/pre-push" + mcb_ok "pre-commit and pre-push hooks installed at $hooks_dir" +} + +mcb_sync_submodules() { + local repo="${1:-$MCB_ROOT}" + local materialized + + git -C "$repo" submodule sync --recursive + git -C "$repo" submodule update --init --recursive + + while :; do + materialized="$(git -C "$repo" submodule foreach --quiet --recursive ' + present="$(git ls-files -z | while IFS= read -r -d "" path; do + if [ -e "$path" ] || [ -L "$path" ]; then + printf 1 + break + fi + done)" + [ -n "$present" ] && exit 0 + tracked="$(git ls-files | wc -l)" + if [ "$tracked" -eq 0 ]; then + git read-tree HEAD + tracked="$(git ls-files | wc -l)" + fi + [ "$tracked" -eq 0 ] && exit 0 + git checkout-index --all + printf "%s\n" "$sm_path" + ')" + [ -z "$materialized" ] && break + git -C "$repo" submodule update --init --recursive + done +} + # Binary lookup chain: PATH > target/release > target/debug > cargo run mcb_bin() { command -v mcb 2>/dev/null && return 0 @@ -77,6 +125,38 @@ mcb_validate() { # $1 = "quick" | "full" # FILES word-split safety (ported from cosmos Makefile:80): refuse shell metachars. mcb_files_safe() { printf '%s' "${1:-}" | grep -qE '[;|&`$()<>]' && mcb_die "$EX_PREREQ" "FILES contem metacaractere de shell perigoso; liste apenas caminhos"; return 0; } +mcb_check_staged() { + local crate_dir deadline=60 manifest package packages="" + local staged + + mcb_require_cmd timeout + staged="$(git -C "$MCB_ROOT" diff --cached --name-only --diff-filter=ACMR)" + [ -n "$staged" ] || { mcb_ok "staged check: no staged paths"; return 0; } + + if printf '%s\n' "$staged" | grep -qE '^Cargo\.(toml|lock)$'; then + packages="--workspace" + else + while IFS= read -r path; do + case "$path" in + crates/*/src/*.rs) + crate_dir="$(printf '%s\n' "$path" | cut -d/ -f1-2)" + manifest="$crate_dir/Cargo.toml" + [ -f "$MCB_ROOT/$manifest" ] || continue + package="$(sed -n '/^\[package\]/,/^\[/s/^name = "\([^"]*\)"/\1/p' "$MCB_ROOT/$manifest" | head -1)" + [ -n "$package" ] || mcb_die "$EX_PREREQ" "package name ausente em '$manifest'" + case " $packages " in *" -p $package "*) ;; *) packages="$packages -p $package" ;; esac + ;; + esac + done <<< "$staged" + fi + + [ -n "$packages" ] || { mcb_ok "staged check: no Rust package affected"; return 0; } + mcb_log "staged check: cargo fmt/clippy scope:$packages (deadline ${deadline}s each)" + timeout --signal=TERM --kill-after=5s "${deadline}s" cargo fmt $packages -- --check + timeout --signal=TERM --kill-after=5s "${deadline}s" cargo clippy $packages --all-targets -- -D warnings + mcb_ok "staged check: clean" +} + # --- banned-pattern guard ---------------------------------------------------- # Scans first-party crates/ for the constructs AGENTS.md forbids in prod paths. # Excludes: tests, #[cfg(test)] modules, target/. Fails EX_GUARD. @@ -94,15 +174,20 @@ mcb_guard() { [ -z "$src" ] && { mcb_warn "guard: no source files found under crates/"; return 0; } fi # 1. unwrap/expect/panic/todo/unimplemented in non-test .rs - hits=$(grep -rnE '\b(unwrap|expect)\(|\bpanic!|\btodo!|\bunimplemented!' $src 2>/dev/null \ - | grep -vE '//.*(unwrap|expect)|#\[cfg\(test\)\]' || true) + hits=$(grep -rnE '\.(unwrap|expect)\(|\b(panic|todo|unimplemented)!\(' $src 2>/dev/null \ + | grep -vE ':[[:space:]]*(//|///|//!)|#\[cfg\(test\)\]|pub const .*: &str = |\.message\("|message = "|suggestion = "|r".*(unwrap|expect|panic|todo|unimplemented)|line\.contains\("todo!"\)' || true) [ -n "$hits" ] && { mcb_warn "prod unwrap/expect/panic/todo:"; printf '%s\n' "$hits" >&2; rc=$EX_GUARD; } # 2. TODO/FIXME markers hits=$(grep -rnE '\b(TODO|FIXME)\b' $src 2>/dev/null || true) [ -n "$hits" ] && { mcb_warn "TODO/FIXME markers:"; printf '%s\n' "$hits" >&2; rc=$EX_GUARD; } - # 3. unjustified suppression directives (#[allow(...)] with no trailing // Why:) - hits=$(grep -rnE '#\[allow\(' $src 2>/dev/null | grep -vE '//\s*Why:' || true) - [ -n "$hits" ] && { mcb_warn "#[allow] without // Why: justification:"; printf '%s\n' "$hits" >&2; rc=$EX_GUARD; } + if [ "$staged" = "1" ]; then + hits=$(git -C "$MCB_ROOT" diff --cached --unified=0 -- crates 2>/dev/null \ + | grep -E '^\+[^+].*#\[allow\(' | grep -vE '#\[allow\([^]]+\)\][[:space:]]*//[[:space:]]*[^[:space:]]' || true) + else + hits=$(git -C "$MCB_ROOT" diff origin/main...HEAD --unified=0 -- crates 2>/dev/null \ + | grep -E '^\+[^+].*#\[allow\(' | grep -vE '#\[allow\([^]]+\)\][[:space:]]*//[[:space:]]*[^[:space:]]' || true) + fi + [ -n "$hits" ] && { mcb_warn "new #[allow] without a trailing rationale:"; printf '%s\n' "$hits" >&2; rc=$EX_GUARD; } [ "$rc" -eq 0 ] && mcb_ok "guard: clean" return "$rc" } @@ -127,7 +212,11 @@ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then version) mcb_version ;; bin) mcb_bin ;; ignores) printf '%s\n' "${MCB_AUDIT_IGNORES[*]}" ;; + git-hooks-dir) mcb_git_hooks_dir "${2:-$MCB_ROOT}" ;; + install-hooks) mcb_install_hooks "${2:-$MCB_ROOT}" ;; + sync-submodules) mcb_sync_submodules "${2:-$MCB_ROOT}" ;; validate) mcb_validate "${2:-full}" ;; + check-staged) mcb_check_staged ;; guard) shift; mcb_guard "$@" ;; guard-bash) mcb_guard_bash ;; files-safe) mcb_files_safe "${2:-}" ;; diff --git a/scripts/lib/tests/test-hooks.sh b/scripts/lib/tests/test-hooks.sh new file mode 100644 index 000000000..c36add9c7 --- /dev/null +++ b/scripts/lib/tests/test-hooks.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +FIXTURE="$(mktemp -d)" +trap 'rm -rf "$FIXTURE"' EXIT + +PRIMARY="$FIXTURE/primary" +LINKED="$FIXTURE/linked" +SUB_SOURCE="$FIXTURE/sub-source" +SUB_ARCHIVE="$FIXTURE/sub-archive" +BIN="$FIXTURE/bin" +MAKE_LOG="$FIXTURE/make.log" +CARGO_LOG="$FIXTURE/cargo.log" + +git init -q "$PRIMARY" +git -C "$PRIMARY" config user.email test@example.com +git -C "$PRIMARY" config user.name "MCB Test" +mkdir -p "$PRIMARY/scripts/hooks" "$PRIMARY/scripts/lib" +cp "$ROOT/scripts/hooks/pre-commit" "$PRIMARY/scripts/hooks/pre-commit" +cp "$ROOT/scripts/hooks/pre-push" "$PRIMARY/scripts/hooks/pre-push" +cp "$ROOT/scripts/lib/mcb.sh" "$PRIMARY/scripts/lib/mcb.sh" +git -C "$PRIMARY" add scripts +git -C "$PRIMARY" commit -q -m initial +git -C "$PRIMARY" worktree add -q -b linked-test "$LINKED" + +NON_MCB="$FIXTURE/non-mcb" +git init -q "$NON_MCB" +git -C "$NON_MCB" config user.email test@example.com +git -C "$NON_MCB" config user.name "MCB Test" +git -C "$NON_MCB" commit --allow-empty -q -m initial + +PRIMARY_HOOKS="$(bash "$ROOT/scripts/lib/mcb.sh" git-hooks-dir "$PRIMARY")" +LINKED_HOOKS="$(bash "$ROOT/scripts/lib/mcb.sh" git-hooks-dir "$LINKED")" +EXPECTED_HOOKS="$(git -C "$PRIMARY" rev-parse --path-format=absolute --git-common-dir)/hooks" +PRIMARY_GIT_DIR="$(git -C "$PRIMARY" rev-parse --path-format=absolute --git-dir)" +LINKED_GIT_DIR="$(git -C "$LINKED" rev-parse --path-format=absolute --git-dir)" + +test "$PRIMARY_HOOKS" = "$EXPECTED_HOOKS" +test "$LINKED_HOOKS" = "$EXPECTED_HOOKS" +test "$PRIMARY_GIT_DIR" != "$LINKED_GIT_DIR" +test -f "$LINKED/.git" + +bash "$PRIMARY/scripts/lib/mcb.sh" install-hooks "$LINKED" +cmp "$ROOT/scripts/hooks/pre-commit" "$EXPECTED_HOOKS/pre-commit" +cmp "$ROOT/scripts/hooks/pre-push" "$EXPECTED_HOOKS/pre-push" +test -x "$EXPECTED_HOOKS/pre-commit" +test -x "$EXPECTED_HOOKS/pre-push" +if grep -q 'make boot' "$EXPECTED_HOOKS/pre-commit"; then + printf 'installed pre-commit references removed make boot target\n' >&2 + exit 1 +fi + +mkdir -p "$BIN" +cat > "$BIN/make" <> "$MAKE_LOG" +EOF +chmod +x "$BIN/make" + +cat > "$BIN/cargo" <> "$CARGO_LOG" +EOF +chmod +x "$BIN/cargo" + +cat > "$BIN/timeout" <<'EOF' +#!/usr/bin/env bash +while [ "$#" -gt 0 ]; do + case "$1" in + --signal=*|--kill-after=*|*[smhd]) shift ;; + *) break ;; + esac +done +exec "$@" +EOF +chmod +x "$BIN/timeout" + +(cd "$PRIMARY" && PATH="$BIN:$PATH" "$EXPECTED_HOOKS/pre-commit") +(cd "$LINKED" && PATH="$BIN:$PATH" "$EXPECTED_HOOKS/pre-commit") +test "$(grep -c 'check WHAT=staged' "$MAKE_LOG")" -eq 2 +if grep -qE 'check WHAT=(lint|validate)' "$MAKE_LOG"; then + printf 'pre-commit invoked an unrelated full-workspace gate\n' >&2 + exit 1 +fi +grep -q "^$PRIMARY|check WHAT=staged$" "$MAKE_LOG" +grep -q "^$LINKED|check WHAT=staged$" "$MAKE_LOG" + +: > "$MAKE_LOG" +(cd "$PRIMARY" && PATH="$BIN:$PATH" "$EXPECTED_HOOKS/pre-push") +grep -q "^$PRIMARY|check WHAT=all$" "$MAKE_LOG" + +mkdir -p "$PRIMARY/crates/sample/src" +cat > "$PRIMARY/crates/sample/Cargo.toml" <<'EOF' +[package] +name = "sample" +version = "0.1.0" +EOF +printf 'pub fn staged() {}\n' > "$PRIMARY/crates/sample/src/lib.rs" +git -C "$PRIMARY" add crates/sample +(cd "$PRIMARY" && PATH="$BIN:$PATH" bash scripts/lib/mcb.sh check-staged) +grep -q '^fmt -p sample -- --check$' "$CARGO_LOG" +grep -q '^clippy -p sample --all-targets -- -D warnings$' "$CARGO_LOG" +if grep -q -- '--workspace' "$CARGO_LOG"; then + printf 'staged crate check expanded to unrelated workspace scope\n' >&2 + exit 1 +fi + +git init -q "$SUB_SOURCE" +git -C "$SUB_SOURCE" config user.email test@example.com +git -C "$SUB_SOURCE" config user.name "MCB Test" +printf '[package]\nname = "fixture"\nversion = "0.1.0"\n' > "$SUB_SOURCE/Cargo.toml" +git -C "$SUB_SOURCE" add Cargo.toml +git -C "$SUB_SOURCE" commit -q -m initial +git -C "$PRIMARY" -c protocol.file.allow=always submodule add -q "$SUB_SOURCE" vendor/sample +PATH="$BIN:$PATH" git -C "$PRIMARY" commit -q -am 'add fixture submodule' + +mkdir -p "$SUB_ARCHIVE" +mv "$PRIMARY/vendor/sample/Cargo.toml" "$SUB_ARCHIVE/Cargo.toml" +git -C "$PRIMARY/vendor/sample" read-tree --empty +test ! -f "$PRIMARY/vendor/sample/Cargo.toml" +test "$(git -C "$PRIMARY/vendor/sample" ls-files | wc -l)" -eq 0 +bash "$PRIMARY/scripts/lib/mcb.sh" sync-submodules "$PRIMARY" +test -f "$PRIMARY/vendor/sample/Cargo.toml" + +printf 'preserved local content\n' > "$PRIMARY/vendor/sample/Cargo.toml" +bash "$PRIMARY/scripts/lib/mcb.sh" sync-submodules "$PRIMARY" +grep -q '^preserved local content$' "$PRIMARY/vendor/sample/Cargo.toml" + +printf 'hook fixtures: primary and linked worktree install and commit gates passed via %s\n' "$EXPECTED_HOOKS"