@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.
Files changed (200) hide show
  1. package/DISCLOSURE +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +545 -0
  4. package/dist/agent-context.js +35 -0
  5. package/dist/agent-guide-audit.js +18 -0
  6. package/dist/attachments.js +19 -0
  7. package/dist/authoring.js +33 -0
  8. package/dist/benchmark.js +19 -0
  9. package/dist/capture.js +49 -0
  10. package/dist/cli.js +3646 -0
  11. package/dist/clip/acquire.js +57 -0
  12. package/dist/clip/args.js +21 -0
  13. package/dist/clip/bounded-byte-buffer.js +9 -0
  14. package/dist/clip/browser-profiles.js +17 -0
  15. package/dist/clip/bundle-reader.js +11 -0
  16. package/dist/clip/cli.js +27 -0
  17. package/dist/clip/cookies.js +21 -0
  18. package/dist/clip/defuddle-worker.js +84 -0
  19. package/dist/clip/doctor.js +32 -0
  20. package/dist/clip/jobs.js +784 -0
  21. package/dist/clip/network-proxy.js +9 -0
  22. package/dist/clip/network.js +28 -0
  23. package/dist/clip/persist.js +30 -0
  24. package/dist/clip/refresh.js +11 -0
  25. package/dist/clip/terminal.js +11 -0
  26. package/dist/evaluation-builder.js +12238 -0
  27. package/dist/evaluation-kb.js +26 -0
  28. package/dist/evaluation.js +35 -0
  29. package/dist/git.js +39 -0
  30. package/dist/graph.js +39 -0
  31. package/dist/index-01jj6rbv.js +1103 -0
  32. package/dist/index-06c9ctr6.js +738 -0
  33. package/dist/index-0kavxzqj.js +2480 -0
  34. package/dist/index-1gwbassd.js +743 -0
  35. package/dist/index-1n418kb9.js +827 -0
  36. package/dist/index-1vrd1rmn.js +56 -0
  37. package/dist/index-1xxnjn0d.js +171 -0
  38. package/dist/index-3rm7cz6h.js +373 -0
  39. package/dist/index-3v2z4f0q.js +328 -0
  40. package/dist/index-48pz4jpc.js +392 -0
  41. package/dist/index-4j3tt0c3.js +234 -0
  42. package/dist/index-5n05se68.js +307 -0
  43. package/dist/index-5vwpzb5a.js +764 -0
  44. package/dist/index-6g2pv9d2.js +370 -0
  45. package/dist/index-7fzc32gf.js +712 -0
  46. package/dist/index-84x0vjjp.js +432 -0
  47. package/dist/index-adx6khj5.js +488 -0
  48. package/dist/index-b88v3vtm.js +805 -0
  49. package/dist/index-cv6fh7z5.js +462 -0
  50. package/dist/index-cxfrakt7.js +1067 -0
  51. package/dist/index-d13v9ckt.js +150 -0
  52. package/dist/index-dycjbhw2.js +1943 -0
  53. package/dist/index-dyqwejk5.js +531 -0
  54. package/dist/index-e5fbsywq.js +658 -0
  55. package/dist/index-f984hw45.js +927 -0
  56. package/dist/index-gh719d91.js +63 -0
  57. package/dist/index-hgve9rh2.js +2046 -0
  58. package/dist/index-hya40gb2.js +474 -0
  59. package/dist/index-j4zgmzjr.js +152 -0
  60. package/dist/index-jsmvyyvf.js +1166 -0
  61. package/dist/index-kh0pwvdd.js +1974 -0
  62. package/dist/index-mqx4nd6v.js +119 -0
  63. package/dist/index-mxxxytys.js +1254 -0
  64. package/dist/index-n5dd7r0v.js +711 -0
  65. package/dist/index-npg9z1a4.js +431 -0
  66. package/dist/index-pj501bh1.js +140 -0
  67. package/dist/index-qry4vhxk.js +1041 -0
  68. package/dist/index-s2gw5aw9.js +199 -0
  69. package/dist/index-vvdj6kca.js +70 -0
  70. package/dist/index-w2zc0vwa.js +571 -0
  71. package/dist/index-x3fthpsc.js +585 -0
  72. package/dist/index-zr53sf63.js +44 -0
  73. package/dist/index-zxdy5pby.js +2728 -0
  74. package/dist/index-zzhgcwyt.js +966 -0
  75. package/dist/index.js +453 -0
  76. package/dist/navigation.js +13 -0
  77. package/dist/pdf.js +85 -0
  78. package/dist/percolate.js +24 -0
  79. package/dist/portfolio.js +77 -0
  80. package/dist/query.js +29 -0
  81. package/dist/repository-memory.js +60 -0
  82. package/dist/sdk.js +36 -0
  83. package/dist/search-rules.js +34 -0
  84. package/dist/search.js +22 -0
  85. package/dist/semantic.js +39 -0
  86. package/dist/source-inbox.js +17 -0
  87. package/dist/untrusted-content.js +32 -0
  88. package/dist/url-intelligence.js +74 -0
  89. package/dist/workflow.js +23 -0
  90. package/dist/workflows/decision-context.js +20 -0
  91. package/dist/workflows/explain-change.js +8 -0
  92. package/dist/workflows/index.js +28 -0
  93. package/dist/workflows/plan-radar.js +10 -0
  94. package/package.json +409 -0
  95. package/skills/kb/AGENTS.md +19 -0
  96. package/skills/kb/SKILL.md +82 -0
  97. package/skills/kb/agents/openai.yaml +4 -0
  98. package/skills/kb/references/pdf-review.md +51 -0
  99. package/skills/kb/references/percolate.md +144 -0
  100. package/skills/kb/references/plan-structure.md +105 -0
  101. package/skills/kb/references/plan.md +115 -0
  102. package/skills/kb/references/query.md +231 -0
  103. package/skills/kb/references/refresh.md +133 -0
  104. package/skills/kb/references/save-pdf.md +125 -0
  105. package/skills/kb/references/save-url.md +178 -0
  106. package/skills/kb/references/url-authentication.md +75 -0
  107. package/skills/kb/references/url-platforms.md +25 -0
  108. package/src/agent-context.ts +1241 -0
  109. package/src/agent-guide-audit.ts +750 -0
  110. package/src/attachments.ts +723 -0
  111. package/src/authoring.ts +1725 -0
  112. package/src/benchmark.ts +362 -0
  113. package/src/capture.ts +68 -0
  114. package/src/cli.ts +3660 -0
  115. package/src/clip/acquire.ts +1273 -0
  116. package/src/clip/archive-today.ts +402 -0
  117. package/src/clip/args.ts +423 -0
  118. package/src/clip/assets.ts +262 -0
  119. package/src/clip/bounded-byte-buffer.ts +75 -0
  120. package/src/clip/browser-profiles.ts +372 -0
  121. package/src/clip/bundle-reader.ts +608 -0
  122. package/src/clip/capture.ts +1054 -0
  123. package/src/clip/cli.ts +181 -0
  124. package/src/clip/cookies.ts +512 -0
  125. package/src/clip/doctor.ts +1110 -0
  126. package/src/clip/extract.ts +868 -0
  127. package/src/clip/jobs.ts +959 -0
  128. package/src/clip/lib.ts +861 -0
  129. package/src/clip/media.ts +1474 -0
  130. package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
  131. package/src/clip/metadata-search-tool/Cargo.toml +18 -0
  132. package/src/clip/metadata-search-tool/runner.ts +685 -0
  133. package/src/clip/metadata-search-tool/src/main.rs +389 -0
  134. package/src/clip/metadata-search.ts +687 -0
  135. package/src/clip/network-proxy.ts +627 -0
  136. package/src/clip/network.ts +950 -0
  137. package/src/clip/package-root.ts +54 -0
  138. package/src/clip/persist.ts +1472 -0
  139. package/src/clip/platforms.ts +1824 -0
  140. package/src/clip/refresh.ts +197 -0
  141. package/src/clip/structured.ts +432 -0
  142. package/src/clip/terminal.ts +197 -0
  143. package/src/clip/url-intelligence.ts +581 -0
  144. package/src/clip/url-metadata-backfill.ts +616 -0
  145. package/src/clip/url-metadata-cli.ts +251 -0
  146. package/src/clip/url-metadata.ts +1397 -0
  147. package/src/evaluation-analysis-v2.ts +1440 -0
  148. package/src/evaluation-builder.ts +2539 -0
  149. package/src/evaluation-corpus-authoring.ts +2052 -0
  150. package/src/evaluation-evidence.ts +1147 -0
  151. package/src/evaluation-execution-v2.ts +426 -0
  152. package/src/evaluation-implementation.ts +131 -0
  153. package/src/evaluation-kb-closure.ts +1430 -0
  154. package/src/evaluation-kb-runner-v2.ts +735 -0
  155. package/src/evaluation-kb-v2.ts +2567 -0
  156. package/src/evaluation-kb.ts +1018 -0
  157. package/src/evaluation-measurement-v2.ts +1462 -0
  158. package/src/evaluation-packing-v2.ts +452 -0
  159. package/src/evaluation-redaction.ts +28 -0
  160. package/src/evaluation-v2.ts +3857 -0
  161. package/src/evaluation.ts +1220 -0
  162. package/src/git.ts +1276 -0
  163. package/src/graph.ts +1487 -0
  164. package/src/index.ts +20 -0
  165. package/src/init.ts +123 -0
  166. package/src/navigation.ts +277 -0
  167. package/src/note-lock.ts +529 -0
  168. package/src/pdf/args.ts +202 -0
  169. package/src/pdf/capture.ts +298 -0
  170. package/src/pdf/cli.ts +159 -0
  171. package/src/pdf/extract.ts +303 -0
  172. package/src/pdf/layout.ts +618 -0
  173. package/src/pdf/markdown.ts +154 -0
  174. package/src/pdf/model.ts +234 -0
  175. package/src/pdf/ocr.ts +190 -0
  176. package/src/pdf/persist.ts +314 -0
  177. package/src/pdf/source.ts +135 -0
  178. package/src/pdf/tools.ts +146 -0
  179. package/src/pdf.ts +92 -0
  180. package/src/percolate.ts +877 -0
  181. package/src/portfolio-audit.ts +574 -0
  182. package/src/portfolio-identity.ts +162 -0
  183. package/src/portfolio-registry.ts +473 -0
  184. package/src/portfolio.ts +765 -0
  185. package/src/query.ts +646 -0
  186. package/src/repository-memory.ts +1150 -0
  187. package/src/sdk.ts +1452 -0
  188. package/src/search-rules.ts +712 -0
  189. package/src/search.ts +710 -0
  190. package/src/semantic-runtime.ts +1139 -0
  191. package/src/semantic.ts +2480 -0
  192. package/src/source-inbox.ts +232 -0
  193. package/src/untrusted-content.ts +401 -0
  194. package/src/url-intelligence.ts +104 -0
  195. package/src/vault.ts +636 -0
  196. package/src/workflow.ts +653 -0
  197. package/src/workflows/decision-context.ts +81 -0
  198. package/src/workflows/explain-change.ts +73 -0
  199. package/src/workflows/index.ts +3 -0
  200. package/src/workflows/plan-radar.ts +101 -0
