The registry of pointers at where knowledge lives, served to LLM workers as
the references tool; it holds references, never copies of what they point at
(the membrane).
The membrane
A reference is a pointer, never content. The registry records where knowledge lives and when to look — a title, the uri that is both identity and deep link, and a description that is local routing knowledge — and it never records what the target says. A worker that has found a reference calls the target's own tool for the live content; this node does not fetch, and copying a target's text into a description defeats the whole arrangement, because the copy goes stale the moment the target moves on and nothing here will ever notice.
That is the rule the node inherits along with the registry, and it is why
remove is a hard delete: a pointer has no history worth preserving.
Coordination
Owned sub-modules against external dependencies:
flowchart TD
R[YmerNode.References]
subgraph owned
Reference[Reference schema]
Sources
Search
end
subgraph external
Repo[(YmerNode.Repo)]
end
R --> Reference
R -->|"source filter"| Sources
R -->|"find_references/1"| Search
R --> Repo
Search -->|"list_classified/1"| R
Search -->|"search_text/1"| ReferenceThe Search --> R back-edge is deliberate and runtime-only — a defdelegate
out and a plain call back, so there is no compile-time cycle. It exists so the
tag-AND SQL, the source filter and the ordering stay implemented once, here,
rather than a second time in the ranking layer.
Design decisions
- Derived, not stored: a reference's source and fetch recipe are computed from
its uri at read time (
YmerNode.References.Sources), so the set of accepted scripts, not a migration, is what decides how an old reference resolves. create_reference/1maps the(uri, fragment)unique-index violation to{:error, {:duplicate, existing}}, so an exact re-add can be answered by pointing at the row that already exists instead of forking the registry.- Filters split by mechanism. Tags are SQL — one
json_eachmembershipEXISTSper required tag — while the source filter and every ordering run in Elixir after the load: source is derived and has no column to filter on, and SQLite'slower()folds ASCII only, so an Elixir sort is what makes a non-ASCII title order correctly. The registry is small by design and a full load per read is the settled cost. - Classify once, and carry it.
list_classified/1is the primitive andlist_references/1is the projection, not the other way round. The source filter has to classify every candidate to decide anything, so the result is kept beside the reference rather than thrown away and re-derived by the renderer.YmerNode.References.Sources.declarations/0is a query, and an operation reads it once and threads it down through:declarations. - No broadcast and no subscription. The node has no UI, so there is no open list to keep live; the tool call that mutates a reference is also the one that reports it.
Summary
Functions
Creates a reference.
Permanently deletes a reference — a pointer, not content, with no rows pointing at it.
The ranked find surface — YmerNode.References.Search.run/1 owns the mode contract.
Lists references, each already classified. Options
Lists references without their classifications — list_classified/1 for a
caller that only wants the rows.
Parses a reference id: a positive integer, or its string form. Pure, and the one place the accepted id form lives.
Updates a reference's fields.
Functions
Creates a reference.
An insert that trips the (uri, fragment) unique index returns
{:error, {:duplicate, existing_reference}} — the duplicate contract, which
lets the caller steer to the existing row. In the vanishingly unlikely race
where that row is deleted between the failed insert and the lookup, the plain
changeset error is returned instead.
Permanently deletes a reference — a pointer, not content, with no rows pointing at it.
Uses a conditional Repo.delete_all rather than a bare-struct Repo.delete,
so a double delete degrades to {:error, :not_found} instead of raising
Ecto.StaleEntryError.
The ranked find surface — YmerNode.References.Search.run/1 owns the mode contract.
Lists references, each already classified. Options:
:tags— a list of tag strings; a reference must carry ALL of them (tag-AND, evaluated in SQL):source— a source token; evaluated in Elixir, because a source is derived from the uri at read time and has no column to filter on:order—:title(default, downcased) or:recent(updated_at desc, then id desc):declarations— the seam, already read by the caller. Omit it and this function reads it once itself.
Each entry is %{reference: %YmerNode.References.Reference{}, classification: %{source: source, recipe: recipe}}. The classification travels with the
reference rather than being derived again downstream: the source filter has to
classify every candidate to decide anything, and a renderer that classified a
second time would do the same work twice on the same row and could, if the two
reads straddled an accept, disagree with itself inside one response.
Lists references without their classifications — list_classified/1 for a
caller that only wants the rows.
Takes the same options, :declarations included: a :source filter still has
to classify, so passing the seam still saves the read.
Parses a reference id: a positive integer, or its string form. Pure, and the one place the accepted id form lives.
Examples
iex> YmerNode.References.parse_id(7)
{:ok, 7}
iex> YmerNode.References.parse_id(" 7 ")
{:ok, 7}
iex> YmerNode.References.parse_id("banana")
:error
iex> YmerNode.References.parse_id(0)
:error
Updates a reference's fields.
Non-raising on a stale row: a bare-struct Repo.update against a row deleted
out from under it raises Ecto.StaleEntryError, which degrades to
{:error, :not_found} — the tool layer holds a %Reference{} across the
window between reading it and writing it back.