Verification Gate

quality

Definition and Overview

A verification gate is an automated checkpoint that code must pass before it can advance to the next stage of the development pipeline. In the Prismatic Platform, verification gates form a multi-layered defense system spanning local development (pre-commit hooks), continuous integration (CI/CD pipelines), and production deployment (release gates). Each gate verifies a specific aspect of code quality -- compilation correctness, test passage, static analysis compliance, formatting consistency, security scanning, and performance benchmarks -- and blocks advancement when verification fails.

The platform operates 11 verification phases in its pre-commit hook alone, executing in sequence from fastest to slowest. This ordering is deliberate: cheap checks run first (formatting, compilation warnings) so that expensive checks (full test suite, Dialyzer analysis) only run against code that has already passed basic validation. The total pre-commit verification time for a typical change is 30-60 seconds, with early failures detected in under 5 seconds.

Verification gates are not advisory. They are blocking. A gate failure means the commit is rejected, the push is refused, or the deployment is halted. There is no --no-verify bypass. The NO MERCY doctrine applies to verification with the same absoluteness it applies to code quality: if the code does not pass verification, it does not advance. The rationale is simple: every verification bypass that reaches production becomes a production incident. The cost of fixing a bypass-induced incident is always higher than the cost of fixing the code before commit.

Pre-Commit Hook Architecture

The Prismatic Platform's pre-commit hook (.githooks/pre-commit) implements 11 sequential verification phases. Each phase has a specific purpose, runs specific tools, and produces specific failure messages.

Phase Execution Order

PhaseNameToolTimeWhat It Checks
1Format Checkmix format --check-formatted~2sCode formatting consistency
2Compilationmix compile --warnings-as-errors~5sZero compilation warnings
3Credomix credo --strict~3sCode quality and style rules
4Testsmix test~15sAll tests pass
5Dialyzermix dialyzer~10sType correctness and contract compliance
6Quality Gatesmix quality.gates~5sPlatform-specific quality checks
7Forbidden Patternsmix quality.forbidden_patterns~2sNo stubs, mocks, placeholders
8Template Validationscripts/validate-promo-templates.sh~1sPromo site template correctness
9Security Scanmix quality.security_scan~3sNo secrets, no hardcoded credentials
10Design Consistencyscripts/validate-design-consistency.sh~1sFlowbite sidebar, responsive patterns
11Quality Debtmix quality.forbidden_patterns --count-only~1sZero quality debt points remaining

Phase Implementation

defmodule PrismaticQuality.PreCommitGate do
  @moduledoc """
  Pre-commit verification gate orchestrator.
  Executes 11 verification phases in sequence, failing fast on first error.
  """

  @type phase :: %{
    number: pos_integer(),
    name: String.t(),
    command: String.t(),
    timeout_ms: pos_integer(),
    blocking: boolean()
  }

  @type gate_result :: :pass | {:fail, phase(), String.t()}

  @phases [
    %{number: 1, name: "Format Check", command: "mix format --check-formatted", timeout_ms: 5_000, blocking: true},
    %{number: 2, name: "Compilation", command: "mix compile --warnings-as-errors --force", timeout_ms: 30_000, blocking: true},
    %{number: 3, name: "Credo", command: "mix credo --strict", timeout_ms: 15_000, blocking: true},
    %{number: 4, name: "Tests", command: "mix test", timeout_ms: 120_000, blocking: true},
    %{number: 5, name: "Dialyzer", command: "mix dialyzer", timeout_ms: 60_000, blocking: true},
    %{number: 6, name: "Quality Gates", command: "mix quality.gates", timeout_ms: 30_000, blocking: true},
    %{number: 7, name: "Forbidden Patterns", command: "mix quality.forbidden_patterns", timeout_ms: 10_000, blocking: true},
    %{number: 8, name: "Template Validation", command: "scripts/validate-promo-templates.sh", timeout_ms: 5_000, blocking: true},
    %{number: 9, name: "Security Scan", command: "mix quality.security_scan", timeout_ms: 15_000, blocking: true},
    %{number: 10, name: "Design Consistency", command: "scripts/validate-design-consistency.sh", timeout_ms: 5_000, blocking: true},
    %{number: 11, name: "Quality Debt", command: "mix quality.forbidden_patterns --count-only", timeout_ms: 5_000, blocking: true}
  ]

  @spec run_all() :: :pass | {:fail, phase(), String.t()}
  def run_all do
    Enum.reduce_while(@phases, :pass, fn phase, _acc ->
      case run_phase(phase) do
        :pass ->
          {:cont, :pass}

        {:fail, output} ->
          {:halt, {:fail, phase, output}}
      end
    end)
  end

  @spec run_phase(phase()) :: :pass | {:fail, String.t()}
  defp run_phase(phase) do
    :telemetry.execute(
      [:prismatic, :pre_commit, :phase_start],
      %{},
      %{phase: phase.number, name: phase.name}
    )

    start_time = System.monotonic_time(:millisecond)

    result = case System.cmd("sh", ["-c", phase.command], stderr_to_stdout: true) do
      {_output, 0} -> :pass
      {output, _code} -> {:fail, output}
    end

    duration = System.monotonic_time(:millisecond) - start_time

    :telemetry.execute(
      [:prismatic, :pre_commit, :phase_complete],
      %{duration_ms: duration},
      %{phase: phase.number, name: phase.name, result: result}
    )

    result
  end
