# `Threadline.ChangeDiff`
[🔗](https://github.com/szTheory/threadline/blob/v0.9.0/lib/threadline/change_diff.ex#L1)

Pure projection of a single captured row change into deterministic, JSON-friendly maps.

## Authority

`from_audit_change/2` is a **pass-through** of persisted `audit_changes` columns only.
It does not query the database, re-apply `Threadline.Capture.RedactionPolicy`, or invent
values that capture did not store. Low-information rows (masked columns, sparse
`changed_from`) are expected and honest.

Relationship to **`Threadline.Export`**: the `:export_compat` format mirrors the
**base** string-key map produced by export's internal `change_map/1` (`"op"`,
`"data_after"`, `"changed_fields"`, `"changed_from"`, identifiers).
**omits** nested `"transaction"` and `"action"` objects; full export rows may still
include those when preloaded. Use export for CSV/NDJSON documents; use this module
when you need **`field_changes`** or the same triple without join metadata.

## `schema_version`

The primary wire map includes `"schema_version" => 1`. Future additive fields may
bump this integer; consumers should treat unknown keys as forward-compatible.

## INSERT / UPDATE / DELETE matrix

| Op | `data_after` | `changed_fields` / `changed_from` | Default `field_changes` |
| -- | ------------ | ----------------------------------- | ------------------------ |
| INSERT | row snapshot | N/A for delta semantics | `[]` (row snapshot is authoritative via `"data_after"`) |
| UPDATE | row after image | capture-driven delta | One entry per name in `changed_fields` (only), sorted by `"name"` |
| DELETE | `nil` | N/A | `[]` — no synthetic per-column removals without a stored pre-image |

## `before_values` and per-field prior epistemics

- **`"none"`** — `changed_from` is `nil`: integrator did not store before-values.
  UPDATE field entries **omit** `"before"` / `"prior"` keys entirely (only `"after"`).
- **`"sparse"`** — `changed_from` is a map (including `%{}`): prior values may be
  incomplete. If a column in `changed_fields` has **no** key in `changed_from`, the
  field object includes **`"prior_state" => "omitted"`** instead of inventing a scalar.
  When the key exists, **`"before"`** reflects JSON truth (including JSON `null`).

## `except_columns` and masking

UPDATE projection iterates **only** `changed_fields` (nil-safe), not
`Map.keys(data_after)`, so columns present in `data_after` but excluded from capture's
delta lists do not appear as false positives.

Values in `data_after` / `changed_from` pass through unchanged—including stable
placeholders such as `:mask` where capture stored them (same class of values as
export JSON).

## Options

- `:format` — set to `:export_compat` for the export-aligned flat map (see `from_audit_change/2` `@doc`).
- `:expand_insert_fields` — default `false`; when `true` on INSERT, derives
  `"kind" => "set"` field rows from keys in `data_after` only (presentation-only; not
  per-field capture facts).

# `from_audit_change`

```elixir
@spec from_audit_change(
  Threadline.Capture.AuditChange.t(),
  keyword()
) :: map()
```

Builds a deterministic map for `audit_change`.

## Primary format (default)

String keys throughout, including `"schema_version"`, `"before_values"` (`"none"` or
`"sparse"`), `"field_changes"` (lexicographically sorted by `"name"`), and core row
identifiers compatible with integrator expectations (`"op"`, `"id"`, `"transaction_id"`,
`"table_schema"`, `"table_name"`, `"table_pk"`, `"captured_at"` as ISO-8601 UTC,
`"data_after"`).

## `:export_compat`

When `opts` contains `format: :export_compat`, returns a **single flat** string-key map
aligned with **`Threadline.Export`** `change_map/1` **base** fields: `"id"`,
`"transaction_id"`, `"table_schema"`, `"table_name"`, `"op"`, `"captured_at"`,
`"table_pk"`, `"data_after"`, `"changed_fields"`, `"changed_from"`. IDs are coerced
with `to_string/1`; `table_pk` defaults to `%{}`, `changed_fields` to `[]`,
`changed_from` to `%{}` when nil. Nested `"transaction"` and `"action"` are **not**
included unless future versions add optional preload parameters.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
