Skip to content
Merged

dev #136

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
188 changes: 188 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<!--
SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>

SPDX-License-Identifier: MIT
-->

# AGENTS.md — Diffo

AI agent guidance for the Diffo source repository.

## What this project is

Diffo is a Telecommunications Management Forum (TMF) Service and Resource Manager, built
on [Ash Framework](https://www.ash-hq.org/) + [AshNeo4j](https://github.com/diffo-dev/ash_neo4j) + [Neo4j](https://github.com/neo4j/neo4j). It models TMF 638/639 Service and Resource inventory and provides a Spark DSL for defining domain-specific instance, party, and place kinds.

## Before making changes

1. Read `usage-rules.md` — Diffo-specific DSL rules.
2. Read `CLAUDE.md` — dependency usage rules (Ash, Elixir, OTP, AshNeo4j, Spark).
3. Consult the skill at `.claude/skills/diffo-framework/` for Ash ecosystem patterns.

## Project structure

```
lib/diffo/provider/
extension.ex # Unified Spark DSL extension (provider do)
extension/
info.ex # Runtime introspection via Spark.InfoGenerator
characteristic.ex # Characteristic build helpers
feature.ex # Feature build helpers
pool.ex # Pool struct + create_pools/2 + update_pools/3
instance_role.ex # InstanceRole struct
party_declaration.ex # PartyDeclaration struct
place_declaration.ex # PlaceDeclaration struct
party_role.ex # PartyRole struct (Party/Place kinds)
place_role.ex # PlaceRole struct (Party/Place kinds)
persisters/ # Spark transformers — bake DSL state into module
transformers/ # TransformBehaviour — action argument injection
verifiers/ # Compile-time DSL correctness checks
assigner/
assigner.ex # Diffo.Provider.Assigner — assign/3 (pools do) and assign/4
assignable_characteristic.ex # AssignableCharacteristic — pool bounds + algorithm
assigned_to_relationship.ex # AssignedToRelationship — assignedTo edges (pool/thing/assigned)
base_instance.ex # Ash Fragment for Instance resources
base_party.ex # Ash Fragment for Party resources
base_place.ex # Ash Fragment for Place resources
components/
base_characteristic.ex # Ash Fragment for typed characteristic resources
base_relationship.ex # Ash Fragment for shared Relationship structure
calculations/
characteristic_value.ex # Calculation: builds .Value TypedStruct from record fields
assigned_values.ex # Calculation: returns list of assigned integers for a pool+thing
instance/extension.ex # Thin marker (sections: []) — kind identification
party/extension.ex # Thin marker
place/extension.ex # Thin marker

test/provider/extension/ # All provider extension tests
instance_transformer_test.exs
party_transformer_test.exs
place_transformer_test.exs
instance_verifier_test.exs
party_verifier_test.exs
place_verifier_test.exs
party_test.exs # Integration: parties enforcement
place_test.exs # Integration: places enforcement
specification_test.exs # Integration: spec roundtrip
characteristic_test.exs # Integration: characteristic creation
feature_test.exs # Integration: feature creation
assigner_test.exs # Integration: resource assignment

test/support/
resources/ # Test domain resources (Shelf, Card, Organization, etc.)
domains/ # Test domains (Servo, Nbn)
```

## The unified `provider do` DSL

All DSL declarations use a single `provider do` section — there is no `structure do`,
top-level `behaviour do`, or bare `instances/parties/places do`.

### Instance resources (`BaseInstance`)

```elixir
provider do
specification do
id "da9b207a-..." # stable UUID4 — never change after first commit
name "myService" # camelCase
type :serviceSpecification
major_version 1
description "..."
category "..."
end

characteristics do
characteristic :slot_value, MyApp.SlotCharacteristic
characteristic :ports, {:array, MyApp.PortCharacteristic}
end

pools do
pool :cores, :core # assignable pool; thing name is :core
pool :vlans, :vlan
end

features do
feature :advanced_routing, is_enabled?: false do
characteristic :policy, MyApp.RoutingPolicy
end
end

parties do
party :provider, MyApp.RSP # singular, direct edge
parties :engineers, MyApp.Engineer, constraints: [min: 1, max: 5]
party_ref :owner, MyApp.Organization # no direct edge
party :operator, MyApp.RSP, calculate: :derive_operator
end

places do
place :installation_site, MyApp.GeographicSite
place_ref :billing_address, MyApp.GeographicAddress
end

behaviour do
actions do
create :build # injects :specified_by, :features, :characteristics
end
end
end
```

### Party and Place resources (`BaseParty` / `BasePlace`)

```elixir
provider do
instances do
role :provider, MyApp.BroadbandService
instance_ref :manages, MyApp.InternalService # no direct edge
end
parties do
role :employer, MyApp.Organization
end
places do
role :headquarters, MyApp.GeographicSite
end
end
```

## Running tests

Integration tests require a running Neo4j instance.

```sh
mix test # full suite
mix test test/provider/extension/ # extension tests only
mix test path/to/test.exs:LINE # single test
mix test --max-failures 5 # stop early
```

## Module naming and Neo4j labels

AshNeo4j derives a node label from the **last segment** of the module name. Two resources
whose names end in the same word get the same label, which causes read collisions.

**Rule:** suffix every resource module with its kind so the last segment is unique:
- Instance resources: `MyApp.Instance.WidgetInstance` (not `MyApp.Instance.Widget`)
- Characteristic resources: `MyApp.Characteristic.WidgetCharacteristic` (not `MyApp.Characteristic.Widget`)
- Party/Place resources: follow the same convention if ambiguity is possible.

E.g. `Diffo.Test.Instance.CardInstance` → label `:CardInstance`,
and `Diffo.Test.Characteristic.CardCharacteristic` → label `:CardCharacteristic` — no collision.

## Common agent mistakes

- Using old `structure do` / top-level `instances do` — use `provider do` only.
- Using `party :role, Type, reference: true` — use `party_ref :role, Type`.
- Using a plain `Ash.TypedStruct` as a `characteristic` DSL target — use a `BaseCharacteristic`-derived resource instead; the TypedStruct belongs in `<Module>.Value`.
- Using `characteristic :name, Diffo.Provider.AssignableCharacteristic` for pools — use `pools do / pool :name, :thing / end` instead.
- Using the removed `AssignableValue` TypedStruct — it no longer exists; use `pools do`.
- Calling `Assigner.assign/4` when a `pools do` declaration exists — prefer `Assigner.assign/3` which looks up the thing automatically.
- Forgetting to call `Pool.update_pools/3` in `:define` actions when the resource has `pools do` — pool bounds (`first`, `last`, `algorithm`) are set here.
- Using `characteristic :pool_name, Diffo.Provider.AssignedToRelationship` — `AssignedToRelationship` is not a characteristic; use `pools do / pool :name, :thing / end` instead.
- Querying `Diffo.Provider.Relationship` for assignment records — assignment relationships are on `Diffo.Provider.AssignedToRelationship`; access them via `instance.assignments`.
- Filtering `instance.forward_relationships` for `type == :assignedTo` — those records no longer exist there; use `instance.assignments` directly.
- Calling `build_before/1` or `build_after/2` in actions — these run automatically.
- Declaring `:specified_by`, `:features`, `:characteristics` as action arguments.
- Editing `documentation/dsls/DSL-Diffo.Provider.Extension.md` — it is Spark-generated;
run `mix spark.cheat_sheets` to regenerate it.
- Editing content between `<!-- usage-rules-start -->` markers in `CLAUDE.md` — that is
auto-generated by `mix usage_rules.sync`.
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,31 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline

<!-- changelog -->

## [v0.3.0](https://github.com/diffo-dev/diffo/compare/v0.2.2...v0.3.0) (2026-05-17)

### Breaking Changes

* `Diffo.Provider.Relationship` no longer stores assignment records. Assignment relationships are now on `Diffo.Provider.AssignedToRelationship`. Any existing graph data with `type: :assignedTo` on `Relationship` nodes will need to be migrated.
* `instance.forward_relationships` no longer contains assignment records — use `instance.assignments` instead.
* `Diffo.Provider.create_assignment_relationship` removed — use `Diffo.Provider.create_assigned_to_relationship`.

### Notable Changes

* `Diffo.Provider.BaseRelationship` — new Ash Resource Fragment providing common attributes and behaviour for all relationship types
* `Diffo.Provider.AssignedToRelationship` — new dedicated resource for pool assignment relationships, split out from `Diffo.Provider.Relationship`
* `Diffo.Provider.Relationship` — now TMF-only; `pool`, `thing`, `assigned` attributes and `:create_assignment` action removed
* `instance.assignments` — new `has_many` on `BaseInstance` for pool assignment relationships; included in JSON encoding and default loads
* `Diffo.Provider.BaseCharacteristic` — new Ash Resource Fragment for typed characteristic resources; `ShelfCharacteristic`, `CardCharacteristic` etc. now extend this rather than using plain `Ash.TypedStruct`
* `pools do` DSL — new section on Instance resources replacing the old `characteristic :name, AssignableValue` pattern; generates `pools/0` and `pool/1` introspection functions
* Module naming convention — Instance resources must be suffixed `…Instance`, Characteristic resources `…Characteristic` to avoid Neo4j label collisions (documented in `usage-rules.md` and `AGENTS.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](https://github.com/diffo-dev/diffo/compare/v0.2.1...v0.2.2) (2026-05-08)

## Notable Changes
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Alternatively, add `diffo` to your list of dependencies in `mix.exs` manually:
```elixir
def deps do
[
{:diffo, "~> 0.2.1"}
{:diffo, "~> 0.3.0"}
]
end
```
Expand Down
2 changes: 1 addition & 1 deletion diffo.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ SPDX-License-Identifier: MIT
```elixir
Mix.install(
[
{:diffo, "~> 0.2.2"}
{:diffo, "~> 0.3.0"}
],
consolidate_protocols: false
)
Expand Down
Loading
Loading