Unreleased — highlights

Highlights accumulate here as work lands, and this heading is retitled to the dated release heading at release time. The heading is deliberately unbracketed: a bracketed form collides with release automation's version-header pattern and would be read as a release.

Nothing yet for the next release.

[0.12.0] - 2026-10-02

This release adds export and retention telemetry with a new telemetry guide, a limit: option for Threadline.history/3, --strict and --all-schemas flags for mix threadline.health.coverage, and a legacy-key health warning for audit rows captured before 0.11. It carries three breaking changes, each with a fix below.

Breaking changes

  • A non-list exclude:/mask:/except_columns: on a :threadline, :trigger_capture table entry (for example exclude: :ssn) used to be ignored silently, so the column was never redacted or omitted. It now raises ArgumentError at config load and at trigger generation. Fix: wrap the column name in a list, e.g. exclude: [:ssn]. A non-string mask_placeholder: now raises ArgumentError instead of FunctionClauseError; no fix is needed beyond passing a string.
  • The [:threadline, :operator_surface, :authorize], [:threadline, :operator_surface, :export_authorize] and [:threadline, :operator_surface, :actor_ref_mismatch] telemetry events no longer carry actor_ref, session_actor_ref or scope_actor_ref, so telemetry never carries actor identity. Fix: remove those keys from your handler's pattern matches and read the actor from your own session or scope instead. result, count, path and scope_keys are unchanged; actor_ref_mismatch is now a pure incidence counter with no metadata.
  • The [:threadline, :health, :checked, :error] telemetry event's metadata is now %{exception: module} instead of %{error: message}, because exception messages can echo database values. Fix: match %{exception: mod} in your handler; Threadline.Telemetry.emit_health_checked_error/1 now takes the exception struct itself, not a string.

Added

  • Threadline.history/3 accepts a new limit: n option, returning at most the n most recent changes (captured_at desc, id desc). It is additive and the default is unchanged (unbounded); use row_history_page/4 for keyset paging.
  • [:threadline, :export, :completed] and [:threadline, :export, :failed] telemetry events, firing once per logical export (eager CSV/JSON, the async export job, and the chunked operator-surface download) with row_count, duration, and format so you can alert on export failures and track export volume without polling.
  • [:threadline, :retention, :purge, :start/:stop/:exception] span events and a [:threadline, :retention, :batch_purged] event per purge batch, with rows-deleted counts, so a scheduled retention purge is observable the same way exports are.
  • The Threadline.Telemetry module documentation now lists every telemetry event in one table (name, measurements, metadata, and when it fires), replacing a partial prose list.
  • A new Telemetry guide with an attach_many example per event family, a metrics-library example, handler-safety and cardinality guidance, and a recipe for observing Threadline's own database queries through your host repo's own [:my_app, :repo, :query] event.
  • Threadline.Health.legacy_key_findings/1 reports a new :unresolved_legacy_keys warning per table for audit rows captured before 0.11 that history/3 cannot find by key, with a link to the upgrade guide's backfill step; it is time-limited and capped.
  • mix threadline.health.coverage --strict: exit 1 via exit({:shutdown, 1}) when any :error-severity finding is present in the checked schema (trigger_findings/1 plus the new legacy_key_findings/1). Composes with --json and --schema. Uncovered tables and :warning findings never fail --strict; use mix threadline.verify_coverage for the positive-list gate. A cancelled :unresolved_legacy_keys probe (typically a missing row-history index) prints a stderr hint and never fails --strict.
  • mix threadline.health.coverage --all-schemas: checks every reportable schema in one batched catalog snapshot (never a per-schema loop) instead of a single --schema, as a schema-keyed table or, with --json, an envelope ({"schemas": {"<name>": <single-schema payload>, ...}, "summary": {...}}) whose schemas values are byte-identical to what --schema=NAME --json prints for that schema. Cannot be combined with --schema. A schema that is itself a member of a PostgreSQL extension is excluded; a schema with no reportable tables and no findings is omitted. --strict --all-schemas gates the union of every reported schema's :error findings.

Fixed

  • mix threadline.health.coverage now raises on unknown or misspelled options (for example --stict) instead of silently ignoring them. No action needed unless you were passing a typo'd flag and relying on it being a no-op; fix the flag name. It also now raises on a stray positional argument (for example a dropped leading --, as in schema=public instead of --schema=public) instead of silently running against the default "public" schema. No action needed unless you were relying on a malformed argument being ignored; fix the argument.
  • [:threadline, :operator_surface, :authorize]'s path metadata now comes from the mount macro's own compile-time path argument instead of the live request path. If you mount threadline_operator_surface/2 under a dynamic router segment (for example /accounts/:account_id/audit), path now reports the un-substituted route template ("/accounts/:account_id/audit/theme") instead of the real segment value matched for that request. No action needed unless you mount under a dynamic segment and pattern-match path's exact value.
  • Threadline.Retention.purge/1's dry run now counts the audit transactions the purge itself would empty, so deleted_transactions matches what a completed run deletes. It used to count only transactions that were already empty before the run. No action needed.
  • CSV export now quotes a field containing a lone carriage return (for example a table_name or correlation id with an embedded \r), which spreadsheet and Python CSV readers otherwise read as a line break, splitting one audit row into two apparent records. Values unaffected by this are byte-identical to before; for an RFC 4180-compliant reader, a newly-quoted value round-trips unchanged.
  • Rolling back a whole mix threadline.gen.triggers chain whose rerun gave a table its own capture function (redaction, exclusions or store_changed_from) no longer leaves that function behind. The first trigger migration's down now removes it too, as long as no trigger still uses it, and never with CASCADE. For a chain whose first trigger migration was generated by 0.11.2 or earlier, see the Rolling back section of the upgrade guide for the cleanup step.
  • Threadline.Retention.purge/1 now returns a dry_run key on every result map: false for a real purge (previously absent) and true for a dry run (unchanged), and its success type documents it. No action needed unless you match the real-purge result map exactly; a pattern on a subset of keys is unaffected. The :dry_run option doc now also states explicitly that :batch_size and :max_batches are ignored when dry_run: true: the preview was always a single full-table count rather than a batched simulation, so passing either alongside dry_run: true was already a no-op. No action needed.

[0.11.2] - 2026-09-29

This release refreshes a locked dependency to clear published security advisories and changes no public API or configuration.

Breaking changes

None.

Security

  • mint bumped to 1.11.0 (with hpax 1.1.0), fixing three advisories: EEF-CVE-2026-91043 (high; GHSA-9x8p-qrf4-jq7g), EEF-CVE-2026-92103 (GHSA-q95c-ccq6-j5j6) and EEF-CVE-2026-94194 (GHSA-gvrc-75rc-7gj9, response smuggling through chunked framing). Threadline reaches mint only through its optional req dependency (req -> finch -> mint), so the package's own requirements do not change. Applications that depend on req should run mix deps.update mint to pick up the fix.

Changed

  • No library code changed. CI now also builds and tests against the newest stable toolchain, Erlang/OTP 29.1.1 with Elixir 1.20.4 and PostgreSQL 18.6. That lane is tested-on evidence, not a change to the supported floor.

[0.11.1] - 2026-09-27

This release refreshes a locked dependency to clear a published security advisory and changes no public API or configuration.

Breaking changes

None.

Security

  • mint bumped to 1.10.1, fixing a response-smuggling advisory (EEF-CVE-2026-82672 / GHSA-rj5m-69wp-cxq9). Threadline reaches mint only through its optional req dependency (req -> finch -> mint), so the package's own requirements do not change. Applications that depend on req should run mix deps.update mint to pick up the fix.

Changed

  • No library code changed. The supported toolchain range is now tested exactly rather than approximately: CI builds and tests against the pinned Erlang/OTP 27.3.4.15 and Elixir 1.17.3, and the minimum lane runs Erlang/OTP 26.2.5.21 with Elixir 1.15.8 on Ubuntu 24.04.

[0.11.0] - 2026-09-26

Capture now resolves every supported primary-key shape -- single-column, composite, or a configured override -- instead of assuming an id column. History and as-of reads match those keys back exactly, including composite keys given as a map or keyword list. Health reporting finds broken or misconfigured capture, including two tables that were silently sharing one capture function, which this release also fixes for tables regenerated together going forward. See guides/upgrading-to-0.11.md for the full upgrade procedure, including a backfill for existing rows.

Security

Releases up to 0.10.2 could give two audited tables one per-table capture function -- a table in public and a same-named table in another schema (for example public.billing_invoices and billing.invoices), or two long table names that begin with the same bytes. Only tables with per-table capture settings -- redaction (mask or exclude) or store_changed_from -- ever get a per-table function, so a table left on the default trigger was never affected.

Two tables sharing one function had their triggers call whichever table's migration ran last, so writes to the other table were captured under that table's rules: a column one table masks or excludes could be stored in the clear for the other, or the reverse. Rolling back one of the two tables' 0.10.x trigger migrations also drops the shared function with a cascade, removing the other table's trigger along with it.

Upgrading does not rewrite audit_changes. Rows already captured for an affected table before you regenerate its trigger stay exactly as written -- review them and remove or redact any your redaction policy requires.

Detect this with mix threadline.health.coverage, which reports shared_capture_function for each affected table. Fix it by regenerating every affected table together, for example mix threadline.gen.triggers --tables billing_invoices,billing.invoices, then mix ecto.migrate -- migrations generated by this release refuse the unsafe order and apply nothing. See the upgrade guide, guides/upgrading-to-0.11.md.

Breaking changes

Threadline.Health.trigger_coverage/1 — and so mix threadline.verify_coverage and mix threadline.health.coverage — no longer counts a table as covered when its Threadline trigger is disabled (ALTER TABLE ... DISABLE TRIGGER) or enabled only for replica sessions (ALTER TABLE ... ENABLE REPLICA TRIGGER), because writes to it are not being captured. A verify_coverage gate that passed before this change can now fail on upgrade. Fix: ALTER TABLE ... ENABLE TRIGGER (or ENABLE ALWAYS TRIGGER to keep firing for replica sessions too).

Threadline.history/3, Threadline.as_of/4, and row history now raise ArgumentError for an id whose keys do not match the schema's key fields exactly, a nil key value, or a value that cannot be cast to the key field's type. Earlier releases silently returned [] (or {:error, :before_audit_horizon}) for these cases instead of raising. History for a table that has since been dropped or renamed, or a key column that was retyped, keeps working: the key's comparison type falls back to the schema field's Ecto type.

mix threadline.install now rejects flags it does not recognise and stops with an error. Earlier releases ignored every flag, so a script that passed a stray flag ran silently; remove any such flag. Positional arguments are still ignored: a directory or repo is chosen only with --migrations-path or --repo.

