Threadline.ChangeDiff (Threadline v0.4.0)

Copy Markdown View Source

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). Phase 31 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

Opdata_afterchanged_fields / changed_fromDefault field_changes
INSERTrow snapshotN/A for delta semantics[] (row snapshot is authoritative via "data_after")
UPDATErow after imagecapture-driven deltaOne entry per name in changed_fields (only), sorted by "name"
DELETEnilN/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).

Summary

Functions

Builds a deterministic map for audit_change.

Functions

from_audit_change(ch, opts \\ [])

@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 in Phase 31 unless a later phase adds optional preload parameters.