Packages

fnord

0.9.39

AI code archaeology

Current section

Files

Jump to
fnord lib indexer.ex
Raw

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