# Changelog

All notable changes to this project are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.0] - 2026-11-10

First release. Notes are markdown files on disk; Postgres indexes them.

0.1.0 was drafted on 2026-08-25 but not published then. The sections below
are what changed after that draft; the draft's own list, which describes the
foundations, follows under its date.

### Added

- **`notes_panel` edits on today's PhoenixKit.** Without a Leaf that supports
  `notify` (0.11; core 3.4.0 caps Leaf at 0.10) the drop-in panel used to show
  notes read-only. It now edits them in a plain markdown textarea: autosave,
  the stale-write guard with its "Take theirs / Keep mine" choice, read-only
  without `:edit` access, and a debounced change for a note no longer open is
  dropped. With Leaf 0.11 the Leaf editor takes over, unchanged.

- **Search snippets:** `search_with_snippets/3` returns each match with a
  snippet of its body around the matched words (`ts_headline`, front matter
  left out), as `{:text, _}` / `{:match, _}` segments for the host to render
  and escape itself.

- **Numbered names on demand.** `create_note/4`, `rename_note/4`,
  `move_note/4` and `restore_note/3` take `unique: true`: a name already taken
  becomes `Ideas 2`, `Ideas 3`, … instead of `:already_exists` /
  `:target_exists` (Obsidian's rule). The note's own current path counts as
  free, so renaming a note to its own name stays a no-op. Past 100 the answer
  is `:too_many_duplicates`, with its own message.
- **Rename and move in one step:** `move_note/4` takes `title:`. One file move,
  so the note is never briefly renamed in the old folder or moved under a name
  that collides there.
- **A trash a host can search:** `list_trash/2` with `frontmatter: true` adds
  each entry's parsed front matter, so a host that binds notes by a property
  (an `id:`) finds its trashed copy without reading `.trash/` itself.
- **Notes by front-matter property:** `list_notes/2` takes `property: {key,
  value}` (compared as text) and `has_property: key | [keys]`, and
  `get_note_by_property/3` returns the first match, so hosts stop querying
  `frontmatter` in this module's tables with their own SQL.

- **Right-click menus on the vault tree.** Folders offer Rename and Move to
  trash, notes offer Open and Move to trash; touch-and-hold does the same on a
  phone, and the menu names the row it was opened on. Core's explorer already
  marks every row with the attributes `<.context_menu>` reads, so this is two
  menu declarations — one element each, not one per row, and no round-trip to
  open. Confined to the sidebar with `within`, so a right-click on a note title
  in the search results or the editor still gets the browser's own menu. The
  folder menu stamps both spellings of the uuid param, because
  `start_rename_folder` takes `folder-uuid` (the sidebar's click vocabulary) and
  `trash_folder` takes `folder_uuid` (the drag hook's) — cheaper than a shim
  clause that exists only to rename a key. One new handler, `open_note`: the
  tree opens a note through a `<.link patch>` and a context menu cannot, since
  item hrefs are fixed at render time and only `phx-value-*` varies per row.

- **`[[wiki-links]]` render as links in the editor.** Leaf 0.6.0 decorates them
  and asks which targets exist; the answer goes through the same resolver and
  same-folder tie-break that builds the stored link index, so a link never
  renders as pointing somewhere the backlinks panel disagrees with. An
  ambiguous title still resolves and now says so on hover, naming the note it
  picked. An unresolved target is styled distinctly and points at the create
  page, and the hover tooltip names the note a link resolved to. Leaf never
  navigates: the target is re-resolved on the server rather than trusting the
  href the page was rendered with.

  Configured `follow: :click`, matching Obsidian's Live Preview. **Following a
  link does not work yet**, through no fault of the configuration: hybrid's
  source swap runs off `selectionchange`, which a mouse press fires before
  `click`, so Leaf's handler finds the link's span already replaced and
  returns. No gesture reaches it. Written up for upstream in
  `dev_docs/2026-08-28-leaf-wikilink-click-bug.md`; the outgoing-links panel
  remains the way to navigate.
- **Folder move and rename**, which the vault had no way to do — the README
  said "`mv` the directory and rescan". `Storage.move_folder/3` plus
  `Notes.relocate_folder/3`: one operation, because a rename and a reparent are
  one operation on a filesystem. Notes underneath keep their uuids and therefore
  their backlinks, empty subdirectories move with the rest, and every key the
  subtree answered to is re-resolved so path-qualified `[[folder/Note]]` links
  follow. Refuses the vault root, an existing target, a move into its own
  subtree, and anything outside the vault.
- **Notes and folders in one tree**, as Obsidian shows them. Core's
  `FolderExplorer` gained leaf items in 2.13.14; this passes every note keyed by
  the folder holding it and renders each as a link. The flat note list now
  appears only under a tag filter — without one it was a second copy of the
  tree.
- **The sidebar now uses core's `FolderExplorer`** — guide lines, expand and
  collapse, inline rename — and core's `MediaDragDrop` hook, so notes and
  folders can be dragged into folders and notes onto Trash. Both come from
  `phoenix_kit`, so this module still ships no JavaScript.
- **Live editing groundwork.** A `Leaf.Collab.Room` per note, started on demand
  under core's supervisor via the `children/0` callback, and a
  `Leaf.Collab.Store` implementation that persists through
  `Notes.update_content/4` rather than writing the file directly — so a
  collaborative edit re-derives the title, front matter, tags, link graph and
  search vector, which `Leaf.Collab.Store.File` would have left stale. The
  editor is not wired to it yet.
- **`scripts/verify_collab.exs`** — what a room writes and what it refuses,
  against a real database and filesystem.
- **`scripts/verify_links.exs`** — link resolution checked against a real
  database: what a link points at after every operation that can change the
  answer. `mix test` is deliberately database-free and `smoke_http.exs` only
  proves a page renders, not that it renders the right note.
- **Trash.** Deleted notes and folders move to `.trash/` inside the vault
  instead of being unlinked, under a per-deletion directory that preserves the
  original path — so restore is a move, and deleting the same path twice keeps
  both copies. Trash panel with restore, permanent delete and empty.
- **Leaf collaboration spec** (`dev_docs/2026-08-25-leaf-collaboration-spec.md`) — what Leaf
  needs for CRDT multi-user editing, staged, with the Hex ecosystem survey
  (`y_ex`, `prosemirror_ex`, the `y-phoenix-channel` demo) and the settled
  storage model: the CRDT lives in Postgres while a note is open and is flushed
  to disk on a timer.
- **Live sync across browsers and devices.** Each vault page subscribes to a
  PubSub topic; note saves, deletions, folder changes and rescans propagate to
  every open session. A note open in reading view is replaced immediately; one
  being edited raises a banner instead, so unsaved keystrokes are never
  discarded on your behalf. Rescans emit a single message rather than one per
  file, so `mix phoenix_kit_knowledge_base.rescan` in a terminal refreshes open
  browsers without flooding them.

### Changed

- **Leaf 0.6.0, phoenix_kit 2.13.13.** No code change was needed — the existing
  `~> 0.5` constraint already admits 0.6.0 — but the pair has to move together:
  core vendors the jsDelivr pin for Leaf's JS, and 2.13.13 is the release that
  points it at `@v0.6.0`. 0.6.0 brings `Leaf.Collab` (live editing) and
  `[[wiki-link]]` rendering, neither wired up here yet.
- **Reading view removed.** There is one surface — the editor — as in Obsidian.
- **File names are validated, not repaired.** An invalid title is refused with
  the reason instead of having its characters silently stripped, which used to
  file `Ideas: Part 2` as `Ideas Part 2`. Adds Obsidian's link-breaking
  characters (`[ ] # ^`) and Windows device names to the refused set.
- **`phoenix_kit` constraint raised from `~> 2.0` to `>= 2.14.0 and < 4.0.0`,
  and all deps updated to their latest allowed versions.** 2.14.0 is the first
  published core with the `<.context_menu>` components the vault tree's
  right-click menus use (and 2.13.15+ has `FolderExplorer`'s `items` attr /
  `:item` slot); with the old lock at 2.13.13 the module did not compile at
  all. The lock now resolves phoenix_kit 3.0.0 (compile-, test- and
  credo-verified; core 3.0 still drives module-owned tables through
  `migration_module/0`, which this module implements) plus mdex 0.14.6,
  ex_doc 0.40.4, yaml_elixir 2.12.2, dialyxir 1.4.8. Hosts upgrading to core
  3.0 must be at V225 (core 2.65.0) first — core refuses lower with
  `BelowFloorError`.

### Fixed

- **Renaming or moving a note listed it twice.** `index_entry/4` looks the row
  up by path, which is the wrong key when the path is what changed: it found
  nothing at the new path and inserted a second row, leaving the old entry
  pointing at a file that had moved, `rename_note/4` returning that stale row,
  and every backlink still attached to it. The row being moved is now named
  explicitly. (Rescan-detected renames were never affected — they update in
  place and match by content hash.)
- **The Reload button did not reload the editor.** Leaf's surfaces are
  `phx-update="ignore"`, so assigning `content` changes nothing on screen —
  replacing the buffer needs `send_update(Leaf, action: :set_content, …)`.
  **Discard mine, reload** and a rescan that changed the open note underneath
  both left the old text in the editor while the server believed otherwise.
- **`[[` autocomplete died at the first space.** Neither suggestion trigger set
  `:token`, so both inherited Leaf's default of `[\p{L}\p{N}_-]`. A title like
  `Attention Is All You Need` could never be completed, nor a path-qualified
  `[[research/Ideas]]`, nor a nested `#project/alpha` tag. Both tokens now match
  the parser that has to read the result back.
- **A resolved `[[link]]` never reconsidered its target.** Resolution only
  filled links that were still unresolved, so a link kept pointing at whichever
  same-named note got there first — creating a nearer `archive/Ideas.md` did
  not take the link from `research/Ideas.md` until the next full rescan,
  contradicting the documented same-folder rule while still rendering as
  resolved. Every operation that changes which notes answer to a name — create,
  rename, move, delete, restore, folder delete — now re-resolves the links
  naming it. Scoped by key, so the work is proportional to how many notes
  mention that title rather than to vault size.
- **A viewer could rename a note, and could start a rescan.** Both were the
  only mutating events without a write check; the title input is `readonly`,
  but readonly is a DOM property, not a permission. Membership changes now
  re-resolve the actor's role on each event too, rather than trusting the check
  done at mount.
- **A vault row could point at a directory that was never created.** The row
  was inserted first and the directory made second, so a failed `mkdir` left a
  vault that failed on its first read. Reversed, as the comment above it always
  claimed.
- **UI copy contradicted the model.** Deleting a note or folder said "from
  disk… cannot be undone" when both go to `.trash/`, and the new-note pane
  claimed titles are unique within a vault — they are unique within a *folder*,
  and two folders each holding an `Ideas` is the case the whole tie-break
  exists for.
- **Every vault page crashed with `BadBooleanError`.** The trash panel added
  `@search_results and not @trash_open` to a section guard; `@search_results` is
  `nil` when no search is running, and `nil and …` raises even though
  `:if={@search_results}` alone is fine. Added `scripts/smoke_http.exs`, which
  renders every page as a logged-in user, because neither the database-free test
  suite nor an unauthenticated request executes a template.
- **Two deletions in the same second lost the first recoverable copy** — trash
  directories are now unique per deletion, not per second.
- **Autosave could silently overwrite a colleague's edit.** Once a conflict was
  flagged, every subsequent autosave passed `force:` — so continuing to type was
  enough to overwrite the other version without ever choosing to. Overwriting is
  now reachable only through an explicit button.

- **Switching notes showed the previous note's text.** Leaf's editing surfaces
  are `phx-update="ignore"`, so LiveView never patches them after mount; with a
  constant component id, patching between notes reused the same hook and every
  note rendered whatever was loaded first. The editor id is now keyed to the
  note, so LiveView mounts a fresh component per note.
- **A late edit could be written into the wrong note.** Leaf debounces, so a
  `:leaf_changed` for the note you just left could arrive after switching and be
  saved into the note now open. Payloads are now matched against the current
  editor id and dropped when stale.
- **A note named with an uppercase extension (`Note.MD`) was indexed but
  unlinkable.** The walk accepts `.MD` (it downcases before comparing), but the
  title and the path-key both stripped only a lowercase `.md`, so the note's
  title came out as `"Note.MD"` and a bare `[[Note]]` never resolved to it.
  Extension stripping is now case-insensitive everywhere
  (`Storage.drop_extension/1`).
- **The vault list ran one count query per card.** `IndexLive` now fetches all
  note counts in one grouped query (`Notes.count_notes_by_vault/1`).
- Removed `Links.unresolve_stale/1` — dead code: rename and move already
  re-resolve through `Links.reresolve_keys/2`, which covers both directions.

## 0.1.0 draft - 2026-08-25 (not published)

### Added

- **Filesystem storage.** A vault is a directory, a folder is a directory, a note
  is a `<Title>.md` file — so `rg`, `git`, `vim` and Obsidian work on the tree
  directly. Layout: `system/<slug>/` and `users/<email-slug>/<slug>/` under a
  configured root.
- **Rescan** — reconciles the index with the disk, via a button or
  `mix phoenix_kit_knowledge_base.rescan`. Compares `(mtime, size)` before
  hashing, and detects renames by content hash so an external `mv` preserves a
  note's uuid, permalink and backlinks.
- **Stale-write guard** — the web UI refuses to overwrite a file that changed
  underneath it, offering reload or deliberate overwrite.
- **Path safety** — two independent containment checks, symlinks inside a vault
  skipped rather than followed, sanitised filenames, atomic writes.
- **Ambiguity-tolerant link resolution** — titles are not unique on a
  filesystem, so `[[Ideas]]` resolves same-folder first, then shortest path, then
  alphabetically, with `[[folder/Ideas]]` for explicit disambiguation.
- **Vaults** — system (organisation-wide) and personal, sharing one
  `owner`/`editor`/`viewer` membership model. `knowledge_base.manage_system_vaults`
  confers ownership of every system vault, and deliberately grants nothing on
  other people's personal vaults.
- **Notes** in a nested folder tree, with YAML front matter and tags — both
  derived from the markdown on every save, never edited separately.
- **`[[Wiki-links]]`** with aliases and heading anchors, resolved case- and
  whitespace-insensitively within a vault.
- **Backlinks panel** and a per-vault **unresolved links** list, ordered by how
  many notes are asking for each missing title. Creating a note retroactively
  resolves every link already pointing at it; deleting one turns links back into
  unresolved rather than dropping the mentions.
- **`[[` autocomplete** driven entirely server-side, plus a caret-aware
  "Link to note" picker for mid-paragraph inserts.
- **Full-text search** over a generated `tsvector` column with a GIN index,
  title-weighted, parsed with `websearch_to_tsquery`.
- **Read / edit toggle** using core's `MarkdownEditor` and MDEx rendering, with
  autosave that rebuilds the link index in the same transaction.
- Versioned migration coordinator for the five `phoenix_kit_kb_*` tables,
  prefix-aware and stamped via `COMMENT ON TABLE`.
- 126 database-free tests covering markdown parsing, path containment and
  filename sanitisation, link-resolution tie-breaking, changeset invariants,
  behaviour compliance, route ordering and migration version bookkeeping.

[0.1.0]: https://github.com/BeamLabEU/phoenix_kit_knowledge_base/releases/tag/v0.1.0
