@hraness/kb 0.17.1
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.
- package/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
navigateLinks
|
|
4
|
+
} from "./index-d13v9ckt.js";
|
|
5
|
+
import {
|
|
6
|
+
queryVault
|
|
7
|
+
} from "./index-48pz4jpc.js";
|
|
8
|
+
import {
|
|
9
|
+
lookupNote
|
|
10
|
+
} from "./index-cxfrakt7.js";
|
|
11
|
+
|
|
12
|
+
// src/search.ts
|
|
13
|
+
var MAX_EXACT_RESULTS = 500;
|
|
14
|
+
var MAX_SEARCH_QUERY_BYTES = 16 * 1024;
|
|
15
|
+
var MAX_SEARCH_QUERY_TERMS = 64;
|
|
16
|
+
var MAX_FUSION_LANES = 16;
|
|
17
|
+
var MAX_FUSION_RESULTS_PER_LANE = 500;
|
|
18
|
+
var MAX_RELATED_SEEDS = 10;
|
|
19
|
+
var MAX_RELATED_RESULTS = 100;
|
|
20
|
+
var MAX_GRAPH_CONNECTIONS_PER_KIND = 200;
|
|
21
|
+
var MAX_GRAPH_EVIDENCE_PER_RESULT = 40;
|
|
22
|
+
var stopWords = new Set([
|
|
23
|
+
"a",
|
|
24
|
+
"an",
|
|
25
|
+
"and",
|
|
26
|
+
"are",
|
|
27
|
+
"as",
|
|
28
|
+
"at",
|
|
29
|
+
"be",
|
|
30
|
+
"by",
|
|
31
|
+
"for",
|
|
32
|
+
"from",
|
|
33
|
+
"how",
|
|
34
|
+
"in",
|
|
35
|
+
"is",
|
|
36
|
+
"it",
|
|
37
|
+
"of",
|
|
38
|
+
"on",
|
|
39
|
+
"or",
|
|
40
|
+
"that",
|
|
41
|
+
"the",
|
|
42
|
+
"this",
|
|
43
|
+
"to",
|
|
44
|
+
"was",
|
|
45
|
+
"what",
|
|
46
|
+
"when",
|
|
47
|
+
"where",
|
|
48
|
+
"why",
|
|
49
|
+
"with"
|
|
50
|
+
]);
|
|
51
|
+
function normalize(value) {
|
|
52
|
+
return value.normalize("NFC").toLocaleLowerCase("en-US");
|
|
53
|
+
}
|
|
54
|
+
function checkedLimit(value, fallback, maximum, label) {
|
|
55
|
+
const limit = value ?? fallback;
|
|
56
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > maximum) {
|
|
57
|
+
throw new RangeError(`${label} must be an integer from 1 through ${maximum}.`);
|
|
58
|
+
}
|
|
59
|
+
return limit;
|
|
60
|
+
}
|
|
61
|
+
function validateSearchQuery(value) {
|
|
62
|
+
if (typeof value !== "string") {
|
|
63
|
+
throw new TypeError("Search query must be a string.");
|
|
64
|
+
}
|
|
65
|
+
let bytes = 0;
|
|
66
|
+
for (const character of value) {
|
|
67
|
+
bytes += Buffer.byteLength(character, "utf8");
|
|
68
|
+
if (bytes > MAX_SEARCH_QUERY_BYTES) {
|
|
69
|
+
throw new RangeError(`Search query must be at most ${MAX_SEARCH_QUERY_BYTES.toLocaleString("en-US")} UTF-8 bytes.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const query = value.trim();
|
|
73
|
+
if (query === "")
|
|
74
|
+
throw new Error("Search query must not be empty.");
|
|
75
|
+
const normalized = normalize(query);
|
|
76
|
+
const unique = new Set;
|
|
77
|
+
for (const match of normalized.matchAll(/[\p{L}\p{N}][\p{L}\p{N}._/-]*/gu)) {
|
|
78
|
+
const term = match[0];
|
|
79
|
+
unique.add(term);
|
|
80
|
+
if (unique.size > MAX_SEARCH_QUERY_TERMS) {
|
|
81
|
+
throw new RangeError(`Search query may contain at most ${MAX_SEARCH_QUERY_TERMS} unique normalized terms.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const raw = [...unique];
|
|
85
|
+
const meaningful = raw.filter((term) => term.length > 1 && !stopWords.has(term));
|
|
86
|
+
return {
|
|
87
|
+
query,
|
|
88
|
+
normalized,
|
|
89
|
+
terms: meaningful.length > 0 ? meaningful : raw
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function occurrenceCount(text, needle, maximum = 8) {
|
|
93
|
+
if (needle === "")
|
|
94
|
+
return 0;
|
|
95
|
+
let count = 0;
|
|
96
|
+
let offset = 0;
|
|
97
|
+
while (count < maximum) {
|
|
98
|
+
const found = text.indexOf(needle, offset);
|
|
99
|
+
if (found < 0)
|
|
100
|
+
break;
|
|
101
|
+
count += 1;
|
|
102
|
+
offset = found + Math.max(1, needle.length);
|
|
103
|
+
}
|
|
104
|
+
return count;
|
|
105
|
+
}
|
|
106
|
+
function originalLineOffset(line, normalizedOffset) {
|
|
107
|
+
let consumed = 0;
|
|
108
|
+
for (const part of new Intl.Segmenter("en-US", {
|
|
109
|
+
granularity: "grapheme"
|
|
110
|
+
}).segment(line)) {
|
|
111
|
+
const width = normalize(part.segment).length;
|
|
112
|
+
if (consumed + width > normalizedOffset)
|
|
113
|
+
return part.index;
|
|
114
|
+
consumed += width;
|
|
115
|
+
}
|
|
116
|
+
return line.length;
|
|
117
|
+
}
|
|
118
|
+
function firstMatchLocation(content, phrase, terms) {
|
|
119
|
+
const normalized = normalize(content);
|
|
120
|
+
let normalizedOffset = normalized.indexOf(phrase);
|
|
121
|
+
if (normalizedOffset < 0) {
|
|
122
|
+
for (const term of terms) {
|
|
123
|
+
normalizedOffset = normalized.indexOf(term);
|
|
124
|
+
if (normalizedOffset >= 0)
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (normalizedOffset < 0)
|
|
129
|
+
return null;
|
|
130
|
+
const normalizedPrefix = normalized.slice(0, normalizedOffset);
|
|
131
|
+
const line = normalizedPrefix.split(`
|
|
132
|
+
`).length;
|
|
133
|
+
const normalizedLineStart = normalizedPrefix.lastIndexOf(`
|
|
134
|
+
`) + 1;
|
|
135
|
+
let lineStart = 0;
|
|
136
|
+
for (let current = 1;current < line; current += 1) {
|
|
137
|
+
lineStart = content.indexOf(`
|
|
138
|
+
`, lineStart) + 1;
|
|
139
|
+
}
|
|
140
|
+
const lineEnd = content.indexOf(`
|
|
141
|
+
`, lineStart);
|
|
142
|
+
const originalLine = content.slice(lineStart, lineEnd < 0 ? content.length : lineEnd);
|
|
143
|
+
return {
|
|
144
|
+
line,
|
|
145
|
+
offset: lineStart + originalLineOffset(originalLine, normalizedOffset - normalizedLineStart)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function exactSnippet(content, location, summary) {
|
|
149
|
+
if (location === null)
|
|
150
|
+
return { snippet: summary };
|
|
151
|
+
const { line, offset } = location;
|
|
152
|
+
const start = Math.max(0, offset - 180);
|
|
153
|
+
const end = Math.min(content.length, start + 600);
|
|
154
|
+
const snippet = content.slice(start, end).replace(/\s+/gu, " ").trim();
|
|
155
|
+
return {
|
|
156
|
+
line,
|
|
157
|
+
snippet: `${start > 0 ? "\u2026" : ""}${snippet}${end < content.length ? "\u2026" : ""}`
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function metadataText(note) {
|
|
161
|
+
return normalize(JSON.stringify(note.metadata));
|
|
162
|
+
}
|
|
163
|
+
function pushEvidence(evidence, candidate) {
|
|
164
|
+
if (evidence.length >= 12)
|
|
165
|
+
return;
|
|
166
|
+
if (evidence.some((item) => item.kind === candidate.kind && item.field === candidate.field && item.value === candidate.value))
|
|
167
|
+
return;
|
|
168
|
+
evidence.push(candidate);
|
|
169
|
+
}
|
|
170
|
+
function exactHit(note, phrase, terms) {
|
|
171
|
+
const title = normalize(note.title);
|
|
172
|
+
const aliases = note.aliases.map(normalize);
|
|
173
|
+
const path = normalize(note.path);
|
|
174
|
+
const id = normalize(note.id);
|
|
175
|
+
const tags = note.tags.map(normalize);
|
|
176
|
+
const metadata = metadataText(note);
|
|
177
|
+
const content = normalize(note.content);
|
|
178
|
+
const matches = [];
|
|
179
|
+
let score = 0;
|
|
180
|
+
let identity = false;
|
|
181
|
+
let phraseMatched = false;
|
|
182
|
+
if (title === phrase) {
|
|
183
|
+
identity = true;
|
|
184
|
+
score += 1000;
|
|
185
|
+
pushEvidence(matches, { kind: "identity", field: "title", value: note.title });
|
|
186
|
+
}
|
|
187
|
+
const exactAlias = note.aliases.find((_, index) => aliases[index] === phrase);
|
|
188
|
+
if (exactAlias !== undefined) {
|
|
189
|
+
identity = true;
|
|
190
|
+
score += 950;
|
|
191
|
+
pushEvidence(matches, { kind: "identity", field: "alias", value: exactAlias });
|
|
192
|
+
}
|
|
193
|
+
if (path === phrase || id === phrase) {
|
|
194
|
+
identity = true;
|
|
195
|
+
score += 900;
|
|
196
|
+
pushEvidence(matches, { kind: "identity", field: "path", value: note.path });
|
|
197
|
+
}
|
|
198
|
+
const phraseFields = [
|
|
199
|
+
["title", title, 400],
|
|
200
|
+
["alias", aliases.join(`
|
|
201
|
+
`), 350],
|
|
202
|
+
["path", `${path}
|
|
203
|
+
${id}`, 300],
|
|
204
|
+
["tag", tags.join(`
|
|
205
|
+
`), 250],
|
|
206
|
+
["content", content, 150],
|
|
207
|
+
["metadata", metadata, 100]
|
|
208
|
+
];
|
|
209
|
+
for (const [field, value, weight] of phraseFields) {
|
|
210
|
+
if (!value.includes(phrase))
|
|
211
|
+
continue;
|
|
212
|
+
phraseMatched = true;
|
|
213
|
+
score += weight;
|
|
214
|
+
pushEvidence(matches, { kind: "phrase", field, value: phrase });
|
|
215
|
+
}
|
|
216
|
+
let matchedTerms = 0;
|
|
217
|
+
for (const term of terms) {
|
|
218
|
+
let matched = false;
|
|
219
|
+
const termFields = [
|
|
220
|
+
["title", title, 40],
|
|
221
|
+
["alias", aliases.join(`
|
|
222
|
+
`), 35],
|
|
223
|
+
["path", `${path}
|
|
224
|
+
${id}`, 30],
|
|
225
|
+
["tag", tags.join(`
|
|
226
|
+
`), 30],
|
|
227
|
+
["metadata", metadata, 10],
|
|
228
|
+
["content", content, 5]
|
|
229
|
+
];
|
|
230
|
+
for (const [field, value, weight] of termFields) {
|
|
231
|
+
if (occurrenceCount(value, term, 1) === 0)
|
|
232
|
+
continue;
|
|
233
|
+
matched = true;
|
|
234
|
+
score += weight;
|
|
235
|
+
pushEvidence(matches, { kind: "term", field, value: term });
|
|
236
|
+
}
|
|
237
|
+
if (matched)
|
|
238
|
+
matchedTerms += 1;
|
|
239
|
+
}
|
|
240
|
+
const requiredTerms = terms.length <= 1 ? terms.length : Math.min(3, Math.ceil(terms.length / 2));
|
|
241
|
+
if (!identity && !phraseMatched && matchedTerms < requiredTerms)
|
|
242
|
+
return null;
|
|
243
|
+
if (terms.length > 0)
|
|
244
|
+
score += Math.round(matchedTerms / terms.length * 100);
|
|
245
|
+
if (score === 0)
|
|
246
|
+
return null;
|
|
247
|
+
const snippet = exactSnippet(note.content, firstMatchLocation(note.content, phrase, terms), note.summary);
|
|
248
|
+
return {
|
|
249
|
+
id: note.id,
|
|
250
|
+
path: note.path,
|
|
251
|
+
title: note.title,
|
|
252
|
+
identity,
|
|
253
|
+
score,
|
|
254
|
+
...snippet,
|
|
255
|
+
matches
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function searchExactVault(notes, analysis, options) {
|
|
259
|
+
const validated = validateSearchQuery(options.query);
|
|
260
|
+
const phrase = validated.normalized;
|
|
261
|
+
const limit = checkedLimit(options.limit, 40, MAX_EXACT_RESULTS, "Exact search limit");
|
|
262
|
+
const allowed = new Set(queryVault(notes, analysis, {
|
|
263
|
+
filters: options.filters ?? [],
|
|
264
|
+
tags: options.tags ?? [],
|
|
265
|
+
repositoryScopes: options.repositoryScopes ?? []
|
|
266
|
+
}).map(({ id }) => id));
|
|
267
|
+
const terms = validated.terms;
|
|
268
|
+
return notes.filter((note) => allowed.has(note.id)).map((note) => exactHit(note, phrase, terms)).filter((hit) => hit !== null).toSorted((left, right) => Number(right.identity) - Number(left.identity) || right.score - left.score || left.path.localeCompare(right.path)).slice(0, limit);
|
|
269
|
+
}
|
|
270
|
+
function fuseRankedCandidates(lanes, k = 60) {
|
|
271
|
+
if (!Number.isSafeInteger(k) || k < 1 || k > 1000) {
|
|
272
|
+
throw new RangeError("Fusion k must be an integer from 1 through 1000.");
|
|
273
|
+
}
|
|
274
|
+
if (lanes.length === 0 || lanes.length > MAX_FUSION_LANES) {
|
|
275
|
+
throw new RangeError(`Fusion requires from 1 through ${MAX_FUSION_LANES} lanes.`);
|
|
276
|
+
}
|
|
277
|
+
const names = new Set;
|
|
278
|
+
const active = lanes.filter(({ ids }) => ids.length > 0);
|
|
279
|
+
let maximum = 0;
|
|
280
|
+
const contributionsById = new Map;
|
|
281
|
+
for (const lane of lanes) {
|
|
282
|
+
if (lane.name.trim() === "" || names.has(lane.name)) {
|
|
283
|
+
throw new Error("Fusion lane names must be non-empty and unique.");
|
|
284
|
+
}
|
|
285
|
+
names.add(lane.name);
|
|
286
|
+
if (!Number.isFinite(lane.weight) || lane.weight <= 0 || lane.weight > 100) {
|
|
287
|
+
throw new RangeError("Fusion lane weights must be greater than 0 and at most 100.");
|
|
288
|
+
}
|
|
289
|
+
if (lane.ids.length > MAX_FUSION_RESULTS_PER_LANE) {
|
|
290
|
+
throw new RangeError(`Fusion lanes may contain at most ${MAX_FUSION_RESULTS_PER_LANE} results.`);
|
|
291
|
+
}
|
|
292
|
+
if (lane.ids.length > 0)
|
|
293
|
+
maximum += lane.weight / (k + 1);
|
|
294
|
+
const seen = new Set;
|
|
295
|
+
for (const [index, id] of lane.ids.entries()) {
|
|
296
|
+
if (id === "" || seen.has(id))
|
|
297
|
+
continue;
|
|
298
|
+
seen.add(id);
|
|
299
|
+
const rank = index + 1;
|
|
300
|
+
const contribution = {
|
|
301
|
+
lane: lane.name,
|
|
302
|
+
rank,
|
|
303
|
+
weight: lane.weight,
|
|
304
|
+
value: lane.weight / (k + rank)
|
|
305
|
+
};
|
|
306
|
+
const existing = contributionsById.get(id) ?? [];
|
|
307
|
+
existing.push(contribution);
|
|
308
|
+
contributionsById.set(id, existing);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (active.length === 0)
|
|
312
|
+
return [];
|
|
313
|
+
const ranked = [...contributionsById].map(([id, contributions]) => ({
|
|
314
|
+
id,
|
|
315
|
+
score: contributions.reduce((sum, item) => sum + item.value, 0) / maximum,
|
|
316
|
+
contributions: contributions.toSorted((left, right) => left.lane.localeCompare(right.lane))
|
|
317
|
+
})).toSorted((left, right) => right.score - left.score || left.id.localeCompare(right.id));
|
|
318
|
+
return ranked.map((candidate, index) => ({ ...candidate, rank: index + 1 }));
|
|
319
|
+
}
|
|
320
|
+
function graphEvidenceForCandidate(seed, seedRank, distance, candidatePath, links, relations, notePathById) {
|
|
321
|
+
const evidence = [];
|
|
322
|
+
for (const relation of relations) {
|
|
323
|
+
const source = notePathById.get(relation.source);
|
|
324
|
+
const target = notePathById.get(relation.target);
|
|
325
|
+
if (source === undefined || target === undefined)
|
|
326
|
+
continue;
|
|
327
|
+
if (source !== candidatePath && target !== candidatePath)
|
|
328
|
+
continue;
|
|
329
|
+
evidence.push({
|
|
330
|
+
kind: "relation",
|
|
331
|
+
seed,
|
|
332
|
+
seedRank,
|
|
333
|
+
distance,
|
|
334
|
+
source,
|
|
335
|
+
target,
|
|
336
|
+
predicate: relation.predicate,
|
|
337
|
+
line: relation.provenance.line
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
for (const link of links) {
|
|
341
|
+
if (link.source !== candidatePath && link.target !== candidatePath)
|
|
342
|
+
continue;
|
|
343
|
+
evidence.push({
|
|
344
|
+
kind: "link",
|
|
345
|
+
seed,
|
|
346
|
+
seedRank,
|
|
347
|
+
distance,
|
|
348
|
+
source: link.source,
|
|
349
|
+
target: link.target,
|
|
350
|
+
line: link.line
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
const sorted = evidence.toSorted((left, right) => left.kind.localeCompare(right.kind) || left.source.localeCompare(right.source) || left.target.localeCompare(right.target) || left.line - right.line);
|
|
354
|
+
return {
|
|
355
|
+
evidence: sorted.slice(0, MAX_GRAPH_EVIDENCE_PER_RESULT),
|
|
356
|
+
truncated: sorted.length > MAX_GRAPH_EVIDENCE_PER_RESULT
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function buildGraphContext(notes, analysis, options) {
|
|
360
|
+
const depth = checkedLimit(options.depth, 1, 2, "Graph context depth");
|
|
361
|
+
const neighborsPerSeed = checkedLimit(options.neighborsPerSeed, 3, 20, "Graph neighbors per seed");
|
|
362
|
+
const limit = checkedLimit(options.limit, 20, MAX_RELATED_RESULTS, "Graph context limit");
|
|
363
|
+
if (options.seeds.length > MAX_RELATED_SEEDS) {
|
|
364
|
+
throw new RangeError(`Graph context accepts at most ${MAX_RELATED_SEEDS} seeds.`);
|
|
365
|
+
}
|
|
366
|
+
if (options.primaryIds.length > MAX_RELATED_RESULTS) {
|
|
367
|
+
throw new RangeError(`Graph context accepts at most ${MAX_RELATED_RESULTS} primary results.`);
|
|
368
|
+
}
|
|
369
|
+
const primary = new Set(options.primaryIds);
|
|
370
|
+
const notePathById = new Map(notes.map((note) => [note.id, note.path]));
|
|
371
|
+
const noteById = new Map(notes.map((note) => [note.id, note]));
|
|
372
|
+
const candidateById = new Map;
|
|
373
|
+
let truncated = false;
|
|
374
|
+
const resolvedSeeds = [];
|
|
375
|
+
for (const [seedIndex, requestedSeed] of options.seeds.entries()) {
|
|
376
|
+
const lookup = lookupNote(notes, requestedSeed);
|
|
377
|
+
if (lookup.kind === "missing") {
|
|
378
|
+
throw new Error(`Graph context seed ${JSON.stringify(requestedSeed)} was not found.`);
|
|
379
|
+
}
|
|
380
|
+
if (lookup.kind === "ambiguous") {
|
|
381
|
+
throw new Error(`Graph context seed ${JSON.stringify(requestedSeed)} is ambiguous: ` + lookup.candidates.map(({ path }) => path).join(", "));
|
|
382
|
+
}
|
|
383
|
+
const seed = lookup.note;
|
|
384
|
+
if (resolvedSeeds.includes(seed.id))
|
|
385
|
+
continue;
|
|
386
|
+
resolvedSeeds.push(seed.id);
|
|
387
|
+
const neighborhood = navigateLinks(notes, analysis, seed, {
|
|
388
|
+
direction: "both",
|
|
389
|
+
depth,
|
|
390
|
+
limit: Math.min(1000, limit + options.seeds.length + primary.size + 1)
|
|
391
|
+
});
|
|
392
|
+
truncated ||= neighborhood.truncated;
|
|
393
|
+
let acceptedForSeed = 0;
|
|
394
|
+
for (const node of neighborhood.nodes) {
|
|
395
|
+
if (node.id === seed.id || primary.has(node.id))
|
|
396
|
+
continue;
|
|
397
|
+
if (acceptedForSeed >= neighborsPerSeed) {
|
|
398
|
+
truncated = true;
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
const note = noteById.get(node.id);
|
|
402
|
+
if (note === undefined)
|
|
403
|
+
continue;
|
|
404
|
+
const candidateEvidence = graphEvidenceForCandidate(seed.id, seedIndex + 1, node.distance, note.path, neighborhood.edges, neighborhood.relations, notePathById);
|
|
405
|
+
truncated ||= candidateEvidence.truncated;
|
|
406
|
+
const { evidence } = candidateEvidence;
|
|
407
|
+
if (evidence.length === 0)
|
|
408
|
+
continue;
|
|
409
|
+
const existing = candidateById.get(note.id);
|
|
410
|
+
if (existing === undefined) {
|
|
411
|
+
candidateById.set(note.id, {
|
|
412
|
+
note,
|
|
413
|
+
evidence: [...evidence],
|
|
414
|
+
distance: node.distance
|
|
415
|
+
});
|
|
416
|
+
} else {
|
|
417
|
+
existing.evidence.push(...evidence);
|
|
418
|
+
existing.distance = Math.min(existing.distance, node.distance);
|
|
419
|
+
}
|
|
420
|
+
acceptedForSeed += 1;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const primaryPaths = new Set(options.primaryIds.map((id) => notePathById.get(id)).filter((path) => path !== undefined));
|
|
424
|
+
const allLinksAmongResults = analysis.contextualLinks.filter(({ source, target }) => primaryPaths.has(source) && primaryPaths.has(target)).toSorted((left, right) => left.source.localeCompare(right.source) || left.target.localeCompare(right.target) || left.line - right.line);
|
|
425
|
+
const allRelationsAmongResults = analysis.authoredRelations.filter(({ source, target }) => primary.has(source) && primary.has(target)).toSorted((left, right) => left.source.localeCompare(right.source) || left.predicate.localeCompare(right.predicate) || left.target.localeCompare(right.target) || left.provenance.line - right.provenance.line);
|
|
426
|
+
if (allLinksAmongResults.length > MAX_GRAPH_CONNECTIONS_PER_KIND || allRelationsAmongResults.length > MAX_GRAPH_CONNECTIONS_PER_KIND) {
|
|
427
|
+
truncated = true;
|
|
428
|
+
}
|
|
429
|
+
const linksAmongResults = allLinksAmongResults.slice(0, MAX_GRAPH_CONNECTIONS_PER_KIND);
|
|
430
|
+
const relationsAmongResults = allRelationsAmongResults.slice(0, MAX_GRAPH_CONNECTIONS_PER_KIND);
|
|
431
|
+
const sorted = [...candidateById.values()].toSorted((left, right) => {
|
|
432
|
+
const leftSeeds = new Set(left.evidence.map(({ seed }) => seed)).size;
|
|
433
|
+
const rightSeeds = new Set(right.evidence.map(({ seed }) => seed)).size;
|
|
434
|
+
const leftTyped = left.evidence.some(({ kind }) => kind === "relation");
|
|
435
|
+
const rightTyped = right.evidence.some(({ kind }) => kind === "relation");
|
|
436
|
+
const leftRank = Math.min(...left.evidence.map(({ seedRank }) => seedRank));
|
|
437
|
+
const rightRank = Math.min(...right.evidence.map(({ seedRank }) => seedRank));
|
|
438
|
+
return rightSeeds - leftSeeds || Number(rightTyped) - Number(leftTyped) || left.distance - right.distance || leftRank - rightRank || left.note.path.localeCompare(right.note.path);
|
|
439
|
+
});
|
|
440
|
+
if (sorted.length > limit)
|
|
441
|
+
truncated = true;
|
|
442
|
+
return {
|
|
443
|
+
seeds: resolvedSeeds,
|
|
444
|
+
linksAmongResults,
|
|
445
|
+
relationsAmongResults,
|
|
446
|
+
related: sorted.slice(0, limit).map(({ note, distance, evidence }) => {
|
|
447
|
+
const sortedEvidence = evidence.toSorted((left, right) => left.seedRank - right.seedRank || left.distance - right.distance || left.kind.localeCompare(right.kind) || left.source.localeCompare(right.source) || left.target.localeCompare(right.target) || left.line - right.line);
|
|
448
|
+
if (sortedEvidence.length > MAX_GRAPH_EVIDENCE_PER_RESULT)
|
|
449
|
+
truncated = true;
|
|
450
|
+
return {
|
|
451
|
+
id: note.id,
|
|
452
|
+
path: note.path,
|
|
453
|
+
title: note.title,
|
|
454
|
+
distance,
|
|
455
|
+
evidence: sortedEvidence.slice(0, MAX_GRAPH_EVIDENCE_PER_RESULT)
|
|
456
|
+
};
|
|
457
|
+
}),
|
|
458
|
+
truncated
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export { MAX_SEARCH_QUERY_BYTES, MAX_SEARCH_QUERY_TERMS, validateSearchQuery, searchExactVault, fuseRankedCandidates, buildGraphContext };
|