Skip to content

Commit ff05068

Browse files
committed
phase 6 - AssignerRelationship extends BaseRelationship
1 parent 51e43e6 commit ff05068

12 files changed

Lines changed: 258 additions & 155 deletions

File tree

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ lib/diffo/provider/
4040
assigner/
4141
assigner.ex # Diffo.Provider.Assigner — assign/3 (pools do) and assign/4
4242
assignable_characteristic.ex # AssignableCharacteristic — pool bounds + algorithm
43+
assigned_to_relationship.ex # AssignedToRelationship — assignedTo edges (pool/thing/assigned)
4344
base_instance.ex # Ash Fragment for Instance resources
4445
base_party.ex # Ash Fragment for Party resources
4546
base_place.ex # Ash Fragment for Place resources
4647
components/
4748
base_characteristic.ex # Ash Fragment for typed characteristic resources
49+
base_relationship.ex # Ash Fragment for shared Relationship structure
4850
calculations/
4951
characteristic_value.ex # Calculation: builds .Value TypedStruct from record fields
5052
assigned_values.ex # Calculation: returns list of assigned integers for a pool+thing
@@ -162,6 +164,9 @@ mix test --max-failures 5 # stop early
162164
- Using the removed `AssignableValue` TypedStruct — it no longer exists; use `pools do`.
163165
- Calling `Assigner.assign/4` when a `pools do` declaration exists — prefer `Assigner.assign/3` which looks up the thing automatically.
164166
- Forgetting to call `Pool.update_pools/3` in `:define` actions when the resource has `pools do` — pool bounds (`first`, `last`, `algorithm`) are set here.
167+
- Using `characteristic :pool_name, Diffo.Provider.AssignedToRelationship``AssignedToRelationship` is not a characteristic; use `pools do / pool :name, :thing / end` instead.
168+
- Querying `Diffo.Provider.Relationship` for assignment records — assignment relationships are on `Diffo.Provider.AssignedToRelationship`; access them via `instance.assignments`.
169+
- Filtering `instance.forward_relationships` for `type == :assignedTo` — those records no longer exist there; use `instance.assignments` directly.
165170
- Calling `build_before/1` or `build_after/2` in actions — these run automatically.
166171
- Declaring `:specified_by`, `:features`, `:characteristics` as action arguments.
167172
- Editing `documentation/dsls/DSL-Diffo.Provider.Extension.md` — it is Spark-generated;

documentation/how_to/use_diffo_provider_extension.livemd

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ arguments automatically onto that action.
153153

