# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.1.0a6] - 2026-05-26

### Added

- Operator dashboard redesign: the run, invocation, and changelog
  lists gain coloured action / state badges and sortable, filterable
  DataTables behind a dual-mode pagination helper. A summary card on
  the run list surfaces fleet-wide audit posture at a glance, and the
  run detail splits into per-section cards.
- Field-level before/after diff for ``ConfigChangeLog`` update rows:
  the changelog detail renders per-field changes against the
  chronologically-previous row instead of a raw JSON blob, with secret
  redaction at render. Changelog payloads render as pretty-printed
  JSON and the actor filter matches case-insensitively.
- Per-finding explain view gains badges, JSON-rendered resolved
  policy, an as-of marker, and render-time redaction.
- Engine-level destructive-action confirmation gate: ``build_plan`` /
  ``apply_plan`` coerce ``strip`` / ``strip_kick`` to ``report``
  unless the run carries an explicit confirmation, so a non-CLI caller
  cannot reach destructive actions without one. The gate keys on the
  confirmation flag, not the trigger type, so the scheduled beat path
  stays authorized; a distinct summary marker separates this coercion
  from the first-run lock, and ``--resume`` inherits the original
  run's confirmation flag.
- ``audit_acknowledge_initial`` management command: release the
  first-run dry-run lock from the console, gated on both ``run_audit``
  and ``acknowledge_initial_audit``, idempotent and attributed.
- ``AA_DISCORD_AUDIT_CLI_ACTOR`` setting: pins the CLI acting user to
  a Django username (the "service principal" pattern), resolved and
  permission-checked per invocation, feeding both the permission gate
  and ``ConfigChangeLog`` attribution from one resolver.
- Beat (periodic) audit attribution: every beat-triggered run now
  writes an accepted system-actor ``AuditInvocation``
  (``triggered_by=BEAT``), closing the audit-the-auditor blind spot
  for unattended runs.
- ``AA_DISCORD_AUDIT_BEAT_ALLOW_DESTRUCTIVE`` setting (default
  ``False``): a dedicated opt-in for destructive actions on the
  unattended beat, independent of the one-shot first-run lock. A beat
  run carrying destructive intent is coerced to ``report`` unless
  armed, with a non-fatal startup warning when the policy and the flag
  disagree.
- ``AA_DISCORD_AUDIT_BEAT_MIN_INTERVAL_MINUTES`` setting (default
  ``0`` = disabled): a coarse global floor that skips a beat tick
  firing too soon after the previous accepted beat run — a backstop
  against a misconfigured fast schedule.
- Forensic breadcrumb before destructive retention pruning:
  ``log_destructive_purges`` emits a WARNING (run id, state, actions
  applied, members scanned, start time) for every run with applied
  actions before its rows are cascade-deleted, so the action history
  survives in the operator's log aggregator.

### Changed

- ``--policy-preview`` renders the policy diff arrow as ASCII ``->``
  instead of the Unicode arrow, so the preview is safe in legacy /
  non-UTF-8 operator shells.

### Fixed

- Beat run lifecycle hardening: a dedicated per-task ``QueueOnce``
  lock now spans the full deadline (it previously self-expired
  mid-run), an idempotency key derived from the run window enables the
  dedup that was dead for the beat, and the terminal state is
  inspected so a FAILED / BUDGET_EXCEEDED beat run no longer logs as
  success.
- Beat runs stranded by ``SoftTimeLimitExceeded`` (a ``BaseException``
  the per-finding handler never caught) or a worker crash are now
  recovered: the in-flight run is flipped to the resumable
  ``INTERRUPTED`` state, and ``reconcile_stuck_runs`` sweeps any active
  run older than the hard time limit at the start of every tick.
- Beat-kick re-dispatch counter now increments on the broker handoff
  rather than the trailing CAS, so a row a racer completed in between
  no longer undercounts a genuine re-dispatch; retention pruning
  deletes in auto-committing batches so a year of findings can no
  longer exceed the task time limit and be killed mid-delete.
