pmat 3.11.0

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP, HTTP)
//! Hook content generation: templates, environment variables, and quality checks

#![cfg_attr(coverage_nightly, coverage(off))]

use super::hooks_command::HooksCommand;
use crate::services::configuration_service::{configuration, PmatConfig};
use anyhow::Result;
use chrono::Local;
use std::fs;
use std::path::Path;

impl HooksCommand {
    /// Normalize hook content by removing timestamp line for comparison
    ///
    /// # Complexity
    /// - Time: O(n) where n is content length
    /// - Cyclomatic: 3
    pub(super) fn normalize_hook_content(content: &str) -> String {
        content
            .lines()
            .filter(|line| !line.contains("# Generated at:"))
            .collect::<Vec<_>>()
            .join("\n")
    }

    /// Check if a hook is PMAT-managed
    pub(super) fn is_pmat_managed(&self, hook_path: &Path) -> Result<bool> {
        if !hook_path.exists() {
            return Ok(false);
        }

        let content = fs::read_to_string(hook_path)?;
        Ok(content.contains("auto-managed by PMAT") && content.contains("DO NOT EDIT"))
    }

    /// Generate hook content from template and configuration
    pub(super) async fn generate_hook_content(&self) -> Result<String> {
        let config_service = configuration();
        let config = config_service.get_config()?;

        let header = self.generate_hook_header();
        let env_vars = self.generate_env_vars(&config);
        let checks = self.generate_quality_checks();

        let hook_content = format!("{header}\n{env_vars}\n{checks}");
        Ok(hook_content)
    }

    /// Generate hook header section
    pub(crate) fn generate_hook_header(&self) -> String {
        format!(
            r#"#!/bin/bash
# Generated pre-commit hook (auto-managed by PMAT)
# DO NOT EDIT: This file is automatically generated
# Generated at: {}

set -e

echo "🔍 PMAT Pre-commit Quality Gates"
echo "================================"
"#,
            Local::now().format("%Y-%m-%d %H:%M:%S")
        )
    }

    /// Generate environment variables section
    fn generate_env_vars(&self, config: &PmatConfig) -> String {
        format!(
            r#"# Load current configuration dynamically
export PMAT_MAX_CYCLOMATIC_COMPLEXITY={}
export PMAT_MAX_COGNITIVE_COMPLEXITY={}
export PMAT_MIN_TEST_COVERAGE={}
export PMAT_MAX_SATD_COMMENTS=5
export PMAT_TASK_ID_PATTERN="PMAT-[0-9]{{4}}"
"#,
            config.quality.max_complexity,
            config.quality.max_cognitive_complexity,
            config.quality.min_coverage as u32
        )
    }

