maintainability-agent docs
Project

Contributing

Thanks for helping improve Maintainability Agent.

Project Rules

Local Verification

Run tools/install-local-gate.sh once per clone. It installs a pre-push hook that runs tools/local-gate.sh — lint and the whole suite under the coverage floor — before every push. CI runs the suite once, on Linux; the pre-push gate is how macOS is verified (Decision 18), so a push from a Mac that skips it leaves macOS unverified.

# Install dev extras (ruff + pip-audit + jsonschema + pytest-cov + PyYAML
# for tools/build_catalog.py). The shipped package does not depend on PyYAML.
# secure-code-agent, which every audit runs (D177), is installed alongside,
# not as a dependency: python3 -m pip install 'secure-code-agent>=0.12.10'.
python3 -m pip install -e ".[dev]"

# Lint, deps scan, tests with coverage gate, self-audit.
ruff check src tests
pip-audit
PYTHONPATH=src python3 -m pytest --cov=maintainability_audit --cov-fail-under=92
PYTHONPATH=src python3 -m maintainability_audit \
  --config maintainability-agent.json --fail-on-gate

Sandbox-friendly invocation (for AI agents that disable plugin autoload) drops coverage and works fine:

PYTHONDONTWRITEBYTECODE=1 PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 \
  PYTHONPATH=src python3 -m pytest -p no:cacheprovider

Single-test fast-iteration (no coverage gate):

PYTHONPATH=src python3 -m pytest tests/test_cli.py::test_version_flag

Documentation

Every document is also rendered into one readable HTML copy in docs/html/: the root documents, everything under docs/, and cards for the standalone HTML pages. The markdown stays the source of truth. After changing any document, re-render and commit both:

python3 tools/render_docs.py

tests/test_rendered_docs.py fails when the committed copy no longer matches its markdown, or when a tracked document has no page. The set of documents is read from git, so a new document is picked up without editing a list.

Pull Requests

Include:

Releasing

Publishing to PyPI is automated. Pushing a version tag builds, verifies and publishes; there are no credentials to manage because authentication uses PyPI Trusted Publishing (OIDC), which mints a short-lived token scoped to release.yml in this repository.

# 1. bump the version in ONE place
#      src/maintainability_audit/__init__.py   __version__
#    `pyproject.toml` derives it and `config.py` re-exports it (D198);
#    writing it into either again fails the suite.
python3 tools/stamp_version.py   # 2. writes the four documents that state it
#    2b. regenerate docs/example-report.html from a clean checkout at a
#        neutral path (the report prints its root), and set the README's
#        "See it" caption to the new version
# 3. add the CHANGELOG entry (## X.Y.Z), open a PR, merge it
# 4. tag the merged commit and push
git checkout main && git pull
git tag -a vX.Y.Z -m "vX.Y.Z — one-line summary"
git push origin vX.Y.Z

Step 2 is not optional and is not a formatting chore. The README's version line, the README's GitHub Action pin, the docs/release-plan.md row and the SECURITY.md support table each have to state the number, each went stale once, and each is now written from the single literal rather than typed. The tool exits non-zero if any of them stopped matching, so a document that drifts out of its reach fails rather than being silently skipped. v3.7.9 was tagged and lost its release because one of those four was missed by hand.

Step 3 is likewise enforced: the GitHub Release job reads its notes from the CHANGELOG section for that version, and the suite fails if the shipping version has no section — otherwise the job publishes a release whose entire notes are the sentence Release X.Y.Z. (D204).

The workflow refuses to publish if the tag disagrees with the version in the source, and again if the built wheel disagrees with the tag (D205). It installs that wheel and runs both the test suite and the tool's own --fail-on-gate audit against it, so what ships is what was verified.

Cutting the GitHub Release is automatic — the tag triggers a job that creates it, or refreshes its notes on a re-run. There is nothing to do by hand.

If the default thresholds changed in the release, recalibrate first — the constants are the anchor for every score the tool emits:

python3 tools/calibration/measure.py --check

A full re-measure (--with-analyzers) audits every corpus repository and takes hours on one core. --jobs N measures N at once with the same rows in the same order; each finished repository is saved to a checkpoint in the cache directory, so an interrupted run started again with the same --cache-dir picks up where it stopped.