PropertyDamage.Invariants.Invariant (PropertyDamage v0.2.0)

View Source

A first-class invariant: the named property a set of assertions verifies (DR-026).

An assertion does not stand alone; it checks an invariant. The invariant is the stable, named thing a model promises to uphold, and one invariant may be checked by more than one assertion (for example a synchronous @trigger and a temporal @poll_state). Giving invariants identity lets the framework build a single authoritative catalog and report which invariants a run actually exercised (anti-vacuity coverage).

Fields

  • :id - the canonical, unique identifier. This is the linking, uniqueness, lookup, and coverage-rollup key. It is an atom and unique within a projection.
  • :name - a human-readable display label. Defaults to :id; it earns its keep only when id is opaque (e.g. a ticket key like :"JIRA-1234").
  • :description - an optional sentence describing the property.

There is deliberately no :kind field. Safety-versus-liveness is a property of a check, not of an invariant (one invariant may have both a synchronous and a polling check); it is surfaced in the catalog, not stored here.

Declaration

Invariants are declared per projection, either centrally with an accumulating module attribute whose value is new!/1's own argument list:

@invariant id: :balance_nonneg, description: "Balance never drops below zero"

or inline on the assertion that checks them:

@trigger every: 1, id: :balance_nonneg, description: "..."
def assert_balance(state, _), do: ...

Other assertions link to a declared invariant with validates: :id. An assertion with neither id: nor validates: validates an invariant whose id is the assertion's own (assert_-stripped) name, so every existing assertion owns a same-named invariant by default.

Summary

Functions

Resolve the %Invariant{} for an id.

Build an %Invariant{} from a keyword list, validating shape.

Types

t()

@type t() :: %PropertyDamage.Invariants.Invariant{
  description: String.t() | nil,
  id: atom(),
  name: atom()
}

Functions

fetch!(id, ctx)

@spec fetch!(atom(), map()) :: t()

Resolve the %Invariant{} for an id.

By default reads the owning projection's compile-time registry (projection.__invariants__/0), found under ctx.projection. Raises on an unknown id -- a condition compile-time validation makes unreachable post-compile, so failing fast is correct.

This is the single resolution seam. A configurable resolver MAY later override the description (and only the description) at report time, best-effort with fallback to the local description; that config plumbing is deferred (DR-026 §9). Resolution is lazy (report/catalog time only): never compile-time, never on the run hot path, and it never feeds generation, shrinking, or assertion logic.

new!(opts)

@spec new!(keyword()) :: t()

Build an %Invariant{} from a keyword list, validating shape.

  • :id (required) must be a non-nil atom.
  • :name (optional) must be an atom; defaults to id.
  • :description (optional) must be a binary or nil.

Raises ArgumentError on malformed input. This is the single validating code path: both the centralized (@invariant) and inline (@trigger ... id:) declaration sites build through it.