diff --git a/src/commands/edge_audit/index.ts b/src/commands/edge_audit/index.ts index 46433ee4..49c58fa1 100644 --- a/src/commands/edge_audit/index.ts +++ b/src/commands/edge_audit/index.ts @@ -36,6 +36,9 @@ export const collect_graph_facts = (graph: NoteGraph): GraphFacts => { .get_outgoing_edges(path) .get_edges() .forEach((edge: EdgeStruct) => { + // self_is_sibling self-loops would show up as dangling edges on + // unresolved notes and block mergeable-field matches (#785). + if (edge.is_self_loop()) return; edges.push({ field: edge.edge_type, source: edge.source_path(graph), diff --git a/src/commands/list_index/index.ts b/src/commands/list_index/index.ts index 014039cb..501a5809 100644 --- a/src/commands/list_index/index.ts +++ b/src/commands/list_index/index.ts @@ -101,6 +101,10 @@ function edge_tree_to_list_index_inner( current_nodes.forEach((datum) => { const { edge, children, depth } = datum; + // self_is_sibling adds a note→itself edge for the views; following it here + // lists the note as its own child and repeats its siblings (#785). + if (edge.is_self_loop()) return; + const display = edge.stringify_target(graph, stringify_options); const link = Links.ify(edge.target_path(graph), display, { diff --git a/src/utils/canvas.ts b/src/utils/canvas.ts index 1e53b535..41451d4b 100644 --- a/src/utils/canvas.ts +++ b/src/utils/canvas.ts @@ -102,6 +102,18 @@ export function build_canvas( const data = result.data; + // self_is_sibling adds a note→itself edge for the views. Leave out each + // self-loop and the subtree the traversal followed back through it (#785). + const skipped = new Set(); + const skip = (index: number) => { + if (skipped.has(index)) return; + skipped.add(index); + for (const c of data[index].children) skip(c); + }; + data.forEach((datum, i) => { + if (datum.edge.is_self_loop()) skip(i); + }); + // Forest roots = data entries that are nobody's child (the first hops). const is_child = new Set(); for (const datum of data) { @@ -121,7 +133,7 @@ export function build_canvas( const existing = placements.get(path); if (existing) return existing.cross; - const kids = Array.from(datum.children); + const kids = Array.from(datum.children).filter((c) => !skipped.has(c)); let cross: number; if (kids.length === 0) { cross = next_slot++; @@ -137,7 +149,7 @@ export function build_canvas( const root_crosses = data .map((_, i) => i) - .filter((i) => !is_child.has(i)) + .filter((i) => !is_child.has(i) && !skipped.has(i)) .map(layout); // The source note sits at depth 0, centred over its roots. @@ -164,7 +176,8 @@ export function build_canvas( const edges: CanvasEdge[] = []; const edge_ids = new Set(); - for (const datum of data) { + for (const [i, datum] of data.entries()) { + if (skipped.has(i)) continue; const from = datum.edge.source_path(graph); const to = datum.edge.target_path(graph); diff --git a/tests/commands/edge_audit.test.ts b/tests/commands/edge_audit.test.ts index 21e4f087..3f77243d 100644 --- a/tests/commands/edge_audit.test.ts +++ b/tests/commands/edge_audit.test.ts @@ -10,7 +10,14 @@ import { type GraphFacts, type NodeFact, } from "src/commands/edge_audit/analyze"; -import { describe, expect, test } from "vitest"; +import { collect_graph_facts } from "src/commands/edge_audit"; +import fs from "node:fs/promises"; +import { beforeEach, describe, expect, test } from "vitest"; +import init, { + create_graph, + GCEdgeData, + GCNodeData, +} from "wasm/pkg/breadcrumbs_graph_wasm"; const node = (path: string, resolved = true): NodeFact => ({ path, resolved }); @@ -270,3 +277,33 @@ describe("build_edge_audit", () => { expect(report.dangling_edges).toStrictEqual([]); }); }); + +describe("collect_graph_facts", () => { + beforeEach(async () => { + const wasmSource = await fs.readFile( + "wasm/pkg/breadcrumbs_graph_wasm_bg.wasm", + ); + // @ts-ignore TS2345 + const wasmModule = await WebAssembly.compile(wasmSource); + await init(wasmModule); + }); + + test("leaves out self_is_sibling self-loops (#785)", () => { + const graph = create_graph(); + graph.build_graph( + [new GCNodeData("Hub.md", [], true, false, false)], + [ + new GCEdgeData("Hub.md", "Missing.md", "same", ""), + new GCEdgeData("Hub.md", "Hub.md", "same", "implied"), + new GCEdgeData("Missing.md", "Missing.md", "same", "implied"), + ], + [], + ); + + const facts = collect_graph_facts(graph); + + expect(facts.edges.map((e) => `${e.source}→${e.target}`)).toEqual([ + "Hub.md→Missing.md", + ]); + }); +}); diff --git a/tests/commands/list_index.test.ts b/tests/commands/list_index.test.ts index 4d03ea05..a3877a14 100644 --- a/tests/commands/list_index.test.ts +++ b/tests/commands/list_index.test.ts @@ -139,4 +139,31 @@ describe("build", () => { ].join("\n"), ); }); + + test("self-loop edges are left out (#785)", () => { + const graph = create_graph(); + graph.build_graph( + ["Hub.md", "Sibling.md", "Twin.md"].map( + (p) => new GCNodeData(p, [], true, false, false), + ), + [ + new GCEdgeData("Hub.md", "Sibling.md", "same", ""), + new GCEdgeData("Hub.md", "Twin.md", "same", ""), + new GCEdgeData("Hub.md", "Hub.md", "same", "implied"), + ], + [], + ); + + const list_index = build_list_index(graph, "Hub.md", undefined, { + indent: " ", + fields: ["same"], + show_attributes: [], + field_group_labels: [], + link_kind: "none", + edge_sort_id: { order: 1, field: "basename" }, + show_node_options: { ext: false, alias: false, folder: false }, + }); + + expect(list_index).toBe(["- Sibling", "- Twin", ""].join("\n")); + }); }); diff --git a/tests/utils/canvas.test.ts b/tests/utils/canvas.test.ts index 2c111e75..78197216 100644 --- a/tests/utils/canvas.test.ts +++ b/tests/utils/canvas.test.ts @@ -109,4 +109,24 @@ describe("build_canvas semantic sides", () => { "a→c": "right/left", }); }); + + test("self-loop edges are left out (#785)", () => { + const graph = graph_of([ + ["Hub", "Sibling", "same"], + ["Hub", "Twin", "same"], + ["Hub", "Hub", "same"], + ]); + const result = traverse(graph, { + entry: ["Hub"], + depth: 5, + separateEdges: false, + flatten: true, + }); + const canvas = build_canvas(graph, result, "Hub", "LR"); + result.free(); + + expect( + canvas.edges.map((e) => `${e.fromNode}→${e.toNode}`).sort(), + ).toEqual(["Hub→Sibling", "Hub→Twin"]); + }); });