package/src/query.ts ADDED
@@ -0,0 +1,646 @@
1
+ import {
2
+ type Backlink,
3
+ type MetadataObject,
4
+ type MetadataScalar,
5
+ type MetadataValue,
6
+ type Note,
7
+ type NoteConnections,
8
+ type VaultAnalysis,
9
+ } from "./graph.js";
10
+ import {
11
+ analyzeAuthoredRepositoryScopes,
12
+ validateRepositoryScopeSelection,
13
+ } from "./repository-memory.js";
14
+
15
+ export const MAX_QUERY_FILTERS = 64;
16
+ export const MAX_QUERY_ONE_OF_VALUES = 64;
17
+ export const MAX_QUERY_FILTER_VALUES = 256;
18
+ export const MAX_QUERY_TAGS = 64;
19
+ export const MAX_QUERY_METADATA_PATH_UTF8_BYTES = 1_024;
20
+ export const MAX_QUERY_METADATA_PATH_SEGMENTS = 32;
21
+ export const MAX_QUERY_TEXT_UTF8_BYTES = 16 * 1_024;
22
+ export const MAX_QUERY_OPTIONS_UTF8_BYTES = 64 * 1_024;
23
+
24
+ export type MetadataPath = string | readonly string[];
25
+
26
+ export type MetadataFilter =
27
+ | {
28
+ readonly kind: "equals";
29
+ readonly path: MetadataPath;
30
+ readonly value: MetadataScalar;
31
+ }
32
+ | {
33
+ readonly kind: "exists";
34
+ readonly path: MetadataPath;
35
+ }
36
+ | {
37
+ readonly kind: "one-of";
38
+ readonly path: MetadataPath;
39
+ readonly values: readonly MetadataScalar[];
40
+ };
41
+
42
+ export type QuerySort =
43
+ | {
44
+ readonly kind: "builtin";
45
+ readonly field: "title" | "path" | "inbound" | "outbound";
46
+ }
47
+ | {
48
+ readonly kind: "metadata";
49
+ readonly path: MetadataPath;
50
+ };
51
+
52
+ export type QueryDirection = "asc" | "desc";
53
+
54
+ export type QueryOptions = {
55
+ /** Repeated filters are combined with AND semantics. */
56
+ readonly filters?: readonly MetadataFilter[];
57
+ /** Repeated tags are combined with AND semantics. */
58
+ readonly tags?: readonly string[];
59
+ /** Match any exact, case-sensitive canonical authored repository scope. */
60
+ readonly repositoryScopes?: readonly string[];
61
+ readonly sort?: QuerySort;
62
+ readonly direction?: QueryDirection;
63
+ readonly limit?: number;
64
+ };
65
+
66
+ export type QueryRow = {
67
+ readonly id: string;
68
+ readonly path: string;
69
+ readonly title: string;
70
+ readonly aliases: readonly string[];
71
+ readonly tags: readonly string[];
72
+ readonly properties: Readonly<Record<string, string>>;
73
+ readonly metadata: MetadataObject;
74
+ readonly summary: string;
75
+ readonly inboundContextualCount: number;
76
+ readonly outboundContextualCount: number;
77
+ readonly backlinks: readonly Backlink[];
78
+ };
79
+
80
+ export type MetadataLookup =
81
+ | { readonly found: true; readonly value: MetadataValue }
82
+ | { readonly found: false };
83
+
84
+ function isMetadataArray(value: MetadataValue): value is readonly MetadataValue[] {
85
+ return Array.isArray(value);
86
+ }
87
+
88
+ function isMetadataObject(value: MetadataValue): value is MetadataObject {
89
+ return value !== null && typeof value === "object" && !isMetadataArray(value);
90
+ }
91
+
92
+ function normalizedString(value: string): string {
93
+ return value.normalize("NFC").toLocaleLowerCase("en-US");
94
+ }
95
+
96
+ function normalizedTag(value: string): string {
97
+ return normalizedString(value.trim().replace(/^#+/u, ""));
98
+ }
99
+
100
+ type QueryTextBudget = { bytes: number; filterValues: number };
101
+
102
+ type PreparedMetadataSegment = {
103
+ readonly exact: string;
104
+ readonly normalized: string;
105
+ readonly arrayIndex: number | null;
106
+ };
107
+
108
+ type PreparedMetadataPath = {
109
+ readonly segments: readonly PreparedMetadataSegment[];
110
+ readonly valid: boolean;
111
+ };
112
+
113
+ type MetadataKeyCache = WeakMap<MetadataObject, ReadonlyMap<string, string | null>>;
114
+
115
+ function formattedBytes(value: number): string {
116
+ return value.toLocaleString("en-US");
117
+ }
118
+
119
+ function boundedTextBytes(value: string, maximum: number, label: string): number {
120
+ const bytes = Buffer.byteLength(value, "utf8");
121
+ if (bytes > maximum) {
122
+ throw new RangeError(
123
+ `${label} must be at most ${formattedBytes(maximum)} UTF-8 bytes.`,
124
+ );
125
+ }
126
+ return bytes;
127
+ }
128
+
129
+ function addQueryText(
130
+ budget: QueryTextBudget,
131
+ bytes: number,
132
+ ): void {
133
+ budget.bytes += bytes;
134
+ if (budget.bytes > MAX_QUERY_OPTIONS_UTF8_BYTES) {
135
+ throw new RangeError(
136
+ `Query options may contain at most ${formattedBytes(MAX_QUERY_OPTIONS_UTF8_BYTES)} UTF-8 bytes of text.`,
137
+ );
138
+ }
139
+ }
140
+
141
+ function prepareMetadataPath(
142
+ path: MetadataPath,
143
+ label: string,
144
+ budget?: QueryTextBudget,
145
+ ): PreparedMetadataPath {
146
+ let rawSegments: readonly string[];
147
+ let pathBytes = 0;
148
+ if (typeof path === "string") {
149
+ pathBytes = boundedTextBytes(path, MAX_QUERY_METADATA_PATH_UTF8_BYTES, label);
150
+ rawSegments = path.split(".");
151
+ } else {
152
+ if (!Array.isArray(path)) throw new TypeError(`${label} must be a string or string array.`);
153
+ if (path.length > MAX_QUERY_METADATA_PATH_SEGMENTS) {
154
+ throw new RangeError(
155
+ `${label} may contain at most ${MAX_QUERY_METADATA_PATH_SEGMENTS} segments.`,
156
+ );
157
+ }
158
+ const segments: string[] = [];
159
+ for (const [index, segment] of path.entries()) {
160
+ if (typeof segment !== "string") {
161
+ throw new TypeError(`${label} segment ${index + 1} must be a string.`);
162
+ }
163
+ pathBytes += Buffer.byteLength(segment, "utf8") + (index === 0 ? 0 : 1);
164
+ if (pathBytes > MAX_QUERY_METADATA_PATH_UTF8_BYTES) {
165
+ throw new RangeError(
166
+ `${label} must be at most ${formattedBytes(MAX_QUERY_METADATA_PATH_UTF8_BYTES)} UTF-8 bytes.`,
167
+ );
168
+ }
169
+ segments.push(segment);
170
+ }
171
+ rawSegments = segments;
172
+ }
173
+ if (rawSegments.length > MAX_QUERY_METADATA_PATH_SEGMENTS) {
174
+ throw new RangeError(
175
+ `${label} may contain at most ${MAX_QUERY_METADATA_PATH_SEGMENTS} segments.`,
176
+ );
177
+ }
178
+ if (budget !== undefined) addQueryText(budget, pathBytes);
179
+
180
+ const segments = rawSegments.map((raw): PreparedMetadataSegment => {
181
+ const exact = raw.trim();
182
+ return {
183
+ exact,
184
+ normalized: normalizedString(exact),
185
+ arrayIndex: /^(?:0|[1-9]\d*)$/u.test(exact) ? Number(exact) : null,
186
+ };
187
+ });
188
+ return {
189
+ segments,
190
+ valid: segments.length > 0 && segments.every(({ exact }) => exact !== ""),
191
+ };
192
+ }
193
+
194
+ function normalizedObjectKeys(
195
+ value: MetadataObject,
196
+ cache: MetadataKeyCache,
197
+ ): ReadonlyMap<string, string | null> {
198
+ const cached = cache.get(value);
199
+ if (cached !== undefined) return cached;
200
+ const indexed = new Map<string, string | null>();
201
+ for (const key of Object.keys(value)) {
202
+ const normalized = normalizedString(key);
203
+ indexed.set(normalized, indexed.has(normalized) ? null : key);
204
+ }
205
+ cache.set(value, indexed);
206
+ return indexed;
207
+ }
208
+
209
+ function objectValue(
210
+ value: MetadataObject,
211
+ segment: PreparedMetadataSegment,
212
+ cache: MetadataKeyCache,
213
+ ): MetadataLookup {
214
+ if (Object.hasOwn(value, segment.exact)) {
215
+ const candidate = value[segment.exact];
216
+ return candidate === undefined ? { found: false } : { found: true, value: candidate };
217
+ }
218
+ const matchedKey = normalizedObjectKeys(value, cache).get(segment.normalized);
219
+ if (matchedKey === undefined || matchedKey === null) return { found: false };
220
+ const candidate = value[matchedKey];
221
+ return candidate === undefined ? { found: false } : { found: true, value: candidate };
222
+ }
223
+
224
+ function metadataAtPreparedPath(
225
+ metadata: MetadataObject,
226
+ path: PreparedMetadataPath,
227
+ cache: MetadataKeyCache,
228
+ ): MetadataLookup {
229
+ if (!path.valid) return { found: false };
230
+
231
+ let current: MetadataValue = metadata;
232
+ for (const segment of path.segments) {
233
+ if (isMetadataArray(current)) {
234
+ if (segment.arrayIndex === null) return { found: false };
235
+ const candidate: MetadataValue | undefined = current[segment.arrayIndex];
236
+ if (candidate === undefined) return { found: false };
237
+ current = candidate;
238
+ continue;
239
+ }
240
+ if (!isMetadataObject(current)) return { found: false };
241
+ const lookup = objectValue(current, segment, cache);
242
+ if (!lookup.found) return lookup;
243
+ current = lookup.value;
244
+ }
245
+ return { found: true, value: current };
246
+ }
247
+
248
+ /** Resolve an exact nested field, with unambiguous case-insensitive object keys. */
249
+ export function metadataAtPath(
250
+ metadata: MetadataObject,
251
+ path: MetadataPath,
252
+ ): MetadataLookup {
253
+ return metadataAtPreparedPath(
254
+ metadata,
255
+ prepareMetadataPath(path, "Metadata path"),
256
+ new WeakMap(),
257
+ );
258
+ }
259
+
260
+ type PreparedEqualsFilter = {
261
+ readonly kind: "equals";
262
+ readonly path: PreparedMetadataPath;
263
+ readonly value: MetadataScalar;
264
+ readonly normalizedValue?: string;
265
+ };
266
+
267
+ type PreparedMetadataFilter =
268
+ | PreparedEqualsFilter
269
+ | {
270
+ readonly kind: "exists";
271
+ readonly path: PreparedMetadataPath;
272
+ }
273
+ | {
274
+ readonly kind: "one-of";
275
+ readonly path: PreparedMetadataPath;
276
+ readonly values: readonly PreparedEqualsFilter[];
277
+ };
278
+
279
+ function equalsScalar(
280
+ value: MetadataValue,
281
+ filter: PreparedEqualsFilter,
282
+ stringCache: Map<string, string>,
283
+ ): boolean {
284
+ if (isMetadataArray(value)) {
285
+ return value.some((candidate) => equalsScalar(candidate, filter, stringCache));
286
+ }
287
+ if (isMetadataObject(value)) return false;
288
+ if (typeof value === "string" && filter.normalizedValue !== undefined) {
289
+ let normalized = stringCache.get(value);
290
+ if (normalized === undefined) {
291
+ normalized = normalizedString(value);
292
+ stringCache.set(value, normalized);
293
+ }
294
+ return normalized === filter.normalizedValue;
295
+ }
296
+ return Object.is(value, filter.value);
297
+ }
298
+
299
+ type QueryableMetadata = Pick<Note, "metadata" | "tags">;
300
+
301
+ function matchesFilter(
302
+ note: QueryableMetadata,
303
+ filter: PreparedMetadataFilter,
304
+ keyCache: MetadataKeyCache,
305
+ stringCache: Map<string, string>,
306
+ ): boolean {
307
+ const lookup = metadataAtPreparedPath(note.metadata, filter.path, keyCache);
308
+ if (filter.kind === "exists") return lookup.found;
309
+ if (!lookup.found) return false;
310
+ if (filter.kind === "one-of") {
311
+ return filter.values.some((candidate) =>
312
+ equalsScalar(lookup.value, candidate, stringCache));
313
+ }
314
+ return equalsScalar(lookup.value, filter, stringCache);
315
+ }
316
+
317
+ function matchesTags(note: QueryableMetadata, tags: ReadonlySet<string>): boolean {
318
+ const noteTags = new Set(note.tags.map(normalizedTag));
319
+ for (const tag of tags) {
320
+ if (tag === "" || !noteTags.has(tag)) return false;
321
+ }
322
+ return true;
323
+ }
324
+
325
+ type PreparedQuerySort =
326
+ | {
327
+ readonly kind: "builtin";
328
+ readonly field: "title" | "path" | "inbound" | "outbound";
329
+ }
330
+ | {
331
+ readonly kind: "metadata";
332
+ readonly path: PreparedMetadataPath;
333
+ };
334
+
335
+ type PreparedQueryOptions = {
336
+ readonly filters: readonly PreparedMetadataFilter[];
337
+ readonly tags: ReadonlySet<string>;
338
+ readonly repositoryScopes: ReadonlySet<string>;
339
+ readonly sort: PreparedQuerySort;
340
+ readonly direction: QueryDirection;
341
+ readonly limit?: number;
342
+ };
343
+
344
+ function isMetadataScalar(value: unknown): value is MetadataScalar {
345
+ return value === null
346
+ || typeof value === "string"
347
+ || typeof value === "number"
348
+ || typeof value === "boolean";
349
+ }
350
+
351
+ function prepareFilter(
352
+ value: unknown,
353
+ index: number,
354
+ budget: QueryTextBudget,
355
+ ): PreparedMetadataFilter {
356
+ const label = `Query filter ${index + 1}`;
357
+ if (value === null || typeof value !== "object") {
358
+ throw new TypeError(`${label} must be an equals, exists, or one-of filter.`);
359
+ }
360
+ const filter = value as Readonly<Record<string, unknown>>;
361
+ const path = prepareMetadataPath(
362
+ filter.path as MetadataPath,
363
+ `${label} metadata path`,
364
+ budget,
365
+ );
366
+ if (filter.kind === "exists") return { kind: "exists", path };
367
+ if (filter.kind === "one-of") {
368
+ if (!Array.isArray(filter.values)) {
369
+ throw new TypeError(`${label} one-of values must be an array.`);
370
+ }
371
+ if (filter.values.length === 0 || filter.values.length > MAX_QUERY_ONE_OF_VALUES) {
372
+ throw new RangeError(
373
+ `${label} one-of values must contain from 1 through ${MAX_QUERY_ONE_OF_VALUES} entries.`,
374
+ );
375
+ }
376
+ budget.filterValues += filter.values.length;
377
+ if (budget.filterValues > MAX_QUERY_FILTER_VALUES) {
378
+ throw new RangeError(
379
+ `Query filters may contain at most ${MAX_QUERY_FILTER_VALUES} scalar values.`,
380
+ );
381
+ }
382
+ return {
383
+ kind: "one-of",
384
+ path,
385
+ values: filter.values.map((candidate, valueIndex) =>
386
+ prepareEqualsValue(candidate, `${label} one-of value ${valueIndex + 1}`, budget, path)),
387
+ };
388
+ }
389
+ if (filter.kind !== "equals") {
390
+ throw new TypeError(`${label} must be an equals, exists, or one-of filter.`);
391
+ }
392
+ budget.filterValues += 1;
393
+ if (budget.filterValues > MAX_QUERY_FILTER_VALUES) {
394
+ throw new RangeError(
395
+ `Query filters may contain at most ${MAX_QUERY_FILTER_VALUES} scalar values.`,
396
+ );
397
+ }
398
+ return prepareEqualsValue(filter.value, `${label} value`, budget, path);
399
+ }
400
+
401
+ function prepareEqualsValue(
402
+ value: unknown,
403
+ label: string,
404
+ budget: QueryTextBudget,
405
+ path: PreparedMetadataPath,
406
+ ): PreparedEqualsFilter {
407
+ if (!isMetadataScalar(value)) {
408
+ throw new TypeError(`${label} must be a metadata scalar.`);
409
+ }
410
+ if (typeof value === "string") {
411
+ const bytes = boundedTextBytes(
412
+ value,
413
+ MAX_QUERY_TEXT_UTF8_BYTES,
414
+ label,
415
+ );
416
+ addQueryText(budget, bytes);
417
+ return {
418
+ kind: "equals",
419
+ path,
420
+ value,
421
+ normalizedValue: normalizedString(value),
422
+ };
423
+ }
424
+ return { kind: "equals", path, value };
425
+ }
426
+
427
+ function prepareSort(
428
+ value: QuerySort | undefined,
429
+ budget: QueryTextBudget,
430
+ ): PreparedQuerySort {
431
+ if (value === undefined) return { kind: "builtin", field: "path" };
432
+ if (value.kind === "metadata") {
433
+ return {
434
+ kind: "metadata",
435
+ path: prepareMetadataPath(value.path, "Query sort metadata path", budget),
436
+ };
437
+ }
438
+ if (
439
+ value.kind !== "builtin"
440
+ || !["title", "path", "inbound", "outbound"].includes(value.field)
441
+ ) {
442
+ throw new TypeError("Query sort must name a supported built-in field or metadata path.");
443
+ }
444
+ return value;
445
+ }
446
+
447
+ function prepareQueryOptions(options: QueryOptions): PreparedQueryOptions {
448
+ const rawFilters: unknown = options.filters ?? [];
449
+ if (!Array.isArray(rawFilters)) throw new TypeError("Query filters must be an array.");
450
+ if (rawFilters.length > MAX_QUERY_FILTERS) {
451
+ throw new RangeError(`Query filters may contain at most ${MAX_QUERY_FILTERS} entries.`);
452
+ }
453
+ const rawTags: unknown = options.tags ?? [];
454
+ if (!Array.isArray(rawTags)) throw new TypeError("Query tags must be an array.");
455
+ if (rawTags.length > MAX_QUERY_TAGS) {
456
+ throw new RangeError(`Query tags may contain at most ${MAX_QUERY_TAGS} entries.`);
457
+ }
458
+ if (options.direction !== undefined && options.direction !== "asc" && options.direction !== "desc") {
459
+ throw new TypeError("Query direction must be asc or desc.");
460
+ }
461
+ if (options.limit !== undefined && (!Number.isSafeInteger(options.limit) || options.limit < 0)) {
462
+ throw new RangeError("Query limit must be a non-negative safe integer.");
463
+ }
464
+
465
+ const budget: QueryTextBudget = { bytes: 0, filterValues: 0 };
466
+ const filters = rawFilters.map((filter, index) => prepareFilter(filter, index, budget));
467
+ const tags = new Set<string>();
468
+ for (const [index, value] of rawTags.entries()) {
469
+ if (typeof value !== "string") throw new TypeError(`Query tag ${index + 1} must be a string.`);
470
+ const bytes = boundedTextBytes(
471
+ value,
472
+ MAX_QUERY_TEXT_UTF8_BYTES,
473
+ `Query tag ${index + 1}`,
474
+ );
475
+ addQueryText(budget, bytes);
476
+ tags.add(normalizedTag(value));
477
+ }
478
+ const repositoryScopes = validateRepositoryScopeSelection(options.repositoryScopes ?? []);
479
+ for (const scope of repositoryScopes) {
480
+ addQueryText(budget, Buffer.byteLength(scope, "utf8"));
481
+ }
482
+ const sort = prepareSort(options.sort, budget);
483
+ return {
484
+ filters,
485
+ tags,
486
+ repositoryScopes: new Set(repositoryScopes),
487
+ sort,
488
+ direction: options.direction ?? "asc",
489
+ ...(options.limit === undefined ? {} : { limit: options.limit }),
490
+ };
491
+ }
492
+
493
+ /** Validate all metadata-query work bounds without scanning a vault. */
494
+ export function validateQueryOptions(options: QueryOptions = {}): void {
495
+ prepareQueryOptions(options);
496
+ }
497
+
498
+ function compareNormalizedText(left: string, right: string): number {
499
+ if (left < right) return -1;
500
+ if (left > right) return 1;
501
+ return 0;
502
+ }
503
+
504
+ function compareText(left: string, right: string): number {
505
+ return compareNormalizedText(normalizedString(left), normalizedString(right));
506
+ }
507
+
508
+ function canonicalMetadata(value: MetadataValue): string {
509
+ if (value === null) return "null";
510
+ if (typeof value === "boolean") return value ? "true" : "false";
511
+ if (typeof value === "number") return String(value);
512
+ if (typeof value === "string") return JSON.stringify(value);
513
+ if (isMetadataArray(value)) return `[${value.map(canonicalMetadata).join(",")}]`;
514
+ return `{${Object.keys(value)
515
+ .toSorted(compareText)
516
+ .map((key) => `${JSON.stringify(key)}:${canonicalMetadata(value[key] ?? null)}`)
517
+ .join(",")}}`;
518
+ }
519
+
520
+ type SortKey =
521
+ | { readonly found: false }
522
+ | {
523
+ readonly found: true;
524
+ readonly rank: number;
525
+ readonly number?: number;
526
+ readonly text?: string;
527
+ };
528
+
529
+ function metadataSortKey(value: MetadataValue): SortKey {
530
+ if (value === null) return { found: true, rank: 0 };
531
+ if (typeof value === "boolean") {
532
+ return { found: true, rank: 1, number: Number(value) };
533
+ }
534
+ if (typeof value === "number") return { found: true, rank: 2, number: value };
535
+ if (typeof value === "string") {
536
+ return { found: true, rank: 3, text: normalizedString(value) };
537
+ }
538
+ return {
539
+ found: true,
540
+ rank: isMetadataArray(value) ? 4 : 5,
541
+ text: normalizedString(canonicalMetadata(value)),
542
+ };
543
+ }
544
+
545
+ function rowSortKey(
546
+ row: QueryRow,
547
+ sort: PreparedQuerySort,
548
+ keyCache: MetadataKeyCache,
549
+ ): SortKey {
550
+ if (sort.kind === "metadata") {
551
+ const lookup = metadataAtPreparedPath(row.metadata, sort.path, keyCache);
552
+ return lookup.found ? metadataSortKey(lookup.value) : { found: false };
553
+ }
554
+ switch (sort.field) {
555
+ case "title":
556
+ return { found: true, rank: 3, text: normalizedString(row.title) };
557
+ case "path":
558
+ return { found: true, rank: 3, text: normalizedString(row.path) };
559
+ case "inbound":
560
+ return { found: true, rank: 2, number: row.inboundContextualCount };
561
+ case "outbound":
562
+ return { found: true, rank: 2, number: row.outboundContextualCount };
563
+ }
564
+ }
565
+
566
+ function compareSortKeys(
567
+ left: SortKey,
568
+ right: SortKey,
569
+ direction: QueryDirection,
570
+ ): number {
571
+ if (!left.found && !right.found) return 0;
572
+ if (!left.found) return 1;
573
+ if (!right.found) return -1;
574
+ const rank = left.rank - right.rank;
575
+ const compared = rank !== 0
576
+ ? rank
577
+ : left.number !== undefined && right.number !== undefined
578
+ ? left.number - right.number
579
+ : compareNormalizedText(left.text ?? "", right.text ?? "");
580
+ return direction === "desc" ? -compared : compared;
581
+ }
582
+
583
+ function queryRow(note: Note, connection: NoteConnections | undefined): QueryRow | null {
584
+ if (connection === undefined) return null;
585
+ return {
586
+ id: note.id,
587
+ path: note.path,
588
+ title: note.title,
589
+ aliases: note.aliases,
590
+ tags: note.tags,
591
+ properties: note.properties,
592
+ metadata: note.metadata,
593
+ summary: note.summary,
594
+ inboundContextualCount: connection.inboundContextualCount,
595
+ outboundContextualCount: connection.outboundContextualCount,
596
+ backlinks: connection.backlinks,
597
+ };
598
+ }
599
+
600
+ function matchesRepositoryScopes(
601
+ note: QueryableMetadata,
602
+ selected: ReadonlySet<string>,
603
+ ): boolean {
604
+ if (selected.size === 0) return true;
605
+ const authored = analyzeAuthoredRepositoryScopes(note.metadata);
606
+ return authored.present
607
+ && authored.valid
608
+ && authored.scopes.some((scope) => selected.has(scope));
609
+ }
610
+
611
+ /** Query content notes while enriching every result with the deterministic graph view. */
612
+ export function queryVault(
613
+ notes: readonly Note[],
614
+ analysis: VaultAnalysis,
615
+ options: QueryOptions = {},
616
+ ): readonly QueryRow[] {
617
+ const prepared = prepareQueryOptions(options);
618
+ const keyCache: MetadataKeyCache = new WeakMap();
619
+
620
+ const connections = new Map(
621
+ analysis.noteConnections.map((connection) => [connection.id, connection]),
622
+ );
623
+ const indexed = notes
624
+ .map((note, index) => ({ index, row: queryRow(note, connections.get(note.id)) }))
625
+ .filter((candidate): candidate is { readonly index: number; readonly row: QueryRow } =>
626
+ candidate.row !== null)
627
+ .filter(({ row }) => {
628
+ const stringCache = new Map<string, string>();
629
+ return prepared.filters.every((filter) =>
630
+ matchesFilter(row, filter, keyCache, stringCache))
631
+ && matchesTags(row, prepared.tags)
632
+ && matchesRepositoryScopes(row, prepared.repositoryScopes);
633
+ })
634
+ .map((candidate) => ({
635
+ ...candidate,
636
+ pathKey: normalizedString(candidate.row.path),
637
+ sortKey: rowSortKey(candidate.row, prepared.sort, keyCache),
638
+ }));
639
+
640
+ const sorted = indexed.toSorted((left, right) =>
641
+ compareSortKeys(left.sortKey, right.sortKey, prepared.direction)
642
+ || compareNormalizedText(left.pathKey, right.pathKey)
643
+ || left.index - right.index);
644
+ const rows = sorted.map(({ row }) => row);
645
+ return prepared.limit === undefined ? rows : rows.slice(0, prepared.limit);
646
+ }