- ``audit_benchmark`` no longer bypasses the authorization gate (it
  resolves the CLI actor and refuses without ``run_audit``) and no
  longer creates and deletes a real ``InitialAuditAcknowledgement`` —
  which briefly released the global first-run lock process-wide, so a
  concurrent live run could skip its REPORT coercion.
- Per-user 24h rate-limit count-then-create now runs inside
  ``transaction.atomic()`` with a row lock on the actor, so two
  concurrent invocations by the same user can no longer both pass the
  gate and breach the per-day cap.
- ``--resume`` checks the pending-findings hard cap before the
  ``RUNNING`` compare-and-set, so an over-cap refusal leaves the run
  in its resumable state instead of stranding it ``RUNNING``.
- A ``SIGTERM`` / ``SIGINT`` arriving while the run is still
  ``PENDING`` (before the subcommand's ``PENDING -> RUNNING``
  transition) now records the interruption too, instead of leaving the
  run stranded ``PENDING``.
- ``audit_discord_roles`` refuses combined subcommands (e.g.
  ``--explain`` together with ``--diff``) instead of silently running
  the first and dropping the rest.
- ``--explain`` lets an operator-facing ``CommandError`` propagate
  verbatim instead of burying it behind a generic "failed to fetch
  member" wrap.
- Render-redaction parity: ``cli_argv`` and changelog diff values are
  redacted at render, non-scalar diff cells are JSON-encoded, and the
  run-detail findings-filter dropdown labels are translated.

### Refactor (internal)

- Web list views share one row-source selector (``_list_objects``)
  and one badge source of truth (``build_badge`` / ``_BADGE``,
  including a ``config_action`` kind); the changelog action badge no
  longer bypasses the display layer.
- CLI consolidation: a single ``_create_invocation`` writer behind the
  accept / refuse recorders, a ``_finalize_resume`` terminal-flip tail,
  an ``_ExplainResult`` bundle shared by both explain emitters, a
  ``Counter``-based policy-preview tally, and ``values_list`` reads
  where only a few columns are needed.
- Engine / persistence consolidation: a shared
  ``_apply_report_coercion`` tail behind the first-run, beat-
  destructive, and confirmation gates, and a single
  ``_persist_policy_coercion`` writer behind the three coercion
  wrappers, with the distinct forensic markers preserved.
- ``OrphanAction.is_destructive`` / ``policy_has_destructive``
  extracted as the single source of truth for "destructive",
  replacing two duplicated frozensets so the engine reachability check
  and the startup validator cannot drift.

### Docs

- Runbook: ``DiscordIdentity`` is not populated by audit runs (the
  table stays empty unless an operator links an identity or a
  ``DiscordUser`` is deleted); documented so the empty table stops
  reading as a bug.
- Recorded the read-only-diagnostics recon trail (``--explain`` /
  ``--diff`` write no ``AuditInvocation`` and consume no rate budget)
  as a deliberately accepted gap, with the switch to make if the
  threat model ever has to cover a trusted-but-curious actor.
- Documented the ``is not None`` subcommand-dispatch invariant and the
  one-finding-per-member-per-run invariant the historical-diff map
  relies on.
- Comment hygiene: dropped internal label tokens from source and test
  comments and corrected three stale comments (a ``type: ignore`` that
  no longer exists in ``request_user_has_perm``, the auth-hook menu
  note, and a spoof-vector overclaim in ``_current_celery_task_id``).

### Tests

- New suites for the confirmation gate, the beat destructive gate, CLI
  actor resolution, ``audit_acknowledge_initial``, and the expanded
  periodic-task lifecycle (locking, idempotency, stuck-run recovery,
  re-dispatch counting, batched pruning). Added a regression pinning
  the ``_link_invocation`` append-only allow-list contract (the
  relation-name vs attname asymmetry the post-destructive linkage write
  depends on).

### Tooling

- Internal brainstorming and implementation working-notes are
  gitignored as scratch artefacts (not shipped or version-tracked).