    /// Generate the cargo fmt --check gate for Rust projects
    fn generate_format_check() -> &'static str {
        r#"# 0. Format check (Rust only — cargo fmt --check on staged .rs files)
STAGED_RS=$(git diff --cached --name-only --diff-filter=ACMR -- '*.rs' 2>/dev/null)
if [ -n "$STAGED_RS" ] && command -v cargo &> /dev/null && [ -f Cargo.toml ]; then
    echo -n "  Format check... "
    FMT_OUTPUT=$(cargo fmt -- --check 2>&1)
    if [ $? -eq 0 ]; then
        echo "✅"
    else
        echo "❌"
        echo "   Unformatted Rust code detected. Run 'cargo fmt' before committing."
        echo "$FMT_OUTPUT" | grep "^Diff in" | head -5
        exit 1
    fi
fi
"#
    }

    /// Generate quality check sections
    ///
    /// The generated hook is project-type aware:
    /// - Non-code repos (no source files at all) get a fast pass
    /// - Mixed repos only check staged source files
    /// - SATD and docs checks only run when source files exist in the repo
    pub(crate) fn generate_quality_checks(&self) -> String {
        let mut hook = String::from(
            r#"# Check if pmat is available
if ! command -v pmat &> /dev/null; then
    echo "⚠️  Warning: pmat not found in PATH"
    echo "   Install with: cargo install pmat"
    exit 0  # Allow commit but warn
fi

echo "📊 Running quality gate checks..."

# Detect if this repo has any source files at all (not just staged ones).
# Non-code repos (docs, configs, YAML, contracts) get a fast pass.
HAS_SOURCE_FILES=$(find . -maxdepth 4 -type f \( -name '*.rs' -o -name '*.py' -o -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.jsx' -o -name '*.go' -o -name '*.c' -o -name '*.cpp' -o -name '*.lua' -o -name '*.php' -o -name '*.swift' \) -not -path './.git/*' -not -path '*/target/*' -not -path '*/node_modules/*' -print -quit 2>/dev/null)

if [ -z "$HAS_SOURCE_FILES" ]; then
    echo "  Project type: non-code (docs/configs/YAML)"
    echo "  Complexity check... ⏭️  (no source files in repo)"
    echo "  SATD check... ⏭️  (no source files in repo)"
    echo ""
    echo "✅ All quality gates passed!"
    echo ""
    exit 0
fi

"#,
        );
        hook.push_str(Self::generate_format_check());
        hook.push_str(r#"
# 1. Complexity analysis (only staged source files, not entire project)
# Supports: Rust, Python, TypeScript, JavaScript, Go, C, C++, Lua, PHP, Swift
STAGED_SRC=$(git diff --cached --name-only --diff-filter=ACMR -- '*.rs' '*.py' '*.ts' '*.tsx' '*.js' '*.jsx' '*.go' '*.c' '*.cpp' '*.lua' '*.php' '*.swift' 2>/dev/null | head -20)
if [ -n "$STAGED_SRC" ]; then
    echo -n "  Complexity check... "
    COMPLEXITY_FAILED=0
    COMPLEXITY_DETAILS=""
    for SRC_FILE in $STAGED_SRC; do
        if [ -f "$SRC_FILE" ]; then
            FILE_OUTPUT=$(pmat analyze complexity --file "$SRC_FILE" --max-cyclomatic $PMAT_MAX_CYCLOMATIC_COMPLEXITY --max-cognitive $PMAT_MAX_COGNITIVE_COMPLEXITY 2>&1)
            if echo "$FILE_OUTPUT" | grep -q 'Errors.*: [1-9]'; then
                COMPLEXITY_FAILED=1
                # Extract the offending file and functions
                COMPLEXITY_DETAILS="${COMPLEXITY_DETAILS}  ${SRC_FILE}:"$'\n'
                FILE_VIOLATIONS=$(echo "$FILE_OUTPUT" | grep -E '^[0-9]+\. ' | grep -E 'Cyclomatic|Cognitive' | head -3)
                COMPLEXITY_DETAILS="${COMPLEXITY_DETAILS}${FILE_VIOLATIONS}"$'\n'
            fi
        fi
    done
    if [ "$COMPLEXITY_FAILED" -eq 0 ]; then
        echo "✅"
    else
        echo "❌"
        echo "Issues Found:"
        echo "$COMPLEXITY_DETAILS" | head -10
        echo "   Complexity exceeds thresholds (Cyclomatic: $PMAT_MAX_CYCLOMATIC_COMPLEXITY, Cognitive: $PMAT_MAX_COGNITIVE_COMPLEXITY)"
        exit 1
    fi
else
    echo "  Complexity check... ⏭️  (no source files staged)"
fi

# 2. SATD (Self-Admitted Quality Issues) check - informational only
echo -n "  SATD check... "
SATD_OUTPUT=$(pmat analyze satd 2>&1)
SATD_COUNT=$(echo "$SATD_OUTPUT" | grep -oP 'Total SATD comments found: \K[0-9]+' || echo "0")
if [ "$SATD_COUNT" -le "$PMAT_MAX_SATD_COMMENTS" ] 2>/dev/null; then
    echo "✅ ($SATD_COUNT SATD comments)"
else
    echo "⚠️  ($SATD_COUNT SATD comments, threshold: $PMAT_MAX_SATD_COMMENTS)"
fi

# 3. Documentation synchronization (only if docs structure exists)
if [ -d "docs/execution" ] || [ -f "CHANGELOG.md" ]; then
    echo -n "  Documentation check... "
    if [ -f "docs/execution/roadmap.md" ] && [ -f "CHANGELOG.md" ]; then
        echo "✅"
    else
        echo "⚠️  (docs/execution/roadmap.md or CHANGELOG.md missing)"
    fi
fi

# 4. Task ID validation (if commit message available)
if [ -n "$1" ]; then
    echo -n "  Task ID check... "
    if echo "$1" | grep -qE "$PMAT_TASK_ID_PATTERN"; then
        echo "✅"
    else
        echo "⚠️"
        echo "   Warning: Commit message should contain task ID matching $PMAT_TASK_ID_PATTERN"
    fi
fi

echo ""
echo "✅ All quality gates passed!"
echo ""

# Success
exit 0
"#);
        hook
    }
}