Chapter 11: Custom Quality Rules
⚠️ Superseded, as of pmat 3.32.0 (2026-08-25).
pmathas norulessubcommand, and never has had one.This chapter used to document a rule-authoring system:
rules init,rules create,rules test,rules validate,rules install,rules publish,rules package,rules benchmark,rules profile,rules scope,rules debug,rules docs,rules optimize,rules archive,rules generate,rules test-suite,rules update— 29 examples in all, plus a YAML rule-definition language stored under.pmat/rules/, acustom_rules_directorykey inpmat.toml, and rule categories for security, performance and team standards.None of it ever shipped. Not “removed in a later version” — never written. There is no
RulesCommandsenum, noRulesArgsstruct and norulesclap subcommand in any commit of pmat’s history, and the YAML files the chapter told you to create are read by nothing. The chapter’s own test script,tests/ch11/test_custom_rules.sh, wrote seven of those YAML files and then asserted only that the files it had just written existed. It never invokedpmatonce, and it passed for a year.The old status badge on this page claimed “✅ 100% Working (8/8 examples)”. It has been removed. A validation badge backed by a test that cannot fail is worse than no badge at all.
tests/ch11/test_custom_rules.shhas been rewritten to run the real binary and assert on its real output — it now makes 16 assertions, every one of which has been checked to go red when the claim behind it is false.What pmat 3.32.0 actually gives you is a configurable threshold system rather than a rule-authoring one:
pmat quality-gatesreading.pmat-gates.toml, andpmat quality-gatewith threshold flags. Both are documented below, and every example was executed againstpmat 3.32.0.
Project-specific quality enforcement in pmat: what you can configure, what you cannot author, and which command actually fails a build.
The Honest Summary
pmat does not let you write a new check. The set of things it knows how to
measure is fixed and compiled in. What it does let you do is choose which of
those checks run, and at what threshold, per project, from a file you commit.
For most teams that covers the real need — “complexity over 15 fails the build
here, but 25 is fine in the legacy service” — without any rule language. If you
genuinely need a bespoke pattern check, use a dedicated tool
(ast-grep, semgrep, a clippy lint) and gate it separately; pmat will not
do it, and no version of pmat ever could.
Two commands are involved, and the difference between them is not obvious from the names.
| Command | Config file | What it runs | Fails the build? |
|---|---|---|---|
pmat quality-gates (plural) | .pmat-gates.toml | clippy, tests, coverage, complexity | Yes, exit 1 |
pmat quality-gate (singular) | flags, .pmat-metrics.toml | complexity, dead code, SATD, entropy, security, duplicates, coverage, sections, provability | Yes, exit 1 on blocking violations |
pmat quality-gates: a Config File You Commit
This is the closest thing pmat has to “custom rules”. Start by generating the
file:
pmat quality-gates init
✅ Created .pmat-gates.toml
The generated file is a commented list of every knob there is:
# PMAT Quality Gate Configuration
# Generated by: pmat quality-gates init
[gates]
# Run clippy linter
run_clippy = true
# Enforce strict clippy (-D warnings)
clippy_strict = true
# Run test suite
run_tests = true
# Test timeout in seconds
test_timeout = 300
# Check code coverage
check_coverage = true
# Minimum coverage percentage (0-100)
min_coverage = 80.0
# Check cyclomatic complexity
check_complexity = true
# Maximum cyclomatic complexity per function
max_complexity = 10
That is the complete schema. Eight keys. There is no [rules] table, no
per-language section, no severity levels and no pattern matching — if a key is
not in the list above, pmat does not read it.
Reading the Effective Configuration
pmat quality-gates show
[gates]
run_clippy = false
clippy_strict = true
run_tests = false
test_timeout = 300
check_coverage = false
min_coverage = 80
check_complexity = true
max_complexity = 10
show prints the merged configuration — your file over the defaults — which
is the value to check when a gate behaves unexpectedly rather than re-reading
your TOML.
Validating It
pmat quality-gates validate
✅ Configuration is valid
Validation is a type check, not a semantic one. A misspelled value is caught:
Error: TOML parse error at line 2, column 18
|
2 | max_complexity = "ten"
| ^^^^^
invalid type: string "ten", expected u32
and the command exits 1. A misspelled key, however, is silently ignored — the
same trap as any serde-backed config. show is how you catch that: if the key
you just added does not appear in show’s output, nothing is reading it.
Running the Gates
A worked example. Create a crate with one moderately branchy function:
mkdir -p rules-demo/src && cd rules-demo
cat > Cargo.toml << 'EOF'
[package]
name = "rules-demo"
version = "0.1.0"
edition = "2021"
EOF
cat > src/lib.rs << 'EOF'
pub fn classify(n: i32) -> &'static str {
if n < 0 {
"negative"
} else if n == 0 {
"zero"
} else if n < 10 {
"small"
} else if n < 100 {
"medium"
} else if n < 1000 {
"large"
} else {
"huge"
}
}
EOF
cat > .pmat-gates.toml << 'EOF'
[gates]
run_clippy = false
run_tests = false
check_coverage = false
check_complexity = true
max_complexity = 10
EOF
classify has cyclomatic complexity 6, so at a threshold of 10 the gate passes:
pmat quality-gates
Running complexity check...
✅ Quality Gate Results
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ complexity (0.00s)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total time: 0.00s
Tighten max_complexity to 5 and it fails, naming the function and the
measured value:
Running complexity check...
❌ Quality Gate Results
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✗ complexity (0.00s)
✗ Complexity: classify in ./src/lib.rs has cyclomatic 6 (> 5); measured 1 function(s) in 1 of 1 file(s)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total time: 0.00s
The process exit code is 1. This is the property that matters for CI, and it is the property the old chapter’s fictional rule engine never had.
Note the denominator in that message — “measured 1 function(s) in 1 of 1 file(s)”. A gate that parsed none of your files would also report zero violations, so read the counts, not just the tick.
Machine-Readable Output
pmat quality-gates --json
{
"gates": [
{
"name": "complexity",
"passed": true,
"duration": 3,
"message": "✓ Complexity: max cyclomatic 6 (≤ 10); measured 1 function(s) in 1 of 1 file(s)"
}
],
"passed": true,
"total_duration": 3,
"timestamp": "2026-08-25T20:45:24.345444719+00:00"
}
and a markdown report for a PR comment:
pmat quality-gates --report
# Quality Gate Report
**Timestamp**: 2026-08-25T20:46:59.425961937+00:00
**Status**: ❌ FAIL
## Gate Results
- ✗ **complexity** (0.00s)
✗ Complexity: classify in ./src/lib.rs has cyclomatic 6 (> 5); measured 1 function(s) in 1 of 1 file(s)
**Total Time**: 0.00s
--json and --report apply only to a bare pmat quality-gates run; they are
ignored when a subcommand (init, show, validate) is given.
Pointing at a Different File
pmat quality-gates --config ci/strict-gates.toml
This is how a repository keeps a lenient local profile and a strict CI one without branching logic in the workflow.
pmat quality-gate: the Broader Check Set
The singular command runs a different, wider set of analyses and is configured
by flags rather than by .pmat-gates.toml.
pmat quality-gate -p .
🔍 Running quality gate checks...
📋 Checks to run:
✓ Complexity analysis
✓ Dead code detection
✓ Self-admitted technical debt (SATD)
✓ Security vulnerabilities
✓ Code entropy
✓ Duplicate code
✓ Test coverage
🔍 Checking complexity... ⚙️ Complexity thresholds: cyclomatic 30, cognitive 25 (from built-in defaults)
0 violations found
🔍 Checking dead code... 0 violations found
🔍 Checking technical debt... 1 violations found
🔍 Checking code entropy... 0 violations found
🔍 Checking security... 0 violations found
🔍 Checking duplicates... 0 violations found
🔍 Checking test coverage... 1 violations found
...
Quality Gate: FAILED
Total violations: 2
Blocking violations: 1
Selecting Checks
--checks accepts a repeated flag or a comma-separated list, from a fixed
vocabulary: dead-code, complexity, coverage, sections, provability,
satd, entropy, security, duplicates, all.
pmat quality-gate -p . --checks complexity,satd
A name outside that list is a clap error, not a silent skip — which is the behaviour you want from a gate.
Thresholds
pmat quality-gate -p . --checks complexity \
--max-complexity-p99 5 \
--max-dead-code 5 \
--min-entropy 0.0
Quality Gate: PASSED
Total violations: 0
Blocking violations: 0
--min-entropy also reads [entropy] min_pattern_diversity from
.pmat-gates.toml, .pmat-metrics.toml or pmat.toml when the flag is
omitted, falling back to 0.3.
Violations vs Blocking Violations
The two counters in the summary are not the same number, and only the second
one decides the exit code. In the run above, satd contributed a violation and
the command still exited 0; the full run exited 1 because of a coverage
violation whose message is worth quoting:
- project - Code coverage was NOT measured (no coverage report at
.pmat/coverage-cache.json or .pmat-metrics/coverage.json), so the 80.0%
minimum is unverified — this gate does not cover coverage
That is the right behaviour: an unmeasured metric fails rather than passing quietly. If you want the report without the verdict — for a dashboard, or while you are still driving the number down — use:
pmat quality-gate -p . --report-only
which always exits 0. (--fail-on-violation still parses but has done nothing
since 3.32.0, when blocking violations began exiting non-zero by default.)
Output Formats
-f accepts summary, detailed, human, json, junit and markdown.
junit is the one to reach for in CI:
pmat quality-gate -p . -f junit -o quality-gate.xml
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="Quality Gate">
<testsuite name="Quality Checks" tests="2" failures="2">
<testcase name="Code coverage was NOT measured ..." classname="coverage">
<failure message="Code coverage was NOT measured ..." type="error"/>
</testcase>
...
</testsuite>
</testsuites>
One <testcase> is emitted per violation, not per check — so a clean run
produces tests="0" failures="0", which several CI systems read as “no tests
ran” rather than “everything passed”. Gate your job on the exit code and use
the XML for annotations, not the other way round.
Rule-ID Compliance: pmat comply
For repositories that adopt pmat’s own conventions there is a third layer,
pmat comply, whose checks carry stable rule identifiers (CB-1400,
CB-2102, …) and whose baselines live in .pmat-ratchet.toml. Its
subcommands are listed by:
pmat comply --help
Two caveats before you wire it into anything. First, pmat comply check is
expensive — it can saturate every core on the machine, so do not run it in
parallel across repositories or inside another build. Second, a large share of
its checks only have anything to measure once pmat’s own local state exists
under .pmat/, so failure counts are not comparable between machines. Treat it
as a project-adoption tool, not as a general-purpose linter.
What You Still Cannot Do
Being explicit, because the old chapter promised all of it:
- Author a new check. The check set is compiled into the binary.
- Write a rule in YAML.
.pmat/rules/*.yamlis read by nothing. - Set per-rule severity. A gate passes or fails; there are no levels.
- Match a code pattern (regex or AST) of your own devising.
- Scope a rule to a path glob from within pmat’s own config.
- Publish or install a rule package. There is no rule registry.
Related Chapters
- Quality Gates — the gate command in a CI workflow
- Auto-clippy Integration —
pmat analyze clippy - Architecture Analysis — the dependency-graph commands
Summary
| The old chapter said | pmat 3.32.0 |
|---|---|
pmat rules init / create / test / publish … | No rules subcommand has ever existed |
YAML rule language under .pmat/rules/ | Read by nothing |
custom_rules_directory in pmat.toml | Read by nothing |
| Per-rule severity and categories | No such concept |
| Rule packaging and publishing | No registry |
| “✅ 100% Working (8/8 examples)” | The test never invoked pmat; badge removed |
The real mechanism is smaller and it works: commit a .pmat-gates.toml, run
pmat quality-gates in CI, and let a non-zero exit stop the line.