mix threadline.gen.triggers migrations now resolve the table's real primary key and require one:

  • Generating triggers for a table with no primary key and no primary_key: override now fails when the migration runs (mix ecto.migrate), instead of silently installing a trigger keyed only on a column named id. The error's hint gives the config to add.
  • A primary-key column whose type has no stable text form for an audit key — timestamptz, numeric, a float, json/jsonb, an array, or bytea — is refused the same way. A table with such a key keeps its existing trigger, capturing exactly as before, until it is regenerated.
  • A primary-key or declared primary_key: column listed in the table's :mask or :exclude is refused: redacting a key column would erase row identity from the audit trail.
  • Once a table's trigger migration refreshes the global capture function to the version that resolves real primary keys, triggers generated by earlier releases on a table with no id column now record table_pk as {} instead of {"id": null}. This affects only rows that never had a usable identity in table_pk; rows with a meaningful key are unchanged.

Required action

Only if mix threadline.verify_coverage now fails on a table it passed before: the table's Threadline trigger is disabled or replica-only. Run ALTER TABLE ... ENABLE TRIGGER (or ENABLE ALWAYS TRIGGER) to restore capture, or remove the table from the expected-table list if it is intentionally not audited.

Existing installs run mix threadline.gen.row_history_index once to add the new audit_changes_row_history_idx index that history, as_of, and row history now rely on (concurrent build; prints recovery steps if a build is left INVALID). A fresh mix threadline.install creates it already.

Only if your scripts pass flags to mix threadline.install: remove the ones it does not document.

Only if your repo sets :priv or is not named Repo: mix threadline.gen.triggers now writes trigger migrations to the repo's own migrations directory, where mix ecto.migrate reads them and where mix threadline.install already wrote. Earlier releases always wrote to priv/repo/migrations, which mix ecto.migrate did not read for such a repo. Trigger migrations written there earlier stay where they are; move them into the repo's migrations directory, or regenerate them, if you want them applied. Otherwise none.

Only if mix ecto.migrate now stops on a trigger migration with a "no primary key" error: add a primary_key: entry under config :threadline, :trigger_capture, tables: %{...} naming a unique index over NOT NULL columns, then rerun mix threadline.gen.triggers --tables <table> and mix ecto.migrate. The error's hint gives a paste-ready snippet when a qualifying index already exists.

Only if mix ecto.migrate now stops on a trigger migration naming a column type such as timestamptz: that table's key has no stable text form for an audit key and cannot be regenerated yet. Its existing trigger keeps capturing under its current (pre-regeneration) behavior; no action is required unless you want the primary key resolved, which is not yet supported for that type.

Only if mix ecto.migrate now stops naming a column in :mask or :exclude: remove that column from the table's redaction rules, or choose a different primary_key: column set, then regenerate.

Only if you match on table_pk = '{"id": null}' for legacy no-id rows: after regenerating that table's trigger, match table_pk = '{}' instead (or match both, since un-regenerated triggers still write the old shape).

Fixed

Threadline.history/3 and Threadline.as_of/4 now return rows for tables with an integer-typed primary key. Earlier releases matched table_pk with a jsonb-containment predicate that silently returned no rows for an integer, string, or keyword-list id on these tables.

Added

Threadline.Health.trigger_findings/1 reports specific, actionable problems with a table's capture trigger, each as a %Threadline.Health.Finding{} with a code, severity, schema, table, message, and details. It is catalog-only (no SET, DDL, or advisory locks), so it is safe to run through PgBouncer transaction pooling and as a role with no table grants. Unlike trigger_coverage/1, which defaults to the "public" schema, it checks every non-system schema by default. The five codes:

  • :legacy_trigger_no_pk_args (warning) — a trigger installed before primary keys were recorded, still keying on id implicitly.
  • :pk_drift (error) — the trigger's recorded key set no longer matches the table's actual (or configured) primary key.
  • :shared_capture_function (error) — more than one table shares a per-table capture function.
  • :duplicate_capture_trigger (error) — more than one Threadline trigger installed on the same table.
  • :capture_trigger_disabled (error) — the trigger is disabled or replica-only, so writes are not being captured.

Each finding's message names the qualified table and an exact fix command. trigger_findings/1 emits telemetry event [:threadline, :health, :findings_checked] with measurements %{errors: n, warnings: n}.

mix threadline.verify_coverage now also fails when trigger_findings/1 reports an :error-severity finding for one of its expected tables, printing warnings and errors on unlisted tables without failing on them.

mix threadline.health.coverage gains a FINDINGS section in its text output, and an additive findings key in its --json output, alongside the existing coverage keys.

Threadline.history/3 and Threadline.as_of/4 accept a composite key as a map or keyword list naming every key field (atom- or string-keyed, any order), and honor a table's primary_key: override, matching its declared columns instead of the schema's Ecto primary key.

mix threadline.gen.row_history_index adds audit_changes_row_history_idx to an existing install, for upgraders whose install predates this index.

mix threadline.install accepts --migrations-path PATH and --repo/-r. --migrations-path is used as given, relative to the current directory, and wins over everything else. --repo picks the repo whose migrations directory is used; it may be given once. Without either flag the directory is chosen as before, from the first repo in :ecto_repos and its :priv setting. In an umbrella, run the task from the child app's directory.

