feat: node.linkable — first-class link targets for the [[ picker (§8.4)
Some checks failed
Build / validate (pull_request) Failing after 8s

The picker listed every node, so each task showed up twice (itself + its
same-titled canonical-context doc) plus tag/log noise — and the new
preview made the duplicates look identical. New `Store::list_linkable_nodes`
/ `node.linkable` returns non-tombstoned nodes minus `tag`s and the docs
that are a task's canonical-context or log attachment (you link the task,
not its body). The Telescope picker now sources from it.

Tests: a socket test (5 nodes → 2 linkable: task + standalone doc).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-06-03 13:15:29 -07:00
commit 2fc48a1aa9
9 changed files with 57 additions and 3 deletions

View file

@ -289,6 +289,10 @@ impl Store for LocalStore {
nodes::list(&self.conn, &self.owner_id, kind)
}
fn list_linkable_nodes(&self) -> Result<Vec<Node>> {
nodes::list_linkable(&self.conn, &self.owner_id)
}
fn journal_open_or_create(&mut self, date: &str) -> Result<Node> {
let now = self.clock.now_ms();
nodes::open_or_create_journal(&self.conn, &self.owner_id, now, date)

View file

@ -407,6 +407,23 @@ pub(super) fn list(conn: &Connection, owner: &str, kind: Option<NodeKind>) -> Re
Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
}
/// First-class wiki-link targets (tech-spec §8.4): non-tombstoned nodes, minus
/// the internal noise — `tag` nodes, and the `doc` nodes that are a task's
/// **canonical-context** or **log** attachment (you link the task, not its
/// auto-created body/log). Title-sorted, for the `[[` picker.
pub(super) fn list_linkable(conn: &Connection, owner: &str) -> Result<Vec<Node>> {
let sql = format!(
"SELECT {COLUMNS} FROM nodes
WHERE owner_id = ?1 AND tombstoned = 0 AND kind != 'tag'
AND id NOT IN (SELECT dst_id FROM links
WHERE type IN ('canonical-context', 'log-of') AND tombstoned = 0)
ORDER BY title COLLATE NOCASE"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map([owner], from_row)?;
Ok(rows.collect::<rusqlite::Result<Vec<_>>>()?)
}
/// A node's aliases (wiki-link names), sorted. Empty until aliases are written.
pub(super) fn aliases(conn: &Connection, id: &str) -> Result<Vec<String>> {
let mut stmt = conn.prepare("SELECT alias FROM aliases WHERE node_id = ?1 ORDER BY alias")?;

View file

@ -46,6 +46,11 @@ pub trait Store {
/// (tech-spec §6 `node.list`).
fn list_nodes(&self, kind: Option<NodeKind>) -> Result<Vec<Node>>;
/// First-class wiki-link targets (tech-spec §8.4): non-tombstoned nodes
/// minus the internal noise — `tag` nodes and the `doc`s that are a task's
/// canonical-context or log attachment. Backs the `[[` picker.
fn list_linkable_nodes(&self) -> Result<Vec<Node>>;
/// Resolve a wiki-link target (`[[title]]`) to a node, **exactly** — an
/// alias match first, then an exact, owner-scoped, non-tombstoned title
/// match; `None` if nothing matches (an unresolved link is allowed, §5).