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
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:
@@ -1,16 +1,14 @@
|
||||
"""Note serialization — turn a Note (+ its labels/items/attachments/previews/links)
|
||||
into the JSON dict the API returns. The bulk loaders (`*_for_notes`) fetch each child
|
||||
"""Note serialization — turn a Note (+ its labels/items/attachments/previews) into
|
||||
the JSON dict the API returns. The bulk loaders (`*_for_notes`) fetch each child
|
||||
collection for a batch of notes in one query, so list endpoints avoid N+1s."""
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import and_, func, or_, select
|
||||
from sqlalchemy.orm import aliased
|
||||
from sqlalchemy import select
|
||||
|
||||
from ..models.label import Label, NoteLabel
|
||||
from ..models.note import Note
|
||||
from ..models.note_attachment import NoteAttachment
|
||||
from ..models.note_item import NoteItem
|
||||
from ..models.note_link import NoteLink
|
||||
from ..models.note_link_preview import NoteLinkPreview
|
||||
|
||||
|
||||
@@ -106,64 +104,6 @@ async def _previews_for_notes(db, note_ids: list) -> dict:
|
||||
return result
|
||||
|
||||
|
||||
async def _links_for_notes(db, note_ids: list) -> dict:
|
||||
"""Map note_id -> [{norm, id, title}] for each note's RESOLVED outgoing links.
|
||||
|
||||
The client cannot work this out for itself any more, and that is deliberate. It
|
||||
used to resolve `[[text]]` by looking the text up in a client-side name index,
|
||||
which only worked because a rename rewrote the text in every linking note. Now
|
||||
that a link is bound to an id and the text is left alone, the stored text can name
|
||||
something the target is no longer called — so the server, which holds the binding,
|
||||
is the only place that knows where a link goes.
|
||||
|
||||
`title` is the target's name RIGHT NOW, so a renamed note reads correctly
|
||||
everywhere it is linked from without a single body having been edited.
|
||||
|
||||
Unresolved links are simply absent: the client renders those as the
|
||||
create-on-click affordance it already has.
|
||||
"""
|
||||
if not note_ids:
|
||||
return {}
|
||||
source = aliased(Note)
|
||||
target = aliased(Note)
|
||||
rows = (
|
||||
await db.execute(
|
||||
select(NoteLink.source_id, NoteLink.target_norm, target.id, target.display_title)
|
||||
.select_from(NoteLink)
|
||||
.join(source, source.id == NoteLink.source_id)
|
||||
.join(
|
||||
target,
|
||||
or_(
|
||||
target.id == NoteLink.target_id,
|
||||
and_(
|
||||
NoteLink.target_id.is_(None),
|
||||
func.lower(func.trim(target.display_title)) == NoteLink.target_norm,
|
||||
),
|
||||
),
|
||||
)
|
||||
.where(
|
||||
NoteLink.source_id.in_(note_ids),
|
||||
target.deleted_at.is_(None),
|
||||
# Owner-scoped, and NOT optional. A bound target_id was resolved
|
||||
# owner-scoped when it was written, but the name fallback matches on
|
||||
# display_title alone — without this, two users who both have a note
|
||||
# called "Groceries" would leak each other's note id and name through
|
||||
# an unresolved link. (Rule 47.)
|
||||
target.owner_id == source.owner_id,
|
||||
)
|
||||
)
|
||||
).all()
|
||||
result: dict = {}
|
||||
for source_id, norm, target_id, title in rows:
|
||||
bucket = result.setdefault(source_id, [])
|
||||
# The name-fallback join can produce more than one candidate for the same
|
||||
# text; first one wins, deterministically enough for a display hint.
|
||||
if any(link["norm"] == norm for link in bucket):
|
||||
continue
|
||||
bucket.append({"norm": norm, "id": str(target_id), "title": title})
|
||||
return result
|
||||
|
||||
|
||||
async def _serialize_note(db, note: Note) -> dict:
|
||||
data = note.serialize()
|
||||
labels = await _labels_for_notes(db, [note.id])
|
||||
@@ -174,8 +114,6 @@ async def _serialize_note(db, note: Note) -> dict:
|
||||
data["attachments"] = attachments.get(note.id, [])
|
||||
previews = await _previews_for_notes(db, [note.id])
|
||||
data["previews"] = previews.get(note.id, [])
|
||||
links = await _links_for_notes(db, [note.id])
|
||||
data["links"] = links.get(note.id, [])
|
||||
return data
|
||||
|
||||
|
||||
@@ -185,7 +123,6 @@ async def _serialize_notes(db, notes: list) -> list:
|
||||
items_map = await _items_for_notes(db, ids)
|
||||
attach_map = await _attachments_for_notes(db, ids)
|
||||
preview_map = await _previews_for_notes(db, ids)
|
||||
link_map = await _links_for_notes(db, ids)
|
||||
out = []
|
||||
for n in notes:
|
||||
data = n.serialize()
|
||||
@@ -193,6 +130,5 @@ async def _serialize_notes(db, notes: list) -> list:
|
||||
data["items"] = items_map.get(n.id, [])
|
||||
data["attachments"] = attach_map.get(n.id, [])
|
||||
data["previews"] = preview_map.get(n.id, [])
|
||||
data["links"] = link_map.get(n.id, [])
|
||||
out.append(data)
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user