Changelog
View SourceAll notable changes to PropertyDamage will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.2.0 - 2026-06-25
This cycle made the headline features that 0.1.0 advertised actually work end to end, and trimmed the documented surface to what has been validated.
Added
- Lifecycle-boundary assertions via a new
at:timing on@trigger(DR-024).@trigger at: :teardownevaluates a synchronous assertion exactly once on the fully-settled final projection state (after both@poll_stateand resource pollers have finalized, beforeAdapter.teardown/1);@trigger at: :startupevaluates it once on the initialinit/0state before the first command. This gives a declarative safety primitive ("never exceeds N", "applied at most once") to complement@poll_state's liveness: a@poll_statepredicate resolves on the transient pass through the expected value and stops watching, so it cannot express a bound that is only violated later, whereas the settled checkpoint sees the persistent overshoot. Detection rests on the projection accumulating evidence (a maximum, a sticky flag, a count) rather than snapshotting the latest value (the "accumulator contract", documented in the projection moduledoc and the eventual-consistency guide). An assertion carries exactly one timing (every:xorat:, enforced at compile time); a failing:startupcheck halts before command 1; a@poll_stateliveness timeout preempts the:teardowncheckpoint; andAdapter.teardown/1always runs so a failing safety check never leaks SUT resources. Violations report as the assertion's named synchronous failure, distinct from a poll timeout. - Continuous async-observation checking (DR-025). A
@trigger every:assertion now fires on every observed event, including events observed asynchronously rather than returned by a command: resource-poller and injector-adapter events, mock-service events, and nemesis (command-injected) events, plus events folded during the finalize-time drains. This makes the documented@trigger every: :event("after any event") contract true; until now those asynchronous observations were silently skipped. A violation is reported at the offending event, carrying that event'scommand_index, so the shrinker converges to a tight reproduction instead of only surfacing the failure at theat: :teardownsettled checkpoint. There is no new trigger surface and no opt-in flag;@trigger every: :commandremains the opt-out for assertions that should fire only after commands. Behavior change: a@trigger every:assertion that previously ran only on a command's own events now also runs on asynchronously-observed events of a matching kind. The shrinker's failure signature now distinguishes assertion failures by name, so distinct assertions are no longer conflated during shrinking (an async-observed failure stays equivalent to a:teardownfailure of the same assertion). - Invariant catalog and anti-vacuity coverage (DR-026). Assertions now validate
first-class invariants with a stable identity
(
%PropertyDamage.Invariants.Invariant{id, name, description}). A projection declares invariants centrally with an accumulating@invariant id: …, description: …attribute, or inline on an assertion withid:; other assertions link to one withvalidates: :id. An assertion with neither owns a same-named invariant by default, so existing models gain a populated catalog with no changes. Identity is per projection: ids are unique within a projection andvalidates:resolves locally, checked at compile time (duplicate id and danglingvalidates:areCompileErrors; an invariant with no check warns as statically vacuous).PropertyDamage.Model.assertion_catalog/1returns the model-wide catalog keyed{projection, id}with each invariant's checks and per-check kind (:synchronous/:lifecycle/:polling). The engine records per-assertion firing across the whole run at every evaluation site (synchronousevery:, lifecycleat:, async observations, and@poll_statespawn, where spawning counts as firing), exposed on the result asassertion_fires.PropertyDamage.assertion_coverage(result, model)joins those firings against the catalog with no re-execution, reporting which invariants were actually exercised: an@trigger every: RareEventthat never triggers was a silent vacuous pass and is now visibly uncovered. A verbose run prints a terseInvariants: N/M exercisedfooter;Coverage.meets_threshold?(tracker, assertion_coverage: 100)fails CI on any uncovered invariant; failure reports headline the invariant's name and description with the failing check as secondary detail. This also completes the previously-stubbedCoverage"check coverage" (per-assertion fire counts now populatecheck_hits) and makes the long-documentedcoverage: truerun option real: it accumulates the heavier command/transition/state dimensions across all generated sequences (attached to the success stats as:coverage), where before they silently reflected a single representative sequence. The additive surface (@invariant,validates:, inlineid:/description:,assertion_fires,coverage: true) is backward compatible. mix pd.replay <failure-file> [--verbose]replays a saved.pdfailure against the SUT. It loads the failing run (model and adapter are read from the file itself, so no flags are needed), re-executes the shrunk sequence through the real engine, prints each step and a verdict, and reports the outcome as an exit code:0when the bug is fixed (good),1when it reproduces (bad), and125when the replay could not run at all (the project does not compile, the file fails to load, it records no model/adapter, or the sequence is branching). The125case is indeterminate rather than a reproduction, which is exactly the "skip" signalgit bisect runneeds, so the task drops into a CI gate orgit bisectdirectly. A thin shell overPropertyDamage.load_failure/1andPropertyDamage.replay/2; use those for custom adapter config or stutter.mix pd.bisect <failure-file> --good <ref> [--bad <ref>] [--verbose]finds the first commit where a saved failure starts reproducing, by drivinggit bisectand replaying the failure at each candidate commit (classified viamix pd.replay's0/1/125exit code, so un-runnable commits are skipped, not blamed). It validates a clean working tree up front, copies the.pdfile outside the tree so it survives checkouts, and always runsgit bisect resetat the end. It replays the saved concrete shrunk sequence (not a re-generation from the seed), so the search is robust across commits that changed generators, weights, orwhen:predicates (DR-023).mix pd.reshrink <failure-file> [--strategy quick|thorough|exhaustive] [--max-iterations N] [--max-time-ms N] [--output PATH | --overwrite]re-runs the shrinker over a saved.pdfailure with a larger budget, to squeeze out reductions the original run missed. It prints the before/after command counts and, by default, writes nothing;--output/--overwritepersist the smaller report to an explicit location. Re-shrink is not a pass/fail gate, so it exits zero on any successful run (reduced or already minimal) and non-zero only on a real error. A thin shell overPropertyDamage.load_failure/1andPropertyDamage.shrink_further/2; use the latter for a custom adapter config.- Unified progress reporting (DR-022): all long-running operations
(
PropertyDamage.run/1,PropertyDamage.Mutation.run/1,PropertyDamage.Differential.run/1, and load tests) now report through a single derived projection, a%PropertyDamage.Progress{}value fanned out to zero or more consumers. Each operation accepts anon_progress:consumer and emits coarse[:property_damage, <operation>, :progress | :result]telemetry events (<operation>is:test_run,:load_test,:mutation, or:differential), additional to and distinct from the existing fine-grainedrun/1spans. With no consumers attached (verbose off, noon_progress:, no telemetry handler), no%Progress{}is built (zero cost on the hot path).Differential.run/1gained anon_progress:option. external()server-generated field markers now work end to end (DR-021): placeholders are created during generation, transported to execution via theSequenceregistry, captured by the producing command's structured position, and remapped through shrinking. New consumer-routing helpersPropertyDamage.Generator.available_externals/2andexternal_from/2.external()values are now captured from events emitted mid-execution viactx.inject(not just events returned fromexecute/2), so a producer can inject its server-generated id and downstream commands resolve it.- The model-free
PropertyDamage.execute/2path now resolvesexternal()values across commands: a consumer carrying a%Placeholder{}for an earlier producer's field receives the captured concrete value. PropertyDamage.Differential.run/1and load tests now capture and resolveexternal()values across commands too (DR-021), so command sequences that chain a server-generated id work on every execution path. Differential keeps a per-target registry, so the same consumer resolves to each adapter's own value; the load test worker resolves per worker. Previously differential passed unresolved placeholders straight through and the load test worker raised on the first one.- Decision Records under
docs/decisions/(DR-001–DR-026). credoas a dev/test lint (non-blocking in CI);PlaceholderRegistry.resolve/3.- Documentation of the command sequence generation loop in the
PropertyDamage.Modelmoduledoc. - New guide: "Building Reusable Components" (
guides/reusable_components.md). - New guide: "Mutation Testing" (
guides/mutation_testing.md). - Seed library replay (DR-023):
PropertyDamage.run/1gained a top-levelseed_library:option (false(default) /true/ path) andseed_library_prune_after:(default 3). When enabled, previously-failing seeds are replayed before random exploration; a still-failing replay halts the run with a shrunk report and a summary, all-passing replays proceed to exploration, and a new exploration failure's seed is appended (deduplicated). The library is an ephemeral, self-pruning working set (aconsecutive_passesstreak per entry, pruned afterKpasses), not a durable corpus — export to ExUnit for durable regressions. The replay phase reports through the unified progress projection via a newReplayUpdatepayload and prints an unconditional banner (and a halt summary) to stdout.
Changed
- BREAKING: The load test's
on_metrics:andon_complete:options are removed in favor ofon_progress:, which receives%PropertyDamage.Progress{}values (periodicLoadUpdatesnapshots and a terminalLoadResult).metrics_interval:is retained as the snapshot cadence. - BREAKING:
PropertyDamage.Mutation.run/1'son_progress:now receives a%PropertyDamage.Progress{}(aMutationUpdateper mutation, then a terminalMutationResult) instead of a raw result map. verbose:output forrun/1,Mutation.run/1, andDifferential.run/1is now produced by a built-in progress consumer rather than inline printing; the printed output is unchanged.- A command spec's
with:override that targets a field the command does not define now raises a clearArgumentErrornaming the command and the offending field(s), instead of an opaqueKeyErrordeep inside generation. Such an override never took effect (the generated map is built into the command struct, which rejects unknown keys), so this surfaces a silent misconfiguration early. - BREAKING: Renamed
state_projection/0tocommand_sequence_projection/0(clearer name: returns the projection used for command sequence generation). - BREAKING: Renamed
extra_projections/0toassertion_projections/0(clearer name: these projections verify invariants). - BREAKING: Removed the weight-first
{weight, Module}command-spec form. It was undocumented, absent from thecommand_spectypespec, and inconsistent with every other (module-first) form. Use{Module, weight: n}(or{Module, weight}).mix pd.scaffold/mix pd.gen.modelnow emit the keyword form, and all moduledoc examples were updated. - Sequence generation is now a pure function of the run seed (seeded
StreamData), so a reported seed reproduces the failing sequence exactly. - Probe/async settle behaviour is sourced from the command spec (DR-019) at execution time.
- Trimmed the README, feature list, and docs to the validated surface. Several
modules (load testing, mutation testing, invariant suggestions, failure
intelligence clustering/verification, production forensics, flakiness
detection, and the telemetry dashboard) are documented
as work in progress and grouped separately; the inaccurate "AI-powered"
framing of
Suggestionswas removed and a chaos/Toxiproxy caveat added to the nemesis docs. ex_doc modules are now grouped by tier and all guides are surfaced. - Guides use seeded selection (
StreamData.member_of) instead ofEnum.random, and validexternal()struct syntax. - BREAKING:
PropertyDamage.SeedLibraryis reframed as an ephemeral replay working set (DR-023). The per-entryrun_count/fail_count/status(:failing/:fixed/:flaky) tri-state is replaced by a singleconsecutive_passesstreak, andrecord_run/3now uses streak semantics plus aprune/2step. The library file version is bumped to 2;load/1tolerates older files.save/2is now atomic (temp file + rename).stats/1/format/1reflect the new schema.get_seeds/2andseed_values/2are removed (they filtered on the now-gone status field). - BREAKING:
PropertyDamage.Regression'sdedup_sourcecollapses to:failuresonly (default:failures); the:libraryand:bothvalues are removed. The library branch always returned no comparable failures, so dedup behavior is unchanged.
Removed
- Removed the unvalidated genetic-algorithm guided generation (
GuidedRunnerand theTargetedGenerationbehaviour). The search was never shown to outperform random generation and had no test coverage. This is not planned for re-implementation: command weighting (weight:),when:/with:shaping, and longer sequences already cover reaching deep states, and the narrow target class where an evolutionary search would add value did not justify the machinery. - Removed the interactive Livebook visualization (
PropertyDamage.LivebookandPropertyDamage.Livebook.Charts). The widgets read a run-result shape the engine does not emit, so they could not work as shipped. Failure-to-notebook export (PropertyDamage.ExportLivebook output) is unaffected. This is not planned for re-implementation: it was packaging over capability that already exists or never did. Failure exploration is covered by theFailureReportformatter, itsInspectimpl, andExport.LiveBook.generate/1(a real, executable per-step notebook); live monitoring is a few cells over the liveTelemetry.Collector; and the run-history charts depended on per-command trace data the engine has never captured. - BREAKING: Removed the deprecated symbolic-reference mechanism, fully
superseded by
external()markers (DR-011/DR-021): thePropertyDamage.Refmodule, the%Ref{}struct andRef.symbolic/1, thecreates_ref/0command callback (and its--creates-refgenerator option), and the now-dead:refsoption onPropertyDamage.execute/2. Declare server-generated values withexternal()on event structs instead. DR-010 is marked superseded. - BREAKING: Removed
PropertyDamage.SeedLibrary'sexport/importfunctions and all "share across a team / build a regression suite" framing (DR-023). The seed library is a local, ephemeral working set;save/loadare the only persistence. Durable, shareable regressions belong to the Export subsystem (ExUnit), which freezes the concrete shrunk sequence.
Fixed
- Converted-branching shrinks now truncate at the linear failure index. When a branching sequence converted to linear during shrinking, the linear phase received the original branch-relative failure index, which for a failure in the second or later branch is smaller than the command's position in the flattened sequence; truncation cut too short, was rejected by the still-fails guard, and left the full sequence to the budget-bounded one-by-one fixpoint, which on long sequences could exhaust its budget and return a non-minimal reproduction. The convert step now derives the failure index from its own linear re-run, so truncation targets the real failure point.
- The settled final state now folds in late resource-poller events even when no
@poll_statepoller is active. Previously the finalize-time drain only ran to feed@poll_statepredicates, so a run with resource pollers but no@poll_stateleft events that arrived after the last command unfolded. A final event-queue drain in result finalization makes the settled state (used by@trigger at: :teardownand the reported projections) reflect every observed event. PropertyDamage.shrink_further/2's documented option defaults no longer drift from the code: it listed a phantom:max_iterationsdefault of 5000, but the defaults are strategy-derived (:thoroughis 2000 iterations / 60_000 ms). The docs now describe the per-strategy budget table.- Standalone reproduction scripts (curl/python/elixir/livebook) now wire
server-generated
external()values (DR-021): the producing command's response field is extracted (by the%Placeholder{}'s path) and referenced by downstream consumers, instead of being rendered as an inert<Placeholder:...>literal. The deprecated name-guessing ref extraction (which never matched what consumers referenced) is removed from the script generators. PropertyDamage.Mutation.run/1could not execute end to end: the runner passed theMutatingAdapterstruct as the:adapteroption, which option validation rejects and the executor cannot dispatch on. It now passesMutatingAdapteras the adapter module with the struct threaded throughadapter_config, matching the adapter's design.PropertyDamage.Integration.health_check/1crashed instead of returning{:error, _}when no usable HTTP client was available: thehttpcfallback called:inets.start()/:ssl.start()unconditionally and:ssl.start/0raises when:sslis not loadable. The fallback is now guarded and degrades to an error result, honouring the documented:ok | {:error, term()}contract.Coverage.new/1mis-parsed command specs: it read the raw command list with a weight-first{_weight, cmd}pattern, so the documented{Module, weight: n}keyword form bound the options list as the "command". It now routes throughModel.normalize_commands/1and handles every spec form.- Configuration validation, the
pd.validate/iexhelpers, and the no-valid-commands error formatter iteratednormalize_commands/1's{weight, module, spec}output with a stale two-element{_weight, cmd}pattern, so most ofValidationwas a silent no-op (command-existence,downstream_observables, and orphan-event checks never ran) and the error formatter raised. Corrected to the three-element form.mix pd.validateandPropertyDamage.IEx.check_preconditions/2also checked the obsoletenew!/2/precondition/1API; they now checkgenerator/1and evaluate the spec's:whenpredicate. - Step-by-step
Replayrebuilt as a stepping shell over the executor (it previously could not execute a single step against any model). - Eventual-consistency pipeline rebuilt: probe/async settle and
@poll_statepolling now function (the latter previously crashed the run on the first command). - Branching/parallel execution, linearization checking, and branch-aware shrinking rebuilt.
- Hierarchical shrinking index handling; placeholder resolution is preserved through shrinking.
- Failure output made crash-proof (JSON serialization, error classification, formatter). Malformed adapter returns, raising adapters, and raising projections now produce graceful failure reports instead of crashing the run.
- Nemesis auto-restore now actually runs: faults whose
duration_mselapses are lifted between commands, and any still-active faults are restored at sequence end (restore/2previously had no call sites despite the behaviour promise). - Nemesis silent no-ops are gone: the Toxiproxy-backed network nemeses
(
NetworkLatency,NetworkPartition,PacketLoss) tag their events withsimulated: truewhen Toxiproxy is not configured, so a fault that injected nothing can no longer be mistaken for a real one (Nemesis.simulated_event?/1reads the marker). All 10 nemesis implementations are now audited (real injection or honest simulation) against a live Redis + Toxiproxy bench. mix pd.scaffoldnow emits a suite that actually compiles and runs against a live HTTP API (validated end to end against a real OpenAPI spec). The generated adapter previously returned{:ok, response}(the raw body), which the executor rejects as a malformed return, and collapsed every non-2xx to an{:error, _}the run halts on. It now maps each completed HTTP response through the command'sevents/3(status-aware, so a404/409can be an observation) and returns{:ok, events}; transport failures stay{:error, _}. Also fixed: missing@impl trueon generatedread_only?/0, the adapter missing the requiredtimeout/1callback (nowuse PropertyDamage.Adapter), an undefined-Reqwarning under--warnings-as-errors, non-mix format-clean output, and a moduledoc that taught a nonexistentnew!/2/Faker/Req.post!API.
0.1.0 - 2024-12-27
Added
Core Framework
- Stateful property-based testing with commands, events, and projections
- Two-phase execution (symbolic and concrete)
- Symbolic references for entity IDs
- Automatic shrinking of failing sequences
- Seed-based reproducibility
Command System
PropertyDamage.Commandbehaviour for defining operations- Two-layer generator architecture (
generator/1andnew!/2) - Command preconditions for state-aware generation
- Ref extraction for entity relationships
Projections
PropertyDamage.Projectionbehaviour for state tracking- State projections for model state
- Assertion projections for invariant checking
- Configurable check triggers (
:always,:end_of_sequence) - Sampling support for expensive checks
Model System
PropertyDamage.Modelbehaviour for test configuration- Weighted command selection
- Lifecycle hooks (
setup_each/1,teardown_each/1)
Adapter System
PropertyDamage.Adapterbehaviour for SUT integration- Setup and teardown lifecycle
- Context passing between executions
Parallel Execution
- Branching sequences for race condition testing
- Linearization checking for parallel results
- Parallel shrinking support
Shrinking
- Automatic sequence minimization
- Command removal strategies
- Value simplification
- Ref dependency analysis
- Exhaustive shrinking option
Analysis & Debugging
- Causal explanation of failures
- Trigger isolation
- Step-by-step replay
- State diff comparison
- Sequence diagrams (Mermaid, PlantUML, WebSequenceDiagrams)
- Diff-based trace comparison
Failure Management
- Failure persistence (save/load)
- Seed library for regression testing
- Automatic regression test management
- Failure fingerprinting and clustering
- Similar failure detection
- Fix verification
Coverage
- Command coverage metrics
- Transition coverage
- State class coverage
- Multiple output formats (terminal, markdown, JSON)
Flakiness Detection
- Non-deterministic behavior detection
- Pass rate analysis
- Likely cause identification
Load Testing
- SPBT-based load generation
- Configurable ramp strategies (linear, step, spike, wave)
- Real-time metrics collection
- Report generation
Export
- ExUnit test generation
- Script generation (curl, Elixir, Python)
- Livebook notebook generation
- Markdown reports
Mutation Testing
- Adapter response mutation
- Multiple operators (value, omission, status, event, boundary)
- Mutation score calculation
- Weakness analysis
- Actionable suggestions
Property & Invariant Suggestions
- Model analysis for missing checks
- Pattern detection
- Priority-based recommendations
Failure Intelligence
- Pattern detection across failures
- Similarity scoring
- Fix verification with seed variations
Chaos Engineering (Nemesis)
PropertyDamage.Nemesisbehaviour for fault injection- Network operations:
NetworkLatency- Add latency with jitterNetworkPartition- Full/asymmetric partitionsPacketLoss- Simulate packet loss
- Resource operations:
MemoryPressure- Memory allocation stressCPUStress- Scheduler stressResourceExhaustion- File descriptors, ports, ETS, processes
- Time operations:
ClockSkew- Clock drift and jumps
- Process operations:
ProcessKill- Kill by name, pattern, supervisorSlowIO- Artificial I/O delay
- Security operations:
CertificateExpiry- TLS certificate failures
- Auto-restore support
- Toxiproxy integration
Telemetry
- Comprehensive telemetry events
- Event collector for dashboards
- HTML dashboard rendering
Livebook Integration
- Interactive visualization dashboard
- Results tables and command statistics
- Charts (bar, histogram, pie, heatmap, timeline)
- Live monitoring
- Command stepper
- Failure exploration
OpenAPI Scaffolding
- Generate command modules from OpenAPI specs
Documentation
- Comprehensive README with all features
- Example projects (Counter, ToyBank, TravelBooking)
- User guides:
- Getting Started
- Writing Effective Invariants
- Debugging Failures
- Chaos Engineering with Nemesis
- Interactive Livebook demo notebook
- ExDoc configuration with module groups