CLI Options¶
Qualimetrix provides the check command for code analysis and several utility commands for baseline management, git hooks, and dependency graph visualization.
check command¶
Paths argument¶
Specify one or more directories or files to analyze:
# Analyze specific directories
bin/qmx check src/ lib/
# Analyze a single file
bin/qmx check src/Service/UserService.php
A missing resolved path or an explicitly named existing regular file whose
extension is not .php is refused with exit code 3 before discovery. This
applies to CLI, YAML and preset paths in check, directives, graph:export,
debug:layer-assignment and all four measuring baseline commands. Directories
are discovered normally; an empty PHP tree remains a separate analysis refusal.
A path you name that is itself a vendor, node_modules or .git directory (bin/qmx check lib/vendor) stops the run with a configuration error: Qualimetrix never walks into one, so the run would analyse nothing. Name a file or a directory inside it instead (bin/qmx check vendor/acme/); a vendor directory inside a path you name is still skipped.
Authored exclude: and --exclude apply to named files and directories from
CLI, YAML and presets. exact: removes only the named entry; subtree: reaches
its descendants. A matching written root is measured as excluded, not refused
as input. When analyzed=0, failed=0 and excluded + generatedExcluded > 0,
the result is intentionally empty: check and directives return 0,
graph:export writes an empty graph, and baseline:generate writes an empty
baseline. Its explanation remains on stderr and in formats with a diagnostic
place. Incompleteness takes priority with exit 4: diagnostic output is retained,
but no baseline mutation or authoritative graph is published. Truly undiscovered
empty input retains each command's existing outcome.
. and canonically equivalent root spellings are valid. A directory or named
alias must canonically target the captured root or a descendant; an external
alias spelling targeting an internal directory is valid, the reverse refuses
with exit 3. For files, the canonical parent is checked and the final name stays
literal, including a named link to an outside file. --working-dir chooses the
invocation root; publication has no outside-root fallback.
If you omit paths, Qualimetrix auto-detects them from every path the autoload section of your composer.json declares — psr-4 and psr-0 roots, classmap entries (a * wildcard expanded to the directories it matches) and files entries alike. These are the same paths a run is judged against when Qualimetrix asks whether it covered the whole project, final scope also measures missing PHP, authored/generated removal and universe certainty. An entry that is, or lies inside, a vendor, node_modules or .git directory — which Qualimetrix never walks into — is left out of both and named in a warning instead: third-party code is not analysed as your project's. A declared path that does not exist on disk stops the run with a configuration error. autoload-dev is not included: test code is analysed only when you name its path (bin/qmx check src/ tests/, or paths: in qmx.yaml), or when you count it as part of the project with --include-autoload-dev.
File options¶
--config, -c¶
Path to a YAML configuration file:
An empty value is not an omitted option. --config= — typically --config=$QMX_CONFIG with
the variable unset — is refused with exit code 3 instead of falling back to the qmx.yaml in
the working directory. The same holds for --baseline=, --output=, --report= and
--preset= (including an empty name in a list such as --preset=strict,): leave the option
out to get its default.
Warnings about a configuration a command accepted — only_rules: [] lifting a
preset's filter, for example — are written to stderr as Warning: lines by every
command that reads the configuration, and check --format=json also carries them
under configurationDiagnostics. See
Configuration warnings.
--exclude¶
Exclude directories from analysis with an explicit path selector. Can be repeated:
Binding is reported through
discovery.unmatched-exclude. PHP-path completeness
and universe certainty permit judging unsettled selectors; authored/generated
removal alone does not close that question. A bound selector remains Removed
on a narrow run. A possible hidden subtree from the same source gives a qualified
warning; one from another source gives a named unjudged value and a request to
rerun without that exclusion. A removed written root is measured, not refused.
See Paths argument.
--include-generated¶
By default, Qualimetrix automatically skips files that contain a @generated annotation in the first 2 KB. This flag overrides that behavior and includes generated files in the analysis:
Can also be set in qmx.yaml:
--include-autoload-dev¶
Counts the code composer.json declares under autoload-dev as part of the project. Both halves of a run follow it together:
- a run with no paths analyses every path
autoload-devdeclares, in any autoload form, beside theautoloadones; - a run is judged against
autoload-devtoo when Qualimetrix asks whether it covered the whole project — sobin/qmx check src/ --include-autoload-devwarns thattests/was not analysed, and the channels that only speak on a whole-project run stay silent.
Paths you name yourself are not widened. Off by default:
Can also be set in qmx.yaml:
--suppress-path¶
Suppress violations for files selected by exact:, subtree:, or regex:. CLI values split on the first colon; regex fragments are delimiterless and automatically full-anchored. The files are still analyzed, and the option can be repeated:
Merged with suppress_paths from qmx.yaml — both sources are combined.
Declared project-scoped channels remain visible
architecture.circular-dependency cycles and project-scoped diagnostics remain exempt.
The option applies to the source of architecture.layer-violation — see Suppress Paths
for why and for the alternatives.
--suppress-namespace¶
Suppress violations for classes selected by an explicit namespace selector. subtree: follows namespace boundaries; regex: is a full-subject PCRE fragment. The classes are still analyzed, and the option can be repeated:
bin/qmx check src/ --suppress-namespace='subtree:App\Entity' --suppress-namespace='regex:App\\DTO(?:\\[^\\]+)*'
Merged with suppress_namespaces from qmx.yaml — both sources are combined.
Declared project-scoped channels remain visible
architecture.circular-dependency cycles and project-scoped diagnostics remain exempt.
The option applies to the source of architecture.layer-violation — see Suppress Namespaces
for why and for the alternatives.
Preset options¶
--preset¶
Apply a named preset or a custom YAML file. Can be repeated or comma-separated:
# Built-in presets
bin/qmx check src/ --preset=strict
bin/qmx check src/ --preset=legacy
# Combine presets (merged left-to-right)
bin/qmx check src/ --preset=strict,ci
bin/qmx check src/ --preset=strict --preset=ci
# Custom preset file
bin/qmx check src/ --preset=./my-preset.yaml
Available built-in presets: strict, legacy, ci.
Presets are applied after composer.json auto-detection but before qmx.yaml, so your config file always takes precedence. See Configuration > Presets for details.
A built-in preset can also be imported from the configuration file — imports: [strict] — which applies it to every run that reads the file, including the pre-commit hook and the GitHub Action, neither of which passes --preset. An imported preset sits in the configuration-file band, above --preset presets. See Configuration > Built-in presets.
Output options¶
--format, -f¶
Choose the output format. Default: summary.
Available formats: summary, text, json, metrics, checkstyle, sarif, gitlab, github, health, html, suppressed.
See Output Formats for details on each format.
--output, -o¶
Write the report to a file instead of stdout:
A named regular destination is prepared after configuration, scope, selector and baseline-input refusals, before analysis. A private sibling holds the result; a new final name remains absent and existing bytes stay unchanged until complete write, flush and identity checks permit atomic publication. Replacement preserves permissions and changes the final inode; other hard links keep their old bytes. The parent must already exist and permit creation and replacement, even when the final file exists and is writable. Move the destination or make its parent writable/searchable before running qmx.
SIGINT/SIGTERM before publication discard the run's own sibling and return 128 + signal when pcntl and exclusive signal ownership are available. Otherwise ordinary atomic publication proceeds without replacing foreign handlers, and interruption may leave the private sibling. Replacing handlers during a guarded operation is unsupported. SIGKILL, failed cleanup and already published targets are also outside that cleanup guarantee. A profile already published before a later report failure is not rolled back.
A symbolic link is followed only when another user cannot place that directory entry; accepted links remain intact and write their resolved referents. This also applies to a dangling link into an existing parent. A placeable link refuses on both ordinary and thread-safe PHP. Writable exposure elsewhere in the path is reported on stderr before analysis. Component swaps, ACLs, hard-link provenance and non-local filesystems retain their platform limits.
An owned link in a 0775 directory is accepted when the directory group is
proved to be the effective user's private primary group: no other primary or
supplementary member. The native check supports complete per-source files
and systemd NSS enumeration. Unknown sources, explicit initgroups, failed
enumeration or unsupported platforms keep the conservative refusal.
/dev/stdout, /dev/stderr, /dev/fd/N, /proc/self/fd/N,
/proc/thread-self/fd/N, php://stdout, php://stderr and php://fd/N
address an existing process stream; /proc spellings require procfs.
file:///absolute/path addresses a file.
Other URI schemes refuse. A named pipe opens only after input refusals and
needs its reader; /dev/null remains supported.
Descriptor existence is checked before analysis. Linux fdinfo also permits an early refusal of a descriptor opened only for reading. On macOS PHP does not expose that original access flag, so an unknown mode is left to the actual write and its environment refusal. Descriptor and stream writes use blocking mode.
Report, profile and log targets are judged together. Two targets for one ordinary inode, or one absent name, refuse before analysis, as does a target naming the baseline input. A target that is a file the run reads refuses before analysis too:
- a configuration file — the auto-discovered
qmx.yaml/qmx.yml(orqmx.dist.yaml/qmx.dist.yml) or the--configfile, each file it imports, each preset file; - the working directory's
composer.json, whether or not it declares autoload paths; - a PHP file the run selects, analysed or excluded as generated.
Files are compared by identity, so another spelling, a symbolic link or a hard
link of one refuses alike. A new file among the analysed sources, such as
--output=src/report.json, is written. graph:export --output and the
baseline file written by baseline:generate, baseline:update and
baseline:cleanup follow the same rule. When the check or graph:export
report goes to standard output and the shell opened standard output on such a
file (>>, 1<>), the run refuses too, before any other target, on stderr
only; with > the shell has already emptied the file before qmx starts. A
refusal raised earlier, while options and configuration are still being read,
is written to standard output in the requested format as usual. Selecting the sources costs one
extra walk of the analysed paths, without reading any file, and only when a
target or redirected standard output is an existing regular file.
Not judged: other Composer files the run reads (a composer.json above the
working directory or beside an analysed package, vendor/composer/installed.json,
vendor class files), and the standard output of the baseline commands. A
shell-inherited 2>&1 is not a second authored destination. Character devices such as a terminal or
/dev/null may coincide. Missing parents, directories, inaccessible paths,
closed descriptors and write failures exit 3; the reason distinguishes
configuration input from an environment failure. A report write failure with
unwritable stdout sends its refusal to stderr.
--group-by¶
Group violations in the output. Default depends on the formatter.
Available values: none, file, rule, severity, class, namespace.
--format-opt¶
Pass formatter-specific options as key=value pairs. Can be repeated:
A key no formatter reads is refused with exit 3. A key some other formatter
reads stays accepted, so a script that sweeps one option set across formats
still works. Each key may be written once: --format-opt=violations=2
--format-opt=violations=1 is refused with exit 3 rather than the later value
winning, and every pair is checked as written, so an unparsable value is
refused even when another pair or --all would have replaced it. Two keys
that set one value are refused the same way: violations beside limit, or
limit beside --all (which writes violations=all).
JSON format options:
| Option | Default | Description |
|---|---|---|
violations=N\|all |
all | Max violations in output (0=none) |
limit=N\|all |
all | The same value as violations, except that 0 means no limit; write one of the two |
top=N |
10 | Number of worst offenders to include |
rank-by=score\|density |
score | Order worst-offender lists by ascending health score (default) or descending violation density |
bin/qmx check src/ --format=json --format-opt=limit=100
bin/qmx check src/ --format=json --format-opt=violations=all
bin/qmx check src/ --format=json --format-opt=rank-by=density
Summary format options:
| Option | Default | Description |
|---|---|---|
top=N |
3 | Number of worst offenders to include |
rank-by=score\|density |
score | Order worst-offender lists by ascending health score (default) or descending violation density |
Offender selection uses the complete captured population before ranking and top. Namespace selectors form a union. JSON/summary --class requires a unique exact declaration for its separate class health heading; duplicate logical names refuse with exit 3 and list their exact declarations. Use --namespace to retain all their exact offender records. rank-by=count is refused with exit 3; use score or density. Summary shows the true remaining count and advises --format-opt=top=<total>.
Two unrelated options named top
--format-opt=top=N (JSON and summary formats) caps the worst-namespace/worst-class
offender lists. The global --top flag is a different knob: it caps the
separate "Top issues by impact" list. The two can be set independently.
Health format options:
| Option | Default | Description |
|---|---|---|
contributors=N |
3 | Number of worst contributors listed per health dimension |
HTML format options:
| Option | Default | Description |
|---|---|---|
project-name=NAME |
auto-detected | Overrides the project name shown in the HTML report |
--fail-on¶
Set the minimum severity that causes a non-zero exit code. Default: error.
# Default behavior: only errors cause non-zero exit code
bin/qmx check src/
# Also fail on warnings
bin/qmx check src/ --fail-on=warning
# Never fail on violations
bin/qmx check src/ --fail-on=none
By default, warnings are shown in the output but do not cause CI failure. Use --fail-on=warning to also fail on warnings.
Can also be set in qmx.yaml:
--exclude-health¶
Exclude specific health dimensions from scoring. The excluded dimensions are not shown in the health summary and do not contribute to the overall score. Can be repeated:
# Exclude typing from health scoring
bin/qmx check src/ --exclude-health=typing
# Exclude multiple dimensions
bin/qmx check src/ --exclude-health=typing --exclude-health=maintainability
Available dimensions: complexity, cohesion, coupling, typing, maintainability.
Can also be set in qmx.yaml:
--detail¶
Adds a grouped violation list after the summary; the value is an optional cap on its
length. Omitted, the list is not shown (unless --namespace or --class is used); written
without a value, it shows up to 200 violations; --detail=N caps it at N; --detail=all or
--detail=0 removes the cap. Only affects summary format.
# Default limit (200 violations)
bin/qmx check src/ --detail
# Show all violations (no limit)
bin/qmx check src/ --detail=all
# Custom limit
bin/qmx check src/ --detail=50
Auto-enabled when --namespace or --class is used.
--top¶
Number of top-impact issues to show. Default: 10; 0 disables the section.
# Default: top 10
bin/qmx check src/
# Show the top 25
bin/qmx check src/ --top=25
# Disable the section
bin/qmx check src/ --top=0
Controls the "Top issues by impact" section of the summary format and the
topIssues key of --format=json — a list of findings ranked by impact
(combining ClassRank, severity and remediation time), separate from the
worst-namespace and worst-class offender lists. No other format renders it.
This is a different knob from --format-opt=top=N, which caps the worst-offender
lists instead — see the format options tables above.
--all¶
Show all violations without truncation. This is a shorthand for --format-opt=violations=all --detail=all.
# Show all violations in JSON format
bin/qmx check src/ --format=json --all
# Show all violations in summary format
bin/qmx check src/ --all
Cannot be combined with --format-opt=violations=N (numeric limit) or with --detail/--detail=N (a capped list; --detail alone is a cap of 200) — each is refused with exit 3 before the analysis, rather than one of the two silently winning. Combining --all with --format-opt=violations=all, --detail=all or --detail=0 is allowed (they are synonyms).
--namespace¶
Filter output with an explicit namespace selector. Use exact: for one namespace, subtree: for it and its boundary-separated descendants, or regex: for a delimiterless PCRE fragment. Bare and empty values are refused; regex always covers the full namespace.
bin/qmx check src/ --namespace='subtree:App\Service'
bin/qmx check src/ --namespace='regex:App\\(?:Billing|Sales)\\Order'
Filters violations and worst offenders to the selected namespaces. Shows subtree health scores. Auto-enables --detail.
Project-wide findings (architecture.coverage-gap and the other diagnostics that judge the run as a whole) are never selected by a namespace selector: they belong to no namespace.
Named detector findings use their declaration namespace. File-subject findings still follow the existing file-subject namespace selection behavior; a file in a namespace is not guaranteed to appear in that namespace's drill-down solely because of its path.
The same matching rule governs the health drill-down and the worst-offender lists this option turns on, and the include_namespaces option of coupling.distance.
A pattern that selects no analysed namespace is refused with exit 3, and the refusal says how many namespaces the run did have. A pattern that does select something and still reports nothing prints the ordinary empty result — that is the half of the pair worth telling apart.
Mutually exclusive with --class. Refused with exit 3 before the analysis
under --format=gitlab or --format=checkstyle, including when a configuration
file selects the format: their consumers read every entry as a finding, so
neither can say the report is a partial view.
--class¶
Filter output to a specific class by exact FQCN match.
Filters violations to the specified class. Auto-enables --detail.
An FQCN matching no analysed class is refused with exit 3, for the same reason
--namespace is: an empty report otherwise reads as a clean class.
Mutually exclusive with --namespace. Refused under gitlab and checkstyle
as --namespace is.
Cache options¶
Qualimetrix caches parsed ASTs to speed up repeated runs. The default is the platform user cache directory under qmx, shared across this user's projects and worktrees; old checkout .qmx-cache entries are not read. XDG_CACHE_HOME takes precedence when it is a supported POSIX absolute path; otherwise Linux/BSD use $HOME/.cache, macOS uses $HOME/Library/Caches, and the Windows branch reads LOCALAPPDATA. Windows drive and UNC paths are not supported by the current path grammar. An unusable default disables caching with one warning.
--no-cache¶
Disable caching entirely:
--cache-dir¶
Set a cache directory that resolves to a supported POSIX path outside the project. Relative paths are resolved from the project root; for example, ../external-cache can be used when it resolves outside the project. The default is the user cache directory under qmx. When the project root is / (for example, a cron invocation from /), containment is not checked; all other location judgements still apply.
When caching is enabled or --clear-cache uses the location, the path is judged without creating directories. --no-cache without clearing leaves an unused cache root unjudged. An explicitly configured path
inside the project, owned by another user, writable by group/others, or unable
to provide a writable, searchable directory is then refused with exit 3. The cache
root is judged; this does not establish trust for every ancestor.
An unusable implicit default disables caching and produces one warning.
An eligible directory is created only after the remaining input checks succeed.
--clear-cache¶
Clear the cache before running analysis:
Clearing starts after input checks and target preparation. A destination or its temporary sibling inside the physical cache root refuses before clearing or analysis, including through a symbolic link. A cache entry that cannot be removed produces an environment error (exit 3), naming the directory, the remaining entry count and the reason. An incomplete clear never prints "Cache cleared.". Clearing removes only qmx entries, but includes entries from every project sharing this user root. Entries not read for 30 days are swept incrementally. Each open sweeps 16 of 256 shards; after 16 successful sweep steps, the per-format target is 2 GiB plus entries written since each shard's last sweep and one oversized newest entry per shard, rather than an immediate hard quota. Temporary publication siblings are aged out after 30 days; recent siblings survive quota eviction so active writes keep their names. Their bytes are additional to the cache-entry target. Old temporary siblings in version metadata are swept too. If unrelated contents keep a detached directory present, clearing preserves them and reports refusal instead of "Cache cleared". A later clear retries retained detached directories.
Baseline options¶
See Baseline for the lifecycle and file format.
--baseline=BASELINE¶
Use a baseline file to apply accepted ceilings to live findings:
--show-resolved¶
Count complete comparable identities that no longer appear in the measured set:
Stale requires complete comparable absence. Stale and inert entries publish the project-level baseline.unused-entry Warning, subject to ordinary --fail-on policy. A group that still fires with fewer members is not resolved.
Baseline lifecycle commands¶
The commands below are the complete baseline write and inspection surface:
bin/qmx baseline:generate <baseline> [<paths>...] [--mode=MODE] [--force]
bin/qmx baseline:update <baseline> [<paths>...] [--force] [--accept-new=CHANNEL]... [--record-exclusions]
bin/qmx baseline:cleanup <baseline> [<paths>...] [--remove=REMOVE]... [--force]
bin/qmx baseline:explain <symbol> [<paths>...] [--baseline=BASELINE] [--channel=CHANNEL]
bin/qmx baseline:rename-channels <baseline> <map> [--format=FORMAT]
The first four commands accept --config=CONFIG, --preset=PRESET, --disable-rule=DISABLE-RULE, --only-rule=ONLY-RULE, and --rule-opt=RULE-OPT. All four also accept --include-generated, --include-autoload-dev, --no-cache, --workers/-w and --memory-limit. They do not accept any exclusion or suppression option. baseline:rename-channels accepts none of them: it runs no analysis, so there is no measured set for them to define.
baseline:generatecaptures the current measured findings.--mode=ratchetis the default;--mode=suppressrecords unconditional acceptance for captured identities. Its--forceoverwrites an existing file.baseline:updatetightens existing entries only. Its--forceoverrides the recorded-scope coverage guard.baseline:cleanuplists candidates by default and removes only repeated--remove=REMOVEselectors. Its--forcealso overrides the scope guard.baseline:explainshows the configured threshold, accepted baseline level, and source override for a canonical symbol;--channel=CHANNELnarrows the answer.baseline:rename-channelsrewrites thechannelfield of the entries a declared tab-separated map names, and nothing else, without analysing anything. Content, grammar and environment refusals follow the command refusal policy (exit 3); no partial write is installed. See Carry a baseline onto renamed channels — note that carrying an entry changes its selector.
The five readers preflight present documents once before analysis/carry; closed grammar refuses early and configured semantics are loaded later. The four analysing commands refuse incomplete analysis with exit 4 before classification or writing. --force overrides file/scope guards only; it cannot make
a partial measured set acceptable. Existing destinations remain byte-identical,
and baseline:generate does not create a missing destination.
Loadable baseline versions and the migration procedure for an older file are documented under Replace an older baseline.
The removed --generate-baseline and --baseline-ignore-stale options have no aliases. Use baseline:generate and explicit baseline:cleanup --remove instead.
--accept-new=CHANNEL and --record-exclusions¶
These are mutually exclusive baseline:update modes. Repeat --accept-new with exact selected declared channels to add only new complete comparable measured identities, preserving existing caps/modes. Wildcards, levels, undeclared, configuration-error and audit channels refuse with exit 3 before analysis. --record-exclusions requires exactly recorded paths even with --force, writes the complete new exclusion definition and recaptures only affected full comparable groups with their modes. Unavailable required proof refuses the whole write. Ordinary update preserves scope/exclusions; equal acceptance is unchanged and no-op preserves bytes/generated. See Baseline.
Suppression options¶
--show-suppressed¶
Show violations that were suppressed by @qmx-ignore tags, and violations suppressed by a
per-rule suppress_namespaces / suppress_namespace_channels / suppress_paths entry in qmx.yaml (see
Rules):
Independently of --show-suppressed, running with -v prints a per-rule count of how many
violations were suppressed this way. The namespace bucket includes both namespace options and
is separate from suppress_paths; each is broken down by rule name. Unlike @qmx-ignore, this suppression is otherwise silent: nothing in
the default output indicates it happened.
--show-suppressed renders part of this as prose on the text surface.
--format=suppressed reports the full composition — all eight suppression
mechanisms, not only these two — as machine-readable JSON; see
Output Formats. Either --show-suppressed or
selecting --format=suppressed (including format: suppressed in
qmx.yaml) is enough to arm the per-rule exclusion capture; you do not need
both. The two surfaces are not otherwise equivalent — see
suppressed for what each one shows.
Suppression composition includes eight mechanisms, including produced-finding
selection removals. A producer that never ran produced nothing to remove and
is reported separately under notRun, not byMechanism. Threshold-audit effects,
formatter truncation and namespace/class drill-down retain their separate roles;
truncation/drill-down change presentation, not underlying finding identity.
--no-suppression-annotations¶
Report every violation, including the ones @qmx-ignore tags suppress:
It does not change what a baseline measures
The flag affects the report only. A baseline measures the findings your
configuration and your source annotations leave standing, so a finding an
@qmx-ignore tag removes is never captured into a baseline and never
compared against one — whether or not this flag is passed.
The visible consequence: under this flag an annotated finding is shown at
its own severity and is never promoted to an error, because no baseline
entry covers it. A flag can narrow what a baseline measures
(--suppress-path, --suppress-namespace); none can widen it.
Git scope options¶
Publish findings relative to changed files while retaining declared project-scoped diagnostics, including architecture cycle findings. Layer violations follow the source file in both modes. See Git Integration for the full guide.
--report¶
Limit publication by Git while preserving selected analysis paths. Non-strict mode also retains relevant namespace/project findings; declared project-scoped findings remain visible:
--report-strict¶
Limit file-scoped code findings to changed files without namespace/project widening. Declared project-scoped findings, including architecture cycles, remain visible even in strict mode. Layer violations follow the source file:
Execution options¶
--workers, -w¶
Control parallel processing. Default: auto-detect based on CPU count.
# Disable parallel processing (single process)
bin/qmx check src/ --workers=1
# Disable parallel processing (sequential)
bin/qmx check src/ --workers=0
# Use exactly 4 workers
bin/qmx check src/ --workers=4
Tip
Use --workers=1 for debugging or single-process environments. --workers=0 disables parallelism (sequential execution); auto-detect is the default when omitted. Worker transport uses processes, not threads or forks. It requires proc_open and, outside Windows, ext-posix. On an eligible run, an explicit count above one refuses with environment exit 3 if the first worker cannot start or answer within 10 seconds; automatic selection warns and falls back to sequential processing. NUMBER_OF_PROCESSORS is read only on Windows and must be a positive decimal integer there.
--memory-limit¶
Set the PHP memory limit for analysis. By default, PHP's memory_limit from php.ini is used.
The limit governs the worker processes too (see --workers), where files are parsed and measured. A worker that runs out of memory fails the file it held: the run reports that file as failed, exits with code 4, and the failure names the limit the workers ran with; PHP's own "Allowed memory size … exhausted" line appears on stderr.
# Set memory limit to 1GB for large projects
bin/qmx check src/ --memory-limit=1G
# Unlimited memory
bin/qmx check src/ --memory-limit=-1
Valid formats: -1 (unlimited), or a positive integer with optional K/M/G suffix (e.g., 512M, 2G). 0, a leading zero (010M, which PHP would read as octal), and values above PHP_INT_MAX bytes are refused with exit code 3. Coordinator OOM after diagnostics binding and reserve allocation exits 4 with the effective limit and its source; an earlier foreign shutdown callback can overwrite the last PHP error before this handler observes it.
Equivalent YAML: memory_limit: 1G
--log-file¶
Write the run's log to a file, one JSON record per line:
The file is appended to; its parent directory must already exist. Log records produced during input checks are buffered. The target is claimed together with the report and profile targets after input checks, then buffered records are appended. Attachment publishes the log name: a successful run leaves an empty file even when no record passes its minimum level. Invalid input leaves the log file untouched.
The same link, descriptor and collision rules as --output apply. An unwritable
target is refused with exit code 3. If writing fails during analysis, the logger
remembers the first cause and counts lost records; the command reports an
environment error before publishing the report. A partial line can remain.
An empty or blank value, including --log-file=$LOG with an unset variable, is
refused with exit code 3. Leave the option out to write no file log. Only check
takes the option.
--log-level¶
Set the minimum level of log lines, for the --log-file file and for the
console:
Available levels: debug, info, warning, error. A value outside them is
refused with exit 3 instead of falling back to a default.
| Written | Console | --log-file |
|---|---|---|
| nothing | warning; info with -v; debug with -vv |
info |
a level, with -v or more |
that level | that level |
a level, without -v |
that level if stricter than warning, else warning |
that level |
Without -v the level can only narrow the console: --log-level=error hides
warnings, while --log-level=debug --log-file=qmx.log writes a detailed file and
leaves the terminal as quiet as it was. -q silences console logs whatever the
level, while command answers and terminal errors remain; the file still receives its lines.
--no-progress¶
Disable the progress bar. Useful in CI pipelines:
Accepted by every command that shows a progress bar: check, directives,
debug:layer-assignment, baseline:generate, baseline:update,
baseline:cleanup and baseline:explain. graph:export analyses a tree too
but draws no bar, so it does not take the option.
The progress bar is written to standard error, so the report on standard output
stays machine-readable even on a terminal — bin/qmx check src/ --format=json >
report.json produces valid JSON without this flag. It is drawn only when
standard error is a terminal; redirecting standard error silences the bar
without touching the report, rather than writing control bytes into the file.
The bar shares the error stream with detailed logging (-v, -vv, -vvv) and
with warnings emitted during a run. Both are drawn through one owner: a
diagnostic line pushes the bar down and stays on the screen, and the bar is
redrawn beneath it. Raising verbosity therefore does not turn the bar off, and
the bar does not eat log lines.
--silent, -q/--quiet¶
Symfony console flags that suppress normal output. Both are accepted by every command:
--silent and -q/--quiet suppress progress and status logs, but preserve
the answer of an answer-producing command and terminal error messages.
SHELL_VERBOSITY=-1 or -2 has the same answer behavior. A refused input
still writes its reason to stderr for prose formats, while JSON formats publish a JSON error envelope on stdout; a successful check --format=json
writes its complete JSON document to stdout. To consume only the exit code,
redirect stdout; redirect stderr as well only if diagnostics are unwanted.
Profiling options¶
--profile¶
Enable the internal profiler. Optionally specify a file to save the profile:
<!-- llms:skip-end -->
# Show profiling summary on screen
bin/qmx check src/ --profile
# Save profile to file
bin/qmx check src/ --profile=profile.json
A target that cannot be written is refused with exit 3 before analysis by
the same target rules as --output. An empty --profile= is
also refused. A refusal before analysis starts, including an invalid baseline,
does not publish a profile. Once analysis has started and the command reaches
its completion path, the profile is published before the report; parallel runs
include collection.workers. A profile write failure follows the run error
envelope instead of leaving a successful report on stdout. A coordinator OOM
before profile publication writes no new profile. A profile already published
before a later report OOM remains; published targets are not rolled back.
Unpublished named targets are released on the bound OOM path. The guarantee
begins after diagnostics are bound and reserve memory is allocated.
--profile-format¶
Choose the profile export format. Default: json. Any other value is refused with exit
code 3 before analysis starts, whether or not --profile names a file.
Available formats: json, chrome-tracing.
Tip
Use chrome-tracing format and open the file in Chrome DevTools (chrome://tracing) for a visual timeline.
Rule options¶
--disable-rule¶
Disable a producer rule, an entire group, or a finding channel. A selector is either an
exact name (a producer rule or a channel), or X.* for strictly the descendants of
X — X itself is not included; a group is selected only this way (complexity.*). A bare
prefix without the star is an error. A channel selector can be narrowed to one level of the aggregation tree with
:level, same as --only-rule. Disabling one channel keeps its producer active so that other
channels can still be reported. Can be repeated:
# Disable one rule
bin/qmx check src/ --disable-rule=size.class-count
# Disable all complexity rules
bin/qmx check src/ --disable-rule=complexity.*
# Disable multiple
bin/qmx check src/ --disable-rule=complexity.* --disable-rule=cohesion.lcom
# Disable only one computed finding channel
bin/qmx check src/ --disable-rule=health.complexity
Duplication memory failure
If PHP exhausts memory during duplication.clone, the detector emits a short
stderr diagnostic with the failure site and current memory_limit, recommends
--memory-limit or memory_limit in qmx.yaml, and exits with code 4. A
fatal OOM may interrupt report delivery; valid or complete JSON is not promised.
A late false read also makes the run incomplete with exit 4 and clears partial
Duplication output. An empty result from that run is not evidence of zero copies.
To skip detection deliberately, disable duplication.clone itself.
--only-rule¶
Run only matching producer rules or finding channels. A selector is either an exact name
(a producer rule or a channel), or X.* for strictly its descendants — the only way to
select a group, either optionally narrowed to one level of the aggregation tree with :level.
A selector carrying a level keeps its producer running, since a producer filtered out would
never emit the level that was asked for. Can be repeated:
# Run only complexity rules
bin/qmx check src/ --only-rule=complexity.*
# Run two specific rules
bin/qmx check src/ --only-rule=complexity.ccn --only-rule=size.method-count
# Select one channel of a built-in health dimension: producer and channel
# share the name, since each of the six dimensions is its own producer
bin/qmx check src/ --only-rule=health.complexity
Selectors must match a registered producer or emitted channel exactly, or resolve an
X.* to at least one descendant. Unknown selectors — including a bare prefix without the
star, or an X.* that matches nothing — fail closed with exit 3 before stdout receives a
report payload. When the starred spelling would have matched, the refusal names it:
Rule selector "complexity" does not match any registered producer or channel. Write "complexity.*" to select its descendants.
computed is the one bare word that is accepted: it is the name of a producer, not a group.
Likewise, the owner before : in --rule-opt=RULE:OPTION=VALUE must be an exact
producer rule, not a group or channel — a group or channel there is an error. The same rule
governs the rules: YAML section keys.
Selection roles are declared per channel
annotation.unresolved-directive and annotation.unused-directive are
directly selectable. Unsupported/invalid thresholds follow the addressed
producer. Explicitly disabling annotation.directive stops its
producer. Root path exclusions and namespace exclusions retain their own
subject-level applicability; they are not a second selection resolver.
--rule-opt¶
Override rule options from the command line. Format: rule-name:option=value, where
rule-name must be an exact producer rule — never a group, never a channel, and never a
wildcard. The same exact-producer constraint governs the rules: YAML section keys;
level-qualified selection instead addresses a declared channel code. Can be repeated:
bin/qmx check src/ --rule-opt=complexity.ccn:callable.warning=15
bin/qmx check src/ --rule-opt=complexity.ccn:callable.error=30
All three ways of getting it wrong are refused with exit 3, where the option
pair used to be dropped without a word: a value written without =VALUE, a
rule name no registered producer answers to, and an option name that rule does
not accept. The refusal for the last one lists the options the rule does
accept.
suppress_namespace_channels is configured in YAML, not through --rule-opt: each selector
requires a non-empty list of namespace patterns, while channel-keyed dictionaries are not CLI dotted addresses. Its
keys are channel selectors and follow the same exact-or-X.* rule as @qmx-ignore — a bare
prefix like health is now an error, not a shorthand for health.*. A key may add :namespace
and no other level: the option is offered namespace aggregates only, so any other level would
name a filter that can never fire.
Rule-specific shortcut flags¶
Many rules have dedicated CLI flags for quick rule-option configuration:
| Flag | Rule | Option |
|---|---|---|
--cyclomatic-warning=N |
complexity.ccn | callable.warning |
--cyclomatic-error=N |
complexity.ccn | callable.error |
--cyclomatic-class-warning=N |
complexity.ccn | class.max_warning |
--cyclomatic-class-error=N |
complexity.ccn | class.max_error |
--cognitive-warning=N |
complexity.cognitive | callable.warning |
--cognitive-error=N |
complexity.cognitive | callable.error |
--cognitive-class-warning=N |
complexity.cognitive | class.max_warning |
--cognitive-class-error=N |
complexity.cognitive | class.max_error |
--npath-warning=N |
complexity.npath | callable.warning |
--npath-error=N |
complexity.npath | callable.error |
--npath-class-warning=N |
complexity.npath | class.max_warning |
--npath-class-error=N |
complexity.npath | class.max_error |
--wmc-warning=N |
complexity.wmc | warning |
--wmc-error=N |
complexity.wmc | error |
--wmc-exclude-data-classes |
complexity.wmc | excludeDataClasses |
| Flag | Rule | Option |
|---|---|---|
--cbo-warning=N |
coupling.cbo | class.warning |
--cbo-error=N |
coupling.cbo | class.error |
--cbo-ns-warning=N |
coupling.cbo | namespace.warning |
--cbo-ns-error=N |
coupling.cbo | namespace.error |
--distance-warning=N |
coupling.distance | max_distance_warning |
--distance-error=N |
coupling.distance | max_distance_error |
--instability-class-warning=N |
coupling.instability | class.max_warning |
--instability-class-error=N |
coupling.instability | class.max_error |
--instability-ns-warning=N |
coupling.instability | namespace.max_warning |
--instability-ns-error=N |
coupling.instability | namespace.max_error |
--class-rank-warning=N |
coupling.class-rank | warning |
--class-rank-error=N |
coupling.class-rank | error |
| Flag | Rule | Option |
|---|---|---|
--class-count-warning=N |
size.class-count | warning |
--class-count-error=N |
size.class-count | error |
--method-count-warning=N |
size.method-count | warning |
--method-count-error=N |
size.method-count | error |
--property-count-warning=N |
size.property-count | warning |
--property-count-error=N |
size.property-count | error |
| Flag | Rule | Option |
|---|---|---|
--dit-warning=N |
design.dit | warning |
--dit-error=N |
design.dit | error |
--lcom-warning=N |
cohesion.lcom | warning |
--lcom-error=N |
cohesion.lcom | error |
--lcom-min-methods=N |
cohesion.lcom | minMethods |
--lcom-exclude-readonly |
cohesion.lcom | excludeReadonly |
--lcom-exclude-methods='[NAME]' |
cohesion.lcom | excludeMethods |
--noc-warning=N |
design.noc | warning |
--noc-error=N |
design.noc | error |
--param-type-coverage-warning=N |
design.type-coverage.param | warning |
--param-type-coverage-error=N |
design.type-coverage.param | error |
--return-type-coverage-warning=N |
design.type-coverage.return | warning |
--return-type-coverage-error=N |
design.type-coverage.return | error |
--property-type-coverage-warning=N |
design.type-coverage.property | warning |
--property-type-coverage-error=N |
design.type-coverage.property | error |
--property-exclude-readonly |
size.property-count | excludeReadonly |
--property-exclude-promoted-only |
size.property-count | excludePromotedOnly |
| Flag | Rule | Option |
|---|---|---|
--mi-warning=N |
maintainability.mi | warning |
--mi-error=N |
maintainability.mi | error |
--mi-min-statements=N |
maintainability.mi | minStatements |
--mi-exclude-tests |
maintainability.mi | excludeTests |
| Flag | Rule | Option |
|---|---|---|
--constructor-overinjection-warning=N |
code-smell.constructor-overinjection | warning |
--constructor-overinjection-error=N |
code-smell.constructor-overinjection | error |
--data-class-woc-threshold=N |
design.data-class | wocThreshold |
--data-class-wmc-threshold=N |
design.data-class | wmcThreshold |
--data-class-min-members=N |
design.data-class | minMembers |
--data-class-exclude-readonly |
design.data-class | excludeReadonly |
--data-class-exclude-promoted-only |
design.data-class | excludePromotedOnly |
--data-class-exclude-exceptions |
design.data-class | excludeExceptions |
--god-class-wmc-threshold=N |
design.god-class | wmcThreshold |
--god-class-lcom-threshold=N |
design.god-class | lcomThreshold |
--god-class-tcc-threshold=N |
design.god-class | tccThreshold |
--god-class-class-loc-threshold=N |
design.god-class | classLocThreshold |
--god-class-min-criteria=N |
design.god-class | minCriteria |
--god-class-min-methods=N |
design.god-class | minMethods |
--god-class-exclude-readonly |
design.god-class | excludeReadonly |
--long-parameter-list-warning=N |
code-smell.long-parameter-list | warning |
--long-parameter-list-error=N |
code-smell.long-parameter-list | error |
--long-parameter-list-vo-warning=N |
code-smell.long-parameter-list | vo-warning |
--long-parameter-list-vo-error=N |
code-smell.long-parameter-list | vo-error |
--unreachable-code-warning=N |
code-smell.unreachable-code | warning |
--unreachable-code-error=N |
code-smell.unreachable-code | error |
| Flag | Rule | Option |
|---|---|---|
--circular-deps |
architecture.circular-dependency | enabled |
--max-cycle-size=N |
architecture.circular-dependency | maxCycleSize |
--layer-violation |
architecture.layer-violation | enabled |
--layer-violation-severity=SEVERITY |
architecture.layer-violation | severity |
--unassigned-class-mode=MODE |
architecture.unassigned-class | mode |
Other commands¶
baseline:cleanup¶
Inspect stale candidates in a baseline. Without --remove, it only lists them and never writes the file; remove an explicitly reviewed selector as described in Baseline:
bin/qmx baseline:cleanup baseline.json src/
bin/qmx baseline:cleanup baseline.json src/ --remove=<selector>
debug:layer-assignment¶
bin/qmx debug:layer-assignment 'App\Service\Foo' --format=json
bin/qmx debug:layer-assignment 'app\service\foo' --config qmx.yaml
| Option | Meaning |
|---|---|
-c, --config=FILE |
Configuration file; default qmx.yaml. |
--preset=PRESET |
Named or file preset; repeatable. |
--format=FORMAT |
text (default) or json. |
The command runs normal Discovery/Collection and looks up observed declarations
and graph ends; install-only classes do not enlarge that population. ASCII
case variants resolve to observed spelling, and parser-observed high-byte names
work. Only an empty normalized name is refused before analysis; other unknown
input exits 3 after lookup even with disabled policy. Informational answers exit 0.
Configure the three layer-policy producers in YAML; this command does not add
the check command's --disable-rule option.
JSON carries meta, canonical fqn, assigned, contendingMatches, shadowed,
shadowedBy, undecided, contenders, chainStopsAt, hasLayers,
policyDisabled, and edgeEndOnly. Shadow verdicts expose their exact
exemption when present; disabled policy omits reported/exemption and the
diagnostic/docs hint. See Architecture
for the full semantics and native example. JSON errors replace the response
with the standard error/exit_code envelope.
directives¶
Report what every inline @qmx-ignore and @qmx-threshold in the analysed tree actually does. A suppression is judged by what it silenced; a threshold directive is judged by removing it and executing the rules again over the run's own measurements — by default only the rule it addresses, one execution per directive.
bin/qmx directives src/
# Re-execute every enabled rule instead of just the addressed one — the control the narrow default is measured against
bin/qmx directives src/ --sweep=full
# Machine-readable output — for agents, scripts and CI
bin/qmx directives src/ --format=json
| Option | Description |
|---|---|
-c, --config=FILE |
Path to qmx.yaml (default: qmx.yaml in the current directory) |
--format=FORMAT |
text (default) or json |
--sweep=SCOPE |
How much of the rule layer each counterfactual re-executes: narrow (default) or full |
--preset=PRESET |
Apply a named preset (repeatable) |
--only-rule=RULE |
Judge under a run that ran only these rules (repeatable) |
--disable-rule=RULE |
Judge under a run with these rules off (repeatable) |
--rule-opt=RULE:OPT=VAL |
Judge under a run with this rule option (repeatable) |
--no-cache |
Do not read or write the AST cache |
--cache-dir=DIR |
AST cache directory, judged as for check |
-w, --workers=N |
Parallel workers (0 = sequential) |
--memory-limit=LIMIT |
PHP memory limit, e.g. 512M or -1 |
The four selection options exist because a verdict is relative to the run that produced it: point the command at the same rules and boundaries your CI checks with, or it will answer about a different run.
A @qmx-threshold names exactly one rule, so under --sweep=narrow a counterfactual re-executes only that rule. --sweep=full re-executes every enabled rule for the same verdicts, at far higher cost — it is not a slower fallback but the control that measures, rather than assumes, that removing a directive of one rule cannot move another rule's findings: the two scopes are swept over the same tree and compared verdict for verdict. On this project's own src/ the narrow sweep is several times cheaper and the two scopes agree on every verdict. Both the text report and --format=json state the sweep the verdicts were measured under.
The execution options mean what they mean for check (--no-cache, --cache-dir, --workers, --memory-limit), so an audit can run isolated from the shared cache: bin/qmx directives src/ --no-cache --workers=0.
Exit codes: 0 no publishable refusal or observable inert directive, including
a complete intentionally empty excluded set; 2 at least one publishable
refusal or inert directive whose boundary is observable; 3 bad
input/configuration, including truly undiscovered empty input; 4 incomplete
input; 5 unexpected internal command failure. Incompleteness takes priority.
Text and JSON retain all sites, including refusals whose channels the final
selection does not publish. A measured scope note appears in text and JSON
scope.note. JSON also publishes the measured project scope at
scope.project_scope, including state, unjudged channels and source reasons.
There are five verdicts:
| Verdict | What it states |
|---|---|
| effective | It silenced a produced finding or its removal changes the threshold result. |
| applied-boundary-only | It applied; only the boundary printed by the finding moved (JSON: overrun). |
| inert | It silenced nothing or its removal changes nothing; exit 2 only if the boundary is observable. |
| unmeasured | Its producer did not run, its subject was not covered, or another threshold directive masks it. |
| refused | It could not be admitted or applied, with a nonempty list of refusal details. |
Every read tag has one site, independent of its declaration bindings.
An otherwise inert suppression stays unmeasured when the run cannot judge its
producer's subject. A narrowed run cannot prove a run-dependent declaration
unused; analyzed local member evidence can still be judged.
Suppression/diagnostic positions distinguish identical comments on one line;
JSON still publishes file, line, form and target, not the internal
position. For next-line controls, line is the tag line, not the target
after the comment. Refused sites carry required
refusals: [{channel, message}]; other effects carry an empty list.
reason describes only Unmeasured, with masked_by when applicable.
Neither refusal JSON nor site JSON publishes the internal addressed producer.
A verdict is relative to the analysed scope
A threshold on a metric computed over the analysed subgraph — coupling above all — can be alive over the whole project and dead over one directory of it, and neither answer is wrong. Point the command at what the project actually analyses. The report prints the scope it measured under, and a run that failed to parse part of the tree exits 4 instead of calling anything dead.
Judged against what the rules produced, not against the report
suppress_paths, suppress_namespaces and suppress_namespace_channels suppress publication, not measurement. A directive that moved a finding inside an excluded namespace still did something, so the audit asks its question against every finding the rules produced, not against the report. The one channel outside that universe is annotation.unused-directive, which a run assembles after the rules have run — no directive may address it, so no verdict is judged against it.
Suppression receives no credit for silencing a configuration error
(annotation.unresolved-directive and its two siblings). Those channels
are exempt from annotation suppression by construction; admission and
reach can still refuse a directive before its effect is judged.
An explicit selector addressing annotation.unused-directive or
duplication.clone is refused, after reach/level admission. The audit
carries the same refusal details as check. Blanket * and bare file
directives remain effective/inert over other findings and cannot silence
either banned channel.
The applied-boundary-only verdict deliberately makes no claim about direction. The rule layer has no notion of which way is stricter — coupling.instability is worse when higher, cohesion.tcc when lower — so a directive that tightens a boundary and one that raises a boundary the measured value had already passed are the same observable. In --format=json this verdict keeps the stable key overrun.
Where a rule publishes no boundary alongside its finding, an inert verdict carries a note saying so, and does not fail the build: a boundary the value had already passed would have looked identical, so demanding the directive be deleted would report an unasked question as proven debt. --format=json reports it as "boundary_observable": false.
On error, --format=json prints {"error": "...", "exit_code": N, "position": ..., "source": ...} to stdout instead of the human <error> line.
graph:export¶
The command reads the shared configuration document. Without path arguments,
it uses configured or Composer defaults and applies exclude and the
@generated filter. It accepts --config, --preset, --exclude,
--include-generated, --include-autoload-dev, --no-cache, --workers/-w
and --memory-limit. --format/-f selects only dot or json, independently
of the document's analysis format. --direction has no short alias; global
-d changes the working directory.
Export the dependency graph for visualization:
# Export as DOT (default)
bin/qmx graph:export src/ -o graph.dot
# Export as JSON (aggregated adjacency list with metadata)
bin/qmx graph:export src/ --format=json -o graph.json
# Filter by namespace
bin/qmx graph:export src/ --namespace='subtree:App\Service' --namespace='subtree:App\Repository'
# Exclude namespaces
bin/qmx graph:export src/ --exclude-namespace='subtree:App\Generated'
# Change layout direction
bin/qmx graph:export src/ --direction=TB
# Disable namespace grouping
bin/qmx graph:export src/ --no-clusters
| Option | Description |
|---|---|
-o, --output=FILE |
Output file (default: stdout) |
-f, --format=FORMAT |
dot (default) or json |
--direction=DIR |
Graph direction: LR, TB, RL, BT (default: LR) |
--no-clusters |
Do not group nodes by namespace |
--namespace=SELECTOR |
Include only namespaces selected by exact:, subtree:, or regex: (repeatable) |
--exclude-namespace=SELECTOR |
Exclude namespaces using the same explicit forms (repeatable) |
Every --namespace or --exclude-namespace selector must bind to an original
graph vertex before filtering; an unbound value is refused with exit 3. Both
doors use the same binding check. A case-only near match suggests the graph's
exact spelling without accepting the wrong case.
An --output target is judged and written the way check treats its own
--output: a directory or a name ending in /, an unwritable
existing file and a new name in a directory that does not exist or does not
allow creating a file are refused with exit 3 before any file is read. A named
regular target is staged and published atomically; a trusted symbolic link keeps
its entry and publishes its referent. /dev/stdout and named pipes retain their
stream delivery. The same SIGINT/SIGTERM capability boundary applies.
If any discovered file fails parsing or processing, graph:export exits 4 and
emits no partial graph. It does not create a missing output file and preserves
an existing destination byte-for-byte.
hook:install¶
Install a git pre-commit hook:
hook:status¶
Show the current status of the pre-commit hook:
hook:uninstall¶
Remove the pre-commit hook:
bin/qmx hook:uninstall
# Restore the original hook from backup
bin/qmx hook:uninstall --restore-backup
rules¶
Accepts --config=FILE and repeatable --preset=PRESET, judges the complete
document and includes named computed metrics. It marks rules disabled by the
current final only_rules/disabled_rules selection. It requests no analysis
paths, so an empty PHP tree does not prevent listing rules. An invalid document
refuses with exit 3. No separate Console parser of rules: false or
enabled: false drives this mark; those switches belong to rule resolution.
List all available rules with their descriptions and CLI options:
Example output (for --group=complexity):
4 rules available
Complexity
complexity.ccn Checks cyclomatic complexity at method and class levels
complexity.ccn judges complexity.ccn, complexity.ccn.max
options: enabled, threshold
options at callable: enabled, error, threshold, warning
options at class: enabled, max-error, max-warning, threshold
--cyclomatic-warning (--rule-opt=complexity.ccn:callable.warning=...)
--cyclomatic-error (--rule-opt=complexity.ccn:callable.error=...)
--cyclomatic-class-warning (--rule-opt=complexity.ccn:class.max-warning=...)
--cyclomatic-class-error (--rule-opt=complexity.ccn:class.max-error=...)
...
Every rule also takes: suppress-namespace-channels, suppress-namespaces, suppress-paths
Usage: bin/qmx check --disable-rule=<name> | --only-rule=<name>
bin/qmx check --rule-opt=<name>:<option>=<value>
Docs: https://qualimetrix.dev · AI agents: https://qualimetrix.dev/llms.txt
Rules are grouped by category. options: names what the rule accepts in its
own block, and an options at <level>: line names what a level slot accepts —
these are the complete set, whether or not an option also has a CLI alias. The
three keys in the footer are legal under every rule. enabled is listed with
each rule, including architecture.unassigned-class: false disables it even in a reportable mode;
explicit true with mode: ignore refuses because the producer would remain inactive.
Each CLI alias is listed with the long --rule-opt form it expands to. Default
threshold values are not part of this output — see
Default thresholds.
Typed rule values and final selection¶
Both --rule-opt and dedicated aliases parse the same declared YAML value form.
For example --lcom-exclude-methods='[getName, getDescription]' writes a sequence;
CSV or a scalar is not that sequence. Null does not become text. Writing the same
canonical option twice through aliases or --rule-opt in one invocation refuses.
A dotted option address traverses only a declared level slot, never arbitrary
channel-keyed dictionaries. Put suppress_namespace_channels maps in YAML.
A bare name selects an exact producer or channel; X.* selects strict descendants.
A pair channel-name:level requires that declared channel code to report at the
specified level. When producer and channel have the same name, it works as a
channel name; there is no differently named producer:level alias. The same witness
must also belong to the supplied producer/set for namespace-channel exclusions.
Higher-layer decisions win before specificity; at the same layer an exact producer
enable beats a group disable. A later exact enable can cancel a lower disable.
An exact enable and disable of the same producer in one layer refuses even if
later overridden. only_rules selects a filter; it does not enable an inactive
producer. Empty effective selection, a dead exact selector, an enable outside its
own/lower effective filter and an explicit enable with muted option activity refuse.
A higher disable may intentionally narrow an earlier filter. Empty only_rules
removes a lower filter, while empty maps never reset options.
Most channels are directly selectable. Declared diagnostic roles admit filter-exempt
channels or diagnostics that follow a selected addressed producer. These are not
extra producer enable statements: explicitly disabling their producer still stops
it. annotation.unresolved-directive and annotation.unused-directive are
directly selectable; unsupported/invalid threshold errors follow the
addressed producer. Fix the source input rather than hiding a configuration error
with an unrelated only filter.
bin/qmx rules prints accepted root/level options independently of CLI aliases,
then the effective only filter and all tied decisive disabled texts. It also prints
Selection source: ... (...; layer N) from the actual origin and layer index.
One writer repeated across cells prints once; equal text from distinct layers does
not merge. Listing judges the document and stated selection, not effective-band
build/conclude or runtime state. Directive text/JSON preserve all decisive disabling
texts and omit ones canceled by later enable; JSON selection.disabled stays a string
list. For values and layer forms see Configuration.
Prose environment¶
QMX_ASCII is judged before any command runs. Case-insensitive
1/true/yes/on enables ASCII; 0/false/no/off, empty and absence select
Unicode. Other values refuse with exit 3 and name the accepted values;
structured refusals identify source.kind: environment and source.name: QMX_ASCII.
The mode affects the four prose analysis formats and every ErrorStream
diagnostic, including progress, stdout reports and --output report files.
It replaces the closed product glyph table; other Unicode such as Café
remains unchanged. A table glyph inside a source name is replaced too:
K✓ becomes K+ in ASCII prose. Use Unicode to preserve it. Structured
identities are unchanged. Other commands' stdout, such as rules, directive
text, selected debug output and DOT, is outside this mode.
Namespace selection uses a finding's declared namespace. For a file aggregate
(annotation.*, duplication.clone), it matches any namespace the file opens
or declares. A file with neither uses the global namespace.
--class never selects a file aggregate. suppress_namespaces deliberately
does not take this file step; use suppress_paths for a file aggregate.
Overlapping namespace exclusions keep credit for their first match.
suppressed --namespace/--class refuses with exit 3 because the composition
document describes the whole run. --show-suppressed remains allowed and
labels its stderr list as whole-run with the selector unapplied. The analysis
and exit verdict always cover the whole run.