diff --git a/.formatter.exs b/.formatter.exs index 35bd347..174948b 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -15,6 +15,10 @@ spark_locals_without_parens = [ feature: 1, feature: 2, id: 1, + inherited_party: 1, + inherited_party: 2, + inherited_place: 1, + inherited_place: 2, instance_ref: 2, is_enabled?: 1, major_version: 1, diff --git a/AGENTS.md b/AGENTS.md index 7faedc3..58c0036 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -271,6 +271,27 @@ Spark runs two separate pipelines during compilation, in this order: New transformers go under `transformers:`. New persisters go under `persisters:`. +## DSL shape changes + +Whenever you add, rename, or remove a DSL entity or section in `Diffo.Provider.Extension` +(or any Spark extension in this project), run this checklist in order: + +1. **Update `.formatter.exs`** — add new entity names to `spark_locals_without_parens` with + each supported arity. Without this, `mix format` will add unwanted parentheses to every + DSL call site. + +2. **Run `mix format`** — apply formatting across the codebase and verify the output looks + correct. Run `mix format --check-formatted` to confirm nothing was missed. + +3. **Run `mix spark.cheat_sheets`** — regenerates + `documentation/dsls/DSL-Diffo.Provider.Extension.md`. This file is Spark-generated; + never edit it by hand. Commit the regenerated file alongside the DSL change. + +4. **Run `mix test`** — confirm no regressions. + +Do not skip step 1 even for a "small" entity addition — the formatter will silently reformat +every call site in CI and produce noisy diffs in future PRs. + ## Raising upstream bugs When a bug is found in a dependency (e.g. AshNeo4j, Bolty), raise a GitHub issue on that @@ -309,11 +330,8 @@ not. Add any useful hypotheses as a follow-up comment on the issue, then leave i - Using module names (e.g. `MyApp.CardInstance`) as role values in `relationships do` — roles are atoms like `:provides`, not module references. - Forgetting that `relationships do` omitted means `:none` for both source and target — any update action with `argument :relationships, {:array, :struct}` will fail unless the resource declares permissions. - Thinking the Assigner requires `relationships do` permissions — it does not. The Assigner writes `DefinedSimpleRelationship` records directly via the Provider domain; `ValidateRelationshipPermitted` only runs on actions that carry `argument :relationships, {:array, :struct}`, which the Assigner's `assign_*` actions do not. -- Editing `documentation/dsls/DSL-Diffo.Provider.Extension.md` — it is Spark-generated; - run `mix spark.cheat_sheets` to regenerate it. Whenever you add, rename, or remove a DSL - entity or section, also check `.formatter.exs` — new entity names must be added to - `spark_locals_without_parens` (with each arity) so the Spark formatter omits parentheses. - Run `mix format` afterward to verify. +- Editing `documentation/dsls/DSL-Diffo.Provider.Extension.md` — it is Spark-generated. + See the **DSL shape changes** section above for the full checklist. - Editing content between `` markers in `CLAUDE.md` — that is auto-generated by `mix usage_rules.sync`. - Forgetting `Diffo.Provider.DomainFragment` on a scenario 3 domain — any domain whose diff --git a/documentation/dsls/DSL-Diffo.Provider.Extension.md b/documentation/dsls/DSL-Diffo.Provider.Extension.md index 4df32ff..90ab0c9 100644 --- a/documentation/dsls/DSL-Diffo.Provider.Extension.md +++ b/documentation/dsls/DSL-Diffo.Provider.Extension.md @@ -103,11 +103,13 @@ Provider DSL — structure, roles, and behaviour for this resource kind * parties * party_ref * role + * inherited_party * [places](#provider-places) * place * places * place_ref * role + * inherited_place * [instances](#provider-instances) * role * instance_ref @@ -330,13 +332,14 @@ Declares an assignable pool — a named range of values for auto-assignment ### provider.parties -Party roles on this resource — `party`/`parties`/`party_ref` for Instance kinds; `role` for Party and Place kinds +Party roles on this resource — `party`/`parties`/`party_ref`/`inherited_party` for Instance kinds; `role` for Party and Place kinds ### Nested DSLs * [party](#provider-parties-party) * [parties](#provider-parties-parties) * [party_ref](#provider-parties-party_ref) * [role](#provider-parties-role) + * [inherited_party](#provider-parties-inherited_party) ### Examples @@ -346,6 +349,7 @@ parties do party :provider, MyApp.Provider party_ref :owner, MyApp.InfrastructureCo parties :technicians, MyApp.Technician, constraints: [min: 1] + inherited_party :customer, source_role: :owner end # Party or Place @@ -483,15 +487,48 @@ Declares a role this Party or Place kind plays with respect to other Parties Target: `Diffo.Provider.Extension.PartyRole` +### provider.parties.inherited_party +```elixir +inherited_party role +``` + + +Declares a party derived by traversing the assignment graph — generates a calculation, no PartyRef node created + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`role`](#provider-parties-inherited_party-role){: #provider-parties-inherited_party-role .spark-required} | `atom` | | The role name — also the default alias to follow on AssignmentRelationship. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`source_role`](#provider-parties-inherited_party-source_role){: #provider-parties-inherited_party-source_role .spark-required} | `atom` | | The PartyRef role to pick up on the arrived-at instance. | +| [`via`](#provider-parties-inherited_party-via){: #provider-parties-inherited_party-via } | `list(atom)` | | Sequence of assignment aliases to traverse. Defaults to [role] for single-hop. Use a list for multi-level. | + + + + + +### Introspection + +Target: `Diffo.Provider.Extension.InheritedPartyDeclaration` + ### provider.places -Place roles on this resource — `place`/`places`/`place_ref` for Instance kinds; `role` for Party and Place kinds +Place roles on this resource — `place`/`places`/`place_ref`/`inherited_place` for Instance kinds; `role` for Party and Place kinds ### Nested DSLs * [place](#provider-places-place) * [places](#provider-places-places) * [place_ref](#provider-places-place_ref) * [role](#provider-places-role) + * [inherited_place](#provider-places-inherited_place) ### Examples @@ -500,6 +537,8 @@ Place roles on this resource — `place`/`places`/`place_ref` for Instance kinds places do place :installation_site, MyApp.GeographicSite place_ref :billing_address, MyApp.GeographicAddress + inherited_place :a_end, source_role: :location + inherited_place :poi, via: [:cvc_link, :nni_link], source_role: :poi end # Party or Place @@ -637,6 +676,38 @@ Declares a role this Party or Place kind plays with respect to Places Target: `Diffo.Provider.Extension.PlaceRole` +### provider.places.inherited_place +```elixir +inherited_place role +``` + + +Declares a place derived by traversing the assignment graph — generates a calculation, no PlaceRef node created + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`role`](#provider-places-inherited_place-role){: #provider-places-inherited_place-role .spark-required} | `atom` | | The role name — also the default alias to follow on AssignmentRelationship. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`source_role`](#provider-places-inherited_place-source_role){: #provider-places-inherited_place-source_role .spark-required} | `atom` | | The PlaceRef role to pick up on the arrived-at instance. | +| [`via`](#provider-places-inherited_place-via){: #provider-places-inherited_place-via } | `list(atom)` | | Sequence of assignment aliases to traverse. Defaults to [role] for single-hop. Use a list for multi-level. | + + + + + +### Introspection + +Target: `Diffo.Provider.Extension.InheritedPlaceDeclaration` + ### provider.instances Declares the roles this Party or Place kind plays with respect to Instances diff --git a/lib/diffo/provider/assigner/assigner.ex b/lib/diffo/provider/assigner/assigner.ex index 92ca66c..fbc2229 100644 --- a/lib/diffo/provider/assigner/assigner.ex +++ b/lib/diffo/provider/assigner/assigner.ex @@ -62,11 +62,14 @@ defmodule Diffo.Provider.Assigner do end defp check_lifecycle(%{type: :resource, resource_state: state}) when state != :operating, - do: {:error, "cannot assign: resource lifecycle state is #{inspect(state)}, must be :operating"} + do: + {:error, "cannot assign: resource lifecycle state is #{inspect(state)}, must be :operating"} defp check_lifecycle(%{type: :service, service_state: state}) when state not in [:active, :inactive], - do: {:error, "cannot assign: service state is #{inspect(state)}, must be :active or :inactive"} + do: + {:error, + "cannot assign: service state is #{inspect(state)}, must be :active or :inactive"} defp check_lifecycle(_), do: :ok diff --git a/lib/diffo/provider/components/assignment_relationship.ex b/lib/diffo/provider/components/assignment_relationship.ex index abbadb2..a6ed4e1 100644 --- a/lib/diffo/provider/components/assignment_relationship.ex +++ b/lib/diffo/provider/components/assignment_relationship.ex @@ -45,7 +45,9 @@ defmodule Diffo.Provider.AssignmentRelationship do } list_name = - Diffo.Provider.Relationship.derive_relationship_characteristic_list_name(record.target_type) + Diffo.Provider.Relationship.derive_relationship_characteristic_list_name( + record.target_type + ) characteristics = [%{name: record.thing, value: record.value}] @@ -61,8 +63,13 @@ defmodule Diffo.Provider.AssignmentRelationship do |> Diffo.Util.set(list_name, characteristics) end - order [:type, :resource, :service, :resourceRelationshipCharacteristic, - :serviceRelationshipCharacteristic] + order [ + :type, + :resource, + :service, + :resourceRelationshipCharacteristic, + :serviceRelationshipCharacteristic + ] end actions do diff --git a/lib/diffo/provider/components/instance/extension/characteristic.ex b/lib/diffo/provider/components/instance/extension/characteristic.ex index 9b003df..5180eb6 100644 --- a/lib/diffo/provider/components/instance/extension/characteristic.ex +++ b/lib/diffo/provider/components/instance/extension/characteristic.ex @@ -35,7 +35,8 @@ defmodule Diffo.Provider.Instance.Characteristic do end defp create_characteristics_from_declarations(declarations, type) do - Enum.reduce_while(declarations, {:ok, []}, fn %{name: name, value_type: value_type}, {:ok, acc} -> + Enum.reduce_while(declarations, {:ok, []}, fn %{name: name, value_type: value_type}, + {:ok, acc} -> try do attrs = case value_type do @@ -148,7 +149,8 @@ defmodule Diffo.Provider.Instance.Characteristic do end) characteristics = - Enum.reduce_while(characteristic_updates, {:ok, []}, fn {characteristic, value}, {:ok, acc} -> + Enum.reduce_while(characteristic_updates, {:ok, []}, fn {characteristic, value}, + {:ok, acc} -> case Provider.update_characteristic(characteristic, %{value: value}) do {:ok, characteristic} -> {:cont, {:ok, [characteristic | acc]}} diff --git a/lib/diffo/provider/components/instance/extension/feature.ex b/lib/diffo/provider/components/instance/extension/feature.ex index 4aa0da3..a67ca29 100644 --- a/lib/diffo/provider/components/instance/extension/feature.ex +++ b/lib/diffo/provider/components/instance/extension/feature.ex @@ -40,7 +40,8 @@ defmodule Diffo.Provider.Instance.Feature do {:ok, []}, fn %{name: name, is_enabled?: isEnabled, characteristics: characteristics}, {:ok, acc} -> characteristic_ids = - Enum.reduce_while(characteristics, {:ok, []}, fn %{name: name, value_type: value_type}, {:ok, ids} -> + Enum.reduce_while(characteristics, {:ok, []}, fn %{name: name, value_type: value_type}, + {:ok, ids} -> try do attrs = case value_type do diff --git a/lib/diffo/provider/components/instance/extension/relationship.ex b/lib/diffo/provider/components/instance/extension/relationship.ex index e637dd6..50c1264 100644 --- a/lib/diffo/provider/components/instance/extension/relationship.ex +++ b/lib/diffo/provider/components/instance/extension/relationship.ex @@ -27,12 +27,12 @@ defmodule Diffo.Provider.Instance.Relationship do _ -> Enum.reduce_while(relationships, :ok, fn %{ - id: id, - alias: name, - type: type, - direction: direction - }, - :ok -> + id: id, + alias: name, + type: type, + direction: direction + }, + :ok -> attrs = case direction do :reverse -> %{source_id: id, party_id: result.id, alias: name, type: type} diff --git a/lib/diffo/provider/components/instance/util.ex b/lib/diffo/provider/components/instance/util.ex index b555255..0e20761 100644 --- a/lib/diffo/provider/components/instance/util.ex +++ b/lib/diffo/provider/components/instance/util.ex @@ -68,6 +68,7 @@ defmodule Diffo.Provider.Instance.Util do nil -> result state -> Diffo.Util.set(result, :lifecycleState, state) end + # |> Diffo.Util.ensure_not_nil(:administrativeState, record.resource_administrative_state) # |> Diffo.Util.ensure_not_nil(:operationalState, record.resource_operational_state) # |> Diffo.Util.ensure_not_nil(:resourceStatus, record.resource_status) diff --git a/lib/diffo/provider/extension.ex b/lib/diffo/provider/extension.ex index aee67ad..3343df6 100644 --- a/lib/diffo/provider/extension.ex +++ b/lib/diffo/provider/extension.ex @@ -339,7 +339,13 @@ defmodule Diffo.Provider.Extension do end """ ], - entities: [@party_entity, @parties_entity, @party_ref_entity, @party_role_entity, @inherited_party_entity] + entities: [ + @party_entity, + @parties_entity, + @party_ref_entity, + @party_role_entity, + @inherited_party_entity + ] } # ── places ───────────────────────────────────────────────────────────────── @@ -441,7 +447,13 @@ defmodule Diffo.Provider.Extension do end """ ], - entities: [@place_entity, @places_entity, @place_ref_entity, @place_role_entity, @inherited_place_entity] + entities: [ + @place_entity, + @places_entity, + @place_ref_entity, + @place_role_entity, + @inherited_place_entity + ] } # ── instances ────────────────────────────────────────────────────────────── diff --git a/lib/diffo/provider/extension/transformers/transform_behaviour.ex b/lib/diffo/provider/extension/transformers/transform_behaviour.ex index 3e41769..3ab8a5c 100644 --- a/lib/diffo/provider/extension/transformers/transform_behaviour.ex +++ b/lib/diffo/provider/extension/transformers/transform_behaviour.ex @@ -127,5 +127,4 @@ defmodule Diffo.Provider.Extension.Transformers.TransformBehaviour do end end) end - end diff --git a/lib/diffo/provider/extension/transformers/transform_inherited_refs.ex b/lib/diffo/provider/extension/transformers/transform_inherited_refs.ex index 8ef4c32..f281927 100644 --- a/lib/diffo/provider/extension/transformers/transform_inherited_refs.ex +++ b/lib/diffo/provider/extension/transformers/transform_inherited_refs.ex @@ -33,7 +33,8 @@ defmodule Diffo.Provider.Extension.Transformers.TransformInheritedRefs do calc = %Ash.Resource.Calculation{ name: decl.role, type: {:array, :map}, - calculation: {Diffo.Provider.Calculations.InheritedPlace, [via: via, source_role: decl.source_role]}, + calculation: + {Diffo.Provider.Calculations.InheritedPlace, [via: via, source_role: decl.source_role]}, description: "Inherited place via assignment alias traversal", arguments: [], public?: true, @@ -50,7 +51,8 @@ defmodule Diffo.Provider.Extension.Transformers.TransformInheritedRefs do calc = %Ash.Resource.Calculation{ name: decl.role, type: {:array, :map}, - calculation: {Diffo.Provider.Calculations.InheritedParty, [via: via, source_role: decl.source_role]}, + calculation: + {Diffo.Provider.Calculations.InheritedParty, [via: via, source_role: decl.source_role]}, description: "Inherited party via assignment alias traversal", arguments: [], public?: true, diff --git a/lib/diffo/provider/validations/validate_relationship_permitted.ex b/lib/diffo/provider/validations/validate_relationship_permitted.ex index 3e7b18f..8601c27 100644 --- a/lib/diffo/provider/validations/validate_relationship_permitted.ex +++ b/lib/diffo/provider/validations/validate_relationship_permitted.ex @@ -50,7 +50,11 @@ defmodule Diffo.Provider.Validations.ValidateRelationshipPermitted do end :error -> - [[field: :relationships, message: "could not resolve target resource for id #{inspect(target_id)}"] + [ + [ + field: :relationships, + message: "could not resolve target resource for id #{inspect(target_id)}" + ] ] end end) @@ -99,7 +103,8 @@ defmodule Diffo.Provider.Validations.ValidateRelationshipPermitted do if role in roles do :ok else - {:error, "relationship role #{inspect(role)} is not permitted as #{direction} on this resource"} + {:error, + "relationship role #{inspect(role)} is not permitted as #{direction} on this resource"} end end end diff --git a/test/provider/extension/inherited_refs_test.exs b/test/provider/extension/inherited_refs_test.exs index d99ca41..1e40da1 100644 --- a/test/provider/extension/inherited_refs_test.exs +++ b/test/provider/extension/inherited_refs_test.exs @@ -44,7 +44,11 @@ defmodule Diffo.Provider.Extension.InheritedRefsTest do {:ok, _card} = Servo.assign_port(card, %{ - assignment: %Assignment{assignee_id: service.id, operation: :auto_assign, alias: :primary} + assignment: %Assignment{ + assignee_id: service.id, + operation: :auto_assign, + alias: :primary + } }) service = Ash.load!(service, [:primary], domain: Servo) @@ -105,12 +109,20 @@ defmodule Diffo.Provider.Extension.InheritedRefsTest do {:ok, _card_a} = Servo.assign_port(card_a, %{ - assignment: %Assignment{assignee_id: service.id, operation: :auto_assign, alias: :primary} + assignment: %Assignment{ + assignee_id: service.id, + operation: :auto_assign, + alias: :primary + } }) {:ok, _card_b} = Servo.assign_port(card_b, %{ - assignment: %Assignment{assignee_id: service.id, operation: :auto_assign, alias: :secondary} + assignment: %Assignment{ + assignee_id: service.id, + operation: :auto_assign, + alias: :secondary + } }) service = Ash.load!(service, [:primary], domain: Servo) diff --git a/test/provider/extension/relationship_dsl_test.exs b/test/provider/extension/relationship_dsl_test.exs index a3c4678..465181d 100644 --- a/test/provider/extension/relationship_dsl_test.exs +++ b/test/provider/extension/relationship_dsl_test.exs @@ -194,6 +194,5 @@ defmodule Diffo.Provider.Extension.RelationshipDslTest do assert {:error, error} = result assert Exception.message(error) =~ "not permitted as target" end - end end