# SPI Pre-commit Configuration
# Enhanced validation for Service Provider Interface architectural compliance
#
# PERFORMANCE OPTIMIZATION NOTES:
# - Uses ruff for formatting, import sorting, and linting (replaces black + isort)
# - Order: ruff format -> ruff check -> mypy (formatting before linting before type checking)
# - Use `pre-commit run --all-files` to test all hooks before pushing

repos:
  # Stub implementation detection — sourced from omnibase_core (OMN-5599)
  - repo: https://github.com/OmniNode-ai/omnibase_core
    rev: 458f44ef24564956c6430ae7671570aed035c853 # pragma: allowlist secret
    hooks:
      - id: check-stub-implementations

  # OMN-14350: non-canonical lifecycle-class ratchet (fan-out from the omnibase_core
  # canary #1430). Consumes the canonical omnibase_core remote hook and ratchets this
  # repo's frozen residual set (.onex_ratchets/noncanonical_class_allowlist.yaml) DOWN
  # toward zero. rev MUST be a specific core SHA carrying the validator + its
  # .pre-commit-hooks.yaml export (core dev HEAD after #1430 squash-merges) — never a
  # moving ref like @dev (required gate; deterministic-truth doctrine). The token below
  # is a HOLD placeholder; this PR stays unopened until the real post-merge SHA is set.
  #
  # OMN-20304: the same block carries the canonical-file-shape ratchet (operator
  # ruling 2026-10-01). It refuses a new code file outside the canonical
  # contract/node/handler/models/protocols/tests/docs locations, new imperative
  # lines in a SKILL.md, and any new baseline, allowlist or suppression entry.
  # Shrink-only baseline at .onex_ratchets/canonical_file_shape_baseline.txt.
  # The rev moved from 6363509770 to the OMN-20304 commit for both hooks; the
  # lifecycle validator's source is identical at the two commits.
  - repo: https://github.com/OmniNode-ai/omnibase_core
    rev: f50d00fc7918d0b12ccd0d3497160325e32e7ac5 # pragma: allowlist secret
    hooks:
      - id: no-noncanonical-lifecycle-classes
        args: [--allowlist, .onex_ratchets/noncanonical_class_allowlist.yaml, src/omnibase_spi]
        files: ^src/omnibase_spi/.*\.py$
      - id: canonical-file-shape

  # OMN-20298: shape-gate detector jobs must not depend on occ-preflight
  # (guard from omnibase_core PR 1833).
  - repo: local
    hooks:
      - id: check-shape-gate-independence
        name: Shape-gate detectors never depend on the OCC preflight (OMN-20298)
        entry: uv run --no-project --with "omnibase-core @ git+https://github.com/OmniNode-ai/omnibase_core@d17f5be8be88a8d35fc6a69e13cb8c1896c3025b" python -m omnibase_core.handlers.handler_shape_gate_independence
        language: system
        files: ^\.github/workflows/.*\.ya?ml$
        pass_filenames: true

  # YAML formatting (standard across ecosystem)
  - repo: https://github.com/google/yamlfmt
    rev: v0.21.0
    hooks:
      - id: yamlfmt
        args: [--formatter, retain_line_breaks=true]
        exclude: ^(tests/fixtures/validation/|\.github/workflows/)

  # Essential file formatting (standard across ecosystem)
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v6.0.0
    hooks:
      - id: end-of-file-fixer
        exclude: ^(\.github/workflows/|\.mcp\.json$)
      - id: check-merge-conflict
      - id: check-added-large-files
        args: [--maxkb=1000]

  # Cross-repo enforcement hooks from onex_change_control
  - repo: https://github.com/OmniNode-ai/onex_change_control
    rev: 8d7e85bc00e7f1b1306d1cb40949be1ab6e7e91a
    hooks:
      - id: no-hardcoded-topics
        # Pre-existing violations — pending migration to canonical topic constants.
        # Justifications for each exclude:
        #   contract_git_hook_event.py        — default_topic in event contract (1 violation)
        #   contract_linear_snapshot_event.py  — default_topic in event contract (1 violation)
        #   contract_github_pr_status_event.py — default_topic in event contract (1 violation)
        #   event_registry.py                 — topic strings in registry mapping (6 violations)
        #   default_github_pr_poller.yaml     — topic in YAML contract default (1 violation)
        #   exceptions_skill_routing.py       — kafka_payload() event_type literal (1 violation)
        exclude: ^(src/omnibase_spi/contracts/events/contract_git_hook_event\.py|src/omnibase_spi/contracts/events/contract_linear_snapshot_event\.py|src/omnibase_spi/contracts/events/contract_github_pr_status_event\.py|src/omnibase_spi/registry/event_registry\.py|src/omnibase_spi/contracts/defaults/default_github_pr_poller\.yaml|src/omnibase_spi/exceptions_skill_routing\.py)$
      - id: cosmetic-lint
      - id: no-untracked-todos
        stages: [pre-commit]

  # ONEX String Version Anti-Pattern Detection
  # TODO(OMN-3832-followup): Re-enable once validate-string-versions is packaged as a
  # proper Python CLI entrypoint in omnibase_core (currently only works as a local script).

  # OMN-19183 plan task A3.x: shrink-only baseline config/hardcoded_model_config_baseline.yaml;
  # no inline suppression marker; pin must equal core_ref in .github/workflows/hardcoded-model-config.yml.
  - repo: https://github.com/OmniNode-ai/omnibase_core
    rev: f873d2c30201c05d4be020fa0b281b5edbfb8307 # v0.47.24
    hooks:
      - id: check-hardcoded-model-config-compute

  # Configuration and Security Hooks
  - repo: local
    hooks:
      - id: no-env-file
        name: Reject committed .env files (except .env.example)
        entry: "bash -c 'if git diff --cached --name-only | grep -qE \"(^|/)\\\\.env([._-].+)?$\" | grep -vE \"\\\\.env\\\\.example\"; then echo \"BLOCKED: .env staged — use ~/.omnibase/.env instead. Run: git reset HEAD <file>\"; exit 1; fi'"
        language: system
        pass_filenames: false
        always_run: true
        stages: [pre-commit]

      - id: check-dep-provenance
        name: Forbid first-party git-source overrides (OMN-13873)
        entry: uv run --frozen python scripts/check_dep_provenance.py
        language: system
        files: ^pyproject\.toml$
        pass_filenames: false
        stages: [pre-commit]

  # SPI Python Development Hooks
  # Order: ruff format -> ruff check (with import sorting) -> mypy
  # Ruff replaces both black and isort for faster, unified tooling
  - repo: local
    hooks:
      - id: ruff-format
        name: ruff format (auto-format)
        entry: uv run ruff format
        language: system
        types: [python]
        exclude: ^(tests/fixtures/validation/|\.github/workflows/).*$
        stages: [pre-commit]

      - id: ruff-fix
        name: ruff check (lint + import sort)
        entry: uv run ruff check --fix
        language: system
        types: [python]
        exclude: ^(tests/fixtures/validation/|\.github/workflows/).*$
        stages: [pre-commit]

      - id: mypy-type-check
        name: mypy (type checking - strict)
        entry: uv run mypy
        language: system
        types: [python]
        args: [--strict, --ignore-missing-imports, --show-error-codes, --no-error-summary]
        files: ^src/omnibase_spi/.*\.py$
        exclude: ^(tests/|scripts/).*\.py$
        stages: [pre-commit]

      # Cleanup: Clear tmp/ directory before commits
      # Prevents accumulation of temporary files from custom commands
      - id: clean-tmp-directory
        name: Clean tmp directory
        entry: bash -c "rm -rf tmp/* 2>/dev/null || true"
        language: system
        always_run: true
        pass_filenames: false
        stages: [pre-commit]

  # SPI Framework Validation Hooks
  - repo: local
    hooks:
      # Core SPI architecture validation
      - id: validate-spi-protocols
        name: SPI Protocol Architecture Validation
        entry: uv run python scripts/validation/validate_spi_protocols.py
        language: system
        types: [python]
        files: ^src/.*\.py$
        exclude: ^(tests/|scripts/|src/omnibase_spi/exceptions\.py|src/omnibase_spi/exceptions_skill_routing\.py).*$
        pass_filenames: false
        args: ['src/', '--exclude-pattern', 'exceptions.py', '--exclude-pattern', 'exceptions_skill_routing.py']
        stages: [pre-commit]

      # Enhanced protocol duplicate detection
      - id: validate-protocol-duplicates
        name: SPI Protocol Duplicate Detection
        entry: uv run python scripts/validation/validate_protocol_duplicates.py
        language: system
        types: [python]
        files: ^src/.*protocol.*\.py$
        exclude: ^(tests/|scripts/).*\.py$
        pass_filenames: false
        args: ['src/']
        stages: [pre-commit]

      # Comprehensive duplicate analysis (backup validation)
      - id: comprehensive-duplicate-analysis
        name: SPI Comprehensive Duplicate Analysis
        entry: uv run python scripts/validation/validate_protocol_duplicates.py
        language: system
        types: [python]
        files: ^src/.*\.py$
        exclude: ^(tests/|scripts/).*\.py$
        pass_filenames: false
        args: ['src/']
        stages: [manual]

      # Enhanced namespace isolation
      # CI keeps the whole-tree backstop in the spi-validation-gate job.
      - id: validate-namespace-isolation
        name: SPI Namespace Isolation Validation
        entry: ./scripts/validate-namespace-isolation.sh
        language: system
        types: [python]
        files: ^src/omnibase_spi/.*\.py$
        pass_filenames: true
        stages: [pre-commit]

      # Enhanced SPI purity check
      - id: validate-spi-purity
        name: SPI Architectural Purity Validation
        entry: ./scripts/validate-spi-purity.sh
        language: system
        always_run: true
        pass_filenames: false
        stages: [pre-commit]

      # Enhanced SPI typing pattern validation (adapted from omnibase_core)
      # CI keeps the whole-tree backstop in the spi-validation-gate job.
      - id: validate-spi-typing-patterns
        name: SPI Typing Pattern Validation
        entry: uv run python scripts/validation/validate_spi_typing_patterns.py
        language: system
        types: [python]
        files: ^src/.*\.py$
        exclude: ^(tests/|scripts/).*\.py$
        pass_filenames: true
        stages: [pre-commit]

      # Enhanced SPI naming convention validation (adapted from omnibase_core)
      # CI keeps the whole-tree backstop in the spi-validation-gate job.
      - id: validate-spi-naming-conventions
        name: SPI Naming Convention Validation
        entry: uv run python scripts/validation/validate_spi_naming.py
        language: system
        types: [python]
        files: ^src/.*\.py$
        exclude: ^(tests/|scripts/).*\.py$
        pass_filenames: true
        stages: [pre-commit]

      # ======================================================================
      # STANDALONE VALIDATORS (Temporary - stdlib only, no omnibase_core)
      # Will be replaced by omnibase_core.validation when circular dep resolved
      # ======================================================================

      # Architecture validation - One protocol per file (ONEX requirement)
      - id: validate-architecture
        name: SPI Architecture Validation (One Protocol Per File)
        entry: python scripts/validation/validate_architecture.py
        language: system
        types: [python]
        files: ^src/omnibase_spi/protocols/.*\.py$
        exclude: ^(tests/|scripts/).*\.py$
        pass_filenames: false
        stages: [manual] # Manual: 92 files have existing violations (technical debt)

      # Naming patterns - Protocol/Error naming and @runtime_checkable
      - id: validate-naming-patterns
        name: SPI Naming Pattern Validation
        entry: python scripts/validation/validate_naming_patterns.py
        language: system
        types: [python]
        files: ^src/omnibase_spi/.*\.py$
        exclude: ^(tests/|scripts/).*\.py$
        pass_filenames: true
        stages: [pre-commit]

      # Namespace isolation - No Infra imports, no Pydantic models
      - id: validate-namespace-isolation-new
        name: SPI Namespace Isolation (No Infra, No Pydantic)
        entry: python scripts/validation/validate_namespace_isolation.py
        language: system
        types: [python]
        files: ^src/omnibase_spi/.*\.py$
        exclude: ^(tests/|scripts/).*\.py$
        pass_filenames: true
        stages: [pre-commit]

      # Unified runner - Run all validators (for CI or manual runs)
      - id: validate-all-spi
        name: SPI Unified Validation Suite
        entry: python scripts/validation/run_all_validations.py --strict
        language: system
        always_run: true
        pass_filenames: false
        stages: [manual] # Manual: Use for CI or explicit validation runs

      # ======================================================================
      # SECURITY VALIDATION HOOKS (aligned with omnibase_core)
      # These hooks prevent common security vulnerabilities in the codebase
      # ======================================================================

      # Security: Secret Detection (prevents hardcoded secrets)
      # Scans Python files for hardcoded API keys, passwords, tokens, and other secrets.
      # Uses AST parsing for reliable detection of secret patterns in variable names
      # and attribute assignments (e.g., self.api_key = "...").
      # Bypass: Add inline comment "# secret-ok: <reason>" for test fixtures.
      - id: validate-secrets
        name: SPI Secret Detection
        entry: uv run python scripts/validation/validate_secrets.py
        language: system
        pass_filenames: true
        files: ^src/.*\.py$
        exclude: ^(tests/|scripts/validation/).*\.py$
        stages: [pre-commit]

      # Security: Hardcoded Environment Variable Prevention
      # Ensures environment variables (UPPER_CASE names) use os.getenv() or similar.
      # Prevents accidental hardcoding of DATABASE_URL, API_KEY, PORT, etc.
      # Bypass: Add inline comment "# env-var-ok: <reason>" for constants.
      - id: validate-hardcoded-env-vars
        name: SPI Hardcoded Environment Variable Prevention
        entry: uv run python scripts/validation/validate_hardcoded_env_vars.py
        language: system
        pass_filenames: true
        files: ^src/.*\.py$
        exclude: ^(tests/|scripts/validation/).*\.py$
        stages: [pre-commit]

      # ======================================================================
      # HEADER VALIDATION HOOKS (aligned with omnibase_core)
      # These hooks ensure consistent file header format
      # ======================================================================

      # SPDX Header Enforcement (reverses OMN-1360 — uniform MIT headers required)
      - id: validate-spdx-headers
        name: ONEX SPDX Header Requirement
        entry: uv run onex spdx validate
        language: system
        pass_filenames: true
        files: ^(src|tests|scripts|examples)/.*\.(py|sh|bash|yml|yaml|toml)$
        exclude: ^(archived/|archive/)
        stages: [pre-commit]

      # ======================================================================
      # STRUCTURE VALIDATION HOOKS (aligned with omnibase_core)
      # These hooks maintain clean directory structure
      # ======================================================================

      # Empty Directory Prevention
      # Detects directories that are empty or only contain __init__.py.
      # Helps identify incomplete refactoring or placeholder modules.
      # Skips symlinks to prevent directory traversal attacks.
      - id: validate-no-empty-directories
        name: SPI Empty Directory Prevention
        entry: uv run python scripts/validation/validate_no_empty_directories.py
        language: system
        pass_filenames: false
        always_run: true
        stages: [pre-commit]
      - id: check-ai-slop
        name: Check for AI-slop patterns
        language: system
        entry: uv run python scripts/validation/check_ai_slop.py --strict
        types_or: [python, markdown]
        pass_filenames: true
        stages: [pre-commit]

      # ======================================================================
      # PLANNING DOCS GUARD (OMN-3618)
      # Planning docs belong in omni_home/docs/plans/, not in individual repos
      # ======================================================================

      - id: no-planning-docs
        name: Block planning docs (belong in omni_home/docs/plans/)
        entry: bash -c 'PLAN_FILES=$(git diff --cached --name-only --diff-filter=ACMR -- "docs/plans/"); if [ -n "$PLAN_FILES" ]; then echo "ERROR - Planning docs must live in omni_home/docs/plans/, not in this repo"; echo "$PLAN_FILES"; echo ""; echo "Move your file to omni_home/docs/plans/"; exit 1; fi'
        language: system
        pass_filenames: false
        always_run: true
        stages: [pre-commit]

    # TODO [OMN-7408]: re-enable after omnibase_core publishes onex-validate-links entry point to PyPI
    # Markdown link validation (OMN-5086) — uses onex-validate-links from omnibase_core
    # - id: onex-validate-links
    #   name: Validate markdown links
    #   entry: uv run onex-validate-links --verbose
    #   language: system
    #   types: [markdown]
    #   pass_filenames: false
    #   stages: [pre-commit]

  - repo: local
    hooks:
      - id: normalization-symmetry
        name: normalization-symmetry
        entry: uv run python -m omnibase_core.validation.checker_normalization_symmetry
        language: system
        pass_filenames: false

  # Skip-token rejection gate (OMN-10414 / OMN-13296)
  # Blocks [skip-*] bypass tokens in staged markdown/yaml/txt files.
  # Escape hatch: add '# skip-token-allowed: <receipt-id>' with a traceable approval receipt.
  - repo: local
    hooks:
      - id: reject-deploy-gate-skip-token
        name: Reject skip-gate bypass tokens (OMN-10414)
        entry: .pre-commit-hooks/reject-deploy-gate-skip-token.sh
        language: script
        pass_filenames: true
        always_run: true
        stages: [pre-commit]
        exclude: ^tests/.*fixtures/
      - id: reject-deploy-gate-skip-token-commit-msg
        name: Reject skip-gate bypass tokens in commit messages (OMN-10414)
        entry: .pre-commit-hooks/reject-deploy-gate-skip-token.sh
        language: script
        pass_filenames: true
        stages: [commit-msg]
      - id: strip-ai-coauthor-trailer
        name: Strip AI-generated Co-Authored-By trailers from commit messages
        entry: perl -i -ne 'print unless /^co-authored-by:.*claude.*noreply\@anthropic\.com/i'
        language: system
        pass_filenames: true
        stages: [commit-msg]

  # OMN-13291 (W0): validator-requirements fleet gate. Reports ALL missing
  # required gates for omnibase_spi against omnibase_core's
  # architecture-handshakes/validator-requirements.yaml, ratcheted by this
  # repo's own architecture-handshakes/validator-requirements-baseline.yaml.
  # Re-pin rev to the merged-dev SHA once the omnibase_core export PR lands.
  - repo: https://github.com/OmniNode-ai/omnibase_core
    rev: d163312724489f10f58ffdf8115ba1fa9d5783e8 # pragma: allowlist secret
    hooks:
      - id: validate-validator-requirements
        args:
          - --repo
          - omnibase_spi
          - --repo-root
          - .
          - --spec
          - architecture-handshakes/validator-requirements.yaml
          - --baseline
          - architecture-handshakes/validator-requirements-baseline.yaml

  # OMN-14670 (WS7 fan-out, canary OMN-14655): CI<->pre-commit byte-match parity
  # gate. Two local meta-gates over this very file, wired as BOTH pre-commit
  # hooks (here) and an unconditional standalone CI job
  # (.github/workflows/precommit-parity-gate.yml) in this same PR so the class
  # cannot regenerate on the next wrapper script
  # (feedback_a_rule_is_not_a_mechanism / CLAUDE.md Rule #5).
  - repo: local
    hooks:
      # Fail-loud meta-gate: hard-rejects the exit-0-on-missing-path /
      # WARN-SKIP-degrade class (a skipped gate must be byte-indistinguishable
      # from a failing one) plus any hook `stages:` value that
      # default_install_hook_types does not install (DRIFT-2 / DRIFT-2a).
      - id: precommit-fail-loud-meta-gate
        name: Pre-commit fail-loud meta-gate (OMN-14670)
        entry: uv run python scripts/validation/validate_precommit_fail_loud.py
        language: system
        pass_filenames: false
        always_run: true
        stages: [pre-commit]
        files: ^(\.pre-commit-config\.yaml|scripts/.*\.(sh|py)|\.pre-commit-hooks/.*\.sh)$
      # Pin-parity ratchet: fails closed the moment the pre-commit `rev:` pin
      # for a shared omnibase_core validator diverges from the SHA the same
      # validator's CI ratchet job pins for it (DRIFT-3). See PIN_PAIRS in the
      # script for the verified pairs (currently no-noncanonical-lifecycle-classes
      # hook rev vs ci.yml's inline omnibase-core git+ pin).
      - id: precommit-pin-parity
        name: Pre-commit/CI pin-parity check (OMN-14670)
        entry: uv run python scripts/validation/validate_precommit_pin_parity.py
        language: system
        pass_filenames: false
        always_run: true
        stages: [pre-commit]
        files: ^(\.pre-commit-config\.yaml|\.github/workflows/ci\.yml)$
      # OMN-15241: Required-Check Skip-Vector Guard (canary OMN-14854) -- LOCAL
      # parity for a guard this repo already runs CI-only via
      # .github/workflows/required-check-skip-guard-caller.yml. Without this hook a
      # skip-vector violation commits with every local hook GREEN and is caught only
      # after push -- detection without local enforcement (root CLAUDE.md Rule #5).
      #
      # Fails closed when a workflow file or the required-checks manifest introduces
      # any of the five skip vectors on a REQUIRED context: a path-scoped trigger, an
      # ungated job-level `if:`, an ungated reusable-caller `if:`, a missing
      # pull_request/merge_group trigger, or a `needs:` with no `if:` that provably
      # runs regardless of the needs result (OMN-15057 vector 5).
      #
      # The wrapper executes the SAME validator the CI reusable workflow executes
      # (canonical copy in omniclaude, resolved via $OMNI_HOME), so local and CI
      # verdicts cannot diverge on validator logic.
      - id: reject-required-check-skip-vector
        name: Reject skip vectors on required-check workflows (OMN-15241)
        entry: .pre-commit-hooks/reject-required-check-skip-vector.sh
        language: script
        pass_filenames: false
        files: '^\.github/(workflows/.*\.ya?ml|required-checks\.yaml)$'
        stages: [pre-commit]

  # ---------------------------------------------------------------------------
  # OMN-16664: a workflow may not execute a repo-local script that is absent.
  #
  # The OMN-15017 fan-out copied call-occ-autobind.yml and
  # call-occ-companion-effect.yml here without carrying
  # scripts/ci/occ_manual_replay_precheck.py, which both of them run, so every
  # manual OCC replay dispatched in this repo died at [Errno 2] before the
  # precheck could decide anything. Local parity for the "Workflow Script
  # Reference Gate" CI job: without this hook the break commits with every
  # local hook green and is caught only after push (root CLAUDE.md Rule #5).
  # ---------------------------------------------------------------------------
  - repo: local
    # Configuration
    # OMN-14670 (DRIFT-2a fix): every stage a hook uses MUST be installed by
    # `pre-commit install`. Previously [pre-commit] only, so the
    # reject-deploy-gate-skip-token-commit-msg hook (stages:[commit-msg]) silently
    # never installed locally — a false-green. The precommit-fail-loud-meta-gate
    # above now fails closed if this drifts again.

    hooks:
      - id: workflow-script-refs
        name: Workflow-referenced repo-local scripts exist (OMN-16664)
        entry: uv run python tests/ci/test_workflow_script_refs_exist.py
        language: system
        pass_filenames: false
        files: ^(\.github/workflows/.*\.ya?ml|scripts/.*)$
        stages: [pre-commit]

  - repo: local
    hooks:
      - id: validate-gitignore-baseline
        name: Gitignore Baseline Validator (OMN-12453)
        description: |
          Assert that every managed block declared in architecture-handshakes/gitignore-baseline.yaml appears VERBATIM in the target repo's .gitignore. Enforces universal block on all repos; python block on repos that contain pyproject.toml. Suppress all findings for a file with: # gitignore-ok: <reason>
        language: python
        entry: python -m omnibase_core.validators.gitignore_baseline
        pass_filenames: false
        stages:
          - pre-commit
        additional_dependencies:
          - omnibase-core==0.47.20 # Match uv.lock; includes the 0.47.18 baseline fix.


  # OMN-19666: VCS-backed additional_dependencies can inherit GIT_INDEX_FILE
  # during a cold-cache commit and overwrite the committing worktree index.
  - repo: local
    hooks:
      - id: no-git-precommit-additional-dependencies
        name: Pre-commit additional dependencies come from package indexes (OMN-19666)
        entry: bash tests/test_precommit_additional_dependencies_omn19666.sh
        language: system
        pass_filenames: false
        files: ^(\.pre-commit-config\.yaml|scripts/validation/check_precommit_additional_dependencies\.sh|tests/test_precommit_additional_dependencies_omn19666\.sh|tests/fixtures/omn19666/.*)$
        stages: [pre-commit]

default_install_hook_types: [pre-commit, commit-msg]
default_stages: [pre-commit]
fail_fast: false

ci:
  autofix_commit_msg: |
    [pre-commit.ci] auto fixes for omnibase_spi

    SPI architectural compliance fixes
  autofix_prs: true
  autoupdate_schedule: weekly
  submodules: false
