Packages
fnord
0.8.80
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/store/memories.ex
defmodule Store.Memories do
@moduledoc """
Unix file store for memories with composite scope (global + per-project).
Directory structure:
~/.fnord/memories/ # Global memories
.metadata.json
<slug>/
meta.json
heuristic.json
children.log
~/.fnord/projects/<project>/memories/ # Project memories
<slug>/
...
"""
# Base path uses Settings.get_user_home/0 to respect test overrides
defp base_path(), do: Path.join(Settings.get_user_home(), ".fnord/memories")
@metadata_file ".metadata.json"
# ----------------------------------------------------------------------------
# Initialization
# ----------------------------------------------------------------------------
@doc """
Initializes the memories store directory structure.
Creates base directory for global memories and root metadata.
Project memories are created under each project's store directory.
"""
@spec init() :: :ok | {:error, term}
def init() do
with :ok <- File.mkdir_p(base_path()),
:ok <- init_metadata() do
:ok
end
end
@doc """
Checks if the memories store has been initialized.
"""
@spec initialized?() :: boolean
def initialized?() do
File.dir?(base_path()) and File.exists?(metadata_path())
end
# ----------------------------------------------------------------------------
# Loading
# ----------------------------------------------------------------------------
@doc """
Loads all global memories from disk.
"""
@spec load_global() :: [AI.Memory.t()]
def load_global() do
global_path()
|> load_memories_from_path()
end
@doc """
Loads all memories for a specific project.
"""
@spec load_project(String.t()) :: [AI.Memory.t()]
def load_project(project_name) do
project_path(project_name)
|> load_memories_from_path()
end
@doc """
Loads all memories: global + project if project_name provided.
"""
@spec load_all(String.t() | nil) :: [AI.Memory.t()]
def load_all(nil), do: load_global()
def load_all(project_name) do
load_global() ++ load_project(project_name)
end
# ----------------------------------------------------------------------------
# CRUD Operations
# ----------------------------------------------------------------------------
@doc """
Creates a new memory directory and writes initial files.
"""
@spec create(AI.Memory.t()) :: :ok | {:error, term}
def create(memory) do
memory_path = get_memory_path(memory)
with :ok <- File.mkdir_p(memory_path),
:ok <- Store.Memories.Meta.write(memory),
:ok <- Store.Memories.Heuristic.write(memory),
:ok <- Store.Memories.Children.write(memory, []) do
:ok
end
end
@doc """
Checks if a memory exists.
"""
@spec exists?(String.t(), AI.Memory.scope()) :: boolean
def exists?(slug, scope) do
scope_path(scope)
|> Path.join(slug)
|> File.dir?()
end
@doc """
Deletes a memory and all its files.
"""
@spec delete(String.t(), AI.Memory.scope()) :: :ok | {:error, term}
def delete(slug, scope) do
scope_path(scope)
|> Path.join(slug)
|> File.rm_rf()
|> case do
{:ok, _} -> :ok
error -> error
end
end
# ----------------------------------------------------------------------------
# Hierarchy Operations
# ----------------------------------------------------------------------------
@doc """
Adds a child slug to a parent's children.log file.
"""
@spec add_child(String.t(), String.t(), AI.Memory.scope()) :: :ok | {:error, term}
def add_child(parent_slug, child_slug, scope) do
parent_path = Path.join(scope_path(scope), parent_slug)
children = Store.Memories.Children.read(parent_path)
unless child_slug in children do
Store.Memories.Children.write_path(parent_path, [child_slug | children])
else
:ok
end
end
@doc """
Removes a child slug from a parent's children.log file.
"""
@spec remove_child(String.t(), String.t(), AI.Memory.scope()) :: :ok | {:error, term}
def remove_child(parent_slug, child_slug, scope) do
parent_path = Path.join(scope_path(scope), parent_slug)
children = Store.Memories.Children.read(parent_path)
updated = Enum.reject(children, &(&1 == child_slug))
Store.Memories.Children.write_path(parent_path, updated)
end
@doc """
Gets all child slugs for a memory.
"""
@spec get_children(String.t(), AI.Memory.scope()) :: [String.t()]
def get_children(slug, scope) do
scope_path(scope)
|> Path.join(slug)
|> Store.Memories.Children.read()
end
@doc """
Finds the parent slug for a given child slug.
Returns nil if no parent found.
"""
@spec find_parent(String.t(), AI.Memory.scope()) :: String.t() | nil
def find_parent(child_slug, scope) do
# Use rg to search children.log files for the child slug
# Note: rg doesn't expand globs, so we need to expand them first
pattern = "^#{Regex.escape(child_slug)}$"
children_logs =
scope_path(scope)
|> Path.join("*/children.log")
|> Path.wildcard()
if Enum.empty?(children_logs) do
nil
else
case System.cmd("rg", ["-l", pattern | children_logs], stderr_to_stdout: true) do
{output, 0} ->
output
|> String.split("\n", trim: true)
|> List.first()
|> case do
nil -> nil
path -> path |> Path.dirname() |> Path.basename()
end
_ ->
nil
end
end
end
# ----------------------------------------------------------------------------
# Private Helpers
# ----------------------------------------------------------------------------
defp global_path(), do: base_path()
defp project_path(name) do
# Project memories stored in the project's store directory
Path.join([Settings.get_user_home(), ".fnord/projects", name, "memories"])
end
defp metadata_path(), do: Path.join(base_path(), @metadata_file)
defp scope_path(:global) do
global_path()
end
defp scope_path(:project) do
case Settings.get_selected_project() do
{:ok, project_name} ->
project_path(project_name)
{:error, :project_not_set} ->
raise "Cannot access project-scoped memories without a selected project"
end
end
defp get_memory_path(memory) do
Path.join(scope_path(memory.scope), memory.slug)
end
defp init_metadata() do
metadata = %{
migrations_applied: [],
created_at: DateTime.utc_now() |> DateTime.to_iso8601()
}
case Jason.encode(metadata, pretty: true) do
{:ok, json} ->
File.write(metadata_path(), json)
error ->
error
end
end
defp load_memories_from_path(path) do
if File.dir?(path) do
path
|> Path.join("*/meta.json")
|> Path.wildcard()
|> Enum.map(&Path.dirname/1)
|> Enum.map(&load_memory_from_dir/1)
|> Enum.reject(&is_nil/1)
else
[]
end
end
defp load_memory_from_dir(memory_dir) do
with {:ok, meta} <- Store.Memories.Meta.read(memory_dir),
{:ok, heuristic} <- Store.Memories.Heuristic.read(memory_dir),
children <- Store.Memories.Children.read(memory_dir) do
AI.Memory.new(
Map.merge(meta, %{
pattern_tokens: heuristic["pattern_tokens"] || %{},
children: children
})
)
else
_ -> nil
end
end
end