All notable changes to this project will be documented in this file. See Conventional Commits for commit guidelines.
v0.7.0 (2026-06-02)
Features:
- compile-time guard requiring the :Provider label on provider resources (#219) by Matt Beanland
Bug Fixes:
- relationship-hop traversal walks the general Relationship, not just DefinedSimpleRelationship (#222) by Matt Beanland
v0.6.0 (2026-06-02)
Breaking Changes:
- unify inherited_characteristic into a general traversal DSL (#213) by Matt Beanland
Features:
- add traversal foundation for unified inherited_characteristic (#213) by Matt Beanland
Bug Fixes:
- ship CHANGELOG in the hex package and silence the git_ops warning by Matt Beanland
v0.5.1 (2026-06-01)
Bug Fixes:
202-surface-inherited-characteristic-jason-step-order by Matt Beanland
201-verify-characteristics-inherited-keyerror by Matt Beanland
0.5.0 (2026-06-01)
Dependencies
- Bump
ash_neo4jto~> 0.8.1(#198) — picks up the data-layer fixes for ash_neo4j#283 (geo attribute set tonilon update now clears persisted companions), #285 (abelongs_toedge present in the graph failed to load when the node carried ≥2 same-label collection edges), and #287 (stale indexable geo companions on a shape-changing update). Six previously-skipped tests re-enabled: theBasePlacelocation → boundstransition and the 5 generic-instance encode tests.
Features
Service / Resource cascade — Phase A (#4) —
Diffo.Provider.BaseInstanceis split into a shared base fragment plus two subtype fragments,Diffo.Provider.Service(TMF638) andDiffo.Provider.Resource(TMF639). A concrete instance composes[BaseInstance, Service]or[BaseInstance, Resource]. This fixes the long-standing modelling bug where a Resource carried aservice_state: Resources now compose only theResourcefragment and have no service lifecycle at all.Servicefragment carries theAshStateMachinelifecycle (state, renamed fromservice_state),operating_status(renamed fromservice_operating_status), the lifecycle actions (feasibilityCheck/reserve/deactivate/activate/suspend/terminate/cancel/status), and the TMF638-shaped jason. A service terminates or cancels — it never "retires".Resourcefragment carrieslifecycle_state(TMF639 v5lifecycleState; ITU-T M.3701 lifecycle —planned/installed/pendingRemoval, withnilas both the initial and terminal state), the orthogonal TMF639 v4 X.731 status axes (administrative_state/operational_state/usage_state/resource_status) plusresource_versionas nullable enums, thelifecycleaction, and the TMF639-shaped jason. The status axes move independently (not a state machine);resource_statusis keptallow_nil?as a v4 back-compat escape hatch. (The resource lifecycle is a state-machine candidate, deferred to #189.)Diffo.Provider.Instancecomposes[BaseInstance, Service]— it is the generic Service and the abstract reader for projection. An instance is exactly one of Service or Resource (not both, not neither).- The jason wire shape is byte-for-byte unchanged (the
state/operatingStatuskeys were already those names); the renames are internal only. - The service-state vocabulary helper moved from
Diffo.Provider.ServicetoDiffo.Provider.ServiceState(the former name now belongs to the fragment). - Specification-kind guards keep an instance and its specification on the same side of the divide: a Service must be specified by a
:serviceSpecification, a Resource by a:resourceSpecification. Two complementary checks — a compile-timeDiffo.Provider.Extension.Verifiers.VerifySpecificationKind(catches a consumer leaf mis-declaring itsspecification do type, failing their build) and a runtimeDiffo.Provider.Validations.ValidateSpecificationKindon theService/Resourcefragments (catches the spec associated at create/specify — covering generic instances andrespecify). Generic instances with no declared specification are not statically checked but are validated at runtime.
Consumer migration: a service leaf now composes
fragments: [BaseInstance, Service]; a resource leaf composesfragments: [BaseInstance, Resource](previously[BaseInstance]).API: reads project to the concrete leaf —
Diffo.Provider.get_instance_by_id!/1/list_instances!/0/find_instances_by_*return the concrete Service/Resource struct (viaAshNeo4j.worlds/1). The lifecycle and record operations (activate_service!,respecify_instance!,delete_instance!, …) are now struct-dispatched functions onDiffo.Providerrather than code-interface definitions; existing call sites are unchanged.Earlier in this cycle, creating a generic instance with both features and characteristics was blocked by an ash_neo4j load-path defect (ash_neo4j#285, originally filed as #284) — a
belongs_toedge present in the graph failed to load when the node also carried ≥2 same-label collection edges. Resolved by the ash_neo4j 0.8.1 bump (below); the 5 encode tests skipped against it are re-enabled.Inherited and reverse-inherited values now surface in the TMF JSON view (#173) — a new sibling transformer
Diffo.Provider.Extension.Transformers.TransformInheritedJasonruns afterTransformInheritedRefs(calc injection) and beforeAshJason.Resource.Transformer(encoder generation). For each inherited kind a resource declares, it injects a focusedjason.customizestep so loaded inherited calcs reach the consumer-visible array — no per-consumer customize required:inherited_place→ theplacearray, as a simulatedPlaceRef(carries the declared role plus the inherited place's flattened identity; there is no backing ref node — the inheritance simulates it)inherited_party→ therelatedPartyarray, as a simulatedPartyRefinherited_characteristic/reverse_inherited_characteristic→ theserviceCharacteristic/resourceCharacteristicarray, as ordinary typed characteristics
Surfaced entries appear after the instance's local entries.
%Diffo.Unknown{}sentinels are filtered out before any ref wrapping — X-state is the Diffo diagnostic surface, not the TMF wire. Unloaded calcs (%Ash.NotLoaded{}) contribute nothing; load the calc to include it. Wire-shape concerns stay in this transformer; calc-shape concerns stay inTransformInheritedRefs.
Bug Fixes
Instance.Party.validate_constraintsskips inherited declarations (#183) — the validator'sEnum.reject(&(&1.reference || &1.calculate))was iterating ALL party declarations and KeyError'd onInheritedPartyDeclaration(which has no:reference/:calculatefields). Same shape of bug as the persister fix in #172 for inherited characteristics. Fix: filter toDiffo.Provider.Extension.PartyDeclarationbefore the reject — inherited variants are pre-validated by their declaration entity and have no min/max constraints to enforce.
Behavior changes
inherited_place/inherited_partycalcs now emit%Diffo.Unknown{}for reached-but-undeclared sources (#183) —Diffo.Provider.Calculations.InheritedPlaceandInheritedPartypreviously silently dropped source instances that didn't carry aPlaceRef/PartyRefat the declaredsource_role. The newInheritedCharacteristic/ReverseInheritedCharacteristiccalcs (from #172) surface that case as%Diffo.Unknown{}; this aligns the older calcs with the same X-state discipline.Single reason vocabulary (no cross-world dispatch needed — PlaceRef/PartyRef are universal indirections):
:role_not_declared— source instance reached by alias traversal but itsPlaceRef/PartyRefrecords carry no entry atsource_role. Context:%{source_id: id, role: source_role}.
:worldis stamped at compile time viaTransformInheritedRefs(previously passed only to the characteristic variants; now passed to all four inherited calcs).Consumer impact: code that
Enum.maps%Diffo.Provider.Place{}(orParty{}) from an inherited_place/inherited_party result must now handle%Diffo.Unknown{}entries (filter, pattern-match, or let them propagate). The empty-list case (no sources reached at all) is unchanged —Unknownis reserved for "tried and couldn't determine," not "nothing to determine."
Bug Fixes
Eliminate fragment-override warnings on cascade leaves (#181) — Spark's
merge_with_warningwas firing during compile time whenever a subtype fragment (BaseGeographicAddress/Site/Location,BaseOrganization/Individual) declared a widerjason.pick/outstanding.expectthanBasePlace/BaseParty. The merge logic has no opt-out for deliberate overrides. Fix: movejason doandoutstanding dooffBasePlaceandBasePartyentirely; each concrete leaf carries its own declaration:- Abstract readers (
Provider.Place,Provider.Party) now declare their own base-shapejason doandoutstanding do(previously inherited from the base fragment) - Cascade subtype fragments continue to declare their own (no change)
- Test-support consumer leaves were already declaring their own (audit confirmed)
BasePlace.encode_geo_json/2stays as a static helper that subtype fragments and consumer leaves reference from their ownjason.customize
Documented as cascade discipline in
usage-rules.mdandAGENTS.md. Zero behaviour change; 757 tests + 90 doctests still pass.- Abstract readers (
Features
Party subtype cascade —
BaseParty→ typed subtype leaves (#186) — TMF632 Organization and Individual now ship as concrete leaves built from fragment composition (BaseParty+BaseOrganization/BaseIndividual). Consumer leaves (e.g.MyApp.Carrier) compose the same two fragments alongside their own attributes.defmodule Diffo.Provider.Organization do use Ash.Resource, fragments: [BaseParty, BaseOrganization], domain: Diffo.Provider endAttributes (TMF632 v5 cut, permissive defaults):
BaseOrganization:trading_name,name_type,organization_type,is_legal_entity,is_head_officeBaseIndividual:given_name,family_name,middle_name,title,gender,birth_date,nationality
Deferred to follow-ups: nested arrays (
otherName[],*Identification[],disability[],languageAbility[],skill[]), state machine attrs (pairs with[[project_specification_lifecycle]]), org parent/child relationships (via PartyRef machinery), richer demographics (deathDate,placeOfBirth, etc.),existsDuring(TimePeriod).Party dispatcher API on
Diffo.Provider(#186) — mirrors the Place dispatcher exactly, with:Entityas an additional abstract-routed type alongside:PartyRef:Diffo.Provider.create_party!(:Organization, %{id: "X", trading_name: "Acme"}) Diffo.Provider.create_party!(:Individual, %{id: "Y", given_name: "Jane"}) Diffo.Provider.create_party!(:PartyRef, %{id: "Z", referred_type: :Organization}) Diffo.Provider.create_party!(:Entity, %{id: "E", name: "Aggregate"}) Diffo.Provider.get_party_by_id!(id) # returns concrete subtype struct via projection Diffo.Provider.list_parties!() # mixed-subtype list, each projected Diffo.Provider.update_party!(record, attrs) # struct-dispatched to :define Diffo.Provider.delete_party!(record)Diffo.Test.Party.Organization→Diffo.Test.Party.Enterprise(#186) — frees the canonicalDiffo.Provider.Organizationname and demonstrates consumer-style naming (paired with existingDiffo.Test.Party.Personwhich similarly demonstrates non-TMF naming for an Individual analogue).
Breaking changes (Party)
Diffo.Provider.create_party!/1removed — replaced bycreate_party!/2. Migration mirrors the Place migration in #185:# Before Diffo.Provider.create_party!(%{type: :Organization, id: "X", ...}) Diffo.Provider.create_party!(%{referred_type: :Individual, id: "Y"}) # After Diffo.Provider.create_party!(:Organization, %{id: "X", ...}) Diffo.Provider.create_party!(:PartyRef, %{referred_type: :Individual, id: "Y"})All per-codedef Party actions on
Diffo.Providerdomain dropped — replaced by the dispatcher functions of the same names (different arities forcreate_party).get_party_by_id/1,list_parties/0,find_parties_by_*/1return concrete subtype structs viaAshNeo4j.worlds/1projection.Type-change updates on cascade leaves are rejected — typed Party leaves have fixed
:type. PartyRef placeholders (Provider.Partyrecords withreferred_type:) still supportreferred_type:updates.
Architectural notes (Party)
Diffo.Provider.Partystays in core minimally — repurposed as the abstract reader for projection bootstrap + PartyRef-typed placeholder support +:Entityrouting. Moduledoc rewritten to reflect this.PartyRef typed
belongs_tounchanged (Option C carries over from #185) — graph integrity preserved.Place subtype cascade —
BasePlace→ typed subtype leaves (#185) — TMF675 GeographicAddress / GeographicSite / GeographicLocation now ship as concrete leaves built from fragment composition (BasePlace+BaseGeographicX). Consumer leaves (e.g.MyApp.SydneyExchange) compose the same two fragments alongside their own attributes.defmodule Diffo.Provider.GeographicSite do use Ash.Resource, fragments: [BasePlace, BaseGeographicSite], domain: Diffo.Provider # … endSubtype fragments carry TMF-camelCase jason wire shape, tightened validations (e.g.
BaseGeographicLocationrequires location-xor-bounds set), and — onBaseGeographicSite— a projected:addresscalc that resolves to a concreteGeographicAddress(or consumer-domain Address leaf) at read time viaAshNeo4j.worlds/1.Diffo.Provider.Calculations.ProjectedRef(#185) — reusable calculation for cross-resource references without a graph edge. Resolves anid_fieldto the outermost concrete world's resource struct viaAshNeo4j.worlds/1. Three-state load surface: concrete struct on success,%Diffo.Unknown{}for resolution failures (:no_target/:no_concrete_world/:projection_failed),%Ash.NotLoaded{}until loaded. Does NOT replacebelongs_to— AshNeo4j'sverify_relaterequires real Ash relationships to maintain edges, so typedbelongs_toon PlaceRef/PartyRef stay intact (Option C).Place dispatcher API on
Diffo.Provider(#185) — replaces per-subtype codedef explosion (7 codedefs × 3 subtypes = 21) with one function per CRUD verb that scales to N subtypes at constant API surface:Diffo.Provider.create_place!(:GeographicSite, %{id: "X", site_type: :exchange}) Diffo.Provider.create_place!(:PlaceRef, %{id: "Y", referred_type: :GeographicAddress}) Diffo.Provider.get_place_by_id!(id) # returns concrete subtype struct via projection Diffo.Provider.list_places!() # mixed-subtype list, each projected Diffo.Provider.update_place!(record, attrs) # struct-dispatched to :define action Diffo.Provider.delete_place!(record)Reads do inline projection (load via
Provider.Placeabstract reader → project viaAshNeo4j.worlds/1). Unknown TMF type atoms raiseArgumentError.Polymorphic-source ref dispatcher (#185) —
create_place_ref!/1/create_party_ref!/1accept a tagged-tuple or structsource:field that unpacks to the right FK column.list_place_refs_from/1/list_place_refs_targeting/1express read intent rather than per-FK (list_place_refs_by_*_id). Schema unchanged — the four FK columns stay.Diffo.Provider.create_place_ref!(%{ role: :installation_site, source: {:instance, "INST-001"}, # or {:party, ...}, {:place, ...}, or a struct target: place_or_id }) Diffo.Provider.list_place_refs_from(source) Diffo.Provider.list_place_refs_targeting(target)
Breaking changes
Diffo.Provider.create_place!/1removed — replaced bycreate_place!/2(type-atom dispatcher). Migration:# Before Diffo.Provider.create_place!(%{type: :GeographicSite, id: "X", ...}) Diffo.Provider.create_place!(%{referred_type: :GeographicAddress, id: "Y"}) # After Diffo.Provider.create_place!(:GeographicSite, %{id: "X", ...}) Diffo.Provider.create_place!(:PlaceRef, %{referred_type: :GeographicAddress, id: "Y"})All per-codedef Place actions on
Diffo.Providerdomain dropped (create_place,get_place_by_id,list_places,find_places_by_id,find_places_by_name,update_place,delete_place) — replaced by the dispatcher functions of the same names (different arities forcreate_place).Diffo.Provider.get_place_by_id/1,list_places/0,find_places_by_id/1,find_places_by_name/1now return concrete subtype structs — projected viaAshNeo4j.worlds/1, not the abstract%Diffo.Provider.Place{}. Tests that pattern-match on%Provider.Place{}need updating to%Provider.GeographicSite{}(etc.). Field-access assertions (.id,.name,.type) continue to work.Type-change updates on cascade leaves are now rejected — a typed Place leaf (e.g.
Provider.GeographicAddress) cannot have its:typechanged to:GeographicSiteviaupdate_place!/2; the typed leaves have fixed:typeset by their:buildaction. PlaceRef-typed placeholders (Provider.Placerecords withreferred_type:) still supportreferred_type:updates.GeographicLocationnow requires geometry —BaseGeographicLocationvalidates that records withtype: :GeographicLocationhave:locationor:boundsset. Pre-cascadeGeographicLocation-typed records without geometry must be backfilled or re-classified as:PlaceRefplaceholders.
Architectural notes
Diffo.Provider.Placestays in core minimally — repurposed as the abstract reader that backs projection bootstrap (symmetric with howProvider.Instancebacksinherited_characteristic) and the PlaceRef-typed placeholder dispatcher path. Production code should use the typed subtype leaves or the dispatcher;Provider.Placeis plumbing, not a recommendation. Moduledoc rewritten to reflect this.AGENTS.md— Fat* pattern section updated — the original "don't split subtypes into fragments" advice was based on a misread of how fragment composition stacks. Fragment composition is additive at the leaf, not a budget spend. The Fat* invariants (graph edges, indexability, no N² explosion) still hold under the cascade because typedbelongs_tokeeps pointing at the abstract reader (Option C).- Reanimates #4 "split Service and Resource" — the cascade pattern
established here is the reusable template for the Instance Service/Resource
cascade in #4, with
ProjectedRef+ dispatcher as the shared artifacts.
v0.4.1 (2026-05-22)
Bug Fixes
- Assigner lifecycle (#168) — broadened the lifecycle states permitted to make assignments. Services may now assign from
:feasibilityChecked,:reserved,:inactive,:active, or:suspended(was:active/:inactiveonly). Resources may now assign from:installingor:operating(was:operatingonly).Assigner.assignable_state?/1exposes the policy directly.
Features
Diffo.Provider.Changes.Define/Relate/Assign(#170) — change modules that wrap the standard after-action patterns every Instance consumer writes. Replace the hand-writtenafter_actionbody threadingCharacteristic.update_all/Pool.update_pools/Relationship.relate_instance/Assigner.assigntogether with a one-liner:update :define do argument :characteristic_value_updates, {:array, :term} change Diffo.Provider.Changes.Define end update :relate do argument :relationships, {:array, :struct} change Diffo.Provider.Changes.Relate end update :assign_port do argument :assignment, :struct, constraints: [instance_of: Assignment] change {Diffo.Provider.Changes.Assign, pool: :ports} endReload happens via the resource's primary
:readaction, so no consumer-specific reader is needed.BaseCharacteristic auto-generated
:create/:updateactions (#171) —BaseCharacteristic-derived resources now get default:createand:updateactions synthesised from their public attributes.:createaccepts[:name | <public_attrs>]with:instance_id/:feature_idarguments andmanage_relationshipchanges;:updateaccepts<public_attrs>. Consumers may still declare their own actions to override the defaults.Typed characteristics and pools in Instance JSON (#169) —
BaseInstancenow loads two new calculations (:typed_characteristics,:pool_characteristics) by default and the jason customize merges their values into theserviceCharacteristic/resourceCharacteristicarray. TypedBaseCharacteristicrecords andAssignableCharacteristicpool records that were already present in the graph are now visible at the TMF JSON surface.
Notable Changes
Diffo.Provider.Calculations.TypedCharacteristicsandDiffo.Provider.Calculations.PoolCharacteristics— new calc modules backing the JSON surfacing for #169.- Regression test added for #62 (characteristic update validation) — typed
BaseCharacteristicupdates now reject unknown fields and invalid types through Ash's standard changeset machinery.
v0.4.0 (2026-05-20)
Breaking Changes
Diffo.Provider.AssignedToRelationshipreplaced byDiffo.Provider.AssignmentRelationship— stores pool assignments with top-levelpool,thing,value, andaliasscalar attributes, enabling graph-level filtering in AshNeo4j queries. Any existing graph data onAssignedToRelationshipnodes must be migrated.create_assigned_to_relationshipcode interface removed — usecreate_assignment_relationshipinstead.instance.assignmentsnow returnsAssignmentRelationshiprecords (struct name change only).
Features
DefinedSimpleRelationship— new resource for relationships carrying an optional single embeddedNameValuePrimitivecharacteristic, frozen at creation. Used by the Assigner and available as a general-purpose committed-relationship primitive. Accessible viainstance.assignments.AssignmentRelationshipaliases — thealiasattribute onAssignmentRelationship(identity[:target_id, :alias]) gives a consuming instance a stable name for an assignment slot. Mirrors the[:source_id, :alias]identity onDefinedSimpleRelationship. Alias semantics are the foundation of the first-order expectation system (#74).relationships doDSL — source and target validation pipeline for Instance resources.ValidateRelationshipPermittedis injected automatically into relate actions. Supports:all,:none, and explicit role-name lists.- Resource lifecycle states —
resource_stateattribute on Instance resources with standard TMF states (:installed,:operating,:retired, etc.). The Assigner enforces:operatingbefore allowing assignment. inherited_place/inherited_partyDSL — declare insideplaces do/parties doon an Instance resource to generate an Ash calculation that traverses the assignment graph by alias and inherits a place or party from the source instance. NoPlaceRef/PartyRefedge is created — the calculation is the reference. Supports single-hop (default: role name as alias) and multi-hop (via:list).FieldFromAssignment(Diffo.Provider.Calculations.FieldFromAssignment) — reads a field directly from anAssignmentRelationshiprecord (:value,:pool,:thing,:alias). Filtered by optionalalias:. Returns a list.FieldViaAssignedRelationship(Diffo.Provider.Calculations.FieldViaAssignedRelationship) — traversesAssignmentRelationshipin reverse (target → source) and reads a named field from each source instance. Supports multi-hopvia:traversal. Returns a list.FieldViaRelationship(Diffo.Provider.Calculations.FieldViaRelationship) — traversesDefinedSimpleRelationshipforward (source → target) filtered by optionalalias:and/ortype:, and reads a named field from each target instance. Returns a list.
Notable Changes
- Assigner rearchitected —
AssignmentRelationshipcarriespool,thing,value,aliasas top-level attributes for AshNeo4j-level filtering;assigned_valuesandfree_valuesuse query-level filtering rather than in-memory computation where possible. TransformBehaviourmoved from persister pipeline to transformer pipeline for correct Spark ordering relative to Ash's own transformers.- Characteristic type verifier improved — rejects
characteristicDSL declarations whose type module is not derived fromBaseCharacteristic.
Documentation
usage-rules.md— new sections covering alias semantics,inherited_place/inherited_partyDSL, and all three field calculation modules including a decision table.AGENTS.md— updated project structure, DSL inline examples for inherited refs, and new common mistakes section entries.- Provider Extension livebook — new section "Aliases, Inherited DSL, and Field Calculations" with Compute-domain examples.
What's Changed
- defined_simple_relationship by @matt-beanland in https://github.com/diffo-dev/diffo/pull/142
- refactored assigner using defined_simple_relationship by @matt-beanland in https://github.com/diffo-dev/diffo/pull/143
- relationships DSL by @matt-beanland in https://github.com/diffo-dev/diffo/pull/146
- relationships target side validation by @matt-beanlanda in https://github.com/diffo-dev/diffo/pull/148
- clean code by @matt-beanland in https://github.com/diffo-dev/diffo/pull/150
- improved assigner using aggregates by @matt-beanland in https://github.com/diffo-dev/diffo/pull/151
- refactor transformers and persisters by @matt-beanland in https://github.com/diffo-dev/diffo/pull/152
- resource lifecycle state by @matt-beanland in https://github.com/diffo-dev/diffo/pull/154
- inherited party and place via instance DSL by @matt-beanland in https://github.com/diffo-dev/diffo/pull/155
- agent guidance by @matt-beanland in https://github.com/diffo-dev/diffo/pull/161
- FieldViaAssignedRelationship calculation by @matt-beanland in https://github.com/diffo-dev/diffo/pull/162
- FieldViaRelationship calculation by @matt-beanland in https://github.com/diffo-dev/diffo/pull/165
- FieldFromAssignment calculation by @matt-beanland in https://github.com/diffo-dev/diffo/pull/164
- docs pass — inherited DSL, aliases, and field calculations by @matt-beanland in https://github.com/diffo-dev/diffo/pull/166
v0.3.0 (2026-05-17)
Breaking Changes
Diffo.Provider.Relationshipno longer stores assignment records. Assignment relationships are now onDiffo.Provider.AssignedToRelationship. Any existing graph data withtype: :assignedToonRelationshipnodes will need to be migrated.instance.forward_relationshipsno longer contains assignment records — useinstance.assignmentsinstead.Diffo.Provider.create_assignment_relationshipremoved — useDiffo.Provider.create_assigned_to_relationship.
Notable Changes
Diffo.Provider.BaseRelationship— new Ash Resource Fragment providing common attributes and behaviour for all relationship typesDiffo.Provider.AssignedToRelationship— new dedicated resource for pool assignment relationships, split out fromDiffo.Provider.RelationshipDiffo.Provider.Relationship— now TMF-only;pool,thing,assignedattributes and:create_assignmentaction removedinstance.assignments— newhas_manyonBaseInstancefor pool assignment relationships; included in JSON encoding and default loadsDiffo.Provider.BaseCharacteristic— new Ash Resource Fragment for typed characteristic resources;ShelfCharacteristic,CardCharacteristicetc. now extend this rather than using plainAsh.TypedStructpools doDSL — new section on Instance resources replacing the oldcharacteristic :name, AssignableValuepattern; generatespools/0andpool/1introspection functions- Module naming convention — Instance resources must be suffixed
…Instance, Characteristic resources…Characteristicto avoid Neo4j label collisions (documented inusage-rules.mdandAGENTS.md) Diffo.Provider.Extension— unified Spark DSL extension consolidating the prior per-kind extensions
What's Changed
- provider extension consolidation by @matt-beanland in https://github.com/diffo-dev/diffo/pull/130
- base characteristic by @matt-beanland in https://github.com/diffo-dev/diffo/pull/133
- assigner refactor — BaseRelationship, AssignedToRelationship, pools DSL, resource naming by @matt-beanland in https://github.com/diffo-dev/diffo/pull/135
v0.2.2 (2026-05-08)
Notable Changes
- Updated to ash_neo4j 0.5.0 with async test support
- Igniter installer —
mix igniter.install diffonow sets up Neo4j config, custom expressions, and Spark DSL formatter - Spark DSL formatter configured for all provider extensions;
mix formatenforced across the codebase usage-rules.mdadded for AI coding assistant guidance when working with Diffo
What's Changed
- async tests by @matt-beanland in https://github.com/diffo-dev/diffo/pull/114
- igniter by @matt-beanlanda in https://github.com/diffo-dev/diffo/pull/116
- spark formatter by @matt-beanlanda in https://github.com/diffo-dev/diffo/pull/117
- usage_rules by @matt-beanlanda in https://github.com/diffo-dev/diffo/pull/118
v0.2.1 (2026-05-06)
Notable Changes
- Updated to ash_neo4j 0.4.1 and bolty 0.0.12, now supporting transactions and test sandbox
- Improvements to provider DSL and documentation
What's Changed
- base party and related DSL and livebook by @matt-beanland in https://github.com/diffo-dev/diffo/pull/82
- Instance DSL parties — multiplicity, validation, and enforcement by @matt-beanland in https://github.com/diffo-dev/diffo/pull/89
- 86 transformers persisters verifiers by @matt-beanland in https://github.com/diffo-dev/diffo/pull/92
- 91 place dsl by @matt-beanland in https://github.com/diffo-dev/diffo/pull/93
- 79 provider instance specification doesnt set description by @matt-beanland in https://github.com/diffo-dev/diffo/pull/95
- 94 provider instance specification dsl additional fields by @matt-beanland in https://github.com/diffo-dev/diffo/pull/97
- document instance versioning lifecycle by @matt-beanland in https://github.com/diffo-dev/diffo/pull/98
- accept raw dynamic by @matt-beanland in https://github.com/diffo-dev/diffo/pull/100
- removed duplicate tests by @matt-beanland in https://github.com/diffo-dev/diffo/pull/108
- 105 latest ash neo4j by @matt-beanland in https://github.com/diffo-dev/diffo/pull/109
v0.2.0 (2026-04-24)
Breaking Changes
- Updated to ash_neo4j 0.3.1 and bolty 0.0.10 — no database compatibility with prior versions due to significant changes in the data layer and Bolt protocol handling
Features
Diffo.Type.Value— union ofDiffo.Type.PrimitiveandDiffo.Type.Dynamic, enabling mixed primitive and typed-struct values on characteristics and other resourcesDiffo.Type.Primitive— typed union of string, integer, float, boolean, date, time, datetime, durationDiffo.Type.Dynamic— runtime-typed struct for Ash.Type.NewType valuesDiffo.Type.Dynamic.is_valid?/1— predicate to check whether a module is a valid Dynamic type (Ash.Type.NewType with storage_type :map) before constructing a valueCharacteristic.values— homogeneous array ofDiffo.Type.Valueon a characteristic, withis_arrayboolean flag; supports morphing between scalar and array representationsDiffo.UnwraponList— unwraps each element, enablingDiffo.Unwrap.unwrap/1to reduce nested wrapped lists to plain Elixir values in one call- Provider instance extension DSL — characteristic and feature characteristic value types now accept
{:array, Module}in addition to plain module references
Fixes
Diffo.Type.Valuenil update — overridehandle_change/3to prevent Ash union type from wrapping nil in the previous member type, which caused malformed JSON to be written to Neo4jDiffo.Type.Valuenil array update — added nil guards tohandle_change_array/3andprepare_change_array/3to prevent enumeration errors when setting an array characteristic to nilDiffo.Type.Dynamicnil safety — added nil clauses tocast_stored/2anddump_to_native/2
Maintenance
- bolty 0.0.10 — native DateTime handling for both BOLT 4.x and BOLT 5.x
Diffo.Unwrapprotocol documentation — recursive unwrap behaviour, custom implementation guide, and array examples added to livebook and module docs
v0.1.6 (2026-03-19)
Fixes
- incorrect domain label
Maintenance
- improved error handling
v0.1.5 (2026-03-19)
Fixes
- fixed relationship enrichment inconsistent across neo4j versions
v0.1.4 (2026-03-12)
Features
- assigner unassign operation
Maintenance
- updated ash_neo4j, uses bolty rather than boltx
v0.1.3 (2025-12-01)
Features
- place_ref source party or place
- party_ref source place or party
- instance events
Maintenance
- remove access domain
v0.1.2 (2025-10-20)
Features
- REUSE compliant
v0.1.1 (2025-09-09)
Features:
- update for AshNeo4j DSL changes
- refactor specification relationships
- characteristic value schemas
- customise instance via specification
- improve relationships to avoid circular loads
v0.1.0 (2025-08-11)
Features:
- initial version on AshNeo4j DataLayer