sensemaking 0.15.1 → 0.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -55,6 +55,15 @@ function _unsupported_iterable_to_array(o, minLen) {
|
|
|
55
55
|
// Wikilinks ([[target]], [[target#anchor|alias]], embeds), internal markdown links, and
|
|
56
56
|
// frontmatter values that are exactly a wikilink ("[[X]]"; mid-string and ![[...]] forms are
|
|
57
57
|
// not links there -- Obsidian's rule, probe-verified).
|
|
58
|
+
// One rule for [[inner]] text wherever it appears: alias stripped after |, a leading #
|
|
59
|
+
// keeps the anchor as the target (a same-note self-link needs a heading name), otherwise
|
|
60
|
+
// the anchor splits off. Null = not a link.
|
|
61
|
+
function parseWikilinkInner(inner) {
|
|
62
|
+
var beforeAlias = inner.split('|')[0].trim();
|
|
63
|
+
if (beforeAlias.startsWith('#')) return beforeAlias.length > 1 ? beforeAlias : null;
|
|
64
|
+
var target = beforeAlias.split('#')[0].trim();
|
|
65
|
+
return target || null;
|
|
66
|
+
}
|
|
58
67
|
function extract(_raw, rawBody, _search, data) {
|
|
59
68
|
var body = /\[\[|\]\(/.test(rawBody) ? (0, _fencests.maskRegions)(rawBody) : rawBody;
|
|
60
69
|
var seen = new Set();
|
|
@@ -71,20 +80,12 @@ function extract(_raw, rawBody, _search, data) {
|
|
|
71
80
|
var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
|
|
72
81
|
try {
|
|
73
82
|
// To the first ]], as Obsidian parses it, so a heading or alias holding a lone ] still
|
|
74
|
-
// matches; anchor and alias split off in
|
|
83
|
+
// matches; anchor and alias split off in parseWikilinkInner.
|
|
75
84
|
for(var _iterator = body.matchAll(/\[\[(.*?)\]\]/g)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
|
|
76
85
|
var m = _step.value;
|
|
77
86
|
var _m_index;
|
|
78
|
-
var
|
|
79
|
-
|
|
80
|
-
// target as written (leading #) so resolveTarget can special-case it.
|
|
81
|
-
var beforeAlias = m[1].split('|')[0].trim();
|
|
82
|
-
if (beforeAlias.startsWith('#')) {
|
|
83
|
-
if (beforeAlias.length > 1) add(beforeAlias, embed);
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
var target = beforeAlias.split('#')[0].trim();
|
|
87
|
-
if (target) add(target, embed);
|
|
87
|
+
var target = parseWikilinkInner(m[1]);
|
|
88
|
+
if (target) add(target, body[((_m_index = m.index) !== null && _m_index !== void 0 ? _m_index : 0) - 1] === '!');
|
|
88
89
|
}
|
|
89
90
|
} catch (err) {
|
|
90
91
|
_didIteratorError = true;
|
|
@@ -111,8 +112,10 @@ function extract(_raw, rawBody, _search, data) {
|
|
|
111
112
|
for(var _iterator1 = body.matchAll(/(!)?\[[^\]]*\]\(((?:[^()\s]|\([^()\s]*\))+)(?:\s+(?:"[^)]*"|'[^)]*'))?\)/g)[Symbol.iterator](), _step1; !(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done); _iteratorNormalCompletion1 = true){
|
|
112
113
|
var m1 = _step1.value;
|
|
113
114
|
var dest = m1[2].trim();
|
|
115
|
+
// External: a URI scheme prefix (mailto:, tel:, data:, https:...), protocol-relative
|
|
116
|
+
// //host, a www. shorthand, or a scheme anywhere (the malformed double-paren case).
|
|
114
117
|
if (!dest || dest.includes('://')) continue;
|
|
115
|
-
if (/^www
|
|
118
|
+
if (/^([a-z][a-z0-9+.-]*:|\/\/|www\.)/i.test(dest)) continue;
|
|
116
119
|
var target1 = dest.startsWith('#') ? dest : dest.split('#')[0];
|
|
117
120
|
if (target1) add(target1, m1[1] === '!');
|
|
118
121
|
}
|
|
@@ -143,10 +146,8 @@ function extract(_raw, rawBody, _search, data) {
|
|
|
143
146
|
if (typeof item !== 'string') continue;
|
|
144
147
|
var m2 = /^\[\[(.*)\]\]$/.exec(item.trim());
|
|
145
148
|
if (!m2) continue;
|
|
146
|
-
var
|
|
147
|
-
var target2 = inner.split('#')[0].trim();
|
|
149
|
+
var target2 = parseWikilinkInner(m2[1]);
|
|
148
150
|
if (target2) add(target2, false);
|
|
149
|
-
else if (inner.startsWith('#') && inner.length > 1) add(inner, false);
|
|
150
151
|
}
|
|
151
152
|
} catch (err) {
|
|
152
153
|
_didIteratorError3 = true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/links.ts"],"sourcesContent":["import posix from 'node:path/posix';\nimport type { DatabaseSync } from 'node:sqlite';\nimport { maskRegions } from '../fences.ts';\nimport type { FileStat } from '../scan.ts';\nimport type { Feature, ReconcileDelta } from './types.ts';\n\n// links(src, target, target_base, dst, embed): target as written, target_base its baseKey\n// (indexed, for the incremental resolve below), dst the resolved path or NULL (a\n// queryable dead link), embed whether it's `![[x]]`/`` rather than `[[x]]`/`[](x.md)`\n// -- Obsidian's grain: a target both linked and embedded in the same note is two rows.\n\n// Wikilinks ([[target]], [[target#anchor|alias]], embeds), internal markdown links, and\n// frontmatter values that are exactly a wikilink (\"[[X]]\"; mid-string and ![[...]] forms are\n// not links there -- Obsidian's rule, probe-verified).\nfunction extract(_raw: string, rawBody: string, _search?: { title: string; summary: string }, data?: Record<string, unknown>): Array<{ target: string; embed: boolean }> {\n const body = /\\[\\[|\\]\\(/.test(rawBody) ? maskRegions(rawBody) : rawBody;\n const seen = new Set<string>();\n const results: Array<{ target: string; embed: boolean }> = [];\n const add = (target: string, embed: boolean) => {\n const key = `${target}\\0${embed ? '1' : '0'}`;\n if (seen.has(key)) return;\n seen.add(key);\n results.push({ target, embed });\n };\n // To the first ]], as Obsidian parses it, so a heading or alias holding a lone ] still\n // matches; anchor and alias split off in code, since a class-based regex stops at the ].\n for (const m of body.matchAll(/\\[\\[(.*?)\\]\\]/g)) {\n const embed = body[(m.index ?? 0) - 1] === '!';\n // [[#Heading]]: a same-note anchor link, which Obsidian resolves to a self-edge. Keep the\n // target as written (leading #) so resolveTarget can special-case it.\n const beforeAlias = m[1].split('|')[0].trim();\n if (beforeAlias.startsWith('#')) {\n if (beforeAlias.length > 1) add(beforeAlias, embed);\n continue;\n }\n const target = beforeAlias.split('#')[0].trim();\n if (target) add(target, embed);\n }\n // Any internal destination, not only .md-suffixed: Obsidian gives markdown links full\n // linkpath resolution, so \\](Zektor) resolves like [[Zektor]] and \\](#anchor) is a\n // self-edge. External URLs (a scheme anywhere survives the malformed double-paren case)\n // and titled links (dest stops at whitespace) are skipped.\n // A space in the destination is only valid ahead of a quoted title; a domain-shaped first\n // segment (www.example.com/...) is external even without a scheme.\n for (const m of body.matchAll(/(!)?\\[[^\\]]*\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+(?:\"[^)]*\"|'[^)]*'))?\\)/g)) {\n const dest = m[2].trim();\n if (!dest || dest.includes('://')) continue;\n if (/^www\\./i.test(dest)) continue;\n const target = dest.startsWith('#') ? dest : dest.split('#')[0];\n if (target) add(target, m[1] === '!');\n }\n for (const value of Object.values(data ?? {})) {\n for (const item of Array.isArray(value) ? value : [value]) {\n if (typeof item !== 'string') continue;\n const m = /^\\[\\[(.*)\\]\\]$/.exec(item.trim());\n if (!m) continue;\n const inner = m[1].split('|')[0].trim();\n const target = inner.split('#')[0].trim();\n if (target) add(target, false);\n else if (inner.startsWith('#') && inner.length > 1) add(inner, false);\n }\n }\n return results;\n}\n\nfunction cleanTarget(target: string): string {\n return target.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n}\n\nfunction baseKey(path: string): string {\n return posix.basename(path).replace(/\\.md$/i, '').toLowerCase();\n}\n\n// Obsidian-style: exact relative path (with/without .md), path relative to the linking\n// note's directory, then basename match (shortest path wins on ties -- verified against a\n// cold-loaded Obsidian vault on 6+ real collision pairs; byBase's lists are pre-sorted that way).\nfunction resolveTarget(src: string, target: string, pathSet: Set<string>, byBase: Map<string, string[]>): string | null {\n // [[#Heading]]: a same-note anchor, always the note itself -- never depends on other files.\n if (target.startsWith('#')) return src;\n const clean = cleanTarget(target);\n const fromSrc = posix.normalize(posix.join(posix.dirname(src), clean));\n for (const candidate of [clean, `${clean}.md`, fromSrc, `${fromSrc}.md`]) {\n if (pathSet.has(candidate)) return candidate;\n }\n const candidates = byBase.get(baseKey(clean));\n if (!candidates) return null;\n // The linking note itself wins a basename collision (Obsidian resolves to self before the\n // shortest-path rule -- cold-load verified on the hub corpus).\n return candidates.includes(src) ? src : candidates[0];\n}\n\n// Shortest path wins a basename collision; equal lengths fall back to lexicographic, our own\n// deterministic tiebreak for a case Obsidian itself leaves registration-order-dependent.\nfunction buildByBase(files: FileStat[]): Map<string, string[]> {\n const byBase = new Map<string, string[]>();\n const paths = files.map((f) => f.relPath).sort((a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0));\n for (const path of paths) {\n const key = baseKey(path);\n const list = byBase.get(key);\n if (list) list.push(path);\n else byBase.set(key, [path]);\n }\n return byBase;\n}\n\ninterface LinkRow {\n src: string;\n target: string;\n dst: string | null;\n embed: number;\n}\n\n// Applies resolveTarget to `rows`, writing only rows whose dst actually changes. The UPDATE\n// keys on (src, target): a link/embed sibling pair shares one resolution, so either row's\n// mismatch rewrites both.\nfunction resolveRows(db: DatabaseSync, rows: LinkRow[], pathSet: Set<string>, byBase: Map<string, string[]>): boolean {\n const update = db.prepare('UPDATE links SET dst = ? WHERE src = ? AND target = ?');\n let changed = false;\n for (const row of rows) {\n const dst = resolveTarget(row.src, row.target, pathSet, byBase);\n if (dst !== row.dst) {\n update.run(dst, row.src, row.target);\n changed = true;\n }\n }\n return changed;\n}\n\n// A new or deleted file can change any note's resolution, so re-resolve the whole table.\n// Fallback for cold builds and large deltas -- see afterReconcile's threshold.\nfunction resolveAll(db: DatabaseSync, files: FileStat[]): boolean {\n const pathSet = new Set(files.map((f) => f.relPath));\n const byBase = buildByBase(files);\n const rows = db.prepare('SELECT src, target, dst, embed FROM links').all() as unknown as LinkRow[];\n return resolveRows(db, rows, pathSet, byBase);\n}\n\n// Re-resolve only what this reconcile could have touched: re-stored sources, links whose\n// target basename matches an added/vanished file, and links whose dst just vanished. All\n// three are indexed lookups, so this never reads the whole table.\nfunction resolveIncremental(db: DatabaseSync, delta: ReconcileDelta): boolean {\n const pathSet = new Set(delta.files.map((f) => f.relPath));\n const byBase = buildByBase(delta.files);\n\n const changedBasenames = new Set<string>();\n for (const p of delta.added) changedBasenames.add(baseKey(p));\n for (const p of delta.vanished) changedBasenames.add(baseKey(p));\n\n // \\0 can't appear in a path or target, so the key never collides; a space can.\n const candidates = new Map<string, LinkRow>();\n const collect = (rows: LinkRow[]) => {\n for (const row of rows) candidates.set(`${row.src}\\0${row.target}\\0${row.embed}`, row);\n };\n // Chunked so a large-but-under-threshold delta never exceeds SQLite's bound-variable\n // limit (SQLITE_MAX_VARIABLE_NUMBER, 32766).\n const collectIn = (column: string, keys: string[]) => {\n for (let i = 0; i < keys.length; i += 500) {\n const chunk = keys.slice(i, i + 500);\n const placeholders = chunk.map(() => '?').join(', ');\n collect(db.prepare(`SELECT src, target, dst, embed FROM links WHERE ${column} IN (${placeholders})`).all(...chunk) as unknown as LinkRow[]);\n }\n };\n\n collectIn('src', delta.reparsed);\n collectIn('target_base', [...changedBasenames]);\n collectIn('dst', delta.vanished);\n\n return resolveRows(db, [...candidates.values()], pathSet, byBase);\n}\n\n// Resolved edges, for rank and for search's graph expansion.\nexport function linkEdges(db: DatabaseSync): [string, string][] {\n // One edge per row, as 0.12's grain always had it -- two written targets resolving to one\n // dst stay two edges (a weight the fusion evals were gated on). The only exclusion is an\n // embed whose exact (src, target) also exists as a link: that second row is new with the\n // embed grain and would double an edge that used to be one row. The NOT EXISTS probes the\n // primary key and fires only for embed rows; both branches still scan the table.\n // Self-edges (same-note anchors) are excluded here so PageRank mass is not self-recycled --\n // Obsidian's own graph view hides self-loops too. The rows themselves stay in the table for\n // backlinks/peek.\n const sql = `SELECT src, dst FROM links WHERE dst IS NOT NULL AND embed = 0 AND src != dst\n UNION ALL\n SELECT src, dst FROM links l WHERE dst IS NOT NULL AND embed = 1 AND src != dst\n AND NOT EXISTS (SELECT 1 FROM links l0 WHERE l0.src = l.src AND l0.target = l.target AND l0.embed = 0)`;\n return (db.prepare(sql).all() as Array<{ src: string; dst: string }>).map((r) => [r.src, r.dst]);\n}\n\n// remove() has already deleted the rows by the time afterReconcile runs, so it records here\n// whether they carried edges. Keyed on the delta object, so the state dies with the reconcile.\nconst removedWithLinks = new WeakMap<ReconcileDelta, Set<string>>();\n\nfunction recordRemoved(delta: ReconcileDelta, path: string): void {\n let set = removedWithLinks.get(delta);\n if (!set) {\n set = new Set();\n removedWithLinks.set(delta, set);\n }\n set.add(path);\n}\n\n// remove() runs per path; the vanished list is an array, so cache the Set per delta.\nconst vanishedSets = new WeakMap<ReconcileDelta, Set<string>>();\nfunction vanishedSet(delta: ReconcileDelta): Set<string> {\n let set = vanishedSets.get(delta);\n if (!set) {\n set = new Set(delta.vanished);\n vanishedSets.set(delta, set);\n }\n return set;\n}\n\nexport const links: Feature = {\n name: 'links',\n schema(db) {\n db.exec('CREATE TABLE IF NOT EXISTS links (src TEXT, target TEXT, target_base TEXT, dst TEXT, embed INTEGER, PRIMARY KEY (src, target, embed))');\n db.exec('CREATE INDEX IF NOT EXISTS links_dst ON links(dst)');\n db.exec('CREATE INDEX IF NOT EXISTS links_target_base ON links(target_base)');\n },\n extract,\n // Reparsed docs are diffed in store() below instead of wiped here: deleting and\n // re-inserting resets dst to NULL, which made every touch-only reparse read as an edge\n // change and recompute PageRank -- measured as most of the remaining update cost.\n remove(db, path, delta) {\n if (!vanishedSet(delta).has(path)) return;\n const result = db.prepare('DELETE FROM links WHERE src = ?').run(path);\n if (Number(result.changes) > 0) recordRemoved(delta, path);\n },\n store(db, path, extracted, delta) {\n const targets = extracted as Array<{ target: string; embed: boolean }>;\n // Stale rows (target/embed pairs the new parse no longer contains) are real edge removals.\n // \\0 can't appear in a target, so the composite key never collides.\n let stale: ReturnType<ReturnType<DatabaseSync['prepare']>['run']>;\n if (targets.length === 0) {\n stale = db.prepare('DELETE FROM links WHERE src = ?').run(path);\n } else {\n const placeholders = targets.map(() => '?').join(', ');\n const keys = targets.map((t) => `${t.target}\\0${t.embed ? '1' : '0'}`);\n stale = db.prepare(`DELETE FROM links WHERE src = ? AND (target || char(0) || embed) NOT IN (${placeholders})`).run(path, ...keys);\n }\n if (Number(stale.changes) > 0) recordRemoved(delta, path);\n // Upsert preserves dst on surviving rows, so an unchanged link resolves to the same\n // value and reports no change; only genuinely new rows start at NULL.\n const insert = db.prepare('INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, NULL, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base');\n // A same-note anchor's dst is always its own src, never another file's basename, so it\n // gets no target_base -- SQL's `IN` never matches NULL, keeping it out of\n // resolveIncremental's cross-file collectIn('target_base', ...) entirely.\n for (const { target, embed } of targets) insert.run(path, target, target.startsWith('#') ? null : baseKey(cleanTarget(target)), embed ? 1 : 0);\n },\n afterReconcile(db, delta) {\n // Any deleted rows that carried edges mean the edge set shrank -- whether the file\n // vanished or was reparsed down to fewer/no links. dstChanged alone misses the\n // reparsed-to-zero-links case (no surviving rows to re-resolve).\n const removeHadLinks = (removedWithLinks.get(delta)?.size ?? 0) > 0;\n\n const churn = delta.reparsed.length + delta.vanished.length;\n // Past this share of the tree, a full pass is the only one guaranteed to match\n // resolveTarget's ambiguity rules. A cold build clears the threshold on its own.\n const large = delta.files.length === 0 || churn > 0.2 * delta.files.length;\n\n const dstChanged = large ? resolveAll(db, delta.files) : resolveIncremental(db, delta);\n delta.linksChanged = dstChanged || removeHadLinks;\n },\n};\n"],"names":["linkEdges","links","extract","_raw","rawBody","_search","data","body","test","maskRegions","seen","Set","results","add","target","embed","key","has","push","matchAll","m","index","beforeAlias","split","trim","startsWith","length","dest","includes","Object","values","value","Array","isArray","item","exec","inner","cleanTarget","replace","baseKey","path","posix","basename","toLowerCase","resolveTarget","src","pathSet","byBase","clean","fromSrc","normalize","join","dirname","candidate","candidates","get","buildByBase","files","Map","paths","map","f","relPath","sort","a","b","list","set","resolveRows","db","rows","update","prepare","changed","row","dst","run","resolveAll","all","resolveIncremental","delta","changedBasenames","added","p","vanished","collect","collectIn","column","keys","i","chunk","slice","placeholders","reparsed","sql","r","removedWithLinks","WeakMap","recordRemoved","vanishedSets","vanishedSet","name","schema","remove","result","Number","changes","store","extracted","targets","stale","t","insert","afterReconcile","removeHadLinks","size","churn","large","dstChanged","linksChanged"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/links.ts"],"sourcesContent":["import posix from 'node:path/posix';\nimport type { DatabaseSync } from 'node:sqlite';\nimport { maskRegions } from '../fences.ts';\nimport type { FileStat } from '../scan.ts';\nimport type { Feature, ReconcileDelta } from './types.ts';\n\n// links(src, target, target_base, dst, embed): target as written, target_base its baseKey\n// (indexed, for the incremental resolve below), dst the resolved path or NULL (a\n// queryable dead link), embed whether it's `![[x]]`/`` rather than `[[x]]`/`[](x.md)`\n// -- Obsidian's grain: a target both linked and embedded in the same note is two rows.\n\n// Wikilinks ([[target]], [[target#anchor|alias]], embeds), internal markdown links, and\n// frontmatter values that are exactly a wikilink (\"[[X]]\"; mid-string and ![[...]] forms are\n// not links there -- Obsidian's rule, probe-verified).\n// One rule for [[inner]] text wherever it appears: alias stripped after |, a leading #\n// keeps the anchor as the target (a same-note self-link needs a heading name), otherwise\n// the anchor splits off. Null = not a link.\nfunction parseWikilinkInner(inner: string): string | null {\n const beforeAlias = inner.split('|')[0].trim();\n if (beforeAlias.startsWith('#')) return beforeAlias.length > 1 ? beforeAlias : null;\n const target = beforeAlias.split('#')[0].trim();\n return target || null;\n}\n\nfunction extract(_raw: string, rawBody: string, _search?: { title: string; summary: string }, data?: Record<string, unknown>): Array<{ target: string; embed: boolean }> {\n const body = /\\[\\[|\\]\\(/.test(rawBody) ? maskRegions(rawBody) : rawBody;\n const seen = new Set<string>();\n const results: Array<{ target: string; embed: boolean }> = [];\n const add = (target: string, embed: boolean) => {\n const key = `${target}\\0${embed ? '1' : '0'}`;\n if (seen.has(key)) return;\n seen.add(key);\n results.push({ target, embed });\n };\n // To the first ]], as Obsidian parses it, so a heading or alias holding a lone ] still\n // matches; anchor and alias split off in parseWikilinkInner.\n for (const m of body.matchAll(/\\[\\[(.*?)\\]\\]/g)) {\n const target = parseWikilinkInner(m[1]);\n if (target) add(target, body[(m.index ?? 0) - 1] === '!');\n }\n // Any internal destination, not only .md-suffixed: Obsidian gives markdown links full\n // linkpath resolution, so \\](Zektor) resolves like [[Zektor]] and \\](#anchor) is a\n // self-edge. External URLs (a scheme anywhere survives the malformed double-paren case)\n // and titled links (dest stops at whitespace) are skipped.\n // A space in the destination is only valid ahead of a quoted title; a domain-shaped first\n // segment (www.example.com/...) is external even without a scheme.\n for (const m of body.matchAll(/(!)?\\[[^\\]]*\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+(?:\"[^)]*\"|'[^)]*'))?\\)/g)) {\n const dest = m[2].trim();\n // External: a URI scheme prefix (mailto:, tel:, data:, https:...), protocol-relative\n // //host, a www. shorthand, or a scheme anywhere (the malformed double-paren case).\n if (!dest || dest.includes('://')) continue;\n if (/^([a-z][a-z0-9+.-]*:|\\/\\/|www\\.)/i.test(dest)) continue;\n const target = dest.startsWith('#') ? dest : dest.split('#')[0];\n if (target) add(target, m[1] === '!');\n }\n for (const value of Object.values(data ?? {})) {\n for (const item of Array.isArray(value) ? value : [value]) {\n if (typeof item !== 'string') continue;\n const m = /^\\[\\[(.*)\\]\\]$/.exec(item.trim());\n if (!m) continue;\n const target = parseWikilinkInner(m[1]);\n if (target) add(target, false);\n }\n }\n return results;\n}\n\nfunction cleanTarget(target: string): string {\n return target.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n}\n\nfunction baseKey(path: string): string {\n return posix.basename(path).replace(/\\.md$/i, '').toLowerCase();\n}\n\n// Obsidian-style: exact relative path (with/without .md), path relative to the linking\n// note's directory, then basename match (shortest path wins on ties -- verified against a\n// cold-loaded Obsidian vault on 6+ real collision pairs; byBase's lists are pre-sorted that way).\nfunction resolveTarget(src: string, target: string, pathSet: Set<string>, byBase: Map<string, string[]>): string | null {\n // [[#Heading]]: a same-note anchor, always the note itself -- never depends on other files.\n if (target.startsWith('#')) return src;\n const clean = cleanTarget(target);\n const fromSrc = posix.normalize(posix.join(posix.dirname(src), clean));\n for (const candidate of [clean, `${clean}.md`, fromSrc, `${fromSrc}.md`]) {\n if (pathSet.has(candidate)) return candidate;\n }\n const candidates = byBase.get(baseKey(clean));\n if (!candidates) return null;\n // The linking note itself wins a basename collision (Obsidian resolves to self before the\n // shortest-path rule -- cold-load verified on the hub corpus).\n return candidates.includes(src) ? src : candidates[0];\n}\n\n// Shortest path wins a basename collision; equal lengths fall back to lexicographic, our own\n// deterministic tiebreak for a case Obsidian itself leaves registration-order-dependent.\nfunction buildByBase(files: FileStat[]): Map<string, string[]> {\n const byBase = new Map<string, string[]>();\n const paths = files.map((f) => f.relPath).sort((a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0));\n for (const path of paths) {\n const key = baseKey(path);\n const list = byBase.get(key);\n if (list) list.push(path);\n else byBase.set(key, [path]);\n }\n return byBase;\n}\n\ninterface LinkRow {\n src: string;\n target: string;\n dst: string | null;\n embed: number;\n}\n\n// Applies resolveTarget to `rows`, writing only rows whose dst actually changes. The UPDATE\n// keys on (src, target): a link/embed sibling pair shares one resolution, so either row's\n// mismatch rewrites both.\nfunction resolveRows(db: DatabaseSync, rows: LinkRow[], pathSet: Set<string>, byBase: Map<string, string[]>): boolean {\n const update = db.prepare('UPDATE links SET dst = ? WHERE src = ? AND target = ?');\n let changed = false;\n for (const row of rows) {\n const dst = resolveTarget(row.src, row.target, pathSet, byBase);\n if (dst !== row.dst) {\n update.run(dst, row.src, row.target);\n changed = true;\n }\n }\n return changed;\n}\n\n// A new or deleted file can change any note's resolution, so re-resolve the whole table.\n// Fallback for cold builds and large deltas -- see afterReconcile's threshold.\nfunction resolveAll(db: DatabaseSync, files: FileStat[]): boolean {\n const pathSet = new Set(files.map((f) => f.relPath));\n const byBase = buildByBase(files);\n const rows = db.prepare('SELECT src, target, dst, embed FROM links').all() as unknown as LinkRow[];\n return resolveRows(db, rows, pathSet, byBase);\n}\n\n// Re-resolve only what this reconcile could have touched: re-stored sources, links whose\n// target basename matches an added/vanished file, and links whose dst just vanished. All\n// three are indexed lookups, so this never reads the whole table.\nfunction resolveIncremental(db: DatabaseSync, delta: ReconcileDelta): boolean {\n const pathSet = new Set(delta.files.map((f) => f.relPath));\n const byBase = buildByBase(delta.files);\n\n const changedBasenames = new Set<string>();\n for (const p of delta.added) changedBasenames.add(baseKey(p));\n for (const p of delta.vanished) changedBasenames.add(baseKey(p));\n\n // \\0 can't appear in a path or target, so the key never collides; a space can.\n const candidates = new Map<string, LinkRow>();\n const collect = (rows: LinkRow[]) => {\n for (const row of rows) candidates.set(`${row.src}\\0${row.target}\\0${row.embed}`, row);\n };\n // Chunked so a large-but-under-threshold delta never exceeds SQLite's bound-variable\n // limit (SQLITE_MAX_VARIABLE_NUMBER, 32766).\n const collectIn = (column: string, keys: string[]) => {\n for (let i = 0; i < keys.length; i += 500) {\n const chunk = keys.slice(i, i + 500);\n const placeholders = chunk.map(() => '?').join(', ');\n collect(db.prepare(`SELECT src, target, dst, embed FROM links WHERE ${column} IN (${placeholders})`).all(...chunk) as unknown as LinkRow[]);\n }\n };\n\n collectIn('src', delta.reparsed);\n collectIn('target_base', [...changedBasenames]);\n collectIn('dst', delta.vanished);\n\n return resolveRows(db, [...candidates.values()], pathSet, byBase);\n}\n\n// Resolved edges, for rank and for search's graph expansion.\nexport function linkEdges(db: DatabaseSync): [string, string][] {\n // One edge per row, as 0.12's grain always had it -- two written targets resolving to one\n // dst stay two edges (a weight the fusion evals were gated on). The only exclusion is an\n // embed whose exact (src, target) also exists as a link: that second row is new with the\n // embed grain and would double an edge that used to be one row. The NOT EXISTS probes the\n // primary key and fires only for embed rows; both branches still scan the table.\n // Self-edges (same-note anchors) are excluded here so PageRank mass is not self-recycled --\n // Obsidian's own graph view hides self-loops too. The rows themselves stay in the table for\n // backlinks/peek.\n const sql = `SELECT src, dst FROM links WHERE dst IS NOT NULL AND embed = 0 AND src != dst\n UNION ALL\n SELECT src, dst FROM links l WHERE dst IS NOT NULL AND embed = 1 AND src != dst\n AND NOT EXISTS (SELECT 1 FROM links l0 WHERE l0.src = l.src AND l0.target = l.target AND l0.embed = 0)`;\n return (db.prepare(sql).all() as Array<{ src: string; dst: string }>).map((r) => [r.src, r.dst]);\n}\n\n// remove() has already deleted the rows by the time afterReconcile runs, so it records here\n// whether they carried edges. Keyed on the delta object, so the state dies with the reconcile.\nconst removedWithLinks = new WeakMap<ReconcileDelta, Set<string>>();\n\nfunction recordRemoved(delta: ReconcileDelta, path: string): void {\n let set = removedWithLinks.get(delta);\n if (!set) {\n set = new Set();\n removedWithLinks.set(delta, set);\n }\n set.add(path);\n}\n\n// remove() runs per path; the vanished list is an array, so cache the Set per delta.\nconst vanishedSets = new WeakMap<ReconcileDelta, Set<string>>();\nfunction vanishedSet(delta: ReconcileDelta): Set<string> {\n let set = vanishedSets.get(delta);\n if (!set) {\n set = new Set(delta.vanished);\n vanishedSets.set(delta, set);\n }\n return set;\n}\n\nexport const links: Feature = {\n name: 'links',\n schema(db) {\n db.exec('CREATE TABLE IF NOT EXISTS links (src TEXT, target TEXT, target_base TEXT, dst TEXT, embed INTEGER, PRIMARY KEY (src, target, embed))');\n db.exec('CREATE INDEX IF NOT EXISTS links_dst ON links(dst)');\n db.exec('CREATE INDEX IF NOT EXISTS links_target_base ON links(target_base)');\n },\n extract,\n // Reparsed docs are diffed in store() below instead of wiped here: deleting and\n // re-inserting resets dst to NULL, which made every touch-only reparse read as an edge\n // change and recompute PageRank -- measured as most of the remaining update cost.\n remove(db, path, delta) {\n if (!vanishedSet(delta).has(path)) return;\n const result = db.prepare('DELETE FROM links WHERE src = ?').run(path);\n if (Number(result.changes) > 0) recordRemoved(delta, path);\n },\n store(db, path, extracted, delta) {\n const targets = extracted as Array<{ target: string; embed: boolean }>;\n // Stale rows (target/embed pairs the new parse no longer contains) are real edge removals.\n // \\0 can't appear in a target, so the composite key never collides.\n let stale: ReturnType<ReturnType<DatabaseSync['prepare']>['run']>;\n if (targets.length === 0) {\n stale = db.prepare('DELETE FROM links WHERE src = ?').run(path);\n } else {\n const placeholders = targets.map(() => '?').join(', ');\n const keys = targets.map((t) => `${t.target}\\0${t.embed ? '1' : '0'}`);\n stale = db.prepare(`DELETE FROM links WHERE src = ? AND (target || char(0) || embed) NOT IN (${placeholders})`).run(path, ...keys);\n }\n if (Number(stale.changes) > 0) recordRemoved(delta, path);\n // Upsert preserves dst on surviving rows, so an unchanged link resolves to the same\n // value and reports no change; only genuinely new rows start at NULL.\n const insert = db.prepare('INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, NULL, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base');\n // A same-note anchor's dst is always its own src, never another file's basename, so it\n // gets no target_base -- SQL's `IN` never matches NULL, keeping it out of\n // resolveIncremental's cross-file collectIn('target_base', ...) entirely.\n for (const { target, embed } of targets) insert.run(path, target, target.startsWith('#') ? null : baseKey(cleanTarget(target)), embed ? 1 : 0);\n },\n afterReconcile(db, delta) {\n // Any deleted rows that carried edges mean the edge set shrank -- whether the file\n // vanished or was reparsed down to fewer/no links. dstChanged alone misses the\n // reparsed-to-zero-links case (no surviving rows to re-resolve).\n const removeHadLinks = (removedWithLinks.get(delta)?.size ?? 0) > 0;\n\n const churn = delta.reparsed.length + delta.vanished.length;\n // Past this share of the tree, a full pass is the only one guaranteed to match\n // resolveTarget's ambiguity rules. A cold build clears the threshold on its own.\n const large = delta.files.length === 0 || churn > 0.2 * delta.files.length;\n\n const dstChanged = large ? resolveAll(db, delta.files) : resolveIncremental(db, delta);\n delta.linksChanged = dstChanged || removeHadLinks;\n },\n};\n"],"names":["linkEdges","links","parseWikilinkInner","inner","beforeAlias","split","trim","startsWith","length","target","extract","_raw","rawBody","_search","data","body","test","maskRegions","seen","Set","results","add","embed","key","has","push","matchAll","m","index","dest","includes","Object","values","value","Array","isArray","item","exec","cleanTarget","replace","baseKey","path","posix","basename","toLowerCase","resolveTarget","src","pathSet","byBase","clean","fromSrc","normalize","join","dirname","candidate","candidates","get","buildByBase","files","Map","paths","map","f","relPath","sort","a","b","list","set","resolveRows","db","rows","update","prepare","changed","row","dst","run","resolveAll","all","resolveIncremental","delta","changedBasenames","added","p","vanished","collect","collectIn","column","keys","i","chunk","slice","placeholders","reparsed","sql","r","removedWithLinks","WeakMap","recordRemoved","vanishedSets","vanishedSet","name","schema","remove","result","Number","changes","store","extracted","targets","stale","t","insert","afterReconcile","removeHadLinks","size","churn","large","dstChanged","linksChanged"],"mappings":"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"}
|
|
@@ -7,6 +7,15 @@ import { maskRegions } from '../fences.js';
|
|
|
7
7
|
// Wikilinks ([[target]], [[target#anchor|alias]], embeds), internal markdown links, and
|
|
8
8
|
// frontmatter values that are exactly a wikilink ("[[X]]"; mid-string and ![[...]] forms are
|
|
9
9
|
// not links there -- Obsidian's rule, probe-verified).
|
|
10
|
+
// One rule for [[inner]] text wherever it appears: alias stripped after |, a leading #
|
|
11
|
+
// keeps the anchor as the target (a same-note self-link needs a heading name), otherwise
|
|
12
|
+
// the anchor splits off. Null = not a link.
|
|
13
|
+
function parseWikilinkInner(inner) {
|
|
14
|
+
const beforeAlias = inner.split('|')[0].trim();
|
|
15
|
+
if (beforeAlias.startsWith('#')) return beforeAlias.length > 1 ? beforeAlias : null;
|
|
16
|
+
const target = beforeAlias.split('#')[0].trim();
|
|
17
|
+
return target || null;
|
|
18
|
+
}
|
|
10
19
|
function extract(_raw, rawBody, _search, data) {
|
|
11
20
|
const body = /\[\[|\]\(/.test(rawBody) ? maskRegions(rawBody) : rawBody;
|
|
12
21
|
const seen = new Set();
|
|
@@ -21,19 +30,11 @@ function extract(_raw, rawBody, _search, data) {
|
|
|
21
30
|
});
|
|
22
31
|
};
|
|
23
32
|
// To the first ]], as Obsidian parses it, so a heading or alias holding a lone ] still
|
|
24
|
-
// matches; anchor and alias split off in
|
|
33
|
+
// matches; anchor and alias split off in parseWikilinkInner.
|
|
25
34
|
for (const m of body.matchAll(/\[\[(.*?)\]\]/g)){
|
|
26
35
|
var _m_index;
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
// target as written (leading #) so resolveTarget can special-case it.
|
|
30
|
-
const beforeAlias = m[1].split('|')[0].trim();
|
|
31
|
-
if (beforeAlias.startsWith('#')) {
|
|
32
|
-
if (beforeAlias.length > 1) add(beforeAlias, embed);
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const target = beforeAlias.split('#')[0].trim();
|
|
36
|
-
if (target) add(target, embed);
|
|
36
|
+
const target = parseWikilinkInner(m[1]);
|
|
37
|
+
if (target) add(target, body[((_m_index = m.index) !== null && _m_index !== void 0 ? _m_index : 0) - 1] === '!');
|
|
37
38
|
}
|
|
38
39
|
// Any internal destination, not only .md-suffixed: Obsidian gives markdown links full
|
|
39
40
|
// linkpath resolution, so \](Zektor) resolves like [[Zektor]] and \](#anchor) is a
|
|
@@ -43,8 +44,10 @@ function extract(_raw, rawBody, _search, data) {
|
|
|
43
44
|
// segment (www.example.com/...) is external even without a scheme.
|
|
44
45
|
for (const m of body.matchAll(/(!)?\[[^\]]*\]\(((?:[^()\s]|\([^()\s]*\))+)(?:\s+(?:"[^)]*"|'[^)]*'))?\)/g)){
|
|
45
46
|
const dest = m[2].trim();
|
|
47
|
+
// External: a URI scheme prefix (mailto:, tel:, data:, https:...), protocol-relative
|
|
48
|
+
// //host, a www. shorthand, or a scheme anywhere (the malformed double-paren case).
|
|
46
49
|
if (!dest || dest.includes('://')) continue;
|
|
47
|
-
if (/^www
|
|
50
|
+
if (/^([a-z][a-z0-9+.-]*:|\/\/|www\.)/i.test(dest)) continue;
|
|
48
51
|
const target = dest.startsWith('#') ? dest : dest.split('#')[0];
|
|
49
52
|
if (target) add(target, m[1] === '!');
|
|
50
53
|
}
|
|
@@ -55,10 +58,8 @@ function extract(_raw, rawBody, _search, data) {
|
|
|
55
58
|
if (typeof item !== 'string') continue;
|
|
56
59
|
const m = /^\[\[(.*)\]\]$/.exec(item.trim());
|
|
57
60
|
if (!m) continue;
|
|
58
|
-
const
|
|
59
|
-
const target = inner.split('#')[0].trim();
|
|
61
|
+
const target = parseWikilinkInner(m[1]);
|
|
60
62
|
if (target) add(target, false);
|
|
61
|
-
else if (inner.startsWith('#') && inner.length > 1) add(inner, false);
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
return results;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/links.ts"],"sourcesContent":["import posix from 'node:path/posix';\nimport type { DatabaseSync } from 'node:sqlite';\nimport { maskRegions } from '../fences.ts';\nimport type { FileStat } from '../scan.ts';\nimport type { Feature, ReconcileDelta } from './types.ts';\n\n// links(src, target, target_base, dst, embed): target as written, target_base its baseKey\n// (indexed, for the incremental resolve below), dst the resolved path or NULL (a\n// queryable dead link), embed whether it's `![[x]]`/`` rather than `[[x]]`/`[](x.md)`\n// -- Obsidian's grain: a target both linked and embedded in the same note is two rows.\n\n// Wikilinks ([[target]], [[target#anchor|alias]], embeds), internal markdown links, and\n// frontmatter values that are exactly a wikilink (\"[[X]]\"; mid-string and ![[...]] forms are\n// not links there -- Obsidian's rule, probe-verified).\nfunction extract(_raw: string, rawBody: string, _search?: { title: string; summary: string }, data?: Record<string, unknown>): Array<{ target: string; embed: boolean }> {\n const body = /\\[\\[|\\]\\(/.test(rawBody) ? maskRegions(rawBody) : rawBody;\n const seen = new Set<string>();\n const results: Array<{ target: string; embed: boolean }> = [];\n const add = (target: string, embed: boolean) => {\n const key = `${target}\\0${embed ? '1' : '0'}`;\n if (seen.has(key)) return;\n seen.add(key);\n results.push({ target, embed });\n };\n // To the first ]], as Obsidian parses it, so a heading or alias holding a lone ] still\n // matches; anchor and alias split off in code, since a class-based regex stops at the ].\n for (const m of body.matchAll(/\\[\\[(.*?)\\]\\]/g)) {\n const embed = body[(m.index ?? 0) - 1] === '!';\n // [[#Heading]]: a same-note anchor link, which Obsidian resolves to a self-edge. Keep the\n // target as written (leading #) so resolveTarget can special-case it.\n const beforeAlias = m[1].split('|')[0].trim();\n if (beforeAlias.startsWith('#')) {\n if (beforeAlias.length > 1) add(beforeAlias, embed);\n continue;\n }\n const target = beforeAlias.split('#')[0].trim();\n if (target) add(target, embed);\n }\n // Any internal destination, not only .md-suffixed: Obsidian gives markdown links full\n // linkpath resolution, so \\](Zektor) resolves like [[Zektor]] and \\](#anchor) is a\n // self-edge. External URLs (a scheme anywhere survives the malformed double-paren case)\n // and titled links (dest stops at whitespace) are skipped.\n // A space in the destination is only valid ahead of a quoted title; a domain-shaped first\n // segment (www.example.com/...) is external even without a scheme.\n for (const m of body.matchAll(/(!)?\\[[^\\]]*\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+(?:\"[^)]*\"|'[^)]*'))?\\)/g)) {\n const dest = m[2].trim();\n if (!dest || dest.includes('://')) continue;\n if (/^www\\./i.test(dest)) continue;\n const target = dest.startsWith('#') ? dest : dest.split('#')[0];\n if (target) add(target, m[1] === '!');\n }\n for (const value of Object.values(data ?? {})) {\n for (const item of Array.isArray(value) ? value : [value]) {\n if (typeof item !== 'string') continue;\n const m = /^\\[\\[(.*)\\]\\]$/.exec(item.trim());\n if (!m) continue;\n const inner = m[1].split('|')[0].trim();\n const target = inner.split('#')[0].trim();\n if (target) add(target, false);\n else if (inner.startsWith('#') && inner.length > 1) add(inner, false);\n }\n }\n return results;\n}\n\nfunction cleanTarget(target: string): string {\n return target.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n}\n\nfunction baseKey(path: string): string {\n return posix.basename(path).replace(/\\.md$/i, '').toLowerCase();\n}\n\n// Obsidian-style: exact relative path (with/without .md), path relative to the linking\n// note's directory, then basename match (shortest path wins on ties -- verified against a\n// cold-loaded Obsidian vault on 6+ real collision pairs; byBase's lists are pre-sorted that way).\nfunction resolveTarget(src: string, target: string, pathSet: Set<string>, byBase: Map<string, string[]>): string | null {\n // [[#Heading]]: a same-note anchor, always the note itself -- never depends on other files.\n if (target.startsWith('#')) return src;\n const clean = cleanTarget(target);\n const fromSrc = posix.normalize(posix.join(posix.dirname(src), clean));\n for (const candidate of [clean, `${clean}.md`, fromSrc, `${fromSrc}.md`]) {\n if (pathSet.has(candidate)) return candidate;\n }\n const candidates = byBase.get(baseKey(clean));\n if (!candidates) return null;\n // The linking note itself wins a basename collision (Obsidian resolves to self before the\n // shortest-path rule -- cold-load verified on the hub corpus).\n return candidates.includes(src) ? src : candidates[0];\n}\n\n// Shortest path wins a basename collision; equal lengths fall back to lexicographic, our own\n// deterministic tiebreak for a case Obsidian itself leaves registration-order-dependent.\nfunction buildByBase(files: FileStat[]): Map<string, string[]> {\n const byBase = new Map<string, string[]>();\n const paths = files.map((f) => f.relPath).sort((a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0));\n for (const path of paths) {\n const key = baseKey(path);\n const list = byBase.get(key);\n if (list) list.push(path);\n else byBase.set(key, [path]);\n }\n return byBase;\n}\n\ninterface LinkRow {\n src: string;\n target: string;\n dst: string | null;\n embed: number;\n}\n\n// Applies resolveTarget to `rows`, writing only rows whose dst actually changes. The UPDATE\n// keys on (src, target): a link/embed sibling pair shares one resolution, so either row's\n// mismatch rewrites both.\nfunction resolveRows(db: DatabaseSync, rows: LinkRow[], pathSet: Set<string>, byBase: Map<string, string[]>): boolean {\n const update = db.prepare('UPDATE links SET dst = ? WHERE src = ? AND target = ?');\n let changed = false;\n for (const row of rows) {\n const dst = resolveTarget(row.src, row.target, pathSet, byBase);\n if (dst !== row.dst) {\n update.run(dst, row.src, row.target);\n changed = true;\n }\n }\n return changed;\n}\n\n// A new or deleted file can change any note's resolution, so re-resolve the whole table.\n// Fallback for cold builds and large deltas -- see afterReconcile's threshold.\nfunction resolveAll(db: DatabaseSync, files: FileStat[]): boolean {\n const pathSet = new Set(files.map((f) => f.relPath));\n const byBase = buildByBase(files);\n const rows = db.prepare('SELECT src, target, dst, embed FROM links').all() as unknown as LinkRow[];\n return resolveRows(db, rows, pathSet, byBase);\n}\n\n// Re-resolve only what this reconcile could have touched: re-stored sources, links whose\n// target basename matches an added/vanished file, and links whose dst just vanished. All\n// three are indexed lookups, so this never reads the whole table.\nfunction resolveIncremental(db: DatabaseSync, delta: ReconcileDelta): boolean {\n const pathSet = new Set(delta.files.map((f) => f.relPath));\n const byBase = buildByBase(delta.files);\n\n const changedBasenames = new Set<string>();\n for (const p of delta.added) changedBasenames.add(baseKey(p));\n for (const p of delta.vanished) changedBasenames.add(baseKey(p));\n\n // \\0 can't appear in a path or target, so the key never collides; a space can.\n const candidates = new Map<string, LinkRow>();\n const collect = (rows: LinkRow[]) => {\n for (const row of rows) candidates.set(`${row.src}\\0${row.target}\\0${row.embed}`, row);\n };\n // Chunked so a large-but-under-threshold delta never exceeds SQLite's bound-variable\n // limit (SQLITE_MAX_VARIABLE_NUMBER, 32766).\n const collectIn = (column: string, keys: string[]) => {\n for (let i = 0; i < keys.length; i += 500) {\n const chunk = keys.slice(i, i + 500);\n const placeholders = chunk.map(() => '?').join(', ');\n collect(db.prepare(`SELECT src, target, dst, embed FROM links WHERE ${column} IN (${placeholders})`).all(...chunk) as unknown as LinkRow[]);\n }\n };\n\n collectIn('src', delta.reparsed);\n collectIn('target_base', [...changedBasenames]);\n collectIn('dst', delta.vanished);\n\n return resolveRows(db, [...candidates.values()], pathSet, byBase);\n}\n\n// Resolved edges, for rank and for search's graph expansion.\nexport function linkEdges(db: DatabaseSync): [string, string][] {\n // One edge per row, as 0.12's grain always had it -- two written targets resolving to one\n // dst stay two edges (a weight the fusion evals were gated on). The only exclusion is an\n // embed whose exact (src, target) also exists as a link: that second row is new with the\n // embed grain and would double an edge that used to be one row. The NOT EXISTS probes the\n // primary key and fires only for embed rows; both branches still scan the table.\n // Self-edges (same-note anchors) are excluded here so PageRank mass is not self-recycled --\n // Obsidian's own graph view hides self-loops too. The rows themselves stay in the table for\n // backlinks/peek.\n const sql = `SELECT src, dst FROM links WHERE dst IS NOT NULL AND embed = 0 AND src != dst\n UNION ALL\n SELECT src, dst FROM links l WHERE dst IS NOT NULL AND embed = 1 AND src != dst\n AND NOT EXISTS (SELECT 1 FROM links l0 WHERE l0.src = l.src AND l0.target = l.target AND l0.embed = 0)`;\n return (db.prepare(sql).all() as Array<{ src: string; dst: string }>).map((r) => [r.src, r.dst]);\n}\n\n// remove() has already deleted the rows by the time afterReconcile runs, so it records here\n// whether they carried edges. Keyed on the delta object, so the state dies with the reconcile.\nconst removedWithLinks = new WeakMap<ReconcileDelta, Set<string>>();\n\nfunction recordRemoved(delta: ReconcileDelta, path: string): void {\n let set = removedWithLinks.get(delta);\n if (!set) {\n set = new Set();\n removedWithLinks.set(delta, set);\n }\n set.add(path);\n}\n\n// remove() runs per path; the vanished list is an array, so cache the Set per delta.\nconst vanishedSets = new WeakMap<ReconcileDelta, Set<string>>();\nfunction vanishedSet(delta: ReconcileDelta): Set<string> {\n let set = vanishedSets.get(delta);\n if (!set) {\n set = new Set(delta.vanished);\n vanishedSets.set(delta, set);\n }\n return set;\n}\n\nexport const links: Feature = {\n name: 'links',\n schema(db) {\n db.exec('CREATE TABLE IF NOT EXISTS links (src TEXT, target TEXT, target_base TEXT, dst TEXT, embed INTEGER, PRIMARY KEY (src, target, embed))');\n db.exec('CREATE INDEX IF NOT EXISTS links_dst ON links(dst)');\n db.exec('CREATE INDEX IF NOT EXISTS links_target_base ON links(target_base)');\n },\n extract,\n // Reparsed docs are diffed in store() below instead of wiped here: deleting and\n // re-inserting resets dst to NULL, which made every touch-only reparse read as an edge\n // change and recompute PageRank -- measured as most of the remaining update cost.\n remove(db, path, delta) {\n if (!vanishedSet(delta).has(path)) return;\n const result = db.prepare('DELETE FROM links WHERE src = ?').run(path);\n if (Number(result.changes) > 0) recordRemoved(delta, path);\n },\n store(db, path, extracted, delta) {\n const targets = extracted as Array<{ target: string; embed: boolean }>;\n // Stale rows (target/embed pairs the new parse no longer contains) are real edge removals.\n // \\0 can't appear in a target, so the composite key never collides.\n let stale: ReturnType<ReturnType<DatabaseSync['prepare']>['run']>;\n if (targets.length === 0) {\n stale = db.prepare('DELETE FROM links WHERE src = ?').run(path);\n } else {\n const placeholders = targets.map(() => '?').join(', ');\n const keys = targets.map((t) => `${t.target}\\0${t.embed ? '1' : '0'}`);\n stale = db.prepare(`DELETE FROM links WHERE src = ? AND (target || char(0) || embed) NOT IN (${placeholders})`).run(path, ...keys);\n }\n if (Number(stale.changes) > 0) recordRemoved(delta, path);\n // Upsert preserves dst on surviving rows, so an unchanged link resolves to the same\n // value and reports no change; only genuinely new rows start at NULL.\n const insert = db.prepare('INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, NULL, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base');\n // A same-note anchor's dst is always its own src, never another file's basename, so it\n // gets no target_base -- SQL's `IN` never matches NULL, keeping it out of\n // resolveIncremental's cross-file collectIn('target_base', ...) entirely.\n for (const { target, embed } of targets) insert.run(path, target, target.startsWith('#') ? null : baseKey(cleanTarget(target)), embed ? 1 : 0);\n },\n afterReconcile(db, delta) {\n // Any deleted rows that carried edges mean the edge set shrank -- whether the file\n // vanished or was reparsed down to fewer/no links. dstChanged alone misses the\n // reparsed-to-zero-links case (no surviving rows to re-resolve).\n const removeHadLinks = (removedWithLinks.get(delta)?.size ?? 0) > 0;\n\n const churn = delta.reparsed.length + delta.vanished.length;\n // Past this share of the tree, a full pass is the only one guaranteed to match\n // resolveTarget's ambiguity rules. A cold build clears the threshold on its own.\n const large = delta.files.length === 0 || churn > 0.2 * delta.files.length;\n\n const dstChanged = large ? resolveAll(db, delta.files) : resolveIncremental(db, delta);\n delta.linksChanged = dstChanged || removeHadLinks;\n },\n};\n"],"names":["posix","maskRegions","extract","_raw","rawBody","_search","data","body","test","seen","Set","results","add","target","embed","key","has","push","m","matchAll","index","beforeAlias","split","trim","startsWith","length","dest","includes","value","Object","values","item","Array","isArray","exec","inner","cleanTarget","replace","baseKey","path","basename","toLowerCase","resolveTarget","src","pathSet","byBase","clean","fromSrc","normalize","join","dirname","candidate","candidates","get","buildByBase","files","Map","paths","map","f","relPath","sort","a","b","list","set","resolveRows","db","rows","update","prepare","changed","row","dst","run","resolveAll","all","resolveIncremental","delta","changedBasenames","p","added","vanished","collect","collectIn","column","keys","i","chunk","slice","placeholders","reparsed","linkEdges","sql","r","removedWithLinks","WeakMap","recordRemoved","vanishedSets","vanishedSet","links","name","schema","remove","result","Number","changes","store","extracted","targets","stale","t","insert","afterReconcile","removeHadLinks","size","churn","large","dstChanged","linksChanged"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/features/links.ts"],"sourcesContent":["import posix from 'node:path/posix';\nimport type { DatabaseSync } from 'node:sqlite';\nimport { maskRegions } from '../fences.ts';\nimport type { FileStat } from '../scan.ts';\nimport type { Feature, ReconcileDelta } from './types.ts';\n\n// links(src, target, target_base, dst, embed): target as written, target_base its baseKey\n// (indexed, for the incremental resolve below), dst the resolved path or NULL (a\n// queryable dead link), embed whether it's `![[x]]`/`` rather than `[[x]]`/`[](x.md)`\n// -- Obsidian's grain: a target both linked and embedded in the same note is two rows.\n\n// Wikilinks ([[target]], [[target#anchor|alias]], embeds), internal markdown links, and\n// frontmatter values that are exactly a wikilink (\"[[X]]\"; mid-string and ![[...]] forms are\n// not links there -- Obsidian's rule, probe-verified).\n// One rule for [[inner]] text wherever it appears: alias stripped after |, a leading #\n// keeps the anchor as the target (a same-note self-link needs a heading name), otherwise\n// the anchor splits off. Null = not a link.\nfunction parseWikilinkInner(inner: string): string | null {\n const beforeAlias = inner.split('|')[0].trim();\n if (beforeAlias.startsWith('#')) return beforeAlias.length > 1 ? beforeAlias : null;\n const target = beforeAlias.split('#')[0].trim();\n return target || null;\n}\n\nfunction extract(_raw: string, rawBody: string, _search?: { title: string; summary: string }, data?: Record<string, unknown>): Array<{ target: string; embed: boolean }> {\n const body = /\\[\\[|\\]\\(/.test(rawBody) ? maskRegions(rawBody) : rawBody;\n const seen = new Set<string>();\n const results: Array<{ target: string; embed: boolean }> = [];\n const add = (target: string, embed: boolean) => {\n const key = `${target}\\0${embed ? '1' : '0'}`;\n if (seen.has(key)) return;\n seen.add(key);\n results.push({ target, embed });\n };\n // To the first ]], as Obsidian parses it, so a heading or alias holding a lone ] still\n // matches; anchor and alias split off in parseWikilinkInner.\n for (const m of body.matchAll(/\\[\\[(.*?)\\]\\]/g)) {\n const target = parseWikilinkInner(m[1]);\n if (target) add(target, body[(m.index ?? 0) - 1] === '!');\n }\n // Any internal destination, not only .md-suffixed: Obsidian gives markdown links full\n // linkpath resolution, so \\](Zektor) resolves like [[Zektor]] and \\](#anchor) is a\n // self-edge. External URLs (a scheme anywhere survives the malformed double-paren case)\n // and titled links (dest stops at whitespace) are skipped.\n // A space in the destination is only valid ahead of a quoted title; a domain-shaped first\n // segment (www.example.com/...) is external even without a scheme.\n for (const m of body.matchAll(/(!)?\\[[^\\]]*\\]\\(((?:[^()\\s]|\\([^()\\s]*\\))+)(?:\\s+(?:\"[^)]*\"|'[^)]*'))?\\)/g)) {\n const dest = m[2].trim();\n // External: a URI scheme prefix (mailto:, tel:, data:, https:...), protocol-relative\n // //host, a www. shorthand, or a scheme anywhere (the malformed double-paren case).\n if (!dest || dest.includes('://')) continue;\n if (/^([a-z][a-z0-9+.-]*:|\\/\\/|www\\.)/i.test(dest)) continue;\n const target = dest.startsWith('#') ? dest : dest.split('#')[0];\n if (target) add(target, m[1] === '!');\n }\n for (const value of Object.values(data ?? {})) {\n for (const item of Array.isArray(value) ? value : [value]) {\n if (typeof item !== 'string') continue;\n const m = /^\\[\\[(.*)\\]\\]$/.exec(item.trim());\n if (!m) continue;\n const target = parseWikilinkInner(m[1]);\n if (target) add(target, false);\n }\n }\n return results;\n}\n\nfunction cleanTarget(target: string): string {\n return target.replace(/\\\\/g, '/').replace(/^\\.\\//, '');\n}\n\nfunction baseKey(path: string): string {\n return posix.basename(path).replace(/\\.md$/i, '').toLowerCase();\n}\n\n// Obsidian-style: exact relative path (with/without .md), path relative to the linking\n// note's directory, then basename match (shortest path wins on ties -- verified against a\n// cold-loaded Obsidian vault on 6+ real collision pairs; byBase's lists are pre-sorted that way).\nfunction resolveTarget(src: string, target: string, pathSet: Set<string>, byBase: Map<string, string[]>): string | null {\n // [[#Heading]]: a same-note anchor, always the note itself -- never depends on other files.\n if (target.startsWith('#')) return src;\n const clean = cleanTarget(target);\n const fromSrc = posix.normalize(posix.join(posix.dirname(src), clean));\n for (const candidate of [clean, `${clean}.md`, fromSrc, `${fromSrc}.md`]) {\n if (pathSet.has(candidate)) return candidate;\n }\n const candidates = byBase.get(baseKey(clean));\n if (!candidates) return null;\n // The linking note itself wins a basename collision (Obsidian resolves to self before the\n // shortest-path rule -- cold-load verified on the hub corpus).\n return candidates.includes(src) ? src : candidates[0];\n}\n\n// Shortest path wins a basename collision; equal lengths fall back to lexicographic, our own\n// deterministic tiebreak for a case Obsidian itself leaves registration-order-dependent.\nfunction buildByBase(files: FileStat[]): Map<string, string[]> {\n const byBase = new Map<string, string[]>();\n const paths = files.map((f) => f.relPath).sort((a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0));\n for (const path of paths) {\n const key = baseKey(path);\n const list = byBase.get(key);\n if (list) list.push(path);\n else byBase.set(key, [path]);\n }\n return byBase;\n}\n\ninterface LinkRow {\n src: string;\n target: string;\n dst: string | null;\n embed: number;\n}\n\n// Applies resolveTarget to `rows`, writing only rows whose dst actually changes. The UPDATE\n// keys on (src, target): a link/embed sibling pair shares one resolution, so either row's\n// mismatch rewrites both.\nfunction resolveRows(db: DatabaseSync, rows: LinkRow[], pathSet: Set<string>, byBase: Map<string, string[]>): boolean {\n const update = db.prepare('UPDATE links SET dst = ? WHERE src = ? AND target = ?');\n let changed = false;\n for (const row of rows) {\n const dst = resolveTarget(row.src, row.target, pathSet, byBase);\n if (dst !== row.dst) {\n update.run(dst, row.src, row.target);\n changed = true;\n }\n }\n return changed;\n}\n\n// A new or deleted file can change any note's resolution, so re-resolve the whole table.\n// Fallback for cold builds and large deltas -- see afterReconcile's threshold.\nfunction resolveAll(db: DatabaseSync, files: FileStat[]): boolean {\n const pathSet = new Set(files.map((f) => f.relPath));\n const byBase = buildByBase(files);\n const rows = db.prepare('SELECT src, target, dst, embed FROM links').all() as unknown as LinkRow[];\n return resolveRows(db, rows, pathSet, byBase);\n}\n\n// Re-resolve only what this reconcile could have touched: re-stored sources, links whose\n// target basename matches an added/vanished file, and links whose dst just vanished. All\n// three are indexed lookups, so this never reads the whole table.\nfunction resolveIncremental(db: DatabaseSync, delta: ReconcileDelta): boolean {\n const pathSet = new Set(delta.files.map((f) => f.relPath));\n const byBase = buildByBase(delta.files);\n\n const changedBasenames = new Set<string>();\n for (const p of delta.added) changedBasenames.add(baseKey(p));\n for (const p of delta.vanished) changedBasenames.add(baseKey(p));\n\n // \\0 can't appear in a path or target, so the key never collides; a space can.\n const candidates = new Map<string, LinkRow>();\n const collect = (rows: LinkRow[]) => {\n for (const row of rows) candidates.set(`${row.src}\\0${row.target}\\0${row.embed}`, row);\n };\n // Chunked so a large-but-under-threshold delta never exceeds SQLite's bound-variable\n // limit (SQLITE_MAX_VARIABLE_NUMBER, 32766).\n const collectIn = (column: string, keys: string[]) => {\n for (let i = 0; i < keys.length; i += 500) {\n const chunk = keys.slice(i, i + 500);\n const placeholders = chunk.map(() => '?').join(', ');\n collect(db.prepare(`SELECT src, target, dst, embed FROM links WHERE ${column} IN (${placeholders})`).all(...chunk) as unknown as LinkRow[]);\n }\n };\n\n collectIn('src', delta.reparsed);\n collectIn('target_base', [...changedBasenames]);\n collectIn('dst', delta.vanished);\n\n return resolveRows(db, [...candidates.values()], pathSet, byBase);\n}\n\n// Resolved edges, for rank and for search's graph expansion.\nexport function linkEdges(db: DatabaseSync): [string, string][] {\n // One edge per row, as 0.12's grain always had it -- two written targets resolving to one\n // dst stay two edges (a weight the fusion evals were gated on). The only exclusion is an\n // embed whose exact (src, target) also exists as a link: that second row is new with the\n // embed grain and would double an edge that used to be one row. The NOT EXISTS probes the\n // primary key and fires only for embed rows; both branches still scan the table.\n // Self-edges (same-note anchors) are excluded here so PageRank mass is not self-recycled --\n // Obsidian's own graph view hides self-loops too. The rows themselves stay in the table for\n // backlinks/peek.\n const sql = `SELECT src, dst FROM links WHERE dst IS NOT NULL AND embed = 0 AND src != dst\n UNION ALL\n SELECT src, dst FROM links l WHERE dst IS NOT NULL AND embed = 1 AND src != dst\n AND NOT EXISTS (SELECT 1 FROM links l0 WHERE l0.src = l.src AND l0.target = l.target AND l0.embed = 0)`;\n return (db.prepare(sql).all() as Array<{ src: string; dst: string }>).map((r) => [r.src, r.dst]);\n}\n\n// remove() has already deleted the rows by the time afterReconcile runs, so it records here\n// whether they carried edges. Keyed on the delta object, so the state dies with the reconcile.\nconst removedWithLinks = new WeakMap<ReconcileDelta, Set<string>>();\n\nfunction recordRemoved(delta: ReconcileDelta, path: string): void {\n let set = removedWithLinks.get(delta);\n if (!set) {\n set = new Set();\n removedWithLinks.set(delta, set);\n }\n set.add(path);\n}\n\n// remove() runs per path; the vanished list is an array, so cache the Set per delta.\nconst vanishedSets = new WeakMap<ReconcileDelta, Set<string>>();\nfunction vanishedSet(delta: ReconcileDelta): Set<string> {\n let set = vanishedSets.get(delta);\n if (!set) {\n set = new Set(delta.vanished);\n vanishedSets.set(delta, set);\n }\n return set;\n}\n\nexport const links: Feature = {\n name: 'links',\n schema(db) {\n db.exec('CREATE TABLE IF NOT EXISTS links (src TEXT, target TEXT, target_base TEXT, dst TEXT, embed INTEGER, PRIMARY KEY (src, target, embed))');\n db.exec('CREATE INDEX IF NOT EXISTS links_dst ON links(dst)');\n db.exec('CREATE INDEX IF NOT EXISTS links_target_base ON links(target_base)');\n },\n extract,\n // Reparsed docs are diffed in store() below instead of wiped here: deleting and\n // re-inserting resets dst to NULL, which made every touch-only reparse read as an edge\n // change and recompute PageRank -- measured as most of the remaining update cost.\n remove(db, path, delta) {\n if (!vanishedSet(delta).has(path)) return;\n const result = db.prepare('DELETE FROM links WHERE src = ?').run(path);\n if (Number(result.changes) > 0) recordRemoved(delta, path);\n },\n store(db, path, extracted, delta) {\n const targets = extracted as Array<{ target: string; embed: boolean }>;\n // Stale rows (target/embed pairs the new parse no longer contains) are real edge removals.\n // \\0 can't appear in a target, so the composite key never collides.\n let stale: ReturnType<ReturnType<DatabaseSync['prepare']>['run']>;\n if (targets.length === 0) {\n stale = db.prepare('DELETE FROM links WHERE src = ?').run(path);\n } else {\n const placeholders = targets.map(() => '?').join(', ');\n const keys = targets.map((t) => `${t.target}\\0${t.embed ? '1' : '0'}`);\n stale = db.prepare(`DELETE FROM links WHERE src = ? AND (target || char(0) || embed) NOT IN (${placeholders})`).run(path, ...keys);\n }\n if (Number(stale.changes) > 0) recordRemoved(delta, path);\n // Upsert preserves dst on surviving rows, so an unchanged link resolves to the same\n // value and reports no change; only genuinely new rows start at NULL.\n const insert = db.prepare('INSERT INTO links (src, target, target_base, dst, embed) VALUES (?, ?, ?, NULL, ?) ON CONFLICT(src, target, embed) DO UPDATE SET target_base = excluded.target_base');\n // A same-note anchor's dst is always its own src, never another file's basename, so it\n // gets no target_base -- SQL's `IN` never matches NULL, keeping it out of\n // resolveIncremental's cross-file collectIn('target_base', ...) entirely.\n for (const { target, embed } of targets) insert.run(path, target, target.startsWith('#') ? null : baseKey(cleanTarget(target)), embed ? 1 : 0);\n },\n afterReconcile(db, delta) {\n // Any deleted rows that carried edges mean the edge set shrank -- whether the file\n // vanished or was reparsed down to fewer/no links. dstChanged alone misses the\n // reparsed-to-zero-links case (no surviving rows to re-resolve).\n const removeHadLinks = (removedWithLinks.get(delta)?.size ?? 0) > 0;\n\n const churn = delta.reparsed.length + delta.vanished.length;\n // Past this share of the tree, a full pass is the only one guaranteed to match\n // resolveTarget's ambiguity rules. A cold build clears the threshold on its own.\n const large = delta.files.length === 0 || churn > 0.2 * delta.files.length;\n\n const dstChanged = large ? resolveAll(db, delta.files) : resolveIncremental(db, delta);\n delta.linksChanged = dstChanged || removeHadLinks;\n },\n};\n"],"names":["posix","maskRegions","parseWikilinkInner","inner","beforeAlias","split","trim","startsWith","length","target","extract","_raw","rawBody","_search","data","body","test","seen","Set","results","add","embed","key","has","push","m","matchAll","index","dest","includes","value","Object","values","item","Array","isArray","exec","cleanTarget","replace","baseKey","path","basename","toLowerCase","resolveTarget","src","pathSet","byBase","clean","fromSrc","normalize","join","dirname","candidate","candidates","get","buildByBase","files","Map","paths","map","f","relPath","sort","a","b","list","set","resolveRows","db","rows","update","prepare","changed","row","dst","run","resolveAll","all","resolveIncremental","delta","changedBasenames","p","added","vanished","collect","collectIn","column","keys","i","chunk","slice","placeholders","reparsed","linkEdges","sql","r","removedWithLinks","WeakMap","recordRemoved","vanishedSets","vanishedSet","links","name","schema","remove","result","Number","changes","store","extracted","targets","stale","t","insert","afterReconcile","removeHadLinks","size","churn","large","dstChanged","linksChanged"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sensemaking",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "Query and search your markdown notes with context-aware progressive disclosure: SQL over frontmatter, links, and text, plus semantic search and link-graph ranking. No server, no build step",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|