Cohort Snapshots

Public analysis cohorts pin to immutable snapshots. A snapshot is backed by one snapshot-intent batch. When the batch finishes, the projector captures the cohort state for that batch and makes it permanently addressable.

Snapshots solve two operational problems:

  • public URLs do not drift when newer runs finish
  • partial retests do not silently change a published cohort

Capture a New Snapshot

From the Cohort Admin Panel

  1. Open Settings > Analysis > Cohorts in the admin UI.
  2. Click Run new snapshot on the cohort row.
  3. The server submits a batch with the cohort source tag, the tag default profile when configured, and snapshot_intent = true.
  4. The snapshot is pending until every job in the batch has graduated.

From a Schedule

A cohort schedule submits the same batch as Run new snapshot on a recurrence, with the schedule’s profile and default pin. See cohorts.md .

From the Batch Form

  1. Open the Batches tab.
  2. Select From tag and choose the cohort source tag.
  3. Enable Capture as cohort snapshot.
  4. Submit the batch.

If the selected domain set is narrower than the full cohort, the UI warns that the snapshot will cover only the selected domains.

Lifecycle

  • pending: jobs in the batch are still graduating.
  • captured: every job has graduated and aggregates are computed.
  • retired: hidden from the public path, but facts remain stored.
  • failed_mixed_profiles: hidden because the batch used more than one profile.

Mixed-profile snapshots are intentionally hidden. A snapshot should represent one comparable run profile across the cohort.

Captured Provenance

A snapshot records the measurement regime its runs were produced under, so a comparison of two snapshots can tell an engine change from a cohort change:

  • Engine version, read from the runs’ own version entries, with a flag for a batch that spanned an upgrade.
  • Tag vocabulary, the test_levels table of the run profile: every tag that carried a severity level at the time. A tag absent from a snapshot means either that the testcase did not exist or that every domain passed; the vocabulary is what separates the two.
  • Scoring configuration hash, the identity of the scoring configuration in force. A score can move because penalties changed rather than because findings changed.

Each value is read from the stored runs, never from the running server. An unrecoverable value stays unknown; it is never defaulted to what the server does today.

The engine version and the vocabulary are backfilled at startup for snapshots captured before they were recorded, from any surviving run of the batch. A snapshot whose runs have been purged stays unknown. The scoring configuration hash cannot be backfilled: what was in force at capture is not recoverable afterwards.

Rebuilding a snapshot’s views does not rewrite any of these. They describe the capture, not the derived views.

The public API reports vocabulary_available and scoring_config_hash on each snapshot. The vocabulary itself stays server side.

Rebuild Snapshot Views

A snapshot’s views are computed at capture. A release that adds a column to a view table leaves earlier snapshots on the old shape until their views are recomputed, so a filter reading that column matches nothing there.

Two controls in Settings > Analysis > Cohorts recompute them:

  • Rebuild aggregates on a snapshot row recomputes that snapshot.
  • Rebuild all aggregates above the snapshot list recomputes every snapshot in the cohort, one at a time, newest first. Snapshots whose source runs are purged are skipped. A second request for the same cohort is refused while one is in progress.

Both preserve slug, label, captured_at and the default pin. The cohort Rebuild button is a different operation: it re-projects the cohort’s runs and recreates its snapshot rows, so custom slugs, labels, the default pin and the public flags do not survive it. Use it for a corrupt projection.

Recomputation reads the stored per-run analysis rows. A statistic added to the projector after a run was projected is absent from those rows, and only a cohort rebuild recovers it.

Stable URLs

Every public analysis URL can carry ?snapshot=<slug>.

/analysis/
/analysis/?snapshot=2026-04-20-a1b2c3d4e5f6
/analysis/domains/example.se?snapshot=2026-04-20-a1b2c3d4e5f6

Without snapshot, the cohort resolves according to its default snapshot policy. With snapshot, the URL stays pinned to that snapshot.

Use pinned URLs for reports, slide decks, tickets, and public references where the numbers must not change later.

The Trends view compares aggregate categories across captured snapshots. Current categories include:

  • severity_distribution
  • grade_distribution
  • signed
  • dnskey_algo

The Diff view compares two snapshots:

  • added domains
  • removed domains
  • grade changes
  • worst-level changes

Rows link back to the relevant domain detail in the selected snapshot.

Retire, Restore, Purge

Use Retire when a snapshot should disappear from public views but remain recoverable. Use Restore to make a retired snapshot public again. Use Purge when the snapshot row and aggregate rows should be deleted.

Retiring or purging a snapshot that was the cohort’s pinned default reverts the cohort to auto_latest.

Batch Deletion Interaction

Manual batch deletion removes the batch and everything derived from it, including cohort snapshots backed by that batch. Use it when the batch itself was wrong, such as a bad profile or a bad domain set.

Use snapshot retire or purge when the batch is valid but the snapshot should no longer be public.

See ../server/batch-deletion.md .

First-Boot Backfill

On first start after the snapshot model is installed, the server enumerates existing (cohort, batch) pairs with materialized analysis rows and creates a captured snapshot for each pair. The migration is idempotent and runs once.

Startup log example:

analysis: snapshot backfill complete - cohorts=2 created=17 skipped=0