Remove [[wiki-links]], backlinks and the graph
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Failing after 6s
CI & Build / Build & push image (push) Skipped
CI & Build / Python tests (push) Successful in 8s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 31s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Failing after 37s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Failing after 1m56s

Operator, 2026-08-22 (note 2897): ThoughtSync is an intermediary surface. You
write here because it's easy — a notebook in your pocket — and later you recall
the thing and go finish it somewhere else. Recall is the product; organization
is secondary. A linking system is organization, and it isn't what this is for.

So: `[[wiki-links]]`, backlinks, the `[[` autocomplete, the note_links table,
`/api/notes/link-search`, `/api/notes/<id>/backlinks`, the whole graph blueprint
and GraphView. Rust core loses `extract_links`, `backlinks`, `link_search` and
`create_titled`; the desktop loses the three Tauri commands that exposed them.

This subsumes 982d24c rather than reverting it. That commit bound links to a
note id so a rename would stop rewriting other notes' bodies — real infra, but
infra for a feature that is now gone, and nothing it added survives. Alembic
0023 stays in the chain anyway: it shipped in an image and may already be
applied, and deleting an applied revision strands a database's version pointer.
0024 drops the table and takes the column with it. The history stays honest
about the fact that it existed for a day.

Two things deliberately kept, because they were serving recall and only
incidentally serving links:

- `/api/notes/titles` and the titles store. The command palette lists them so
  you can jump to a note by name. `resolve()` — the name→note lookup that only
  linking needed — is gone.
- `display_title`. Every note still has a name for search results and export
  filenames. What that name is FOR changed; that it exists did not.

`notes/links.py` is now `notes/tags.py`, holding the #tag→label reconciliation
it always also owned. A file called links.py with no links in it would have been
exactly the drift this removal is meant to end.

Also swept out on the way: `_escape_like`, whose only caller was link-search,
and the `graph` icon. Nothing lost that a person typed — note_links was always
derived, and the `[[text]]` is still sitting in every body it was written in.
This commit is contained in:
2026-08-22 12:00:57 -04:00
parent 982d24c83b
commit bc22f8e249
38 changed files with 216 additions and 1485 deletions
+1 -29
View File
@@ -63,27 +63,11 @@ export interface NoteRevision {
created_at: string | null;
}
// One resolved [[wiki-link]] out of a note: the normalized text as WRITTEN, and the
// note it actually points at with that note's name as it stands NOW.
//
// The server sends this because the client can no longer work it out. Resolution used
// to be a name lookup in the titles index, which only held together because renaming
// a note rewrote the link text inside every note that linked to it. Links are bound
// by id now and bodies are left alone, so the written text can name something the
// target is no longer called — and only the server holds the binding.
export interface NoteLinkRef {
/** The link text as written, trimmed and lowercased — the key a token matches on. */
norm: string;
id: string;
/** The target's CURRENT name, which is what gets rendered. */
title: string;
}
export interface Note {
id: string;
title: string | null;
// The note's display NAME: explicit title, else its first body line (server-derived).
// Every note has one, so body-only notes are still nameable + [[link]]-able.
// Every note has one, so a body-only note still has something to be called.
display_title: string;
body: string;
color: NoteColor;
@@ -101,11 +85,6 @@ export interface Note {
items: ChecklistItem[];
attachments: Attachment[];
previews: LinkPreview[];
// Absent offline: the desktop's local store derives links at query time and has no
// resolution to send. Rendering falls back to the titles index there, which is
// exactly right for a store where nothing else can have renamed the target behind
// this client's back.
links?: NoteLinkRef[];
created_at: string | null;
updated_at: string | null;
}
@@ -245,12 +224,6 @@ export const useNotesStore = defineStore("notes", () => {
}
}
async function createTitled(title: string): Promise<Note> {
const created = await repo.notes.createTitled(title);
reconcile(created);
return created;
}
async function reorder(orderedIds: string[]): Promise<void> {
// Optimistically assign positions matching the backend (total - index), sort,
// then persist.
@@ -327,7 +300,6 @@ export const useNotesStore = defineStore("notes", () => {
deletePreview,
importNotes,
fetchOne,
createTitled,
reorder,
trash,
restore,
+7 -7
View File
@@ -7,7 +7,12 @@ export interface TitleEntry {
title: string;
}
// Owner's {id,title} index, used to resolve [[wiki-links]] client-side.
// Owner's {id, name} index of every non-trashed note.
//
// Outlived [[wiki-links]] (note 2897), which is what it was originally built for,
// because the command palette lists it so someone can jump straight to a note by
// name. That is recall, which is what this app is for; `resolve()` went with the
// links.
export const useTitlesStore = defineStore("titles", () => {
const items = ref<TitleEntry[]>([]);
const loaded = ref(false);
@@ -23,10 +28,5 @@ export const useTitlesStore = defineStore("titles", () => {
await load();
}
function resolve(title: string): TitleEntry | null {
const norm = title.trim().toLowerCase();
return items.value.find((t) => t.title.trim().toLowerCase() === norm) ?? null;
}
return { items, loaded, load, reload, resolve };
return { items, loaded, load, reload };
});