@wirecat/cli-core 0.500.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/calendar/mcp/index.d.ts +12 -12
  3. package/dist/cli/envelope.d.ts +11 -1
  4. package/dist/cli/envelope.js +15 -3
  5. package/dist/cli/index.d.ts +1 -1
  6. package/dist/cli/index.js +1 -1
  7. package/dist/cli/meeting-preparation-answer.d.ts +8 -0
  8. package/dist/cli/meeting-preparation-projects.d.ts +8 -0
  9. package/dist/cli/meeting-preparation-read.d.ts +8 -0
  10. package/dist/cli/meeting-series-inventory-read.d.ts +8 -0
  11. package/dist/cli/messenger/messages-search-command.js +4 -1
  12. package/dist/cli/messenger/search-answers.d.ts +3 -0
  13. package/dist/cli/messenger/search-answers.js +6 -0
  14. package/dist/cli/messenger/search-command.js +4 -1
  15. package/dist/entities/index.d.ts +24 -0
  16. package/dist/entities/meeting-preparation.d.ts +8 -0
  17. package/dist/entities/meeting-series-inventory.d.ts +8 -0
  18. package/dist/mcp/meeting-preparation.d.ts +8 -0
  19. package/dist/mcp/meeting-series-inventory.d.ts +8 -0
  20. package/dist/meetings/mcp/index.d.ts +15 -15
  21. package/dist/meetings/project-evidence-answer.d.ts +8 -0
  22. package/dist/search/total.d.ts +13 -0
  23. package/dist/search/total.js +14 -0
  24. package/dist/services/index.d.ts +2 -1
  25. package/dist/services/index.js +2 -1
  26. package/dist/services/messages-archive-search.js +13 -3
  27. package/dist/services/messages-discovery.d.ts +6 -0
  28. package/dist/services/messages-discovery.js +14 -11
  29. package/dist/services/messages-search.d.ts +5 -0
  30. package/dist/services/messages-search.js +22 -0
  31. package/dist/services/messages.d.ts +3 -0
  32. package/dist/services/notes-search.d.ts +14 -2
  33. package/dist/services/notes-search.js +101 -19
  34. package/dist/store/queries.d.ts +2 -0
  35. package/dist/store/queries.js +1 -0
  36. package/dist/store/sqlite/lucene.d.ts +5 -0
  37. package/dist/store/sqlite/lucene.js +17 -0
  38. package/dist/store/sqlite/note-search.d.ts +18 -0
  39. package/dist/store/sqlite/note-search.js +40 -4
  40. package/package.json +3 -3
