Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
28 changes: 23 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 `<!-- usage-rules-start -->` 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
Expand Down
75 changes: 73 additions & 2 deletions documentation/dsls/DSL-Diffo.Provider.Extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions lib/diffo/provider/assigner/assigner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 10 additions & 3 deletions lib/diffo/provider/components/assignment_relationship.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}]
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]}}
Expand Down
3 changes: 2 additions & 1 deletion lib/diffo/provider/components/instance/extension/feature.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions lib/diffo/provider/components/instance/extension/relationship.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions lib/diffo/provider/components/instance/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 14 additions & 2 deletions lib/diffo/provider/extension.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 ─────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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 ──────────────────────────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,4 @@ defmodule Diffo.Provider.Extension.Transformers.TransformBehaviour do
end
end)
end

end
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Loading
Loading