The analyzer pool: inventory, depth and license policy
The catalog of every code-analysis tool this agent knows about, how the pool is narrowed at run time, and what is fact versus what is still work.
The machine-readable form is src/maintainability_audit/_assets/analyzer-catalog.json, regenerated by tools/build_catalog.py. This page explains it; the JSON is the authority.
What is in the catalog
760 tools. 755 from the analysis-tools.dev database at commit 66668c6, captured 2026-06-10 (the database itself is CC0), plus 5 this project installed and ran that the database does not list.
Every count on this page is read from src/maintainability_audit/_assets/analyzer-catalog.json and held to it by test_the_pool_document_states_the_catalogs_own_counts — they drifted by a tool and a licence class before that test existed, which is exactly long enough for a reader to start trusting them.
Every entry carries the source's own facts — name, license, languages, homepage — and two fields that are this project's judgment, not the source's: tier and adapter.
By license class
License strings in the wild are messy: 89 distinct spellings, from MIT License to GPL with Classpath exception / MIT License to NOASSERTION. They are classified by explicit pattern, and anything unmatched stays unverified rather than being guessed.
| Class | Count | Meaning |
|---|---|---|
permissive |
466 | MIT, BSD, Apache, ISC, Zlib, Boost, Artistic, UPL and similar |
unverified |
72 | License could not be confirmed from the source data. Not a claim that the tool is unfree — flake8 (MIT) lands here because GitHub could not map its text to an SPDX id |
strong-copyleft |
72 | GPL, AGPL, CC-BY-SA, EUPL |
proprietary |
70 | Paid, no free tier recorded |
weak-copyleft |
43 | LGPL, MPL, EPL, CDDL |
proprietary-free-tier |
32 | Commercial, with a free or open-source plan recorded in the source's plans block |
source-available |
5 | Elastic License, Business Source License, Sustainable Use License. Not open source — visible source with restricted commercial use |
A dual license takes the most permissive class on offer, because the licensee chooses: GPL v3 or Perl Artistic License 2.0 classifies as permissive.
PMD is an explicit catalog override in this tranche: its BSD-style license is verified against the upstream PMD LICENSE file, rather than inferred from the source catalog's Other value. Checkstyle is the matching weak-copyleft override: its LGPL-2.1-or-later license is verified against the upstream Checkstyle LICENSE file. SpotBugs is already classified weak-copyleft from the upstream catalog's GNU Lesser General Public License v2.1.
None of this is legal advice. Whether invoking a GPL analyzer as a separate process has any effect on the code it scans is a question for your own counsel. The classification exists so that whatever answer your organization reaches can be enforced, rather than left to whichever engineer installed what.
Eligible pool
448 tools are eligible: open-source license class, not deprecated, targets at least one language, and not security-only. Security scanning is secure-code-agent's job — those tools are cataloged and marked, never run from here.
Eligible by class: 368 permissive, 45 strong-copyleft, 35 weak-copyleft.
The three run-time selectors
Independent questions, and the pool is their intersection. Concerns asks what you want examined, depth how much work you want done, license policy what your organization is allowed to run. A user never names a tool.
Concerns
| Concern | Covers |
|---|---|
complexity |
Cyclomatic, cognitive, nesting, declaration size |
duplication |
Exact and near clones |
dead-code |
Unreachable or unreferenced |
documentation |
Docstring and comment coverage |
structure |
File size, coupling, cohesion, dependency shape |
testing |
Test presence, coverage, mutation |
style |
Naming and convention conformance |
types |
Type coverage and soundness |
metrics |
Maintainability index, Halstead, raw counts |
all |
Every concern (the default) |
The vocabulary is the scoring model's, so an answer maps onto aspects that exist and can move a score.
It cannot come from the upstream data. Those tags are languages, ecosystems and frameworks — rails, nodejs, spring — and only 5 of the 448 eligible tools carry an upstream tag that names one of this project's concerns. What a tool measures is knowable only by running it, so measures is populated exactly as fast as adapters are written; the upstream tags are kept separately as upstream_tags.
python tools/resolve_pool.py --concerns duplication,dead-code
python tools/resolve_pool.py --concerns documentation
Depth
| Depth | Tools | What it means |
|---|---|---|
baseline |
10 | Multi-language or zero-config tools, one install, seconds to run |
moderate |
18 | Baseline plus the mainstream per-language linters |
heavy |
18 | Moderate plus slower or configuration-hungry tools; none are added today |
all |
up to 448 | Every eligible tool that speaks a detected language |
Analyzers never take their configuration from the tree they audit.
Decision 9 draws the line at executing code, and
configuration is code: pylint's init-hook= runs arbitrary Python
before analysis begins, a mypy plugins = imports a module out of the
repository, and an eslint flat config is a JavaScript program. So
pylint is spawned with --rcfile, mypy with --config-file, and
eslint — which cannot run at all without the tree's config — is refused
by selection and reported as refused rather than run. The analyzer
child also gets an environment with PYTHONPATH, NODE_PATH,
NODE_OPTIONS, PYTHONSTARTUP and the LD_/DYLD_ pair removed, so
nothing outside the operator's choice can decide what it loads. This is
not a sandbox and is not claimed as one; the child is an ordinary local
process.
The two isolation flags are structurally checked everywhere and
spawned only where the binary exists — the same disclosure the two
JVM tools carry below. test_every_declared_tool_is_classified_for_tree_configuration
forces every declared tool to be isolated or to state that it reads no
tree configuration, and the live tests plant a hostile pylintrc and
mypy.ini and assert the tree's code never runs. Those live tests skip
wherever the tool is absent, including this project's CI, so on a
machine without pylint and mypy the flags are documented-and-asserted
rather than demonstrated.
A tier below all is a promise that the tool works. Nothing is placed in baseline, moderate or heavy until this project has installed it, run it, and parsed its output — with two disclosed exceptions: Checkstyle and SpotBugs are verified against recorded real output (their conditional live tests skip wherever no binary exists, including this project's CI), so their live spawn is unproven until someone supplies the binaries. PMD has run live. That is why these numbers are small and why they grow one verified tool at a time.
all is the honest name for the rest: known to exist, license recorded, invocation not yet implemented. Selecting all today runs the same tools as heavy and reports the remainder as unavailable-no-adapter. It is not a lie by omission — the report says exactly which of them ran.
License policy
| Policy | Adds | Eligible tools |
|---|---|---|
permissive |
MIT, BSD, Apache, ISC, Zlib… | 368 |
copyleft-weak |
+ LGPL, MPL, EPL | 403 |
copyleft-any |
+ GPL, AGPL | 448 |
commercial-free-tier |
+ proprietary tools with a free or OSS plan | 474 |
unverified |
+ tools whose license could not be confirmed | opt-in, never a default |
source-available and paid proprietary tools are cataloged but never selected. Adding them needs a deliberate configuration entry naming the tool, not a policy tier.
Sonar-family tools are cataloged and unadapted, not blanket-classified as commercial. The catalog classifies SonarAnalyzer.CSharp, SonarQube for IDE and SonarQube Server as weak-copyleft, while SonarQube Cloud is proprietary-free-tier; none is runnable until an adapter ships.
How the selection is made
Read from configuration (analyzers.depth, analyzers.license_policy, analyzers.concerns). A first run with no config asks the same setup questions on chat, MCP, and a CLI TTY (first run) — whether to run the pool, depth, license policy, and scan-history recording — and persists them through the same apply_answers, so both surfaces write a byte-identical maintainability-agent.json with an explicit analyzers.run. Every later run reads that file, so the pool never changes between runs of the same tree (promise P1). A non-TTY run — CI — never asks and never writes, because an audit that stopped to ask a question in a pipeline would be a broken audit, not because the CLI has a shorter questionnaire. Preview a pool without editing the file with tools/resolve_pool.py --depth / --license-policy.
The chosen depth and policy are recorded in the report, because a score computed from four tools and a score computed from forty are not the same measurement and must not be silently comparable.
Runtime prerequisites
Analyzers live in ecosystems, and the agent runs the ones it can reach. Nothing here is required — a missing runtime costs coverage, never a failed run — but each absence is reported with what would close it.
| Runtime | Needed for | Install |
|---|---|---|
| Python 3.11+ | The agent itself, and lizard, radon, ruff, vulture, complexipy, interrogate, pydocstyle, multimetric, pylint, mypy | ships with the package |
| Node.js 18+ | jscpd (multi-language clone detection) and the JS/TS toolchain. Not eslint: it is refused, see above | brew install node / your platform's package manager |
| JDK 17+ | PMD, Checkstyle, and SpotBugs | brew install openjdk; install PMD with brew install pmd or its upstream distribution; install Checkstyle with brew install checkstyle; install SpotBugs with brew install spotbugs. SpotBugs reads bytecode that already exists (target/classes, build/classes); the agent never runs the build. |
| Go toolchain | golangci-lint (no adapter yet) | brew install go |
| Rust toolchain | clippy (no adapter yet) | brew install rust |
Node tools are fetched only if you opt in. With analyzers.acquire_tools set, a missing jscpd is invoked through npx --yes, which downloads the package — a network action P1 discloses, with the fetched version recorded in the report. By default it is off: a missing Node tool is reported not-installed in coverage and appears in the environment work order with its install command, and nothing is fetched. This agent does not transmit your code in either mode.
Child tools are not network-sandboxed. Once lizard, jscpd, or any other adapter runs, this process does not police outbound connections. Install tools yourself and keep them off npx if you need an air-gapped analysis.
To avoid the fetch entirely — in an air-gapped build, or to pin a version — install the tool ahead of time and it will be used directly:
npm install -g jscpd@5
Adapter status, stated plainly
18 tools have adapters. An inventory entry without an adapter is not a shipped capability.
| Verified | Depth | Concern | Evidence |
|---|---|---|---|
| lizard | baseline | Complexity, NLOC, params, tokens | 205 functions in src/; C/C++/C#/Java/Fortran/Go/Rust in one pass |
| radon | baseline | Maintainability Index, CC, Halstead | 34 files; lowest MI 42.23 |
| ruff | baseline | ~800 lint rules | The repo's own linter |
| jscpd | baseline | Copy-paste clones | 30-line Java clone across two files |
| vulture | baseline | Dead code | 0 at ≥80% confidence |
| complexipy | baseline | Cognitive complexity | history_section = 14 |
| interrogate | baseline | Docstring coverage | installed, run |
| pydocstyle | baseline | Docstring conventions | installed, run |
| fortitude | baseline | Fortran lint, 100+ rules | version 0.9.2; 8 findings on an 11-line legacy module, including C191 unreachable code |
| multimetric | baseline | Multi-language metrics | version 2.4.4; contributed in the checked-in analyzer corpus |
| pylint | moderate | Design smells | 107 messages on src/ |
| mypy | moderate | Type diagnostics | Generic diagnostic output |
| eslint | moderate | JS/TS complexity, depth, params | refused since D39 — it cannot run without executing the tree's flat config. The adapter is kept, not deleted: it becomes usable the day the tool can be invoked without that config |
| flake8 | moderate | Style, mccabe complexity, unused code | parses its default path:row:col: CODE message lines |
| cohesion | moderate | Per-class cohesion percentage | parses its verbose class report; measurements, not gates |
| pmd | moderate | Java cognitive and cyclomatic complexity verdicts | pmd check with a pinned local ruleset; SARIF output; install with brew install pmd, verify with pmd --version |
| checkstyle | moderate | Java style and documentation verdicts — the bundled Google ruleset is a convention and Javadoc guide, not a complexity or structure source | checkstyle over explicit .java targets with the bundled google_checks.xml from a neutral working directory (the audited tree's optional suppressions file is never read); XML via the shared checkstyle parser; install with brew install checkstyle, verify with checkstyle --version |
| spotbugs | moderate | Java bytecode STYLE findings only — a bug-pattern tool, not a complexity or structure source; SECURITY detectors are omitted (secure-code-agent) | spotbugs -textui -exitcode over existing target/classes / build/classes (or analyzers.class_dirs); BugCollection XML, never the Checkstyle parser; missing bytecode is not-applicable with a build-then-rerun work-order item, never not-installed; coverage records source vs class mtimes (ADR 012). Install with brew install spotbugs, verify with spotbugs -version. Source-read and artifact-read JVM adapters compose in one report (tests/test_d15_composition.py). |
The full inventory, including everything without an adapter, is in the JSON. Additional adapters remain incremental work; the inventory alone does not make a tool runnable.
Regenerating
pip install -e ".[dev]" # PyYAML: the upstream catalog is YAML; not a runtime dep
git clone https://github.com/analysis-tools-dev/static-analysis /tmp/sa
python tools/build_catalog.py /tmp/sa
The pinned commit in tools/build_catalog.py is what the checked-in JSON was built from. Bumping it is a deliberate act: tool licenses and deprecations change, and a silent refresh would move the pool under a score without anyone deciding to.