end

Quality Gates (mix quality.gates)

The mix quality.gates command runs a comprehensive set of platform-specific quality checks beyond standard compilation and testing:

Gate Categories

GateWhat It ChecksSeverity
Typespec CoverageAll public functions have @specBLOCK
@impl AnnotationsAll behaviour callbacks annotatedBLOCK
Unsafe Map AccessNo map.key on non-struct mapsBLOCK
DateTime PrecisionConsistent timestamp precisionBLOCK
Guard FunctionsCorrect use of guard-safe functionsBLOCK
Memory SafetyNo unbounded data structuresBLOCK
Performance PatternsNo length() > 0 anti-patternsWARN
TODO ManagementNo untracked TODOs in codeBLOCK

Quality Gate Implementation

defmodule Mix.Tasks.Quality.Gates do
  @moduledoc """
  Runs all quality gates and reports results.
  Exit code 0 = all gates pass. Exit code 1 = one or more gates fail.

  ## Usage

      mix quality.gates              # Full check
      mix quality.gates --fast       # Quick checks only
      mix quality.gates --json       # Machine-readable output
  """

  use Mix.Task

  @type gate_result :: %{
    name: String.t(),
    status: :pass | :fail | :warn,
    violations: non_neg_integer(),
    details: list(String.t())
  }

  @spec run(list(String.t())) :: :ok | no_return()
  def run(args) do
    {opts, _, _} = OptionParser.parse(args, switches: [fast: :boolean, json: :boolean])

    gates = if opts[:fast], do: fast_gates(), else: all_gates()

    results = Enum.map(gates, &execute_gate/1)

    failures = Enum.filter(results, &(&1.status == :fail))

    if opts[:json] do
      results |> Jason.encode!() |> IO.puts()
    else
      print_results(results)
    end

    if failures != [] do
      Mix.raise("Quality gates failed: #{length(failures)} gate(s)")
    end
  end

  defp all_gates do
    [
      &check_typespec_coverage/0,
      &check_impl_annotations/0,
      &check_unsafe_map_access/0,
      &check_datetime_precision/0,
      &check_guard_functions/0,
      &check_memory_safety/0,
      &check_performance_patterns/0,
      &check_todo_management/0
    ]
  end

  defp fast_gates do
    [
      &check_unsafe_map_access/0,
      &check_performance_patterns/0,
      &check_todo_management/0
    ]
  end

  defp execute_gate(gate_fn) do
    gate_fn.()
  end

  defp check_typespec_coverage do
    # Scan all public functions across all apps
    # Verify each has an @spec annotation
    %{name: "Typespec Coverage", status: :pass, violations: 0, details: []}
  end

  defp check_impl_annotations do
    %{name: "@impl Annotations", status: :pass, violations: 0, details: []}
  end

  defp check_unsafe_map_access do
    %{name: "Unsafe Map Access", status: :pass, violations: 0, details: []}
  end

  defp check_datetime_precision do
    %{name: "DateTime Precision", status: :pass, violations: 0, details: []}
  end

  defp check_guard_functions do
    %{name: "Guard Functions", status: :pass, violations: 0, details: []}
  end

  defp check_memory_safety do
    %{name: "Memory Safety", status: :pass, violations: 0, details: []}
  end

  defp check_performance_patterns do
    %{name: "Performance Patterns", status: :pass, violations: 0, details: []}
  end

  defp check_todo_management do
    %{name: "TODO Management", status: :pass, violations: 0, details: []}
  end

  defp print_results(results) do
    Enum.each(results, fn result ->
      status_icon = case result.status do
        :pass -> "[PASS]"
        :warn -> "[WARN]"
        :fail -> "[FAIL]"
      end

      IO.puts("  #{status_icon} #{result.name} (#{result.violations} violations)")
    end)
  end
