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

# Drafts — Mermaid export for Artefact + ArtefactKino

These are drafts only. Per `.claude/settings.json` no `git commit / push / add`
was run; per AGENTS.md the work belongs to the relation, not to the agent.
Review, edit, and use as you see fit.

---

## Draft commit message — `artefact`

```
feat(artefact): add Mermaid export

Artefact.Mermaid.export/2 emits Mermaid `graph` source from an
%Artefact{}, sitting alongside Artefact.Cypher and Artefact.Arrows
as a third derived form.

- legacy `graph` syntax for broad renderer reach (GitHub, Notion,
mdBook, Livebook)
- nodes render as circles (`id(("..."))`) — property-graph
convention, matches the vis-network ellipses in the heartside
panel
- node label mirrors the ArtefactKino vis-network panel:
name (or id) on top, semantic labels joined with a space below,
separated by <br/>
- base_label is dropped from per-node labels at output time,
consistent with the Cypher exporter's effective_labels rule
- artefact.title becomes Mermaid front-matter `title:` plus an
`accTitle:` line for screen readers; nil title omits both
- :direction option (:LR default, :RL :TB :BT :TD)
- escape rules:
* double quote in node label -> &quot;
* pipe in edge label -> &#124;
* YAML-unsafe chars in title -> double-quoted scalar with \"
and \\ escaped

Lossy: position, style, and properties beyond `name` are not
represented — Mermaid is a render concern, not a persistence form.

Fixture added at test/data/us_two/mermaid.mmd; ExUnit cases cover
the us_two round-trip, direction option, both escapes, the empty
graph, and the no-name fallback to node id.
```

## Draft commit message — `artefact` (branch `9-support-description`)

```
feat(artefact): optional description field, surfaced as Mermaid accDescr

%Artefact{} now carries an optional :description alongside :title.
It defaults to nil and is only emitted into Mermaid when set.

- new field on the struct + typespec
- Artefact.new accepts description: passthrough; macro docstring updated
- Artefact.Arrows round-trips description as a top-level "description"
key (peer of "title", may be null)
- Artefact.Mermaid emits accDescr (inline form for single-line,
block form `accDescr { ... }` when the description contains
newlines); placed between accTitle and the node lines
- ArtefactKino inspector grows a `description` row on the Artefact tab

AGENTS.md schema updated.

compose/harmonise are deliberately left to default description to nil —
combining two descriptions is a judgement call we don't want the
library making silently. Pass description: in opts when needed.
```

## Draft commit message — `artefact_kino`

```
feat(artefact_kino): MERMAID button on the export panel

Adds Artefact.Mermaid.export/1 alongside CREATE / MERGE / JSON
in the export panel of the three-panel widget. Pure text output
for now — copy with click-to-select, same as the existing buttons.

Live Mermaid rendering via mermaid.js was discussed but deferred —
keeping the panel symmetric with the other text exports for this
pass.
```

---

## Draft issue — *Live Mermaid rendering in ArtefactKino*

**Title:** `artefact_kino: render Mermaid live in the export panel`

**Body:**

> Today the MERMAID button shows the source as text, the same as
> CREATE / MERGE / JSON. Useful for copying out, less useful as a
> second view of the graph next to the vis-network panel.
>
> A follow-up would load mermaid.js from CDN (matching the existing
> vis-network bootstrap) and render the diagram inside the export
> panel, with a small toggle to flip back to source view.
>
> ### Why it might be worth doing
>
> - vis-network is force-directed; Mermaid layouts are deterministic.
> Two renderings of the same artefact, side by side, can show
> structure that one alone does not.
> - Mermaid is what most readers paste into a doc. Seeing it render
> the same way it will appear in the doc closes a feedback loop.
>
> ### Why we deferred it
>
> - The current panel is symmetric across CREATE / MERGE / JSON / MERMAID
> as text. Adding a render mode for one of them breaks that symmetry.
> - mermaid.js is a heavier CDN load than vis-network. Worth measuring
> before adding.
> - The vis-network panel already does live rendering — the question is
> whether a second live view earns its keep.
>
> ### Sketch
>
> - keep the `MERMAID` button
> - add a small `source / rendered` switch that only appears when
> MERMAID is selected
> - bootstrap mermaid.js the same way `loadVis()` bootstraps vis-network
> - render into a `<div>` swapped in for the `<pre>`

---

## Draft issue — *Mermaid fixtures for the remaining test data sets*

**Title:** `artefact: add mermaid.mmd fixtures for artefact_*, artefactory, lexical_categories, create_merge`

**Body:**

> `test/data/us_two/mermaid.mmd` is in. The other fixture folders
> (`artefact`, `artefact_combine`, `artefact_harmonise`, `artefactory`,
> `lexical_categories`, `create_merge`) all have `arrows.json` plus
> Cypher fixtures but no Mermaid one yet.
>
> Either:
> 1. Generate fixtures by running `Artefact.Mermaid.export/1` once
> against each, eyeball the output, commit. (Risk: locks in
> whatever the implementation does today.)
> 2. Hand-author each one, then assert the export matches. (Slower,
> but each fixture acts as a spec for what the diagram should
> say to a reader.)
>
> Recommendation: option 2 for `artefact` and `artefactory` (the
> self-describing artefacts — the fixture *is* the documentation),
> option 1 for the rest.

---

*The artefact belongs to the edge.*
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,12 @@ Do not build this yet. Hold it as the direction `artefactory` could grow toward.

```elixir
%Artefact{
id: String.t(), # generated UUID
title: String.t() | nil, # human label
style: atom() | nil, # render style reference — not persisted in graph
graph: %Artefact.Graph{}, # the knowledge
metadata: map() # open map — consumers add their own keys
id: String.t(), # generated UUID
title: String.t() | nil, # human label
description: String.t() | nil, # human description — surfaced as Mermaid accDescr
style: atom() | nil, # render style reference — not persisted in graph
graph: %Artefact.Graph{}, # the knowledge
metadata: map() # open map — consumers add their own keys
}

%Artefact.Graph{
Expand Down
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
46 changes: 45 additions & 1 deletion artefact/lib/artefact.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ defmodule Artefact do
persistence.
"""

defstruct [:id, :uuid, :title, :base_label, :style, metadata: %{}, graph: %Artefact.Graph{}]
defstruct [:id, :uuid, :title, :description, :base_label, :style, metadata: %{}, graph: %Artefact.Graph{}]

@type t :: %__MODULE__{
id: String.t(),
uuid: String.t(),
title: String.t() | nil,
description: String.t() | nil,
base_label: String.t() | nil,
style: atom() | nil,
graph: Artefact.Graph.t(),
Expand All @@ -27,6 +28,10 @@ defmodule Artefact do
Create a new Artefact. Defaults `base_label` and `title` to the short name
of the calling module. Override with `title:` or `base_label:` in attrs.

Optional `description:` is a longer human-readable note about the artefact —
surfaced as Mermaid `accDescr` and in the `ArtefactKino` inspector. Defaults
to `nil`; pass it explicitly when you have something to say.

Records `:struct` provenance with the calling module.
"""
defmacro new(attrs \\ []) do
Expand Down Expand Up @@ -70,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
4 changes: 3 additions & 1 deletion artefact/lib/artefact/arrows.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ defmodule Artefact.Arrows do
id: Keyword.get(opts, :id, Artefact.UUID.generate_v7()),
uuid: Keyword.get(opts, :uuid, Map.get(raw, "uuid", Artefact.UUID.generate_v7())),
title: Keyword.get(opts, :title, Map.get(raw, "title")),
description: Keyword.get(opts, :description, Map.get(raw, "description")),
base_label: base_label,
style: Keyword.get(opts, :style),
graph: graph,
Expand Down Expand Up @@ -87,10 +88,11 @@ defmodule Artefact.Arrows do

# -- encode --

defp encode(%Artefact{uuid: uuid, title: title, base_label: base_label, graph: graph}) do
defp encode(%Artefact{uuid: uuid, title: title, description: description, base_label: base_label, graph: graph}) do
%{
"uuid" => uuid,
"title" => title,
"description" => description,
"base_label" => base_label,
"style" => %{},
"nodes" => Enum.map(graph.nodes, &encode_node(&1, base_label)),
Expand Down
Loading
Loading