Packages
fnord
0.9.12
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/memory/project.ex
defmodule Memory.Project do
@moduledoc """
Project-level memory storage implementation for the `Memory` behaviour.
Memories are stored as JSON files in `~/.fnord/projects/<project>/memory`.
"""
# ----------------------------------------------------------------------------
# Behaviour Implementation
# ----------------------------------------------------------------------------
@behaviour Memory
@impl Memory
def init() do
with {:ok, project} <- get_project(),
{:ok, _path} <- ensure_storage_path(project) do
drop_old_storage(project)
:ok
end
end
@impl Memory
def list() do
with {:ok, project} <- get_project(),
{:ok, files} <- project |> storage_path() |> File.ls() do
titles =
files
|> Enum.filter(&String.ends_with?(&1, ".json"))
|> Enum.map(fn file ->
path = Path.join(storage_path(project), file)
case read_file(path) do
{:ok, contents} ->
case Memory.unmarshal(contents) do
{:ok, mem} when is_map(mem) and is_binary(mem.title) -> mem.title
_ -> Memory.slug_to_title(Path.rootname(file))
end
{:error, _} ->
Memory.slug_to_title(Path.rootname(file))
end
end)
|> Enum.uniq()
|> Enum.sort()
{:ok, titles}
end
end
@impl Memory
def exists?(title) do
with {:ok, project} <- get_project(),
{:ok, _path} <- find_file_path_by_title(title, project) do
true
else
_ -> false
end
end
@impl Memory
def read(title) do
with {:ok, project} <- get_project(),
{:ok, path} <- find_file_path_by_title(title, project),
{:ok, content} <- read_file(path),
{:ok, memory} <- Memory.unmarshal(content) do
{:ok, memory}
end
end
@impl Memory
def save(%{title: title} = memory) do
with {:ok, project} <- get_project(),
{:ok, json} <- Memory.marshal(memory),
{:ok, _path} <- ensure_storage_path(project) do
lockfile = Path.join(storage_path(project), ".alloc.lock")
case FileLock.with_lock(lockfile, fn ->
# If a file with this title already exists, overwrite it in place.
# Only allocate a new unique path for genuinely new memories (which
# handles slug collisions from different titles).
{:ok, path} =
case find_file_path_by_title(title, project) do
{:ok, existing_path} -> {:ok, existing_path}
{:error, :not_found} -> allocate_unique_path_for_title(title, project)
end
write_file(path, json)
end) do
{:ok, {:ok, :ok}} -> :ok
{:ok, {:ok, {:error, reason}}} -> {:error, reason}
{:ok, {:error, reason}} -> {:error, reason}
{:error, reason} -> {:error, reason}
end
end
end
@impl Memory
def forget(title) do
with {:ok, project} <- get_project(),
{:ok, path} <- find_file_path_by_title(title, project) do
rm_path(path)
end
end
@impl Memory
def is_available?() do
case Store.get_project() do
{:ok, _project} -> true
_ -> false
end
end
# ----------------------------------------------------------------------------
# Internals
# ----------------------------------------------------------------------------
defp get_project() do
Store.get_project()
end
defp storage_path(%Store.Project{store_path: store_path}) do
Path.join(store_path, "memory")
end
defp old_storage_path(%Store.Project{store_path: store_path}) do
Path.join(store_path, "memories")
end
defp ensure_storage_path(project) do
path = storage_path(project)
with false <- File.exists?(path),
:ok <- File.mkdir_p(path) do
{:ok, path}
else
true -> {:ok, path}
end
end
defp drop_old_storage(project) do
path = old_storage_path(project)
if File.exists?(path) do
UI.debug("memory:project", "Removing old project memory storage at #{path}")
File.rm_rf!(path)
end
end
defp read_file(path) do
case FileLock.with_lock(path, fn -> File.read(path) end) do
{:ok, {:ok, contents}} ->
{:ok, contents}
{:ok, {:error, reason}} ->
{:error, reason}
{:error, reason} ->
{:error, reason}
end
end
defp write_file(path, json) do
# Optional debug (controlled by FNORD_DEBUG_MEMORY). Use UI.debug so the
# output can be enabled via environment when diagnosing issues in CI or
# locally. This avoids noisy stdout in normal runs.
if Util.Env.looks_truthy?("FNORD_DEBUG_MEMORY") do
UI.debug("memory.project.write", "writing memory to #{path} (#{byte_size(json)} bytes)")
end
case FileLock.with_lock(path, fn -> File.write(path, json) end) do
{:ok, :ok} ->
{:ok, :ok}
{:ok, {:error, reason}} ->
{:error, reason}
{:error, reason} ->
{:error, reason}
end
end
defp rm_path(path) do
case FileLock.with_lock(path, fn -> File.rm(path) end) do
{:ok, :ok} ->
:ok
{:ok, {:error, reason}} ->
{:error, reason}
{:error, reason} ->
{:error, reason}
{:callback_error, reason} ->
{:error, reason}
end
end
# Helper functions for title-based lookup and unique path allocation
defp find_file_path_by_title(title, project) do
storage = storage_path(project)
case File.ls(storage) do
{:ok, files} ->
files
|> Enum.filter(&String.ends_with?(&1, ".json"))
|> Enum.find_value({:error, :not_found}, fn file ->
path = Path.join(storage, file)
case read_file(path) do
{:ok, content} ->
case Memory.unmarshal(content) do
{:ok, mem} when is_map(mem) and is_binary(mem.title) ->
if mem.title == title, do: {:ok, path}, else: false
_ ->
if Memory.slug_to_title(Path.rootname(file)) == title,
do: {:ok, path},
else: false
end
{:error, _reason} ->
if Memory.slug_to_title(Path.rootname(file)) == title, do: {:ok, path}, else: false
end
end)
{:error, reason} ->
{:error, reason}
end
end
defp allocate_unique_path_for_title(title, project) do
with {:ok, storage} <- ensure_storage_path(project) do
base = Memory.title_to_slug(title)
path = Path.join(storage, "#{base}.json")
if File.exists?(path) do
generate_suffixed_path(storage, base)
else
{:ok, path}
end
end
end
defp generate_suffixed_path(storage, base, counter \\ 1) do
name = "#{base}_#{counter}"
path = Path.join(storage, "#{name}.json")
if File.exists?(path) do
generate_suffixed_path(storage, base, counter + 1)
else
{:ok, path}
end
end
end