From bff34946516987a0761e8800f455442b23fabe27 Mon Sep 17 00:00:00 2001 From: Yugal Kaushik Date: Sat, 22 Aug 2026 21:50:27 +0530 Subject: [PATCH] ANG-015:update manifest.json and added golden-set tests --- src/manifest.json | 9 +- .../AnalysisController.golden.test.ts | 180 ++++++++ .../AnalysisController.golden.test.ts.snap | 224 ++++++++++ .../graph/GraphPipeline.golden.test.ts | 128 ++++++ .../GraphPipeline.golden.test.ts.snap | 397 ++++++++++++++++++ 5 files changed, 935 insertions(+), 3 deletions(-) create mode 100644 src/services/AnalysisController.golden.test.ts create mode 100644 src/services/__snapshots__/AnalysisController.golden.test.ts.snap create mode 100644 src/services/graph/GraphPipeline.golden.test.ts create mode 100644 src/services/graph/__snapshots__/GraphPipeline.golden.test.ts.snap diff --git a/src/manifest.json b/src/manifest.json index 3d17012..6f46758 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -4,7 +4,7 @@ "app_min_version": "3.5", "version": "1.0.0", "name": "Note Graph Plugin", - "description": "A Joplin plugin that uses AI to discover hidden connections between your notes.", + "description": "Visualizes your notes as an interactive graph, connecting them by links, tags and AI-detected semantic similarity.", "author": "yugalkaushik", "homepage_url": "", "repository_url": "https://github.com/joplin/plugin-note-graph", @@ -16,8 +16,11 @@ "text embeddings", "knowledge graph" ], - "categories": [], - "screenshots": [], + "categories": [ + "productivity", + "personal knowledge management" + ], + "screenshots": {}, "icons": {}, "promo_tile": {} } diff --git a/src/services/AnalysisController.golden.test.ts b/src/services/AnalysisController.golden.test.ts new file mode 100644 index 0000000..cdaf740 --- /dev/null +++ b/src/services/AnalysisController.golden.test.ts @@ -0,0 +1,180 @@ +import joplin from 'api'; +import { AnalysisController } from './AnalysisController'; +import { NoteRepository } from '../data/NoteRepository'; +import { NotePreprocessor } from '../data/NotePreprocessor'; +import { Note } from '../data/Types'; + +jest.mock('../data/Database/VectorRepository', () => ({ + VectorRepository: jest.fn().mockImplementation(() => ({ + getMany: jest.fn().mockResolvedValue(new Map()), + saveMany: jest.fn().mockResolvedValue(undefined), + })), +})); + +jest.mock('../data/Database/GraphCacheRepository', () => ({ + GraphCacheRepository: jest.fn().mockImplementation(() => ({ + loadGraph: jest.fn().mockResolvedValue(null), + saveGraph: jest.fn().mockResolvedValue(undefined), + saveScopeKey: jest.fn().mockResolvedValue(undefined), + saveEnrichments: jest.fn().mockResolvedValue(undefined), + loadEnrichments: jest.fn().mockResolvedValue([]), + })), +})); + +const hex = (digit: string): string => digit.repeat(32); + +const A = hex('1'); +const B = hex('2'); +const C = hex('3'); +const D = hex('4'); + +const rawNotes = [ + { + id: A, + parent_id: 'p', + title: 'Alpha Note', + body: `See [Beta](:/${B}) and [Gamma](:/${C}).`, + created_time: 0, + updated_time: 1, + deleted_time: 0, + }, + { + id: B, + parent_id: 'p', + title: 'Beta', + body: `Related to [Gamma](:/${C}).`, + created_time: 0, + updated_time: 1, + deleted_time: 0, + }, + { + id: C, + parent_id: 'p', + title: 'Gamma', + body: `Back to [Alpha](:/${A}).`, + created_time: 0, + updated_time: 1, + deleted_time: 0, + }, + { + id: D, + parent_id: 'p', + title: 'Delta', + body: `References [Alpha](:/${A}).`, + created_time: 0, + updated_time: 1, + deleted_time: 0, + }, +]; + +const tags = [ + { id: 't1', title: 'project' }, + { id: 't2', title: 'todo' }, +]; + +const tagNoteIds: Record = { + t1: [A, B], + t2: [A, D], +}; + +const embeddings = [ + { noteId: A, chunkIndex: 0, chunkText: '', vector: [1, 0] }, + { noteId: B, chunkIndex: 0, chunkText: '', vector: [1, 0] }, + { noteId: C, chunkIndex: 0, chunkText: '', vector: [0, 1] }, + { noteId: D, chunkIndex: 0, chunkText: '', vector: [0.6, 0.8] }, +]; + +const chatResponse = JSON.stringify({ + notes: [ + { id: A, category: 'Gardening', centralityAdjustment: 0 }, + { id: B, category: 'Gardening', centralityAdjustment: 0 }, + { id: C, category: 'Cooking', centralityAdjustment: 1 }, + { id: D, category: 'Gardening', centralityAdjustment: -1 }, + ], + relationships: [ + { from: A, to: B, label: 'both list watering schedules' }, + { from: C, to: D, label: 'expands the retry logic' }, + ], +}); + +type ApiMock = { + data: { get: jest.Mock }; + ai: { getIndexStatus: jest.Mock; getEmbeddings: jest.Mock; chat: jest.Mock }; + settings: { value: jest.Mock; values: jest.Mock }; +}; + +function stubApi(): void { + const api = joplin as unknown as ApiMock; + + api.data.get.mockImplementation(async (path: string[]) => { + if (path[0] === 'notes' && path.length === 1) { + return { items: rawNotes, has_more: false }; + } + if (path[0] === 'tags' && path.length === 1) { + return { items: tags, has_more: false }; + } + if (path[0] === 'tags' && path[2] === 'notes') { + return { items: (tagNoteIds[path[1]] ?? []).map((id) => ({ id })), has_more: false }; + } + return { items: [], has_more: false }; + }); + + api.ai.getIndexStatus.mockResolvedValue({ + state: 'ready', + modelId: 'test-model', + notesIndexed: rawNotes.length, + ready: true, + totalNotes: rawNotes.length, + }); + api.ai.getEmbeddings.mockResolvedValue({ + chunks: embeddings, + dimension: 2, + modelId: 'test-model', + }); + api.ai.chat.mockResolvedValue(chatResponse); + + api.settings.value.mockImplementation( + (key: string) => + key === 'noteGraph.aiAnalysisEnabled' || key === 'noteGraph.llmEnrichmentEnabled' + ); + api.settings.values.mockResolvedValue({ + 'noteGraph.similarityThreshold': 50, + 'noteGraph.maxEdgesPerNote': 5, + }); +} + +async function loadEnrichedNotes(): Promise { + const { notes } = await new NoteRepository().getAllNotes(); + return new NotePreprocessor().process(notes); +} + +describe('AnalysisController (full pipeline golden set)', () => { + beforeEach(() => { + jest.spyOn(console, 'info').mockImplementation(() => undefined); + jest.spyOn(console, 'warn').mockImplementation(() => undefined); + jest.spyOn(console, 'error').mockImplementation(() => undefined); + stubApi(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('renders the structural graph end-to-end from notes fetched and enriched through the Joplin API', async () => { + const controller = new AnalysisController(); + const notes = await loadEnrichedNotes(); + + expect(controller.buildStructural(notes)).toMatchSnapshot(); + }); + + it('renders the enriched semantic graph end-to-end through embedding fetch and Pass B LLM labelling', async () => { + const controller = new AnalysisController(); + const notes = await loadEnrichedNotes(); + + const result = await controller.embedAndBuildSemantic(notes); + await controller.enrichCurrentGraph(); + + expect(result?.usedAi).toBe(true); + expect(controller.getLastGraphData()).toMatchSnapshot(); + }); +}); diff --git a/src/services/__snapshots__/AnalysisController.golden.test.ts.snap b/src/services/__snapshots__/AnalysisController.golden.test.ts.snap new file mode 100644 index 0000000..b691f25 --- /dev/null +++ b/src/services/__snapshots__/AnalysisController.golden.test.ts.snap @@ -0,0 +1,224 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AnalysisController (full pipeline golden set) renders the enriched semantic graph end-to-end through embedding fetch and Pass B LLM labelling 1`] = ` +{ + "edges": [ + { + "data": { + "id": "11111111111111111111111111111111::22222222222222222222222222222222::link", + "source": "11111111111111111111111111111111", + "target": "22222222222222222222222222222222", + "type": "link", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::33333333333333333333333333333333::link", + "source": "11111111111111111111111111111111", + "target": "33333333333333333333333333333333", + "type": "link", + }, + }, + { + "data": { + "id": "22222222222222222222222222222222::33333333333333333333333333333333::link", + "source": "22222222222222222222222222222222", + "target": "33333333333333333333333333333333", + "type": "link", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::44444444444444444444444444444444::link", + "source": "11111111111111111111111111111111", + "target": "44444444444444444444444444444444", + "type": "link", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::22222222222222222222222222222222::tag", + "source": "11111111111111111111111111111111", + "tagName": "project", + "target": "22222222222222222222222222222222", + "type": "tag", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::44444444444444444444444444444444::tag", + "source": "11111111111111111111111111111111", + "tagName": "todo", + "target": "44444444444444444444444444444444", + "type": "tag", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::22222222222222222222222222222222::semantic", + "relationshipLabel": "both list watering schedules", + "score": 1.15, + "source": "11111111111111111111111111111111", + "target": "22222222222222222222222222222222", + "type": "semantic", + }, + }, + { + "data": { + "id": "33333333333333333333333333333333::44444444444444444444444444444444::semantic", + "relationshipLabel": "expands the retry logic", + "score": 0.6000000000000001, + "source": "33333333333333333333333333333333", + "target": "44444444444444444444444444444444", + "type": "semantic", + }, + }, + ], + "nodes": [ + { + "data": { + "category": "Gardening", + "community": 0, + "degree": 6, + "id": "11111111111111111111111111111111", + "label": "Alpha Note", + "noteId": "11111111111111111111111111111111", + "size": 10, + }, + }, + { + "data": { + "category": "Gardening", + "community": 0, + "degree": 4, + "id": "22222222222222222222222222222222", + "label": "Beta", + "noteId": "22222222222222222222222222222222", + "size": 6, + }, + }, + { + "data": { + "category": "Cooking", + "community": 0, + "degree": 3, + "id": "33333333333333333333333333333333", + "label": "Gamma", + "noteId": "33333333333333333333333333333333", + "size": 2, + }, + }, + { + "data": { + "category": "Gardening", + "community": 0, + "degree": 3, + "id": "44444444444444444444444444444444", + "label": "Delta", + "noteId": "44444444444444444444444444444444", + "size": 1, + }, + }, + ], +} +`; + +exports[`AnalysisController (full pipeline golden set) renders the structural graph end-to-end from notes fetched and enriched through the Joplin API 1`] = ` +{ + "allNotesVeryShort": false, + "edges": [ + { + "data": { + "id": "11111111111111111111111111111111::22222222222222222222222222222222::link", + "source": "11111111111111111111111111111111", + "target": "22222222222222222222222222222222", + "type": "link", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::33333333333333333333333333333333::link", + "source": "11111111111111111111111111111111", + "target": "33333333333333333333333333333333", + "type": "link", + }, + }, + { + "data": { + "id": "22222222222222222222222222222222::33333333333333333333333333333333::link", + "source": "22222222222222222222222222222222", + "target": "33333333333333333333333333333333", + "type": "link", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::44444444444444444444444444444444::link", + "source": "11111111111111111111111111111111", + "target": "44444444444444444444444444444444", + "type": "link", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::22222222222222222222222222222222::tag", + "source": "11111111111111111111111111111111", + "tagName": "project", + "target": "22222222222222222222222222222222", + "type": "tag", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::44444444444444444444444444444444::tag", + "source": "11111111111111111111111111111111", + "tagName": "todo", + "target": "44444444444444444444444444444444", + "type": "tag", + }, + }, + ], + "nodes": [ + { + "data": { + "community": 0, + "degree": 5, + "id": "11111111111111111111111111111111", + "label": "Alpha Note", + "noteId": "11111111111111111111111111111111", + "size": 10, + }, + }, + { + "data": { + "community": 0, + "degree": 3, + "id": "22222222222222222222222222222222", + "label": "Beta", + "noteId": "22222222222222222222222222222222", + "size": 6, + }, + }, + { + "data": { + "community": 0, + "degree": 2, + "id": "33333333333333333333333333333333", + "label": "Gamma", + "noteId": "33333333333333333333333333333333", + "size": 1, + }, + }, + { + "data": { + "community": 0, + "degree": 2, + "id": "44444444444444444444444444444444", + "label": "Delta", + "noteId": "44444444444444444444444444444444", + "size": 1, + }, + }, + ], +} +`; diff --git a/src/services/graph/GraphPipeline.golden.test.ts b/src/services/graph/GraphPipeline.golden.test.ts new file mode 100644 index 0000000..7bfba6d --- /dev/null +++ b/src/services/graph/GraphPipeline.golden.test.ts @@ -0,0 +1,128 @@ +import { GraphBuilder } from './GraphBuilder'; +import { LinkExtractor } from '../../data/LinkExtractor'; +import { Note } from '../../data/Types'; +import { EmbeddedNote } from '../embeddings/Types'; + +const linkExtractor = new LinkExtractor(); + +const hex = (digit: string): string => digit.repeat(32); + +const A = hex('1'); +const B = hex('2'); +const C = hex('3'); +const D = hex('4'); +const E = hex('5'); +const F = hex('6'); +const OUT_OF_SCOPE = hex('9'); + +function note(id: string, title: string, body: string, tags: string[] = []): Note { + return { + id, + parent_id: 'parent', + title, + body, + created_time: 0, + updated_time: 1, + links: linkExtractor.extractLinks(body), + tags, + }; +} + +describe('graph pipeline (golden set)', () => { + let builder: GraphBuilder; + + beforeEach(() => { + jest.spyOn(console, 'info').mockImplementation(() => undefined); + jest.spyOn(console, 'warn').mockImplementation(() => undefined); + jest.spyOn(console, 'error').mockImplementation(() => undefined); + builder = new GraphBuilder(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('renders the structural graph end-to-end from links, tags, communities and centrality', () => { + const notes: Note[] = [ + note(A, 'Alpha Note', `See [Beta](:/${B}) and [Gamma](:/${C}).`, ['project', 'todo']), + note(B, 'Beta', `Related to [Gamma](:/${C}).`, ['project']), + note( + C, + 'Gamma', + `Back to [Alpha](:/${A}).\n\n\`[Beta](:/${B})\` in inline code.\n\n\`\`\`\n[Alpha](:/${A}) fenced\n\`\`\``, + ['urgent'] + ), + note(D, '', `References [Alpha](:/${A}) and again.`, ['todo']), + note(E, 'E'.repeat(80), `Links to an [external note](:/${OUT_OF_SCOPE}) not in scope.`), + note(F, 'Standalone', 'A note with no connections.'), + ]; + + expect(builder.build(notes)).toMatchSnapshot(); + }); + + it('renders the semantic graph end-to-end from embeddings through thresholding and top-K', async () => { + const notes: Note[] = [ + note(A, 'Alpha', `[Beta](:/${B})`, ['project']), + note(B, 'Beta', '', ['project']), + note(C, 'Gamma', '', []), + ]; + const embeddedNotes: EmbeddedNote[] = [ + { note: notes[0], embedding: [1, 0] }, + { note: notes[1], embedding: [1, 0] }, + { note: notes[2], embedding: [0, 1] }, + ]; + + await expect( + builder.buildWithSimilarity(notes, embeddedNotes, 0.5, 5) + ).resolves.toMatchSnapshot(); + }); + + it('marks an empty corpus as not very short and a stub-only corpus as very short', () => { + expect(builder.build([]).allNotesVeryShort).toBe(false); + expect(builder.build([note(A, 'A', 'stub')]).allNotesVeryShort).toBe(true); + }); + + it('groups disconnected notes by keyword when the graph is too sparse for Louvain', () => { + const notes: Note[] = [ + note(A, 'Gardening Basics', ''), + note(B, 'Gardening Tools', ''), + note(C, 'Cooking Pasta', ''), + note(D, 'Cooking Pizza', ''), + note(E, 'Photography', ''), + ]; + + expect(builder.build(notes)).toMatchSnapshot(); + }); + + it('assigns a flat mid-range size and a single community to a cycle of equal-degree notes', () => { + const notes: Note[] = [ + note(A, 'Alpha', `[Beta](:/${B})`), + note(B, 'Beta', `[Gamma](:/${C})`), + note(C, 'Gamma', `[Alpha](:/${A})`), + ]; + + expect(builder.build(notes)).toMatchSnapshot(); + }); + + it('merges every shared tag onto a single tag edge', () => { + const notes: Note[] = [ + note(A, 'Alpha', '', ['alpha', 'beta']), + note(B, 'Beta', '', ['alpha', 'beta']), + note(C, 'Gamma', '', ['gamma']), + ]; + + expect(builder.build(notes)).toMatchSnapshot(); + }); + + it('keeps a sub-floor linked pair as a link edge without manufacturing a semantic edge', async () => { + const notes: Note[] = [note(A, 'Alpha', `[Beta](:/${B})`), note(B, 'Beta', '')]; + const embeddedNotes: EmbeddedNote[] = [ + { note: notes[0], embedding: [1, 0] }, + { note: notes[1], embedding: [0, 1] }, + ]; + + await expect( + builder.buildWithSimilarity(notes, embeddedNotes, 0.5, 5) + ).resolves.toMatchSnapshot(); + }); +}); diff --git a/src/services/graph/__snapshots__/GraphPipeline.golden.test.ts.snap b/src/services/graph/__snapshots__/GraphPipeline.golden.test.ts.snap new file mode 100644 index 0000000..12cedba --- /dev/null +++ b/src/services/graph/__snapshots__/GraphPipeline.golden.test.ts.snap @@ -0,0 +1,397 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`graph pipeline (golden set) assigns a flat mid-range size and a single community to a cycle of equal-degree notes 1`] = ` +{ + "allNotesVeryShort": false, + "edges": [ + { + "data": { + "id": "11111111111111111111111111111111::22222222222222222222222222222222::link", + "source": "11111111111111111111111111111111", + "target": "22222222222222222222222222222222", + "type": "link", + }, + }, + { + "data": { + "id": "22222222222222222222222222222222::33333333333333333333333333333333::link", + "source": "22222222222222222222222222222222", + "target": "33333333333333333333333333333333", + "type": "link", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::33333333333333333333333333333333::link", + "source": "11111111111111111111111111111111", + "target": "33333333333333333333333333333333", + "type": "link", + }, + }, + ], + "nodes": [ + { + "data": { + "community": 0, + "degree": 2, + "id": "11111111111111111111111111111111", + "label": "Alpha", + "noteId": "11111111111111111111111111111111", + "size": 5, + }, + }, + { + "data": { + "community": 0, + "degree": 2, + "id": "22222222222222222222222222222222", + "label": "Beta", + "noteId": "22222222222222222222222222222222", + "size": 5, + }, + }, + { + "data": { + "community": 0, + "degree": 2, + "id": "33333333333333333333333333333333", + "label": "Gamma", + "noteId": "33333333333333333333333333333333", + "size": 5, + }, + }, + ], +} +`; + +exports[`graph pipeline (golden set) groups disconnected notes by keyword when the graph is too sparse for Louvain 1`] = ` +{ + "allNotesVeryShort": true, + "edges": [], + "nodes": [ + { + "data": { + "community": 0, + "degree": 0, + "id": "11111111111111111111111111111111", + "label": "Gardening Basics", + "noteId": "11111111111111111111111111111111", + "size": 5, + }, + }, + { + "data": { + "community": 0, + "degree": 0, + "id": "22222222222222222222222222222222", + "label": "Gardening Tools", + "noteId": "22222222222222222222222222222222", + "size": 5, + }, + }, + { + "data": { + "community": 1, + "degree": 0, + "id": "33333333333333333333333333333333", + "label": "Cooking Pasta", + "noteId": "33333333333333333333333333333333", + "size": 5, + }, + }, + { + "data": { + "community": 1, + "degree": 0, + "id": "44444444444444444444444444444444", + "label": "Cooking Pizza", + "noteId": "44444444444444444444444444444444", + "size": 5, + }, + }, + { + "data": { + "community": 2, + "degree": 0, + "id": "55555555555555555555555555555555", + "label": "Photography", + "noteId": "55555555555555555555555555555555", + "size": 5, + }, + }, + ], +} +`; + +exports[`graph pipeline (golden set) keeps a sub-floor linked pair as a link edge without manufacturing a semantic edge 1`] = ` +{ + "allNotesVeryShort": false, + "edges": [ + { + "data": { + "id": "11111111111111111111111111111111::22222222222222222222222222222222::link", + "source": "11111111111111111111111111111111", + "target": "22222222222222222222222222222222", + "type": "link", + }, + }, + ], + "nodes": [ + { + "data": { + "community": 0, + "degree": 1, + "id": "11111111111111111111111111111111", + "label": "Alpha", + "noteId": "11111111111111111111111111111111", + "size": 5, + }, + }, + { + "data": { + "community": 0, + "degree": 1, + "id": "22222222222222222222222222222222", + "label": "Beta", + "noteId": "22222222222222222222222222222222", + "size": 5, + }, + }, + ], +} +`; + +exports[`graph pipeline (golden set) merges every shared tag onto a single tag edge 1`] = ` +{ + "allNotesVeryShort": true, + "edges": [ + { + "data": { + "id": "11111111111111111111111111111111::22222222222222222222222222222222::tag", + "source": "11111111111111111111111111111111", + "tagName": "alpha, beta", + "target": "22222222222222222222222222222222", + "type": "tag", + }, + }, + ], + "nodes": [ + { + "data": { + "community": 0, + "degree": 1, + "id": "11111111111111111111111111111111", + "label": "Alpha", + "noteId": "11111111111111111111111111111111", + "size": 10, + }, + }, + { + "data": { + "community": 0, + "degree": 1, + "id": "22222222222222222222222222222222", + "label": "Beta", + "noteId": "22222222222222222222222222222222", + "size": 10, + }, + }, + { + "data": { + "community": 1, + "degree": 0, + "id": "33333333333333333333333333333333", + "label": "Gamma", + "noteId": "33333333333333333333333333333333", + "size": 1, + }, + }, + ], +} +`; + +exports[`graph pipeline (golden set) renders the semantic graph end-to-end from embeddings through thresholding and top-K 1`] = ` +{ + "allNotesVeryShort": false, + "edges": [ + { + "data": { + "id": "11111111111111111111111111111111::22222222222222222222222222222222::link", + "source": "11111111111111111111111111111111", + "target": "22222222222222222222222222222222", + "type": "link", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::22222222222222222222222222222222::tag", + "source": "11111111111111111111111111111111", + "tagName": "project", + "target": "22222222222222222222222222222222", + "type": "tag", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::22222222222222222222222222222222::semantic", + "score": 1.15, + "source": "11111111111111111111111111111111", + "target": "22222222222222222222222222222222", + "type": "semantic", + }, + }, + ], + "nodes": [ + { + "data": { + "community": 0, + "degree": 3, + "id": "11111111111111111111111111111111", + "label": "Alpha", + "noteId": "11111111111111111111111111111111", + "size": 10, + }, + }, + { + "data": { + "community": 0, + "degree": 3, + "id": "22222222222222222222222222222222", + "label": "Beta", + "noteId": "22222222222222222222222222222222", + "size": 10, + }, + }, + { + "data": { + "community": 1, + "degree": 0, + "id": "33333333333333333333333333333333", + "label": "Gamma", + "noteId": "33333333333333333333333333333333", + "size": 1, + }, + }, + ], +} +`; + +exports[`graph pipeline (golden set) renders the structural graph end-to-end from links, tags, communities and centrality 1`] = ` +{ + "allNotesVeryShort": false, + "edges": [ + { + "data": { + "id": "11111111111111111111111111111111::22222222222222222222222222222222::link", + "source": "11111111111111111111111111111111", + "target": "22222222222222222222222222222222", + "type": "link", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::33333333333333333333333333333333::link", + "source": "11111111111111111111111111111111", + "target": "33333333333333333333333333333333", + "type": "link", + }, + }, + { + "data": { + "id": "22222222222222222222222222222222::33333333333333333333333333333333::link", + "source": "22222222222222222222222222222222", + "target": "33333333333333333333333333333333", + "type": "link", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::44444444444444444444444444444444::link", + "source": "11111111111111111111111111111111", + "target": "44444444444444444444444444444444", + "type": "link", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::22222222222222222222222222222222::tag", + "source": "11111111111111111111111111111111", + "tagName": "project", + "target": "22222222222222222222222222222222", + "type": "tag", + }, + }, + { + "data": { + "id": "11111111111111111111111111111111::44444444444444444444444444444444::tag", + "source": "11111111111111111111111111111111", + "tagName": "todo", + "target": "44444444444444444444444444444444", + "type": "tag", + }, + }, + ], + "nodes": [ + { + "data": { + "community": 0, + "degree": 5, + "id": "11111111111111111111111111111111", + "label": "Alpha Note", + "noteId": "11111111111111111111111111111111", + "size": 10, + }, + }, + { + "data": { + "community": 0, + "degree": 3, + "id": "22222222222222222222222222222222", + "label": "Beta", + "noteId": "22222222222222222222222222222222", + "size": 8, + }, + }, + { + "data": { + "community": 0, + "degree": 2, + "id": "33333333333333333333333333333333", + "label": "Gamma", + "noteId": "33333333333333333333333333333333", + "size": 7, + }, + }, + { + "data": { + "community": 0, + "degree": 2, + "id": "44444444444444444444444444444444", + "label": "(untitled)", + "noteId": "44444444444444444444444444444444", + "size": 7, + }, + }, + { + "data": { + "community": 1, + "degree": 0, + "id": "55555555555555555555555555555555", + "label": "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE...", + "noteId": "55555555555555555555555555555555", + "size": 1, + }, + }, + { + "data": { + "community": 2, + "degree": 0, + "id": "66666666666666666666666666666666", + "label": "Standalone", + "noteId": "66666666666666666666666666666666", + "size": 1, + }, + }, + ], +} +`;