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
24 changes: 24 additions & 0 deletions .drafts/mermaid-export.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,30 @@ 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`

```
Expand Down
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: 6 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
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
31 changes: 29 additions & 2 deletions artefact/lib/artefact/mermaid.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ defmodule Artefact.Mermaid do
for screen readers. A nil title produces neither — the export starts at
`graph <direction>`.

When `artefact.description` is set, an `accDescr:` line follows the
`accTitle:` line. Multi-line descriptions use the block form
(`accDescr { ... }`); single-line descriptions use the inline form. A nil
description is omitted. Like `accTitle`, the description is screen-reader
only — Mermaid does not render it visually.

Lossy: `position`, `style`, properties beyond `name`, and the artefact-level
`base_label` (collapsed into per-node labels at output time) are not represented.
"""
Expand Down Expand Up @@ -45,7 +51,10 @@ defmodule Artefact.Mermaid do
n0 -->|US_TWO| n1

"""
def export(%Artefact{title: title, base_label: base_label, graph: graph}, opts \\ []) do
def export(
%Artefact{title: title, description: description, base_label: base_label, graph: graph},
opts \\ []
) do
direction = Keyword.get(opts, :direction, :LR)

unless direction in @directions do
Expand All @@ -56,7 +65,8 @@ defmodule Artefact.Mermaid do
node_lines = Enum.map(graph.nodes, &node_line(&1, base_label))
rel_lines = Enum.map(graph.relationships, &rel_line/1)

body = ["graph #{direction}" | acc_title_lines(title) ++ node_lines ++ rel_lines]
accessibility = acc_title_lines(title) ++ acc_descr_lines(description)
body = ["graph #{direction}" | accessibility ++ node_lines ++ rel_lines]

Enum.join(front_matter(title) ++ body, "\n")
end
Expand All @@ -69,6 +79,23 @@ defmodule Artefact.Mermaid do
defp acc_title_lines(nil), do: []
defp acc_title_lines(title), do: [" accTitle: #{single_line(title)}"]

defp acc_descr_lines(nil), do: []

defp acc_descr_lines(description) do
s = to_string(description)

if String.contains?(s, "\n") do
indented =
s
|> String.split("\n")
|> Enum.map_join("\n", &" #{&1}")

[" accDescr {", indented, " }"]
else
[" accDescr: #{s}"]
end
end

# YAML plain scalar where safe; double-quoted (with `"` and `\` escaped)
# when the value contains characters that break a bare scalar.
defp yaml_scalar(value) do
Expand Down
93 changes: 93 additions & 0 deletions artefact/test/artefact_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -882,4 +882,97 @@ defmodule ArtefactTest do
assert String.contains?(Artefact.Mermaid.export(a), " accTitle: Sand Talk: a yarn")
end
end

describe "Artefact.new/1 — :description option" do
test "defaults to nil when not provided" do
a = Artefact.new()
assert a.description == nil
end

test "stores the description when provided" do
a = Artefact.new(description: "the simplest true thing about us_two")
assert a.description == "the simplest true thing about us_two"
end

test "description is independent of title" do
a = Artefact.new(title: "UsTwo", description: "Me toward You")
assert a.title == "UsTwo"
assert a.description == "Me toward You"
end
end

describe "Artefact.Arrows round-trip — description" do
test "preserves a set description" do
original =
Artefact.new(
title: "UsTwo",
description: "the simplest true thing",
base_label: "UsTwo",
nodes: [a: [labels: ["Agent"]]],
relationships: []
)

round_tripped = original |> Artefact.Arrows.to_json() |> Artefact.Arrows.from_json!()
assert round_tripped.description == "the simplest true thing"
end

test "preserves a nil description" do
original = Artefact.new(title: "Bare", nodes: [], relationships: [])
assert original.description == nil

round_tripped = original |> Artefact.Arrows.to_json() |> Artefact.Arrows.from_json!()
assert round_tripped.description == nil
end
end

describe "Artefact.Mermaid.export/2 — description" do
test "emits accDescr inline when description is single-line" do
a =
Artefact.new(
title: "UsTwo",
description: "Me toward You",
nodes: [],
relationships: []
)

assert String.contains?(Artefact.Mermaid.export(a), " accDescr: Me toward You")
end

test "uses block form when description contains newlines" do
a =
Artefact.new(
title: "UsTwo",
description: "first line\nsecond line",
nodes: [],
relationships: []
)

mmd = Artefact.Mermaid.export(a)
assert String.contains?(mmd, " accDescr {\n first line\n second line\n }")
refute String.contains?(mmd, "accDescr:")
end

test "omits accDescr when description is nil" do
a = Artefact.new(title: "Titled but undescribed", nodes: [], relationships: [])
mmd = Artefact.Mermaid.export(a)
refute String.contains?(mmd, "accDescr")
end

test "accDescr appears after accTitle and before nodes" do
a =
Artefact.new(
title: "Order",
description: "matters",
nodes: [n: [labels: ["X"]]],
relationships: []
)

mmd = Artefact.Mermaid.export(a)
title_idx = :binary.match(mmd, "accTitle:") |> elem(0)
descr_idx = :binary.match(mmd, "accDescr:") |> elem(0)
node_idx = :binary.match(mmd, "n0((") |> elem(0)
assert title_idx < descr_idx
assert descr_idx < node_idx
end
end
end
31 changes: 23 additions & 8 deletions artefact_kino/lib/artefact_kino.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ defmodule ArtefactKino do
mermaid: Artefact.Mermaid.export(artefact),
default: Atom.to_string(default),
title: artefact.title || artefact.base_label || "Artefact",
description: artefact.description,
artefact_rows: artefact_rows(artefact),
nodes_rows: nodes_rows(artefact),
rels_rows: rels_rows(artefact)
Expand Down Expand Up @@ -68,11 +69,12 @@ defmodule ArtefactKino do

defp artefact_rows(%Artefact{} = a) do
[
%{key: "id", value: a.id},
%{key: "uuid", value: a.uuid},
%{key: "title", value: inspect(a.title)},
%{key: "base_label", value: inspect(a.base_label)},
%{key: "metadata", value: inspect(a.metadata, pretty: true)}
%{key: "id", value: a.id},
%{key: "uuid", value: a.uuid},
%{key: "title", value: inspect(a.title)},
%{key: "description", value: inspect(a.description)},
%{key: "base_label", value: inspect(a.base_label)},
%{key: "metadata", value: inspect(a.metadata, pretty: true)}
]
end

Expand Down Expand Up @@ -187,10 +189,23 @@ defmodule ArtefactKino do
export function init(ctx, data) {
ctx.root.style.cssText = "font-family:monospace;background:#111;color:#e0e0e0;";

const escapeHtml = (s) => String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");

const headerHtml = `
<div style="padding:6px 8px;border-bottom:1px solid #333;">
<div style="font-size:13px;color:#aaa;">${escapeHtml(data.title)}</div>
${data.description
? `<div style="font-size:11px;color:#888;margin-top:2px;font-style:italic;white-space:pre-line;">${escapeHtml(data.description)}</div>`
: ""}
</div>`;

ctx.root.innerHTML = `
<div style="padding:6px 8px;border-bottom:1px solid #333;font-size:13px;color:#aaa;">
${data.title}
</div>
${headerHtml}
<div style="display:flex;height:560px;gap:0;">

<!-- graph panel -->
Expand Down
Loading