[build-system]
build-backend = "uv_build"
requires = [ "uv_build>=0.5,<0.20" ]

[project]
name = "aa-discord-audit"
version = "0.1.0a8"
description = "Reconciliation audit for Alliance Auth's Discord integration."
readme = "README.md"
# PEP 639: ``license`` is a SPDX expression and ``license-files`` lists
# the files to ship in the dist's ``licenses/`` metadata directory.
# The ``License :: OSI Approved :: …`` classifier was deprecated by PEP
# 639 and is intentionally absent.
license = "MIT"
license-files = [ "LICENSE" ]
authors = [ { name = "Boris Talovikov", email = "boris.t.66@gmail.com" } ]
maintainers = [ { name = "Boris Talovikov", email = "boris.t.66@gmail.com" } ]
requires-python = ">=3.10,<3.14"
keywords = [ "allianceauth", "discord", "audit", "reconciliation" ]
classifiers = [
    "Development Status :: 3 - Alpha",
    "Environment :: Web Environment",
    "Framework :: Django",
    "Framework :: Django :: 4.2",
    "Framework :: Django :: 5.2",
    "Intended Audience :: Developers",
    "Intended Audience :: System Administrators",
    "Programming Language :: Python",
    "Programming Language :: Python :: 3 :: Only",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "Topic :: Communications :: Chat",
    "Topic :: System :: Systems Administration",
]
# No `dynamic = […]` field. `version` is static here (single source of truth);
# runtime access goes through ``importlib.metadata.version`` in
# ``aa_discord_audit/__init__.py``. This avoids depending on any
# build-backend-specific dynamic-resolution behaviour.

# Test runner: ``python -m django test`` (via ``nox -s tests``). pytest
# is intentionally NOT configured — the Django runner integrates with
# ``tests/settings.py`` and the ``tests/_fakeredis.py`` monkey-patch
# directly. ``pytest-django`` would duplicate that wiring and split
# the toolchain. Run the suite with ``make tests`` or ``uv run nox -s tests``.

# Discord audit reuses the discord_client + DiscordUser models that ship
# inside Alliance Auth core. AA 4.x (Django 4.2) was the original
# development target; AA 5.x (Django 5.2) is supported on the same
# private-API surface (see ``noxfile.py:tests_aa5*`` for the per-major
# test matrices). The ``>=4,<6`` envelope keeps both lines installable
# from the published wheel; operators select via their environment's
# AA major.
dependencies = [
    "allianceauth>=4,<6",
    # ``typing_extensions`` provides backports the code relies on
    # under Python 3.10/3.11 (``assert_never`` on 3.10; transitional
    # access path while the typing surface settles). Pinned as a
    # direct dependency rather than transitively so deptry can audit
    # the contract; the actual import is gated through
    # ``sys.version_info`` where a 3.11+-only stdlib alternative
    # exists.
    "typing_extensions>=4.10",
]

urls.Source = "https://gitlab.com/eveo7/aa-discord-audit"
urls.Tracker = "https://gitlab.com/eveo7/aa-discord-audit/-/issues"

# Optional extras — installed via ``pip install aa-discord-audit[metrics]``
# (or pulled in transitively when another AA module's chain already
# brings ``django-prometheus`` in). ``aa_discord_audit/_metrics.py``
# uses a cooperate-don't-depend pattern: when the extra is absent
# every metric reference resolves to a near-zero-cost no-op stub.
[project.optional-dependencies]
metrics = [
    # ``prometheus-client`` is a transitive dependency of
    # ``django-prometheus``; pinning only the parent keeps the extra
    # surface minimal and avoids drift if upstream bumps the inner
    # constraint.
    "django-prometheus>=2.3,<3",
]