mix threadline.gen.triggers accepts the same --migrations-path PATH and --repo/-r flags and resolves its directory exactly as mix threadline.install does, so both tasks write to and read from one directory.

Trigger migrations now capture a table's real primary key, whatever its name, type, or column count: a single column of any supported name, or a composite key of more than one column. A table with no primary key can declare one with primary_key: in config/config.exs:

config :threadline, :trigger_capture,
  tables: %{"posts_tags" => [primary_key: ["post_id", "tag_id"]]}

enforced at migrate time against a qualifying unique index over the declared columns. See "Primary keys" in the mix threadline.gen.triggers moduledoc.

Changed

In default capture mode, mix threadline.gen.triggers now generates a trigger migration for a table whose trigger name would exceed PostgreSQL's 63-byte identifier limit, instead of failing. The trigger gets the name PostgreSQL would store, threadline_audit_<table> cut to 63 bytes, and a rerun for that table is recognised. Trigger names that fit are unchanged. Tables using redaction rules or --store-changed-from now generate too: a table that is not in the public schema, or whose name is longer than 36 bytes, gets its own capture function whose name ends in a hash. Generated migrations drop a capture function only when no trigger still uses it, and never cascade. A trigger migration name that would exceed 63 bytes is shortened and ends in a hash of its tables.

An invalid or oversized --tables value now stops the task with an error that names the host table and its size in bytes, instead of a stack trace.

When the first repo in :ecto_repos cannot be loaded or its config raises, mix threadline.install and mix threadline.gen.triggers write to priv/repo/migrations, as before, and now print a warning naming the repo.

[0.10.2] - 2026-09-24

mix threadline.install could give two or three of its generated migrations the same version, so mix ecto.migrate refused to run them. Every release through 0.10.1 is affected. Since 0.1.0 the installer has computed each migration's version from the current second, so the audit and semantics migrations shared a version whenever both were written in the same second. The governance migration, written since 0.6.0, could share it too. The installer now gives each migration a distinct version that sorts after every migration already in the directory.

Rerunning mix threadline.gen.triggers for tables that already had a trigger migration, as the redaction drift guides instruct, wrote a migration that could not be applied. Every release from 0.1.0 through 0.10.1 is affected. The generator reused the first migration's name and module, and its trigger statement could not replace a trigger that already existed. A rerun whose table-derived name is already taken now gets a numbered name and module, such as threadline_triggers_posts_2; a rerun for a different table set, and a first run, are named as before. The rerun migration replaces the trigger in place, drops a per-table capture function left behind when a table returns to the default trigger, and rolling it back keeps capture on for tables that already had a trigger migration.

Every generated trigger migration, including a first run, now uses CREATE OR REPLACE TRIGGER (PostgreSQL 14 or later, the supported floor), so it no longer applies on PostgreSQL 13 or older. For each table on the default trigger it also drops a leftover per-table capture function; on a table without one, PostgreSQL prints a harmless NOTICE during mix ecto.migrate.

Breaking changes

None.

Required action

None for an app that has already migrated, including one whose migration files were renamed by hand, because the installer runs once.

If you are on an earlier release and mix ecto.migrate failed with the error below, rename the numeric prefixes of every Threadline migration that shares the duplicated version, including any _threadline_triggers_*.exs migration written by mix threadline.gen.triggers. Keep the audit migration's prefix and give each of the others a distinct, later timestamp so the order is _threadline_audit_schema.exs, then _threadline_semantics_schema.exs, then _threadline_governance_schema.exs, then any _threadline_triggers_*.exs migration, which must run after the audit migration. Then re-run mix ecto.migrate.

None for trigger migrations, unless a rerun of mix threadline.gen.triggers wrote a migration that failed with one of the trigger errors listed under Fixed. That migration never applied: delete its file, upgrade, and run mix threadline.gen.triggers again.

Fixed

  • mix threadline.install no longer writes duplicate migration versions, which made mix ecto.migrate fail with (Ecto.MigrationError) migrations can't be executed, migration version <N> is duplicated. The versions are computed once, before any file is written, and sort after the newest existing migration, including a future-dated one.
  • mix threadline.gen.triggers had the same kind of bug: run in the same second as the installer, its migration could share a version. It now uses the same version logic.
  • The dedicated-schema advice now prints only after a fresh install that wrote all three migrations. A re-run that finds some Threadline migrations already present is told to keep :storage_schema unset, because switching then would split Threadline's tables across two schemas. The fresh-install advice no longer ends with a paragraph that contradicted its own steps.
  • Rerunning mix threadline.gen.triggers for a table that already had a trigger migration now writes a migration that applies. Before, it failed with (Ecto.MigrationError) migrations can't be executed, migration name threadline_triggers_<tables> is duplicated when the rerun listed the same tables and both trigger migrations were pending together (a fresh or CI database, mix ecto.reset, or a rollback across both). Otherwise it failed with trigger "threadline_audit_<table>" for relation "<table>" already exists: on a database that had already applied the first trigger migration, or when the rerun listed a different set of tables, such as posts after posts,users.
  • Rolling back a rerun trigger migration no longer leaves the table uncaptured. The generated migration explains its rollback: capture stays on with the policy the rerun installed, and if the rerun removed redaction rules, a rolled-back rerun continues unredacted until you regenerate the trigger migration.
  • guides/audit-indexing.md, guides/production-checklist.md, guides/how-threadline-works.md and guides/domain-reference.md now state the real storage_schema default, public.

