Code Quality Analyzer
Prove the health of a codebase without a single byte leaving the machine.
Code Quality Analyzer is a privacy-first static analysis tool for eight languages — Python in depth, with bounded Go, TypeScript/JavaScript, Java, Kotlin, C#/.NET, C/C++ and Rust support — built for environments where source code cannot leave the trusted development boundary: regulated industries, air-gapped networks, client codebases under NDA, and anyone who refuses to ship their source to a SaaS dashboard to learn whether it is healthy.
The privacy contract
Most quality platforms want your code on their servers. This tool inverts that: analysis is local-only by design, and the guarantees are enforced, not promised.
- No network, provably. Analysis has no network-backed integration, and
--offlineenforces it at runtime by denying socket and name-resolution operations while analysis runs — an accidental future network call fails the command instead of silently connecting. - Anonymized reports for untrusted destinations.
--anonymizereplaces project, file, and function identities with opaque report-local tokens and strips source-derived evidence, so a report can leave the machine while the code's identity does not. - Baselines are hashes, not source. CI baseline files contain only schema metadata and SHA-256 fingerprints — no paths, messages, identifiers, or snippets.
- No shell, no Git, no execution. The analyzer never invokes Git or a shell, and package intelligence never imports, builds, or executes project code. Changed-line manifests are supplied externally.
- Honest results, enforced. Every report carries
analysis_healthand an authority verdict; incomplete analysis cannot silently pass as a green build (--strictmakes it fail).
See docs/PRIVACY.md for the exact data boundary.
What it detects
- Actionable correctness, maintainability, duplication, and package
findings with stable rule IDs, severities, remediation, and normalized
locations — including cross-file duplicate function implementations
(
PY-DUP-001) detected by exact AST structure, so renamed copies still report and docstring changes cannot hide one - Data Structures & Algorithms (DSA) patterns in Python, Go, TypeScript/JavaScript, Java, Kotlin, C#, C/C++, and Rust
- System Design principles implemented in Python, Go, TypeScript/JavaScript, Java, Kotlin, C#, C/C++, and Rust
- A compatibility architecture signal score from 1-10
- A security rule family (
*-SEC-*, 27 rules across all eight languages) for the defect classes a lexer can see honestly — shell commands andevalbuilt from runtime strings, unsafe deserialization, disabled TLS verification, non-cryptographic randomness for secrets, unbounded C buffer writes, format-string sinks, undocumented Rustunsafe— each anchored to a CWE and emitted with SARIFsecurity-severityso code scanning ranks it as a security alert. No taint tracking, no vulnerability database: those need a data-flow engine or network egress, and both are out of scope by design.
Reports render as text, versioned JSON, or SARIF 2.1.0, and gate CI through
baselines, changed-line selection, and severity thresholds — all under the
same privacy contract. Any finding can be suppressed on its line, in every
language, with a reason: // cqa: ignore=GO-SEC-001 reason="local test proxy".
Architecture Signal Score Scale
| Score | Architecture-signal breadth |
|---|---|
| 1-2 | Very few recognized DSA or design signals |
| 3-4 | A small set of recognized signals |
| 5-6 | Moderate signal breadth |
| 7-8 | Broad signal coverage across multiple files |
| 9-10 | Very broad recognized DSA and design signals |
Architecture signal scores from 2.x are not comparable to 1.x ratings. Detection got stricter
and project size no longer adds score, so most projects will score lower than
they did before. Scores under scoring policy 2.0.0 (analyzer 2.32.0+) are
likewise not comparable to policy 1.0.0: the catalog grew from 38 to 56
patterns to cover production-systems engineering, and the rating curves were
rescaled. Scoring policy 2.1.0 (analyzer 2.44.0+) grew the catalog to 62 —
distributed locking, optimistic concurrency, security hardening, scheduling,
ring buffers and randomized sampling — and raised the maturity target to 31;
curves are unchanged, so scores move by at most a few tenths and only for
projects that have the new patterns. Every JSON report carries
scoring_policy_version. See Scoring,
docs/adr/002-scoring-policy-2-production-systems-catalog.md
and docs/adr/003-scoring-policy-2-1-locking-hardening-labuladong.md.
Project Structure
code-quality-analyzer/
├── cqa_analyzer/
│ ├── __init__.py
│ ├── __main__.py # CLI entry point (argparse; no third-party dependencies)
│ ├── mcp_server.py # MCP server for coding agents (stdio, stdlib only)
│ ├── text_render.py # Deterministic plain-text panels and tables (stdlib)
│ ├── baseline.py # Hashed finding baselines and comparison
│ ├── cache.py # Bounded local parse-artifact cache
│ ├── changed_lines.py # Bounded changed-line finding selection
│ ├── config.py # Bounded configuration, path, and rule policy
│ ├── discovery.py # Safe file discovery (limits, symlink guard)
│ ├── duplication.py # Cross-file duplicate function detection
│ ├── findings.py # Language-neutral actionable finding model
│ ├── signals.py # Per-file signal extraction + pattern matching
│ ├── python_rules.py # Source-located Python correctness rules
│ ├── patterns.py # DSA & System Design pattern definitions
│ ├── go_patterns.py # Go-idiom signal definitions for the shared catalog
│ ├── ts_patterns.py # TypeScript/JavaScript signal definitions
│ ├── java_patterns.py # Java signal definitions
│ ├── kotlin_patterns.py # Kotlin signal definitions (extends Java)
│ ├── c_patterns.py # C/C++ signal definitions (include-anchored)
│ ├── production_patterns.py # Shared production-systems and GoF specs
│ ├── csharp_patterns.py # C#/.NET signal definitions
│ ├── manifests.py # Shared nested-manifest discovery and hardened XML
│ ├── package_intelligence.py # Metadata, modules, imports, cycles
│ ├── protocols.py # Source, parse, rule, provider, and reporter contracts
│ ├── registry.py # Versioned plugin and capability negotiation registry
│ ├── plugins.py # Built-in plugin assembly
│ ├── reporters.py # Registered text, JSON, and SARIF renderers
│ ├── rule_metadata.py # Stable built-in rule catalog for standard reports
│ ├── languages/ # Built-in language adapters and rule packs
│ ├── scanner.py # Language-neutral orchestration through plugins
│ ├── complexity.py # Time/space complexity analyzer
│ └── rater.py # Rating calculator (1-10)
├── tests/ # Regression tests
├── docs/ # Rule, workflow, and privacy guidance
├── .pre-commit-hooks.yaml # Published local-analysis hook contract
├── LICENSE
├── SECURITY.md
├── CHANGELOG.md
├── MANIFEST.in
├── pyproject.toml
├── README.md
└── .gitignore
Installation
Install from PyPI. Two names matter, and mixing them up is the most common first-run mistake:
| What | Name |
|---|---|
PyPI distribution (pip install …) |
cqa-analyzer |
| Installed command | code-quality-analyzer |
Python module (python -m …) |
cqa_analyzer |
code-quality-analyzer on PyPI is an unrelated project — installing it
will not give you this tool.
Requirements: Python 3.11 or newer (python3 --version). The default
install is pure Python with no native dependencies.
Optional: [deep] — duplication and complexity for Go, C/C++, and Rust
pipx install 'cqa-analyzer[deep]' # or: pip install 'cqa-analyzer[deep]'
Adds tree-sitter and the Go, C, C++ and Rust grammars (compiled wheels, no
network, no toolchain execution) and unlocks GO-DUP-001, C-DUP-001
(cross-file duplicate functions) and GO-MAINT-001, C-MAINT-001
(cyclomatic complexity over 10) — the same metrics, thresholds, and
reporting Python already has. Without the extra, the report says so
("available": false with the install hint) instead of inventing a
number. See docs/RULES.md.
For Rust the extra unlocks RS-DUP-001, RS-MAINT-001 and RS-MAINT-002
through tree-sitter-rust in the same way; Rust itself (discovery, rules,
signals, Cargo intelligence) is part of the default install since 2.43.0.
Recommended: pipx (isolated, always on PATH)
pipx install cqa-analyzer
code-quality-analyzer --version
pipx keeps the tool in its own environment and puts the command on your
PATH regardless of which Python your shell or a version manager (mise,
pyenv, asdf) resolves in a given directory. Install pipx with
brew install pipx && pipx ensurepath (macOS) or your platform's
package manager, then restart the shell.
Alternative: pip into a specific interpreter
Always install through the interpreter you will run, so the install and the invocation cannot target different Pythons:
python3 -m pip install cqa-analyzer
python3 -m cqa_analyzer --version
Inside a virtual environment, the command lives in that environment's
bin/ directory: .venv/bin/code-quality-analyzer /path/to/project.
Verify
code-quality-analyzer --version # code-quality-analyzer, version X.Y.Z
code-quality-analyzer /path/to/project
Upgrade
Upgrade the same way you installed. The installed command name does not change between versions, so nothing else needs updating.
# pipx
pipx upgrade cqa-analyzer
pipx upgrade --include-injected cqa-analyzer # if you also installed [deep]
# pip (use the same interpreter you installed into)
python3 -m pip install --upgrade cqa-analyzer
python3 -m pip install --upgrade 'cqa-analyzer[deep]' # with the optional extra
# a specific version
pipx install --force 'cqa-analyzer==3.4.1'
python3 -m pip install 'cqa-analyzer==3.4.1'
Check with code-quality-analyzer --version. If the number does not
change, you upgraded a different interpreter than the one on your PATH —
see Troubleshooting below. Nothing else needs migrating: cache entries
are keyed on the adapter and codec versions that produced them, so a
release that changes a language adapter simply recomputes its entries;
baselines are keyed on finding fingerprints and persist, so a release
that adds a rule surfaces its findings as new until you re-baseline. The
CHANGELOG lists anything that changes a score or a rule.
Troubleshooting
zsh: command not found: code-quality-analyzer — the shell cannot
find the console script. Confirm the package is installed at all with
python3 -m cqa_analyzer --version; if that works, only PATH is
missing. pip drops scripts next to the interpreter that installed them
(for example ~/.local/share/mise/installs/python/3.12.x/bin/ or
~/Library/Python/3.x/bin/ on macOS), and that directory may not be on
PATH. Either add it to your shell profile, run pipx ensurepath if you
used pipx, or use python3 -m cqa_analyzer instead of the command. If you
run the module form from inside a repository you do not trust, add -P
(python3 -P -m cqa_analyzer .): -m puts the current directory first on
sys.path, so a checked-in cqa_analyzer/ directory would run instead of
the installed analyzer. The console script and the MCP server are not
affected (the server already launches its subprocess with -P).
No module named cqa_analyzer — the package is installed in a
different Python than the one you are running. This is common with
version managers such as mise or pyenv, where python3 changes per
directory. Reinstall through the exact interpreter:
python3 -m pip install cqa-analyzer. To stop this recurring, use pipx.
No matching distribution found for cqa-analyzer — your Python is
older than 3.11. Check python3 --version and install a newer Python
(for example brew install python@3.12). Python 3.10 users can pin
cqa-analyzer==2.29.0, the last release supporting 3.10.
error: externally-managed-environment — a system or Homebrew
Python is refusing a bare pip install (PEP 668). Use pipx or a
virtual environment; do not override the protection with
--break-system-packages.
Installed but analyzing the wrong thing — PROJECT_PATH must be a
directory; pointing at a single file is rejected. Generated output such
as node_modules, .next, dist, and .venv is excluded
automatically.
Releases are published through PyPI Trusted Publishing with digital
attestations, so every artifact is provably built from this
repository's tagged source by CI — no maintainer-held upload token
exists. That attestation covers the pure-Python cqa-analyzer wheel and
sdist only. The optional [deep] extra installs third-party compiled
wheels (tree-sitter, tree-sitter-go, tree-sitter-c,
tree-sitter-cpp) that this project does not build or attest; CI pins
their version ranges and exercises them on the floor and ceiling Python
versions, but you are trusting their maintainers' release process, as
with any native dependency. The default install has no such dependency.
To work on the analyzer itself, install from a source checkout:
cd code-quality-analyzer
python3 -m venv .venv
.venv/bin/pip install -e .
Pre-commit
Pin a released tag in .pre-commit-config.yaml:
repos:
- repo: https://github.com/AmitSinghOM/code-quality-analyzer
rev: v2.29.0
hooks:
- id: code-quality-analyzer
The built-in hook runs one serial
full-repository analysis as code-quality-analyzer . --offline; it does not
pass staged filenames because the CLI accepts a directory. Findings are
advisory by default. Add args: [--fail-on, error], or baseline/new-only
arguments, when the project is ready to gate commits.
The analyzer invocation remains local-only. Pre-commit itself may need network
access once to clone the pinned release and create its environment. See
docs/PRE_COMMIT.md for trigger paths, strict and baseline
profiles, and the installation privacy boundary.
Usage
# Analyze a project
code-quality-analyzer /path/to/project
# Show which signals triggered each match
code-quality-analyzer /path/to/project -v
# Output as JSON
code-quality-analyzer /path/to/project -f json
# Output normalized findings as SARIF 2.1.0
code-quality-analyzer /path/to/project -f sarif > results.sarif
# Select findings that overlap an externally generated line manifest
code-quality-analyzer /path/to/project \
--changed-lines-manifest changed-lines.json
# Include time/space complexity analysis
code-quality-analyzer /path/to/project -c
PROJECT_PATH must be a directory. Pointing at a single file is rejected
rather than silently returning an architecture signal score of 1.
Project configuration
Place .code-quality.toml in the analyzed project root to define deterministic
source and rule policy:
[analysis]
include = ["src/**/*.py", "cmd/**/*.go"]
exclude = ["src/generated/**"]
respect_gitignore = true
# Scan directories the built-in skip list would prune (vendor, third_party,
# external, Pods, .terraform, node_modules, build outputs, ...). Bare names only.
keep_directories = ["external"]
[rules."PY-COR-001"]
enabled = true
severity = "error"
Gating a pull request you do not trust. The configuration above lives in the tree being scanned, so a PR could edit it to disable a rule. Pin the gate in the workflow instead:
# use a config file the PR cannot touch, or none at all
code-quality-analyzer . --config ci/code-quality.toml --fail-on warning
code-quality-analyzer . --no-project-config --fail-on warning
# or keep the repo's file but fail (exit 6) if its fingerprint changes
code-quality-analyzer . --expect-config-fingerprint <sha256> --fail-on warning
scan_health reports what discovery did not read: pruned_directories
and pruned_examples (skip-list directories), bytes_read, and
truncated_reasons (file_limit, byte_budget — a 512 MB total read
budget protects against trees that would otherwise exhaust memory). It also
reports what was read but could not be parsed: unparsed_files (exact
count) and unparsed_examples (up to five project-relative paths, redacted
or tokenized like every other path in the report). -v prints the same
paths under the parse-failure warning so a parse_failures verdict is
actionable.
Minified and bundled assets are left out on purpose (3.2.1): by name
(*.min.js, *-min.js, *.bundle.js, *.pack.js, *.umd.js and their
.mjs/.cjs/.ts forms) or by content (a 5 000-character line in a file
whose lines average 500 characters or more, or that has at most ten lines).
Their findings are real shapes nobody will fix in place, and a 40 KB line
makes every column number meaningless. They are accounted under
scan_health.excluded_generated / excluded_generated_examples, separately
from skips, so a vendored bundle never marks the analysis non-authoritative.
The name rule is mirrored by the MCP preview tool; the content rule needs
the bytes and is decided at read time.
Filters are project-relative, exclusion wins, and filtered files do not consume
candidate or file-limit accounting. The root .gitignore is respected by
default. Python findings can be suppressed on their reported line only with an
explicit rule ID and nonempty quoted reason, for example
# cqa: ignore=PY-COR-001 reason="legacy API". A suppression is visible
evidence, not an absence: the report carries scan_health.suppressed
(count, by_rule) and a suppressed_findings list with each suppressed
finding and its suppression_reason; SARIF emits them as results with
suppressions: [{"kind": "inSource", "justification": ...}], which GitHub
code scanning shows as suppressed; the text report prints one summary line.
Suppressed findings never reach the score, the exit code, or baselines, and
under --anonymize the reason text is redacted. JSON reports identify the
validated effective policy with a privacy-safe configuration_fingerprint.
See docs/CONFIGURATION.md for glob semantics,
validation limits, rule policy, suppressions, and compatibility boundaries.
Incremental parse cache
Caching is explicit so analysis never writes source-derived state unless a location is selected:
code-quality-analyzer . \
--cache-dir ~/.cache/code-quality-analyzer/my-project \
--offline
The bounded JSON cache stores only Python/Go parse artifacts. Rules, package providers, policy, scoring, baselines, changed-line selection, reports, and gates rerun on every analysis. Content, project-relative path, adapter, codec, plugin API, and runtime identities participate in cache keys, so changed or renamed files miss safely. Corrupt, stale, oversized, or incompatible entries also become misses; cache state never reduces analysis authority.
Cache artifacts contain source-derived data and must be protected like source.
Reports expose only whether caching was enabled, never its path, keys, digests,
hit counts, or errors. See docs/CACHING.md for invalidation,
security bounds, warm/cold determinism, and lifecycle guidance.
Privacy and offline options
Use --anonymize when a report may leave the trusted development environment:
code-quality-analyzer /path/to/project --anonymize -f json
code-quality-analyzer /path/to/project --anonymize -f sarif > results.sarif
code-quality-analyzer /path/to/project --anonymize --offline -v -c
Anonymized reports replace project, file, and function identities with opaque report-local tokens; remove package/module/dependency/script names; replace finding messages and remediation; reduce package intelligence to aggregate counts; and remove source-derived pattern signals and complexity reasoning. Rule IDs, locations, counts, scores, pattern names, complexity classes, and line numbers remain so the report is still useful.
--offline adds runtime enforcement by denying socket connection and
name-resolution operations while analysis runs. Normal analysis is already
local and has no network-backed integration; this option makes an accidental
future network call fail the command instead of silently connecting.
See docs/PRIVACY.md for the exact data boundary and
remaining disclosure considerations.
Options
| Option | Default | Purpose |
|---|---|---|
--version |
– | Print the analyzer version and exit |
-v, --verbose |
off | Include matched files and evidence; JSON omits evidence unless enabled |
-f, --output-format |
text |
text, versioned json, or SARIF 2.1.0 |
-c, --complexity |
off | Add experimental time/space complexity estimates |
--max-file-size |
2 MB | Skip files larger than this positive byte count |
--max-files |
20000 | Stop after this positive number of registered source files |
--redact-paths |
off | Report file names only, no directory structure |
--anonymize |
off | Remove project paths, metadata, and source identifiers |
--offline |
off | Deny socket operations while analysis runs |
--cache-dir |
none | Reuse bounded local parse artifacts from this directory |
--fail-under |
none | Exit non-zero when the compatibility architecture signal score is below 1–10; exits 5 when the score is not applicable |
--fail-on |
none | Exit 4 for reported findings at warning or error severity |
--baseline |
none | Compare findings with an existing hashed baseline |
--write-baseline |
none | Atomically write current finding fingerprints |
--new-findings-only |
off | Report and gate only findings absent from --baseline |
--changed-lines-manifest |
none | Report and gate findings overlapping a bounded line manifest |
--strict |
off | Exit non-zero when any requested analysis is incomplete |
--anonymize is stronger than --redact-paths and takes precedence for report
presentation. Baseline fingerprints continue to use hidden project-relative
identities, so changing either presentation option does not change CI identity.
-f was --format in 1.x. It is now --output-format so it no longer shadows
the format builtin. JSON reports include schema, analyzer, and ruleset versions
plus explicit privacy state, scoring-policy version, effective-configuration
fingerprint, and analysis authority so consumers can identify the contract,
protections, and completeness that produced a result. SARIF emits the same
baseline- and changed-line-filtered findings for standard code-scanning
consumers. Changed-line manifests are supplied externally; the analyzer never
invokes Git or a shell to derive them. JSON schema 1.11.0 records aggregate
selection metadata and whether the local parse cache was enabled; see
docs/CHANGED_LINES.md,
docs/CACHING.md, and docs/SARIF.md.
architecture_signal_score is the primary score field;
rating is a transitional equal-valued 2.x compatibility alias.
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Analysis completed and any threshold was met |
| 1 | Architecture signal score below --fail-under |
| 2 | No registered-language source candidates were discovered |
| 3 | Source candidates produced no successful analysis, or --strict found incomplete analysis |
| 4 | A reported finding met the --fail-on severity threshold |
| 5 | --fail-under was set but the architecture signal score is not applicable (no signal-capable source analyzed) |
Analysis Authority
Every JSON report includes analysis_health with source-candidate, readable,
and successfully analyzed counts; a completeness ratio; stable reason codes;
and complete/authoritative booleans. Text reports show the same
qualification before the architecture signal score.
No source candidates exit with code 2. If candidates exist but none can be
successfully parsed, analysis exits with code 3 even without --strict.
Partial non-strict analysis may exit successfully for inspection, but it is
always marked non-authoritative. See the versioned schema in
docs/report-schema-1.14.0.json and the decision
record in
docs/adr/001-analysis-authority-and-score-migration.md.
Use in CI
The quickest path is the GitHub Action, one line in a workflow:
- uses: AmitSinghOM/cqa-action@v1
It installs a pinned release, gates on warning-or-higher findings with
--strict, restricts gating to the lines a pull request changed, writes a
job summary and emits SARIF. Inputs and outputs:
AmitSinghOM/cqa-action. This
repository gates itself with it (gate-action in .github/workflows/ci.yml).
Or by hand — create a baseline once after reviewing existing findings:
code-quality-analyzer . --write-baseline .code-quality-baseline.json
Then gate only newly introduced warning-or-higher findings:
code-quality-analyzer . \
--baseline .code-quality-baseline.json \
--new-findings-only \
--changed-lines-manifest changed-lines.json \
--fail-on warning \
--strict \
-f sarif > code-quality-results.sarif
The changed-lines manifest is generated by an external CI step. The analyzer
never invokes Git, and it continues to scan the full project for authority and
strict-mode decisions. Baseline filtering runs before changed-line selection;
see docs/CHANGED_LINES.md.
The SARIF artifact is emitted before the gate exit code and contains the same
selected findings used by --fail-on. See docs/SARIF.md for
the deterministic ordering, URI, metadata, and privacy contract.
Baseline files contain only schema metadata and SHA-256 fingerprints. They do
not contain source paths, messages, identifiers, snippets, or report evidence.
Baseline writes are atomic, malformed or oversized baselines are rejected, and
fingerprints remain stable when --redact-paths changes report presentation.
See docs/BASELINES.md for workflow and review guidance.
--fail-under remains available as an architecture-signal compatibility
ratchet, but actionable finding gates are more explicit. Treat the score as
usable only when analysis_health.authoritative is true. --strict fails when
files cannot be read or parsed, discovery is truncated, package metadata is invalid, or requested complexity
analysis has a coverage gap. Incomplete analysis therefore cannot silently
pass as a green build.
See .github/workflows/ci.yml for a working example that also runs the test
suite, lint, and a dependency vulnerability scan.
For coding agents (MCP)
The analyzer is a deterministic, offline gate that coding agents (Claude
Code, Kiro, Codex, Cursor, …) can call in their edit loop. cqa-mcp serves
it over the Model Context Protocol stdio
transport with no additional dependencies:
{
"mcpServers": {
"cqa-analyzer": { "command": "cqa-mcp" }
}
}
| Tool | What it returns |
|---|---|
gate |
pass/fail, the reason, the CLI exit code, a flat list of findings with remediation, the score, and the configuration fingerprint. Accepts fail_on, fail_under, changed_lines_manifest, baseline, new_findings_only, strict, expect_config_fingerprint. |
scan |
The full JSON report (schema REPORT_SCHEMA_VERSION), verbatim. |
explain_rule |
Title, description, severity, confidence, remediation and not_when (the conditions under which the detector deliberately stays silent) for one rule ID. |
list_rules |
The built-in catalog, optionally filtered by language. |
preview |
The scan plan without a scan: every source file an adapter owns is either planned (with its language) or counted under an exclusion reason, plus pruned directories and a coverage_rate. Never reads file contents. |
rules_for_files |
For a list of project-relative paths: whether each would be analyzed (and why not), and the enabled rules with severity, confidence, remediation and not_when, grouped so files sharing a rule set list each rule once. |
diff_to_manifest |
Unified diff text (from git diff, which the agent runs) converted to the changed-lines manifest scan/gate accept, optionally written to a file. |
The server is a transport, not a second analysis path: scan and gate
run the installed CLI with --output-format json --offline and return its
report, so an agent sees byte-for-byte what CI sees under the same ruleset
version, scoring policy and configuration fingerprint. Pass the
changed_lines_manifest the Agent Skill generates to gate only the lines the
agent touched, and expect_config_fingerprint so a change to the gate's own
configuration cannot pass silently. Rationale and the longevity plan behind
this interface: docs/adr/004-positioning-and-longevity.md.
Delegate mode
preview, rules_for_files and diff_to_manifest let a host agent that
does its own reasoning borrow only the analyzer's deterministic half. The
split follows the design Alibaba published with
open-code-review: engineering
decides which files and which rules, the agent decides what is wrong.
Nothing in delegate mode calls an LLM, runs git, or reads source text.
A review loop that guarantees coverage:
diff_to_manifeston the agent'sgit diffoutput → a changed-lines manifest (write it withwrite_to).preview→ the planned file list andcoverage_rate; anything excluded comes with its reason, so nothing is silently skipped.rules_for_fileson the changed paths → rule groups. Thenot_whenclauses tell the agent when a candidate finding is a known false-positive class and should not be reported.gatewithchanged_lines_manifestfor the deterministic verdict; the agent reviews the same files for what a lexical tool cannot see.
not_when describes implemented detector behaviour (test-path downgrades,
literal blanking, thresholds, allowlists), not aspirations. When a detector
changes, its clauses change with it; tests/test_not_when_claims.py runs a
should-fire and a should-stay-silent fixture for each clause through the
real CLI, so the prose cannot drift from the code.
Paths must be project-relative. If the project lives inside a larger git
repository (a monorepo, or a ~/projects directory that is itself a repo),
git show --name-only and git diff return paths relative to the git
toplevel, and rules_for_files will honestly report them as missing
rather than guess. Run git from the project directory with --relative:
git diff --relative main...HEAD # for diff_to_manifest
git show --name-only --relative HEAD # for rules_for_files
Paths
Absolute paths are never written into reports. The project is identified by its
directory name, and file paths—including skipped-file examples—are reported
relative to the project root. --redact-paths reduces file paths to bare names
for reports shared outside your machine.
Relative paths work fine as arguments (., ../my-project)—they are resolved
internally before discovery, but the resolved absolute path is not reported.
--redact-paths is path minimization, not full anonymization: findings,
package metadata, identifiers, and architecture signals can remain sensitive.
Review docs/PRIVACY.md before sharing reports. Security
issues should follow SECURITY.md; do not attach proprietary
source to vulnerability reports.
How Detection Works
Every pattern is defined by signals, all matched case-insensitively:
| Signal type | Matched against |
|---|---|
identifiers |
An exact identifier from the AST: name, attribute, def, class, argument, import alias |
identifier_contains |
A substring of an identifier, for naming conventions like OrderRepository |
text |
A substring of the source with comments and string literals blanked out, for syntax like dp[ or @app.route |
imports |
A substring of an imported module path |
Two rules keep the noise down:
Literals and comments are not evidence. A docstring saying "we should use Dijkstra here" no longer reports a shortest-path algorithm. Comments and string literals are blanked out before any text matching, with layout preserved so positional patterns still match.
Bare words match identifiers, not raw text. pop matches items.pop() but
not population.
Each pattern declares min_signals — how many distinct signals a file must
provide before the pattern is reported. Specific patterns like heappush need
one. Generic ones need corroboration: a lone visited is not a graph
traversal, and OrderedDict on its own is not an LRU cache.
Run with -v to see exactly which signals fired, so a false positive is
reviewable rather than mysterious.
Signals are per file
Imports are collected per file. In 1.x the import set accumulated across the
whole project, so a single import heapq anywhere made every file scanned
afterwards look like it used heaps — and because results depended on directory
walk order, renaming a file could change the rating.
Scoring
rating = dsa_score × 0.4 + design_score × 0.5 + maturity_score × 0.1
Breadth discount. A pattern found in one file counts for 60% of its weight; two files 80%; four or more, full weight. One incidental use is weaker evidence than a habit.
Size is not quality. The 1.x "complexity bonus" grew with file count and
line count, so padding a codebase raised its rating. Size now only gates how
much of the maturity_score component is reachable, saturating at 2000 lines.
Past that, more code adds nothing.
Coverage gaps lower the score. If more than 10% of discovered files could not be read or parsed, the rating is scaled down and reported as a lower bound with an explicit warning.
Scoring policy 2.0.0. The DSA and design curves map total matched pattern
weight to a 1–10 score. When the catalog grew to 56 patterns, the curve
ceilings were scaled ×1.2 (DSA) and ×1.5 (design) and the maturity breadth
target rose from 20 to 28 distinct patterns — partial rather than
proportional scaling, because the added production-systems patterns are rarer
than the originals. Re-derive any --fail-under threshold once after
upgrading.
Scoring policy 2.1.0. Six IDs were added (62 patterns) for coordination
and safety mechanisms the 2.0.0 catalog could not see — leases / SKIP LOCKED / fencing tokens, expected-version writes, HMAC signing and
SSRF-egress control, schedulers — plus ring buffers and weighted / reservoir
sampling, and eight DSA IDs gained labuladong's vocabulary (monotonic queues,
difference arrays, two-heap medians, ordered maps, Floyd-Warshall, bipartite
and cycle checks, sweep line, Rabin-Karp). The maturity target moved 28 → 31;
curves did not move. On the 24-project corpus 17 scores changed, all within
−0.1 … +0.5 (median +0.1); every increase traces to a verified pattern.
Scan Safety
File reads go through cqa_analyzer/discovery.py, which refuses to:
- read a path that resolves outside the project root, so a symlink pointing at
~/.aws/credentialsis skipped rather than parsed - read a non-regular file such as a FIFO, which would otherwise block forever
- read a file above
--max-file-size - walk more than
--max-filespaths
Skips are counted by reason and reported. Nothing is silently dropped, so a clean project is distinguishable from a project that failed to parse.
Excluded directories: .git, __pycache__, .venv, venv, env,
node_modules, dist, build, site-packages, and the usual tool caches.
Actionable Findings
Actionable Python findings are separate from descriptive DSA and architecture
signals. Every finding includes a stable rule ID, category, severity,
confidence, message, remediation, and a one-based project-relative source
location. JSON reports include both findings and an aggregate
finding_summary; terminal reports show an Actionable Findings table.
The built-in Python rules detect mutable function defaults, broad exception
handlers, silently swallowed exceptions, directly unreachable statements,
functions above measured cyclomatic or cognitive complexity limits, oversized
functions, excessive parameter lists, boolean mode proliferation, known
blocking calls in async functions, locally unmanaged file/temporary
resources, and cross-file duplicate function implementations. Duplication
(PY-DUP-001) compares docstring-stripped bodies, parameter lists, and
return annotations by exact AST structure: renamed or re-decorated copies
still report, trivial functions never do, and only the outermost of nested
duplicates reports. Rule enablement and severity can be configured, and valid
same-line suppressions
require an explicit rule ID and quoted reason. See
docs/RULES.md for rule behavior and remediation examples and
docs/CONFIGURATION.md for policy details.
Files are parsed once for signal extraction, actionable Python rules, package intelligence, and optional complexity analysis. Malformed files emit no semantic findings and remain visible through analysis health.
Package Intelligence
Package intelligence is passive: it does not import, build, or execute project code. Every normal scan now reports:
- Parsed
pyproject.tomlname, Python requirement, build backend, dependencies, optional dependency groups, and console scripts - Detected
srcor flat package layout and source roots - Statically configured setuptools PEP 420 namespace modules from safe, explicit discovery roots and prefixes
- First-party modules and their local import graph
- Circular import groups
- Console scripts that target missing local modules
- Literal
__all__exports with missing bindings or duplicate names - Missing literal static setuptools package-data source targets
Invalid TOML produces PY-PKG-003 and makes --strict fail. Circular imports
produce PY-PKG-001; invalid console-script module targets produce
PY-PKG-002; missing and duplicate literal public exports produce
PY-PKG-004 and PY-PKG-005. Dynamic or ambiguous __all__ declarations are
conservatively skipped. Configured PEP 420 discovery accepts only setuptools
metadata with namespace discovery enabled, safe project-relative where roots,
and nonempty exact or prefix.* includes. Custom backends, remapping, excludes,
complex globs, and ambiguous roots retain conventional discovery without
executing build hooks. Literal package-data validation is limited to exact
package keys and safe, non-glob paths when setuptools is the only declared build
requirement and no custom setup file, backend path, command class, or package
remapping is present. It checks source-tree regular-file existence with metadata
operations only; wildcard declarations, generated files, and built-artifact
contents remain out of scope. Test-only directories without a package
initializer are excluded from unconfigured flat-layout package modules. See
docs/RULES.md.
TOML parsing uses the standard-library tomllib parser; the analyzer
requires Python 3.11 or newer.
Complexity Analysis
Complexity output is an experimental static estimate, not an authoritative Big-O guarantee. Use its assumptions, reasoning, and confidence to prioritize manual review; do not use inferred Big-O alone as a CI quality gate.
Use the -c flag to include time/space complexity analysis:
code-quality-analyzer /path/to/project -c
code-quality-analyzer /path/to/project -v -c # verbose adds reasoning
Handled correctly as of 2.0:
async forcounts as a loop. Async-heavy code used to reportO(1).- Early exit is scoped to its own loop. A
breakin an inner loop no longer marks the outer loop as having an early exit. Areturnstill does, from any depth. - Nested definitions are separate units. An inner function's loops no longer inflate the enclosing function's depth, and its self-calls no longer count as the parent's recursion. Each nested function is analyzed on its own.
- Recursion inside a loop is classified as
fan_outrather than linear. The branching factor is data-dependent, so it reportsO(n)over visited nodes with reduced confidence instead of claiming certainty.
Confidence Score
The confidence score indicates how reliable the complexity estimate is.
Starting point: 80%
| Condition | Deduction | Reason |
|---|---|---|
| Recursion without memoization | -20% | Hard to tell O(n) from O(2^n) without runtime analysis |
| Recursive call inside a loop | -20% | Branching factor depends on the data |
| Deep nesting (>2 loops) | -10% | Complex control flow makes static analysis less reliable |
| No type hints on parameters | -10% | Can't infer if input is a collection or its size relationship |
Minimum: 30%
Interpreting confidence:
- 80%+ → High confidence, estimate is likely accurate
- 60-80% → Medium confidence, estimate is reasonable but verify manually
- 30-60% → Low confidence, treat as rough estimate only
What It Detects
DSA Patterns (31 patterns)
Data Structures:
- Hash maps (Counter, defaultdict)
- Sets and set algebra
- Trees (binary, general)
- Linked lists
- Queues and stacks (deque)
- Heaps and priority queues
- Trie/prefix tree
- Segment tree
- Fenwick/Binary Indexed Tree
- Bloom filter
Algorithms:
- Sorting
- Binary search
- Graph traversal (BFS/DFS)
- Dynamic programming/memoization
- Union-Find/Disjoint Set
- Topological sort
- Shortest path (Dijkstra/Bellman-Ford/A*)
- Minimum spanning tree (Kruskal/Prim)
- Backtracking
Techniques:
- Sliding window
- Two pointers
- Monotonic stack
- Interval operations
- Manual LRU cache
- LFU cache
- Bit manipulation and bitmasks
- Prefix sums
- String matching (KMP, Rabin-Karp, Z, Aho-Corasick)
- Consistent hashing
- Ring buffers (circular buffers)
- Randomized sampling (weighted choice, reservoir, Fisher-Yates)
System Design Patterns (31 patterns)
- API design (FastAPI, Flask, Django, Starlette)
- Database access (ORMs and raw drivers)
- Caching layers
- Message queues
- Factory, Singleton, Repository, Strategy, Observer, Adapter, Decorator, Builder patterns
- Dependency injection
- Error handling
- Logging
- Authentication/Authorization
- Testing
- Microservices/service clients
- Configuration management
- Resilience: retries with backoff, circuit breakers, bulkheads
- Rate limiting and throttling
- Idempotency and duplicate suppression
- Event sourcing and CQRS
- Dead-letter queues, redrive, and transactional outbox
- Observability: tracing, metrics, health checks
- Concurrency and parallelism primitives
- Pagination
- Distributed locking (leases, fencing tokens,
SKIP LOCKED, advisory locks) - Optimistic concurrency (expected-version writes, version conflicts)
- Security hardening (SSRF/egress control, HMAC signing, constant-time compares)
- Scheduling (cron, periodic and background jobs)
Production-systems patterns are recognized primarily through naming
conventions (CircuitBreaker, RetryPolicy, DeadLetterQueue,
EventStore, HealthCheck, …) and well-known library imports (tenacity,
resilience4j, Polly, OpenTelemetry, Micrometer, …) in every supported
language. Tokens that are common words in other contexts (projection,
cursor, subscribe) require corroboration before a pattern is reported.
Python receives actionable rules, package intelligence, architecture signals,
and experimental complexity analysis. The Go pilot discovers .go files,
preserves import aliases, emits GO-COR-001 for discarded errors from a
narrow set of imported standard-library calls, GO-COR-002 for SQL built
from runtime values, GO-COR-003 for unchecked type assertions and
GO-COR-004 for defer inside a loop, extracts Go-idiom DSA and
design signals from blanked source (container/heap, sort.Search,
corroborated BFS/DFS, net/http/gRPC API design, database/sql/GORM,
message queues, sync.Once singletons, and more), and passively aggregates
multi-file packages plus local module import edges from go.mod. It never
invokes Go tooling. Go patterns reuse the shared scoring catalog IDs, so
mixed projects aggregate signals across both languages under one score.
Python and Go findings share the same report, baseline,
privacy, offline, and CI-gate contracts. JSON project_analyses entries expose
provider results normally and health-only projections under --anonymize.
See docs/RULES.md.
The TypeScript/JavaScript pilot covers .ts, .tsx, .js, .jsx,
.mjs, and .cjs with the same bounded, no-toolchain discipline: it
blanks comments, strings, and template literals (interpolations included,
so literals are never evidence), extracts bounded identifiers and import
specifiers by regex, emits TS-COR-001 for empty catch blocks,
TS-COR-002 for SQL built from runtime values, TS-COR-003 for *Sync
I/O inside async functions, TS-COR-004 for @ts-ignore without a
reason and TS-COR-005 for non-null-assertion density, and passively reads the root package.json to flag imported-but-undeclared
dependencies (TS-PKG-001) and invalid manifests (TS-PKG-002).
Workspace (monorepo) manifests skip drift analysis, node builtins and
path aliases are never flagged, and generated output directories
(.next, dist, build, coverage, and friends) are excluded from
discovery. It never invokes node, tsc, or a package manager. TS/JS
architecture signals match ecosystem idioms (new Map/new Set,
memoization, express/fastify/NestJS/tRPC API design, Prisma/TypeORM
data access, redis/react-query caching, kafkajs/bullmq queues,
jsonwebtoken/next-auth authentication, vitest/jest/playwright testing)
through the same shared scoring catalog, so full-stack projects
aggregate one score across all three languages.
The Java pilot (.java) blanks comments, strings, char literals, and
""" text blocks; extracts imports and bounded identifiers
(declarations, annotations, generic type uses, new targets, calls);
emits JAVA-COR-001 for empty catch blocks, JAVA-COR-002 for SQL built
from runtime values and JAVA-COR-003 for broad catch (Exception | Throwable) handlers (rethrowing handlers are notes); and passively discovers
pom.xml and build.gradle(.kts) modules with the same nested-manifest
machinery, reporting invalid manifests (JAVA-PKG-002) and — for a
curated set of almost-always-direct libraries only, because Maven and
Gradle make transitive classes importable — undeclared dependencies
(JAVA-PKG-001). Spring/JAX-RS, JPA/Hibernate/JDBC, Kafka/JMS,
@Autowired/Guice, SLF4J/Log4j, JUnit/Mockito, and the java.util
collections feed the shared catalog.
The C#/.NET pilot (.cs) blanks comments and every string form —
regular, verbatim @"", interpolated $"" with nested holes, raw
""", and char literals; extracts using directives (static, alias,
global), declared namespaces, and bounded identifiers; emits
CS-COR-001 for empty catch blocks (including when-filtered),
CS-COR-002 for SQL built from runtime values, CS-COR-003 for broad or
bare catch and CS-COR-004 for .Result/.Wait() inside async
bodies; and reads .csproj PackageReferences (CS-PKG-002 on invalid files),
flagging using namespaces with no package matching by prefix in
either direction (CS-PKG-001) while skipping System.*,
shared-framework Microsoft.*, and the project's own namespaces.
ASP.NET Core, EF Core/Dapper, MassTransit/Kafka, IServiceCollection
DI, Serilog/ILogger, xUnit/NUnit/Moq, and the BCL collections feed the
shared catalog.
The Kotlin pilot (.kt, .kts) shares the JVM ecosystem with Java, so
it reuses the Maven/Gradle module intelligence (KT-PKG-001/KT-PKG-002)
and extends the Java signal catalog with Kotlin idioms —
kotlinx.coroutines, Ktor/http4k, Exposed/Room/Ktorm, Koin/Hilt,
kotest/MockK, and the mapOf/mutableListOf collection builders. Its
lexer handles nested block comments, $name/${expr} string templates
(lexed as code holes, blanked), raw """ strings whose terminator is the
last quote of a run, and semicolon-free imports with as aliases.
KT-COR-001 reports empty catch blocks, KT-COR-002 SQL built from
$ templates or concatenation, KT-COR-003 broad catches, KT-COR-004
runBlocking/Thread.sleep inside suspend fun, and KT-COR-005 !!
density.
None of the JVM or .NET pilots execute javac, kotlinc, Maven, Gradle,
dotnet, or MSBuild, and all parse build XML fail-closed: documents
declaring a DOCTYPE or
entities are rejected before parsing. Build output (target, obj,
.gradle, TestResults) is excluded from discovery.
C and C++ (bounded pilot)
The C-family pilot (.c, .cc, .cpp, .cxx, .h, .hh, .hpp,
.hxx) is deliberately narrower than the others, and says so:
- The preprocessor is not modelled. Every
#directive line (including\-continued lines) is blanked from the code text, so macro bodies are never evidence and macro-generated syntax is never matched. Conditional compilation is not evaluated — code under#if 0remains visible, which can only over-report signals, never hide a finding.#includepaths are captured as the file's imports and are the strongest evidence in C-family code. - CMake only.
CMakeLists.txtis the sole manifest understood.C-PKG-001(medium confidence) reports a well-known third-party header (Boost, GoogleTest, fmt, spdlog, OpenSSL, gRPC, …) whose CMake tokens appear nowhere in the governing manifest chain —find_package, imported targets,FetchContent, andpkg_check_modulesall count. Conan, vcpkg, Bazel, Meson, and Makefiles are not read. - Four lexical rules.
C-COR-001reports empty C++ catch blocks,C-COR-002SQL built with+/snprintf/std::format,C-COR-003catch (...), andC-COR-004file-scopeusing namespacein headers. Nothing here claims to understand types, ownership, or lifetimes; no compiler is executed.
The lexer honours \-newline continuation inside // comments,
encoding prefixes and C++11 raw strings (R"delim(...)delim"), and
treats a ' between two hexadecimal digits as a C++14 digit separator
rather than a character literal. Signals are anchored on STL headers
and identifiers (std::unordered_map, priority_queue, std::mutex,
lock_guard, pthread_create, co_await) and on library headers
(spdlog/glog, gtest/Catch2/cmocka, gRPC/Crow/Drogon/Pistache,
sqlite3/libpq/pqxx/RocksDB, librdkafka/ZeroMQ/NATS, OpenSSL/libsodium,
yaml-cpp/toml++/cxxopts, OpenTelemetry/prometheus-cpp, libuv/libevent/
Asio/TBB/liburing). Calibrated on drogon and hiredis.
Rust
The Rust adapter (.rs) blanks comments (nested /* */ included), "…",
raw r#"…"# and byte b"…" strings and char literals while keeping
lifetimes ('a, 'static) as code; r#ident raw identifiers resolve to
their keyword name. use/extern crate roots and paths are the imports
(read from blanked text, so an extern crate inside a string literal is
never one), and bounded identifiers cover declarations, let bindings,
calls, method calls, type uses and attributes. It never invokes cargo
or rustc.
- Rules.
RS-COR-001.unwrap()density outside test code (#[cfg(test)]blocks,#[test]/#[tokio::test]/#[rstest]functions,tests/,benches/,examples/excluded;.expect("why")is documented intent and not counted);RS-COR-002SQL assembled withformat!/write!or+;RS-COR-003thread::sleep,std::fs,block_onor a blocking connect inside anasync fn(closures are excluded becausespawn_blocking(|| …)is the fix, and a file that importstokio::fs/async_std::fskeeps its unqualifiedfs::calls);RS-COR-004crate-wide#![allow(dead_code | unused | warnings | clippy::all)]without areason = "…"— Stack Overflow's 4th most-voted Rust question is how to do exactly this. - Cargo. The nearest
Cargo.tomlchain governs: every dependency table,[workspace.dependencies],[target.*]tables andpackage =renames count, names compare with-/_removed (md-5providesmd5), declared modules and the crate's own name are never drift.RS-PKG-001reports a crate root no manifest declares;RS-PKG-002an unreadable manifest (stdlibtomllib, no drift claims under it). - Signals. The full shared catalog, anchored on std collections
(
BinaryHeap,VecDeque,BTreeMap,HashMap) and the crate ecosystem (tokio/async-std/rayon, serde, sqlx/diesel/sea-orm, tracing/log, axum/actix/tonic, anyhow/thiserror, proptest/mockall/criterion …). Import anchors match whole::segments, not substrings — calibration on ripgrep showedhypermatching a localhyperlinkmodule — and universal Rust syntax (dyn,impl From<,new,default) is not evidence for any pattern. [deep].RS-DUP-001,RS-MAINT-001,RS-MAINT-002viatree-sitter-rust; without the extra they reportavailable: false.
Calibrated on redis-rs (8.7), axum (8.6) and ripgrep (6.8); the
recalibration that produced those numbers removed 16 phantom patterns
from ripgrep and is recorded in docs/CALIBRATION.md.
The architecture signal score covers Python, Go, TypeScript/JavaScript,
Java, Kotlin, C#, C/C++, and Rust signals. A project where
no signal-capable source was successfully analyzed reports the score as
not applicable — null in JSON with an explicit
architecture_signal_scope field — rather than a misleading floor value,
and --fail-under exits with code 5 instead of silently passing or failing.
Is the score fair across languages? docs/CALIBRATION.md
scans the same three domains — a Redis client, a web framework and a
command-line tool — in all eight languages and root-causes every gap; comparably sized projects score
within about a point of each other regardless of language, and the
remaining spread tracks project scope. The corpus is reproducible with
scripts/calibration_corpus.py. docs/ROADMAP.md
tracks what comes next, including the decision-gated tree-sitter path to
duplication and complexity metrics for Go and C/C++.
Development
.venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest # tests
.venv/bin/python -m ruff check . # lint
.venv/bin/python -m pip_audit # dependency vulnerabilities
The test suite carries a regression test for each detection and safety bug fixed in 2.0, including symlink escape, FIFO reads, cross-file import leakage, and the complexity analyzer's scope handling.
License
Release files for cqa-analyzer 3.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cqa_analyzer-3.4.1.tar.gz | 522.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cqa_analyzer-3.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 806.6 kB
Release files / cqa_analyzer-3.4.1.tar.gz
| Download URL | cqa_analyzer-3.4.1.tar.gz |
|---|---|
| Size | 522.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
465728c1ea8c13cd4d0877daf5d50d61e3d020ab490b94bca04c0c6c39fc301a
|
|
BLAKE2b-256 checksum How to use checksums |
a790c0f126ac2eb4d2f4897d987d2b6609a2fd45d3cf77abfeaf2ea8593ef35b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency logRelease files / cqa_analyzer-3.4.1-py3-none-any.whl
| Download URL | cqa_analyzer-3.4.1-py3-none-any.whl |
|---|---|
| Size | 284.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6b396454c1c74e3da2420d2b48868f70954a06078dfb8fc51de1c92da89c05c2
|
|
BLAKE2b-256 checksum How to use checksums |
04a0232062a19e609c520be8abd955918d2a82edecfa5d48ad6f346f6e675587
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency log