Skip to content
Merged
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
10 changes: 9 additions & 1 deletion artefact/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
61 changes: 61 additions & 0 deletions artefact/lib/mix/tasks/artefact.install.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# SPDX-FileCopyrightText: 2026 artefactory contributors <https://github.com/diffo-dev/artefactory/graphs/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
3 changes: 2 additions & 1 deletion artefact/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,15 @@ 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

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
Expand Down
16 changes: 16 additions & 0 deletions artefact/mix.lock
Original file line number Diff line number Diff line change
@@ -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"},
}
160 changes: 160 additions & 0 deletions artefact/usage-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<!--
SPDX-FileCopyrightText: 2026 artefactory contributors <https://github.com/diffo-dev/artefactory/graphs/contributors>
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.

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<br/>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<br/>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.
10 changes: 10 additions & 0 deletions artefactory_neo4j/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
[
Expand Down
90 changes: 90 additions & 0 deletions artefactory_neo4j/lib/mix/tasks/artefactory_neo4j.install.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# SPDX-FileCopyrightText: 2026 artefactory contributors <https://github.com/diffo-dev/artefactory/graphs/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
Loading
Loading