Packages
fnord
0.9.21
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/skills/loader.ex
defmodule Skills.Loader do
@moduledoc """
Load and validate skill definitions from TOML files.
This module is responsible for file IO and TOML decoding. It returns
validated `%Skills.Skill{}` structs and includes file provenance so higher
layers can explain override behavior.
"""
require Logger
@type skill_source :: :user | :project
@type loaded_skill :: %{
skill: Skills.Skill.t(),
name: String.t(),
source: skill_source,
path: String.t()
}
@type load_error ::
{:toml_error, path :: String.t(), term()}
| {:fs_error, path :: String.t(), term()}
| {:invalid_skill, path :: String.t(), Skills.Skill.decode_error()}
| {:duplicate_skill_name, String.t(), [String.t()]}
@doc """
Load all `*.toml` skill definitions from the given directory.
Returns a list of loaded skill maps containing the validated skill struct and
provenance (`source`, `path`).
Duplicate names within the directory are returned as an error.
"""
@spec load_dir(String.t(), skill_source) :: {:ok, [loaded_skill]} | {:error, load_error}
def load_dir(dir, source) when is_binary(dir) do
case File.ls(dir) do
{:ok, entries} ->
entries
|> Enum.filter(&String.ends_with?(&1, ".toml"))
|> Enum.sort()
|> Enum.map(&Path.join(dir, &1))
|> load_files(source)
{:error, :enoent} ->
{:ok, []}
{:error, reason} ->
{:error, {:fs_error, dir, reason}}
end
end
@doc """
Load a list of TOML files as skill definitions.
Duplicate names within the file set are returned as an error.
"""
@spec load_files([String.t()], skill_source) :: {:ok, [loaded_skill]} | {:error, load_error}
def load_files(paths, source) when is_list(paths) do
with {:ok, loaded} <- do_load_files(paths, source),
:ok <- ensure_unique_names(loaded) do
{:ok, loaded}
end
end
defp do_load_files(paths, source) do
paths
|> Enum.reduce_while({:ok, []}, fn path, {:ok, acc} ->
case load_file(path, source) do
{:ok, loaded} -> {:cont, {:ok, [loaded | acc]}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
|> case do
{:ok, loaded} -> {:ok, Enum.reverse(loaded)}
other -> other
end
end
@doc """
Load a single skill TOML file.
"""
@spec load_file(String.t(), skill_source) :: {:ok, loaded_skill} | {:error, load_error}
def load_file(path, source) when is_binary(path) do
with {:ok, data} <- decode_toml(path),
{:ok, skill} <- decode_skill(path, data) do
{:ok, %{skill: skill, name: skill.name, source: source, path: path}}
end
end
defp decode_toml(path) do
case Fnord.Toml.decode_file(path) do
{:ok, data} -> {:ok, data}
{:error, reason} -> {:error, {:toml_error, path, reason}}
end
end
defp decode_skill(path, data) do
case Skills.Skill.from_map(data) do
{:ok, skill, warnings} ->
Enum.each(warnings, fn
{:dropped_non_strings, key, values} ->
Logger.warning(
"#{path}: non-string values in '#{key}' were ignored: #{inspect(values)}"
)
end)
{:ok, skill}
{:error, reason} ->
{:error, {:invalid_skill, path, reason}}
end
end
defp ensure_unique_names(loaded) do
duplicates =
loaded
|> Enum.group_by(& &1.name)
|> Enum.filter(fn {_name, items} -> length(items) > 1 end)
|> Enum.map(fn {name, items} -> {name, Enum.map(items, & &1.path)} end)
case duplicates do
[] ->
:ok
[{name, paths} | _] ->
{:error, {:duplicate_skill_name, name, paths}}
end
end
end