[0.10.1] - 2026-09-22

A patch release that corrects the storage-schema advice mix threadline.install prints on a new install, and the default the configuration reference states for storage_schema. No library behavior changes: an install that followed the getting-started guide, or that ignored the installer's advice, is unaffected.

Breaking changes

None.

Required action

None for most installs. One case needs a check: on 0.10.0, if you ran mix threadline.install with no storage_schema configured, then followed its advice to add config :threadline, storage_schema: "threadline" and re-ran the task, the re-run kept the migrations it had already generated for public. Your config then names a schema your migrations do not create. To fix it:

  • Not yet migrated: delete the three generated *_threadline_*_schema.exs migrations and run mix threadline.install again. The dedicated schema is then frozen into the new migrations.
  • Already migrated: remove the storage_schema key (or set it to "public") so Threadline reads the tables where your migrations put them. Moving them to a dedicated schema is deliberate migration work — see guides/upgrade-path.md.

Fixed

  • mix threadline.install now gives its storage-schema advice after generating, names the migration files it just wrote, and says to delete them before re-running. It gives no advice when every migration already exists, which is an existing install that public already describes correctly.
  • guides/configuration-and-commands.md stated the storage_schema default as "threadline"; it has been "public" since 0.10.0. A test now ties the documented default to the resolved one.

[0.10.0] - 2026-09-22

The public-surface and release-truth release: a documented surface an evaluator can read end to end, a storage-schema default that matches what existing installs already have, and an operator surface with a theme lane and row-level deep links.

Breaking changes

None. No commit in this release carries a breaking-change footer or a breaking-change subject marker, and the one default that did change — the storage schema — was changed toward what every existing install already runs, which is what makes this release non-breaking rather than merely declared so. See "Storage schema default" below.

Required action

Four adopter actions remain. None of them break an install that does nothing, but each one leaves something unchanged that you probably wanted changed.

  • S3 export adopters — the export HTTP client moved from :hackney to {:req, "~> 0.7"}, and the :ex_aws floor rose from ~> 2.4 to ~> 2.7. Swap the dependency and raise the floor in your host mix.exs; a raised floor is not additive.
  • Operator-surface mounters — two new routes, POST <path>/theme and <path>/rows/:table/:record_id, must pass any method allowlist, proxy rule, or Content-Security-Policy in front of your /audit mount.
  • Custom Threadline.Storage adapters — the Threadline.Storage.put/2 callback narrowed to binary content. This is visible to Dialyzer with no runtime change; update your adapter's typespec.
  • Callers of implementation modules — 25 implementation modules became @moduledoc false. They remain callable for Threadline's own composition, but they are no longer a supported surface.

Storage schema default

Threadline-owned tables, functions, and triggers now default to the host's public schema. A dedicated schema became an explicit opt-in: config :threadline, storage_schema: "threadline", set before mix threadline.install.

  • Existing installs need no action. The default now matches the schema your install already generated into, so the read paths and your already-deployed triggers keep agreeing about where Threadline-owned objects live. This is settled by the change in this release, not by an earlier one.
  • New installs should opt in. The default no longer provides schema isolation. Choose storage_schema before you run the installer — the choice is frozen at generation time, and changing it later is deliberate migration work rather than a runtime config edit.

Added

  • Architecture documentation — rewrote How Threadline Works as an end-to-end visual architecture guide and added a source-driven Code Walkthrough, with dark/light Mermaid rendering and the Threadline mark as the HexDocs favicon.
  • Operator-surface theme lane — threadline_operator_surface/2 accepts :theme (:dark | :light | :system, default :dark), backed by a session-persisted picker served from the new POST <path>/theme route.
  • Row-level history deep links — <path>/rows/:table/:record_id addresses a single audited row's history directly, rather than only through a transaction.

Changed

  • Documented surface — generated module documentation now focuses on supported façades, returned data, extension points, integrations, the operator Threadline.OperatorSurface.Router and Threadline.OperatorSurface.Auth boundary, and adopter Mix tasks. The following implementation modules that had pages in the 0.9 documentation are no longer listed:

    • Capture and lifecycle implementation: Threadline.Capture.Migration, Threadline.Capture.RedactionPolicy, Threadline.Capture.TriggerCaptureConfig, Threadline.Capture.TriggerSQL, Threadline.Export.CleanupTask, Threadline.Governance.ExportJob, Threadline.Governance.Migration, Threadline.Governance.RetentionRun, Threadline.Governance.SavedView, Threadline.Policy.RedactionPresenter, Threadline.Retention.Pruner, and Threadline.Semantics.Migration.
    • Operator implementation: Threadline.OperatorSurface.Style, Threadline.OperatorSurface.Script, Threadline.OperatorSurface.Scope, Threadline.OperatorSurface.SessionPlug, Threadline.OperatorSurface.ExportAuthPlug, Threadline.OperatorSurface.Components.SurfaceHeader, Threadline.OperatorSurface.Controllers.ExportController, Threadline.OperatorSurface.Coverage.OnMount, Threadline.OperatorSurface.Coverage.Snapshot, Threadline.OperatorSurface.Exports.Filename, Threadline.OperatorSurface.Exports.FilterParams, Threadline.OperatorSurface.Live.ActorLive, and Threadline.OperatorSurface.Live.TransactionLive.

    These modules remain callable for Threadline's own composition; this is a documentation-surface clarification, not runtime privacy or a change to supported façade behavior.

  • S3 export HTTP client — :hackney gave way to {:req, "~> 0.7"} and the :ex_aws floor rose to ~> 2.7. Both remain optional dependencies; only hosts that export to S3 are affected.

  • Threadline.Storage.put/2 — the first argument narrowed from a path or content union to binary content.

