Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased¶
0.23.0 - 2026-07-29¶
Fixed¶
code-smell.boolean-argumentno longer flags promoted constructor properties (public bool $x) by default — a promoted parameter declares a field, not a behavior switch, so the rule's "split into two methods" advice never applied to it. Setflag_promoted_properties: trueto restore the previous behavior.duplication.code-duplicationno longer flags a duplicate block that lies entirely inside aconstarray or a static/instance property's array-literal initializer — repeated key/value shape across the rows of a data table is normal, and "extract a shared method" was never actionable advice for it. A block that extends past the declaration into surrounding code is still reported, so two otherwise identical classes wrapping the same table remain a finding.
0.22.0 - 2026-07-28¶
Fixed¶
- Rule options set through the config file or
--rule-optwere silently ignored when the option name had more than one word (vo-warning,param_threshold, …), while the dedicated CLI flag for the same option worked but printed a bogusUnknown optionwarning. All three channels now agree, and--preset=strictapplies itsvo-errorvalue instead of dropping it. Values were dropped forcode-smell.long-parameter-listanddesign.type-coverage;coupling.distanceonly ever suffered the false warning. - The documented
threshold:shorthand crashed the whole run withCannot mix "threshold" with "warning"/"error"(exit code 3) whenever it was written at the top level of a rule, which is exactly howwebsite/docs/getting-started/configuration.mdshows it. 15 rules were affected; the nestedmethod: {threshold: …}form was never broken. - The
threshold,vo-thresholdand*_thresholdshorthands no longer produce a falseUnknown optionwarning on rules that support them. coupling.cboandcoupling.instabilitynow accept thethresholdshorthand at the rule's top level, applying it uniformly to the class and namespace dimensions. They were the only two threshold rules of twenty that rejected it, answering a barethresholdwith a bewilderingUnknown optionwarning.code-smell.long-parameter-listnever applied itsvo-warning/vo-errorthresholds: value-object constructors were reported against the ordinary thresholds instead. The VO detection flag never reached the rule.architecture.unreachable-layerno longer fires for layers that only ever match as the target of a dependency — such as vendor boundary layers (ClickHouseDB\**). Such a layer was reported unreachable in the same run wherearchitecture.layer-violationflagged a real edge into it.- Parameter and return types of closures and arrow functions are now collected into the dependency graph. Previously only their bodies were, so a layer violation that entered exclusively through a closure signature was invisible to
architecture.layer-violation, coupling metrics andgraph:export. - Global
exclude_namespaces(and--exclude-namespace) no longer suppressarchitecture.*violations. Silencing a noisy metric in a namespace used to switch off layer-policy enforcement there as a side effect. Per-rule exclusions still work — see below.
Changed¶
- Severities of
architecture.unreachable-layer,architecture.potential-shadowandarchitecture.empty-templateare configurable viaunreachable_layer_severity,potential_shadow_severityandempty_template_severityon thearchitecture.layer-violationrule. Defaults are unchanged, so a typo inpatterns:can now fail the build instead of only whispering at info level. - Violations dropped by per-rule
exclude_namespaces/exclude_pathsare now reported:-vprints how many were suppressed and by which rules, and--show-suppressedlists them in a block of their own, separate from@qmx-ignore. They used to disappear without a trace — on this repository's own configuration that hid 387 violations.
Breaking¶
architecture.coveragewithcoverage: warnnow reportsWarningseverity instead ofInfo, matching the mode's name. If you relied on it staying silent underfail_on: warning, switch tocoverage: ignoreor raisefail_ontoerror.- Dependency graph gained edges: parameter and return types of closures and arrow functions, plus attributes on their parameters. Coupling metrics that read the graph — CBO, ClassRank, instability, distance and the derived health scores — shift accordingly, and
architecture.circular-dependencymay report cycles that were previously invisible. Thresholds tuned against the old graph may need revisiting; a baseline generated before this release stays valid only for violations whose identity did not change. ThresholdParser::parse()replaced thelegacyWarningKeys/legacyErrorKeysparameters with a singlelegacyKeysarray keyed bywarning/error/threshold. Named-argument calls fail withUnknown named parameter; the old positional form silently loses its legacy keys. Only affects third-party rule packages calling the parser directly.RuleExecutorInterfacegainedgetRuleExclusionStats(). Third-party implementations of the interface must add it.ViolationFilterOrchestrator::__construct()takes an additional requiredRuleExecutorInterfaceargument. Only affects code constructing it directly; the container wires it automatically.
0.21.0 - 2026-07-28¶
Fixed¶
qmx rulescrashed with a fatalArgumentCountErrorinstead of listing the rules. The command built rule objects itself, which breaks for rules that take constructor dependencies besides their options (architecture.layer-violation). Rule instances now always come from the DI container;qmx checkwas never affected.
Breaking¶
RuleRegistryInterface::getAll()removed — it could not build rules that declare constructor dependencies beyond their options. Embedding consumers that need rule instances should take them from the container (tagqmx.rule);getClasses()andgetAllCliAliases()still cover metadata.RulesCommand::__construct()now takesiterable<RuleInterface> $rulesinstead of aRuleRegistryInterface. Only affects code that constructs the command directly; the container wires it automatically.
0.20.1 - 2026-07-28¶
Fixed¶
- Qualimetrix reported the consuming project's version as its own.
qmx --versionprinted things like1.0.0+no-version-set, and the same wrong value was stamped into every analysis artifact —versionin JSON and SARIF,toolVersionin the metrics format, and the HTML report footer. The version is now resolved by package name instead of through Composer's root package, which is the host project whenever Qualimetrix is installed as a dependency.
0.20.0 - 2026-07-28¶
Earlier releases can no longer be installed. The repository history was rewritten to remove content that should never have been published. Every tag before v0.20.0 now points at a commit that no longer exists, so
composer require qualimetrix/qualimetrix:<older version>fails with a 404 from GitHub. The published archives cannot be restored — upgrade to v0.20.0.
Security¶
symfony/yamlupdated to v8.0.14, clearing three advisories: a ReDoS via catastrophic backtracking in the parser cleanup regex (CVE-2026-45305), stack exhaustion via unbounded recursion in nested blocks (CVE-2026-45133), and CVE-2026-45304. Qualimetrix parses YAML configuration on every run, so this affects all users.symfony/cacheupdated to v8.0.14, clearing CVE-2026-45073. It is pulled in transitively bysymfony/expression-language, which backs computed metric formulas.
Breaking¶
AnalysisConfiguration::{projectRoot, cacheDir, composerJsonPath}are now typed asAbsolutePath/?AbsolutePathinstead ofstring/?string. Embedding consumers that constructAnalysisConfigurationdirectly must wrap path arguments inAbsolutePath::fromString(...). The no-arg constructor still works as before — defaults resolve lazily togetcwd()and${projectRoot}/.qmx-cache.fromArray()andmerge()continue to accept string values from YAML / CLI input and resolve them viaPathFactory::fromCliArgument(). ADR 0015 Phase 5.BaselineWriter::write()now requiresAbsolutePathfor the$projectRootparameter (was optionalstring = '.'). Embedded callers must wrap their project root and pass it explicitly.GitClient::getProjectRoot()accessor removed. The project root is now owned byGitScopeResolution(returned fromGitScopeResolver::resolve()); pass it explicitly to consumers that previously read it fromGitClient.FileProcessorInterfacegainssetProjectRoot(AbsolutePath): void.CollectionOrchestratorInterface::collect()gains a requiredAbsolutePath $projectRootparameter. Custom orchestrators / processors must add the method or update the call. ADR 0015 Phase 6.
Changed¶
- Configuration, cache, parallel pipeline, namespace detection, and dependency analysis now consume
AbsolutePath/RelativePathVOs at every internal boundary instead of untyped strings. The migration closes the path-type ambiguity that motivated the T10 git-subdirectory bug class. ADR 0015 Phase 5. - Git infrastructure now uses typed
AbsolutePathandRelativePathVOs instead ofstringthroughoutGitClient,GitRepositoryLocator, andGitScopeFilter. ADR 0015 Phase 1b. GitScopeFilternow performs eager git-to-project path translation at theGitClientboundary. Project roots that sit in a strict subdirectory of the git tree (T10) are now handled correctly: changed files outside the project are filtered out early, and namespace extraction for violations is resolved against the project root instead of the git top-level.- The project's own dogfooding
qmx.yamlnow declares the full 27-layer architecture topology (Core + Configuration + Architecture slice + per-categorymetrics-{Category}template + 10analysis-*sub-layers + 10infra-*sub-layers) that previously lived indeptrac.yaml. Sub-layer enforcement (e.g.analysis-discovery → analysis-pipelineis now caught) gained, on top of features deptrac never had: per-category metric isolation via template expansion, and arelations:filter that permitsinfra-di → metrics-*references but forbids inheritance. ADR 0014. Violation::$location->$fileis now typed as?RelativePath(wasstring). Architecture violations not tied to a single file useLocation::none()(file isnull). Wire/comparator surface preserved viaLocation::pathString()andLocation::isNone()— formatters and JSON output emit the same shape as before, but file paths going intoLocationmust be project-relative.WorstOffender::$fileandDuplicateLocation::$filemigrated similarly;ParseException::$filePathnow carriesAbsolutePath(lives at the parser boundary, where absolute paths are the natural representation). ADR 0015 Phase 1a.
Removed¶
deptrac/deptracdev-dependency.composer checkis nowcs-check + test + phpstan + selfcheck; architecture enforcement runs entirely through Qualimetrix's ownarchitecture.layer-violationrule.- Internal
Qualimetrix\Core\Util\PathNormalizerhelper (was@internalsince v0.18). Superseded byCore\Path\PathFactory. ADR 0015 Phase 6 also wires a PHPStan rule (qmx.bannedStringPathProperty) as a regression guard against re-introducingstring-typed$file/$filePath/$oldPathproperties in scoped namespaces.
Fixed¶
- The HTML report build manifest (
src/Reporting/Template/package.jsonand its lockfile) is now tracked. A blanket*.jsonignore rule had been excluding it, so a fresh clone could not runcomposer test:jsorcomposer build:js, and the committeddist/bundle could not be regenerated or audited.
0.19.0 - 2026-05-17¶
Breaking¶
ThresholdAwareOptionsInterfacegains a staticgetOverrideValidator()accessor that returns the per-ruleOverrideValidatorInterfacestrategy used to validate@qmx-thresholdannotations. Custom Options classes in extension code must implement the new method oruse StandardOverrideValidatorTrait;for the defaultwarning ≤ error + non-negativesemantics. See ADR 0013.
Changed¶
- Invalid
@qmx-thresholdannotations now surface a rule-specific code (e.g.warning_exceeds_error,error_exceeds_warning,error_not_supported) asviolationCode: annotation.invalid-threshold.<code>in JSON / SARIF / Checkstyle output; the human message is unchanged. Validators that provide a remediation hint (e.g. WarningOnly's "omiterror=...") now flow through torecommendationso users see actionable follow-up.
Fixed¶
@qmx-threshold maintainability.index warning=N error=Mannotations withN > Mwere silently rejected by the parser, even though the rule's defaults arewarning=40 error=20(inverted thresholds are the natural orientation). The bug was latent across releases — Maintainability annotations work for the first time in v0.19.@qmx-threshold design.type-coverage warning=N error=MwithN > Mwas rejected on the same parser invariant; type coverage is an inverted-threshold rule and now accepts the natural form.@qmx-threshold design.data-class warning=N error=Mwas rejected whenN > M, but the rule maps warning towocThreshold(high) and error towmcThreshold(low) — independent metrics on independent axes. The annotation now validates accordingly.@qmx-threshold design.god-class warning=W error=Epreviously accepted theerrorvalue silently and then discarded it insidewithOverride(). Expliciterror=Nis now rejected at parse time with a clear diagnostic; the shorthand form@qmx-threshold design.god-class Nstill works.
0.18.0 - 2026-05-16¶
Breaking¶
architecture.layersYAML schema is now an ordered list (long form only), not a map. The first layer whose patterns match a class FQN owns the class — declaration order is meaningful. Migration: replacelayers: { name: pattern }withlayers: [{ name: x, patterns: [pattern] }]. See ADR 0006.RuleInterface::getCliAliases()removed. CLI aliases are now declared via the repeatable class-level attribute#[CliAlias('alias', 'optionName')]. Custom rules in extension code must drop the method and add attributes on the class.- Architecture-feature classes moved to a vertical slice under
Qualimetrix\Architecture\{Domain,Configuration,Processing,Rules}per ADR 0010. Extension authors importing from the oldQualimetrix\Core\Architecture,Qualimetrix\Configuration\Architecture,Qualimetrix\Analysis\Architecture, orQualimetrix\Rules\Architecturenamespaces must update imports.
Changed¶
- New rule
architecture.layer-violation: declare layers in YAML and enforce allowed inter-layer dependencies. Membership supportspatterns,suffix,attributes,implements,extends(combined viamatch: any | all); parameterised template layers expand against the observed class set ({var}capture);exclude:blocks hard-filter assignment; allow-listrelations:whitelists restrict permittedDependencyTypekinds; capture-binding ('app-{m}': ['domain-{m}']) constrains allows to same-instance edges for DDD bounded contexts. Incremental adoption viaarchitecture.coverage; expansion capped byarchitecture.max_expanded_layers(default 500). See ADRs 0006–0008. - New diagnostics:
architecture.empty-template(warning — template expanded to zero layers),architecture.unreachable-layer(info — layer pattern matched zero classes),architecture.potential-shadow(info — evidence-based detection of layers silently stealing classes from later, narrower layers). - New CLI command
debug:layer-assignment <fqn>: per-class introspection of layer assignment — reports the assigned layer and which other layers' patterns would also have matched. Runs full Discovery + Collection so output matchesqmx checkbyte-for-byte. qmx.yaml.exampleincludes a commented-outarchitecture:stanza demonstrating multi-criterion membership,exclude:, templates, vendor layers,allow:(plain, captured same-instance, long-form withrelations:),coverage, andmax_expanded_layers.
Fixed¶
@qmx-thresholdannotations ondesign.type-coverage,design.god-class, anddesign.data-classpreviously had no effect — the Options classes did not implementThresholdAwareOptionsInterface. The three Options now implement it and apply overrides per class.architecture.layer-violationnow respects@qmx-ignoresuppressions placed on the offending class — the dependency visitor used absolute paths while the suppression map was keyed by relative paths.architecture.layer-violationno longer false-positives mutual-allow when the two directions use disjointrelations:filters orallow_cross_instance: true.architecture.max_expanded_layersnow actually takes effect when set in YAML (previously silently camelCased and ignored). See ADR 0009.architecture.allowsource and target selectors now reject[brackets at config-load time with an actionable hint suggesting{var}capture-variable syntax.debug:layer-assignmentnow honoursmemory_limitfromqmx.yaml.- Architecture configuration warnings (e.g. mutual-allow detection) now actually reach the user logger.
- SARIF formatter
$schemaURL updated to the OASIS canonical location after the upstream repo reorganized.
0.17.0 - 2026-05-12¶
Fixed¶
health.typingno longer reports 0% for namespaces with no typeable declarations (e.g. marker interfaces used for Symfony Messenger routing). Empty type surface now yields 100% (vacuous truth) at namespace and project levels, matching the existing class-level semantic.- Disabling a health dimension via
computed_metrics.health.X.enabled: falseno longer breakshealth.overall. Bothenabled: falseandexclude_health: [X]now follow the same pipeline — the dimension is removed andhealth.overallweights are renormalized across the remaining dimensions.
Changed¶
- Excluding a health dimension when
health.overallhas been overridden with a non-canonical formula (one that does not match(health__dim ?? fallback) * weight) now throws an explicit error instead of silently dropping the formula. Custom formulas should handle disabled dimensions via??fallbacks.
0.16.0 - 2026-05-01¶
Changed¶
health.couplingnamespace formula rewritten to use efferent-only signals (ce.avg,ce_packages.avg,ce.max,ce, distance). Stable contracts namespaces (high incoming, low outgoing dependencies) are no longer unfairly penalized by bidirectional CBO. Class- and project-level formulas are unchanged.- New aggregations for the
cemetric at namespace and project levels:ce.avg,ce.max,ce.p95.
0.15.0 - 2026-04-04¶
Changed¶
- Strict configuration validation: unknown section sub-keys (
cache.typo), invalid value types (cache.enabled: "yes"), and unknown rule names (rules.complexty) now produce clear errors with "Did you mean?" suggestions - Warnings (e.g., unknown rule option keys) are now visible at default verbosity via stderr, without requiring
-v
Fixed¶
- Configuration warnings were invisible without
-vflag due toNullLoggerat default verbosity
0.14.0 - 2026-04-03¶
Changed¶
--exclude-namespaceCLI option for violation suppression by namespace (prefix or glob), merged withexclude_namespacesfromqmx.yaml
Fixed¶
- Computed metric names with underscores (e.g.,
computed.my_score) were incorrectly normalized to camelCase in YAML config
0.13.0 - 2026-04-03¶
Changed¶
--show-suppressednow lists each suppressed violation with file, line, message, and rule name (was count-only)exclude_pathsandexclude_namespacesnow support both prefix matching (src/Entity) and glob patterns (src/Metrics/*Visitor.php); simple directory/namespace names work without trailing/*--exclude-healthwith invalid dimension name now produces an error instead of silently ignoring
Fixed¶
- "No PHP files found" message shown when all files had parse errors — now shows "All N file(s) were skipped due to parse errors"
0.12.0 - 2026-04-03¶
Changed¶
- LCOM4 rule:
exclude_methodsoption to exclude specific methods from the cohesion graph (reduces false positives from interface-mandated methods likegetName,getDescription) - Partial scope warning when analysis paths don't cover all composer.json autoload entries
coupling.instability:min_afferentoption replacesskip_leaf— configurable minimum afferent coupling (Ca) threshold for skipping symbols (default: 1, skip Ca=0)code-smell.boolean-argument: parameters with common boolean prefixes (is*,has*,can*,should*,will*,did*,was*) are now allowed by default (configurable viaallowed_prefixes: [])code-smell.error-suppression:allowed_functionsoption to whitelist functions where@usage is acceptable (e.g.,fopen,unlink)- Per-rule
exclude_pathsoption for targeted violation suppression by file path patterns @qmx-ignoretags now work in regular comments (//,/* */), not just PHPDoc docblocks- JSON format (
--format=json) now outputs all violations by default (was limited to 50); use--format-opt=violations=50to restore the old behavior - Global
exclude_namespacesconfig option for suppressing violations by namespace prefix (likeexclude_pathsbut for namespaces) - Computed metric formulas referencing non-existent metrics now produce a clear error instead of silently failing
- Warnings (partial scope, unknown rules, missing composer.json) now go to stderr to avoid corrupting machine-readable output
- Exit codes: config/input errors now return exit code 3 (was 1, overlapping with "warnings found"). Scheme: 0=clean, 1=warnings, 2=errors, 3=config error
Fixed¶
graph:exportcommand crash due to-dshortcut conflict with global--working-dir
Removed¶
--analyzeoption — was misleading (analyzed all files regardless, only filtered violations like--report). Use--reportinsteadanalyzecommand alias — usecheckinsteadbaseline.json— replaced with properqmx.yamlconfiguration using new features
0.11.2 - 2026-04-02¶
Changed¶
- Project
qmx.yamlfor self-analysis with tuned coupling thresholds andexclude_namespacesfor Core value objects qmx.yaml.example— comprehensive annotated example with documentation links, default values, and all available options (replacesqmx.yaml.dist)parallelsection in config file for setting worker count (was CLI-only via--workers)
Fixed¶
couplingsection in config file was rejected as unknown key
0.11.1 - 2026-04-01¶
Changed¶
--memory-limitoption andmemory_limitconfig key to control PHP memory limit (e.g.,--memory-limit=1G)- Removed hidden 512M memory limit override — PHP's
memory_limitfrom php.ini is now respected by default
0.11.0 - 2026-04-01¶
Changed¶
- Cognitive Complexity violations include breakdown of top contributors:
Top: nested if +5 L12, foreach +4 L15, &&/|| +1 L22 - NPath Complexity violations include multiplicative chain:
Chain: ×6 if/else L25, ×4 match L31, ×3 switch L20
0.10.0 - 2026-03-29¶
Breaking¶
- Rule IDs
code-smell.god-classandcode-smell.data-classrenamed todesign.god-classanddesign.data-class --format=healthnow produces a text table (was HTML). Use--format=htmlfor the interactive HTML report
Changed¶
@qmx-thresholdannotations for per-class/method threshold overrides in source code- Framework CBO distinction:
cbo_appandce_frameworkmetrics separate application from framework coupling - Full dependency graph in
--analyze=git:*modes — coupling metrics now correct in partial analysis --group-by=class|namespacefor JSON output- Worst contributors per health dimension in
--format=health, configurable via--format-opt=contributors=N - Violation density metric (
violationDensity: violations per 100 LOC) in worst offenders - NPath violations include severity categories (low/moderate/high/very high/extreme)
- VO constructor exemption for
long-parameter-list— relaxed thresholds (vo-warning,vo-error) - LCOM4: stateless methods grouped together, reducing false positives on utility classes
- Duplication violations include content preview hint
- Martin Diagram view in HTML report with parent-namespace instability/abstractness/distance
- NamespaceTree: canonical namespace hierarchy replaces flat aggregator
- Warn when
@qmx-thresholdtargets rules that don't support overrides - Decomposed 13 large classes into focused components (SRP)
Fixed¶
- Health: complexity contributors always empty; recalibrated formulas for per-method aggregation
- Metrics: namespace
.max/.avg/.p95now aggregated from raw method values, not pre-aggregated class values - Reporting: aggregation suffixes stripped from metric keys in health text; uppercase metric keys fixed
- Git: absolute path mismatch in
GitScopeFilterfor--analyze=git:* - Security: hardcoded credentials no longer flag dot-notation identifiers (e.g.,
config.database.host) - Duplication: self-duplication for overlapping/adjacent ranges in same file eliminated
- Removed dead weighted average from aggregation, dead
GitFileDiscoveryclass
0.9.2 - 2026-03-26¶
Fixed¶
- CI: refactored
ConfigDataNormalizerto eliminate complexity violations (NPath 442K → 4), regenerated baseline
0.9.1 - 2026-03-26¶
Changed¶
- "Top issues by impact" redesigned: file path on the first line (clickable in terminal), rule name + message + symbol context on the second line. Shows
recommendationwhen available. Handles architectural violations ([project]) - HTML report: violations table now shows
Filecolumn, usesviolationCode(more specific thanruleName), and prefersrecommendationover technicalmessage
0.9.0 - 2026-03-26¶
Changed¶
- Analysis presets:
--preset=strict|legacy|cifor one-flag configuration. Multiple presets can be combined (--preset=strict,ci). Custom preset files supported via path (--preset=./team.yaml) ruleskey now uses deep merge across pipeline stages — partial rule overrides inqmx.yamlno longer replace entire preset rule configurations
0.8.0 - 2026-03-26¶
Changed¶
- Effort-aware prioritization: "Top issues by impact" section in summary and JSON output. Violations ranked by
classRank × severity × remediation time— answering "what should I fix first?" New--top=Noption (default 10,--top=0to disable)
0.7.1 - 2026-03-25¶
Changed¶
- CBO metric no longer counts PHP built-in classes (
Exception,DateTime,Iterator, etc.) — only project and third-party dependencies contribute to coupling scores. Dependency graph exports (graph:export) are also affected
0.7.0 - 2026-03-25¶
Changed¶
--fail-onnow defaults toerror— warnings are shown in output but don't cause non-zero exit code. Use--fail-on=warningorfail_on: warningin config for the old behaviorthresholdshorthand for rule configuration — sets both warning and error to the same value, making all violations errors at that threshold- Health score labels renamed to industry-standard terminology:
Excellent/Good/Fair/Poor/Critical(wasStrong/Good/Acceptable/Weak/Critical) - Line numbers shown only for violations with precise locations (method/class level), not for file-level violations
Fixed¶
- Technical debt breakdown now calculated from all violations, not just the truncated display list
Breaking¶
- Default
--fail-onchanged fromwarningtoerror. CI pipelines relying on exit code 1 for warnings must add--fail-on=warningexplicitly
0.6.0 - 2026-03-18¶
Fixed¶
- Baseline now correctly matches file-level violations (duplication, code smell, security rules) — previously ~150 violations passed through a freshly generated baseline
- Duplicate code block locations are now sorted deterministically, making baseline entries stable across runs
- File paths are normalized to relative (vs CWD) to prevent mismatches with absolute or
./-prefixed paths
Breaking¶
- Baseline version bumped to 5 — existing v4 baselines must be regenerated with
--generate-baseline
0.5.0 - 2026-03-18¶
Changed¶
exclude_namespacesis now a universal per-rule option available for any rule, not just coupling rules
Breaking¶
exclude_namespacesforcoupling.cboandcoupling.instabilitymoves from nestednamespace:to top-level rule configexclude_namespacesnow filters violations at all levels (class + namespace), not just namespace level
0.4.0 - 2026-03-18¶
Changed¶
- Health scores redesigned: 5-tier labels (
Excellent/Good/Fair/Poor/Critical), recalibrated formulas for complexity (avg + P95 + sqrt(max) penalties), coupling (efferent-based, P95 + sqrt-scaled max), cohesion (TCC neutral value for small classes), maintainability (MI anchor shifted to 30).--exclude-health=DIMENSIONto exclude dimensions from scoring - Computed metrics: 6 built-in
health.*scores plus user-definablecomputed.*metrics via Symfony Expression Language formulas, per-level formulas, threshold-based violations - Summary-first CLI:
--format=summaryis now the default output — health bars, worst offenders, violation summary, and contextual hints in one screen - Drill-down navigation:
--namespace=App\Serviceand--class=App\Service\UserServicefor progressive filtering with auto-enabled--detail. Namespace/class health scores shown in drill-down headers - Interactive HTML report:
--format=health— self-contained D3.js treemap, health coloring, search, metric selector, dark mode. Use--output/-oto write any format to a file - JSON output redesigned: summary-oriented with
meta,summary,healthdecomposition,worstNamespaces,worstClasses,violations(top 50 by default).--format-opt=violations=all|0|N,--format-opt=top=N - New rules:
code-smell.long-parameter-list,code-smell.unreachable-code,code-smell.identical-subexpression,design.god-class(Lanza & Marinescu),design.data-class,code-smell.constructor-overinjection,code-smell.unused-private,design.type-coverage,duplication.code-duplication(Rabin-Karp token hashing),coupling.class-rank(PageRank),security.sql-injection,security.xss,security.command-injection,security.sensitive-parameter,security.hardcoded-credentials - New output formats:
--format=metrics(raw metric values),--format=github(PR annotations) - Technical debt: remediation time estimates per violation, aggregated debt in reports,
--detailshows per-rule breakdown --fail-on=erroroption to allow warnings without failing the build--include-generatedto override automatic@generatedfile skipping--disable-rule=duplicationnow skips the memory-intensive detection phase entirely (not just violations). Same for circular dependency detection- Violation messages improved: actionable recommendations, parameter names in boolean-argument, coupling direction in CBO, CCN divergence hints, top-5 dependencies in coupling violations
bin/qmx graph:export --format=json— dependency graph as aggregated JSON adjacency listcomposer benchmark:checkregression suite — validates health scores against 15 open-source projectsllms.txtandllms-full.txt— machine-readable documentation for AI coding agents
Fixed¶
- Metric algorithm corrections: cognitive complexity nesting in closures, cyclomatic complexity for
matcharms, NPath formulas aligned with Nejmeh/PMD standards, Maintainability Index class-level aggregation, WOC formula, RFC for traits/enums, abstractness formula for interfaces - Anonymous class isolation: methods inside anonymous classes no longer attributed to enclosing class (CCN, NPath, Halstead, ParameterCount, UnreachableCode visitors)
- Suppression system (
@qmx-ignore): fully wired into pipeline,@qmx-ignore-next-linescoped to single line, file-level regex fixed, symbol-level no longer leaks to file-level - Output formatters: SARIF schema compliance (paths, locations, helpUri), Checkstyle/Text relative paths, GitLab project-level path, JSON NaN/Infinity handling
- Configuration:
--confignow functional,exclude_pathsaccepted, YAML key normalization preserves rule IDs, deep merge for CLI overrides,fromArray([])applies defaults - Security rules: XSS and command injection detect superglobals in interpolated strings
- Infrastructure: cache hit skips AST traversal, runtime state reset between runs, baseline v3 migration errors, parallel worker validation
Breaking¶
--format=htmlrenamed to--format=health;--format=metrics-jsonrenamed to--format=metrics--format=summaryis now the default (wastext). Use--format=textfor the previous behavior--format=jsonredesigned — no longer PHPMD-compatible. See documentation for new schema- JSON field
humanMessagerenamed torecommendationin violation objects - Health scores: 5-tier labels (was 4-tier), recalibrated formulas — baselines may need regeneration
- NPath values changed due to formula corrections — baselines may need regeneration
- Baseline version 3 no longer supported — regenerate with
--generate-baseline
0.3.0 - 2026-03-08¶
Changed¶
- CLI command renamed from
analyzetocheck, with aliases for backward compatibility - Canonical config file name is now
qmx.yaml exclude_pathsoption for violation suppression by file path patterns- MkDocs Material documentation website (EN/RU)
- Version derived from Composer/git tag instead of hardcoded constant
Fixed¶
- LCOM4 calculation aligned with original Hitz & Montazeri specification
- Maintainability Index accuracy: use ELOC instead of physical LOC
--workers=0semantics corrected
0.2.2 - 2026-03-05¶
Changed¶
- Rule NAME constants follow
group.rule-nameformat (kebab-case) SizeRulesplit intoMethodCountRuleandClassCountRuleCouplingRulesplit intoInstabilityRuleandCboRuleRuleMatcherutility for prefix-based rule matching- ANSI colors, grouping, and
FormatterContextfor formatters - Baseline v3 format with duplicate NAME validation
- Suppression system updated for dotted rule names and prefix matching
0.2.1 - 2026-03-05¶
Fixed¶
- TTY output written line by line to prevent macOS terminal truncation
0.2.0 - 2026-03-05¶
Changed¶
- Category filtering for rules
- Default thresholds calibrated
0.1.1 - 2026-03-04¶
Changed¶
violationCodefield inViolationfor stable baseline hashing- Improved violation messages with thresholds and actionable advice
Fixed¶
- Namespace-level violation display and
minClassCountfilter
0.1.0 - 2026-03-04¶
Initial release.
- PHP static analysis CLI tool
- Metrics: Cyclomatic Complexity, Cognitive Complexity, NPATH, Halstead, Maintainability Index
- Metrics: RFC, Instability, Abstractness, Distance from Main Sequence
- Metrics: TCC/LCC, LCOM4, WMC, LOC, DIT, NOC
- Rules with configurable thresholds
- Circular dependency detection with DOT graph export
- Output formats: Text, JSON, Checkstyle, SARIF, GitLab Code Quality
- Parallel file processing via amphp/parallel
- Git integration:
--staged,--diff - Baseline support with
@qmx-ignoresuppression tags - AST caching, progress bar, PSR-3 logging
- Git hook installation (
hook:install,hook:status) - Symfony DI with autowiring and autoconfiguration
- GitHub Actions workflow