Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/graph/builders/explicit/traverse_note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ function dfs_edges(
if (visited.has(item.node)) continue;
visited.add(item.node);
if (item.parent !== null) edges.push([item.parent, item.node]);
for (const child of Object.keys(links[item.node] ?? {})) {
// Push in reverse so the stack pops children in link order (#790).
for (const child of Object.keys(links[item.node] ?? {}).reverse()) {
if (!visited.has(child)) {
stack.push({ node: child, parent: item.node });
}
Expand Down
8 changes: 8 additions & 0 deletions tests/graph/builders/traverse_note.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ describe("traverse_note builder", () => {
t.expect(r.edges.every((e) => e.edge_type === "up")).toBe(true);
});

test("children are emitted in link order (#790)", async (t) => {
const r = await _add_explicit_edges_traverse_note(
plugin({ "A.md": { "B.md": 1, "C.md": 1, "D.md": 1 } }),
make_all_files([mock_file("A.md", { frontmatter: FIELD })]),
);
t.expect(r.edges.map((e) => e.target)).toEqual(["B.md", "C.md", "D.md"]);
});

test("cycles are not revisited", async (t) => {
const r = await _add_explicit_edges_traverse_note(
plugin({ "A.md": { "B.md": 1 }, "B.md": { "A.md": 1 } }),
Expand Down
5 changes: 3 additions & 2 deletions wasm/pkg/breadcrumbs_graph_wasm.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ export class NoteGraph {
*/
get_filtered_grouped_outgoing_edges(node: string, edge_types?: string[] | null): GroupedEdgeList;
/**
* Get all outgoing edges from a node, filtered by edge type.
* Get all outgoing edges from a node, filtered by edge type, in insertion
* (document) order.
*/
get_filtered_outgoing_edges(node: string, edge_types?: string[] | null): EdgeList;
/**
Expand All @@ -244,7 +245,7 @@ export class NoteGraph {
get_incoming_edges(node: string): EdgeList;
get_node(node: string): NodeData | undefined;
/**
* Get all outgoing edges from a node.
* Get all outgoing edges from a node, in insertion (document) order.
*/
get_outgoing_edges(node: string): EdgeList;
/**
Expand Down
5 changes: 3 additions & 2 deletions wasm/pkg/breadcrumbs_graph_wasm.js
Original file line number Diff line number Diff line change
Expand Up @@ -1604,7 +1604,8 @@ export class NoteGraph {
return GroupedEdgeList.__wrap(ret);
}
/**
* Get all outgoing edges from a node, filtered by edge type.
* Get all outgoing edges from a node, filtered by edge type, in insertion
* (document) order.
* @param {string} node
* @param {string[] | null} [edge_types]
* @returns {EdgeList}
Expand Down Expand Up @@ -1639,7 +1640,7 @@ export class NoteGraph {
return ret === 0 ? undefined : NodeData.__wrap(ret);
}
/**
* Get all outgoing edges from a node.
* Get all outgoing edges from a node, in insertion (document) order.
* @param {string} node
* @returns {EdgeList}
*/
Expand Down
Binary file modified wasm/pkg/breadcrumbs_graph_wasm_bg.wasm
Binary file not shown.
36 changes: 18 additions & 18 deletions wasm/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,20 +185,13 @@ impl NoteGraph {
});
}

/// Get all outgoing edges from a node.
/// Get all outgoing edges from a node, in insertion (document) order.
pub fn get_outgoing_edges(&self, node: String) -> EdgeList {
let node_index = self.int_get_node_index(&node);

EdgeList::from_vec(match node_index {
Some(node_index) => self
.int_iter_outgoing_edges(node_index)
.map(|edge| EdgeStruct::from_edge_ref(edge, self))
.collect(),
None => Vec::new(),
})
self.get_filtered_outgoing_edges(node, None)
}

/// Get all outgoing edges from a node, filtered by edge type.
/// Get all outgoing edges from a node, filtered by edge type, in insertion
/// (document) order.
pub fn get_filtered_outgoing_edges(
&self,
node: String,
Expand All @@ -207,13 +200,20 @@ impl NoteGraph {
let node_index = self.int_get_node_index(&node);

EdgeList::from_vec(match node_index {
Some(node_index) => self
.int_iter_outgoing_edges(node_index)
.filter(|edge_ref| {
edge_matches_edge_filter_string(edge_ref.weight(), edge_types.as_ref())
})
.map(|edge| EdgeStruct::from_edge_ref(edge, self))
.collect(),
Some(node_index) => {
let mut edges: Vec<EdgeStruct> = self
.int_iter_outgoing_edges(node_index)
.filter(|edge_ref| {
edge_matches_edge_filter_string(edge_ref.weight(), edge_types.as_ref())
})
.map(|edge| EdgeStruct::from_edge_ref(edge, self))
.collect();
// petgraph prepends each new edge to the node's adjacency list, so
// iteration is newest-first. Reverse to restore insertion order,
// which is the order the builders read fields from the note (#790).
edges.reverse();
edges
}
None => Vec::new(),
})
}
Expand Down
47 changes: 47 additions & 0 deletions wasm/tests/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,53 @@ fn test_get_filtered_outgoing_edges_selects_one_of_two_types() {
assert_eq!(down_edges.edges.len(), 1);
}

// ---- outgoing edge order (#790) ----

fn targets(g: &NoteGraph, edges: &[breadcrumbs_graph_wasm::data::edge_struct::EdgeStruct]) -> Vec<String> {
edges.iter().map(|e| e.target_path(g).unwrap()).collect()
}

/// `2026-W38` from #790: two `next` fields, in that order in the note.
fn week_graph() -> NoteGraph {
let mut g = NoteGraph::new();
g.build_graph(
vec![node("W38"), node("2026-09"), node("W37"), node("W39"), node("My weeks")],
vec![
edge("W38", "2026-09", "up"),
edge("W38", "W37", "prev"),
edge("W38", "W39", "next"),
edge("W38", "My weeks", "next"),
],
vec![],
)
.unwrap();
g
}

#[wasm_bindgen_test]
fn test_get_outgoing_edges_keeps_insertion_order() {
let g = week_graph();
let edges = g.get_outgoing_edges("W38".to_owned()).edges;
assert_eq!(targets(&g, &edges), vec!["2026-09", "W37", "W39", "My weeks"]);
}

#[wasm_bindgen_test]
fn test_get_filtered_outgoing_edges_keeps_insertion_order() {
let g = week_graph();
let edges = g
.get_filtered_outgoing_edges("W38".to_owned(), Some(vec!["next".to_owned()]))
.edges;
assert_eq!(targets(&g, &edges), vec!["W39", "My weeks"]);
}

#[wasm_bindgen_test]
fn test_get_filtered_grouped_outgoing_edges_keeps_insertion_order() {
let g = week_graph();
let grouped = g.get_filtered_grouped_outgoing_edges("W38".to_owned(), None);
let next = grouped.get_edges("next").unwrap();
assert_eq!(targets(&g, &next), vec!["W39", "My weeks"]);
}

// ---- edge_matches_edge_filter (Rc<str> variant) ----

#[wasm_bindgen_test]
Expand Down
Loading