A domain that can search() looks complete. Then createNote imports FTS, and a unit test is a database. Putting the graph on Note looks honest. Then archive is a query, and the entity is an index.
Dripnex keeps two ports. The core page is the write side: operations take a NoteRepository. The storage page is the rest: ExtendedNoteRepository in @dripnex/storage-core is the query port the app actually uses.
The renderer never seeing SQL is written elsewhere. SQLite as the store is elsewhere. This is how the port is cut.
The problem#
If createNoteOperation can list, every write test stands up search. If core owns backlinks, the graph is a domain invariant and a derived view at once. If the desktop talks to NoteRepository only, the sidebar has no list.
The write port is three methods:
interface NoteRepository {
get(id: NoteId): Promise<Note | null>;
save(note: Note): Promise<void>;
delete(id: NoteId): Promise<void>;
}
List, search, counts, tags, backlinks, graph live on ExtendedNoteRepository. SQLiteNoteRepository implements that. Sync bookkeeping stays on the SQLite class, not on core. Native lives in one package — that cut is written elsewhere.
One hard decision#
The write port is not the index. Domain operations get get, save, delete. The app lists through storage-core. Do not put search on Note. Do not make archive a core query.
If create cannot run without FTS, it is not this core.
What I would not do again#
Give operations listNotes so "the domain can show a sidebar." Then a create test is an index, and the index is a second product.
Put backlinks on the write port because "links are data." The link is in the Markdown. The graph is derived. That cut is written elsewhere.
The bar#
An operation you can test with a fake save, and a sidebar that does not live in @dripnex/core. Internals: core · storage. Live at dripnex.app.