- Type-checking cleanup in ``_changelog_diff``: ``_prior_row`` reads
  the primary key via ``pk`` and binds the ``.first()`` result through
  an annotated local, clearing the package's two remaining mypy
  diagnostics (``no-any-return`` / ``attr-defined`` raised without the
  django-stubs plugin) without tripping basedpyright's redundant-cast
  check.

## [0.1.0a5] - 2026-05-23

### Added

- ``AuditInvocation`` application-level acceptance invariant: ``save()``
  on INSERT rejects inconsistent ``(accepted, refusal_reason)`` tuples
  (``accepted=True`` requires empty refusal text; ``accepted=False``
  requires a non-empty ``InvocationRefusal`` value). Defense-in-depth
  for the audit-trail without a schema migration; raw-SQL writes
  bypass, but no in-codebase write path uses them.

### Fixed

- ``--resume`` policy regression: the resume path now re-applies the
  per-finding policy resolution (per-uid override → per-group / per-
  state policy → ``ProtectedDiscordMember``) instead of inheriting the
  action snapshot from the predecessor run. A finding the operator
  marked as ``ProtectedDiscordMember`` between runs is now correctly
  coerced to ``report`` on resume.
- Prefetch-bypass N+1 in the apply loop: per-finding role-derivation
  was re-issuing the ``ManagedRoleException.objects.values_list``
  query under the apply lock. The exempt role-id set is now frozen
  once at ``build_plan`` time and snapshotted onto the ``AuditPlan``;
  apply-time lookup is O(1).
- Webhook leak through partial-strip path and self-elevation gap in
  operator-perm checking — both surfaced by the fifth-pass security
  review.
- AA-4 × Py3.10 matrix cell: ``unittest.TestCase.enterContext`` joined
  stdlib in 3.11, but six ``setUp`` paths across ``tests/_factories.py``,
  ``tests/test_management_cmd_gate.py`` and ``tests/test_notifications.py``
  called it. The 3.10 cell broke with ``AttributeError`` masked by
  Django's parallel runner as ``cannot serialise 'traceback' object``
  (tracebacks only became serialisable across processes in 3.11). Test-
  only shim mirrors the CPython implementation under a
  ``sys.version_info < (3, 11)`` guard in ``tests/__init__.py``; drop
  together with the 3.10 floor.

### Tooling

- MariaDB smoke matrix via testcontainers (``nox -s tests_mariadb``).
  Zero-touch local flow: ``AA_DISCORD_AUDIT_TEST_DB=mariadb`` lazy-
  starts a ``mariadb:10.11`` container; ``AA_DISCORD_AUDIT_TEST_DB_HOST``
  routes to an operator-managed server for CI. ``utf8mb4`` +
  ``STRICT_TRANS_TABLES`` pinned for production parity.
- Pre-commit pipeline hardening: SHA-pinned hooks, dedicated audit
  gate session, test runner now uses ``--shuffle`` to surface order
  dependencies.

### Tests

- Mutation-survivor pin sweep over 16 rounds + two clean-up passes
  (P0 tautological assertion kill, P1 misleading test kill). Test
  count: 630 → 768 (+138). New pins cover operator forensics,
  redaction recursion, abandon CAS-loss, ``observed_uids`` contract,
  ``--resume`` hard-cap, ``cli_override`` propagation, bulk-PATCH
  tracking, shim-canary dispatchers, boundary completeness,
  ``AuditResult`` divergence, abandon idempotency, ``ContextVar``
  reset across signal-driven tests, ``RECLASSIFIED_USER_CHANGE``,
  per-finding ``__context__`` walk, and AGENTS.md drift.
- Post-audit review pass: frozen pins restored, helper dedup
  finalised, tighter assertions across the regression set.

### Refactor (internal)

- Sequence of audit polish waves (second through fifth pass): CAS
  gate tightening, cause-chain ``auth_failed`` propagation,
  terminal-CAS dedup scaffolding, ``_finding/`` decomposition
  follow-up, invariants and consistency cleanup across the resume /
  abandon path. Test coverage advanced to ~95% line / ~88% branch;
  mutation score ~96% on the trimmed corpus.