154154
Each characteristic is a dedicated Ash resource using the `Diffo.Provider.BaseCharacteristic` fragment. It carries direct typed attributes and a `:value` calculation that builds a companion `<Module>.Value` TypedStruct for ordered JSON encoding. The TypedStruct uses [AshJason.TypedStruct](https://hexdocs.pm/ash_jason/) to control field order in the JSON output.
155155

156-
For partial resource allocation and assignment we've created `Diffo.Provider.Assigner`. The host resource declares a `pools do` section listing each assignable pool by name and the kind of thing being assigned. Pool bounds (first/last value, algorithm) are set via a `:define` action. Assignment to Services or Resources is via `type: :assignedTo` Relationships that carry the assigned value directly on the Relationship node.
156+
For partial resource allocation and assignment we've created `Diffo.Provider.Assigner`. The host resource declares a `pools do` section listing each assignable pool by name and the kind of thing being assigned. Pool bounds (first/last value, algorithm) are set via a `:define` action. Each assignment is stored as a `Diffo.Provider.AssignedToRelationship` node (Neo4j label `:AssignmentRelationship`) carrying `pool`, `thing`, and the `assigned` value. These are distinct from regular TMF `Diffo.Provider.Relationship` nodes and are accessible on an instance via `instance.assignments`.
157157

158158
Let's imagine a Compute domain which operates GPU and NPU resources. We want to expose a Cluster composite resource which can be dynamically composed of a number of GPU and NPU cores.
159159

@@ -763,7 +763,7 @@ gpu_1 = Compute.define_gpu!(gpu_1, %{characteristic_value_updates: gpu_attrs})
763763
gpu_2 = Compute.define_gpu!(gpu_2, %{characteristic_value_updates: gpu_attrs})
764764
```
765765

766-
The `:cores` pool is backed by an `AssignableCharacteristic` node that records the range bounds and algorithm. Free cores are computed at assignment time from the count of existing `:assignedTo` relationships — there is no stored `free` counter. We can render one as json:
766+
The `:cores` pool is backed by an `AssignableCharacteristic` node that records the range bounds and algorithm. Free cores are computed at assignment time from the count of existing `AssignmentRelationship` records — there is no stored `free` counter. We can render one as json:
767767

768768
```elixir
769769
Jason.encode!(gpu_1, pretty: true) |> IO.puts
@@ -783,16 +783,16 @@ gpu_1 = Compute.assign_gpu_core!(gpu_1, assignment)
783783
gpu_2 = Compute.assign_gpu_core!(gpu_2, assignment)
784784
```
785785

786-
Now our cluster should have a core from each GPU. Check in the Neo4j browser for `:assignedTo` Relationship nodes from `gpu_1` and `gpu_2` to `cluster_1`. There should be four — each Relationship carries `pool: :cores`, `thing: :core`, and the `assigned` integer value (e.g. 1, 2, 3 from gpu_1 and 1 from gpu_2).
786+
Now our cluster should have a core from each GPU. Check in the Neo4j browser for `:AssignmentRelationship` nodes from `gpu_1` and `gpu_2` to `cluster_1`. There should be four — each carries `pool: :cores`, `thing: :core`, and the `assigned` integer value (e.g. 1, 2, 3 from gpu_1 and 1 from gpu_2).
787787

788-
The GPU's `forward_relationships` include each `:assignedTo` relationship, showing the assigned core number in the JSON encoding as a `resourceRelationshipCharacteristic`:
788+
The GPU's `assignments` hold each assignment, showing the assigned core number in the JSON encoding as a `resourceRelationshipCharacteristic`:
789789

790790
```elixir
791791
Jason.encode!(gpu_1, pretty: true) |> IO.puts
792792
```
793793

794-
Make sure you have a look at it in the neo4j browser. There should be Relationship nodes with a role of :assignedTo from each GPU resource instance to the cluster_1 resource instance. Each Relationship should be defined by a Characteristic with the assigned core number.
795-
There is no central assignment table, rather the relationships ARE the assignments.
794+
Make sure you have a look at it in the neo4j browser. There should be `:AssignmentRelationship` nodes from each GPU resource instance to the `cluster_1` resource instance, each carrying the assigned core number.
795+
There is no central assignment table the `AssignedToRelationship` nodes ARE the assignments. They are separate from the regular `:Relationship` nodes used for TMF service/resource relationships, and are accessible in Elixir via `instance.assignments`.
796796

797797
As an exercise, clone the GPU resource to create an NPU resource and assign some NPU cores from it to your cluster. Check that the assigned NPU cores are unique.
798798

@@ -805,7 +805,7 @@ In this tutorial you've used Diffo's unified `provider do` extension to define a
805805

806806
- A composite Cluster resource that receives GPU cores via `Diffo.Provider.Assigner`
807807
- A GPU resource using `pools do` to declare the `:cores` assignable pool — `pool :cores, :core` replaces the old `characteristic :cores, AssignableValue` pattern
808-
- Assignment stored directly on `:assignedTo` Relationship nodes (no separate Characteristic nodes for assignments)
808+
- Assignments stored on `Diffo.Provider.AssignedToRelationship` nodes (Neo4j label `:AssignmentRelationship`, distinct from TMF `:Relationship` nodes); accessible via `instance.assignments`
809809
- `Tenant` and `Engineer` Party kinds declared with `provider do` that express which instances they operate and manage
810810
- A `DataCentre` Place kind that declares the instances located at it
811811

lib/diffo/provider.ex

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ defmodule Diffo.Provider do
5959

6060
resource Diffo.Provider.Relationship do
6161
define :create_relationship, action: :create
62-
define :create_assignment_relationship, action: :create_assignment
6362

6463
define :get_relationship_by_id, action: :read, get_by: :id
6564
define :list_relationships, action: :list
@@ -78,6 +77,12 @@ defmodule Diffo.Provider do
7877
define :delete_relationship, action: :destroy
7978
end
8079

80+
resource Diffo.Provider.AssignedToRelationship do
81+
define :create_assigned_to_relationship, action: :create_assignment
82+
define :get_assigned_to_relationship_by_id, action: :read, get_by: :id
83+
define :delete_assigned_to_relationship, action: :destroy
84+
end
85+
8186
resource Diffo.Provider.AssignableCharacteristic do
8287
define :create_assignable_characteristic, action: :create
8388
define :get_assignable_characteristic_by_id, action: :read, get_by: :id
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule Diffo.Provider.AssignedToRelationship do
6+
@moduledoc """
7+
Ash Resource for a pool assignment relationship.
8+
9+
Carries the assignment attributes (`pool`, `thing`, `assigned`) that link a
10+
source instance to an assignee instance. Stored as an `:AssignedToRelationship`
11+
Neo4j node, distinct from the `:Relationship` nodes used for TMF service/resource
12+
relationships. Accessible on an instance via `instance.assignments`.
13+
14+
Created by `Diffo.Provider.Assigner` via `Diffo.Provider.create_assigned_to_relationship/1`.
15+
"""
16+
use Ash.Resource,
17+
fragments: [Diffo.Provider.BaseRelationship],
18+
otp_app: :diffo,
19+
domain: Diffo.Provider
20+
21+
resource do
22+
description "An Ash Resource for a pool assignment relationship"
23+
plural_name :assigned_to_relationships
24+
end
25+
26+
neo4j do
27+
relate [
28+
{:source, :RELATES, :incoming, :Instance},
29+
{:target, :RELATES, :outgoing, :Instance}
30+
]
31+
end
32+
33+
jason do
34+
pick [:type]
35+
36+
customize fn result, record ->
37+
target_type = Map.get(record, :target_type)
38+
39+
reference = %Diffo.Provider.Reference{
40+
id: record.target_id,
41+
href: Map.get(record, :target_href)
42+
}
43+
44+
list_name =
45+
Diffo.Provider.Relationship.derive_relationship_characteristic_list_name(target_type)
46+
47+
result
48+
|> Diffo.Util.set(target_type, reference)
49+
|> Diffo.Util.set(list_name, [%{name: record.thing, value: record.assigned}])
50+
end
51+
52+
order [
53+
:type,
54+
:service,
55+
:resource,
56+
:serviceRelationshipCharacteristic,
57+
:resourceRelationshipCharacteristic
58+
]
59+
end
60+
61+
actions do
62+
create :create_assignment do
63+
description "creates an assignedTo relationship with pool/thing/assigned attributes"
64+
accept [:pool, :thing, :assigned]
65+
66+
argument :source_id, :uuid
67+
argument :target_id, :string
68+
69+
change set_attribute(:type, :assignedTo)
70+
change manage_relationship(:source_id, :source, type: :append)
71+
change manage_relationship(:target_id, :target, type: :append)
72+
change Diffo.Changes.DetailRelationship
73+
end
74+
end
75+
76+
attributes do
77+
attribute :pool, :atom do
78+
description "the pool name on the source instance"
79+
allow_nil? true
80+
public? true
81+
end
82+
83+
attribute :thing, :atom do
84+
description "the kind of thing being assigned within the pool"
85+
allow_nil? true
86+
public? true
87+
end
88+
89+
attribute :assigned, :integer do
90+
description "the assigned value from the pool"
91+
allow_nil? true
92+
public? true
93+
end
94+
end
95+
96+
identities do
97+
identity :unique_assignment, [:source_id, :target_id, :pool, :thing, :assigned]
98+
end
99+
100+
preparations do
101+
prepare build(sort: [created_at: :asc])
102+
end
103+
end

lib/diffo/provider/assigner/assigner.ex

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
defmodule Diffo.Provider.Assigner do
66
@moduledoc """
7-
Helper to perform Assignment using Relationship attributes.
7+
Helper to perform Assignment using `Diffo.Provider.AssignedToRelationship`.
88
9-
Assignment state is stored directly on `Diffo.Provider.Relationship` nodes
10-
(pool, thing, assigned) rather than creating a separate Characteristic node.
9+
Assignment state is stored on `AssignedToRelationship` nodes (pool, thing, assigned),
10+
distinct from regular TMF `Diffo.Provider.Relationship` nodes.
1111
"""
1212
alias Diffo.Provider.AssignableCharacteristic
13-
alias Diffo.Provider.Relationship
13+
alias Diffo.Provider.AssignedToRelationship
1414

1515
@doc """
1616
Assign a thing using the pool declared via `pools do` on the instance module.
@@ -66,7 +66,7 @@ defmodule Diffo.Provider.Assigner do
6666
defp relate_is_assigned(result, pool, thing, value, assignee_id)
6767
when is_struct(result) and is_atom(pool) and is_atom(thing) and is_integer(value) and
6868
is_bitstring(assignee_id) do
69-
case Diffo.Provider.create_assignment_relationship(%{
69+
case Diffo.Provider.create_assigned_to_relationship(%{
7070
pool: pool,
7171
thing: thing,
7272
assigned: value,
@@ -103,15 +103,14 @@ defmodule Diffo.Provider.Assigner do
103103
end
104104

105105
defp find_assignment(source_id, target_id, pool, thing, value) do
106-
Relationship
106+
AssignedToRelationship
107107
|> Ash.Query.new()
108108
|> Ash.Query.filter_input(
109109
source_id: source_id,
110110
target_id: target_id,
111111
pool: pool,
112112
thing: thing,
113-
assigned: value,
114-
type: :assignedTo
113+
assigned: value
115114
)
116115
|> Ash.read_one(domain: Diffo.Provider)
117116
end

lib/diffo/provider/components/base_instance.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ defmodule Diffo.Provider.BaseInstance do
188188
{:process_statuses, :STATUSES, :incoming, :ProcessStatus},
189189
{:forward_relationships, :RELATES, :outgoing, :Relationship},
190190
{:reverse_relationships, :RELATES, :incoming, :Relationship},
191+
{:assignments, :RELATES, :outgoing, :AssignedToRelationship},
191192
{:features, :HAS, :outgoing, :Feature},
192193
{:characteristics, :HAS, :outgoing, :Characteristic},
193194
{:entities, :RELATES, :outgoing, :EntityRef},
@@ -209,6 +210,7 @@ defmodule Diffo.Provider.BaseInstance do
209210
:specification,
210211
:process_statuses,
211212
:forward_relationships,
213+
:assignments,
212214
:features,
213215
:characteristics,
214216
:entities,
@@ -407,6 +409,12 @@ defmodule Diffo.Provider.BaseInstance do
407409
public? true
408410
end
409411

412+
has_many :assignments, Diffo.Provider.AssignedToRelationship do
413+
description "the instance's outgoing pool assignment relationships"
414+
destination_attribute :source_id
415+
public? true
416+
end
417+
410418
has_many :features, Diffo.Provider.Feature do
411419
description "the instance's collection of defining features"
412420
public? true
@@ -655,6 +663,7 @@ defmodule Diffo.Provider.BaseInstance do
655663
:specification,
656664
:process_statuses,
657665
:forward_relationships,
666+
:assignments,
658667
:entities,
659668
:notes,
660669
:features,
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule Diffo.Provider.BaseRelationship do
6+
@moduledoc """
7+
Ash Resource Fragment which is the shared foundation for TMF Relationship resources.
8+
9+
Provides the common attributes, relationships, validations, and actions shared
10+
between `Diffo.Provider.Relationship` (TMF service/resource relationships) and
11+
`Diffo.Provider.AssignedToRelationship` (pool assignment relationships).
12+
13+
## Common attributes
14+
15+
- `id` — uuid4 primary key
16+
- `type` — relationship type atom
17+
- `target_href` — denormalised target href (set by the `DetailRelationship` change)
18+
- `target_type` — denormalised target type (`:service` or `:resource`)
19+
- `created_at`, `updated_at` — timestamps
20+
21+
## Common Ash relationships
22+
23+
- `belongs_to :source, Diffo.Provider.Instance`
24+
- `belongs_to :target, Diffo.Provider.Instance`
25+
"""
26+
use Spark.Dsl.Fragment,
27+
of: Ash.Resource,
28+
otp_app: :diffo,
29+
domain: Diffo.Provider,
30+
data_layer: AshNeo4j.DataLayer,
31+
extensions: [AshJason.Resource]
32+
33+
attributes do
34+
uuid_primary_key :id do
35+
description "a uuid4, unique to this relationship, generated by default"
36+
public? true
37+
end
38+
39+
attribute :type, :atom do
40+
description "the type of the relationship from the source to the target"
41+
allow_nil? false
42+
public? true
43+
end
44+
45+
attribute :target_href, :string do
46+
description "the target href, denormalised from the target instance"
47+
allow_nil? true
48+
writable? false
49+
public? true
50+
end
51+
52+
attribute :target_type, :atom do
53+
description "the target type, denormalised from the target instance"
54+
allow_nil? true
55+
writable? false
56+
public? true
57+
end
58+
59+
create_timestamp :created_at
60+
update_timestamp :updated_at
61+
end
62+
63+
relationships do
64+
belongs_to :source, Diffo.Provider.Instance do
65+
description "the source instance which originates this relationship"
66+
allow_nil? false
67+
public? true
68+
end
69+
70+
belongs_to :target, Diffo.Provider.Instance do
71+
description "the target instance which is the destination of this relationship"
72+
allow_nil? false
73+
public? true
74+
end
75+
end
76+
77+
validations do
78+
validate {Diffo.Validations.IsUuid4OrNil, attribute: :source_id}, on: :create
79+
validate {Diffo.Validations.IsUuid4OrNil, attribute: :target_id}, on: :create
80+
end
81+
82+
actions do
83+
defaults [:read, :destroy]
84+
end
85+
end

lib/diffo/provider/components/calculations/assigned_values.ex

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,12 @@ defmodule Diffo.Provider.Calculations.AssignedValues do
1414
thing = context.arguments[:thing]
1515

1616
Enum.map(records, fn record ->
17-
Diffo.Provider.Relationship
17+
Diffo.Provider.AssignedToRelationship
1818
|> Ash.Query.new()
1919
|> Ash.Query.filter_input(
2020
source_id: record.instance_id,
2121
pool: record.name,
22-
thing: thing,
23-
type: :assignedTo
22+
thing: thing
2423
)
2524
|> Ash.read!(domain: Diffo.Provider)
2625
|> Enum.map(& &1.assigned)

0 commit comments

Comments
 (0)