Build Saner.AI

KINDAback to the verdict

0%0 of 18 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A local-first notes app whose hard problems are getting a captured thought onto disk before anything can lose it, keeping a search index honest about files edited outside the app, and making the optional model answer only from what you actually wrote.

estimated effort weekendthe files for this build are in the project pack

StorageMarkdown files in a folder you choose, one per note, with front matterIndexSQLite with FTS5, rebuildable from the folder by one commandCaptureA global hotkey window that writes the file before it renders anything elseLinksWiki-style links resolved through an identifier in the front matterAIOptional, one model behind one interface, answering only with quotes from your notesShellTauri 2 with a small web front end

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why The application owns a folder on disk, which is a desktop job rather than a server one.

    Get it rustup.rs, then the Tauri 2 prerequisites page for your platform. open ↗

    Verify cargo --version prints and a blank Tauri 2 application launches

  2. installfree

    Why Builds the front end and runs its tests.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  3. installfree

    Why The search index depends on it, and a build without FTS5 fails in a confusing way at the first query rather than at startup.

    Get it Most distributions ship it enabled; check before building anything on top of it. open ↗

    Verify A CREATE VIRTUAL TABLE USING fts5 statement succeeds

  4. have readyfree

    Why Rename handling, outside edits and broken links only misbehave on notes written by a person over time, and a generated corpus has none of that texture.

    Get it Export from whatever you use now, or point the app at a folder you already write in, with a copy taken first.

    Verify At least a few hundred files, including some with links to notes that do not exist

  5. API keyfree locally, pennies per question hosted

    Why It is genuinely optional, and the suite has to pass with it removed, so treat it as an add-on rather than a dependency.

    Get it A local runtime with a downloaded model, or any API key you are happy sending note text to.

    Verify One answer returns with citations, and removing the configuration leaves the app working

  6. decidefree

    Why All three are asserted in tests, and the retrieval threshold in particular decides how often the app refuses to answer.

    Get it Write down where the notes live, how quickly outside edits must be picked up, and the similarity below which no answer is given.

    Verify All three appear in configuration and are named in tests

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- The notes are files. Every table below is derived and can be deleted without losing anything.
- notes(path, uuid, title, created_at, modified_at, mtime, size, content_hash) · the uuid lives in the front matter, so a rename changes the path and nothing else
- notes_fts(title, body) as an FTS5 external content table over notes · which, by design, does not keep itself in sync
- links(from_uuid, target_text, resolved_uuid, is_broken) · broken is a state worth showing, not an error worth hiding
- aliases(uuid, former_title) · so a link written against yesterday's title still lands
- tags(uuid, tag, source) · from front matter and from inline markers, and both round-trip to the file
- chunks(note_uuid, ordinal, text, start_offset, end_offset) and embeddings(chunk_id, model, vector) · created only when a model is configured
- answers(question, answer, cited_chunk_ids_json, model, at) · every answer keeps its citations, so a wrong one can be traced to what it read

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
NOTES_DIRrequired~/Documents/notesThe folder that is the record. Everything else can be rebuilt from it.
INDEX_DBrequired~/.local/share/notes/index.dbDisposable by design. Deleting it triggers a rebuild.
WATCH_DEBOUNCE_MSrequired500How long to wait after a file changes before re-indexing it.
REBUILD_ON_STARTrequiredhashReconcile by mtime and hash at startup. Never assume the index is current.
AI_ENDPOINToptionalhttp://localhost:11434Optional. Absent means search only, which is a complete mode.
AI_API_KEYsecretoptionalsk-...redactedOnly if the endpoint needs one. Note text is sent there, so choose deliberately.
RETRIEVAL_THRESHOLDrequired0.35Below this, the panel refuses to answer rather than guessing.
CONFLICT_SUFFIXrequired.conflictAppended to the losing side of a simultaneous edit, which is kept rather than discarded.

The build, in order

  1. Capture that writes before it draws

    A thought is on disk before the window has finished appearing.

    1. The rename is the only moment the note becomes real.

      Files src-tauri/src/capture.rstests/atomic_write.rs

    2. It is what survives a rename, a move and a reorganisation.

      Files src-tauri/src/frontmatter.rs

    3. Indexing is a consequence of a capture, never a precondition.

      Files src-tauri/src/capture.rs

    done when · tick each as it passes
    watch out
    • Writing the note into the database and flushing to disk on a timer loses whatever was captured when the process is killed or the machine sleeps, which for a capture tool is most of a day's thinking, and the loss is invisible because the list keeps showing the row until the next restart. Write the file first, atomically, and index afterwards, so that an indexer that never runs has cost you nothing.
  2. An index you can throw away

    Search is fast, and deleting the database costs nothing but a rebuild.

    1. Cheap, correct, and only half the problem.

      Files src-tauri/src/index/triggers.sql

    2. The other half, and the one that makes search trustworthy.

      Files src-tauri/src/index/watch.rstests/external_edit.rs

    3. A rebuild nobody has run is a rebuild that does not work.

      Files src-tauri/src/index/rebuild.rs

    done when · tick each as it passes
    watch out
    • An FTS5 external content table does not maintain itself: the documentation is explicit that keeping the index consistent with the content is the caller's job, so a note edited in another editor leaves search confidently returning the old text and opening a note that no longer contains the phrase you searched for. Keep triggers for the app's own writes, reconcile outside edits by mtime and hash, and make a full rebuild a single tested command.
  3. Links, backlinks and the note that does not exist yet

    Linking to something unwritten is useful rather than broken.

    1. The two are different things and conflating them is the bug.

      Files src-tauri/src/links/resolve.rstests/rename.rs

    2. People write links by hand and do not update them.

      Files src-tauri/src/links/aliases.rs

    3. The gaps are the most useful list in a notes app.

      Files src/ui/Unresolved.tsx

    done when · tick each as it passes
    watch out
    • Resolving links by file name means renaming a note breaks every link into it, and because the link still renders as ordinary text the breakage stays silent until the day you go looking for something and find nothing. Resolve through the identifier, keep former titles as aliases so hand-written links still land, and show unresolved links as a list rather than hiding them.
  4. An optional model that has to cite

    An answer without a quote from your own notes is refused.

    1. The refusal is the feature, and it needs a number.

      Files src-tauri/src/ai/answer.rstests/refusal.rs

    2. A citation you cannot click is a claim.

      Files src/ui/Answer.tsx

    3. Tested by unsetting the configuration and running the suite.

      Files src-tauri/src/ai/mod.rs

    done when · tick each as it passes
    watch out
    • A model handed a question with nothing retrieved will answer from its training and produce something plausible about your own life that you never wrote, and because the tone matches the rest of the app it gets believed and then quoted back to you months later. Refuse to answer below the retrieval threshold, and render every claim beside the quote it came from.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more notes & knowledge.