From 3e51e62a8b4546688587b3de036528ae9013fe2b Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 29 Jul 2026 08:00:47 -0500 Subject: [PATCH] chore: add AI-drift guardrails per Zornek article - Enable key Jump credo checks: TestHasNoAssertions, VacuousTest, WeakAssertion, UnusedLiveViewAssign, UndeclaredExternalResource, AssertReceiveTimeout - Add mix xref graph --label compile-connected --fail-above 4 - Add --warnings-as-errors to mix test step - Add sobelow and mix_audit deps; wire into audit CI target --- .credo.exs | 35 +++++++++++++++-------------------- Makefile | 30 +++++++++++++++++++++++++----- mix.exs | 6 ++++++ mix.lock | 4 ++++ 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/.credo.exs b/.credo.exs index f54a4695..04240938 100644 --- a/.credo.exs +++ b/.credo.exs @@ -189,26 +189,21 @@ # ## Jump Credo Checks # - {Jump.CredoChecks.AssertElementSelectorCanNeverFail, []} - # {Jump.CredoChecks.AssertReceiveTimeout, []}, - # {Jump.CredoChecks.AvoidFunctionLevelElse, []}, - # {Jump.CredoChecks.AvoidLoggerConfigureInTest, []}, - # {Jump.CredoChecks.AvoidSocketAssignsInTest, []}, - # {Jump.CredoChecks.ConditionalAssertion, []}, - # {Jump.CredoChecks.DoctestIExExamples, []}, - # {Jump.CredoChecks.ForbiddenFunction, []}, - # {Jump.CredoChecks.LiveViewFormCanBeRehydrated, []}, - # {Jump.CredoChecks.UndeclaredExternalResource, []}, - # {Jump.CredoChecks.PreferChangeOverUpDownMigrations, [start_after: "0"]}, - # {Jump.CredoChecks.PreferTextColumns, [start_after: "0"]}, - # {Jump.CredoChecks.SafeBinaryToTerm, []}, - # {Jump.CredoChecks.TestHasNoAssertions, []}, - # {Jump.CredoChecks.TooManyAssertions, []}, - # {Jump.CredoChecks.TopLevelAliasImportRequire, []}, - # {Jump.CredoChecks.UnusedLiveViewAssign, []}, - # {Jump.CredoChecks.UseObanProWorker, []}, - # {Jump.CredoChecks.VacuousTest, []}, - # {Jump.CredoChecks.WeakAssertion, []} + {Jump.CredoChecks.AssertElementSelectorCanNeverFail, []}, + # Catch tests that run green without proving anything. + # Directly addresses the AI drift of writing tests that pass but don't + # verify behavior — the false confidence Mike Zornek warns about. + {Jump.CredoChecks.TestHasNoAssertions, []}, + {Jump.CredoChecks.VacuousTest, []}, + {Jump.CredoChecks.WeakAssertion, []}, + # Catch LiveView assigns that are set but never rendered — a common + # pattern in AI-generated templates. + {Jump.CredoChecks.UnusedLiveViewAssign, []}, + # Ensure references to external resources (images, fonts, static + # assets) are declared so the build system knows about them. + {Jump.CredoChecks.UndeclaredExternalResource, []}, + # Catch tests relying on fragile timing assumptions. + {Jump.CredoChecks.AssertReceiveTimeout, []} ], disabled: [ # diff --git a/Makefile b/Makefile index 34acad57..032b0004 100644 --- a/Makefile +++ b/Makefile @@ -1,32 +1,52 @@ -.PHONY: precommit format test deps credo audit hex-audit cargo-audit +.PHONY: precommit format test deps credo xref audit hex-audit cargo-audit deps-audit sobelow +# Runs every code-quality check from Mike Zornek's "Guarding Against AI Drift" +# article in one shot. The agent and CI both gate on this alias. precommit: @EXIT=0; \ $(MAKE) format || EXIT=1; \ $(MAKE) test || EXIT=1; \ $(MAKE) deps || EXIT=1; \ $(MAKE) credo || EXIT=1; \ + $(MAKE) xref || EXIT=1; \ exit $$EXIT # Security audit targets — run independently (network-dependent, not in precommit). -# Add these to CI pipeline in addition to precommit. - -audit: hex-audit cargo-audit +# Per Zornek: security scanning lives in its own CI job so a red mark there clearly +# means "a dependency needs attention" rather than "your own code has a style problem." +audit: hex-audit cargo-audit deps-audit sobelow hex-audit: MIX_ENV=test mix hex.audit +deps-audit: + MIX_ENV=test mix deps.audit + cargo-audit: cargo audit --file rust/prop_grid_rs/Cargo.lock +sobelow: + MIX_ENV=test mix sobelow --config --quiet + format: MIX_ENV=test mix format --check-formatted test: - MIX_ENV=test mix test + MIX_ENV=test mix test --warnings-as-errors deps: MIX_ENV=test mix deps.unlock --check-unused credo: MIX_ENV=test mix credo --strict + +# Catches compile-time coupling creep. Fails if a change introduces new +# compile-time dependencies between modules (which balloon recompile times). +# Baselines at the current 4 pre-existing LiveView→band_config deps. +xref: + MIX_ENV=test mix xref graph --label compile-connected --fail-above 4 + +# Security-focused static analysis for Phoenix apps. Catches XSS, CSRF, +# insecure config, and other common web vulnerabilities. +sobelow: + MIX_ENV=test mix sobelow --config --quiet diff --git a/mix.exs b/mix.exs index 31b55854..ead2da72 100644 --- a/mix.exs +++ b/mix.exs @@ -99,6 +99,12 @@ defmodule Microwaveprop.MixProject do {:oban_web, "~> 2.12", path: "vendor/oban_web", override: true}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:jump_credo_checks, "~> 0.4", only: [:dev, :test], runtime: false}, + # Security scanning for Phoenix apps — flags common vulnerabilities + # like XSS, CSRF, and insecure config patterns. + {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false}, + # Cross-references locked deps against community security advisories + # so CVEs in transitive deps don't go unnoticed. + {:mix_audit, "~> 2.0", only: [:dev, :test], runtime: false}, {:nx, "~> 0.13", only: [:dev, :test]}, {:axon, "~> 0.7", only: [:dev, :test]}, {:exla, "~> 0.9", only: [:dev, :test]}, diff --git a/mix.lock b/mix.lock index fec7883a..cad5e95c 100644 --- a/mix.lock +++ b/mix.lock @@ -39,6 +39,7 @@ "mdex_native": {:hex, :mdex_native, "0.2.6", "16c0d9ffca05990982ef8113c0a61950ffc250bf0792e17b95b28b74e1f572dc", [:mix], [{:rustler, "~> 0.32", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "eaaf2af27162c9879f5aef66344101dadf1898e71ea6ac9eb811c8d92d8e49ad"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mint": {:hex, :mint, "1.9.3", "3337184d69179695c7a9f1714d92c11e629d36c8c037a21cf490131d3d150554", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5f7c9342480c069dbbc4eeac3490303c9e01870ff01a7f1d29b6107054fc1e74"}, + "mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"}, "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, "msgpax": {:hex, :msgpax, "2.4.0", "4647575c87cb0c43b93266438242c21f71f196cafa268f45f91498541148c15d", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ca933891b0e7075701a17507c61642bf6e0407bb244040d5d0a58597a06369d2"}, "nimble_csv": {:hex, :nimble_csv, "1.3.0", "b7f998dc62b222bce9596e46f028c7a5af04cb5dde6df2ea197c583227c54971", [:mix], [], "hexpm", "41ccdc18f7c8f8bb06e84164fc51635321e80d5a3b450761c4997d620925d619"}, @@ -71,6 +72,7 @@ "req": {:hex, :req, "0.6.3", "7fe5e68792ff0546e45d5919104fa1764a13694cfe3e48c8a0f32ad051ae77e4", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "e85b5c6c990e6c3f52bbba68e6f099118f2b8252825f96c7c3636b97a3de307d"}, "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"}, + "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, "sourceror": {:hex, :sourceror, "1.12.2", "85bfd48159f020c0cbfc72f289f11456fdc05dc43719b6f2589fb969faefa113", [:mix], [], "hexpm", "da37d3da09c5b890528802c7056a8f585a061973820d7656b6e3649c14f0e9cb"}, "spitfire": {:hex, :spitfire, "0.3.13", "edd207b065eaec57acc5484097d0aa3e97fe4246168c54e67a9af040b8dee4c1", [:mix], [], "hexpm", "3601be88ceed4967b584e96444de3e1d12d6555ae0864a7390b9cd5332d134b4"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, @@ -91,4 +93,6 @@ "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.6.0", "73db5ab8aaefd1a876a97ce3e6afc96562625de69ef17a4e04426e034849d0b8", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "50021a85bce8f203b086705d9e0c5415e2c7eb05d319111b0428fe71f9934617"}, "xla": {:hex, :xla, "0.10.0", "41121e9f011456242d3a79b9289910ce43419be0b0e7ebe67cc1292c6b3f232f", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f57d91aea6e661b52bf12239316c598679e9170628122bbd941235f040122bc6"}, + "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"}, }