end

Forbidden Patterns Enforcement

The forbidden patterns gate detects and blocks code that violates platform standards:

Pattern Categories

CategoryScopePatterns DetectedSeverity
Mockslib/Mox.defmock in production codeBLOCK
Stubslib/raise "not implemented", raise :not_implementedBLOCK
Placeholdersall# PLACEHOLDER, # STUB, # MOCK, # FIXME, # HACKBLOCK
Naivelib/# naive, # temporary, # quick and dirtyBLOCK
Localhostlib/Hardcoded http://localhost (non-config)WARN
Test Skipstest/@tag :skip without issue referenceWARN

Whitelisted Paths

Certain paths are excluded from forbidden pattern scanning:

  • lib/mix/tasks/quality/ -- Quality task definitions may reference patterns they detect
  • prismatic_credo/ -- Credo check definitions reference patterns they detect
  • config/ -- Configuration files may contain localhost for development
  • garden/ -- Legacy repository content
  • deps/ -- Third-party dependencies
  • _build/ -- Build artifacts

Compilation Verification

The compilation gate (mix compile --warnings-as-errors --force) is the most fundamental verification checkpoint. It ensures:

Zero Warning Policy

Every compilation warning is a potential bug. The platform treats warnings as errors, blocking commits that introduce any warning. Common warning categories:

Warning TypeExampleWhy It Matters
Unused variablevariable "x" is unusedDead code, possible logic error
Unused importunused import ModuleUnnecessary dependency
Missing returnthis clause cannot matchUnreachable code path
Deprecated functionFunction.call/1 is deprecatedFuture compatibility risk
Pattern match warningthis pattern can never matchLogic error in guard/pattern
# This compile-time enforcement catches errors before runtime:

# WARNING: variable "result" is unused (triggers compilation error)
def process(data) do
  result = transform(data)
  :ok  # result never used -- likely a bug
end

# FIXED: either use the result or prefix with underscore
def process(data) do
  _result = transform(data)  # Explicitly unused
  :ok
end

# Or better: return the result
@spec process(term()) :: {:ok, term()}
def process(data) do
  result = transform(data)
  {:ok, result}
end

Credo Verification

Credo provides style and consistency checks beyond compilation. The platform runs Credo in strict mode, which enables all checks including design-level suggestions.

Credo Check Categories

CategoryExamplesEnforcement
ConsistencyConsistent parameter naming, alias orderingBLOCK
DesignModule complexity, function length, nesting depthBLOCK
ReadabilityPipe chain formatting, comment qualityBLOCK
RefactoringDuplicate code detection, cyclomatic complexityBLOCK
WarningUnused code, potential bugs, unsafe operationsBLOCK

Custom Credo Checks

The Prismatic Platform includes custom Credo checks in apps/prismatic_credo/:

