You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
source [:provides, :requires] # pipeline — last step wins; omitting defaults to :none
123
+
target :all
124
+
end
125
+
115
126
features do
116
127
feature :advanced_routing, is_enabled?:falsedo
117
128
characteristic :policy, MyApp.RoutingPolicy
@@ -179,6 +190,22 @@ whose names end in the same word get the same label, which causes read collision
179
190
E.g. `Diffo.Test.Instance.CardInstance` → label `:CardInstance`,
180
191
and `Diffo.Test.Characteristic.CardCharacteristic` → label `:CardCharacteristic` — no collision.
181
192
193
+
## Spark transformer vs persister pipeline
194
+
195
+
Spark runs two separate pipelines during compilation, in this order:
196
+
197
+
1.**Transformers** (`transformers:` in the extension) — run in dependency order via `before?`/`after?`. Can read and modify DSL state. May also call `Transformer.persist/3` to bake results — a transformer that had to compute something to do its job should persist that result rather than delegating to a separate persister.
198
+
2.**Persisters** (`persisters:` in the extension) — always run after ALL transformers from ALL extensions. `before?`/`after?` ordering works relative to other persisters only — ordering declarations targeting transformers are silently ignored.
199
+
3.**Verifiers** — read-only, run last.
200
+
201
+
**Rules:**
202
+
- A module that injects into actions, modifies DSL state, or needs to order itself relative to Ash's own transformers belongs in `transformers:`.
203
+
- A module that only reads final DSL state and bakes module functions belongs in `persisters:`.
204
+
- A transformer that needs to expose baked state does not need a separate persister — call `Transformer.persist/3` inline and emit the module function via `Transformer.eval/3`.
205
+
- Do not put a transformer in `persisters:` hoping `after?` declarations will order it relative to transformers — those declarations are silently ignored across pipeline boundaries.
206
+
207
+
**Current state:**`TransformBehaviour` is misregistered under `persisters:` — a known issue tracked for refactoring. New transformers go under `transformers:`.
208
+
182
209
## Common agent mistakes
183
210
184
211
- Using old `structure do` / top-level `instances do` — use `provider do` only.
@@ -188,12 +215,18 @@ and `Diffo.Test.Characteristic.CardCharacteristic` → label `:CardCharacteristi
188
215
- Using the removed `AssignableValue` TypedStruct — it no longer exists; use `pools do`.
189
216
- Calling `Assigner.assign/4` when a `pools do` declaration exists — prefer `Assigner.assign/3` which looks up the thing automatically.
190
217
- Forgetting to call `Pool.update_pools/3` in `:define` actions when the resource has `pools do` — pool bounds (`first`, `last`, `algorithm`) are set here.
191
-
- Using `characteristic :pool_name, Diffo.Provider.AssignedToRelationship` — `AssignedToRelationship`is not a characteristic; use `pools do / pool :name, :thing / end` instead.
192
-
- Querying `Diffo.Provider.Relationship` for assignment records — assignment relationships are on `Diffo.Provider.AssignedToRelationship`; access them via `instance.assignments`.
218
+
- Using `characteristic :pool_name, Diffo.Provider.AssignedToRelationship` — `AssignedToRelationship`no longer exists; use `pools do / pool :name, :thing / end` instead.
219
+
- Querying `Diffo.Provider.Relationship` for assignment records — assignments are stored as `Diffo.Provider.DefinedSimpleRelationship`; access them via `instance.assignments`.
193
220
- Filtering `instance.forward_relationships` for `type == :assignedTo` — those records no longer exist there; use `instance.assignments` directly.
194
221
- Calling `build_before/1` or `build_after/2` in actions — these run automatically.
195
222
- Declaring `:specified_by`, `:features`, `:characteristics` as action arguments.
223
+
- Using module names (e.g. `MyApp.CardInstance`) as role values in `relationships do` — roles are atoms like `:provides`, not module references.
224
+
- 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.
225
+
- 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.
196
226
- Editing `documentation/dsls/DSL-Diffo.Provider.Extension.md` — it is Spark-generated;
197
-
run `mix spark.cheat_sheets` to regenerate it.
227
+
run `mix spark.cheat_sheets` to regenerate it. Whenever you add, rename, or remove a DSL
228
+
entity or section, also check `.formatter.exs` — new entity names must be added to
229
+
`spark_locals_without_parens` (with each arity) so the Spark formatter omits parentheses.
230
+
Run `mix format` afterward to verify.
198
231
- Editing content between `<!-- usage-rules-start -->` markers in `CLAUDE.md` — that is
Copy file name to clipboardExpand all lines: documentation/dsls/DSL-Diffo.Provider.Extension.md
+72Lines changed: 72 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -111,6 +111,9 @@ Provider DSL — structure, roles, and behaviour for this resource kind
111
111
*[instances](#provider-instances)
112
112
* role
113
113
* instance_ref
114
+
*[relationships](#provider-relationships)
115
+
* source
116
+
* target
114
117
*[behaviour](#provider-behaviour)
115
118
* actions
116
119
* create
@@ -712,6 +715,75 @@ Declares a reference instance role — no direct edge created, reachable by grap
712
715
Target: `Diffo.Provider.Extension.InstanceRole`
713
716
714
717
718
+
### provider.relationships
719
+
Relationship role permissions for this Instance — declares which aliases it may participate in as source or target. Omitting defaults to `:none` per direction.
|[`roles`](#provider-relationships-source-roles){: #provider-relationships-source-roles .spark-required} |`any`||`:all`, `:none`, or a list of role name atoms. |
|[`roles`](#provider-relationships-target-roles){: #provider-relationships-target-roles .spark-required} |`any`||`:all`, `:none`, or a list of role name atoms. |
779
+
780
+
781
+
782
+
783
+
784
+
785
+
786
+
715
787
### provider.behaviour
716
788
Defines the behavioural wiring for the Instance — actions, and in future triggers
Copy file name to clipboardExpand all lines: documentation/how_to/use_diffo_provider_extension.livemd
+15-5Lines changed: 15 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -153,7 +153,7 @@ arguments automatically onto that action.
153
153
154
154
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.
155
155
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`.
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.DefinedSimpleRelationship` node carrying `type: :assignedTo` and a single `NameValuePrimitive` characteristic holding the thing name and assigned value. These are distinct from regular TMF `Diffo.Provider.Relationship` nodes and are accessible on an instance via `instance.assignments`.
157
157
158
158
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.
159
159
@@ -278,6 +278,11 @@ defmodule Diffo.Compute.Cluster do
278
278
place :data_centre, Diffo.Compute.DataCentre
279
279
end
280
280
281
+
relationships do
282
+
source :all
283
+
target :all
284
+
end
285
+
281
286
behaviour do
282
287
actions do
283
288
create :build
@@ -426,6 +431,11 @@ defmodule Diffo.Compute.GPU do
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).
796
+
Now our cluster should have a core from each GPU. Check in the Neo4j browser for `:DefinedSimpleRelationship` nodes from `gpu_1` and `gpu_2` to `cluster_1`. There should be four — each has `type: :assignedTo` and a single characteristic carrying the thing name (`:core`) and the assigned integer value (e.g. 1, 2, 3 from gpu_1 and 1 from gpu_2).
787
797
788
798
The GPU's `assignments` hold each assignment, showing the assigned core number in the JSON encoding as a `resourceRelationshipCharacteristic`:
789
799
790
800
```elixir
791
801
Jason.encode!(gpu_1, pretty:true) |>IO.puts
792
802
```
793
803
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`.
804
+
Make sure you have a look at it in the neo4j browser. There should be `:DefinedSimpleRelationship` nodes from each GPU resource instance to the `cluster_1` resource instance, each carrying the assigned core number.
805
+
There is no central assignment table — the `DefinedSimpleRelationship` 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`.
796
806
797
807
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.
798
808
@@ -805,7 +815,7 @@ In this tutorial you've used Diffo's unified `provider do` extension to define a
805
815
806
816
- A composite Cluster resource that receives GPU cores via `Diffo.Provider.Assigner`
807
817
- A GPU resource using `pools do` to declare the `:cores` assignable pool — `pool :cores, :core` replaces the old `characteristic :cores, AssignableValue` pattern
808
-
- Assignments stored on`Diffo.Provider.AssignedToRelationship` nodes (Neo4j label `:AssignmentRelationship`, distinct from TMF `:Relationship` nodes); accessible via `instance.assignments`
818
+
- Assignments stored as`Diffo.Provider.DefinedSimpleRelationship` records with `type: :assignedTo` (distinct from TMF `Relationship` nodes); accessible via `instance.assignments`
809
819
-`Tenant` and `Engineer` Party kinds declared with `provider do` that express which instances they operate and manage
810
820
- A `DataCentre` Place kind that declares the instances located at it
0 commit comments