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
58 changes: 58 additions & 0 deletions .drafts/convenience-combine.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!--
SPDX-FileCopyrightText: 2026 artefactory contributors <https://github.com/diffo-dev/artefactory/graphs/contributors>
SPDX-License-Identifier: MIT
-->

# Drafts — Artefact.combine convenience wrapper

Drafts only. Branch `10-convenience-combine`. Per `.claude/settings.json` no
`git commit / push / add` was run.

---

## Draft commit message — `artefact`

```
feat(artefact): pipeline-friendly combine/3

Artefact.combine(heart, other, opts \\ []) is a convenience wrapper around
Artefact.Binding.find/2 + Artefact.harmonise/4 designed for pipelines —
the heart flows through the pipe as the first argument:

me_knowing
|> Artefact.combine(me_valuing)
|> Artefact.combine(me_being)
|> Artefact.combine(me_doing, title: "MeMind", description: "Mind of Me")

Bindings are auto-found via shared uuids; opts pass through to harmonise
for :title and :base_label overrides. :description is patched onto the
result (since harmonise itself does not yet honour :description in opts).
Raises MatchError when there are no shared nodes.

Provenance: :harmonised, with the calling module. Combine is sugar over
harmonise — the underlying operation IS a harmonise, so the trace stays
honest. The convenience is the binding-find and the heart-first arg order.
```

## Notes for the next yarn

- **harmonise/compose could honour :description in opts.** Right now combine
patches it post-hoc. If you later extend `do_harmonise/5` and `do_compose/4`
to read `:description` from opts and pass it into `build/1`, combine can
drop the patch and just delegate. Worth a small follow-up issue, not part
of this branch.

- **Portmanteau base_label grows.** Each combine step concatenates the
heart and other base_labels. Through a five-step pipeline that becomes a
long word like `KnowingValuingBeingKnowingMoreDoing`. The final step's
opts can rename it (`base_label: "MeMind"`), which is what the example
in the docstring shows. Worth a note in any usage example.

- **No `combine!` variant.** combine raises MatchError on no bindings, which
matches the user's original livebook helper. If we later want a
`combine_with/3` that explicitly accepts `inject:` bindings, that's a new
function — keep combine as the simple shared-uuid case.

---

*Held in the commons.*
7 changes: 7 additions & 0 deletions artefact/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ SPDX-License-Identifier: MIT

# Changelog

## 0.1.3 — 2026-04-30

- `Artefact.Mermaid.export/2` — derives Mermaid `graph` source from an `%Artefact{}`, alongside `Artefact.Cypher` and `Artefact.Arrows`; nodes render as circles, `:direction` option for `:LR`, `:RL`, `:TB`, `:BT`, `:TD`
- `:description` field on `%Artefact{}` — optional human-readable description, defaults to `nil`; accepted by `Artefact.new/1` and round-tripped through `Artefact.Arrows`
- Mermaid front-matter `title:` (Mermaid 9.4+ heading) and body `accTitle:` derived from `artefact.title`; `accDescr:` derived from `artefact.description` (inline form for single-line, block form `accDescr { ... }` for multi-line)
- `Artefact.combine/3` — pipeline-friendly convenience over `Artefact.Binding.find/2` + `Artefact.harmonise/4`; the heart flows through the pipe as the first argument, opts honour `:title`, `:base_label` and `:description` overrides; raises `MatchError` when no shared bindings exist

## 0.1.2 — 2026-04-21

- Improved `Artefact.new/1` macro — nodes and relationships declared inline with atom keys and keyword options
Expand Down
39 changes: 39 additions & 0 deletions artefact/lib/artefact.ex
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,45 @@ defmodule Artefact do
build([{:title, title}, {:base_label, base_label}, {:graph, graph}, {:metadata, metadata}])
end

@doc """
Combine `other` into `heart` using bindings auto-found between them.

Designed for pipelines — `heart` flows through the pipe as the first argument,
so a chain of combines accumulates a single heart from many others:

me_knowing
|> Artefact.combine(me_valuing)
|> Artefact.combine(me_being)
|> Artefact.combine(me_doing, title: "MeMind", description: "Mind of Me.")

Bindings are found via `Artefact.Binding.find/2` — every node sharing a uuid
between `heart` and `other` becomes a binding. Raises `MatchError` if no
bindings are found.

Internally delegates to `harmonise/4`, so `:title` and `:base_label` overrides
in `opts` are honoured. `:description` is also accepted and applied to the
result.

Records `:harmonised` provenance with the calling module.
"""
defmacro combine(heart, other, opts \\ []) do
caller = __CALLER__.module
quote do
Artefact.do_combine(unquote(heart), unquote(other), unquote(opts), unquote(caller))
end
end

@doc false
def do_combine(%__MODULE__{} = heart, %__MODULE__{} = other, opts, caller) do
{:ok, bindings} = Artefact.Binding.find(heart, other)
result = do_harmonise(heart, other, bindings, opts, caller)

case Keyword.fetch(opts, :description) do
{:ok, description} -> %{result | description: description}
:error -> result
end
end

@doc """
Harmonise two artefacts using declared bindings.

Expand Down
2 changes: 1 addition & 1 deletion artefact/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule Artefact.MixProject do
@moduledoc false
use Mix.Project

@version "0.1.2"
@version "0.1.3"
@github_url "https://github.com/diffo-dev/artefactory"

def project do
Expand Down
115 changes: 115 additions & 0 deletions artefact/test/artefact_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -975,4 +975,119 @@ defmodule ArtefactTest do
assert descr_idx < node_idx
end
end

describe "Artefact.combine/3" do
@uuid_shared "019d0000-0000-7000-8000-000000000000"

defp combine_artefact(base_label, uuid) do
%Artefact{
id: Artefact.UUID.generate_v7(),
uuid: Artefact.UUID.generate_v7(),
title: base_label,
base_label: base_label,
style: nil,
metadata: %{},
graph: %Artefact.Graph{
nodes: [%Artefact.Node{id: "n0", uuid: uuid, labels: [], properties: %{}}],
relationships: []
}
}
end

test "combines two artefacts via auto-found bindings" do
heart = combine_artefact("Knowing", @uuid_shared)
other = combine_artefact("Valuing", @uuid_shared)

result = Artefact.combine(heart, other)
assert length(result.graph.nodes) == 1
end

test "default base_label is portmanteau of heart + other" do
heart = combine_artefact("Knowing", @uuid_shared)
other = combine_artefact("Valuing", @uuid_shared)

result = Artefact.combine(heart, other)
assert result.base_label == "KnowingValuing"
end

test "title defaults to base_label when not given" do
heart = combine_artefact("Knowing", @uuid_shared)
other = combine_artefact("Valuing", @uuid_shared)

result = Artefact.combine(heart, other)
assert result.title == "KnowingValuing"
end

test "description defaults to nil when not given" do
heart = combine_artefact("Knowing", @uuid_shared)
other = combine_artefact("Valuing", @uuid_shared)

result = Artefact.combine(heart, other)
assert result.description == nil
end

test "applies title override from opts" do
heart = combine_artefact("Knowing", @uuid_shared)
other = combine_artefact("Valuing", @uuid_shared)

result = Artefact.combine(heart, other, title: "Custom")
assert result.title == "Custom"
end

test "applies description override from opts" do
heart = combine_artefact("Knowing", @uuid_shared)
other = combine_artefact("Valuing", @uuid_shared)

result = Artefact.combine(heart, other, description: "yarned")
assert result.description == "yarned"
end

test "applies title and description together" do
heart = combine_artefact("Knowing", @uuid_shared)
other = combine_artefact("Valuing", @uuid_shared)

result = Artefact.combine(heart, other, title: "MeMind", description: "Mind of Me")
assert result.title == "MeMind"
assert result.description == "Mind of Me"
end

test "chains in a pipeline" do
a = combine_artefact("Knowing", @uuid_shared)
b = combine_artefact("Valuing", @uuid_shared)
c = combine_artefact("Being", @uuid_shared)

result = a |> Artefact.combine(b) |> Artefact.combine(c)
assert result.base_label == "KnowingValuingBeing"
assert length(result.graph.nodes) == 1
end

test "pipeline applies title and description on the final step" do
a = combine_artefact("Knowing", @uuid_shared)
b = combine_artefact("Valuing", @uuid_shared)
c = combine_artefact("Being", @uuid_shared)

result =
a
|> Artefact.combine(b)
|> Artefact.combine(c, title: "MeMind", description: "Mind of Me")

assert result.title == "MeMind"
assert result.description == "Mind of Me"
end

test "records :harmonised provenance with calling module" do
heart = combine_artefact("Knowing", @uuid_shared)
other = combine_artefact("Valuing", @uuid_shared)

result = Artefact.combine(heart, other)
assert %{provenance: %{source: :harmonised, module: ArtefactTest}} = result.metadata
end

test "raises when artefacts have no shared nodes" do
heart = combine_artefact("Knowing", "019d0000-0000-7000-8000-000000000010")
other = combine_artefact("Valuing", "019d0000-0000-7000-8000-000000000020")

assert_raise MatchError, fn -> Artefact.combine(heart, other) end
end
end
end
7 changes: 7 additions & 0 deletions artefact_kino/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ SPDX-License-Identifier: MIT

# Changelog

## 0.1.3 — 2026-04-30

- Compatible with `artefact ~> 0.1.3`
- MERMAID button on the export panel — pasteable Mermaid `graph` source alongside CREATE / MERGE / JSON
- Header bar renders `artefact.description` under `artefact.title` when set; multi-line descriptions preserve their newlines (`white-space: pre-line`)
- `description` row added to the Artefact tab in the Elixir inspector, alongside `title`, `base_label` and `metadata`

## 0.1.2 — 2026-04-21

- Compatible with `artefact ~> 0.1.2`
Expand Down
4 changes: 1 addition & 3 deletions artefact_kino/artefact_kino.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ SPDX-License-Identifier: MIT

```elixir
Mix.install([
#{:artefact_kino, "~> 0.1.2"},
{:artefact_kino, path: "/Users/beanlanda/git/artefactory/artefact_kino"},
{:artefact, path: "/Users/beanlanda/git/artefactory/artefact", override: true},
{:artefact_kino, "~> 0.1.3"},
{:req, "~> 0.5"}
])
```
Expand Down
4 changes: 2 additions & 2 deletions artefact_kino/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule ArtefactKino.MixProject do
@moduledoc false
use Mix.Project

@version "0.1.2"
@version "0.1.3"
@github_url "https://github.com/diffo-dev/artefactory"

def project do
Expand All @@ -29,7 +29,7 @@ defmodule ArtefactKino.MixProject do

defp deps do
[
{:artefact, "~> 0.1"},
{:artefact, "~> 0.1.3"},
{:kino, "~> 0.14"},
{:ex_doc, "~> 0.37", only: [:dev, :test], runtime: false}
]
Expand Down
Loading