[dependency-groups]
dev = [
    "bandit[toml]>=1.9",
    "coverage>=7.6",
    # Self-reference to the ``[metrics]`` optional extra: pulls
    # ``django-prometheus`` (and transitively ``prometheus-client``)
    # into the dev environment so ``basedpyright`` resolves the
    # symbols imported behind the ``_detect_django_prometheus()`` gate.
    # Routing the dev install through the extra keeps the version pin
    # in a single place (``[project.optional-dependencies].metrics``).
    # Production installs without the ``[metrics]`` extra remain free
    # of the dependency; the cooperate-don't-depend stub in
    # ``aa_discord_audit/_metrics.py`` covers that path at runtime.
    "aa-discord-audit[metrics]",
    "django-stubs>=5.2.9",
    "docformatter>=1.7",
    "fakeredis>=2.33",
    # cosmic-ray (mutation testing). Chosen over mutmut 2.x because the
    # latter's Pony-ORM result cache is incompatible with Python 3.13's
    # tightened iterator protocol (no upstream fix), and mutmut 3.x is
    # pytest-only which would require pulling pytest-django just for
    # one tool. cosmic-ray uses sqlite for its work-queue, supports any
    # test runner via subprocess exit code, and is actively maintained.
    "cosmic-ray>=8.3",
    # ``deptry`` walks the AST and compares actual imports to
    # ``[project.dependencies]`` declarations. Resolved through the
    # project venv (was ``uvx`` before — that path resolved deptry
    # on every commit, paying a multi-second hit on a cold cache).
    "deptry>=0.20",
    # Forward/backward migration testing harness. Catches data
    # migrations missing ``reverse_code`` and schema migrations that
    # do not roll back cleanly — failure modes the static
    # ``migrations_concurrency_check`` grep cannot see.
    "django-test-migrations>=1.4",
    "mypy>=1.19",
    "nox>=2026.2.9",
    "parameterized>=0.9",
    # ``tomllib`` joined stdlib in Python 3.11. The ``_nox.mutation``
    # session module reads ``cosmic-ray.toml`` and is imported as a
    # side effect from ``noxfile.py`` on every nox invocation, so
    # matrix cells pinned to 3.10 need the ``tomli`` back-port (same
    # API). Marker keeps 3.11+ environments lean.
    "tomli>=2.0; python_version < '3.11'",
    # ``tblib`` lets Django's parallel test runner ship tracebacks
    # across subprocess boundaries. Without it, a worker SystemExit
    # surfaces as the cryptic "cannot pickle 'traceback' object"
    # error and the underlying failure is hidden. Required for the
    # 3.10 matrix lane (3.13 carries an equivalent fix natively).
    "tblib>=3.0",
    "pip-audit>=2.10",
    "basedpyright>=1.31",
    "pre-commit>=4.5.1",
    "requests>=2.32",
    "ruff>=0.15",
    # ``testcontainers[mysql]`` powers the local ``tests_mariadb``
    # quickstart: when ``AA_DISCORD_AUDIT_TEST_DB=mariadb`` is set
    # without an explicit ``AA_DISCORD_AUDIT_TEST_DB_HOST``,
    # ``tests/_mariadb_container.py`` lazy-starts a MariaDB container
    # via docker and mutates the env so Django's test runner workers
    # reuse the same container. CI's ``.gitlab-ci.yml::test_mariadb``
    # sets ``HOST=mariadb`` explicitly, so this branch is bypassed —
    # CI continues to use the declarative ``services:`` container and
    # never pulls testcontainers at runtime. ``MySqlContainer`` is the
    # right class for MariaDB too: testcontainers 4.x exposes no
    # ``mariadb`` extra, but MariaDB is wire-protocol-compatible with
    # MySQL and the container class accepts any image, so passing
    # ``image="mariadb:10.11"`` brings up MariaDB through the same
    # plumbing.
    "testcontainers[mysql]>=4",
    "vulture>=2.16",
    "xenon>=0.9",
]
# AA-major selector groups. Mutually exclusive (see ``[tool.uv]``
# ``conflicts`` below) — exactly one is active at a time. ``aa4`` is
# the default track (the project's primary development target);
# ``aa5`` is opted into by the ``tests_aa5*`` nox sessions and the
# AA-5 CI matrix jobs. Both ranges are open-upper within their major,
# so ``uv lock --upgrade-package allianceauth`` picks up AA point
# releases on demand.
aa4 = [ "allianceauth>=4,<5" ]
aa5 = [ "allianceauth>=5,<6" ]

[tool.uv]
# ``uv sync`` with no flags picks up dev + aa4 → AA 4.x track. The
# aa5 track is opted in via ``--group aa5`` (which deactivates aa4
# through the conflict declaration below). These groups are
# uv-internal — none of them appear in the published wheel metadata,
# downstream pip still sees the public ``[project.dependencies]``
# contract.
default-groups = [ "dev", "aa4" ]
# Mark aa4 / aa5 as alternatives so uv resolves both branches into
# the same lockfile and ``--all-groups`` cannot activate them
# simultaneously (which would be a contradiction).
conflicts = [
    [
        { group = "aa4" },
        { group = "aa5" },
    ],
]

