// Web Worker: парсит .md-тексты в агрегаты (off-main, без фриза UI). // Получает RawNote[], возвращает Stats. Тексты заметок дальше воркера не идут. import type { RawNote, Stats, Period, Archetype, GraphData } from "./types"; const WIKILINK = /\[\[([^\]|#]+)/g; const TAG = /(^|\s)#([\p{L}\d_/-]+)/gu; const DAY = 864e5; function filterByPeriod(notes: RawNote[], period: Period): RawNote[] { if (period === "all") return notes; const cutoff = Date.now() - (period === "year" ? 365 * DAY : 90 * DAY); return notes.filter((n) => n.mtime >= cutoff); } function buildGraph(degree: Map, edges: { source: string; target: string }[]): GraphData { const TOP = 150; const top = [...degree.entries()].sort((a, b) => b[1] - a[1]).slice(0, TOP); const nodes = top.map(([id, deg]) => ({ id, deg })); const ids = new Set(nodes.map((n) => n.id)); const links = edges.filter((e) => ids.has(e.source) && ids.has(e.target)).slice(0, 600); return { nodes, links }; } function buildStats( notes: RawNote[], degree: Map, tags: Map, linkCount: number, period: Period, edges: { source: string; target: string }[], ): Stats { const noteCount = notes.length; const topTags = [...tags.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([tag, n]) => ({ tag, n })); const topLinked = [...degree.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([name, d]) => ({ name, degree: d })); // Сирота — заметка, на которую никто не ссылается (грубая оценка по имени). const linkedTargets = new Set(degree.keys()); const orphans = notes.filter((n) => !linkedTargets.has(n.name.replace(/\.md$/i, ""))).length; const months = new Map(); for (const n of notes) { const d = new Date(n.mtime); const k = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`; months.set(k, (months.get(k) ?? 0) + 1); } const busiestMonth = [...months.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? ""; const linkDensity = noteCount ? linkCount / noteCount : 0; const tagDensity = noteCount ? tags.size / noteCount : 0; const orphanRatio = noteCount ? orphans / noteCount : 1; let archetype: Archetype; if (linkDensity >= 2) archetype = "architect"; else if (tagDensity >= 0.5) archetype = "librarian"; else if (orphanRatio >= 0.4) archetype = "gardener"; else archetype = "pragmatist"; const score = Math.max(0, Math.min(100, Math.round( Math.min(linkDensity / 3, 1) * 45 + Math.min(tagDensity, 1) * 25 + Math.min(noteCount / 300, 1) * 30, ))); const achievements: string[] = []; if (noteCount >= 100) achievements.push(`${noteCount} заметок`); if (linkCount >= 200) achievements.push(`${linkCount} связей`); if (tags.size >= 30) achievements.push(`${tags.size} тегов`); if (linkDensity >= 3) achievements.push("Плотная сеть знаний"); if (orphanRatio < 0.1 && noteCount >= 50) achievements.push("Почти нет сирот"); return { noteCount, linkCount, tagCount: tags.size, topTags, topLinked, orphans, busiestMonth, achievements, archetype, score, period, graph: buildGraph(degree, edges) }; } self.onmessage = (e: MessageEvent<{ notes: RawNote[]; period: Period }>) => { const { notes, period } = e.data; const filtered = filterByPeriod(notes, period); const degree = new Map(); const tags = new Map(); let linkCount = 0; const edges: { source: string; target: string }[] = []; for (const n of filtered) { const src = n.name.replace(/\.md$/i, ""); for (const m of n.text.matchAll(WIKILINK)) { linkCount++; const t = m[1].trim(); if (t) { degree.set(t, (degree.get(t) ?? 0) + 1); edges.push({ source: src, target: t }); } } for (const m of n.text.matchAll(TAG)) { const t = m[2]; tags.set(t, (tags.get(t) ?? 0) + 1); } } self.postMessage(buildStats(filtered, degree, tags, linkCount, period, edges)); };