From 2ba0b7bc485bd09123bf73e6f1e02a0f2eeda4c7 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Mon, 26 Oct 2026 21:32:37 +0000 Subject: [PATCH] app: CRAP scores as a Lens sensor (ex_crap) The coverage export from `test.coverage` now also feeds a `crap` sensor, second in app's VERIFY_SENSORS. It runs with MIX_ENV=test so it reads the export's own environment instead of compiling a dev build and scoring a different module set, and it guards on the export's existence because ex_crap silently falls back to 0% coverage when the file is missing. Baseline at ex_crap's default threshold (30): 23 files, 139 functions, worst score 20.00 (CoreComponents.table/1, 0% - unused upstream scaffold). No offenders, so nothing to refactor or cover yet. --- app/Makefile | 23 +++++++++++++++++++---- app/mix.exs | 3 ++- app/mix.lock | 2 ++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/Makefile b/app/Makefile index 36dbd80..bf9c005 100644 --- a/app/Makefile +++ b/app/Makefile @@ -3,7 +3,7 @@ MISE_BIN ?= mise MISE_EXEC = $(MISE_BIN) exec -- -.PHONY: check precommit deps assets compile test test.coverage coverage format credo verify +.PHONY: check precommit deps assets compile test test.coverage coverage crap format credo verify # Run all static analysis checks (no database required) check: credo format @@ -48,6 +48,20 @@ coverage: @test -f cover/default.coverdata || { echo "coverage: no exported coverage - run 'make test.coverage' first" >&2; exit 1; } $(MISE_EXEC) mix test.coverage +# Enforce CRAP scores (complexity x coverage) from the same export. No test run. +# Runs in MIX_ENV=test because that is the environment the export came from: +# running in dev would compile a second build and score against a different set +# of modules. The guard is load-bearing for the same reason as `coverage`: +# ex_crap falls back to 0% coverage when the export is missing, which would +# report scores that are pessimistic instead of absent - a false failure is +# less bad than a false pass, but neither is acceptable in the Lens pass. +# +# Threshold is ex_crap's default (30); whether to ratchet it down is an open +# decision tracked in the "ratchets, deferred decisions, handoff practice" yak. +crap: + @test -f cover/default.coverdata || { echo "crap: no exported coverage - run 'make test.coverage' first" >&2; exit 1; } + MIX_ENV=test $(MISE_EXEC) mix crap + # Format code format: $(MISE_EXEC) mix format @@ -132,15 +146,16 @@ credo: # - coverage (yak: coverage floors + framework-glue exclusions policy) # WIRED below (test.coverage + coverage). # - crap (yak: CRAP scores with ex_crap (both apps)) +# WIRED below (crap); reuses the coverage export. # - mutation (yak: mutation testing with muex — a sensor, not a gate) # # Runtime note: the coverage sensor exports from a single suite run -# (`test.coverage` -> `mix test --cover --export-coverage default`) that CRAP is -# meant to reuse, instead of running the suite twice. +# (`test.coverage` -> `mix test --cover --export-coverage default`) that CRAP +# reuses, instead of running the suite twice. # # The suite runs FIRST, because it is what produces the coverage export the # sensors read; sensors still run cheapest-first among themselves. -VERIFY_SENSORS := coverage +VERIFY_SENSORS := coverage crap verify: @echo "==> verify(app): suite (coverage exported), then sensors [$(VERIFY_SENSORS)]" diff --git a/app/mix.exs b/app/mix.exs index b091dd1..e2cbaaf 100644 --- a/app/mix.exs +++ b/app/mix.exs @@ -81,7 +81,8 @@ defmodule Firehose.MixProject do {:microprints, git: "https://gitea.apps.sustainabledelivery.com/QWAN/microprints-phoenix.git", branch: "main"}, - {:credo, "~> 1.7", only: [:dev, :test], runtime: false} + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:ex_crap, "~> 0.1.1", only: [:dev, :test], runtime: false} ] end diff --git a/app/mix.lock b/app/mix.lock index d1c9612..068a819 100644 --- a/app/mix.lock +++ b/app/mix.lock @@ -1,6 +1,7 @@ %{ "bandit": {:hex, :bandit, "1.13.1", "1ce25a7beb750ed3fa2e905962f1f9a679cb585f1da0ae45637d06e385ce2eae", [:mix], [{:hpax, "~> 1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.6", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "39028f0b5a21404661410a6c19275060a9822c9dcca70b7c3e4e4fa50b11da58"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, + "boundary": {:hex, :boundary, "0.11.0", "56cff565b5565930765e02782281b022935fe8c2e1504543c8c1b44b85893b40", [:mix], [], "hexpm", "b9297a96ea14c0e63dbd77b8135e0d483f9c6f34ed1c9dee4b3fea82c596f55a"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, @@ -13,6 +14,7 @@ "ecto_sql": {:hex, :ecto_sql, "3.14.0", "06446ab8410d2f85bfbb80857ee224ab3b693700cbb38f6535d507449a627b2e", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.8", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949"}, "elixir_make": {:hex, :elixir_make, "0.10.0", "16577e2583a79bb79237bbff349619ef5d80afffc07eac6e4faf0d00e2ddaf7d", [:mix], [], "hexpm", "dc1f09fb7fa68866b886abd5f0f3c83553b1a19a52359a899e92af1bb3b31982"}, "esbuild": {:hex, :esbuild, "0.10.1", "32b44b468d2c6386b6bf9229617cbf51be025ce4380eacd383a8979cbb8327ef", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "890f251330052dcacafa40c409769e7c42e3876fb0ae450707bde831be7b34ea"}, + "ex_crap": {:hex, :ex_crap, "0.1.1", "c1770e7ce1843f304a27f5d32bfa867a340140e337684ee1929c770b2499f884", [:mix], [{:boundary, "~> 0.10", [hex: :boundary, repo: "hexpm", optional: false]}], "hexpm", "ada5d59bce442c1a8a987cb5328cbf4469fb56eb8d04641886f2199569a75b12"}, "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.24.0", "4022b6194e907b6d9b597c168001cac38e488a35fe36cef9d8297e3159782510", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "33ba40069c3587c2f99f9125b766e19dad87d6d54be3c6961db2304df04cef00"},