@wirecat/cli-core 0.499.0 → 0.501.0
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/CHANGELOG.md +48 -0
- package/dist/calendar/mcp/index.d.ts +12 -12
- package/dist/cli/envelope.d.ts +11 -1
- package/dist/cli/envelope.js +15 -3
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/meeting-preparation-answer.d.ts +8 -0
- package/dist/cli/meeting-preparation-projects.d.ts +8 -0
- package/dist/cli/meeting-preparation-read.d.ts +8 -0
- package/dist/cli/meeting-series-inventory-read.d.ts +8 -0
- package/dist/cli/messenger/messages-search-command.js +4 -1
- package/dist/cli/messenger/search-answers.d.ts +3 -0
- package/dist/cli/messenger/search-answers.js +6 -0
- package/dist/cli/messenger/search-command.js +4 -1
- package/dist/domain/knowledge.d.ts +1 -1
- package/dist/domain/knowledge.js +1 -1
- package/dist/entities/fact.d.ts +15 -1
- package/dist/entities/fact.js +6 -1
- package/dist/entities/fields.d.ts +11 -0
- package/dist/entities/fields.js +11 -0
- package/dist/entities/index.d.ts +49 -1
- package/dist/entities/meeting-preparation.d.ts +8 -0
- package/dist/entities/meeting-series-inventory.d.ts +8 -0
- package/dist/entities/memory.d.ts +14 -0
- package/dist/entities/memory.js +6 -1
- package/dist/mcp/meeting-preparation.d.ts +8 -0
- package/dist/mcp/meeting-series-inventory.d.ts +8 -0
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/meetings/project-evidence-answer.d.ts +8 -0
- package/dist/search/total.d.ts +13 -0
- package/dist/search/total.js +14 -0
- package/dist/services/index.d.ts +2 -1
- package/dist/services/index.js +2 -1
- package/dist/services/messages-archive-search.js +13 -3
- package/dist/services/messages-discovery.d.ts +6 -0
- package/dist/services/messages-discovery.js +14 -11
- package/dist/services/messages-search.d.ts +5 -0
- package/dist/services/messages-search.js +22 -0
- package/dist/services/messages.d.ts +3 -0
- package/dist/services/notes-search.d.ts +14 -2
- package/dist/services/notes-search.js +101 -19
- package/dist/store/index.d.ts +1 -0
- package/dist/store/project-transfer/request.d.ts +1 -1
- package/dist/store/project-transfer/schema.d.ts +2 -2
- package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
- package/dist/store/queries.d.ts +2 -0
- package/dist/store/queries.js +1 -0
- package/dist/store/sqlite/evidence-sources.d.ts +23 -0
- package/dist/store/sqlite/evidence-sources.js +59 -0
- package/dist/store/sqlite/facts.d.ts +16 -0
- package/dist/store/sqlite/facts.js +39 -5
- package/dist/store/sqlite/lucene.d.ts +5 -0
- package/dist/store/sqlite/lucene.js +17 -0
- package/dist/store/sqlite/memories.d.ts +28 -0
- package/dist/store/sqlite/memories.js +62 -3
- package/dist/store/sqlite/note-search.d.ts +18 -0
- package/dist/store/sqlite/note-search.js +40 -4
- package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
- package/package.json +3 -3
|
@@ -118,6 +118,11 @@ export declare const coverageOf: (store: MessageStore, { scopeAccounts, selected
|
|
|
118
118
|
coverage: SearchCoverage;
|
|
119
119
|
}>;
|
|
120
120
|
export declare const searchLucene: (store: MessageStore, account: AccountKey, request: SearchQuery, messenger?: Partial<Pick<MessengerTraits, "savedChatId" | "app" | "history" | "provider">>) => Promise<SearchFound>;
|
|
121
|
+
/**
|
|
122
|
+
* How many messages the query matches, mail included, each counted no further than `cap + 1`; `undefined` when only
|
|
123
|
+
* the search itself could tell.
|
|
124
|
+
*/
|
|
125
|
+
export declare const countLucene: (store: MessageStore, account: AccountKey, request: SearchQuery, messenger: Partial<Pick<MessengerTraits, "savedChatId" | "app">>, cap: number) => Promise<number | undefined>;
|
|
121
126
|
export type StatsGrouping = "chat" | "sender" | "day" | "hour";
|
|
122
127
|
export interface StatsRow {
|
|
123
128
|
key: string;
|
|
@@ -366,6 +366,28 @@ export const searchLucene = async (store, account, request, messenger = {}) => {
|
|
|
366
366
|
: {}),
|
|
367
367
|
};
|
|
368
368
|
};
|
|
369
|
+
/**
|
|
370
|
+
* How many messages the query matches, mail included, each counted no further than `cap + 1`; `undefined` when only
|
|
371
|
+
* the search itself could tell.
|
|
372
|
+
*/
|
|
373
|
+
export const countLucene = async (store, account, request, messenger, cap) => {
|
|
374
|
+
const prepared = await prepareLucene(store, account, request, messenger);
|
|
375
|
+
if (!prepared.scopeAccounts.length)
|
|
376
|
+
return 0;
|
|
377
|
+
const count = (execution) => store.queries.countMatches(execution, cap);
|
|
378
|
+
const mailAccounts = prepared.scopeAccounts.filter(({ provider }) => provider === MAIL);
|
|
379
|
+
const messages = await count(prepared.execution);
|
|
380
|
+
let mail = 0;
|
|
381
|
+
if (mailAccounts.length)
|
|
382
|
+
try {
|
|
383
|
+
mail = await count({ ...prepared.execution, accounts: mailAccounts, corpus: "mail" });
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
if (!(request.kind !== "mail" && error instanceof CliError && error.code === "validation_error"))
|
|
387
|
+
throw error;
|
|
388
|
+
}
|
|
389
|
+
return messages === undefined || mail === undefined ? undefined : messages + mail;
|
|
390
|
+
};
|
|
369
391
|
/** The garbled spellings voice-note hits were found through, one entry per query word. */
|
|
370
392
|
const correctionsOf = (items) => {
|
|
371
393
|
const found = new Map();
|
|
@@ -3,6 +3,7 @@ import { type MessageLink } from "../domain/message-link.js";
|
|
|
3
3
|
import type { Deletion, Discussion, Id, Message, Page, Provider, WindowedMessage } from "../domain/models.js";
|
|
4
4
|
import { type After, type Download, type Sent } from "../ports/messenger.js";
|
|
5
5
|
import type { SkippedSource } from "../search/account-list.js";
|
|
6
|
+
import type { SearchTotal } from "../search/total.js";
|
|
6
7
|
import { type Operated } from "../sends/guarded.js";
|
|
7
8
|
import type { PermissionKey } from "../sends/permissions.js";
|
|
8
9
|
import type { AccountKey, ChatCompleteness, MatchPlace, StoredHit } from "../store/store.js";
|
|
@@ -104,6 +105,8 @@ export type FoundMessage = StoredHit & {
|
|
|
104
105
|
export interface SearchFound extends Page<FoundMessage> {
|
|
105
106
|
/** Where the next page starts, as `offset`; absent on the last page. */
|
|
106
107
|
nextOffset?: number;
|
|
108
|
+
/** How many messages match, every page together, as Elasticsearch's `hits.total`; set by the archive search. */
|
|
109
|
+
total?: SearchTotal;
|
|
107
110
|
refreshed?: SearchRefreshed;
|
|
108
111
|
server?: ServerSearched;
|
|
109
112
|
query?: QueryMetadata;
|
|
@@ -6,6 +6,7 @@ import type { AppIdentity } from "../ports/app.js";
|
|
|
6
6
|
import { type SkippedSource } from "../search/account-list.js";
|
|
7
7
|
import { type ResolvedNode } from "../search/lucene/resolved.js";
|
|
8
8
|
import { type Stemmer } from "../search/stem.js";
|
|
9
|
+
import { type SearchTotal } from "../search/total.js";
|
|
9
10
|
import type { Link, NearestNote, Note, NoteHit } from "../store/index.js";
|
|
10
11
|
import type { AccountKey, MessageStore } from "../store/store.js";
|
|
11
12
|
export interface NotesSearchRequest {
|
|
@@ -71,7 +72,8 @@ export declare const embedMail: (store: MessageStore, options?: NotesEmbedOption
|
|
|
71
72
|
command?: string;
|
|
72
73
|
}) => Promise<NotesEmbedded>;
|
|
73
74
|
/** Notes nearest in meaning to the query, best first; only notes already embedded can be found. */
|
|
74
|
-
export declare const nearestNotes: (store: MessageStore, query: string,
|
|
75
|
+
export declare const nearestNotes: (store: MessageStore, query: string, options: NearestOptions) => Promise<NearestNote[]>;
|
|
76
|
+
type NearestOptions = Omit<NotesEmbedOptions, "maxChunks"> & {
|
|
75
77
|
limit: number;
|
|
76
78
|
folderIds?: string[];
|
|
77
79
|
withInternal?: boolean;
|
|
@@ -79,7 +81,7 @@ export declare const nearestNotes: (store: MessageStore, query: string, { model:
|
|
|
79
81
|
command?: string;
|
|
80
82
|
/** The search's own, when another part of it loads the same model. */
|
|
81
83
|
embedders?: Pick<WarmEmbedders, "get">;
|
|
82
|
-
}
|
|
84
|
+
};
|
|
83
85
|
export interface FoundNote {
|
|
84
86
|
kind?: "document" | "note";
|
|
85
87
|
provider?: string;
|
|
@@ -120,6 +122,8 @@ export interface NotesFound {
|
|
|
120
122
|
/** Why meaning was not searched, when it was not. */
|
|
121
123
|
meaning_skipped?: string;
|
|
122
124
|
hits: FoundNote[];
|
|
125
|
+
/** How many notes match the query, every page together, as Elasticsearch's `hits.total`. */
|
|
126
|
+
total?: SearchTotal;
|
|
123
127
|
has_more: boolean;
|
|
124
128
|
next_offset?: number;
|
|
125
129
|
/** What the notes found link to, most linked first. */
|
|
@@ -173,6 +177,13 @@ export declare const noteSourceOfQuery: (query: string) => {
|
|
|
173
177
|
export declare const fieldsOfQuery: (root: QueryNode) => QueryNode | undefined;
|
|
174
178
|
/** The words and phrases the query asks for, for the meaning half to embed; none it excludes, no field. */
|
|
175
179
|
export declare const meaningWords: (root: QueryNode) => string;
|
|
180
|
+
/**
|
|
181
|
+
* The most notes a filter may hold for meaning to rank all of them, each by its best chunk. Above it, meaning ranks
|
|
182
|
+
* the nearest notes overall and keeps those inside the filter, so a narrow filter over a large store loses some. On
|
|
183
|
+
* the owner's store (1,454 documents, 23,308 chunks) scoring all of them took ~0.5 s against ~0.17 s for the
|
|
184
|
+
* nearest 50 overall, and the cost grows with chunks, so 2,000 notes keeps it under a second at that density.
|
|
185
|
+
*/
|
|
186
|
+
export declare const RANK_WITHIN = 2000;
|
|
176
187
|
/**
|
|
177
188
|
* `search notes`: words and stems, and meaning when the local model is there, merged by reciprocal rank;
|
|
178
189
|
* each hit says which half found it, and the answer lists what the notes found link to. Every field of the query and
|
|
@@ -199,3 +210,4 @@ export declare const searchNotesOf: (store: MessageStore, account: AccountKey, t
|
|
|
199
210
|
accounts?: string;
|
|
200
211
|
app?: AppIdentity;
|
|
201
212
|
}) => Promise<NotesFoundOf>;
|
|
213
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliError } from "@wirecat/cli-base";
|
|
1
|
+
import { CliError, isCliError } from "@wirecat/cli-base";
|
|
2
2
|
import { dateRange, parseLucene, timezoneOf } from "@wirecat/cli-base/query";
|
|
3
3
|
import { INTERNAL_NOTES } from "../domain/providers.js";
|
|
4
4
|
import { normalizeTag } from "../domain/tags.js";
|
|
@@ -10,6 +10,7 @@ import { validateFields } from "../search/lucene/registry.js";
|
|
|
10
10
|
import { hasStems } from "../search/lucene/resolved.js";
|
|
11
11
|
import { SEARCH_QUERY } from "../search/lucene/types.js";
|
|
12
12
|
import { createStemmer, DEFAULT_STEMMERS } from "../search/stem.js";
|
|
13
|
+
import { TOTAL_CAP, totalOf } from "../search/total.js";
|
|
13
14
|
import { openPictureEmbedderFor } from "./document-pictures.js";
|
|
14
15
|
import { documentSource } from "./document-source.js";
|
|
15
16
|
import { plainQueryWords, titleQuery, titleTiers } from "./notes-title-tier.js";
|
|
@@ -122,7 +123,17 @@ export const embedEvents = (store, options = {}) => embedChunks(store, store.cal
|
|
|
122
123
|
/** Embeds mail's chunks as notes' are embedded: same model and key, so search by meaning reads both. */
|
|
123
124
|
export const embedMail = (store, options = {}) => embedChunks(store, store.mail, options);
|
|
124
125
|
/** Notes nearest in meaning to the query, best first; only notes already embedded can be found. */
|
|
125
|
-
export const nearestNotes = async (store, query,
|
|
126
|
+
export const nearestNotes = async (store, query, options) => {
|
|
127
|
+
const { key, vector, floor } = await queryVector(store, query, options);
|
|
128
|
+
const nearest = await store.notes.nearest(key, vector, {
|
|
129
|
+
limit: options.limit,
|
|
130
|
+
...(options.folderIds ? { folderIds: options.folderIds } : {}),
|
|
131
|
+
...(options.withInternal ? { withInternal: true } : {}),
|
|
132
|
+
...(options.source ? { source: options.source } : {}),
|
|
133
|
+
});
|
|
134
|
+
return nearest.filter(({ score }) => score > floor);
|
|
135
|
+
};
|
|
136
|
+
const queryVector = async (store, query, { model: choice, folderIds, withInternal, source, env, threads, command, embedders }) => {
|
|
126
137
|
if (!query.trim())
|
|
127
138
|
throw new CliError("validation_error", "say what to find in the notes");
|
|
128
139
|
const { model, directory, key } = localModel(choice, env, command);
|
|
@@ -134,14 +145,12 @@ export const nearestNotes = async (store, query, { model: choice, limit, folderI
|
|
|
134
145
|
if (!(await store.notes.embedded(key, scope)))
|
|
135
146
|
throw new CliError("not_found", `nothing searched here has vectors of ${model.id} yet, so only words were matched`);
|
|
136
147
|
const [vector] = await withLocal(model, directory, threads, (embedder) => embedder.embed([query], "query"), embedders);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
144
|
-
return nearest.filter(({ score }) => score > floor);
|
|
148
|
+
return { key, vector: vector, floor: meaningFloor(key) };
|
|
149
|
+
};
|
|
150
|
+
/** Every note of `refs` nearer in meaning than the model's floor, best first. */
|
|
151
|
+
const nearestAmong = async (store, query, refs, options) => {
|
|
152
|
+
const { key, vector, floor } = await queryVector(store, query, options);
|
|
153
|
+
return (await store.notes.scoreNotes(key, vector, refs)).filter(({ score }) => score > floor);
|
|
145
154
|
};
|
|
146
155
|
const MAX_LINE = 200;
|
|
147
156
|
const MAX_NOTES_OFFSET = 1000;
|
|
@@ -289,6 +298,62 @@ const matching = async (store, refs, root, scope) => {
|
|
|
289
298
|
});
|
|
290
299
|
return new Map(found.items.map(({ ref, note, at }) => [ref, { note, at }]));
|
|
291
300
|
};
|
|
301
|
+
/**
|
|
302
|
+
* The most notes a filter may hold for meaning to rank all of them, each by its best chunk. Above it, meaning ranks
|
|
303
|
+
* the nearest notes overall and keeps those inside the filter, so a narrow filter over a large store loses some. On
|
|
304
|
+
* the owner's store (1,454 documents, 23,308 chunks) scoring all of them took ~0.5 s against ~0.17 s for the
|
|
305
|
+
* nearest 50 overall, and the cost grows with chunks, so 2,000 notes keeps it under a second at that density.
|
|
306
|
+
*/
|
|
307
|
+
export const RANK_WITHIN = 2000;
|
|
308
|
+
/** The notes of a filter, newest first, when there are at most `RANK_WITHIN`; `undefined` when more, or too costly. */
|
|
309
|
+
const filterRefs = async (store, narrowing, scope) => {
|
|
310
|
+
const resolved = await resolvedQuery(store, narrowing, scope.timezone);
|
|
311
|
+
try {
|
|
312
|
+
const found = await store.notes.matchingRefs({
|
|
313
|
+
root: resolved.root,
|
|
314
|
+
limit: RANK_WITHIN,
|
|
315
|
+
...(scope.folderIds ? { folderIds: scope.folderIds } : {}),
|
|
316
|
+
...(scope.withInternal ? { withInternal: true } : {}),
|
|
317
|
+
...(scope.source ? { source: scope.source } : {}),
|
|
318
|
+
...(resolved.stemmer ? { stemmer: resolved.stemmer } : {}),
|
|
319
|
+
});
|
|
320
|
+
return found.hasMore ? undefined : found.refs;
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (tooLarge(error))
|
|
324
|
+
return undefined;
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const tooLarge = (error) => isCliError(error) && error.details.reason === "query.too-large";
|
|
329
|
+
/**
|
|
330
|
+
* The notes the words find, with those found by meaning: every note the search would list, paged to the end. Never
|
|
331
|
+
* below what the page already showed; a count the store refuses as too costly is that lower bound.
|
|
332
|
+
*/
|
|
333
|
+
const notesTotal = async (store, text, { exact, scope, shown, hasMore, }, others, othersComplete) => {
|
|
334
|
+
let words;
|
|
335
|
+
try {
|
|
336
|
+
const { root, stemmer } = await storeQuery(store, text, {
|
|
337
|
+
exact,
|
|
338
|
+
...(scope.timezone ? { timezone: scope.timezone } : {}),
|
|
339
|
+
});
|
|
340
|
+
words = await store.notes.matchingRefs({
|
|
341
|
+
root,
|
|
342
|
+
limit: TOTAL_CAP,
|
|
343
|
+
...(scope.folderIds ? { folderIds: scope.folderIds } : {}),
|
|
344
|
+
...(scope.withInternal ? { withInternal: true } : {}),
|
|
345
|
+
...(scope.source ? { source: scope.source } : {}),
|
|
346
|
+
...(stemmer ? { stemmer } : {}),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (tooLarge(error))
|
|
351
|
+
return totalOf(undefined, shown, hasMore);
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
const count = new Set([...words.refs, ...others]).size + (words.hasMore ? 1 : 0);
|
|
355
|
+
return totalOf(count, shown, hasMore, othersComplete);
|
|
356
|
+
};
|
|
292
357
|
/**
|
|
293
358
|
* `search notes`: words and stems, and meaning when the local model is there, merged by reciprocal rank;
|
|
294
359
|
* each hit says which half found it, and the answer lists what the notes found link to. Every field of the query and
|
|
@@ -317,24 +382,39 @@ export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, f
|
|
|
317
382
|
};
|
|
318
383
|
let meaningSkipped;
|
|
319
384
|
let meaning;
|
|
385
|
+
/** Every note found by meaning; `meaningComplete` is false when the nearest overall were cut at the depth. */
|
|
386
|
+
let meaningRefs;
|
|
387
|
+
let meaningComplete = true;
|
|
320
388
|
if (exact)
|
|
321
389
|
meaningSkipped = "--exact searches words as written";
|
|
322
390
|
else if (words) {
|
|
391
|
+
const lane = {
|
|
392
|
+
command,
|
|
393
|
+
...(env === undefined ? {} : { env }),
|
|
394
|
+
...(embedders === undefined ? {} : { embedders }),
|
|
395
|
+
...(folderIds === undefined ? {} : { folderIds }),
|
|
396
|
+
...(withInternal ? { withInternal: true } : {}),
|
|
397
|
+
...(source === undefined ? {} : { source }),
|
|
398
|
+
};
|
|
399
|
+
const depth = Math.max(window, CANDIDATES);
|
|
323
400
|
try {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
401
|
+
const within = narrowing === undefined ? undefined : await filterRefs(store, narrowing, scope);
|
|
402
|
+
let nearest;
|
|
403
|
+
if (within === undefined) {
|
|
404
|
+
nearest = await nearestNotes(store, words, { ...lane, limit: depth });
|
|
405
|
+
meaningComplete = nearest.length < depth;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
const ranked = within.length ? await nearestAmong(store, words, within, lane) : [];
|
|
409
|
+
meaningRefs = new Set(ranked.map(({ ref }) => ref));
|
|
410
|
+
nearest = ranked.slice(0, depth);
|
|
411
|
+
}
|
|
333
412
|
const allowed = await matching(store, nearest.map(({ ref }) => ref), narrowing, scope);
|
|
334
413
|
meaning = nearest.flatMap(({ ref }) => {
|
|
335
414
|
const found = allowed.get(ref);
|
|
336
415
|
return found ? [{ ref, ...found }] : [];
|
|
337
416
|
});
|
|
417
|
+
meaningRefs ??= new Set(meaning.map(({ ref }) => ref));
|
|
338
418
|
}
|
|
339
419
|
catch (error) {
|
|
340
420
|
if (!(error instanceof CliError && error.code === "not_found"))
|
|
@@ -378,6 +458,7 @@ export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, f
|
|
|
378
458
|
];
|
|
379
459
|
const page = slice ? ordered : ordered.slice(offset, window);
|
|
380
460
|
const hasMore = slice ? matched.hasMore : ordered.length > window || matched.hasMore || titled.hasMore;
|
|
461
|
+
const total = await notesTotal(store, text, { exact, scope, shown: offset + page.length, hasMore }, [...tier.map(({ ref }) => ref), ...(meaningRefs ?? [])], meaningComplete);
|
|
381
462
|
const hits = [];
|
|
382
463
|
const counts = new Map();
|
|
383
464
|
const accounts = await store.accounts.storedAccounts();
|
|
@@ -441,6 +522,7 @@ export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, f
|
|
|
441
522
|
by: meaning === undefined ? "words" : "words and meaning",
|
|
442
523
|
...(meaningSkipped === undefined ? {} : { meaning_skipped: meaningSkipped }),
|
|
443
524
|
hits,
|
|
525
|
+
total,
|
|
444
526
|
has_more: hasMore,
|
|
445
527
|
...(hasMore && window <= MAX_NOTES_OFFSET ? { next_offset: window } : {}),
|
|
446
528
|
linked: [...counts.values()].sort((a, b) => b.notes - a.notes || (a.name ?? a.ref ?? "").localeCompare(b.name ?? b.ref ?? "")),
|
package/dist/store/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export { CATALOG_QUERY } from "./sqlite/catalog.js";
|
|
|
31
31
|
export type { ChatMetadata } from "./sqlite/chats.js";
|
|
32
32
|
export type { Email, EmailAddress, EmailAttachment, EmailFacts, EmailFilter, EmailInput, EmailInThread, EmailLocationInput, EmailRecipient, EmailRef, EmailThread, EmailVectorHit, FolderEmail, Mailbox, MailboxInput, MailFilter, MailStore, SenderScore, ThreadDetails, ThreadSave, } from "./sqlite/emails.js";
|
|
33
33
|
export { EVENT_FIELDS, type EventChunkToEmbed, type EventHit, type EventQuery, type EventSearch, type NearestEvent, } from "./sqlite/event-search.js";
|
|
34
|
+
export type { EvidenceSource } from "./sqlite/evidence-sources.js";
|
|
34
35
|
export type { Fact, FactInput, FactsStore } from "./sqlite/facts.js";
|
|
35
36
|
export type { FileListQuery, FilesStore, StoredFile } from "./sqlite/files.js";
|
|
36
37
|
export { IMPORTED_DOCUMENT_QUERY, type ImportedDocuments } from "./sqlite/imported-documents.js";
|
|
@@ -6,7 +6,7 @@ export declare const projectAnnotationRequest: (request: ProjectAnnotationTarget
|
|
|
6
6
|
ref: string;
|
|
7
7
|
type: "decision" | "fact" | "promise" | "question" | "statement";
|
|
8
8
|
statement: string;
|
|
9
|
-
status: "accepted" | "proposed" | "reversed" | "superseded";
|
|
9
|
+
status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
|
|
10
10
|
subject: string | null;
|
|
11
11
|
occurred_at: number | null;
|
|
12
12
|
supersedes: string | null;
|
|
@@ -25,7 +25,7 @@ export declare const projectAnnotationsSchema: v.StrictObjectSchema<{
|
|
|
25
25
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
26
26
|
readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
|
|
27
27
|
readonly statement: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
28
|
-
readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed"], undefined>;
|
|
28
|
+
readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed", "rejected"], undefined>;
|
|
29
29
|
readonly subject: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
30
30
|
readonly occurred_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
31
31
|
readonly supersedes: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
@@ -40,7 +40,7 @@ export declare const projectAnnotationsSchema: v.StrictObjectSchema<{
|
|
|
40
40
|
ref: string;
|
|
41
41
|
type: "decision" | "fact" | "promise" | "question" | "statement";
|
|
42
42
|
statement: string;
|
|
43
|
-
status: "accepted" | "proposed" | "reversed" | "superseded";
|
|
43
|
+
status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
|
|
44
44
|
subject: string | null;
|
|
45
45
|
occurred_at: number | null;
|
|
46
46
|
supersedes: string | null;
|
|
@@ -6,7 +6,7 @@ export declare const projectAnnotationSnapshotSchema: v.StrictObjectSchema<{
|
|
|
6
6
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
7
7
|
readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
|
|
8
8
|
readonly statement: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
9
|
-
readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed"], undefined>;
|
|
9
|
+
readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed", "rejected"], undefined>;
|
|
10
10
|
readonly subject: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
11
11
|
readonly occurred_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
12
12
|
readonly supersedes: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
@@ -21,7 +21,7 @@ export declare const projectAnnotationSnapshotSchema: v.StrictObjectSchema<{
|
|
|
21
21
|
ref: string;
|
|
22
22
|
type: "decision" | "fact" | "promise" | "question" | "statement";
|
|
23
23
|
statement: string;
|
|
24
|
-
status: "accepted" | "proposed" | "reversed" | "superseded";
|
|
24
|
+
status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
|
|
25
25
|
subject: string | null;
|
|
26
26
|
occurred_at: number | null;
|
|
27
27
|
supersedes: string | null;
|
package/dist/store/queries.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface QueriesStore {
|
|
|
13
13
|
matchQuery(execution: QueryExecution): Promise<Page<ScoredHit>>;
|
|
14
14
|
/** The same matches as `matchQuery`, each counted once, grouped by chat, sender or quarter hour. */
|
|
15
15
|
countQuery(execution: QueryExecution, by: QueryGrouping): Promise<QueryGroup[]>;
|
|
16
|
+
/** How many messages match, counted no further than `cap + 1`; `undefined` when only a full search could tell. */
|
|
17
|
+
countMatches(execution: QueryExecution, cap: number): Promise<number | undefined>;
|
|
16
18
|
conversationEligibility(execution: QueryExecution): Promise<ConversationEligibility>;
|
|
17
19
|
rankQuery(execution: QueryExecution, request: RankingRequest): Promise<RankedStoreFound>;
|
|
18
20
|
rankingEvidence(execution: QueryExecution, request: RankingEvidenceRequest): Promise<RankedEvidence>;
|
package/dist/store/queries.js
CHANGED
|
@@ -13,6 +13,7 @@ export const queriesStoreOver = (context, stemmerFor) => {
|
|
|
13
13
|
return {
|
|
14
14
|
matchQuery: async (execution) => lucene.matchQuery(context, mailIndexed(execution)),
|
|
15
15
|
countQuery: async (execution, by) => lucene.countQuery(context, mailIndexed(execution), by),
|
|
16
|
+
countMatches: async (execution, cap) => lucene.countMatches(context, mailIndexed(execution), cap),
|
|
16
17
|
conversationEligibility: async (execution) => conversationEligibility(context, execution),
|
|
17
18
|
rankQuery: async (execution, request) => rankQuery(context, execution, request),
|
|
18
19
|
rankingEvidence: async (execution, request) => rankingEvidence(context, execution, request),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CacheDatabase } from "../driver.js";
|
|
2
|
+
/** One reference a memory or fact rests on, read back whole enough to judge it without opening it. */
|
|
3
|
+
export interface EvidenceSource {
|
|
4
|
+
/** `null` when the row is gone from the store and its reference with it. */
|
|
5
|
+
ref: string | null;
|
|
6
|
+
kind: string;
|
|
7
|
+
title: string | null;
|
|
8
|
+
sender: string | null;
|
|
9
|
+
at: string | null;
|
|
10
|
+
excerpt: string | null;
|
|
11
|
+
/** The provider the source came through: `telegram`, `email`, `notes`, `zoom`… */
|
|
12
|
+
tool: string | null;
|
|
13
|
+
/** Deleted at its source since it was cited, or no longer in the store. */
|
|
14
|
+
missing: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Every `evidence` link from a memory or fact, in the order they were written, including one whose target was
|
|
18
|
+
* deleted since: `evidenceOf` drops those, so a reader would never learn the record lost a source.
|
|
19
|
+
*/
|
|
20
|
+
export declare const evidenceSourcesOf: (database: CacheDatabase, from: {
|
|
21
|
+
type: string;
|
|
22
|
+
id: number;
|
|
23
|
+
}) => EvidenceSource[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { linkKind } from "./link-kinds.js";
|
|
2
|
+
import { referenceOfThing, stateOfThing, storedThing } from "./things.js";
|
|
3
|
+
const EXCERPT_CHARS = 280;
|
|
4
|
+
const excerptOf = (text) => {
|
|
5
|
+
if (text == null)
|
|
6
|
+
return null;
|
|
7
|
+
const flat = String(text).replace(/\s+/g, " ").trim();
|
|
8
|
+
if (!flat)
|
|
9
|
+
return null;
|
|
10
|
+
return flat.length > EXCERPT_CHARS ? `${flat.slice(0, EXCERPT_CHARS - 1)}…` : flat;
|
|
11
|
+
};
|
|
12
|
+
const iso = (value) => (value == null ? null : new Date(Number(value)).toISOString());
|
|
13
|
+
const textOf = (value) => (value == null || String(value).trim() === "" ? null : String(value));
|
|
14
|
+
const READS = {
|
|
15
|
+
message: "SELECT c.title, coalesce(m.sender_name, i.name) AS sender, m.sent_at AS at, m.text AS body, a.provider AS tool " +
|
|
16
|
+
"FROM messages m JOIN chats c ON c.id = m.chat_id JOIN accounts a ON a.id = c.account_id " +
|
|
17
|
+
"LEFT JOIN identities i ON i.id = m.sender_identity_id WHERE m.id = ?",
|
|
18
|
+
email: "SELECT e.subject AS title, coalesce(e.from_name, e.from_address) AS sender, e.sent_at AS at, " +
|
|
19
|
+
"coalesce(e.body_text, e.snippet) AS body, 'email' AS tool FROM emails e WHERE e.id = ?",
|
|
20
|
+
note: "SELECT title, NULL AS sender, updated_at AS at, body, 'notes' AS tool FROM notes WHERE id = ?",
|
|
21
|
+
document: "SELECT d.title, NULL AS sender, coalesce(d.external_updated_at, d.updated_at) AS at, d.body, a.provider AS tool " +
|
|
22
|
+
"FROM documents d JOIN accounts a ON a.id = d.account_id WHERE d.id = ?",
|
|
23
|
+
meeting: "SELECT m.title, NULL AS sender, m.started_at AS at, m.description AS body, a.provider AS tool " +
|
|
24
|
+
"FROM meetings m JOIN accounts a ON a.id = m.account_id WHERE m.id = ?",
|
|
25
|
+
meeting_transcript: "SELECT m.title, NULL AS sender, m.started_at AS at, NULL AS body, a.provider AS tool FROM meeting_transcripts t " +
|
|
26
|
+
"JOIN meetings m ON m.id = t.meeting_id JOIN accounts a ON a.id = m.account_id WHERE t.id = ?",
|
|
27
|
+
meeting_transcript_row: "SELECT m.title, r.speaker_name AS sender, m.started_at + coalesce(r.start_ms, 0) AS at, r.text AS body, " +
|
|
28
|
+
"a.provider AS tool FROM meeting_transcript_rows r JOIN meeting_transcripts t ON t.id = r.meeting_transcript_id " +
|
|
29
|
+
"JOIN meetings m ON m.id = t.meeting_id JOIN accounts a ON a.id = m.account_id WHERE r.id = ?",
|
|
30
|
+
};
|
|
31
|
+
const KIND = {
|
|
32
|
+
meeting_transcript: "meeting",
|
|
33
|
+
meeting_transcript_row: "meeting",
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Every `evidence` link from a memory or fact, in the order they were written, including one whose target was
|
|
37
|
+
* deleted since: `evidenceOf` drops those, so a reader would never learn the record lost a source.
|
|
38
|
+
*/
|
|
39
|
+
export const evidenceSourcesOf = (database, from) => database
|
|
40
|
+
.prepare(`SELECT to_type, to_id FROM links WHERE from_type = ? AND from_id = ? AND kind = ${linkKind("evidence")} ` +
|
|
41
|
+
"AND to_id IS NOT NULL ORDER BY id")
|
|
42
|
+
.all(from.type, from.id)
|
|
43
|
+
.map((link) => {
|
|
44
|
+
const thing = storedThing(link.to_type, link.to_id);
|
|
45
|
+
const state = stateOfThing(database, thing);
|
|
46
|
+
const read = READS[thing.type];
|
|
47
|
+
const row = read === undefined || state === "unavailable" ? undefined : database.prepare(read).get(thing.id);
|
|
48
|
+
const gone = state !== "available";
|
|
49
|
+
return {
|
|
50
|
+
ref: referenceOfThing(database, thing) ?? null,
|
|
51
|
+
kind: KIND[thing.type] ?? thing.type,
|
|
52
|
+
title: textOf(row?.title),
|
|
53
|
+
sender: textOf(row?.sender),
|
|
54
|
+
at: iso(row?.at),
|
|
55
|
+
excerpt: gone ? null : excerptOf(row?.body),
|
|
56
|
+
tool: textOf(row?.tool),
|
|
57
|
+
missing: gone,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type FACT_STATUSES, FACT_TYPES, MEMORY_SCOPES } from "../../domain/knowledge.js";
|
|
2
2
|
import type { Actor } from "./actors.js";
|
|
3
|
+
import { type EvidenceSource } from "./evidence-sources.js";
|
|
3
4
|
import { type Author } from "./memories.js";
|
|
4
5
|
import type { StoreContext } from "./open.js";
|
|
5
6
|
/** What happened, as its evidence shows; its evidence is `links` of kind `evidence`. */
|
|
@@ -12,6 +13,8 @@ export interface Fact {
|
|
|
12
13
|
status: (typeof FACT_STATUSES)[number];
|
|
13
14
|
project: string | null;
|
|
14
15
|
subject: string | null;
|
|
16
|
+
subjectName: string | null;
|
|
17
|
+
subjectIsOwner: boolean;
|
|
15
18
|
occurredAt: string | null;
|
|
16
19
|
supersedes: string | null;
|
|
17
20
|
/** `name` is a bot's handle; `null` for a person. */
|
|
@@ -61,6 +64,19 @@ export interface FactsStore {
|
|
|
61
64
|
accept(reference: string): Promise<Fact>;
|
|
62
65
|
/** An accepted decision no longer holds and nothing replaced it. */
|
|
63
66
|
reverse(reference: string): Promise<Fact>;
|
|
67
|
+
/** The owner turns down a proposed fact; it stays, marked `rejected`. */
|
|
68
|
+
reject(reference: string): Promise<Fact>;
|
|
69
|
+
/**
|
|
70
|
+
* Erases the fact: its row, its links, its tags, and the memory it was migrated from, which still held its text.
|
|
71
|
+
* A decision that replaced it no longer names it. A backup made before keeps it.
|
|
72
|
+
*/
|
|
73
|
+
remove(reference: string): Promise<{
|
|
74
|
+
ref: string;
|
|
75
|
+
removed: true;
|
|
76
|
+
links: number;
|
|
77
|
+
}>;
|
|
78
|
+
/** Each reference it rests on, read back; one deleted since is `missing`. */
|
|
79
|
+
sources(reference: string): Promise<EvidenceSource[]>;
|
|
64
80
|
}
|
|
65
81
|
type FactOperations = {
|
|
66
82
|
[K in keyof FactsStore]: FactsStore[K] extends (...args: infer A) => Promise<infer R> ? (...args: A) => R : never;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { FACT_TYPES, MEMORY_SCOPES } from "../../domain/knowledge.js";
|
|
3
3
|
import { calendarMoment } from "../../domain/moment.js";
|
|
4
|
+
import { evidenceSourcesOf } from "./evidence-sources.js";
|
|
4
5
|
import { linkKind } from "./link-kinds.js";
|
|
5
|
-
import { authorActor, authorOf, evidenceOf, linkEvidence } from "./memories.js";
|
|
6
|
+
import { authorActor, authorOf, dropLinksOf, erase, evidenceOf, linkEvidence, subjectOf, } from "./memories.js";
|
|
6
7
|
import { requiredThing } from "./notes.js";
|
|
7
|
-
import { referenceOfThing, storedThing } from "./things.js";
|
|
8
8
|
import { write } from "./transaction.js";
|
|
9
9
|
const NAMED = "SELECT f.*, b.name AS author_name FROM facts f LEFT JOIN bots b ON f.author_type = 'bot' AND b.id = f.author_id";
|
|
10
10
|
const iso = (value) => new Date(Number(value)).toISOString();
|
|
@@ -22,9 +22,7 @@ export const factOperationsOver = (context) => {
|
|
|
22
22
|
statement: String(row.statement),
|
|
23
23
|
status: row.status,
|
|
24
24
|
project: row.project_id == null ? null : `project:${row.project_id}`,
|
|
25
|
-
|
|
26
|
-
? null
|
|
27
|
-
: (referenceOfThing(database, storedThing(row.subject_type, row.subject_id)) ?? null),
|
|
25
|
+
...subjectOf(database, row),
|
|
28
26
|
occurredAt: row.occurred_at == null ? null : iso(row.occurred_at),
|
|
29
27
|
supersedes: row.supersedes_id == null ? null : `fact:${row.supersedes_id}`,
|
|
30
28
|
author: authorOf(row),
|
|
@@ -125,8 +123,41 @@ export const factOperationsOver = (context) => {
|
|
|
125
123
|
database.prepare("UPDATE facts SET status = 'reversed', updated_at = ? WHERE id = ?").run(now(), Number(row.id));
|
|
126
124
|
return factOf(rowOf(reference));
|
|
127
125
|
},
|
|
126
|
+
reject: (reference) => {
|
|
127
|
+
const row = rowOf(reference);
|
|
128
|
+
if (row.status !== "proposed")
|
|
129
|
+
throw new CliError("validation_error", `only a proposed fact can be rejected; this one is ${String(row.status)}`);
|
|
130
|
+
database.prepare("UPDATE facts SET status = 'rejected', updated_at = ? WHERE id = ?").run(now(), Number(row.id));
|
|
131
|
+
return factOf(rowOf(reference));
|
|
132
|
+
},
|
|
133
|
+
remove: (reference) => {
|
|
134
|
+
const row = rowOf(reference);
|
|
135
|
+
const id = Number(row.id);
|
|
136
|
+
const twin = row.source === "memory" && row.metadata != null ? migratedFrom(String(row.metadata)) : undefined;
|
|
137
|
+
const links = erase(database, () => {
|
|
138
|
+
database.prepare("UPDATE facts SET supersedes_id = NULL WHERE supersedes_id = ?").run(id);
|
|
139
|
+
const count = dropLinksOf(database, { type: "fact", id });
|
|
140
|
+
database.prepare("DELETE FROM links WHERE from_type = 'fact' AND from_id = ?").run(id);
|
|
141
|
+
database.prepare("DELETE FROM taggings WHERE taggable_type = 'fact' AND taggable_id = ?").run(id);
|
|
142
|
+
database.prepare("DELETE FROM facts WHERE id = ?").run(id);
|
|
143
|
+
if (twin !== undefined)
|
|
144
|
+
database.prepare("DELETE FROM memories WHERE id = ? AND deleted_at IS NOT NULL").run(twin);
|
|
145
|
+
return count;
|
|
146
|
+
});
|
|
147
|
+
return { ref: `fact:${id}`, removed: true, links };
|
|
148
|
+
},
|
|
149
|
+
sources: (reference) => evidenceSourcesOf(database, { type: "fact", id: Number(rowOf(reference).id) }),
|
|
128
150
|
};
|
|
129
151
|
};
|
|
152
|
+
const migratedFrom = (metadata) => {
|
|
153
|
+
try {
|
|
154
|
+
const { memory } = JSON.parse(metadata);
|
|
155
|
+
return Number.isInteger(memory) ? Number(memory) : undefined;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
130
161
|
export const factsStoreOver = (context) => {
|
|
131
162
|
const operations = factOperationsOver(context);
|
|
132
163
|
return {
|
|
@@ -135,5 +166,8 @@ export const factsStoreOver = (context) => {
|
|
|
135
166
|
list: async (options) => operations.list(options),
|
|
136
167
|
accept: async (reference) => operations.accept(reference),
|
|
137
168
|
reverse: async (reference) => operations.reverse(reference),
|
|
169
|
+
reject: async (reference) => operations.reject(reference),
|
|
170
|
+
remove: async (reference) => operations.remove(reference),
|
|
171
|
+
sources: async (reference) => operations.sources(reference),
|
|
138
172
|
};
|
|
139
173
|
};
|
|
@@ -18,6 +18,11 @@ export declare const matchQuery: (context: StoreContext, execution: QueryExecuti
|
|
|
18
18
|
export declare const countQuery: (context: StoreContext, execution: QueryExecution, by: QueryGrouping) => Promise<QueryGroup[]>;
|
|
19
19
|
/** All matching message keys, without materializing message bodies or imposing a hit-page limit. */
|
|
20
20
|
export declare const queryMessagePks: (context: StoreContext, execution: QueryExecution) => Promise<number[]>;
|
|
21
|
+
/**
|
|
22
|
+
* How many messages match, counting no further than `cap + 1`: one SQL count, unranked. `undefined` when a leaf is
|
|
23
|
+
* checked row by row (a preset, a pattern), where a count would cost as much as the search itself.
|
|
24
|
+
*/
|
|
25
|
+
export declare const countMatches: (context: StoreContext, execution: QueryExecution, cap: number) => number | undefined;
|
|
21
26
|
interface QuerySelection {
|
|
22
27
|
sql: string;
|
|
23
28
|
params: SqlValue[];
|
|
@@ -48,6 +48,23 @@ export const matchQuery = (context, execution) => runQuery(context, execution);
|
|
|
48
48
|
export const countQuery = (context, execution, by) => runQuery(context, execution, by);
|
|
49
49
|
/** All matching message keys, without materializing message bodies or imposing a hit-page limit. */
|
|
50
50
|
export const queryMessagePks = (context, execution) => runQuery(context, execution, "pks");
|
|
51
|
+
/**
|
|
52
|
+
* How many messages match, counting no further than `cap + 1`: one SQL count, unranked. `undefined` when a leaf is
|
|
53
|
+
* checked row by row (a preset, a pattern), where a count would cost as much as the search itself.
|
|
54
|
+
*/
|
|
55
|
+
export const countMatches = (context, execution, cap) => {
|
|
56
|
+
const { database, check, where, from, withRanking, ranked, leaves } = compileQuery(context, {
|
|
57
|
+
...execution,
|
|
58
|
+
newest: true,
|
|
59
|
+
});
|
|
60
|
+
if (leaves.some(({ test }) => test))
|
|
61
|
+
return undefined;
|
|
62
|
+
const row = database
|
|
63
|
+
.prepare(`SELECT count(*) AS n FROM (${withRanking}SELECT m.id ${from} WHERE ${where.sql} LIMIT ?)`)
|
|
64
|
+
.get(...ranked.params, ...where.params, cap + 1);
|
|
65
|
+
check();
|
|
66
|
+
return Number(row?.n ?? 0);
|
|
67
|
+
};
|
|
51
68
|
const compileQuery = (context, execution, boundedAttachments = false) => {
|
|
52
69
|
const { database } = context;
|
|
53
70
|
const corpus = execution.corpus === "mail" ? MAIL : MESSAGES;
|