@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
package/src/search.ts
ADDED
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthoredRelation,
|
|
3
|
+
Note,
|
|
4
|
+
ResolvedLink,
|
|
5
|
+
VaultAnalysis,
|
|
6
|
+
} from "./graph.js";
|
|
7
|
+
import { lookupNote } from "./graph.js";
|
|
8
|
+
import { navigateLinks } from "./navigation.js";
|
|
9
|
+
import {
|
|
10
|
+
queryVault,
|
|
11
|
+
type MetadataFilter,
|
|
12
|
+
} from "./query.js";
|
|
13
|
+
|
|
14
|
+
const MAX_EXACT_RESULTS = 500;
|
|
15
|
+
export const MAX_SEARCH_QUERY_BYTES = 16 * 1_024;
|
|
16
|
+
export const MAX_SEARCH_QUERY_TERMS = 64;
|
|
17
|
+
const MAX_FUSION_LANES = 16;
|
|
18
|
+
const MAX_FUSION_RESULTS_PER_LANE = 500;
|
|
19
|
+
const MAX_RELATED_SEEDS = 10;
|
|
20
|
+
const MAX_RELATED_RESULTS = 100;
|
|
21
|
+
const MAX_GRAPH_CONNECTIONS_PER_KIND = 200;
|
|
22
|
+
const MAX_GRAPH_EVIDENCE_PER_RESULT = 40;
|
|
23
|
+
|
|
24
|
+
const stopWords = new Set([
|
|
25
|
+
"a",
|
|
26
|
+
"an",
|
|
27
|
+
"and",
|
|
28
|
+
"are",
|
|
29
|
+
"as",
|
|
30
|
+
"at",
|
|
31
|
+
"be",
|
|
32
|
+
"by",
|
|
33
|
+
"for",
|
|
34
|
+
"from",
|
|
35
|
+
"how",
|
|
36
|
+
"in",
|
|
37
|
+
"is",
|
|
38
|
+
"it",
|
|
39
|
+
"of",
|
|
40
|
+
"on",
|
|
41
|
+
"or",
|
|
42
|
+
"that",
|
|
43
|
+
"the",
|
|
44
|
+
"this",
|
|
45
|
+
"to",
|
|
46
|
+
"was",
|
|
47
|
+
"what",
|
|
48
|
+
"when",
|
|
49
|
+
"where",
|
|
50
|
+
"why",
|
|
51
|
+
"with",
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
export type ExactMatchField =
|
|
55
|
+
| "alias"
|
|
56
|
+
| "content"
|
|
57
|
+
| "metadata"
|
|
58
|
+
| "path"
|
|
59
|
+
| "tag"
|
|
60
|
+
| "title";
|
|
61
|
+
|
|
62
|
+
export type ExactMatchEvidence = {
|
|
63
|
+
readonly kind: "identity" | "phrase" | "term";
|
|
64
|
+
readonly field: ExactMatchField;
|
|
65
|
+
readonly value: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type ExactSearchHit = {
|
|
69
|
+
readonly id: string;
|
|
70
|
+
readonly path: string;
|
|
71
|
+
readonly title: string;
|
|
72
|
+
readonly identity: boolean;
|
|
73
|
+
/** A deterministic lane-local score used only to establish exact-search rank. */
|
|
74
|
+
readonly score: number;
|
|
75
|
+
readonly line?: number;
|
|
76
|
+
readonly snippet: string;
|
|
77
|
+
readonly matches: readonly ExactMatchEvidence[];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type ExactSearchOptions = {
|
|
81
|
+
readonly query: string;
|
|
82
|
+
readonly filters?: readonly MetadataFilter[];
|
|
83
|
+
readonly tags?: readonly string[];
|
|
84
|
+
/** Match any exact, case-sensitive canonical authored repository scope. */
|
|
85
|
+
readonly repositoryScopes?: readonly string[];
|
|
86
|
+
readonly limit?: number;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type ValidatedSearchQuery = {
|
|
90
|
+
/** Trimmed, original-casing query passed to retrieval backends. */
|
|
91
|
+
readonly query: string;
|
|
92
|
+
/** NFC-normalized query used for deterministic exact matching. */
|
|
93
|
+
readonly normalized: string;
|
|
94
|
+
/** Unique normalized terms used by exact matching. */
|
|
95
|
+
readonly terms: readonly string[];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type FusionLaneName = string;
|
|
99
|
+
|
|
100
|
+
export type FusionLane = {
|
|
101
|
+
readonly name: FusionLaneName;
|
|
102
|
+
readonly weight: number;
|
|
103
|
+
readonly ids: readonly string[];
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type FusionContribution = {
|
|
107
|
+
readonly lane: FusionLaneName;
|
|
108
|
+
readonly rank: number;
|
|
109
|
+
readonly weight: number;
|
|
110
|
+
readonly value: number;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type FusedCandidate = {
|
|
114
|
+
readonly id: string;
|
|
115
|
+
readonly rank: number;
|
|
116
|
+
/** Normalized weighted reciprocal-rank score. It is not a probability. */
|
|
117
|
+
readonly score: number;
|
|
118
|
+
readonly contributions: readonly FusionContribution[];
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export type GraphContextEvidence =
|
|
122
|
+
| {
|
|
123
|
+
readonly kind: "link";
|
|
124
|
+
readonly seed: string;
|
|
125
|
+
readonly seedRank: number;
|
|
126
|
+
readonly distance: number;
|
|
127
|
+
readonly source: string;
|
|
128
|
+
readonly target: string;
|
|
129
|
+
readonly line: number;
|
|
130
|
+
}
|
|
131
|
+
| {
|
|
132
|
+
readonly kind: "relation";
|
|
133
|
+
readonly seed: string;
|
|
134
|
+
readonly seedRank: number;
|
|
135
|
+
readonly distance: number;
|
|
136
|
+
readonly source: string;
|
|
137
|
+
readonly target: string;
|
|
138
|
+
readonly predicate: string;
|
|
139
|
+
readonly line: number;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export type RelatedContextHit = {
|
|
143
|
+
readonly id: string;
|
|
144
|
+
readonly path: string;
|
|
145
|
+
readonly title: string;
|
|
146
|
+
readonly distance: number;
|
|
147
|
+
readonly evidence: readonly GraphContextEvidence[];
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export type GraphContext = {
|
|
151
|
+
readonly seeds: readonly string[];
|
|
152
|
+
readonly linksAmongResults: readonly ResolvedLink[];
|
|
153
|
+
readonly relationsAmongResults: readonly AuthoredRelation[];
|
|
154
|
+
readonly related: readonly RelatedContextHit[];
|
|
155
|
+
readonly truncated: boolean;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export type GraphContextOptions = {
|
|
159
|
+
readonly seeds: readonly string[];
|
|
160
|
+
readonly primaryIds: readonly string[];
|
|
161
|
+
readonly depth?: number;
|
|
162
|
+
readonly neighborsPerSeed?: number;
|
|
163
|
+
readonly limit?: number;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
function normalize(value: string): string {
|
|
167
|
+
return value.normalize("NFC").toLocaleLowerCase("en-US");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function checkedLimit(
|
|
171
|
+
value: number | undefined,
|
|
172
|
+
fallback: number,
|
|
173
|
+
maximum: number,
|
|
174
|
+
label: string,
|
|
175
|
+
): number {
|
|
176
|
+
const limit = value ?? fallback;
|
|
177
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > maximum) {
|
|
178
|
+
throw new RangeError(`${label} must be an integer from 1 through ${maximum}.`);
|
|
179
|
+
}
|
|
180
|
+
return limit;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Validate one bounded query before exact scanning or local-model work begins. */
|
|
184
|
+
export function validateSearchQuery(value: unknown): ValidatedSearchQuery {
|
|
185
|
+
if (typeof value !== "string") {
|
|
186
|
+
throw new TypeError("Search query must be a string.");
|
|
187
|
+
}
|
|
188
|
+
let bytes = 0;
|
|
189
|
+
for (const character of value) {
|
|
190
|
+
bytes += Buffer.byteLength(character, "utf8");
|
|
191
|
+
if (bytes > MAX_SEARCH_QUERY_BYTES) {
|
|
192
|
+
throw new RangeError(
|
|
193
|
+
`Search query must be at most ${MAX_SEARCH_QUERY_BYTES.toLocaleString("en-US")} UTF-8 bytes.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const query = value.trim();
|
|
198
|
+
if (query === "") throw new Error("Search query must not be empty.");
|
|
199
|
+
const normalized = normalize(query);
|
|
200
|
+
const unique = new Set<string>();
|
|
201
|
+
for (const match of normalized.matchAll(/[\p{L}\p{N}][\p{L}\p{N}._/-]*/gu)) {
|
|
202
|
+
const term = match[0];
|
|
203
|
+
unique.add(term);
|
|
204
|
+
if (unique.size > MAX_SEARCH_QUERY_TERMS) {
|
|
205
|
+
throw new RangeError(
|
|
206
|
+
`Search query may contain at most ${MAX_SEARCH_QUERY_TERMS} unique normalized terms.`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const raw = [...unique];
|
|
211
|
+
const meaningful = raw.filter((term) => term.length > 1 && !stopWords.has(term));
|
|
212
|
+
return {
|
|
213
|
+
query,
|
|
214
|
+
normalized,
|
|
215
|
+
terms: meaningful.length > 0 ? meaningful : raw,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function occurrenceCount(text: string, needle: string, maximum = 8): number {
|
|
220
|
+
if (needle === "") return 0;
|
|
221
|
+
let count = 0;
|
|
222
|
+
let offset = 0;
|
|
223
|
+
while (count < maximum) {
|
|
224
|
+
const found = text.indexOf(needle, offset);
|
|
225
|
+
if (found < 0) break;
|
|
226
|
+
count += 1;
|
|
227
|
+
offset = found + Math.max(1, needle.length);
|
|
228
|
+
}
|
|
229
|
+
return count;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
type ExactMatchLocation = {
|
|
233
|
+
readonly offset: number;
|
|
234
|
+
readonly line: number;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
function originalLineOffset(
|
|
238
|
+
line: string,
|
|
239
|
+
normalizedOffset: number,
|
|
240
|
+
): number {
|
|
241
|
+
let consumed = 0;
|
|
242
|
+
for (const part of new Intl.Segmenter("en-US", {
|
|
243
|
+
granularity: "grapheme",
|
|
244
|
+
}).segment(line)) {
|
|
245
|
+
const width = normalize(part.segment).length;
|
|
246
|
+
if (consumed + width > normalizedOffset) return part.index;
|
|
247
|
+
consumed += width;
|
|
248
|
+
}
|
|
249
|
+
return line.length;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function firstMatchLocation(
|
|
253
|
+
content: string,
|
|
254
|
+
phrase: string,
|
|
255
|
+
terms: readonly string[],
|
|
256
|
+
): ExactMatchLocation | null {
|
|
257
|
+
const normalized = normalize(content);
|
|
258
|
+
let normalizedOffset = normalized.indexOf(phrase);
|
|
259
|
+
if (normalizedOffset < 0) {
|
|
260
|
+
for (const term of terms) {
|
|
261
|
+
normalizedOffset = normalized.indexOf(term);
|
|
262
|
+
if (normalizedOffset >= 0) break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (normalizedOffset < 0) return null;
|
|
266
|
+
|
|
267
|
+
const normalizedPrefix = normalized.slice(0, normalizedOffset);
|
|
268
|
+
const line = normalizedPrefix.split("\n").length;
|
|
269
|
+
const normalizedLineStart = normalizedPrefix.lastIndexOf("\n") + 1;
|
|
270
|
+
let lineStart = 0;
|
|
271
|
+
for (let current = 1; current < line; current += 1) {
|
|
272
|
+
lineStart = content.indexOf("\n", lineStart) + 1;
|
|
273
|
+
}
|
|
274
|
+
const lineEnd = content.indexOf("\n", lineStart);
|
|
275
|
+
const originalLine = content.slice(
|
|
276
|
+
lineStart,
|
|
277
|
+
lineEnd < 0 ? content.length : lineEnd,
|
|
278
|
+
);
|
|
279
|
+
return {
|
|
280
|
+
line,
|
|
281
|
+
offset: lineStart + originalLineOffset(
|
|
282
|
+
originalLine,
|
|
283
|
+
normalizedOffset - normalizedLineStart,
|
|
284
|
+
),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function exactSnippet(content: string, location: ExactMatchLocation | null, summary: string): {
|
|
289
|
+
readonly line?: number;
|
|
290
|
+
readonly snippet: string;
|
|
291
|
+
} {
|
|
292
|
+
if (location === null) return { snippet: summary };
|
|
293
|
+
const { line, offset } = location;
|
|
294
|
+
const start = Math.max(0, offset - 180);
|
|
295
|
+
const end = Math.min(content.length, start + 600);
|
|
296
|
+
const snippet = content.slice(start, end).replace(/\s+/gu, " ").trim();
|
|
297
|
+
return {
|
|
298
|
+
line,
|
|
299
|
+
snippet: `${start > 0 ? "…" : ""}${snippet}${end < content.length ? "…" : ""}`,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function metadataText(note: Note): string {
|
|
304
|
+
return normalize(JSON.stringify(note.metadata));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function pushEvidence(
|
|
308
|
+
evidence: ExactMatchEvidence[],
|
|
309
|
+
candidate: ExactMatchEvidence,
|
|
310
|
+
): void {
|
|
311
|
+
if (evidence.length >= 12) return;
|
|
312
|
+
if (evidence.some((item) =>
|
|
313
|
+
item.kind === candidate.kind
|
|
314
|
+
&& item.field === candidate.field
|
|
315
|
+
&& item.value === candidate.value)) return;
|
|
316
|
+
evidence.push(candidate);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function exactHit(
|
|
320
|
+
note: Note,
|
|
321
|
+
phrase: string,
|
|
322
|
+
terms: readonly string[],
|
|
323
|
+
): ExactSearchHit | null {
|
|
324
|
+
const title = normalize(note.title);
|
|
325
|
+
const aliases = note.aliases.map(normalize);
|
|
326
|
+
const path = normalize(note.path);
|
|
327
|
+
const id = normalize(note.id);
|
|
328
|
+
const tags = note.tags.map(normalize);
|
|
329
|
+
const metadata = metadataText(note);
|
|
330
|
+
const content = normalize(note.content);
|
|
331
|
+
const matches: ExactMatchEvidence[] = [];
|
|
332
|
+
let score = 0;
|
|
333
|
+
let identity = false;
|
|
334
|
+
let phraseMatched = false;
|
|
335
|
+
|
|
336
|
+
if (title === phrase) {
|
|
337
|
+
identity = true;
|
|
338
|
+
score += 1_000;
|
|
339
|
+
pushEvidence(matches, { kind: "identity", field: "title", value: note.title });
|
|
340
|
+
}
|
|
341
|
+
const exactAlias = note.aliases.find((_, index) => aliases[index] === phrase);
|
|
342
|
+
if (exactAlias !== undefined) {
|
|
343
|
+
identity = true;
|
|
344
|
+
score += 950;
|
|
345
|
+
pushEvidence(matches, { kind: "identity", field: "alias", value: exactAlias });
|
|
346
|
+
}
|
|
347
|
+
if (path === phrase || id === phrase) {
|
|
348
|
+
identity = true;
|
|
349
|
+
score += 900;
|
|
350
|
+
pushEvidence(matches, { kind: "identity", field: "path", value: note.path });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const phraseFields: readonly [ExactMatchField, string, number][] = [
|
|
354
|
+
["title", title, 400],
|
|
355
|
+
["alias", aliases.join("\n"), 350],
|
|
356
|
+
["path", `${path}\n${id}`, 300],
|
|
357
|
+
["tag", tags.join("\n"), 250],
|
|
358
|
+
["content", content, 150],
|
|
359
|
+
["metadata", metadata, 100],
|
|
360
|
+
];
|
|
361
|
+
for (const [field, value, weight] of phraseFields) {
|
|
362
|
+
if (!value.includes(phrase)) continue;
|
|
363
|
+
phraseMatched = true;
|
|
364
|
+
score += weight;
|
|
365
|
+
pushEvidence(matches, { kind: "phrase", field, value: phrase });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let matchedTerms = 0;
|
|
369
|
+
for (const term of terms) {
|
|
370
|
+
let matched = false;
|
|
371
|
+
const termFields: readonly [ExactMatchField, string, number][] = [
|
|
372
|
+
["title", title, 40],
|
|
373
|
+
["alias", aliases.join("\n"), 35],
|
|
374
|
+
["path", `${path}\n${id}`, 30],
|
|
375
|
+
["tag", tags.join("\n"), 30],
|
|
376
|
+
["metadata", metadata, 10],
|
|
377
|
+
["content", content, 5],
|
|
378
|
+
];
|
|
379
|
+
for (const [field, value, weight] of termFields) {
|
|
380
|
+
if (occurrenceCount(value, term, 1) === 0) continue;
|
|
381
|
+
matched = true;
|
|
382
|
+
score += weight;
|
|
383
|
+
pushEvidence(matches, { kind: "term", field, value: term });
|
|
384
|
+
}
|
|
385
|
+
if (matched) matchedTerms += 1;
|
|
386
|
+
}
|
|
387
|
+
const requiredTerms = terms.length <= 1
|
|
388
|
+
? terms.length
|
|
389
|
+
: Math.min(3, Math.ceil(terms.length / 2));
|
|
390
|
+
if (!identity && !phraseMatched && matchedTerms < requiredTerms) return null;
|
|
391
|
+
if (terms.length > 0) score += Math.round((matchedTerms / terms.length) * 100);
|
|
392
|
+
if (score === 0) return null;
|
|
393
|
+
|
|
394
|
+
const snippet = exactSnippet(
|
|
395
|
+
note.content,
|
|
396
|
+
firstMatchLocation(note.content, phrase, terms),
|
|
397
|
+
note.summary,
|
|
398
|
+
);
|
|
399
|
+
return {
|
|
400
|
+
id: note.id,
|
|
401
|
+
path: note.path,
|
|
402
|
+
title: note.title,
|
|
403
|
+
identity,
|
|
404
|
+
score,
|
|
405
|
+
...snippet,
|
|
406
|
+
matches,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Search the live Markdown snapshot for exact identities, phrases, and terms. */
|
|
411
|
+
export function searchExactVault(
|
|
412
|
+
notes: readonly Note[],
|
|
413
|
+
analysis: VaultAnalysis,
|
|
414
|
+
options: ExactSearchOptions,
|
|
415
|
+
): readonly ExactSearchHit[] {
|
|
416
|
+
const validated = validateSearchQuery(options.query);
|
|
417
|
+
const phrase = validated.normalized;
|
|
418
|
+
const limit = checkedLimit(options.limit, 40, MAX_EXACT_RESULTS, "Exact search limit");
|
|
419
|
+
const allowed = new Set(queryVault(notes, analysis, {
|
|
420
|
+
filters: options.filters ?? [],
|
|
421
|
+
tags: options.tags ?? [],
|
|
422
|
+
repositoryScopes: options.repositoryScopes ?? [],
|
|
423
|
+
}).map(({ id }) => id));
|
|
424
|
+
const terms = validated.terms;
|
|
425
|
+
return notes
|
|
426
|
+
.filter((note) => allowed.has(note.id))
|
|
427
|
+
.map((note) => exactHit(note, phrase, terms))
|
|
428
|
+
.filter((hit): hit is ExactSearchHit => hit !== null)
|
|
429
|
+
.toSorted((left, right) =>
|
|
430
|
+
Number(right.identity) - Number(left.identity)
|
|
431
|
+
|| right.score - left.score
|
|
432
|
+
|| left.path.localeCompare(right.path))
|
|
433
|
+
.slice(0, limit);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Fuse incomparable retrieval scores through deterministic weighted ranks. */
|
|
437
|
+
export function fuseRankedCandidates(
|
|
438
|
+
lanes: readonly FusionLane[],
|
|
439
|
+
k = 60,
|
|
440
|
+
): readonly FusedCandidate[] {
|
|
441
|
+
if (!Number.isSafeInteger(k) || k < 1 || k > 1_000) {
|
|
442
|
+
throw new RangeError("Fusion k must be an integer from 1 through 1000.");
|
|
443
|
+
}
|
|
444
|
+
if (lanes.length === 0 || lanes.length > MAX_FUSION_LANES) {
|
|
445
|
+
throw new RangeError(
|
|
446
|
+
`Fusion requires from 1 through ${MAX_FUSION_LANES} lanes.`,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
const names = new Set<string>();
|
|
450
|
+
const active = lanes.filter(({ ids }) => ids.length > 0);
|
|
451
|
+
let maximum = 0;
|
|
452
|
+
const contributionsById = new Map<string, FusionContribution[]>();
|
|
453
|
+
for (const lane of lanes) {
|
|
454
|
+
if (lane.name.trim() === "" || names.has(lane.name)) {
|
|
455
|
+
throw new Error("Fusion lane names must be non-empty and unique.");
|
|
456
|
+
}
|
|
457
|
+
names.add(lane.name);
|
|
458
|
+
if (!Number.isFinite(lane.weight) || lane.weight <= 0 || lane.weight > 100) {
|
|
459
|
+
throw new RangeError("Fusion lane weights must be greater than 0 and at most 100.");
|
|
460
|
+
}
|
|
461
|
+
if (lane.ids.length > MAX_FUSION_RESULTS_PER_LANE) {
|
|
462
|
+
throw new RangeError(
|
|
463
|
+
`Fusion lanes may contain at most ${MAX_FUSION_RESULTS_PER_LANE} results.`,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
if (lane.ids.length > 0) maximum += lane.weight / (k + 1);
|
|
467
|
+
const seen = new Set<string>();
|
|
468
|
+
for (const [index, id] of lane.ids.entries()) {
|
|
469
|
+
if (id === "" || seen.has(id)) continue;
|
|
470
|
+
seen.add(id);
|
|
471
|
+
const rank = index + 1;
|
|
472
|
+
const contribution: FusionContribution = {
|
|
473
|
+
lane: lane.name,
|
|
474
|
+
rank,
|
|
475
|
+
weight: lane.weight,
|
|
476
|
+
value: lane.weight / (k + rank),
|
|
477
|
+
};
|
|
478
|
+
const existing = contributionsById.get(id) ?? [];
|
|
479
|
+
existing.push(contribution);
|
|
480
|
+
contributionsById.set(id, existing);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (active.length === 0) return [];
|
|
484
|
+
const ranked = [...contributionsById].map(([id, contributions]) => ({
|
|
485
|
+
id,
|
|
486
|
+
score: contributions.reduce((sum, item) => sum + item.value, 0) / maximum,
|
|
487
|
+
contributions: contributions.toSorted((left, right) =>
|
|
488
|
+
left.lane.localeCompare(right.lane)),
|
|
489
|
+
})).toSorted((left, right) =>
|
|
490
|
+
right.score - left.score || left.id.localeCompare(right.id));
|
|
491
|
+
return ranked.map((candidate, index) => ({ ...candidate, rank: index + 1 }));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function graphEvidenceForCandidate(
|
|
495
|
+
seed: string,
|
|
496
|
+
seedRank: number,
|
|
497
|
+
distance: number,
|
|
498
|
+
candidatePath: string,
|
|
499
|
+
links: readonly ResolvedLink[],
|
|
500
|
+
relations: readonly AuthoredRelation[],
|
|
501
|
+
notePathById: ReadonlyMap<string, string>,
|
|
502
|
+
): {
|
|
503
|
+
readonly evidence: readonly GraphContextEvidence[];
|
|
504
|
+
readonly truncated: boolean;
|
|
505
|
+
} {
|
|
506
|
+
const evidence: GraphContextEvidence[] = [];
|
|
507
|
+
for (const relation of relations) {
|
|
508
|
+
const source = notePathById.get(relation.source);
|
|
509
|
+
const target = notePathById.get(relation.target);
|
|
510
|
+
if (source === undefined || target === undefined) continue;
|
|
511
|
+
if (source !== candidatePath && target !== candidatePath) continue;
|
|
512
|
+
evidence.push({
|
|
513
|
+
kind: "relation",
|
|
514
|
+
seed,
|
|
515
|
+
seedRank,
|
|
516
|
+
distance,
|
|
517
|
+
source,
|
|
518
|
+
target,
|
|
519
|
+
predicate: relation.predicate,
|
|
520
|
+
line: relation.provenance.line,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
for (const link of links) {
|
|
524
|
+
if (link.source !== candidatePath && link.target !== candidatePath) continue;
|
|
525
|
+
evidence.push({
|
|
526
|
+
kind: "link",
|
|
527
|
+
seed,
|
|
528
|
+
seedRank,
|
|
529
|
+
distance,
|
|
530
|
+
source: link.source,
|
|
531
|
+
target: link.target,
|
|
532
|
+
line: link.line,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
const sorted = evidence.toSorted((left, right) =>
|
|
536
|
+
left.kind.localeCompare(right.kind)
|
|
537
|
+
|| left.source.localeCompare(right.source)
|
|
538
|
+
|| left.target.localeCompare(right.target)
|
|
539
|
+
|| left.line - right.line);
|
|
540
|
+
return {
|
|
541
|
+
evidence: sorted.slice(0, MAX_GRAPH_EVIDENCE_PER_RESULT),
|
|
542
|
+
truncated: sorted.length > MAX_GRAPH_EVIDENCE_PER_RESULT,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Expand explicit graph context without inserting neighbors into relevance rank. */
|
|
547
|
+
export function buildGraphContext(
|
|
548
|
+
notes: readonly Note[],
|
|
549
|
+
analysis: VaultAnalysis,
|
|
550
|
+
options: GraphContextOptions,
|
|
551
|
+
): GraphContext {
|
|
552
|
+
const depth = checkedLimit(options.depth, 1, 2, "Graph context depth");
|
|
553
|
+
const neighborsPerSeed = checkedLimit(
|
|
554
|
+
options.neighborsPerSeed,
|
|
555
|
+
3,
|
|
556
|
+
20,
|
|
557
|
+
"Graph neighbors per seed",
|
|
558
|
+
);
|
|
559
|
+
const limit = checkedLimit(
|
|
560
|
+
options.limit,
|
|
561
|
+
20,
|
|
562
|
+
MAX_RELATED_RESULTS,
|
|
563
|
+
"Graph context limit",
|
|
564
|
+
);
|
|
565
|
+
if (options.seeds.length > MAX_RELATED_SEEDS) {
|
|
566
|
+
throw new RangeError(
|
|
567
|
+
`Graph context accepts at most ${MAX_RELATED_SEEDS} seeds.`,
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
if (options.primaryIds.length > MAX_RELATED_RESULTS) {
|
|
571
|
+
throw new RangeError(
|
|
572
|
+
`Graph context accepts at most ${MAX_RELATED_RESULTS} primary results.`,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
const primary = new Set(options.primaryIds);
|
|
576
|
+
const notePathById = new Map(notes.map((note) => [note.id, note.path]));
|
|
577
|
+
const noteById = new Map(notes.map((note) => [note.id, note]));
|
|
578
|
+
const candidateById = new Map<string, {
|
|
579
|
+
readonly note: Note;
|
|
580
|
+
readonly evidence: GraphContextEvidence[];
|
|
581
|
+
distance: number;
|
|
582
|
+
}>();
|
|
583
|
+
let truncated = false;
|
|
584
|
+
const resolvedSeeds: string[] = [];
|
|
585
|
+
|
|
586
|
+
for (const [seedIndex, requestedSeed] of options.seeds.entries()) {
|
|
587
|
+
const lookup = lookupNote(notes, requestedSeed);
|
|
588
|
+
if (lookup.kind === "missing") {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`Graph context seed ${JSON.stringify(requestedSeed)} was not found.`,
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
if (lookup.kind === "ambiguous") {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`Graph context seed ${JSON.stringify(requestedSeed)} is ambiguous: `
|
|
596
|
+
+ lookup.candidates.map(({ path }) => path).join(", "),
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
const seed = lookup.note;
|
|
600
|
+
if (resolvedSeeds.includes(seed.id)) continue;
|
|
601
|
+
resolvedSeeds.push(seed.id);
|
|
602
|
+
const neighborhood = navigateLinks(notes, analysis, seed, {
|
|
603
|
+
direction: "both",
|
|
604
|
+
depth,
|
|
605
|
+
limit: Math.min(1_000, limit + options.seeds.length + primary.size + 1),
|
|
606
|
+
});
|
|
607
|
+
truncated ||= neighborhood.truncated;
|
|
608
|
+
let acceptedForSeed = 0;
|
|
609
|
+
for (const node of neighborhood.nodes) {
|
|
610
|
+
if (node.id === seed.id || primary.has(node.id)) continue;
|
|
611
|
+
if (acceptedForSeed >= neighborsPerSeed) {
|
|
612
|
+
truncated = true;
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
const note = noteById.get(node.id);
|
|
616
|
+
if (note === undefined) continue;
|
|
617
|
+
const candidateEvidence = graphEvidenceForCandidate(
|
|
618
|
+
seed.id,
|
|
619
|
+
seedIndex + 1,
|
|
620
|
+
node.distance,
|
|
621
|
+
note.path,
|
|
622
|
+
neighborhood.edges,
|
|
623
|
+
neighborhood.relations,
|
|
624
|
+
notePathById,
|
|
625
|
+
);
|
|
626
|
+
truncated ||= candidateEvidence.truncated;
|
|
627
|
+
const { evidence } = candidateEvidence;
|
|
628
|
+
if (evidence.length === 0) continue;
|
|
629
|
+
const existing = candidateById.get(note.id);
|
|
630
|
+
if (existing === undefined) {
|
|
631
|
+
candidateById.set(note.id, {
|
|
632
|
+
note,
|
|
633
|
+
evidence: [...evidence],
|
|
634
|
+
distance: node.distance,
|
|
635
|
+
});
|
|
636
|
+
} else {
|
|
637
|
+
existing.evidence.push(...evidence);
|
|
638
|
+
existing.distance = Math.min(existing.distance, node.distance);
|
|
639
|
+
}
|
|
640
|
+
acceptedForSeed += 1;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const primaryPaths = new Set(
|
|
645
|
+
options.primaryIds
|
|
646
|
+
.map((id) => notePathById.get(id))
|
|
647
|
+
.filter((path): path is string => path !== undefined),
|
|
648
|
+
);
|
|
649
|
+
const allLinksAmongResults = analysis.contextualLinks
|
|
650
|
+
.filter(({ source, target }) => primaryPaths.has(source) && primaryPaths.has(target))
|
|
651
|
+
.toSorted((left, right) =>
|
|
652
|
+
left.source.localeCompare(right.source)
|
|
653
|
+
|| left.target.localeCompare(right.target)
|
|
654
|
+
|| left.line - right.line);
|
|
655
|
+
const allRelationsAmongResults = analysis.authoredRelations
|
|
656
|
+
.filter(({ source, target }) => primary.has(source) && primary.has(target))
|
|
657
|
+
.toSorted((left, right) =>
|
|
658
|
+
left.source.localeCompare(right.source)
|
|
659
|
+
|| left.predicate.localeCompare(right.predicate)
|
|
660
|
+
|| left.target.localeCompare(right.target)
|
|
661
|
+
|| left.provenance.line - right.provenance.line);
|
|
662
|
+
if (
|
|
663
|
+
allLinksAmongResults.length > MAX_GRAPH_CONNECTIONS_PER_KIND
|
|
664
|
+
|| allRelationsAmongResults.length > MAX_GRAPH_CONNECTIONS_PER_KIND
|
|
665
|
+
) {
|
|
666
|
+
truncated = true;
|
|
667
|
+
}
|
|
668
|
+
const linksAmongResults = allLinksAmongResults.slice(0, MAX_GRAPH_CONNECTIONS_PER_KIND);
|
|
669
|
+
const relationsAmongResults = allRelationsAmongResults.slice(
|
|
670
|
+
0,
|
|
671
|
+
MAX_GRAPH_CONNECTIONS_PER_KIND,
|
|
672
|
+
);
|
|
673
|
+
const sorted = [...candidateById.values()].toSorted((left, right) => {
|
|
674
|
+
const leftSeeds = new Set(left.evidence.map(({ seed }) => seed)).size;
|
|
675
|
+
const rightSeeds = new Set(right.evidence.map(({ seed }) => seed)).size;
|
|
676
|
+
const leftTyped = left.evidence.some(({ kind }) => kind === "relation");
|
|
677
|
+
const rightTyped = right.evidence.some(({ kind }) => kind === "relation");
|
|
678
|
+
const leftRank = Math.min(...left.evidence.map(({ seedRank }) => seedRank));
|
|
679
|
+
const rightRank = Math.min(...right.evidence.map(({ seedRank }) => seedRank));
|
|
680
|
+
return rightSeeds - leftSeeds
|
|
681
|
+
|| Number(rightTyped) - Number(leftTyped)
|
|
682
|
+
|| left.distance - right.distance
|
|
683
|
+
|| leftRank - rightRank
|
|
684
|
+
|| left.note.path.localeCompare(right.note.path);
|
|
685
|
+
});
|
|
686
|
+
if (sorted.length > limit) truncated = true;
|
|
687
|
+
return {
|
|
688
|
+
seeds: resolvedSeeds,
|
|
689
|
+
linksAmongResults,
|
|
690
|
+
relationsAmongResults,
|
|
691
|
+
related: sorted.slice(0, limit).map(({ note, distance, evidence }) => {
|
|
692
|
+
const sortedEvidence = evidence.toSorted((left, right) =>
|
|
693
|
+
left.seedRank - right.seedRank
|
|
694
|
+
|| left.distance - right.distance
|
|
695
|
+
|| left.kind.localeCompare(right.kind)
|
|
696
|
+
|| left.source.localeCompare(right.source)
|
|
697
|
+
|| left.target.localeCompare(right.target)
|
|
698
|
+
|| left.line - right.line);
|
|
699
|
+
if (sortedEvidence.length > MAX_GRAPH_EVIDENCE_PER_RESULT) truncated = true;
|
|
700
|
+
return {
|
|
701
|
+
id: note.id,
|
|
702
|
+
path: note.path,
|
|
703
|
+
title: note.title,
|
|
704
|
+
distance,
|
|
705
|
+
evidence: sortedEvidence.slice(0, MAX_GRAPH_EVIDENCE_PER_RESULT),
|
|
706
|
+
};
|
|
707
|
+
}),
|
|
708
|
+
truncated,
|
|
709
|
+
};
|
|
710
|
+
}
|