Packages
fnord
0.9.38
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/indexer.ex
defmodule Indexer do
@moduledoc """
This behaviour wraps the AI-powered operations used by `Cmd.Index` to allow
overrides for testing. See `impl/0`.
"""
# -----------------------------------------------------------------------------
# Input Types
# -----------------------------------------------------------------------------
@type indexer :: module()
@type file_path :: String.t()
@type file_content :: String.t()
# -----------------------------------------------------------------------------
# Output Types
# -----------------------------------------------------------------------------
@type completion :: {:ok, String.t()}
@type embeddings :: {:ok, [float]}
@type error :: {:error, term}
# -----------------------------------------------------------------------------
# Behaviour Definition
# -----------------------------------------------------------------------------
@callback get_embeddings(file_content) :: embeddings | error
@callback get_summary(file_path, file_content) :: completion | error
# -----------------------------------------------------------------------------
# Behaviour Implementation
# -----------------------------------------------------------------------------
@behaviour Indexer
@impl Indexer
def get_embeddings(content) do
AI.Embeddings.get(content)
end
@impl Indexer
def get_summary(file, content) do
AI.Agent.FileSummary
|> AI.Agent.new(named?: false)
|> AI.Agent.get_response(%{file: file, content: content})
end
# -----------------------------------------------------------------------------
# API Functions
# -----------------------------------------------------------------------------
@doc """
Returns the current indexer module. This can be overridden by config for unit
testing. See `test/test_helper.exs`.
"""
def impl() do
Services.Globals.get_env(:fnord, :indexer) || __MODULE__
end
@doc """
Indexes a single entry: reads the source, generates the summary, computes
embeddings, and persists everything. Returns the entry struct on success so
callers can immediately read its data.
This is the canonical single-file indexing pipeline, used by `Cmd.Index`
for bulk indexing.
"""
@spec index_entry(Store.Project.Entry.t()) ::
{:ok, Store.Project.Entry.t()} | {:error, term}
def index_entry(entry) do
indexer = impl()
with {:ok, contents} <- Store.Project.Entry.read_source_file(entry),
:ok <- guard_text(contents),
{:ok, summary} <- indexer.get_summary(entry.file, contents),
{:ok, embeddings} <- get_file_embeddings(indexer, entry.file, summary),
:ok <- Store.Project.Entry.save(entry, summary, embeddings) do
{:ok, entry}
end
end
# Downstream text-splitting (AI.Splitter / String.split_at) assumes a
# valid UTF-8 binary. Tracked binaries (images, compiled assets, anything
# the user has in .gitattributes or just hasn't excluded) would crash the
# grapheme walker. Bail out here with a structured error so the caller
# can classify the entry as skipped rather than a failure.
defp guard_text(content) do
if String.valid?(content), do: :ok, else: {:error, :binary_file}
end
# Build the embedding input from the file summary. The local embedding model
# (all-MiniLM-L12-v2) has a 256-token window optimized for natural language,
# so we embed the LLM-generated prose summary rather than raw code.
defp get_file_embeddings(indexer, file, summary) do
to_embed = """
# File
`#{file}`
## Summary
#{summary}
"""
indexer.get_embeddings(to_embed)
end
end