[tool.uv.build-backend]
# Flat layout (package at repo root, not under src/). The project name
# `aa-discord-audit` already maps cleanly to module `aa_discord_audit`,
# so module-name is left to the default name-normalization rule.
module-root = ""

# Keep dev-only artefacts out of the published distributions:
#   * ``AGENTS.md`` — AI-readable orchestration docs that live next to
#     the code for navigation help, but have no role in operator
#     installs and would only leak internal tooling structure.
#   * ``.omc/`` — oh-my-claudecode session/state directory; gitignored
#     at the repo root, but uv-build does not honour ``.gitignore`` and
#     would otherwise sweep up any ``.omc`` that happened to exist
#     inside a subpackage on the build host.
# Patterns are unanchored, matching anywhere in the tree.
# ``CHANGELOG.md`` lives at the repo root and is not auto-shipped by
# uv-build the way ``README.md`` is (the latter is pulled in via the
# ``[project].readme`` field). ``source-include`` puts it in the
# sdist so operators downloading from PyPI see the release history.
source-include = [ "CHANGELOG.md" ]
source-exclude = [ "AGENTS.md", ".omc", ".omc/**" ]
# ``django.pot`` is the translator-template artefact kept under
# version control for translation-platform workflows. Operators
# only need the compiled ``.mo`` catalogues at runtime, so the
# template itself is excluded from the operator-facing wheel.
wheel-exclude = [ "AGENTS.md", ".omc", ".omc/**", "**/*.pot" ]

[tool.ruff]
line-length = 79
target-version = "py310"
extend-exclude = [ ".tox", ".venv", "htmlcov", "dist", "build" ]

[tool.ruff.lint]
select = [ "E", "F", "I", "B", "UP", "N", "SIM", "C4", "PIE", "RUF", "D", "DJ", "DTZ", "G", "LOG", "RET", "TID", "FURB", "A", "TRY", "ARG", "PERF", "BLE", "TC", "SLF", "PTH", "ICN", "PGH", "PLE", "PLW", "PLC", "PLR" ]
# D213 (multi-line summary on second line) is the project's chosen
# docstring shape. It is mutually exclusive with D212 (summary on the
# first line), which the pep257 convention enables — so we explicitly
# extend-select D213 and ignore D212.
extend-select = [ "D213" ]
ignore = [
    "B008",   # function-call-in-default-argument — common in Django (e.g. timezone.now)
    "D205",   # docformatter and D205 disagree on blank line after summary
    "D212",   # we use D213 (summary on second line); see extend-select above
    "D401",   # imperative mood is subjective for descriptive helpers
    "TRY003",  # long messages in `raise ValueError(...)` are idiomatic in CLI / Django code; a custom exception class per validation site is over-engineering
    "PLC0415", # function-local imports are intentional: AA-core has heavy import-time side effects, so we lazy-import inside the call sites that actually need it (apps.ready()-gated, signal handlers, management commands)
    "TID252",  # project convention: every intra-package import is relative (``from .x``, ``from ...x``); module identity flows through ``permissions.APP_LABEL``, not import paths
]

[tool.ruff.lint.pydocstyle]
convention = "pep257"

