From b09d75776c2e35dd79bd0e9605ac383ed3d34584 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 13 May 2026 12:58:38 +0930 Subject: [PATCH 1/3] usage_rules produced and consumed --- artefact/mix.exs | 2 +- artefact/usage-rules.md | 150 +++++++++++++++++++++++++++++++ artefactory_neo4j/mix.exs | 3 +- artefactory_neo4j/usage-rules.md | 99 ++++++++++++++++++++ 4 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 artefact/usage-rules.md create mode 100644 artefactory_neo4j/usage-rules.md diff --git a/artefact/mix.exs b/artefact/mix.exs index 121c326..1bff897 100644 --- a/artefact/mix.exs +++ b/artefact/mix.exs @@ -42,7 +42,7 @@ defmodule Artefact.MixProject do defp package do [ licenses: ["MIT"], - files: ~w(lib .formatter.exs mix.exs README* CHANGELOG* MIGRATION* LICENSES), + files: ~w(lib .formatter.exs mix.exs README* CHANGELOG* MIGRATION* LICENSES usage-rules.md), links: %{"GitHub" => @github_url} ] end diff --git a/artefact/usage-rules.md b/artefact/usage-rules.md new file mode 100644 index 0000000..ad33c4c --- /dev/null +++ b/artefact/usage-rules.md @@ -0,0 +1,150 @@ + + +# Rules for working with Artefact + +## What Artefact is + +Artefact is an Elixir library for building, combining, and persisting knowledge graph fragments. An `%Artefact{}` is a named, typed property graph — a small, self-contained piece of knowledge. It is not an application data model, a database schema, or a general-purpose graph library. + +The intended use is knowledge memorialisation: capturing shared understanding between people, agents, or systems in a form that can be combined, versioned, and persisted. + +## require Artefact + +`Artefact.new/1`, `Artefact.new!/1`, `Artefact.combine!/2`, and all other operations are **macros**. Always `require Artefact` before calling them: + +```elixir +require Artefact + +artefact = Artefact.new!( + title: "Us Two", + nodes: [ + matt: [labels: ["Agent"], properties: %{"name" => "Matt"}], + claude: [labels: ["Agent"], properties: %{"name" => "Claude"}] + ], + relationships: [ + [from: :matt, type: "US_TWO", to: :claude] + ] +) +``` + +Without `require`, you will get a compile-time error about undefined functions. This is the most common source of confusion when first using Artefact. + +## UUID is identity + +Every node carries a UUIDv7 `uuid`. This is its identity — the same UUID in two artefacts means the same node. `combine!/2` uses UUID equality to find shared nodes and merge them. + +**Never change a UUID once it has been used in a persisted or shared artefact.** If you assign a UUID explicitly at construction time, keep it. If you do not assign one, Artefact generates a time-ordered UUIDv7 automatically. + +For importing from external sources — Mermaid diagrams, Cypher files, JSON — derive UUIDs deterministically from a stable identifier using `Artefact.UUID.from_name/1`: + +```elixir +uuid = Artefact.UUID.from_name("std_ulogic") +# same name always → same UUID, valid UUIDv7 +``` + +This means the same external id imported twice always produces the same node, and `combine!/2` will bind correctly across imports. + +## Operations at a glance + +| Operation | What it does | Key constraint | +|---|---|---| +| `new!/1` | Build a fresh artefact | Macro — `require Artefact` | +| `combine!/2` | Union two artefacts via shared UUIDs | Different `base_label` required | +| `harmonise!/3` | Union via explicit bindings | Different `base_label` required | +| `compose!/2` | Concatenate — nodes stay disjoint | No shared UUIDs expected | +| `graft!/2` | Extend an existing artefact inline | Every new node must carry `:uuid` | + +Each operation has a `!/n` (raises) and `/n` (returns `{:ok, _} | {:error, _}`) variant. + +## combine!/2 requires different base_label values + +`combine!/2` finds shared nodes automatically by UUID. It requires the two artefacts to have **different** `base_label` values — this distinguishes "what I know" from "what I am adding": + +```elixir +# Raises Artefact.Error.Operation with same_base_label +Artefact.combine!(a, b) # both default base_label to calling module name + +# Correct +a = Artefact.new!(base_label: "Signals", ...) +b = Artefact.new!(base_label: "Values", ...) +combined = Artefact.combine!(a, b) +``` + +When no `base_label` is set explicitly, it defaults to the short name of the calling module — so two artefacts built in the same module will clash. + +## Mermaid import and export + +`Artefact.Mermaid.export/2` converts an artefact to a Mermaid `graph` source string. `Artefact.Mermaid.from_mmd!/2` parses it back. The round-trip is lossless for: title, description, node names, labels, and relationship types. + +### UUID identity anchors on the Mermaid node id + +When importing with `from_mmd!/2`, the UUID of each node is derived from its **Mermaid node id** — the `\w+` identifier (e.g. `val_0`, `std_ulogic`) — not the display label inside the shape. Keep node ids stable across diagram versions; changing an id changes the UUID and breaks bindings. + +```mermaid +graph LR + std_ulogic(("std_ulogic
Signal")) ← id is std_ulogic, UUID derived from "std_ulogic" +``` + +### Declare nodes separately from edges for label recovery + +When a node's shape is declared inline on an edge line, the label is not captured: + +``` +val_0["VALUE · 0"] -->|ENUMERATES| value ← label "VALUE" is lost +``` + +Use a separate declaration line: + +``` +graph LR + val_0["VALUE · 0"] + val_0 -->|ENUMERATES| value +``` + +The export format produced by `export/2` always uses separate lines, so this only applies to hand-authored Mermaid. + +### Node label conventions in Mermaid + +Two formats are recognised inside node shapes: + +- `name
Label1 Label2` — our export format +- `LABEL · name` — yarn convention (one label and name separated by ` · `) + +### Node descriptions via click tooltips + +Node `description` properties are exported as `click id "text"` lines and recovered on import. They are visible as hover tooltips in Mermaid renderers. + +## %Artefact{} struct shape + +```elixir +%Artefact{ + uuid: "019e...", # UUIDv7 — the artefact's own identity + title: "My Artefact", # optional + description: "...", # optional + base_label: "Concept", # optional — collapsed into per-node labels at export + graph: %Artefact.Graph{ + nodes: [ + %Artefact.Node{ + id: "n0", # internal sequential id — do not rely on this across artefacts + uuid: "019e...", # UUIDv7 — stable identity + labels: ["Concept", "Thing"], + properties: %{"name" => "Alpha", "description" => "..."} + } + ], + relationships: [ + %Artefact.Relationship{ + id: "r0", # internal sequential id + type: "RELATES", # MACRO_CASE convention + from_id: "n0", + to_id: "n1", + properties: %{} + } + ] + } +} +``` + +Node `id` values (`n0`, `r0`) are internal and sequential within one artefact. Use `uuid` for stable cross-artefact identity. diff --git a/artefactory_neo4j/mix.exs b/artefactory_neo4j/mix.exs index c77467e..1950259 100644 --- a/artefactory_neo4j/mix.exs +++ b/artefactory_neo4j/mix.exs @@ -32,6 +32,7 @@ defmodule ArtefactoryNeo4j.MixProject do [ {:artefact, "~> 0.1"}, {:bolty, "~> 0.0.9"}, + {:usage_rules, "~> 1.0", only: :dev, runtime: false}, {:ex_doc, "~> 0.37", only: [:dev, :test], runtime: false} ] end @@ -39,7 +40,7 @@ defmodule ArtefactoryNeo4j.MixProject do defp package do [ licenses: ["MIT"], - files: ~w(lib .formatter.exs mix.exs README* CHANGELOG* LICENSES), + files: ~w(lib .formatter.exs mix.exs README* CHANGELOG* LICENSES usage-rules.md), links: %{"GitHub" => @github_url} ] end diff --git a/artefactory_neo4j/usage-rules.md b/artefactory_neo4j/usage-rules.md new file mode 100644 index 0000000..949ee73 --- /dev/null +++ b/artefactory_neo4j/usage-rules.md @@ -0,0 +1,99 @@ + + +# Rules for working with ArtefactoryNeo4j + +## What ArtefactoryNeo4j is + +ArtefactoryNeo4j persists `%Artefact{}` structs to a Neo4j-compatible graph database via the Bolt protocol (Bolty driver). It is a persistence boundary for knowledge graph fragments — not an ORM, not a data layer, not a query builder. You bring the artefact; it handles the Cypher. + +Multi-database support (one named database per entity — Me, Mob, a You) requires DozerDB or Neo4j Enterprise. It is not available on Neo4j Community Edition. + +## Connection model + +ArtefactoryNeo4j uses **direct Bolty connections**, not a named supervised pool. There is no global `Bolt` process, no `Repo` module, no application-level config key to set. + +```elixir +{:ok, conn} = ArtefactoryNeo4j.connect( + uri: "bolt://localhost:7688", + auth: [username: "neo4j", password: "password"] +) +``` + +This is different from `ash_neo4j`, which starts a named `Bolt` process in the supervision tree and reads from `config :bolty, Bolt, ...`. Do not carry that pattern here. + +## The `db:` option is required on every query + +Every `write/3` and `fetch/3` call requires a `db:` option that names the target database. There is no default database. + +```elixir +:ok = ArtefactoryNeo4j.write(conn, artefact, db: "matt_artefactory") +{:ok, rows} = ArtefactoryNeo4j.fetch(conn, uuid, db: "matt_artefactory") +``` + +Omitting `db:` raises `KeyError` — it is a required key, not optional. + +## Database naming convention + +Database names follow Elixir convention in code — `snake_case` atom or string. ArtefactoryNeo4j converts them to Neo4j `kebab-case` automatically: + +```elixir +# These are equivalent +ArtefactoryNeo4j.write(conn, artefact, db: :matt_artefactory) +ArtefactoryNeo4j.write(conn, artefact, db: "matt_artefactory") +# Both write to Neo4j database "matt-artefactory" +``` + +Do not pass a `kebab-case` string directly — it will be double-converted. + +## Property naming convention + +Node property keys are converted at the Bolt boundary automatically: + +- Elixir `snake_case` → Neo4j `camelCase` on write +- Neo4j `camelCase` → Elixir `snake_case` on read + +```elixir +# In Elixir: %{"first_name" => "Matt"} +# In Neo4j: {firstName: "Matt"} +``` + +Do not manually convert property keys before passing them to ArtefactoryNeo4j, and do not expect `camelCase` keys in results. + +## write/3 uses MERGE — it is idempotent + +`write/3` generates parameterised `MERGE` Cypher, not `CREATE`. Calling it twice with the same artefact will update in place rather than create duplicate nodes. This is intentional — artefacts are knowledge fragments, and re-writing the same knowledge should be safe. + +## Connection pooling (DBConnection) + +Bolty is built on [DBConnection](https://hexdocs.pm/db_connection), which provides connection pooling. The `connect/1` call starts a pool — `pool_size:` controls how many concurrent Bolt connections it maintains. Tune this based on load in production. + +```elixir +{:ok, conn} = ArtefactoryNeo4j.connect( + uri: "bolt://localhost:7688", + auth: [username: "neo4j", password: "password"], + pool_size: 10 +) +``` + +ArtefactoryNeo4j does not currently expose transactions. If you need transactional writes, use `Bolty.transaction/4` directly on the connection returned by `connect/1` — see the Bolty documentation. + +## Database lifecycle (DozerDB) + +Named databases are a DozerDB / Neo4j Enterprise feature. Each entity in the diffo model has its own named database: + +```elixir +:ok = ArtefactoryNeo4j.create_database(conn, "matt_artefactory") +:ok = ArtefactoryNeo4j.write(conn, artefact, db: "matt_artefactory") + +# Lifecycle +:ok = ArtefactoryNeo4j.stop_database(conn, "matt_artefactory") +:ok = ArtefactoryNeo4j.start_database(conn, "matt_artefactory") +:ok = ArtefactoryNeo4j.drop_database(conn, "matt_artefactory") +``` + +All lifecycle operations route through the `system` database internally — you do not need to manage that yourself. + +`create_database/2` uses `IF NOT EXISTS` — safe to call repeatedly. From 4c0c11a83ee77769735854de959efd81e2c1ca28 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 13 May 2026 13:01:51 +0930 Subject: [PATCH 2/3] igniters --- artefact/lib/mix/tasks/artefact.install.ex | 61 +++++++++++++ artefact/mix.exs | 1 + artefact/mix.lock | 16 ++++ .../mix/tasks/artefactory_neo4j.install.ex | 90 +++++++++++++++++++ artefactory_neo4j/mix.exs | 1 + artefactory_neo4j/mix.lock | 16 ++++ 6 files changed, 185 insertions(+) create mode 100644 artefact/lib/mix/tasks/artefact.install.ex create mode 100644 artefactory_neo4j/lib/mix/tasks/artefactory_neo4j.install.ex diff --git a/artefact/lib/mix/tasks/artefact.install.ex b/artefact/lib/mix/tasks/artefact.install.ex new file mode 100644 index 0000000..066a8a8 --- /dev/null +++ b/artefact/lib/mix/tasks/artefact.install.ex @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: 2026 artefactory contributors +# SPDX-License-Identifier: MIT + +defmodule Mix.Tasks.Artefact.Install.Docs do + @moduledoc false + + def short_doc, do: "Installs Artefact" + def example, do: "mix igniter.install artefact" + + def long_doc do + """ + #{short_doc()} + + ## Example + + ```bash + #{example()} + ``` + """ + end +end + +if Code.ensure_loaded?(Igniter) do + defmodule Mix.Tasks.Artefact.Install do + @shortdoc "#{__MODULE__.Docs.short_doc()}" + @moduledoc __MODULE__.Docs.long_doc() + + use Igniter.Mix.Task + + @impl Igniter.Mix.Task + def info(_argv, _composing_task) do + %Igniter.Mix.Task.Info{ + group: :artefact, + example: __MODULE__.Docs.example() + } + end + + @impl Igniter.Mix.Task + def igniter(igniter) do + igniter + |> Igniter.Project.Formatter.import_dep(:artefact) + end + end +else + defmodule Mix.Tasks.Artefact.Install do + @shortdoc "#{__MODULE__.Docs.short_doc()} | Install `igniter` to use" + @moduledoc __MODULE__.Docs.long_doc() + + use Mix.Task + + def run(_argv) do + Mix.shell().error(""" + The task 'artefact.install' requires igniter. Please install igniter and try again. + + For more information, see: https://hexdocs.pm/igniter/readme.html#installation + """) + + exit({:shutdown, 1}) + end + end +end diff --git a/artefact/mix.exs b/artefact/mix.exs index 1bff897..5291760 100644 --- a/artefact/mix.exs +++ b/artefact/mix.exs @@ -35,6 +35,7 @@ defmodule Artefact.MixProject do [ {:jason, "~> 1.4"}, {:splode, "~> 0.3"}, + {:igniter, ">= 0.6.29 and < 1.0.0-0", optional: true}, {:ex_doc, "~> 0.37", only: [:dev, :test], runtime: false} ] end diff --git a/artefact/mix.lock b/artefact/mix.lock index 550b7ce..11a6ee0 100644 --- a/artefact/mix.lock +++ b/artefact/mix.lock @@ -1,10 +1,26 @@ %{ "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, + "ex_ast": {:hex, :ex_ast, "0.11.2", "8a0330e7b2bc664b52d7b60b8d5faa2628cd6e02fe80f89e726ff20328411dbf", [:mix], [{:sourceror, "~> 1.7", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "df343260bd443306ca5da232615f1534a4aa740960055a4699e493fad8b4bd66"}, "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, + "finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"}, + "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, + "igniter": {:hex, :igniter, "0.8.0", "c7cab589440e5f20ff68e00f60eb094378114dab3105c0784ce8140f8dfdd2c0", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "fcd99096fde4797f7b48bebddcfc58785569acd696346a3eb385bf813f47a7cc"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mint": {:hex, :mint, "1.8.0", "b964eaf4416f2dee2ba88968d52239fca5621b0402b9c95f55a08eb9d74803e9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "f3c572c11355eccf00f22275e9b42463bc17bd28db13be1e28f8e0bb4adbc849"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"}, + "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, + "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, + "sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"}, + "spitfire": {:hex, :spitfire, "0.3.11", "79dfcb033762470de472c1c26ea2b4e3aca74700c685dbffd9a13466272c323d", [:mix], [], "hexpm", "eb6e2dadf63214e8bfe65ca9788cef2b03b01027365d78d3c0e3d9ebd3d5b7b4"}, "splode": {:hex, :splode, "0.3.1", "9843c54f84f71b7833fec3f0be06c3cfb5be6b35960ee195ea4fad84b1c25030", [:mix], [], "hexpm", "8f2309b6ec2ecbb01435656429ed1d9ed04ba28797a3280c3b0d1217018ecfbd"}, + "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, + "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, } diff --git a/artefactory_neo4j/lib/mix/tasks/artefactory_neo4j.install.ex b/artefactory_neo4j/lib/mix/tasks/artefactory_neo4j.install.ex new file mode 100644 index 0000000..a9ee268 --- /dev/null +++ b/artefactory_neo4j/lib/mix/tasks/artefactory_neo4j.install.ex @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: 2026 artefactory contributors +# SPDX-License-Identifier: MIT + +defmodule Mix.Tasks.ArtefactoryNeo4j.Install.Docs do + @moduledoc false + + def short_doc, do: "Installs ArtefactoryNeo4j" + def example, do: "mix igniter.install artefactory_neo4j" + + def long_doc do + """ + #{short_doc()} + + ## Example + + ```bash + #{example()} + ``` + """ + end +end + +if Code.ensure_loaded?(Igniter) do + defmodule Mix.Tasks.ArtefactoryNeo4j.Install do + @shortdoc "#{__MODULE__.Docs.short_doc()}" + @moduledoc __MODULE__.Docs.long_doc() + + use Igniter.Mix.Task + + @impl Igniter.Mix.Task + def info(_argv, _composing_task) do + %Igniter.Mix.Task.Info{ + group: :artefactory_neo4j, + installs: [{:artefact, "~> 0.2"}], + example: __MODULE__.Docs.example() + } + end + + @impl Igniter.Mix.Task + def igniter(igniter) do + igniter + |> Igniter.Project.Formatter.import_dep(:artefactory_neo4j) + |> Igniter.Project.Config.configure( + "runtime.exs", + :bolty, + [Bolt, :uri], + "bolt://localhost:7687" + ) + |> Igniter.Project.Config.configure( + "runtime.exs", + :bolty, + [Bolt, :auth], + username: "neo4j", + password: "password" + ) + |> Igniter.Project.Config.configure( + "runtime.exs", + :bolty, + [Bolt, :pool_size], + 10 + ) + |> Igniter.Project.Config.configure( + "runtime.exs", + :bolty, + [Bolt, :name], + Bolt + ) + |> Igniter.Project.Application.add_new_child( + {Bolty, {:code, quote(do: Application.get_env(:bolty, Bolt))}} + ) + end + end +else + defmodule Mix.Tasks.ArtefactoryNeo4j.Install do + @shortdoc "#{__MODULE__.Docs.short_doc()} | Install `igniter` to use" + @moduledoc __MODULE__.Docs.long_doc() + + use Mix.Task + + def run(_argv) do + Mix.shell().error(""" + The task 'artefactory_neo4j.install' requires igniter. Please install igniter and try again. + + For more information, see: https://hexdocs.pm/igniter/readme.html#installation + """) + + exit({:shutdown, 1}) + end + end +end diff --git a/artefactory_neo4j/mix.exs b/artefactory_neo4j/mix.exs index 1950259..1ea96e8 100644 --- a/artefactory_neo4j/mix.exs +++ b/artefactory_neo4j/mix.exs @@ -32,6 +32,7 @@ defmodule ArtefactoryNeo4j.MixProject do [ {:artefact, "~> 0.1"}, {:bolty, "~> 0.0.9"}, + {:igniter, ">= 0.6.29 and < 1.0.0-0", optional: true}, {:usage_rules, "~> 1.0", only: :dev, runtime: false}, {:ex_doc, "~> 0.37", only: [:dev, :test], runtime: false} ] diff --git a/artefactory_neo4j/mix.lock b/artefactory_neo4j/mix.lock index 6a21235..7236db8 100644 --- a/artefactory_neo4j/mix.lock +++ b/artefactory_neo4j/mix.lock @@ -3,11 +3,27 @@ "bolty": {:hex, :bolty, "0.0.9", "c8026ce9804347f71e23b3a0cbc01b918ef94b61e159b5ba7fb48527878033ad", [:mix], [{:db_connection, "~> 2.7.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "fc20c42550c0fce370276b4ef119e92792761b2fea1aef9cccf8de946bc39d35"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, + "ex_ast": {:hex, :ex_ast, "0.11.2", "8a0330e7b2bc664b52d7b60b8d5faa2628cd6e02fe80f89e726ff20328411dbf", [:mix], [{:sourceror, "~> 1.7", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "df343260bd443306ca5da232615f1534a4aa740960055a4699e493fad8b4bd66"}, "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, + "finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"}, + "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, + "igniter": {:hex, :igniter, "0.8.0", "c7cab589440e5f20ff68e00f60eb094378114dab3105c0784ce8140f8dfdd2c0", [:mix], [{:ex_ast, "~> 0.5", [hex: :ex_ast, repo: "hexpm", optional: false]}, {:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "fcd99096fde4797f7b48bebddcfc58785569acd696346a3eb385bf813f47a7cc"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mint": {:hex, :mint, "1.8.0", "b964eaf4416f2dee2ba88968d52239fca5621b0402b9c95f55a08eb9d74803e9", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "f3c572c11355eccf00f22275e9b42463bc17bd28db13be1e28f8e0bb4adbc849"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"}, + "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, + "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, + "sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"}, + "spitfire": {:hex, :spitfire, "0.3.11", "79dfcb033762470de472c1c26ea2b4e3aca74700c685dbffd9a13466272c323d", [:mix], [], "hexpm", "eb6e2dadf63214e8bfe65ca9788cef2b03b01027365d78d3c0e3d9ebd3d5b7b4"}, "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"}, } From 21848452032c1b8f1c2614dbb1aa0b2442a14e01 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 13 May 2026 13:03:51 +0930 Subject: [PATCH 3/3] ignite is the way --- artefact/README.md | 10 +++++++++- artefact/usage-rules.md | 10 ++++++++++ artefactory_neo4j/README.md | 10 ++++++++++ artefactory_neo4j/usage-rules.md | 10 ++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/artefact/README.md b/artefact/README.md index eae6ce1..f6fda73 100644 --- a/artefact/README.md +++ b/artefact/README.md @@ -20,10 +20,18 @@ As we yarn we naturally exchange and create Artefacts. ## Installation +The preferred way to install Artefact is via Igniter: + +```bash +mix igniter.install artefact +``` + +Or add the dependency manually: + ```elixir def deps do [ - {:artefact, "~> 0.1"} + {:artefact, "~> 0.2"} ] end ``` diff --git a/artefact/usage-rules.md b/artefact/usage-rules.md index ad33c4c..f272315 100644 --- a/artefact/usage-rules.md +++ b/artefact/usage-rules.md @@ -5,6 +5,16 @@ SPDX-License-Identifier: MIT # Rules for working with Artefact +## Installation + +The preferred way to add Artefact to a project is via Igniter: + +```bash +mix igniter.install artefact +``` + +This wires up the formatter automatically. If your project does not use Igniter, add the dep manually and run `mix deps.get`. + ## What Artefact is Artefact is an Elixir library for building, combining, and persisting knowledge graph fragments. An `%Artefact{}` is a named, typed property graph — a small, self-contained piece of knowledge. It is not an application data model, a database schema, or a general-purpose graph library. diff --git a/artefactory_neo4j/README.md b/artefactory_neo4j/README.md index ffb71ab..160a173 100644 --- a/artefactory_neo4j/README.md +++ b/artefactory_neo4j/README.md @@ -17,6 +17,16 @@ Named databases on Neo4j Community Edition require **DozerDB** — a free plugin ## Installation +The preferred way to install ArtefactoryNeo4j is via Igniter: + +```bash +mix igniter.install artefactory_neo4j +``` + +This automatically configures Bolty in `runtime.exs`, adds `Bolty` to the supervision tree, and installs the `artefact` dependency. + +Or add the dependency manually: + ```elixir def deps do [ diff --git a/artefactory_neo4j/usage-rules.md b/artefactory_neo4j/usage-rules.md index 949ee73..7b289b1 100644 --- a/artefactory_neo4j/usage-rules.md +++ b/artefactory_neo4j/usage-rules.md @@ -5,6 +5,16 @@ SPDX-License-Identifier: MIT # Rules for working with ArtefactoryNeo4j +## Installation + +The preferred way to add ArtefactoryNeo4j to a project is via Igniter: + +```bash +mix igniter.install artefactory_neo4j +``` + +This automatically: configures Bolty connection details in `runtime.exs`, adds `Bolty` to the supervision tree, and installs the `artefact` dependency. If your project does not use Igniter, do these steps manually and run `mix deps.get`. + ## What ArtefactoryNeo4j is ArtefactoryNeo4j persists `%Artefact{}` structs to a Neo4j-compatible graph database via the Bolt protocol (Bolty driver). It is a persistence boundary for knowledge graph fragments — not an ORM, not a data layer, not a query builder. You bring the artefact; it handles the Cypher.