Skip to content

Plugin: graph view + backlinks / unlinked mentions (#71)#146

Merged
thetechjon merged 2 commits into
devfrom
feat/graph-plugin-71
Jun 7, 2026
Merged

Plugin: graph view + backlinks / unlinked mentions (#71)#146
thetechjon merged 2 commits into
devfrom
feat/graph-plugin-71

Conversation

@thetechjon
Copy link
Copy Markdown
Collaborator

Why a plugin, not a core component

The brief asked for two complementary surfaces — a backlinks /
unlinked-mentions sidebar panel and a force-directed global graph
view — and Jon's standing rule is "build new feature surfaces as
plugins where the Plugin API can carry them." v1.2 (PRs A, B, C, F
plus the post-v1.2 VNode event delivery + wikilink intercept
follow-up in #142) added every capability this work needs:

  • vault.read.all for body access across the whole vault (PR C).
  • vault.events for debounced re-derivation on save and on
    active-note change (PR F).
  • fullscreenViews surface for the global graph (PR B).
  • The full v1.2 VNode set including svg, link, button, box,
    and list (PR A).
  • ctx.onVNodeEvent plus the wikilink:// click intercept (Wire VNode event delivery + wikilink intercept (post-v1.2 followup) #142),
    without which the SVG and the per-link backlinks list would be
    inert.

Building this as core today would re-introduce the gap v1.2 was
supposed to close. Shipping it as the reference plugin for this
capability set instead validates the API surface end-to-end and
gives the next plugin author a working blueprint.

What this PR changes

Two new directories, four new files:

public/plugins/noteser-graph/manifest.json       (25 lines)
public/plugins/noteser-graph/main.js             (~950 lines)
public/plugins/noteser-graph/README.md           (~160 lines)
src/__tests__/noteserGraphPlugin.test.ts         (~380 lines, 35 cases)

No core source changes. The existing
src/components/sidebar/BacklinksView.tsx and
src/utils/backlinks.ts stay in place; the swap plan is documented
in public/plugins/noteser-graph/README.md under "Co-existence
with core BacklinksView".

Surfaces shipped

Sidebar panel Graph (active note):

  • Backlinks: notes whose body contains a wikilink to this note's
    title (case-insensitive).
  • Unlinked mentions: whole-word title matches that are NOT inside
    an existing [[wikilink]], NOT inside a fenced code block, and
    NOT inside an inline code span.
  • "Open global graph" button at the bottom.

Fullscreen view Graph: hand-rolled force-directed SVG of the
whole vault. Adaptive iteration schedule keeps 1 k nodes under the
500 ms budget without Barnes-Hut. Header buttons cover Recompute,
Reset view, Zoom in / out, and four-direction Pan.

Performance numbers (measured on the worktree)

Operation Vault size Time Budget
Panel re-derive on note switch 5 000 notes ~23 ms <50 ms
Graph derive + layout 1 000 nodes / 3 000 edges ~400 ms total (derive ~6 ms + simulate ~400 ms) <500 ms

The plugin emits console.log lines for both numbers so the
budget is verifiable on any real vault from the devtools console.

Deprecation plan for core BacklinksView

Documented in detail in the plugin's README.md. Three steps:

  1. Default-install the plugin so a brand-new vault has both
    surfaces.
  2. Port alias support across. The core view honours
    getAliasesForNote(note) from src/utils/aliases.ts; the
    plugin currently matches title only. NoteWithBody.frontmatter
    already exposes parsed frontmatter, so the alias scanner only
    needs to be re-implemented inside the plugin.
  3. Delete src/components/sidebar/BacklinksView.tsx and its
    right-sidebar registry entry. The internal
    src/utils/backlinks.ts helper stays for the sync layer's
    broken-link check.

v1.2 API gaps surfaced

Three pieces of the brief did NOT map cleanly to the v1.2 VNode
event set, so the plugin ships button-based equivalents and a
two-click open path. Both are called out in the plugin's README:

  • Wheel = zoom: no onWheel on VNodes. Shipped as Zoom in /
    out buttons.
  • Drag = pan: no onPointerDown / onMouseMove on VNodes.
    Shipped as four-direction Pan buttons.
  • Click a circle opens the note in one click: SVG children
    accept onClick only (no link-wrapping in the SvgChild union),
    and PluginCtx has no ctx.openNote(id) method. Shipped as
    two-click: click circle to select, then click the link VNode
    the plugin renders in the header to navigate (the host's
    wikilink:// intercept fires on that click).

A v1.3 increment that adds onWheel + pointer events on SVG and a
ctx.openNote(id) method would close all three without breaking
this plugin's UI. Not blocking the merge.

Test plan

  • npm run lint clean.
  • npx tsc --noEmit zero errors.
  • npm test -- --ci green (208 suites, 2 599 tests pass; 1
    skipped suite is unrelated).
  • New tests at src/__tests__/noteserGraphPlugin.test.ts
    cover the unlinked-mention detector (code-block exclusion,
    inline-code exclusion, wikilink exclusion, whole-word
    matching, multi-word titles, case-insensitivity), graph
    derivation on a 5-note fixture (self-link drop, parallel-
    edge de-dup, untitled-source handling, degree counting,
    orphan handling), the snapshot-SHA cache key, and the
    force simulator's determinism + bounds.
  • Manual smoke: run npm run dev, paste
    http://localhost:3001/plugins/noteser-graph/manifest.json
    into Settings -> Plugins, grant vault.read.all +
    vault.events, open a note with wikilinks, verify the
    panel shows backlinks; click "Open global graph", verify
    the SVG renders, the zoom / pan buttons work, and clicking
    a node followed by clicking the resulting link row opens
    that note.

🤖 Generated with Claude Code

noteser-agent and others added 2 commits June 7, 2026 09:07
Ships a reference plugin at public/plugins/noteser-graph/ that closes
issue #71 by delivering both the backlinks / unlinked-mentions sidebar
panel and the force-directed global graph view, all on top of the
Plugin API v1.2 capability surface that landed last week.

Sidebar panel "Graph" (one of the v1.2 sidebarPanels surfaces):
  - Backlinks section: notes whose body contains a wikilink to the
    active note (case-insensitive title match, same shape as the core
    findBacklinks helper).
  - Unlinked mentions section: notes containing the active note's
    title as plain text, with whole-word matching plus exclusion of
    occurrences inside fenced code blocks, inline code spans, and
    existing wikilinks.
  - "Open global graph" button at the bottom.

Fullscreen view "Graph" (v1.2 fullscreenViews surface, PR B):
  - Force-directed SVG of the entire vault. Nodes are notes, edges
    are wikilinks. Hand-rolled O(n^2) repulsion + spring attraction
    + center pull + damping, with adaptive iteration count so 1 k
    nodes lay out comfortably under the 500 ms budget the brief
    calls for.
  - Header buttons: Recompute, Reset view, Zoom in / out, Pan four
    directions. Clicking a circle stores the picked node id and
    re-renders with a `link` VNode (`{ kind: 'note', noteId }`) in
    the header so the host's wikilink:// intercept opens the note on
    the confirmation click.

Permissions: vault.read.all (snapshot every note's body for the
graph + mention scan) and vault.events (re-derive the panel + graph
on save / active-note change; host already debounces at 250 ms).

Tests at src/__tests__/noteserGraphPlugin.test.ts cover the pure
derivation helpers (35 cases): unlinked-mention detector including
code-block / wikilink exclusion and whole-word matching; graph
derivation on a 5-note fixture covering self-link drop, parallel-
edge de-dup, untitled-source handling, and degree counting; the
snapshot-SHA cache key; and the force simulator's determinism +
bounds.

Perf on the worktree:
  - Panel re-derive on a 5 000-note vault: ~23 ms (target <50 ms).
  - Graph layout open on 1 000 nodes / 3 000 edges: ~400 ms (target
    <500 ms).

The plugin co-exists with the core src/components/sidebar/BacklinksView.tsx;
the README documents the swap plan (default-install the plugin, port
alias support across, then retire the core view).

The README also flags the v1.2 API gaps the brief surfaced: there is
no `onWheel` / pointer event on SVG children and no `ctx.openNote(id)`,
so wheel-zoom and drag-pan ship as button-based equivalents and
clicking a circle is a two-click navigation through a `link` VNode.
A v1.3 increment would close both.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Strips Unicode em dashes, ellipses, middle dots, and box-drawing
characters from main.js and README.md so the source diffs render as
text rather than binary in code review. Replaces them with ASCII
equivalents that read the same in fixed-width fonts.

Also corrects the README's force-simulator description: the
iteration count is adaptive (220 for <=100 nodes down to 25 above
1 000), not a flat 220, so a 1 k-node layout completes in ~400 ms
rather than the ~1.95 s the flat schedule produced.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@thetechjon thetechjon merged commit 7c04a49 into dev Jun 7, 2026
3 checks passed
@thetechjon thetechjon deleted the feat/graph-plugin-71 branch June 7, 2026 06:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant