Packages

Elixir code quality checker. Runs format, compile, Credo, Dialyzer, coverage, Sobelow and mix_audit in parallel from one mix task. Findings with file:line, a JSON report, scoped runs. Umbrella-aware.

Current section

Files

Jump to
ex_quality CHANGELOG.md
Raw

CHANGELOG.md

# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.7.0] - 2026-08-01
### Added
- `ExQuality.Finding`, a structured representation of a single actionable problem (file, line, column, severity, check, message, and the raw tool output it came from)
- Stage results may carry `findings`; `ExQuality.Stage.findings/1` reads them
- Credo parses its issues into findings, and failure output renders them grouped by file and sorted by line
- Stages without a parser, and output that does not parse, still print in full: findings never replace output they did not account for
- Every stage that is considered and not run now prints a line saying so, with the reason (`○ Dialyzer: skipped (--quick)`, `○ Doctor: skipped (:doctor not installed)`, `○ Credo: skipped (disabled in .quality.exs)`)
- `ExQuality.Config.skip_reason/2` and `ExQuality.Stage.skipped/2`
- `ExQuality.Umbrella`, which answers which child apps exist, where they live, and what they declare
- Findings carry the umbrella app their file belongs to, and rendered output is grouped by app
- Test failures name the apps they came from (`3 of 4,180 failed (web: 3)`)
- `mix quality --report PATH` writes a JSON report of the run, and `--format json` puts it on stdout with the human output on stderr. A caller can route on which stage failed and on its findings instead of scraping the console or re-running the tool
- `ExQuality.Report`, which builds that report from the same results the human output is rendered from
- A Sobelow security stage, auto-enabled on `:sobelow`, with `--skip-sobelow` to turn it off. Findings at or above the project's `exit:` threshold block the run and are rendered; the rest are reported as a count (`2 blocking findings (1 high, 1 medium), 3 informational not shown`) and shown under `--verbose` or `sobelow: [show_informational: true]`
- The Sobelow stage runs once per Phoenix child app in an umbrella, where `mix sobelow` alone finds nothing to scan, and tags each finding with its app
- `ExQuality.Umbrella.app_deps/0`, the child dependencies keyed by app
- Coverage without ExCoveralls: a project that sets `test_coverage: [summary: [threshold: N]]` is measured with Elixir's own `mix test --cover`. In an umbrella the run exports per app and aggregates with `mix test.coverage`, so a module exercised by another app's tests no longer reads as 0%
- A failing coverage check reports the modules under the threshold as findings, with their source files, instead of the whole per-module table
- `test: [coverage: true | false]` in `.quality.exs`, to measure coverage in a project that states no threshold, or to never measure it
- `:native_coverage` in `ExQuality.Tools.detect/0`, true when `:excoveralls` is absent
- `mix quality.plt`, which builds the Dialyzer PLT outside a run so a container image or CI job can cache it instead of paying for it inside a check
- A run that has to build the PLT says so while it happens (`⋯ Dialyzer: building PLT (this is a one-time cost)`), instead of a multi-minute wait behind a stage that prints one line at the end, and reports it afterwards (`No warnings (PLT built this run)`, `stats.plt_built`)
- `ExQuality.Plt`, which recognises PLT work in dialyxir's output
- `ExQuality.OutputCollector.new/1` takes an `:on_line` handler, called with each line of a command's output as it arrives
- `ExQuality.Json`, which reads a JSON document out of output a compiler or a tool also wrote to
- Guides under `docs/`, shipped with the package and published with the docs: configuration, stages, reports, umbrella projects, and CI and pre-commit
- `ExQuality.Finding.relative_path/1`, which normalises a path a tool reported into one relative to the run's root
### Changed
- The README leads with the output contract - a passing stage costs one line, every stage the run considered is reported with its reason, a failure renders as findings with `file:line` - instead of a feature list, and the reference detail it carried moves into `docs/`. The comparison table is gone: it made unverifiable claims about other tools that would rot
- `usage-rules.md` is organised around what an agent has to decide: which command to run, what each line shape means, what to do about each stage's findings, and the fixes that are never acceptable. The anti-fixes are collected in one place rather than scattered, and cover skipping a failing test, adding a `--skip-*` flag and weakening a check, not just coverage and Sobelow thresholds
- The package description names the tools, the output and the audience, so it is findable by what someone would search for on Hex
- A passing run and `mix quality.init` end with `✓` rather than `✅`, matching the stage lines
- `:doctor` moves to `~> 0.23`, which requires `decimal ~> 3.1` and clears GHSA-rhv4-8758-jx7v, an unbounded exponent in `Decimal.new` that enables unauthenticated denial of service. `:jason` moves to 1.4.5, the first release whose optional `:decimal` requirement admits 3.x
- Dialyzer runs with `--format short --format dialyxir`. Each warning's one-line form becomes a finding naming the warning (`no_return`, `pattern_match`), and the warning count is the number of them instead of a count of lines shaped like `file.ex:12:`, which also counted PLT chatter and any explanation that named a second file. dialyxir's long explanation of each warning is still printed, so it stays in the stage's output and in the JSON report
- The security audit runs `mix deps.audit --format json`. Each vulnerability becomes a finding against the lockfile, naming the advisory, the version in use and the version that fixes it, instead of being counted by searching the human output for `Advisory:` and `severity: high`. Unused dependencies become findings too. `stats.high_severity` and its siblings are replaced by `stats.vulnerabilities_by_severity`
- Credo runs with `--format json`. Findings name the check that produced them (`Credo.Check.Readability.ModuleDoc`) instead of its category, the issue count is the number of issues rather than a summary-line parse, and an issue that credo reported is never dropped for having an unfamiliar line shape. A stage summary now reads `5 issues (2 readability, 3 design)`
### Fixed
- Findings report a path relative to the run's root. mix_audit reports an absolute lockfile, so a vulnerability rendered as `/Users/someone/code/app/mix.lock`: longer to read, not what a reader would type, different between a laptop and CI for the same problem, and comparing as a different finding. It is the same base `ExQuality.Umbrella.app_for_path/2` matches, so a finding's `file` and its `app` can no longer disagree about where it is. A path outside the project root stays absolute
- A run that stops at a compile error now reports the analysis stages it never reached as skipped, instead of saying nothing about them
- A disabled or uninstalled stage no longer vanishes from the output, where it read as a stage that passed
- Tool auto-detection reads every umbrella child app's dependencies, not just the root's. An umbrella root usually declares no deps, so credo, dialyzer and friends were reported as not installed and a run that checked almost nothing passed
- Test statistics sum every app's summary line instead of reporting the first app's numbers as the whole suite's
- Coverage reads every `[TOTAL]` line; when apps are measured separately the lowest leads, with the per-app numbers alongside it
- The coverage threshold is read from `coveralls.json`'s `coverage_options`, where excoveralls actually writes `minimum_coverage`. Only the top level was looked at, so a project configuring it the standard way had no threshold enforced
- An integer threshold (`minimum_coverage: 70`) no longer crashes the summary
- The Format stage reports a failing `mix format` instead of discarding its exit code. A file with a syntax error names no `.ex` file to count, so the first line of the run was a green tick on a broken file
- The Format stage reports a project with no `.formatter.exs` as skipped, rather than failing the run over a config file it never had
- `.quality.exs` is read from the project root instead of the working directory, and an umbrella child with no file of its own now reads the umbrella root's
## [0.6.0] - 2026-04-05
### Fixed
- Output full, non-truncated content when errors occur.
## [0.5.0] - 2026-02-23
### Changed
- Condensed `usage-rules.md` by removing first-time setup, auto-detection, and detailed configuration sections that aren't needed for day-to-day LLM usage
## [0.4.0] - 2026-02-23
### Fixed
- Include `:jason` as a runtime dependency instead of dev/test only, fixing compilation warnings in host projects
- Pass `--no-compile` to `mix dialyzer` to avoid race conditions with parallel analysis stages competing over `_build/dev`
- Mark tests using `File.cd!` as `async: false` to prevent intermittent compilation failures from global working directory changes
### Changed
- Updated `usage-rules.md` to instruct LLMs not to truncate `mix quality` output
## [0.3.0] - 2026-02-03
### Added
- **Test options pass-through**: Pass extra arguments to `mix test` or `mix coveralls`:
- Via CLI using `--` separator: `mix quality --quick -- --only integration`
- Via config file: `test: [args: ["--only", "integration"]]`
- CLI args override config file args (no merge)
- Supports any test flags: `--only`, `--include`, `--exclude`, `--seed`, etc.
## [0.2.0] - 2026-01-09
### Added
- **`mix quality.init`** now automatically configures ExCoveralls in `mix.exs` when coverage is selected:
- Adds `test_coverage: [tool: ExCoveralls]` to project configuration
- Adds `preferred_cli_env` settings for all coveralls commands
- Smart detection prevents duplicate configuration
- Properly indents to match existing project style
## [0.1.0] - 2026-01-09
### Added
- Initial release of ExQuality (formerly Quality)
- **Three-phase execution pipeline:**
- Phase 1: Auto-fix (format)
- Phase 2: Compilation (dev + test in parallel)
- Phase 3: Parallel analysis with streaming output
- **Quality stages:**
- Format: Auto-fixes code formatting with `mix format`
- Compile: Compiles dev + test environments in parallel with warnings as errors
- Credo: Static analysis with `--strict` mode (configurable)
- Dialyzer: Type checking with graceful PLT handling
- Doctor: Documentation coverage checking
- Gettext: Translation completeness validation
- Test: Test suite with optional coverage via excoveralls
- **Quick mode** (`--quick`):
- Skips dialyzer (slow type checking)
- Skips coverage enforcement (tests run, % not checked)
- Perfect for rapid iteration during development
- **Auto-detection system:**
- Automatically enables stages based on installed dependencies
- No configuration needed for standard setups
- **Configuration system:**
- 4-tier precedence: Defaults → Auto-detection → `.quality.exs` → CLI flags
- Project-level customization via `.quality.exs`
- Per-stage enable/disable controls
- CLI flags for runtime overrides
- **Streaming output:**
- Results display as each stage completes
- No interleaving (serialized via `Quality.Printer`)
- Fast stages provide immediate feedback
- **Actionable feedback:**
- Full tool output preserved in failure details
- File:line references for easy navigation
- Works for both humans and LLM coding assistants
- **CLI options:**
- `--quick` - Fast mode for iterative development
- `--skip-dialyzer` - Skip Dialyzer type checking
- `--skip-credo` - Skip Credo static analysis
- `--skip-doctor` - Skip Doctor documentation checks
- `--skip-gettext` - Skip Gettext translation checks
- `--verbose` - Show full output even on success
- **Documentation:**
- Comprehensive README with workflow examples
- `usage-rules.md` for LLM integration
- Example `.quality.exs` configuration file
- **Coverage threshold:**
- Single source of truth (reads from coveralls config)
- Respects `coveralls.json` or `mix.exs` settings
- No duplicate configuration needed
### Technical Details
- Zero runtime dependencies
- Optional dev dependencies: credo, dialyxir, doctor, excoveralls, gettext
- Parallel execution using Elixir Tasks
- Agent-based output serialization (ExQuality.Printer)
- Collectable protocol for silent output capture (ExQuality.OutputCollector)
- Tool detection via dependency scanning (ExQuality.Tools)
- Deep-merge configuration system (ExQuality.Config)
### Philosophy
ExQuality is designed for rapid, iterative development with confidence:
1. Fast feedback loop with `--quick` mode
2. Comprehensive verification with full mode
3. Actionable output with file:line references
4. Zero configuration required (works out of the box)
5. Progressive enhancement (add tools as needed)
[Unreleased]: https://github.com/riddler/ex_quality/compare/v0.6.0...HEAD
[0.6.0]: https://github.com/riddler/ex_quality/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/riddler/ex_quality/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/riddler/ex_quality/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/riddler/ex_quality/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/riddler/ex_quality/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/riddler/ex_quality/releases/tag/v0.1.0