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

Reconciles a vault's index with what is actually on disk.

There is no file watcher by design, so this is how edits made outside the web
UI — `vim`, a script, Obsidian, `git pull` — become visible. Run it from the
Rescan button, or call it from a Mix task or a cron job.

## How change is detected

Cheap first, authoritative second. A file whose `(mtime, size)` still match
the index is skipped without being read, which is what keeps a rescan of a
large vault fast. Anything else gets hashed, and only a differing hash counts
as a real change — so `touch`, a `git checkout` that rewrites mtimes, or a
save that produced identical bytes cost one read and no reindex.

## Renames survive

A file that disappeared plus a new file with identical content is a **move**,
not a delete and a create. Recognising that keeps the note's uuid, and with it
every permalink and every backlink pointing at it. Without this step, `mv
Ideas.md archive/Ideas.md` in a shell would quietly break every link into that
note — the single most likely thing anyone does outside the app.

Identical *content* is the only signal available after the fact. Two genuinely
distinct notes with byte-identical content are indistinguishable here; pairing
them off arbitrarily is still better than dropping both uuids, and the
resulting index is correct either way.

## One bad file is one error

A file the index cannot hold — binary bytes renamed `.md`, a path too long to
index — is reported in `:errors` and the scan goes on. Everything per file is
guarded, down to an exception: before this, one such file made the whole
rescan raise, every time, until somebody found it by hand.

## Links are rebuilt wholesale

Once files have settled, every link in the vault is rebuilt rather than
patched. Adding one note can change what an *existing* link resolves to,
because a nearer same-name file may have appeared — an incremental update
cannot see that, so it would leave links quietly pointing at the wrong note.

# `report`

```elixir
@type report() :: %{
  added: non_neg_integer(),
  updated: non_neg_integer(),
  removed: non_neg_integer(),
  renamed: non_neg_integer(),
  unchanged: non_neg_integer(),
  folders_added: non_neg_integer(),
  folders_removed: non_neg_integer(),
  errors: [%{path: String.t(), reason: String.t()}]
}
```

What a rescan did, for reporting back to whoever pressed the button.

# `empty_report`

```elixir
@spec empty_report() :: report()
```

An empty report, so callers can render 'nothing to do' without a special case.

# `no_changes?`

```elixir
@spec no_changes?(report()) :: boolean()
```

True when a rescan found nothing to do — the common case, worth saying plainly.

# `run`

```elixir
@spec run(PhoenixKitKnowledgeBase.Schemas.Vault.t()) ::
  {:ok, report()} | {:error, term()}
```

Reconcile one vault and return what changed.

Not run in a transaction, deliberately. A rescan of a big vault would hold a
write transaction open for its whole duration, and every step here is
independently idempotent — an interrupted rescan leaves a partially updated
index that the next run finishes, rather than a rolled-back one that has to
start over.

# `run_all`

```elixir
@spec run_all([PhoenixKitKnowledgeBase.Schemas.Vault.t()]) :: [
  {PhoenixKitKnowledgeBase.Schemas.Vault.t(),
   {:ok, report()} | {:error, term()}}
]
```

Reconcile every vault, for a Mix task or a scheduled job.

# `summarize`

```elixir
@spec summarize(report()) :: String.t()
```

A one-line summary of a report, in the current locale.

---

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