Skip to content

Commit fb686ff

Browse files
authored
Merge pull request #10 from maichler/fix/suggestlinks-noise
fix(linkdetect): reduce suggestLinks noise with structural top-5 cap and project size filter
2 parents 65db12f + a275a90 commit fb686ff

2 files changed

Lines changed: 85 additions & 5 deletions

File tree

src/core/linkdetect.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { describe, it, expect } from "vitest";
2+
import { suggestLinks, type VaultIndex } from "./linkdetect.js";
3+
4+
function makeVaultIndex(
5+
titles: string[],
6+
frontmatter: Map<string, Record<string, unknown>> = new Map(),
7+
): VaultIndex {
8+
return {
9+
titles,
10+
frontmatter,
11+
graph: { outgoing: new Map(), incoming: new Map() },
12+
};
13+
}
14+
15+
describe("suggestLinks", () => {
16+
it("skips project-overlap when project has more than 10 notes", () => {
17+
// 15 notes all sharing the same project
18+
const titles = Array.from({ length: 15 }, (_, i) => `note-${i}`);
19+
const frontmatter = new Map(
20+
titles.map((t) => [t, { project: ["big-project"] }]),
21+
);
22+
const vaultIndex = makeVaultIndex(titles, frontmatter);
23+
24+
const suggestions = suggestLinks(
25+
{ project: ["big-project"] },
26+
"some body text with no title matches",
27+
vaultIndex,
28+
);
29+
30+
const projectSuggestions = suggestions.filter(
31+
(s) => s.reason === "project-overlap",
32+
);
33+
expect(projectSuggestions).toHaveLength(0);
34+
});
35+
36+
it("keeps project-overlap when project has 10 or fewer notes", () => {
37+
const titles = ["note-a", "note-b", "note-c"];
38+
const frontmatter = new Map(
39+
titles.map((t) => [t, { project: ["small-project"] }]),
40+
);
41+
const vaultIndex = makeVaultIndex(titles, frontmatter);
42+
43+
const suggestions = suggestLinks(
44+
{ project: ["small-project"] },
45+
"unrelated body",
46+
vaultIndex,
47+
);
48+
49+
const projectSuggestions = suggestions.filter(
50+
(s) => s.reason === "project-overlap",
51+
);
52+
expect(projectSuggestions.length).toBeGreaterThan(0);
53+
});
54+
55+
it("caps total suggestions to 5", () => {
56+
// 8 notes with tag overlap to generate many suggestions
57+
const titles = Array.from({ length: 8 }, (_, i) => `tagged-${i}`);
58+
const frontmatter = new Map(
59+
titles.map((t) => [t, { tags: ["common-tag"] }]),
60+
);
61+
const vaultIndex = makeVaultIndex(titles, frontmatter);
62+
63+
const suggestions = suggestLinks(
64+
{ tags: ["common-tag"] },
65+
"unrelated body",
66+
vaultIndex,
67+
);
68+
69+
expect(suggestions.length).toBeLessThanOrEqual(5);
70+
});
71+
});

src/core/linkdetect.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,23 @@ export function suggestLinks(
160160
}
161161
}
162162

163-
// Project overlap
163+
// Project overlap (skip when a project has too many notes to be a useful signal)
164164
if (noteProject.length > 0) {
165+
const projectSizes = new Map<string, number>();
166+
for (const [, fm] of vaultIndex.frontmatter) {
167+
const projects = Array.isArray(fm.project) ? (fm.project as string[]) : [];
168+
for (const p of projects)
169+
projectSizes.set(p, (projectSizes.get(p) ?? 0) + 1);
170+
}
171+
165172
for (const [title, fm] of vaultIndex.frontmatter) {
166173
if (suggestions.has(title)) continue;
167174
const otherProject = Array.isArray(fm.project)
168175
? (fm.project as string[])
169176
: [];
170-
const overlap = noteProject.filter((p) => otherProject.includes(p));
177+
const overlap = noteProject.filter(
178+
(p) => otherProject.includes(p) && (projectSizes.get(p) ?? 0) <= 10,
179+
);
171180
if (overlap.length > 0) {
172181
suggestions.set(title, {
173182
title,
@@ -227,9 +236,9 @@ export function suggestLinks(
227236
}
228237
}
229238

230-
return Array.from(suggestions.values()).sort(
231-
(a, b) => b.confidence - a.confidence
232-
);
239+
return Array.from(suggestions.values())
240+
.sort((a, b) => b.confidence - a.confidence)
241+
.slice(0, 5);
233242
}
234243

235244
/**

0 commit comments

Comments
 (0)