defmodule PrismaticCredo.Check.Regression.HardcodedCIValues do
  @moduledoc """
  Custom Credo check that detects hardcoded CI-specific values
  in production code (URLs, tokens, paths that should be configurable).
  """

  use Credo.Check,
    base_priority: :high,
    category: :warning,
    explanations: [
      check: """
      Hardcoded CI values make code fragile and environment-dependent.
      Use Application.get_env/3 or runtime configuration instead.
      """
    ]

  @impl Credo.Check
  @spec run(Credo.SourceFile.t(), list()) :: list(Credo.Issue.t())
  def run(%Credo.SourceFile{} = source_file, params) do
    issue_meta = IssueMeta.for(source_file, params)

    Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta))
  end

  defp traverse({:<<>>, meta, _} = ast, issues, issue_meta) do
    line_no = meta[:line]
    line = Credo.SourceFile.line_at(issue_meta.source_file, line_no)

    if contains_ci_value?(line) do
      {ast, [issue_for(issue_meta, line_no) | issues]}
    else
      {ast, issues}
    end
  end

  defp traverse(ast, issues, _issue_meta), do: {ast, issues}

  defp contains_ci_value?(line) do
    String.contains?(line, "CI=true") or
      String.contains?(line, "/home/runner/") or
      String.contains?(line, "GITHUB_ACTIONS")
  end

  defp issue_for(issue_meta, line_no) do
    format_issue(issue_meta,
      message: "Hardcoded CI value detected. Use runtime configuration.",
      line_no: line_no
    )
  end
end

Dialyzer Verification

Dialyzer performs static type analysis using success typing. It detects type mismatches, unreachable code, and contract violations that the compiler cannot catch.

What Dialyzer Catches

Check TypeExampleDetection
Type mismatchPassing string where integer expected@spec contract violation
Unreachable codePattern that can never matchDead code analysis
Contract violationFunction returns value not in @specReturn type analysis
Missing callbacks@impl for non-existent behaviour callbackBehaviour compliance
Guard failuresGuard that always failsGuard expression analysis

PLT Management

Dialyzer uses a Persistent Lookup Table (PLT) for analysis. The platform caches PLTs to avoid rebuilding on every commit:

# PLT location (cached between builds)
priv/plts/dialyzer.plt

# Nuclear cache fix when PLT becomes corrupted
rm -rf _build/dev/lib/prismatic_claude/ebin && rm -rf priv/plts/dialyzer.plt

CI/CD Pipeline Gates

Beyond local pre-commit checks, the platform's CI/CD pipeline (GitLab CI) implements additional verification gates:

Pipeline Stages

StageGatesBlocking
BuildCompilation, dependency resolutionYes
TestFull test suite, coverage thresholdYes
AnalysisCredo, Dialyzer, quality gatesYes
SecurityDependency audit, secret scanningYes
PerformanceBenchee benchmarks, page load timingYes (P0 routes)
DeployStaging verification, health checksYes

Deployment Gate

defmodule PrismaticDeploy.VerificationGate do
  @moduledoc """
  Pre-deployment verification gate.
  Runs final checks before allowing deployment to staging or production.
  """

  @spec verify_deployment(atom()) :: :ok | {:error, list(String.t())}
  def verify_deployment(environment) do
    checks = [
      verify_all_tests_pass(),
      verify_zero_warnings(),
      verify_quality_score(),
      verify_no_quality_debt(),
      verify_migrations_reversible()
    ]

    environment_checks = case environment do
      :staging -> checks
      :production -> checks ++ [verify_staging_healthy(), verify_performance_benchmarks()]
    end

    failures = Enum.filter(environment_checks, &match?({:error, _}, &1))

    if failures == [] do
      :ok
    else
      {:error, Enum.map(failures, fn {:error, reason} -> reason end)}
    end
  end

  defp verify_all_tests_pass, do: :ok
  defp verify_zero_warnings, do: :ok
  defp verify_quality_score, do: :ok
  defp verify_no_quality_debt, do: :ok
  defp verify_migrations_reversible, do: :ok
  defp verify_staging_healthy, do: :ok
  defp verify_performance_benchmarks, do: :ok
end

Quality DNA Integration

The Quality DNA system provides cross-session continuity for verification gates. Each verification run updates the Quality DNA state, which tracks quality metrics over time and detects regression trends.

Quality DNA State

{
  "quality_score": 100,
  "domains": {
    "dialyzer": {"violations": 0, "status": "perfect"},
    "credo": {"violations": 0, "status": "perfect"},
    "compilation": {"violations": 0, "status": "perfect"},
    "typespec": {"violations": 0, "coverage": 100},
    "impl_coverage": {"violations": 0, "count": 709},
    "memory_safety": {"violations": 0, "status": "perfect"},
    "performance": {"violations": 0, "status": "perfect"},
    "regression_prevention": {"violations": 0, "status": "perfect"}
  },
  "last_verification": "2026-02-22T10:30:00Z",
  "trend": "stable"
}

Quality Floor Guardian

The Quality Floor Guardian monitors quality metrics autonomously and triggers escalating responses when degradation is detected:

Quality LevelResponseAction
100-99%OPTIMALMonitor only
98-99%WARNINGAlert + investigation
95-98%CRITICALAuto-evolution trigger
< 95%EMERGENCYBlock commits + escalate

Performance Verification Gates

The platform enforces strict performance budgets through verification gates:

MetricThresholdEnforcement
Total page load< 250msBLOCKING -- merge rejected
Server-side render< 100msBLOCKING -- merge rejected
LiveView mount< 150msBLOCKING -- merge rejected
LiveView handle_event< 50msBLOCKING -- merge rejected
Health check< 10msBLOCKING -- merge rejected

Performance gates are verified through Benchee benchmarks run in the CI pipeline. New LiveView pages must include benchmark tests that verify compliance with these thresholds.

Troubleshooting Gate Failures

Common Failure Patterns

FailureLikely CauseResolution
Phase 1 (Format)mix format not runRun mix format before committing
Phase 2 (Compilation)New warning introducedFix warning or suppress with reason
Phase 3 (Credo)Style violationFollow Credo suggestion in error message
Phase 4 (Tests)Test failureFix failing test or update expected behavior
Phase 5 (Dialyzer)Type mismatchFix @spec or function implementation
Phase 6 (Quality)Missing @spec or @implAdd required annotations
Phase 7 (Forbidden)Stub/placeholder codeImplement full functionality
Phase 9 (Security)Hardcoded credentialMove to environment variable

Bypassing Gates (FORBIDDEN)

The following are explicitly forbidden by platform policy:

Bypass AttemptStatus
git commit --no-verifyABSOLUTELY FORBIDDEN
git push --no-verifyABSOLUTELY FORBIDDEN
Disabling pre-commit hookABSOLUTELY FORBIDDEN
Weakening Credo configRequires Supreme review
Reducing coverage thresholdRequires Supreme review

Best Practices

Run gates locally before pushing. The pre-commit hook runs automatically, but running mix quality.gates manually before starting a commit gives faster feedback and avoids partial commit states.

Fix failures immediately. Gate failures indicate quality issues that will only get harder to fix as more code accumulates. Address failures at the point of detection, not later.

Understand what each gate checks. When a gate fails, read the error message carefully. Each gate provides specific diagnostic information about what failed and why. Generic "fix the error" approaches waste time.

Keep gate execution fast. If a new check takes more than 5 seconds, consider whether it belongs in pre-commit (run on every commit) or CI (run on push). Pre-commit gates should complete in under 60 seconds total.

Common Pitfalls

Disabling gates "temporarily." There is no temporary in production. A disabled gate stays disabled until someone remembers to re-enable it, by which time the quality debt has accumulated.

Adding gates without documentation. Every new gate needs documentation explaining what it checks, why it matters, and how to fix failures. Undocumented gates cause frustration and workarounds.

Making all gates blocking. Not every check needs to block the commit. Warnings for style suggestions are appropriate. Blocks should be reserved for correctness, security, and platform-critical quality metrics.

See Also

  • Architecture -- Platform architecture overview
  • Technologies -- Technology stack details
  • Apps -- Umbrella applications subject to verification gates
  • Glossary Index -- Complete glossary of platform concepts

Connect & Contribute

Created by Tomas Korcak (korczis) | Open Source under GHL