# `PhoenixKitKnowledgeBase.Links`
[🔗](https://github.com/BeamLabEU/phoenix_kit_knowledge_base/blob/v0.1.0/lib/phoenix_kit_knowledge_base/links.ex#L1)

The `[[wiki-link]]` index: backlinks, unresolved links, and the resolution
rules that make a bare `[[Title]]` unambiguous when the filesystem allows two
files to share a name.

The index is a cache of the markdown, rebuilt whenever a note's bytes change,
and reproducible from scratch by re-reading the vault.

## Resolving a link

A vault may legitimately hold `research/Ideas.md` and `archive/Ideas.md` —
the filesystem permits it, so the index has to describe it rather than reject
it. `[[Ideas]]` therefore resolves by:

  1. a note in the **same folder** as the linking note, then
  2. the **shortest path** (a note nearer the vault root is the more general
     one, and the likelier intent), then
  3. alphabetical order, purely so the answer is stable rather than
     whichever row Postgres returned first.

Write `[[research/Ideas]]` to say exactly which one you meant. Path-qualified
links are matched against the path instead of the title, and never ambiguous.

## Unresolved links are a feature

A link to a note that does not exist is stored as a real row with a nil
`target_note_uuid` rather than discarded, so clicking a dangling link offers
to create that note, the vault can list everything it has referred to but not
written, and creating a note **retroactively resolves** every link already
pointing at its title. Deleting a note runs it backwards: `nilify_all` turns
links into it back into unresolved ones, so the mentions survive the target.

# `resolver`

```elixir
@type resolver() :: %{
  by_title: %{
    required(String.t()) =&gt; [PhoenixKitKnowledgeBase.Schemas.Note.t()]
  },
  by_path: %{required(String.t()) =&gt; PhoenixKitKnowledgeBase.Schemas.Note.t()}
}
```

Everything needed to resolve links in one vault, loaded once.

# `backlinks`

```elixir
@spec backlinks(PhoenixKitKnowledgeBase.Schemas.Note.t()) :: [
  PhoenixKitKnowledgeBase.Schemas.Note.t()
]
```

Notes that link *to* this one — the 'linked mentions' panel, in title order.

Each source once. A note may name the same target two ways — `[[Ideas]]` and
`[[research/Ideas]]` are different keys, so two link rows — and listing it
twice would say two notes mention this one.

# `candidate_count`

```elixir
@spec candidate_count(resolver(), String.t()) :: non_neg_integer()
```

How many notes a bare title could refer to.

Used by the UI to mark a link ambiguous — the link still resolves, but it is
worth telling the author that their `[[Ideas]]` had to be guessed at.

# `keys_for`

```elixir
@spec keys_for(PhoenixKitKnowledgeBase.Schemas.Note.t()) :: [String.t()]
```

The two keys a note answers to: its bare title, and its path-qualified form.

# `outgoing`

```elixir
@spec outgoing(PhoenixKitKnowledgeBase.Schemas.Note.t()) :: [
  PhoenixKitKnowledgeBase.Schemas.NoteLink.t()
]
```

Distinct outgoing links from a note, resolved and unresolved, in title order.

# `path_key`

```elixir
@spec path_key(String.t()) :: String.t()
```

The normalised form of a path, matching what a path-qualified link normalises to.

# `reindex_note`

```elixir
@spec reindex_note(PhoenixKitKnowledgeBase.Schemas.Note.t(), resolver() | nil) :: :ok
```

Rebuild the outgoing link rows for one note.

Deletes and re-inserts rather than diffing: a note has a handful of links, and
"delete what's there, insert what the text says" cannot drift, whereas a diff
can.

# `reindex_vault`

```elixir
@spec reindex_vault(String.t()) :: non_neg_integer()
```

Rebuild every link in a vault.

Used after a rescan. Incremental updates cannot be correct there: adding one
note can change what an *existing* link resolves to, because a nearer
same-name file may have appeared. Rebuilding the lot is O(notes) once, and it
is the only version of this that is actually right.

# `reresolve_for_note`

```elixir
@spec reresolve_for_note(PhoenixKitKnowledgeBase.Schemas.Note.t()) ::
  non_neg_integer()
```

Re-resolve every link naming this note, under either of its keys.

# `reresolve_keys`

```elixir
@spec reresolve_keys(String.t(), [String.t()]) :: non_neg_integer()
```

Re-resolve every link in a vault that names one of `keys`, whether or not it
currently resolves.

This is the difference between `resolve_for_note/1` and being *right*.
`resolve_for_note/1` only fills rows where `target_note_uuid` is nil, so an
already-resolved link stays pointed at whichever note got there first:

  1. `archive/A.md` writes `[[Ideas]]` — nothing to resolve to, so unresolved
  2. `research/Ideas.md` is created — the link now points at research
  3. `archive/Ideas.md` is created — same-folder should win, but the row is
     already resolved, so it does not move until a full rescan

Step 3 contradicts the resolution rules at the top of this module, which is a
worse bug than it looks: the link renders as resolved, so nothing tells the
author it went to the wrong note.

Scoped by key rather than rebuilding the vault, because only links *naming*
the changed note can change meaning. The work is proportional to how many
notes mention that title, not to vault size, which is what makes it cheap
enough to run on every create, rename, move, restore and delete.

# `resolve`

```elixir
@spec resolve(resolver(), String.t(), String.t()) :: map() | nil
```

The note a link points at, or `nil` when nothing matches.

`from_folder` is the linking note's folder, which is what makes the
same-folder tie-break possible; pass `""` from the vault root.

# `resolve_for_note`

```elixir
@spec resolve_for_note(PhoenixKitKnowledgeBase.Schemas.Note.t()) :: non_neg_integer()
```

Point unresolved links at a note that now exists.

Matches both spellings a link can take: the bare title, and the
path-qualified form.

# `resolver`

```elixir
@spec resolver(String.t()) :: resolver()
```

Load a vault's notes into a structure that can resolve any link without
further queries.

Built once per render or per reindex pass. Resolving link-by-link would be a
query per `[[…]]` on the page, and a query per link per note during a rescan.

# `resolver_from`

```elixir
@spec resolver_from([map()]) :: resolver()
```

A resolver over the given notes only — for a notes panel, whose links must
resolve inside its own folder and nowhere else: a title from outside it is
somebody else's note, and saying it exists would tell its name to a person who
may only see this folder. Each note needs `title_key`, `relative_path` and
`folder_path`.

# `unresolved`

```elixir
@spec unresolved(String.t()) :: [
  %{title: String.t(), key: String.t(), count: non_neg_integer()}
]
```

Every unresolved target in a vault, with how many notes want it.

Sorted by mention count: the note five others already reference is a better
next thing to write than one mentioned once.

# `unresolved_count`

```elixir
@spec unresolved_count(String.t()) :: non_neg_integer()
```

How many distinct unresolved targets a vault has.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