# D100-D107 are missing-docstring rules; the per-file blocks below opt out
# for code where docstrings would be boilerplate (tests, migrations, Django
# nested Meta classes).
[tool.ruff.lint.per-file-ignores]
# Django migrations are auto-generated; class attrs intentionally use literal
# lists/dicts (RUF012), operations are long lines (E501), and migration
# modules carry no docstring. SLF001 fires on the ``bases=(...)`` tuple
# Django emits when a model inherits a mixin from a private submodule
# (e.g. ``models._append_only.AppendOnlyModelMixin``) — the dotted path is
# what Django writes into the migration verbatim and we cannot rename the
# underscore-prefixed submodule without a parallel migration rewrite.
"aa_discord_audit/migrations/*" = [
    "E501", "RUF012", "N806", "SLF001",
    "D100", "D101", "D102", "D103", "D104", "D106", "D107",
]
# ModelAdmin subclasses don't carry a docstring (their contract — list_display,
# search_fields, etc. — IS the documentation), and overridden hooks
# (has_*_permission, save_model, get_queryset) inherit their docstring from
# ``ModelAdmin`` itself. The ARG002 carve-out is for the framework's
# ``request`` / ``obj`` parameters, which subclasses must accept even when
# they only branch on one of them.
"aa_discord_audit/admin.py" = [ "D101", "D102", "ARG002" ]
"aa_discord_audit/models/*" = [ "RUF012", "D105", "D106" ]
# Django signal handlers must accept ``sender`` and ``**kwargs`` per the
# dispatcher's contract even when they only use ``instance``.
"aa_discord_audit/signals.py" = [ "ARG001" ]
# Management commands: ``Foo = get_foo_model()`` returns a class, and
# Django convention is to bind it to a PascalCase local — N806 would
# flag every such call site. ``add_arguments`` / ``handle`` are
# framework hooks whose contract is documented on ``BaseCommand``,
# not on each subclass — D102 docstring requirements there are noise.
"aa_discord_audit/management/commands/*" = [
    "N806", "D101", "D102",
    # ``handle(*args, **options)`` inherits its signature from BaseCommand;
    # commands that ignore *args still must accept it to remain dispatchable.
    "ARG002",
]
# Empty package markers required for Django command discovery.
"aa_discord_audit/management/__init__.py" = [ "D104" ]
"aa_discord_audit/management/commands/__init__.py" = [ "D104" ]
# Test docstrings and assertions are routinely long; custom assertXxx methods
# follow unittest.TestCase camelCase convention (N802); N806: factory
# functions like get_foo_model() return classes and are conventionally bound
# to PascalCase locals.
# D1xx: a test method's name is its primary documentation.
# SLF001 stays *enabled* for tests so every private-member access carries
# an inline ``# noqa: SLF001 -- <reason>`` annotation; the rationale at the
# point of use is more valuable than a blanket carve-out.
"tests/*" = [
    "E501", "N802", "N806",
    "D100", "D101", "D102", "D103", "D104", "D106", "D107",
]

[tool.ruff.lint.isort]
known-first-party = [ "aa_discord_audit" ]

[tool.ruff.lint.flake8-self]
# Django Model._meta is documented public API despite the leading
# underscore (historical reasons predating __dunder__ as the convention
# for "framework-internal but stable").
extend-ignore-names = [ "_meta" ]

[tool.ruff.lint.pylint]
# Domain-rich entry points (build_plan, _http_error_or_auth, _api_request
# overriding upstream, derive_managed_role_ids) carry 6–9 parameters by
# design; the default 5 is too aggressive for this codebase.
max-args = 10
# ``_apply_finding`` runs an 8-branch finding classifier that returns
# early on each skip path; default 6 forces an awkward refactor.
max-returns = 10

# django-stubs is in [dependency-groups].dev for IDE / type-checker
# inspection of Django ORM internals. The mypy plugin
# (``mypy_django_plugin.main``) is intentionally NOT enabled here:
# wiring it would require a ``[tool.django-stubs]`` block pointing at
# ``tests.settings`` and would shrink — but not eliminate — the
# disable_error_code lists below. Per-call-site cast / disable_error_code
# is the deliberate trade-off; the basedpyright config below uses its
# own Django heuristics to bridge the gap.

