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.
Summary
Functions
An empty report, so callers can render 'nothing to do' without a special case.
True when a rescan found nothing to do — the common case, worth saying plainly.
Reconcile one vault and return what changed.
Reconcile every vault, for a Mix task or a scheduled job.
A one-line summary of a report, in the current locale.
Types
@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.
Functions
@spec empty_report() :: report()
An empty report, so callers can render 'nothing to do' without a special case.
True when a rescan found nothing to do — the common case, worth saying plainly.
@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.
@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.
A one-line summary of a report, in the current locale.