diff --git a/.gitignore b/.gitignore index 9481f4e..3fdc82c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ diffo-*.tar .DS_Store # Agent related -.claude/* \ No newline at end of file +.claude/* +CLAUDE.md \ No newline at end of file diff --git a/mix.exs b/mix.exs index d412f59..3135d3a 100644 --- a/mix.exs +++ b/mix.exs @@ -26,7 +26,8 @@ defmodule Diffo.MixProject do docs: &docs/0, deps: deps(), aliases: aliases(), - consolidate_protocols: Mix.env() != :dev + consolidate_protocols: Mix.env() != :dev, + usage_rules: usage_rules() ] end @@ -93,9 +94,27 @@ defmodule Diffo.MixProject do ] end + defp usage_rules do + [ + file: "CLAUDE.md", + usage_rules: ["usage_rules:all"], + skills: [ + location: ".claude/skills", + build: [ + "diffo-framework": [ + description: + "Use when working with Diffo or its underlying Ash ecosystem. Consult when making any domain, resource, or provider changes.", + usage_rules: [:ash, :ash_neo4j, :spark, :reactor, :igniter] + ] + ] + ] + ] + end + # Run "mix help deps" to learn about dependencies. defp deps do [ + {:usage_rules, "~> 1.2", only: [:dev]}, {:ash_outstanding, "~> 0.2.3"}, {:ash_jason, "~> 3.0"}, {:ash_state_machine, "~> 0.2.12"}, diff --git a/mix.lock b/mix.lock index c903b26..a8c2edd 100644 --- a/mix.lock +++ b/mix.lock @@ -39,6 +39,7 @@ "stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"}, "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, + "usage_rules": {:hex, :usage_rules, "1.2.6", "a7b3f8d6e5d265701139d5714749c37c54bb82230a4c51ec54a12a1e4769b9d1", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "608411b9876a16a9d62a427dbaf42faf458e4cd0a508b3bd7e5ee71502073582"}, "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yaml_elixir": {:hex, :yaml_elixir, "2.12.1", "d74f2d82294651b58dac849c45a82aaea639766797359baff834b64439f6b3f4", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "d9ac16563c737d55f9bfeed7627489156b91268a3a21cd55c54eb2e335207fed"}, diff --git a/usage-rules.md b/usage-rules.md new file mode 100644 index 0000000..0afb07d --- /dev/null +++ b/usage-rules.md @@ -0,0 +1,181 @@ + + +# Rules for working with Diffo + +## What Diffo is + +Diffo is an Ash Framework layer that models [TM Forum](https://www.tmforum.org/) (TMF) Service +and Resource Management domains on top of a Neo4j graph database. It provides three base +fragments — `BaseInstance`, `BaseParty`, `BasePlace` — plus the `Diffo.Provider.Instance.Extension` +and `Diffo.Provider.Party.Extension` DSLs. Read these rules and the Ash/AshNeo4j usage rules +**before** writing any domain code. + +## The three kinds of domain resource + +| Kind | Base fragment | DSL extension | +|---|---|---| +| Instance (service or resource) | `Diffo.Provider.BaseInstance` | `Diffo.Provider.Instance.Extension` | +| Party (organisation, person, entity) | `Diffo.Provider.BaseParty` | `Diffo.Provider.Party.Extension` | +| Place (site, address, location) | `Diffo.Provider.BasePlace` | `Diffo.Provider.Party.Extension` | + +Do **not** use plain `Ash.Resource` + `AshNeo4j.DataLayer` directly for domain resources. +Always start from the appropriate base fragment: + +```elixir +defmodule MyApp.BroadbandService do + use Ash.Resource, fragments: [Diffo.Provider.BaseInstance], domain: MyApp.Domain + ... +end +``` + +## Instance Extension DSL + +Every resource using `BaseInstance` gains two top-level DSL sections: `structure do` and +`behaviour do`. + +### structure + +`specification do` — declares the TMF Specification for this Instance kind. The `id` is a +**stable UUID4 that must be the same in every environment** — generate it once and never +change it. A new major version requires a new module with a new `id`. + +```elixir +structure do + specification do + id "da9b207a-26c3-451d-8abd-0640c6349979" + name "DSL Access Service" + type :serviceSpecification + major_version 1 + description "An access network service connecting a subscriber premises to an NNI via DSL" + category "Network Service" + end +end +``` + +`characteristics do` — declares typed value slots. Each characteristic is backed by an +`Ash.TypedStruct`. Do **not** add plain Ash attributes for data that belongs in a characteristic. + +```elixir +characteristics do + characteristic :downstream_speed, MyApp.Speed + characteristic :access_technology, MyApp.AccessTechnology +end +``` + +`features do` — declares optional capabilities, each with an enabled/disabled default and +optionally its own typed characteristic payload: + +```elixir +features do + feature :voice, is_enabled?: false + feature :static_ip, is_enabled?: false do + characteristic :ip_address, MyApp.IpAddress + end +end +``` + +`parties do` — declares party roles. Use `party` for singular (at most one) and `parties` +for plural relationships: + +```elixir +parties do + party :provider, MyApp.RSP + parties :installer, MyApp.Engineer, constraints: [min: 1, max: 3] + party :owner, MyApp.Organization, reference: true + party :operator, MyApp.RSP, calculate: :derive_operator +end +``` + +- `reference: true` — no direct `PartyRef` edge is created; the party is reachable by graph + traversal. Do not add a `PartyRef` relationship manually when `reference: true` is set. +- `calculate:` — names an Ash calculation on this resource that produces the party struct at + build time. The calculation runs inside `build_before/1`; do not call it manually. + +`places do` — mirrors `parties do` in structure and options: + +```elixir +places do + place :installation_site, MyApp.GeographicSite + places :coverage_areas, MyApp.GeographicLocation, constraints: [min: 1] +end +``` + +### behaviour + +`behaviour do actions do create :name end end` — marks a named create action for build +wiring. This injects the `:specified_by`, `:features`, and `:characteristics` Ash action +arguments automatically. Do **not** declare these arguments in the action body. + +```elixir +behaviour do + actions do + create :build + end +end +``` + +## Generated functions on Instance resources + +Every resource with a complete `specification do` block gets these compile-time generated +functions: + +- `specification/0`, `characteristics/0`, `features/0`, `parties/0`, `places/0` +- `characteristic/1`, `feature/1`, `feature_characteristic/2`, `party/1`, `place/1` +- `build_before/1` — upserts the Specification node; creates Feature, Characteristic, and + Party nodes; sets action argument ids. Called automatically before every create action. +- `build_after/2` — relates the created TMF entities to the new instance node. Called + automatically after every create action. + +**Never call `build_before/1` or `build_after/2` manually** in action bodies or changesets. +They are wired to every create action via global `BuildBefore` and `BuildAfter` changes on +`BaseInstance`. + +## Instance versioning + +- **Minor/patch version bumps** — update `minor_version` or `patch_version` in `specification do`. + The existing Specification node is updated in place. No instance changes required. +- **Major version bump** — create a new module (e.g. `BroadbandServiceV2`) with a new `id` + and `major_version 2`. The original module and all its instances remain untouched. +- **Never change the `id`** of an existing specification. It is a stable cross-environment + identity; changing it orphans existing instances. + +## Party and Place resources + +Party and Place resources use `BaseParty`/`BasePlace` and the Party Extension DSL to declare +the Instance and Party roles they participate in: + +```elixir +defmodule MyApp.RSP do + use Ash.Resource, fragments: [Diffo.Provider.BaseParty], domain: MyApp.Domain + + instances do + role :provider, MyApp.BroadbandService + role :provider, MyApp.VoiceService + end + + parties do + role :employer, MyApp.Organization + end +end +``` + +Role names are domain nouns from the party's perspective — timeless, `camelCase` when +multi-word (e.g. `:dataCentre`, not `:data_centre`). + +## Common mistakes + +- **Do not add raw Ash attributes for TMF-modelled data** — use `characteristics`, `features`, + `parties`, and `places` in the DSL instead. +- **Do not declare `:specified_by`, `:features`, or `:characteristics` Ash action arguments** + — the `behaviour do` block injects them automatically. +- **Do not call `build_before/1` / `build_after/2` yourself** — they run automatically. +- **Do not create a separate Specification resource manually** — the Specification node is + managed entirely by the `build_before/1` generated function. +- **Do not use `party/1` in place of `parties/3`** (and vice versa) — `party` declares a + singular role; `parties` declares a plural role. Mismatching causes compile-time errors. +- **Do not set a `referred_type` without also setting `type: :PartyRef`** — TMF requires + both fields when using a party reference.