0.9.0 (2026-06-03)

Features

  • operator-surface: first-class positioning + accessibility pass (#16) (4bf1a07)

[0.8.0] - 2026-06-03

Operator-surface release: the /audit admin UI matured into a coherent, branded operator console, backed by a fully automated (zero-human-verification) test gate.

Added

  • Operator surface overhaul — dark "night infrastructure" theme; a Home task-launcher (Find / Verify / Prove) as the default /audit page; copy-to-clipboard affordances for correlation and transaction ids; evidence verdicts (Proven / Inferred / Unsupported) with drill-down history; forward "completion" links so every Verify/Prove screen reaches a done state; a scoped-view indicator and scope-aware empty states for support-read-only operators; explicit "all clear" success states; and restrained, brand-coherent motion.
  • Asset/CSP controls — config :threadline, operator_surface_embed_scripts: false opts out of the embedded (zero-dependency) copy-to-clipboard helper; the new "Assets and Content-Security-Policy" section in guides/operator-surface.md documents the inline style/font/script embeds and CSP guidance.

Changed

  • Design system — consolidated onto tokenized status stripes and a letter-spacing scale, a canonical metric card (tl-card--metric + [data-status]) and metadata row (tl-meta), and ARIA-driven selected/active state.
  • CI / quality — official GitHub Actions bumped to their Node 24 majors ahead of GitHub's forced migration; an opt-in/nightly flake-detection gate; and test-determinism hardening across the suite. The operator-surface behaviors are locked by deterministic LiveView + Playwright assertions that gate every PR.

0.7.0 (2026-05-30)

Features

  • documentation: add Configure Threadline subsection to getting-started (7b929a1)
  • documentation: add host-repository wiring prerequisite to the production checklist (a07775d)
  • operator surface: wire the schemas mount and synchronize operator-surface examples (17507ba)
  • auth integration: add the PhxGenAuthReference.Audit guide module (2836be1)

Bug Fixes

  • ci: format the phx-gen-auth integration contract for the ci.all gate (e04f275)
  • operator timeline crash on correlation_id filter (+ release-please changelog guard) (43a6f23)
  • operator-surface: prevent timeline crash on correlation_id filter (d62e509)
  • release: run publish chain when release-ref succeeds via dispatch (19c7549)
  • release: use hex.build preflight instead of verify.release in CI (d4413ef)

[0.6.0] - 2026-05-27

Threadline 0.6.0 is the adopter-ready release: it packages the in-repo stack since 0.5.0 — the Evidence plane (Threadline.Evidence, proof vocabulary, /audit/evidence), the blessed audited write path (Threadline.Audit.transaction/3), and operator/demo surfaces from the realistic walkthrough — so Hex evaluators and pilot hosts see the same truth the library already ships in-tree.

Added

  • Evidence plane — Threadline.Evidence, Threadline.Evidence.Proof, Threadline.Evidence.Subject, evidence persistence schema, and mix threadline.evidence.show for machine-readable proof export.
  • Audited write path — Threadline.Audit.transaction/3 as the blessed helper wrapping capture + semantics in one transaction.
  • Operator and evidence surfaces — /audit/evidence LiveView, host-owned evidence_authorize_fn (not inherited from /audit auth), and viewer parity with coverage/policy Mix tasks.
  • Reference composition (sigra-reference) — example app and maintainer walkthrough demonstrate end-to-end audited writes and evidence mounts; see examples/threadline_phoenix/README.md and walkthrough docs.

Changed

  • Public documentation and evidence-plane contract — Evidence-plane contract locks across public docs: canonical non-goals list, shared verdict vocabulary, and narrower /audit/evidence support language. Public guidance treats /audit/evidence as a separately authorized capability under the phoenix-surface lane instead of a blanket /audit inheritance claim.
  • Release metadata — install snippets target {:threadline, "~> 0.6"}; Hex metadata and adoption-pilot distribution preflight align with 0.6.0.

Deprecated

  • Manual SET LOCAL GUC recipes and hand-rolled record_action/2-only write paths remain supported as legacy escape hatches; new code should prefer Threadline.Audit.transaction/3.

Breaking

  • None for existing capture-only and phoenix-surface adopters who do not opt into Evidence or the audited write helper.

Upgrade from 0.5.x

  • Bump dependency to {:threadline, "~> 0.6"} in host mix.exs.
  • Run mix deps.get and mix deps.compile.
  • If using Evidence: apply evidence schema migrations from library docs / example migrations before calling Threadline.Evidence APIs.
  • Wire evidence_authorize_fn on threadline_operator_surface/2 when mounting /audit/evidence — it does not inherit timeline/export authorize_fn.
  • Adopt Threadline.Audit.transaction/3 for new write paths; keep legacy GUC/record_action/2 only where migration cost is high.
  • Use mix threadline.evidence.show (not the earlier verify.evidence alias) for CLI proof export.
  • Re-run host verification: mix threadline.verify_coverage, mix verify.doc_contract (host), and operator-surface smoke tests if mounted.
  • See guides/upgrade-path.md for lane matrix (capture-only, phoenix-surface, phx-gen-auth-reference, sigra-reference) and surface deprecation policy.
  • ExDoc sidebar adds Evidence group and Core API entries for Audit, Query, Investigation, ChangeDiff.
  • Maintainer pre-flight before tag: mix verify.release on a clean tree (see CONTRIBUTING.md).
  • Apply evidence migrations before enabling /audit/evidence in production — schema must exist before first proof query.
  • Deny /audit/evidence with the same host-owned auth UX as timeline/export; do not rely on blanket /audit session checks.
  • Correlation filter semantics on timeline/export are unchanged from 0.5.x — no migration needed for existing query params.
  • Re-run mix threadline.gen.triggers after evidence schema changes if audited tables gain new columns.
  • Confirm evidence_authorize_fn returns explicit deny reasons for operator logs — inherited /audit auth is not sufficient.

[0.5.0] - 2026-05-08

Threadline 0.5.0 is the integration-breadth release: the package now ships a narrower and more honest support matrix, a first-party Sigra/Phoenix reference path, canonical admin and support-read-only operator-surface mount recipes, an explicit threadline_web extraction-readiness scorecard with a documented stay-in-tree decision, and a repaired shared authorization/scope contract that keeps auth and scoping host-owned across timeline, actor, transaction, and export flows.

Added

  • Upgrade-path guide at guides/upgrade-path.md — canonical lifecycle policy for the optional Phoenix/LiveView/HTML/PubSub surface. It distinguishes capture-only from surface-mounted, documents the supported compatibility matrix from declared deps + current lock resolution + CI coverage, and locks the surface-only deprecation/removal overlap policy in one place.
  • Integration breadth guide at guides/integration-contracts.md — canonical host-integration contract for actor extraction, additive audit context, optional dependency posture, operator-surface composition, and fallback CLI workflows.
  • Support-matrix closeout — the project now names only the three proven lanes capture-only, phoenix-surface, and sigra-reference, and the compile-without-optional / example / doc-contract proof chain is locked to those claims.
  • Sigra/Phoenix reference refresh — the first-party Sigra integration path and example app were refreshed to the current supported lines while keeping Sigra a soft dependency.
  • Canonical access-tier runbooks — docs, example code, and tests now prove one shared host-owned authorize_fn contract, one host-owned scope_query_fn seam, and a real support-read-only exports: false/scoped-query story across the operator surface.
  • Packaging Boundary Scorecard — guides/upgrade-path.md now records the explicit threadline_web extraction-readiness rubric and the current answer: stay in-tree for now.
  • Coverage dashboard at /audit/coverage — polled three-bucket coverage view with a surface-header pill on every operator-surface LV. ?schema=NAME URL param for multi-schema adopters; manual Refresh affordance with cancel-and-reschedule timer semantics; on-poll-error UX that keeps the last-good snapshot and ALWAYS reschedules.
  • mix threadline.health.coverage parity Mix task with --json and --schema=NAME flags. Viewer-only — always exits 0; the CI gate remains mix threadline.verify_coverage.
  • Policy redaction drift viewer at /audit/policy/redaction — read-only configured-vs-deployed redaction reconciliation with the three operator-safe states Config matches deployed, Drift detected, and Could not introspect. The surface never shows sample values; it exposes only column names and placeholder metadata, and drift/introspection failures instruct operators to rerun mix threadline.gen.triggers and apply the migration.
  • mix threadline.policy.show parity Mix task with --json. Default output prints one summary line plus an aligned TABLE / STATUS / CONFIG / DEPLOYED / HINT table; --json emits the same stable state taxonomy as config_matches_deployed, drift_detected, and could_not_introspect. Viewer-only — drift does not exit non-zero by itself.
  • Threadline.Health.trigger_coverage/1 :schema opt (default "public"). Both inner SQL queries are now parameterized; the pg_trigger/pg_class query gains a pg_namespace join so cross-schema results no longer leak into the covered set. Programmatic callers are responsible for sanitizing :schema; surfaces that take untrusted input validate at the edge.
  • Three-bucket return shape on Threadline.Health.trigger_coverage/1 — [{:covered | :uncovered | :expected_uncovered, name}]. The third bucket is hardcoded to ["schema_migrations"] plus config :threadline, :health, expected_uncovered_tables: [...], with :audit_anyway removing entries. Existing pattern-match callsites (Continuity.assert_capture_ready!/2, TimelineLive datalist) remain unchanged — the third tuple variant is purely additive.

  • Threadline.Health.Policy.validate!/1 — pure-stdlib config validator mirroring capture-time redaction validation. Validate at boot to fail loud on bad config.
  • [:threadline, :health, :checked] event metadata gains expected_uncovered measurement key (additive). Old subscribers reading only covered/uncovered keep working unchanged.
  • [:threadline, :health, :checked, :error] sibling event for polled coverage check failures.
  • mix threadline.verify_coverage --schema=NAME additive flag with the same edge validation contract as the new Mix task. Default behavior unchanged.

Changed

  • Release metadata — install snippets now target {:threadline, "~> 0.5"}, ExDoc names the operator surface Optional In-Tree, and the release/audit artifacts record the integration-breadth release boundary.
  • Operator-surface auth/scoping contract — the example app no longer relies on a socket-only auth bypass, and timeline, actor, transaction, and export flows all consume the same host-owned scope seam.
  • Threadline.Verify.CoveragePolicy.violations/2 treats {:expected_uncovered, _} as covered-equivalent for tables not in the adopter's :expected_tables. Existing semantics preserved for tables IN :expected_tables.

[0.4.0] - 2026-05-06

Threadline 0.4.0 is the operator-surface foundation release: an opt-in web UI ships behind optional Phoenix/LiveView/HTML/PubSub deps so capture-only adopters keep zero new transitive bloat, the timeline / export query and Mix-task surface gains a :correlation_id filter that walks audit_actions.correlation_id via the action linkage, and exports learn an opt-in action-metadata pair (JSON action object, CSV include_action_metadata: true) so incident-response tooling can correlate rows back to the action that produced them — all without changing the default column order or breaking pre-0.4 callers.

Added

  • Operator Surface — introduces an opt-in web UI via Threadline.OperatorSurface.Router. phoenix, phoenix_live_view, phoenix_html, and phoenix_pubsub are now declared as optional: true dependencies, meaning zero bloat for capture-only adopters. Hosts that want the UI must add these dependencies to their mix.exs and use the threadline_operator_surface mount macro in their router.
  • examples/threadline_phoenix — audit_transaction_id on POST /api/posts and GET /api/audit_transactions/:id/changes returning ordered changes with change_diff maps per row (composition demo; add auth in production). guides/domain-reference.md documents the pattern under COMP-EXAMPLE-INCIDENT-JSON.
  • :correlation_id timeline / export filter — optional keyword on Threadline.Query.timeline/2, timeline_query/1, export_changes_query/1, and export entrypoints. Values are trimmed; empty after trim, nil, non-binary, or longer than 256 UTF-8 bytes raise ArgumentError. When set, only audit_changes whose transaction has a matching audit_actions.correlation_id (via action_id) are returned (inner join; omit the key for previous behavior). See Threadline.Query moduledoc for full rules.
  • Export JSON action object — each change may include "action": {"id", "correlation_id"} when the transaction is linked to an audit_actions row.
  • Export CSV include_action_metadata: true — opt-in trailing columns correlation_id and action_id; default CSV column order is unchanged.
  • guides/adoption-pilot-backlog.md — matrix aligned to the production checklist for host pilots, plus distribution preflight and prioritized issue rows.
  • Telemetry (operator reference) — [:threadline, …] event table in guides/domain-reference.md, linked from guides/production-checklist.md observability section.

Changed

  • README — Documentation list includes the adoption pilot backlog; ExDoc extras include the new guide.

[0.3.0] - 2026-05-05

Threadline 0.3.0 is the drop-in production adoption release for Phoenix SaaS teams: the release packages the first-hour SaaS onboarding path, Sigra-ready actor capture, operator incident guidance, and published capture baselines into one taggable Hex surface.

Added

  • SaaS onboarding route — guides/getting-started-saas.md ships as the first-hour Phoenix SaaS path and is now promoted from the package front door.
  • Sigra integration route — guides/integrations/sigra.md ships as the best-supported auth bridge for Sigra-backed Phoenix hosts and is surfaced separately in ExDoc navigation.
  • Published capture baselines — the cold-single-table benchmark now anchors the release story with insert at 4.87 K IPS / 205.13 µs, update at 4.30 K IPS / 232.49 µs, and delete at 7.61 K IPS / 131.39 µs.
  • Release-surface contract — test/threadline/release_artifact_contract_test.exs locks package files, ExDoc extras/module grouping, guide presence on disk, and release-only README / maintainer literals.

Changed

  • README install and routing — the install snippet now targets {:threadline, "~> 0.3"} and sends new adopters first to the SaaS quickstart and Sigra guide, with performance and incident docs one step deeper.
  • ExDoc information architecture — guides/integrations/sigra.md now matches an Integrations extras group before the broader reference bucket, and Threadline.Integrations.Sigra now appears under a new plural Integrations module group while Plug / Job / Health / Continuity / Telemetry remain under the singular Integration group.
  • Release pre-flight — mix verify.release now validates the exact taggable tree through release metadata checks, pure file-read release contracts, MIX_ENV=dev mix docs, and mix hex.build.
  • Maintainer publish runbook — CONTRIBUTING.md now documents the mix verify.release pre-flight and the main CI wait before tagging v0.3.0.

Deprecated

  • No runtime API is deprecated in 0.3.0. Older install snippets using {:threadline, "~> 0.2"} should be treated as stale documentation, not a supported release target.

Breaking

  • No breaking runtime, dependency, config, or schema changes are introduced in 0.3.0.

Upgrade from 0.2.x

  • Dependencies: no runtime dependency changes are required to adopt 0.3.0.
  • Config changes: none.
  • Migration steps: none beyond bumping the dependency and re-running your normal dependency fetch/docs sync flow.
  • Sigra adapter: use Threadline.Integrations.Sigra.actor_ref_from_conn/1 as the Threadline.Plug actor_fn when your host already authenticates requests with Sigra.

[0.2.0] - 2026-04-23

Added

Changed

  • Timeline filter errors — validate_timeline_filters!/1 messages now point at allowed keys and Threadline.Export.
  • Validation order — timeline/2 and export entrypoints validate filter keys before resolving :repo, so unknown keys surface before a missing-repo error.

Release notes (capabilities since 0.1.0)

This minor release documents and packages capabilities shipped after 0.1.0 that were not fully reflected in that changelog entry:

[0.1.0] - 2026-04-23

Added