[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
warn_redundant_casts = true
warn_unused_ignores = true
strict_equality = true
no_implicit_optional = true
disallow_untyped_defs = true
# Type-check production code only. Django dynamic attrs (User.profile,
# setUpTestData ClassVars) need allianceauth-stubs which don't exist.
exclude = [
    "aa_discord_audit/migrations/.*",
    "tests/.*",
    "build/.*",
    "dist/.*",
]

[[tool.mypy.overrides]]
module = [
    "allianceauth.*",
    "celery.*",
    "cosmic_ray.*",
    "requests.*",
    # Optional ``[metrics]`` extra. ``_metrics.py`` imports both behind
    # a ``_detect_django_prometheus()`` gate; mypy still needs the
    # symbol resolvable when the extra is not installed in the dev
    # environment.
    "django_prometheus.*",
    "prometheus_client.*",
]
ignore_missing_imports = true

# Django ORM fields can't be inferred without django-stubs[compatible-mypy].
# Pyright handles these correctly through its own Django heuristics.
# attr-defined: ``_id`` shadow attributes (e.g. ``run_id``,
# ``triggered_by_user_id``) auto-generated by Django from FK columns
# are unknown to mypy without django-stubs.
[[tool.mypy.overrides]]
module = [ "aa_discord_audit.models.*" ]
disable_error_code = [ "var-annotated", "attr-defined" ]

# Django admin overrides return ``request.user.has_perm(...)`` whose
# return type is Any without django-stubs. Suppressing no-any-return
# here is a deliberate carve-out — we accept Any-leaks at the
# framework boundary. ModelAdmin subclass methods
# (``has_add_permission``, ``save_model`` etc.) inherit their typed
# contract from ``ModelAdmin`` itself; restating it here would be
# noise without django-stubs in place.
[[tool.mypy.overrides]]
module = [ "aa_discord_audit.admin" ]
disable_error_code = [ "no-any-return" ]
disallow_untyped_defs = false

# Management commands inherit the same Django framework Any-leak from
# request.user / get_user_model() and additionally use ``audit_run``
# attribute writes that mypy does not associate with the FK column
# without django-stubs.
[[tool.mypy.overrides]]
module = [ "aa_discord_audit.management.commands.*" ]
disable_error_code = [ "no-any-return", "attr-defined" ]
# ``add_arguments(parser)`` and ``handle(*args, **options)`` inherit their
# contract from ``BaseCommand``; restating annotations here is noise.
disallow_untyped_defs = false

# The invocation core wraps ``AuditInvocation.objects.create()`` and writes
# the ``audit_run`` FK via its ``_id`` shadow attribute. Both leaks are
# the same Django framework Any-leak documented above for management
# commands; without django-stubs mypy cannot resolve either.
[[tool.mypy.overrides]]
module = [ "aa_discord_audit.invocation" ]
disable_error_code = [ "no-any-return", "attr-defined" ]

[tool.basedpyright]
venv = ".venv"
venvPath = "."
include = [ "aa_discord_audit" ]
exclude = [
    "aa_discord_audit/migrations",
    "tests",
    "**/__pycache__",
]
reportMissingTypeStubs = "none"
# basedpyright defaults are stricter than pyright; the Django/Alliance Auth
# integration relies on dynamic attrs that no third-party stub exposes.
reportAny = "none"
reportExplicitAny = "none"
reportUnknownMemberType = "none"
reportUnknownVariableType = "none"
reportUnknownArgumentType = "none"
reportUnknownParameterType = "none"
reportMissingParameterType = "none"
reportImplicitOverride = "none"
reportUntypedFunctionDecorator = "none"
reportUnusedCallResult = "none"
# Django ClassView/ModelAdmin idioms.
reportMissingTypeArgument = "none"
reportUnusedParameter = "none"
reportUnannotatedClassAttribute = "none"
# Django auto-generates _id shadow attributes (run_id, aa_user_id,
# triggered_by_user_id) from FK columns and reverse-FK descriptors
# (run.findings) — both invisible to pyright without django-stubs.
reportAttributeAccessIssue = "none"
# Implicit adjacent-string concatenation is idiomatic Python and the
# project uses it for line-wrapping long strings.
reportImplicitStringConcatenation = "none"
# clean_setting returns object|None|Any per Django's settings API; we
# annotate call sites with concrete types and accept the cast.
reportArgumentType = "none"
# Cross-module use of leading-underscore helpers (e.g. _apply_finding
# called from the management command's --resume path) is explicit
# project surface.
reportPrivateUsage = "none"
# Module-level conditional re-assignment to upper-case private names
# in tasks.py guards on AA core's QueueOnce import availability.
reportConstantRedefinition = "none"
reportUnnecessaryContains = "none"
reportUnknownLambdaType = "none"
reportOptionalMemberAccess = "none"
reportImportCycles = "none"
reportCallInDefaultInitializer = "none"
# Side-effect imports (signals) intentionally referenced by import only.
reportUnusedImport = "none"
# Settings whose runtime value can be None per spec ("opt out") but
# whose static type narrows to int via clean_setting's overload —
# defensive ``is None`` checks fire as unreachable to pyright.
reportUnnecessaryComparison = "none"
reportUnreachable = "none"
# Leading-underscore helpers used cross-module: ``_apply_finding``
# (audit/__init__.py re-export for the ``--resume`` reach-in),
# every ``_audit/_invocation.py`` helper imported by sibling
# subcommand modules, ``_RoleStub`` / ``_FixtureClient`` consumed by
# the bench + fixture entries. pyright sees them as private to the
# defining module and reports unused; the project explicitly uses
# private names as the cross-module surface, so the warning is
# systemically false-positive here.
reportUnusedFunction = "none"
reportUnusedClass = "none"

[tool.coverage.run]
# Branch coverage catches `if x: a` paths whose else-branch never runs;
# without it those count as 100% statement coverage but only 50% branch.
branch = true
source = [ "aa_discord_audit" ]
omit = [
    "aa_discord_audit/migrations/*",
    # Module markers carry no testable logic; counting them inflates the
    # denominator without exercising real behaviour.
    "aa_discord_audit/__init__.py",
]

[tool.coverage.report]
show_missing = true
# Skip the "Covered Lines" boilerplate Django generates in test output.
skip_covered = false
exclude_also = [
    # Type-only branches never execute at runtime.
    "if TYPE_CHECKING:",
    # `assert_never` / unreachable sentinels.
    "raise NotImplementedError",
    "assert_never\\(",
    # Pragma escape hatch for lines that cannot be exercised in tests
    # (e.g. AA-core-dependent fallbacks).
    "pragma: no cover",
]

[tool.bandit]
# Test files contain intentional anti-patterns (e.g. hardcoded test secrets).
# They are inline-suppressed with `# nosec` where needed; this just trims the
# scan to the production code.
exclude_dirs = [ "tests", "aa_discord_audit/migrations" ]

# cosmic-ray (mutation testing) configuration lives in
# ``cosmic-ray.toml`` at the repo root rather than under
# ``[tool.cosmic-ray]`` here — cosmic-ray's CLI takes the config file
# as a positional argument and the upstream examples uniformly use a
# dedicated file. See ``docs/mutation-testing.md`` for run instructions
# and the cosmic-ray.toml inline comments for scope rationale.

# xenon (cyclomatic complexity guard) — invoked from pre-commit /
# nox / CI as: ``xenon --max-absolute D --max-modules B --max-average A
# aa_discord_audit/``. Xenon has no pyproject.toml schema, so the
# thresholds live here as inline doc:
# - max-absolute D (≤ 30 per block): protects ``build_plan`` (21) +
#   ``_apply_finding`` (17) — domain-rich orchestrators that resist
#   decomposition without losing cohesion.
# - max-modules B (≤ 10 module average): every module stays
#   well-structured even if it carries one complex function.
# - max-average A (≤ 5 overall): keeps the codebase as a whole simple.
# Adding a function with complexity > 30 or letting a module's average
# drift past 10 is a regression that pre-commit will catch.

[tool.vulture]
# 80% strikes the right balance for this codebase: 60% drowns in Django
# framework boilerplate (ModelAdmin attrs, has_*_permission hooks); 100%
# misses anything ruff isn't already catching. 80% surfaces only items
# vulture is highly sure are unused.
min_confidence = 80
paths = [ "aa_discord_audit" ]
exclude = [ "aa_discord_audit/migrations/*" ]
ignore_names = [
    # Django signal handler dispatch contract: post_save / post_delete
    # invoke handlers as ``handler(sender, instance, **kwargs)``. The
    # ``sender`` arg is required by the dispatcher even when the body
    # uses only ``instance``. ruff ARG001 carve-out lives in
    # ``[tool.ruff.lint.per-file-ignores]`` for ``signals.py``; this
    # mirrors that decision for vulture.
    "sender",
]

[tool.deptry]
# Tests + nox + migrations import dev / framework things that production
# code does not — exclude them from the production-import scan.
extend_exclude = [
    "tests",
    "_nox",
    "noxfile\\.py",
    "aa_discord_audit/migrations",
]

# Frameworks pulled in transitively by ``allianceauth`` (the plugin
# never pins them directly to avoid drift from whatever AA itself
# constrains them to). The exact deptry code depends on whether the
# scan sees the package installed:
#   - DEP001 (missing): the import has no declaration AND deptry
#     cannot see an installed copy (isolated ``uvx`` invocation).
#   - DEP003 (transitive): same import, but deptry can see the
#     package installed in the active venv (``uv run`` invocation
#     against the project venv after ``uv sync``). The pre-commit
#     hook runs through ``uv run`` so DEP003 is the live path; the
#     DEP001 carve-out covers ad-hoc isolated runs.
#   - DEP004 (dev-only declared): ``requests`` lives in the ``dev``
#     group for testing fixtures, but production code imports it
#     too via AA's transitive graph. Pinning it as a direct prod
#     dep would create the same drift risk.
[tool.deptry.per_rule_ignores]
# ``prometheus_client`` is imported lazily inside the
# cooperate-don't-depend factories in ``aa_discord_audit/_metrics.py``;
# the direct project dependency is ``django-prometheus`` (declared in
# the ``[metrics]`` optional extra), and ``prometheus-client`` is picked
# up transitively. The carve-out keeps the deptry scan from flagging
# the lazy import as a missing / transitive direct dependency.
DEP001 = ["django", "celery", "asgiref", "prometheus_client"]
DEP003 = ["django", "celery", "asgiref", "prometheus_client"]
DEP004 = ["requests"]

[tool.interrogate]
# Threshold reflects current baseline; bumping it ratchets coverage up
# over time. ``fail-under`` is intentionally conservative — set to a
# value the repo actually clears today; lift it 5% at a time when a
# refactor pushes the headroom up. Baseline 2026-05-11 is 41.1%; the
# 40 floor turns this hook into a no-regression gate without forcing
# a mass docstring sweep today.
fail-under = 40
# Coverage scope: production code only. Tests, migrations (Django-
# generated), and the per-package ``__init__.py`` carry their own
# rules (or no rules) under ruff D-codes already.
ignore-init-module = true
ignore-init-method = true
ignore-magic = true  # __repr__ / __str__ are covered by ruff D105
ignore-nested-functions = true
ignore-private = true  # underscore-prefixed helpers
ignore-property-decorators = true  # property docstrings often duplicate getter name
ignore-semiprivate = false
ignore-setters = true
exclude = [
    "tests",
    "_nox",
    "noxfile.py",
    "aa_discord_audit/migrations",
    "aa_discord_audit/admin.py",  # ModelAdmin docstrings are framework-shape
    "aa_discord_audit/apps.py",
    # Safety net: even if interrogate is invoked without explicit
    # PATHS, never walk into venv-shaped caches. GitLab CI sets
    # PRE_COMMIT_HOME=.cache/pre-commit, which puts third-party hook
    # site-packages under the working dir; some carry pre-3.7
    # ``foo.async(...)`` syntax that ast.parse rejects.
    ".cache",
    ".venv",
    ".nox",
    "dist",
    "build",
]
quiet = false
verbose = 0
omit-covered-files = true

[tool.codespell]
# Skip large / binary / generated artefacts. Mutation harness
# files (``mutation.sqlite*``) contain mutated source fragments
# that look like typos out of context.
skip = ".git,.venv,.nox,uv.lock,.secrets.baseline,htmlcov,build,dist,*.po,*.mo,*.pyc,*/__pycache__/*,coverage.xml,mutation.sqlite*"
# Domain vocabulary that codespell mis-flags by default:
# * ``eve`` — EVE Online (the game whose users this audit covers).
# * ``allianceauth`` — package name.
# * ``aa`` — Alliance Auth abbreviation (also the ``aa_*`` prefix
#   convention for installed-app labels).
# * ``hax`` — intentional "attempt to defeat the append-only
#   manager" fixture string in
#   ``tests/test_models_constraints.py`` and ``tests/test_signals.py``.
# * ``unparseable`` — enum-value name from upstream ``cosmic-ray``
#   (``IncompetentMutant.Unparseable``); referenced verbatim in the
#   AST-aware mutation filter.
ignore-words-list = "eve,allianceauth,aa,hax,unparseable"

[tool.djlint]
# Django template profile. ``max_line_length`` matches the
# project's Markdown convention (120) — separate from the
# Python line length (79) because HTML attributes inflate
# horizontal use more than Python statements do.
profile = "django"
max_line_length = 120
max_attribute_length = 120
max_blank_lines = 1
# Stylistic rules excluded so the hook focuses on structural
# template defects (unclosed tags, broken attribute lines,
# bad block balance). These three render identically across
# the chosen alternatives — they are taste, not correctness:
# * T002 — single vs double quotes inside ``{% tag '…' %}``.
# * T003 — named ``{% endblock body %}`` instead of bare
#   ``{% endblock %}``. Helpful for >200-line templates; ours
#   are under 200 and reviewable at a glance.
# * H023 — ``&mdash;`` / ``&laquo;`` / ``&raquo;`` vs the
#   corresponding Unicode codepoints. Browsers render the
#   entity refs identically; readers / linters see HTML 4
#   compatibility (some tooling, e.g. PDF exporters, still
#   prefers entities).
ignore = "T002,T003,H023"