- Test factory consolidation: ``make_audit_plan`` envelope helper,
  ``make_cas_loss_at(*states)`` selective ``transition_run_state``
  CAS-loss side-effect, ``LINKED_UID`` canonical constant, and
  ``CurrentUserResetMixin`` reuse across signal-driven test classes.

### Docs

- Multi-pass documentation audit (cross-cutting + 4th-pass drift
  fixes): runbook rate-limit polarity, phantom-flag removal, symbol-
  grained code refs replacing brittle ``file.py:NN`` pointers, spec
  drift register surfacing 13 landed-in-code features not yet
  written into ``§``-blocks.
- ``tests/`` and models hierarchy ``AGENTS.md`` refreshed for the
  session's factory + invariant additions; ``.omc/plans/AGENTS.md``
  gains a ticket-id glossary (``Round-2`` / ``Round-3 H##`` / etc.)
  pointing at the research artefacts.

## [0.1.0a4] - 2026-05-15

### Added

- ``AA_DISCORD_AUDIT_IDEMPOTENCY_KEY_TTL_DAYS`` setting (default
  ``0``, disabled). When positive, ``prune_audit_runs`` NULL-ifies
  ``AuditRun.idempotency_key`` on rows older than the cutoff so a
  caller-supplied dedup token (e.g. a date-derived key on a daily
  Beat) can be reused independently of
  ``AA_DISCORD_AUDIT_RUN_RETENTION_DAYS``. The run row itself is
  preserved for the longer retention window; only the unique slot
  is released.
- ``prune_audit_runs --keys-only`` flag to execute the
  idempotency-key expiry phase without deleting any rows.
- Optional Prometheus instrumentation behind the ``[metrics]``
  extra (``pip install aa-discord-audit[metrics]``). Cooperate-
  don't-depend layer in ``aa_discord_audit/_metrics.py``: when
  ``django-prometheus`` is installed the audit's counters and
  histograms register into the default ``prometheus_client.REGISTRY``
  and are exposed by ``django-prometheus``' ``/metrics`` view alongside
  its own HTTP / cache / migration / Celery series. Without the extra
  every metric reference resolves to a no-op stub at near-zero
  runtime cost. Metric catalog: ``aa_discord_audit_runs_total``,
  ``aa_discord_audit_run_duration_seconds``,
  ``aa_discord_audit_findings_total``,
  ``aa_discord_audit_invocations_total``,
  ``aa_discord_audit_webhook_deliveries_total``,
  ``aa_discord_audit_webhook_delivery_seconds``,
  ``aa_discord_audit_idempotency_keys_expired_total``,
  ``aa_discord_audit_runs_pruned_total``. Full label vocabulary,
  bucket layouts, and Grafana recipes live in ``docs/METRICS.md``
  (and ``docs/METRICS.ru.md``).

## [0.1.0a3] - 2026-05-14

### Fixed

- ``noxfile`` import failed under Python 3.10 with
  ``ModuleNotFoundError: No module named 'tomllib'`` — ``tomllib``
  only joined the stdlib in 3.11, but ``requires-python`` accepts
  3.10. ``_nox.mutation`` (imported as a side effect from
  ``noxfile.py``) now does
  ``import tomllib`` on 3.11+ and falls back to the ``tomli``
  back-port on 3.10. ``tomli>=2.0`` is added to the dev group with
  marker ``python_version < '3.11'``, so 3.11+ environments stay
  lean. Verified against all four interpreters (3.10–3.13). This
  unblocks the 3.10 cells of both AA-4 and AA-5 matrix tracks in
  GitLab CI.

## [0.1.0a2] - 2026-05-14

### Fixed

- ``interrogate`` pre-commit hook was failing in GitLab CI with a
  ``SyntaxError`` from a vendored third-party hook (pre-3.7
  ``foo.async(...)`` syntax). Root cause: ``PRE_COMMIT_HOME`` lands
  under the working directory in CI, and ``interrogate`` without an
  explicit PATHS argument scanned every cached hook's site-packages.
  Hook now scopes scanning to ``aa_discord_audit/`` and the
  ``[tool.interrogate].exclude`` config carries belt-and-braces
  entries for ``.cache`` / ``.venv`` / ``.nox`` / ``dist`` /
  ``build``. Side effect: the hook is now roughly an order of
  magnitude faster.

### i18n

- Translate the one Russian-locale entry that ``make messages``
  surfaced after 0.1.0a1: ``FindingSkipReason.INTERNAL_INCONSISTENCY``
  ("Build/apply disagreed on which findings exist" →
  "Расхождение фаз build/apply по составу находок"). Header bumps on
  both ``ru/`` and ``en/`` catalogues to match the cut.

## [0.1.0a1] - 2026-05-14

### Changed

- Collapse the per-finding apply pipeline into `_ApplyFindingExecutor`
  (a frozen dataclass holding `finding` / `run` / `db_finding` /
  `action`, optionally `plan` / `client`). Module-level shim functions
  preserve
  the `~30 mock.patch("..._finding.<name>")` paths that test code and
  external integrations already bind, so the namespace mapping
  survives the move into class scope.
- Squash the migration chain back into a single `0001_initial`. The
  pre-release-only `0002_alter_auditfinding_skipped_reason` is folded
  into the squash so a fresh install applies one migration. No
  deployment ever applied `0002` — there were no published wheels
  between the two.
- Address pre-release code-review findings in the audit core (terminal
  state clobber prevention, resume/abandon loop-safety hardening).

### Fixed

- Resume-loop priority: when an `auth_failed` outcome and a deadline
  expiry race in the same retry sweep, `auth_failed` now wins, so the
  pending kick is not silently abandoned on a transient 401.
- Terminal-state clobber in the apply pipeline that could re-enter an
  `APPLIED` finding on a partial-failure recovery path.
- Tighten redaction across the resume/abandon path so the webhook
  credential cannot leak through a `cli_argv` re-emit on retry.

### Security

- Webhook-URL redaction now also filters non-string log records before
  the regex pass, closing a gap where a `LogRecord` with a non-string
  message body would skip the filter entirely.

### Tooling

- `cosmic-ray` mutation-testing infrastructure (`mutation.sqlite`
  work-queue, `make mutation` / `make mutation-parallel` /
  `make mutation-html`). The AST-aware filter
  (`_nox/filters/cr_type_annotation_filter.py`) drops the PEP-604
  `X | Y` typing-annotation noise that otherwise dominates the
  survivor list. Mutation score on the trimmed corpus: 96.04 %
  (2399 killed / 2498 ran, 836 filtered as type-annotation
  equivalents, 4 incompetent).
- Pre-commit additions: `deptry`, `interrogate`, `detect-secrets`,
  `conventional-pre-commit`. `django-test-migrations` now covers
  forward-apply invariants in CI.
- `nox -s audit` closed (was a stub); refactored `mutation_parallel`
  to dispatch through nox sessions instead of `bash -c`.

### Tests

- Test count: 325 → 630 (+305). The new tests are mutation-targeted
  (truth-table coverage for boolean conditions, bit-disjoint operands
  for `Add_BitOr` / `Add_BitXor`, edge-distinguishing inputs for
  ordering comparisons, structural assertions on keyword-flag pins).
- Auth-fail and resume-race tests hardened against tautological
  assertions (previously a missing CAS would still pass; now the
  assertion locks the CAS state explicitly).

## [0.1.0a0] - 2026-05-09

### Added

- Reconciliation audit core (`build_plan` + `apply_plan`) with frozen
  `AuditPlan`, per-finding 3-phase atomic dance (lock + reclassify +
  decide → HTTP role-strip → persist), and dead-letter queue
  (`PendingKick`) with CAS-guarded retry beat task.
- Per-category action policy with optional per-group and per-state
  overrides nested inside each `OrphanCategory`.
- Per-uid `FindingActionOverride` admin table layered above the
  policy resolver.
- Operator-managed config tables: `BotAccountUid`,
  `ProtectedDiscordMember`, `ManagedRoleException`,
  `FindingActionOverride`, with full append-only `ConfigChangeLog`
  trail attributed via a `ContextVar`-based middleware.
- Append-only audit-trail rows on `AuditRun`, `AuditFinding`,
  `AuditInvocation`, and `ConfigChangeLog` enforced at both the
  manager (`AppendOnlyManager`) and instance (`AppendOnlyModelMixin`)
  layers; FSM transitions go through compare-and-set.
- Operator dashboard: run list + detail, per-finding explain page,
  invocation list, config-change-log list. Bootstrap 5 templates
  extending `allianceauth/base-bs5.html`.
- `audit_discord_roles` management command with subcommands
  `--resume`, `--abandon`, `--diff`, `--explain`, `--policy-preview`,
  `--from-fixture`. Allowlist-based argv redaction.
- `audit_benchmark` synthetic-load command and `prune_audit_runs`
  retention command.
- Celery `audit_orphan_members` task under
  `QueueOnce(graceful=True)`, sharing the
  `discord.user_actions.<uid>` lock with AA's `update_groups`.
- Per-user 24-hour rolling rate limit on accepted audit triggers
  (refused attempts do not count toward the limit).
- Idempotency-key support on `build_plan` to make the same observed
  state reproducibly map to the same `AuditRun`.
- Operator runbook ([`docs/runbook.md`](https://gitlab.com/eveo7/aa-discord-audit/-/blob/main/docs/runbook.md)) and
  performance reference ([`docs/performance.md`](https://gitlab.com/eveo7/aa-discord-audit/-/blob/main/docs/performance.md)).
- Russian translation catalogue under
  `aa_discord_audit/locale/ru/`. Compiled `.mo` ships in the wheel.
- Alliance Auth 5.x compatibility on the same private-API surface
  the AA 4.x build uses; tests exercise both majors via the
  `tests_aa5*` nox sessions.

### Security

- First-run dry-run lock: the very first run after install is coerced
  to `report` regardless of operator settings, until an
  `InitialAuditAcknowledgement` row exists. Re-arming requires shell
  access — intentional friction.
- `ProtectedDiscordMember` admin table coerces any matched finding to
  `report`, above policy and per-uid override.
- Webhook URL is treated as a credential: redaction filter masks it
  from logs, exception messages, persisted `cli_argv`, and
  `AuditRun.summary`.
- `X-Audit-Log-Reason` header builder strips Cc and Cf Unicode
  categories before transmission and validates the 512-byte budget at
  `build_plan` time so apply-time calls cannot raise inside an open
  transaction.
- Allowlist-based `cli_argv` redaction — anything outside the known
  argument keys is recorded as `[REDACTED]` regardless of shape.
- `CurrentUserMiddleware` registration is validated at app load —
  `ImproperlyConfigured` on absence so the audit-the-auditor trail
  cannot silently degrade to `<system>`.
- Migration concurrency guard refuses any post-`0001_initial`
  migration that adds a constraint or index without
  `CONCURRENTLY`. Pre-commit and `nox -s migrations_concurrency_check`
  both enforce.

### Tooling

- `uv`-only toolchain (build, lock, sync, run, publish).
- `nox` sessions: `tests`, `tests_matrix`, `tests_aa5`,
  `tests_aa5_matrix`, `tests_compat`, `lint`, `coverage`,
  `typecheck`, `audit`, `migrations_check`,
  `migrations_concurrency_check`, `makemigrations`, `makemessages`,
  `compilemessages`, `markdown_lint`.
- Pre-commit pipeline: ruff (check + format), mypy, basedpyright,
  vulture, xenon, bandit, `uv lock --check`, migrations concurrency
  guard, editorconfig + validate-pyproject + django-upgrade.
- Test coverage ~87% across 325 tests.

## Translations

- [Русский](CHANGELOG.ru.md)