@@ -38,6 +38,7 @@ export declare const meetingTools: ({
38
38
  max_read_bytes?: undefined;
39
39
  cursor?: undefined;
40
40
  } | {
41
+ limit?: undefined;
41
42
  event_id?: undefined;
42
43
  series_id?: undefined;
43
44
  page?: undefined;
@@ -53,11 +54,11 @@ export declare const meetingTools: ({
53
54
  format?: undefined;
54
55
  transcript_id?: undefined;
55
56
  query?: undefined;
56
- limit?: undefined;
57
57
  scan_limit?: undefined;
58
58
  max_read_bytes?: undefined;
59
59
  cursor?: undefined;
60
60
  } | {
61
+ limit?: undefined;
61
62
  event_id?: undefined;
62
63
  series_id?: undefined;
63
64
  page?: undefined;
@@ -75,11 +76,11 @@ export declare const meetingTools: ({
75
76
  format?: undefined;
76
77
  transcript_id?: undefined;
77
78
  query?: undefined;
78
- limit?: undefined;
79
79
  scan_limit?: undefined;
80
80
  max_read_bytes?: undefined;
81
81
  cursor?: undefined;
82
82
  } | {
83
+ limit?: undefined;
83
84
  event_id?: undefined;
84
85
  series_id?: undefined;
85
86
  page?: undefined;
@@ -101,7 +102,6 @@ export declare const meetingTools: ({
101
102
  minimum: number;
102
103
  };
103
104
  query?: undefined;
104
- limit?: undefined;
105
105
  scan_limit?: undefined;
106
106
  max_read_bytes?: undefined;
107
107
  cursor?: undefined;
@@ -188,6 +188,7 @@ export declare const meetingTools: ({
188
188
  max_read_bytes?: undefined;
189
189
  cursor?: undefined;
190
190
  } | {
191
+ limit?: undefined;
191
192
  event_id?: undefined;
192
193
  series_id?: undefined;
193
194
  page?: undefined;
@@ -203,11 +204,11 @@ export declare const meetingTools: ({
203
204
  format?: undefined;
204
205
  transcript_id?: undefined;
205
206
  query?: undefined;
206
- limit?: undefined;
207
207
  scan_limit?: undefined;
208
208
  max_read_bytes?: undefined;
209
209
  cursor?: undefined;
210
210
  } | {
211
+ limit?: undefined;
211
212
  event_id?: undefined;
212
213
  series_id?: undefined;
213
214
  page?: undefined;
@@ -225,11 +226,11 @@ export declare const meetingTools: ({
225
226
  format?: undefined;
226
227
  transcript_id?: undefined;
227
228
  query?: undefined;
228
- limit?: undefined;
229
229
  scan_limit?: undefined;
230
230
  max_read_bytes?: undefined;
231
231
  cursor?: undefined;
232
232
  } | {
233
+ limit?: undefined;
233
234
  event_id?: undefined;
234
235
  series_id?: undefined;
235
236
  page?: undefined;
@@ -251,7 +252,6 @@ export declare const meetingTools: ({
251
252
  minimum: number;
252
253
  };
253
254
  query?: undefined;
254
- limit?: undefined;
255
255
  scan_limit?: undefined;
256
256
  max_read_bytes?: undefined;
257
257
  cursor?: undefined;
@@ -338,6 +338,7 @@ export declare const meetingTools: ({
338
338
  max_read_bytes?: undefined;
339
339
  cursor?: undefined;
340
340
  } | {
341
+ limit?: undefined;
341
342
  event_id?: undefined;
342
343
  series_id?: undefined;
343
344
  page?: undefined;
@@ -353,11 +354,11 @@ export declare const meetingTools: ({
353
354
  format?: undefined;
354
355
  transcript_id?: undefined;
355
356
  query?: undefined;
356
- limit?: undefined;
357
357
  scan_limit?: undefined;
358
358
  max_read_bytes?: undefined;
359
359
  cursor?: undefined;
360
360
  } | {
361
+ limit?: undefined;
361
362
  event_id?: undefined;
362
363
  series_id?: undefined;
363
364
  page?: undefined;
@@ -375,11 +376,11 @@ export declare const meetingTools: ({
375
376
  format?: undefined;
376
377
  transcript_id?: undefined;
377
378
  query?: undefined;
378
- limit?: undefined;
379
379
  scan_limit?: undefined;
380
380
  max_read_bytes?: undefined;
381
381
  cursor?: undefined;
382
382
  } | {
383
+ limit?: undefined;
383
384
  event_id?: undefined;
384
385
  series_id?: undefined;
385
386
  page?: undefined;
@@ -401,7 +402,6 @@ export declare const meetingTools: ({
401
402
  minimum: number;
402
403
  };
403
404
  query?: undefined;
404
- limit?: undefined;
405
405
  scan_limit?: undefined;
406
406
  max_read_bytes?: undefined;
407
407
  cursor?: undefined;
@@ -488,6 +488,7 @@ export declare const meetingTools: ({
488
488
  max_read_bytes?: undefined;
489
489
  cursor?: undefined;
490
490
  } | {
491
+ limit?: undefined;
491
492
  event_id?: undefined;
492
493
  series_id?: undefined;
493
494
  page?: undefined;
@@ -503,11 +504,11 @@ export declare const meetingTools: ({
503
504
  format?: undefined;
504
505
  transcript_id?: undefined;
505
506
  query?: undefined;
506
- limit?: undefined;
507
507
  scan_limit?: undefined;
508
508
  max_read_bytes?: undefined;
509
509
  cursor?: undefined;
510
510
  } | {
511
+ limit?: undefined;
511
512
  event_id?: undefined;
512
513
  series_id?: undefined;
513
514
  page?: undefined;
@@ -525,11 +526,11 @@ export declare const meetingTools: ({
525
526
  format?: undefined;
526
527
  transcript_id?: undefined;
527
528
  query?: undefined;
528
- limit?: undefined;
529
529
  scan_limit?: undefined;
530
530
  max_read_bytes?: undefined;
531
531
  cursor?: undefined;
532
532
  } | {
533
+ limit?: undefined;
533
534
  event_id?: undefined;
534
535
  series_id?: undefined;
535
536
  page?: undefined;
@@ -551,7 +552,6 @@ export declare const meetingTools: ({
551
552
  minimum: number;
552
553
  };
553
554
  query?: undefined;
554
- limit?: undefined;
555
555
  scan_limit?: undefined;
556
556
  max_read_bytes?: undefined;
557
557
  cursor?: undefined;
@@ -638,6 +638,7 @@ export declare const meetingTools: ({
638
638
  max_read_bytes?: undefined;
639
639
  cursor?: undefined;
640
640
  } | {
641
+ limit?: undefined;
641
642
  event_id?: undefined;
642
643
  series_id?: undefined;
643
644
  page?: undefined;
@@ -653,11 +654,11 @@ export declare const meetingTools: ({
653
654
  format?: undefined;
654
655
  transcript_id?: undefined;
655
656
  query?: undefined;
656
- limit?: undefined;
657
657
  scan_limit?: undefined;
658
658
  max_read_bytes?: undefined;
659
659
  cursor?: undefined;
660
660
  } | {
661
+ limit?: undefined;
661
662
  event_id?: undefined;
662
663
  series_id?: undefined;
663
664
  page?: undefined;
@@ -675,11 +676,11 @@ export declare const meetingTools: ({
675
676
  format?: undefined;
676
677
  transcript_id?: undefined;
677
678
  query?: undefined;
678
- limit?: undefined;
679
679
  scan_limit?: undefined;
680
680
  max_read_bytes?: undefined;
681
681
  cursor?: undefined;
682
682
  } | {
683
+ limit?: undefined;
683
684
  event_id?: undefined;
684
685
  series_id?: undefined;
685
686
  page?: undefined;
@@ -701,7 +702,6 @@ export declare const meetingTools: ({
701
702
  minimum: number;
702
703
  };
703
704
  query?: undefined;
704
- limit?: undefined;
705
705
  scan_limit?: undefined;
706
706
  max_read_bytes?: undefined;
707
707
  cursor?: undefined;
@@ -60,11 +60,19 @@ export declare const meetingProjectEvidence: v.StrictObjectSchema<{
60
60
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
61
61
  readonly has_more: v.BooleanSchema<undefined>;
62
62
  readonly next_page: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 2, undefined>]>, undefined>;
63
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
64
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
65
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
66
+ }, undefined>, undefined>;
63
67
  }, undefined>, v.StrictObjectSchema<{
64
68
  readonly mode: v.LiteralSchema<"cursor", undefined>;
65
69
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
66
70
  readonly has_more: v.BooleanSchema<undefined>;
67
71
  readonly next_cursor: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
72
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
73
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
74
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
75
+ }, undefined>, undefined>;
68
76
  }, undefined>], undefined>;
69
77
  readonly coverage: v.StrictObjectSchema<{
70
78
  readonly archive_completeness: v.LiteralSchema<"unknown", undefined>;
@@ -0,0 +1,13 @@
1
+ /** How many records match a search, every page together, shaped as Elasticsearch's `hits.total`. */
2
+ export interface SearchTotal {
3
+ value: number;
4
+ /** `eq`: the exact count. `gte`: at least `value`; counting stopped at the cap or could not be done cheaply. */
5
+ relation: "eq" | "gte";
6
+ }
7
+ /** Matches counted at most, as Elasticsearch's default `track_total_hits`; past it the total is `gte`. */
8
+ export declare const TOTAL_CAP = 10000;
9
+ /**
10
+ * A total from a count capped at `TOTAL_CAP` (`undefined` when it could not be counted), never below what the pages
11
+ * already showed: `shown` hits, and one more when `hasMore`.
12
+ */
13
+ export declare const totalOf: (count: number | undefined, shown: number, hasMore: boolean, exact?: boolean) => SearchTotal;
@@ -0,0 +1,14 @@
1
+ /** Matches counted at most, as Elasticsearch's default `track_total_hits`; past it the total is `gte`. */
2
+ export const TOTAL_CAP = 10_000;
3
+ /**
4
+ * A total from a count capped at `TOTAL_CAP` (`undefined` when it could not be counted), never below what the pages
5
+ * already showed: `shown` hits, and one more when `hasMore`.
6
+ */
7
+ export const totalOf = (count, shown, hasMore, exact = true) => {
8
+ const least = shown + (hasMore ? 1 : 0);
9
+ if (count === undefined || count < least)
10
+ return { value: least, relation: "gte" };
11
+ if (count > TOTAL_CAP)
12
+ return { value: TOTAL_CAP, relation: "gte" };
13
+ return { value: count, relation: exact ? "eq" : "gte" };
14
+ };
@@ -142,6 +142,7 @@ export declare const servicesFor: (deps: ServiceDeps) => Services;
142
142
  export { entriesOfList, type ListContext, type ListedEntry } from "../search/account-list.js";
143
143
  export { FIELD_VERSION, QUERY_FIELDS, QUERY_OPERATORS, validateAst } from "../search/lucene/registry.js";
144
144
  export { QUERY_LIMITS, QUERY_VERSION } from "../search/lucene/types.js";
145
+ export { type SearchTotal, TOTAL_CAP } from "../search/total.js";
145
146
  export { TASK_KINDS, TASK_ORIGINS, type TaskKind, type TaskOrigin } from "../tasks/index.js";
146
147
  export type { AdminFound, AdminQuery, AdminSelection, AdminStatisticsService } from "./admin-statistics.js";
147
148
  export { adminStatisticsService } from "./admin-statistics.js";
@@ -170,7 +171,7 @@ export type { MeaningLane, MeaningSkipped } from "./messages-discovery.js";
170
171
  export { meaningLane } from "./messages-meaning.js";
171
172
  export type { QueryMetadata, SearchCoverage } from "./messages-search.js";
172
173
  export { metadataService } from "./metadata.js";
173
- export { embedEvents, embedMail, embedNotes, type FoundNote, type LinkedRecord, type NotesEmbedded, type NotesEmbedOptions, type NotesFound, type NotesFoundOf, type NotesQuery, type NotesSearchRequest, nearestNotes, searchNotes, searchNotesOf, searchNotesQuery, storeQuery, } from "./notes-search.js";
174
+ export { embedEvents, embedMail, embedNotes, type FoundNote, type LinkedRecord, type NotesEmbedded, type NotesEmbedOptions, type NotesFound, type NotesFoundOf, type NotesQuery, type NotesSearchRequest, nearestNotes, RANK_WITHIN, searchNotes, searchNotesOf, searchNotesQuery, storeQuery, } from "./notes-search.js";
174
175
  export { type PackageUpgradeOutcome, type PackageUpgradePorts, type PackageUpgradeResult, type ServerRestarts, upgradePackage, } from "./package-upgrade.js";
175
176
  export { type PersonMeetingContext, type PersonMeetingContextItem, type PersonMeetingContextOptions, type PersonMeetingReadStore, personMeetingContext, } from "./person-meeting-context.js";
176
177
  export { privatePeopleService } from "./private-people.js";
@@ -107,6 +107,7 @@ export const servicesFor = (deps) => {
107
107
  export { entriesOfList } from "../search/account-list.js";
108
108
  export { FIELD_VERSION, QUERY_FIELDS, QUERY_OPERATORS, validateAst } from "../search/lucene/registry.js";
109
109
  export { QUERY_LIMITS, QUERY_VERSION } from "../search/lucene/types.js";
110
+ export { TOTAL_CAP } from "../search/total.js";
110
111
  export { TASK_KINDS, TASK_ORIGINS } from "../tasks/index.js";
111
112
  export { adminStatisticsService } from "./admin-statistics.js";
112
113
  export { GAP_BOUNDS, gapsService } from "./archive-gaps.js";
@@ -128,7 +129,7 @@ export { proposeMeetingTask } from "./meeting-task-proposal.js";
128
129
  export { createTaskFromMeeting, readMeetingTask, } from "./meeting-tasks.js";
129
130
  export { meaningLane } from "./messages-meaning.js";
130
131
  export { metadataService } from "./metadata.js";
131
- export { embedEvents, embedMail, embedNotes, nearestNotes, searchNotes, searchNotesOf, searchNotesQuery, storeQuery, } from "./notes-search.js";
132
+ export { embedEvents, embedMail, embedNotes, nearestNotes, RANK_WITHIN, searchNotes, searchNotesOf, searchNotesQuery, storeQuery, } from "./notes-search.js";
132
133
  export { upgradePackage, } from "./package-upgrade.js";
133
134
  export { personMeetingContext, } from "./person-meeting-context.js";
134
135
  export { privatePeopleService } from "./private-people.js";
@@ -1,7 +1,8 @@
1
- import { CliError } from "@wirecat/cli-base";
1
+ import { CliError, isCliError } from "@wirecat/cli-base";
2
2
  import { parseLocator } from "../domain/locator.js";
3
- import { searchDiscovery } from "./messages-discovery.js";
4
- import { searchLucene, statsLucene } from "./messages-search.js";
3
+ import { TOTAL_CAP, totalOf } from "../search/total.js";
4
+ import { discoveryRequest, searchDiscovery } from "./messages-discovery.js";
5
+ import { countLucene, searchLucene, statsLucene } from "./messages-search.js";
5
6
  import { readThreadContext, threadBounds } from "./thread-context.js";
6
7
  /** How long a search may spend building the word index, then the stems, first (phase 2 plan S3: about 200 ms). */
7
8
  export const SEARCH_FILL_MS = 200;
@@ -48,8 +49,17 @@ export const searchStore = async (store, account, request, messenger = {}, meani
48
49
  if (!Number.isInteger(offset) || offset < 0 || offset > MAX_SEARCH_OFFSET)
49
50
  throw new CliError("validation_error", `offset takes a whole number from 0 to ${MAX_SEARCH_OFFSET}`);
50
51
  const found = await searchPage(store, account, { ...request, limit: request.limit + offset }, messenger, meaning, rerank);
52
+ const discovery = found.query?.discovery;
53
+ const total = discovery
54
+ ? totalOf(discovery.candidates, found.items.length, found.hasMore, !discovery.truncated)
55
+ : totalOf(await countLucene(store, account, { ...(request.discover ? discoveryRequest(request) : request), limit: request.limit + offset }, messenger, TOTAL_CAP).catch((error) => {
56
+ if (isCliError(error) && error.details.reason === "query.too-large")
57
+ return undefined;
58
+ throw error;
59
+ }), found.items.length, found.hasMore);
51
60
  return {
52
61
  ...found,
62
+ total,
53
63
  items: found.items.slice(offset),
54
64
  ...(found.hasMore && offset + request.limit <= MAX_SEARCH_OFFSET ? { nextOffset: offset + request.limit } : {}),
55
65
  };
@@ -55,6 +55,12 @@ export type MeaningLane = (question: string, accounts: AccountKey[], embedders?:
55
55
  } | {
56
56
  skipped: MeaningSkipped;
57
57
  }>;
58
+ /**
59
+ * The query discovery searches. A question's own "?" would parse as a one-character wildcard and send it to strict
60
+ * search (BUG-395); the colon of a time or ratio ("22:00", "1:1") would parse as a field and fail the whole search,
61
+ * and escaped it is syntax again, so it splits the numbers (BUG-450). A quoted timestamp keeps its colons (BUG-458).
62
+ */
63
+ export declare const discoveryRequest: (asked: SearchQuery) => SearchQuery;
58
64
  export declare const searchDiscovery: (store: MessageStore, account: AccountKey, asked: SearchQuery, messenger?: Partial<Pick<MessengerTraits, "savedChatId" | "app" | "history" | "provider">>, meaning?: MeaningLane, rerank?: RerankLane) => Promise<SearchFound>;
59
65
  /** The nearest messages the question's filters allow, as hits, nearest first. */
60
66
  export declare const nearEligible: (store: MessageStore, account: AccountKey, request: SearchQuery, messenger: Partial<Pick<MessengerTraits, "savedChatId" | "app" | "history" | "provider">>, ranked: MeaningRanked, scope: string) => Promise<(FoundMessage & {
@@ -28,18 +28,21 @@ const FOLLOW_UPS_PER_HIT = 2;
28
28
  const RARE_AT = 3;
29
29
  /** Later messages read per hit before the search's filters; the latest that pass are kept. */
30
30
  const FOLLOW_UP_CANDIDATES = 20;
31
+ /**
32
+ * The query discovery searches. A question's own "?" would parse as a one-character wildcard and send it to strict
33
+ * search (BUG-395); the colon of a time or ratio ("22:00", "1:1") would parse as a field and fail the whole search,
34
+ * and escaped it is syntax again, so it splits the numbers (BUG-450). A quoted timestamp keeps its colons (BUG-458).
35
+ */
36
+ export const discoveryRequest = (asked) => asked.ast !== undefined || asked.exact || !asked.text
37
+ ? asked
38
+ : {
39
+ ...asked,
40
+ text: asked.text
41
+ .replace(/(?<=\p{L})\?(?=\s|$)/gu, "")
42
+ .replace(/"(?:[^"\\]|\\.)*"|(?<=\d):(?=\d)/gu, (found) => (found === ":" ? " " : found)),
43
+ };
31
44
  export const searchDiscovery = async (store, account, asked, messenger = {}, meaning, rerank) => {
32
- // A question's own "?" would parse as a one-character wildcard and send it to strict search (BUG-395); the
33
- // colon of a time or ratio ("22:00", "1:1") would parse as a field and fail the whole search, and escaped it is
34
- // syntax again, so it splits the numbers (BUG-450). A quoted timestamp keeps its colons (BUG-458).
35
- const request = asked.ast !== undefined || asked.exact || !asked.text
36
- ? asked
37
- : {
38
- ...asked,
39
- text: asked.text
40
- .replace(/(?<=\p{L})\?(?=\s|$)/gu, "")
41
- .replace(/"(?:[^"\\]|\\.)*"|(?<=\d):(?=\d)/gu, (found) => (found === ":" ? " " : found)),
42
- };
45
+ const request = discoveryRequest(asked);
43
46
  const original = await prepareLucene(store, account, request, messenger);
44
47
  const text = request.text ?? "";
45
48
  const plan = planQuestion(text, request.ast !== undefined || !!request.exact || !!request.newest);
@@ -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, { model: choice, limit, folderIds, withInternal, source, env, threads, command, embedders, }: Omit<NotesEmbedOptions, "maxChunks"> & {
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
- }) => Promise<NearestNote[]>;
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 {};