@wirecat/cli-core 0.562.0 → 0.564.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 +139 -0
- package/dist/calendar/mcp/index.d.ts +12 -12
- package/dist/cli/config-command.js +18 -1
- package/dist/cli/envelope.js +13 -1
- package/dist/cli/execution.js +1 -0
- package/dist/cli/messenger/archive-commands.js +17 -0
- package/dist/cli/messenger/catch-up-answers.d.ts +5 -0
- package/dist/cli/messenger/catch-up-answers.js +3 -0
- package/dist/cli/messenger/reply-stats-command.d.ts +4 -0
- package/dist/cli/messenger/reply-stats-command.js +57 -0
- package/dist/cli/messenger/search-answers.d.ts +1 -0
- package/dist/cli/messenger/search-answers.js +1 -1
- package/dist/cli/messenger/search-command.js +8 -0
- package/dist/cli/messenger/stats-answers.d.ts +6 -3
- package/dist/cli/messenger/stats-answers.js +39 -9
- package/dist/cli/messenger/stats-command.js +5 -0
- package/dist/cli/messenger/tasks-command.js +35 -14
- package/dist/cli/output-budget.d.ts +26 -0
- package/dist/cli/output-budget.js +73 -0
- package/dist/cli/program.js +14 -1
- package/dist/cli/settings.d.ts +5 -0
- package/dist/cli/settings.js +88 -5
- package/dist/cli/shared-command-exports.d.ts +8 -0
- package/dist/conversations/mail-text.d.ts +6 -0
- package/dist/conversations/mail-text.js +73 -0
- package/dist/domain/admin-statistics.d.ts +5 -0
- package/dist/domain/admin-statistics.js +4 -3
- package/dist/domain/calendar.d.ts +2 -0
- package/dist/domain/calendar.js +6 -0
- package/dist/entities/chat-stats.d.ts +16 -2
- package/dist/entities/chat-stats.js +18 -3
- package/dist/entities/index.d.ts +80 -802
- package/dist/entities/index.js +26 -16
- package/dist/entities/mail-counts.d.ts +15 -0
- package/dist/entities/mail-counts.js +16 -0
- package/dist/entities/meeting.d.ts +1 -1
- package/dist/entities/meeting.js +1 -1
- package/dist/entities/reply-stats.d.ts +33 -0
- package/dist/entities/reply-stats.js +19 -0
- package/dist/entities/store-records.d.ts +8 -0
- package/dist/entities/store-records.js +8 -0
- package/dist/errors/reasons.d.ts +1 -1
- package/dist/errors/reasons.js +7 -3
- package/dist/mail/attachments.d.ts +15 -2
- package/dist/mail/attachments.js +19 -3
- package/dist/mail/cli/index.d.ts +22 -1
- package/dist/mail/cli/index.js +28 -4
- package/dist/mail/cli/review.d.ts +5 -2
- package/dist/mail/cli/review.js +58 -43
- package/dist/mail/index.d.ts +4 -0
- package/dist/mail/index.js +4 -0
- package/dist/mail/partial.d.ts +3 -1
- package/dist/mail/partial.js +2 -2
- package/dist/mail/retry.d.ts +3 -1
- package/dist/mail/retry.js +3 -3
- package/dist/mail/stats.d.ts +50 -0
- package/dist/mail/stats.js +60 -0
- package/dist/mail/sync-types.d.ts +3 -0
- package/dist/mail/sync.js +5 -3
- package/dist/mail/whole.d.ts +28 -0
- package/dist/mail/whole.js +70 -0
- package/dist/meetings/index.d.ts +2 -2
- package/dist/meetings/index.js +2 -2
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/meetings/read-answers.d.ts +22 -1
- package/dist/meetings/read-answers.js +38 -0
- package/dist/meetings/stats.d.ts +44 -0
- package/dist/meetings/stats.js +77 -7
- package/dist/search/lucene/registry.js +2 -1
- package/dist/search/question-plan.d.ts +16 -0
- package/dist/search/question-plan.js +34 -0
- package/dist/services/events-search.d.ts +5 -1
- package/dist/services/events-search.js +10 -4
- package/dist/services/index.d.ts +3 -1
- package/dist/services/index.js +3 -1
- package/dist/services/meeting-stats.d.ts +12 -1
- package/dist/services/meeting-stats.js +12 -0
- package/dist/services/messages-discovery.d.ts +1 -5
- package/dist/services/messages-discovery.js +5 -18
- package/dist/services/messages-meaning.js +25 -3
- package/dist/services/messages.js +4 -3
- package/dist/services/notes-search.d.ts +5 -1
- package/dist/services/notes-search.js +16 -5
- package/dist/services/proposal-tasks.js +3 -12
- package/dist/services/proposals/ids.d.ts +2 -2
- package/dist/services/proposals/ids.js +5 -3
- package/dist/services/proposals/kinds/action.js +3 -2
- package/dist/services/proposals/kinds/findings.d.ts +1 -1
- package/dist/services/proposals/kinds/findings.js +1 -1
- package/dist/services/proposals/kinds/meeting-findings.d.ts +7 -0
- package/dist/services/proposals/kinds/meeting-findings.js +73 -0
- package/dist/services/proposals/kinds/task-close.d.ts +7 -0
- package/dist/services/proposals/kinds/task-close.js +76 -0
- package/dist/services/proposals/registry.js +12 -4
- package/dist/services/proposals/types.d.ts +2 -0
- package/dist/services/reply-stats.d.ts +48 -0
- package/dist/services/reply-stats.js +137 -0
- package/dist/services/search-all.d.ts +5 -0
- package/dist/services/search-all.js +47 -14
- package/dist/services/task-stats.d.ts +41 -0
- package/dist/services/task-stats.js +126 -0
- package/dist/services/tasks.d.ts +17 -4
- package/dist/services/tasks.js +31 -14
- package/dist/store/chats.d.ts +3 -1
- package/dist/store/chats.js +6 -0
- package/dist/store/decisions.d.ts +16 -11
- package/dist/store/decisions.js +11 -2
- package/dist/store/exports.d.ts +1 -1
- package/dist/store/exports.js +1 -1
- package/dist/store/index.d.ts +5 -4
- package/dist/store/index.js +3 -3
- package/dist/store/queries.d.ts +4 -0
- package/dist/store/queries.js +3 -0
- package/dist/store/sqlite/completeness.d.ts +17 -0
- package/dist/store/sqlite/completeness.js +49 -13
- package/dist/store/sqlite/email-locations.d.ts +4 -0
- package/dist/store/sqlite/email-locations.js +26 -11
- package/dist/store/sqlite/lucene-fields.d.ts +2 -0
- package/dist/store/sqlite/lucene-fields.js +5 -1
- package/dist/store/sqlite/lucene-groups.d.ts +15 -0
- package/dist/store/sqlite/lucene-groups.js +21 -0
- package/dist/store/sqlite/lucene.d.ts +2 -9
- package/dist/store/sqlite/lucene.js +1 -12
- package/dist/store/sqlite/mail-findings.js +1 -1
- package/dist/store/sqlite/meeting-findings.js +1 -1
- package/dist/store/sqlite/meeting-stats.d.ts +2 -0
- package/dist/store/sqlite/meeting-stats.js +27 -1
- package/dist/store/sqlite/proposed-actions.d.ts +2 -0
- package/dist/store/sqlite/proposed-actions.js +24 -17
- package/dist/store/sqlite/reply-times.d.ts +57 -0
- package/dist/store/sqlite/reply-times.js +113 -0
- package/dist/store/sqlite/retention.js +2 -7
- package/dist/store/sqlite/task-board.js +2 -0
- package/dist/store/sqlite/task-stats.d.ts +32 -0
- package/dist/store/sqlite/task-stats.js +45 -0
- package/dist/store/sqlite/tasks.d.ts +2 -1
- package/dist/store/sqlite/tasks.js +2 -0
- package/dist/store/sqlite/work-tasks.d.ts +12 -2
- package/dist/store/sqlite/work-tasks.js +35 -26
- package/dist/store/store.d.ts +1 -0
- package/package.json +1 -1
- package/parity.json +37 -1
package/dist/store/chats.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Chat, ChatKind, ChatUpdate, Id, Page } from "../domain/models.js";
|
|
|
3
3
|
import type { HistorySkip } from "../services/history-limits.js";
|
|
4
4
|
import { type ChatRekey, type ChatRekeys } from "./sqlite/chat-rekey.js";
|
|
5
5
|
import type { ChatMetadata, LeftChat } from "./sqlite/chats.js";
|
|
6
|
-
import type { ChatCompleteness } from "./sqlite/completeness.js";
|
|
6
|
+
import type { ChatCompleteness, ChatCoverage } from "./sqlite/completeness.js";
|
|
7
7
|
import type { ChatHistoryShape, StoredHistorySkip } from "./sqlite/history-skips.js";
|
|
8
8
|
import type { StoreContext } from "./sqlite/open.js";
|
|
9
9
|
import type { Where } from "./sqlite/query-filter.js";
|
|
@@ -59,6 +59,8 @@ export interface ChatsStore {
|
|
|
59
59
|
}): Promise<LeftChats>;
|
|
60
60
|
/** Per chat: whether its history is held up to date, without gaps, back to its start. */
|
|
61
61
|
chatCompleteness(key: AccountKey, chatIds: Id[]): Promise<ChatCompleteness[]>;
|
|
62
|
+
/** Over every searchable chat: how many are held in full, in part, or never fetched, and the oldest message held. */
|
|
63
|
+
chatCoverage(key: AccountKey): Promise<ChatCoverage>;
|
|
62
64
|
/** Per chat that has messages: how many, the oldest and newest, and when the last one was stored. */
|
|
63
65
|
chatStats(key: AccountKey, chatId?: Id): Promise<ChatStats[]>;
|
|
64
66
|
/**
|
package/dist/store/chats.js
CHANGED
|
@@ -78,6 +78,12 @@ export const chatsStoreOver = (context) => {
|
|
|
78
78
|
const accountKey = findAccountPk(key);
|
|
79
79
|
return accountKey === undefined ? [] : completeness.chatCompleteness(context, accountKey, chatIds);
|
|
80
80
|
},
|
|
81
|
+
chatCoverage: async (key) => {
|
|
82
|
+
const accountKey = findAccountPk(key);
|
|
83
|
+
return accountKey === undefined
|
|
84
|
+
? { chats: 0, fetched: 0, partial: 0, never: 0, oldestMessageAt: null }
|
|
85
|
+
: completeness.chatCoverage(context, accountKey);
|
|
86
|
+
},
|
|
81
87
|
chatStats: async (key, chatId) => {
|
|
82
88
|
const accountKey = findAccountPk(key);
|
|
83
89
|
return accountKey === undefined ? [] : reads.chatStats(context, accountKey, chatId);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** An agent's suggested close of a task: kept on the task, not in `proposed_actions`. */
|
|
2
|
+
export declare const TASK_CLOSE_KIND = "task_close";
|
|
1
3
|
/**
|
|
2
4
|
* Who proposed something waiting for the owner. `unknown` is a suggestion that names no author, such as an
|
|
3
5
|
* unconfirmed link written without provenance: only the owner decides it. `verified: false` is a bot named only by
|
|
@@ -21,6 +23,12 @@ export type Decider = "owner" | {
|
|
|
21
23
|
model?: string;
|
|
22
24
|
reason: string;
|
|
23
25
|
};
|
|
26
|
+
export interface DecidedRow {
|
|
27
|
+
id: string;
|
|
28
|
+
proposedBy: Proposer;
|
|
29
|
+
ownerOnly?: boolean;
|
|
30
|
+
kind?: string;
|
|
31
|
+
}
|
|
24
32
|
export interface DecisionPolicy {
|
|
25
33
|
/** The host's `agent_decisions`: off unless the owner sets it to `true`; off keeps every decision with the owner. */
|
|
26
34
|
agentDecisions: boolean;
|
|
@@ -34,17 +42,18 @@ export interface Refusal {
|
|
|
34
42
|
reason: string;
|
|
35
43
|
message: string;
|
|
36
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Proposal kinds only the owner decides, whatever `agent_decisions` says. Read from the row's kind, so a path that
|
|
47
|
+
* sees the row as something else — a generic action, a host's own kind — still refuses an agent.
|
|
48
|
+
*/
|
|
49
|
+
export declare const isOwnerOnlyKind: (kind: string | undefined) => boolean;
|
|
37
50
|
/**
|
|
38
51
|
* The one check every decide path runs before it confirms or rejects a proposal. `null` means allowed. Every Claude
|
|
39
52
|
* Code session is the bot `claude-code`, so comparing bot names keeps one session from deciding another's proposal.
|
|
40
53
|
*/
|
|
41
54
|
export declare const mayDecide: (
|
|
42
|
-
/** `ownerOnly`: what only the owner may say, such as that a payment was made. */
|
|
43
|
-
row:
|
|
44
|
-
id: string;
|
|
45
|
-
proposedBy: Proposer;
|
|
46
|
-
ownerOnly?: boolean;
|
|
47
|
-
}, by: Decider, policy?: DecisionPolicy) => Refusal | null;
|
|
55
|
+
/** `ownerOnly`: what only the owner may say, such as that a payment was made; `kind`: the stored kind. */
|
|
56
|
+
row: DecidedRow, by: Decider, policy?: DecisionPolicy) => Refusal | null;
|
|
48
57
|
/** One bot whatever its letter case or spacing: `Claude-Code` is `claude-code`. */
|
|
49
58
|
export declare const botKey: (name: string) => string;
|
|
50
59
|
/**
|
|
@@ -59,11 +68,7 @@ export declare const assertMayWithdraw: (row: {
|
|
|
59
68
|
bot: string;
|
|
60
69
|
}) => void;
|
|
61
70
|
/** `mayDecide` for a store method: throws the refusal instead of returning it. */
|
|
62
|
-
export declare const assertMayDecide: (row:
|
|
63
|
-
id: string;
|
|
64
|
-
proposedBy: Proposer;
|
|
65
|
-
ownerOnly?: boolean;
|
|
66
|
-
}, by: Decider, policy?: DecisionPolicy) => void;
|
|
71
|
+
export declare const assertMayDecide: (row: DecidedRow, by: Decider, policy?: DecisionPolicy) => void;
|
|
67
72
|
/** The author a decision is recorded under. */
|
|
68
73
|
export declare const deciderAuthor: (by: Decider) => "owner" | {
|
|
69
74
|
bot: string;
|
package/dist/store/decisions.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { MEETING_FINDINGS_KIND } from "../meetings/index.js";
|
|
3
|
+
import { MAIL_FINDINGS_KIND } from "./mail-findings.js";
|
|
4
|
+
/** An agent's suggested close of a task: kept on the task, not in `proposed_actions`. */
|
|
5
|
+
export const TASK_CLOSE_KIND = "task_close";
|
|
2
6
|
export const DEFAULT_DECISION_POLICY = { agentDecisions: false, ruleProposalsOwnerOnly: true };
|
|
7
|
+
/**
|
|
8
|
+
* Proposal kinds only the owner decides, whatever `agent_decisions` says. Read from the row's kind, so a path that
|
|
9
|
+
* sees the row as something else — a generic action, a host's own kind — still refuses an agent.
|
|
10
|
+
*/
|
|
11
|
+
export const isOwnerOnlyKind = (kind) => kind === MAIL_FINDINGS_KIND || kind === MEETING_FINDINGS_KIND || kind === TASK_CLOSE_KIND;
|
|
3
12
|
/**
|
|
4
13
|
* The one check every decide path runs before it confirms or rejects a proposal. `null` means allowed. Every Claude
|
|
5
14
|
* Code session is the bot `claude-code`, so comparing bot names keeps one session from deciding another's proposal.
|
|
6
15
|
*/
|
|
7
16
|
export const mayDecide = (
|
|
8
|
-
/** `ownerOnly`: what only the owner may say, such as that a payment was made. */
|
|
17
|
+
/** `ownerOnly`: what only the owner may say, such as that a payment was made; `kind`: the stored kind. */
|
|
9
18
|
row, by, policy = DEFAULT_DECISION_POLICY) => {
|
|
10
19
|
if (by === "owner")
|
|
11
20
|
return null;
|
|
@@ -15,7 +24,7 @@ row, by, policy = DEFAULT_DECISION_POLICY) => {
|
|
|
15
24
|
reason,
|
|
16
25
|
message,
|
|
17
26
|
});
|
|
18
|
-
if (row.ownerOnly)
|
|
27
|
+
if (row.ownerOnly || isOwnerOnlyKind(row.kind))
|
|
19
28
|
return refuse("permission_error", "proposal.owner-only", `only the owner decides ${row.id}`);
|
|
20
29
|
if (!policy.agentDecisions)
|
|
21
30
|
return refuse("permission_error", "proposals.agent-decisions-off", "agent decisions are turned off; the owner decides");
|
package/dist/store/exports.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export { pendingNormalization } from "./sqlite/backfill.js";
|
|
|
20
20
|
export type { ChatRekey, ChatRekeys } from "./sqlite/chat-rekey.js";
|
|
21
21
|
export { CHAT_COLUMNS, TRACKED_CHAT_COLUMNS, titlePart } from "./sqlite/chats.js";
|
|
22
22
|
export { storeClients } from "./sqlite/clients.js";
|
|
23
|
-
export { CHAT_LIST_KEY, type ChatCompleteness, fetchedKey, historyStartKey } from "./sqlite/completeness.js";
|
|
23
|
+
export { CHAT_LIST_KEY, type ChatCompleteness, type ChatCoverage, fetchedKey, historyStartKey, } from "./sqlite/completeness.js";
|
|
24
24
|
export { CONVERSATION_COLUMNS } from "./sqlite/conversations.js";
|
|
25
25
|
export type { CounterTarget } from "./sqlite/counters.js";
|
|
26
26
|
export { type EmailMarks, MAIL_AGENDA_COLUMNS, type MarkedEmail, type NewestEmail, type ThreadActivity, } from "./sqlite/email-activity.js";
|
package/dist/store/exports.js
CHANGED
|
@@ -4,7 +4,7 @@ export { attachmentColumns, resetAttachmentWords, } from "./sqlite/attachment-te
|
|
|
4
4
|
export { pendingNormalization } from "./sqlite/backfill.js";
|
|
5
5
|
export { CHAT_COLUMNS, TRACKED_CHAT_COLUMNS, titlePart } from "./sqlite/chats.js";
|
|
6
6
|
export { storeClients } from "./sqlite/clients.js";
|
|
7
|
-
export { CHAT_LIST_KEY, fetchedKey, historyStartKey } from "./sqlite/completeness.js";
|
|
7
|
+
export { CHAT_LIST_KEY, fetchedKey, historyStartKey, } from "./sqlite/completeness.js";
|
|
8
8
|
export { CONVERSATION_COLUMNS } from "./sqlite/conversations.js";
|
|
9
9
|
export { MAIL_AGENDA_COLUMNS, } from "./sqlite/email-activity.js";
|
|
10
10
|
export { messageOfEmail, threadConversation } from "./sqlite/emails.js";
|
package/dist/store/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type { FindingEvidence, FindingOutcome, MeetingFactFinding, MeetingFindin
|
|
|
9
9
|
export { PAYMENT_DIRECTIONS, PAYMENT_STATES, type Payment, paymentTitle, validatePayment } from "../meetings/index.js";
|
|
10
10
|
export { CLOSED_STATES, isClosedState, PRIORITY_LABELS, type PriorityLabel, priorityLabel, priorityOf, priorityOption, TASK_STATES, type TaskState, } from "../tasks/index.js";
|
|
11
11
|
export type { AccountMail, AccountMeetings, AccountStore } from "./account-store.js";
|
|
12
|
-
export { assertMayDecide, DEFAULT_DECISION_POLICY, type Decider, type DecisionPolicy, mayDecide, type Proposer, type Refusal, } from "./decisions.js";
|
|
12
|
+
export { assertMayDecide, DEFAULT_DECISION_POLICY, type Decider, type DecisionPolicy, isOwnerOnlyKind, mayDecide, type Proposer, type Refusal, } from "./decisions.js";
|
|
13
13
|
export type { DocumentTransferOptions, DocumentTransferPreview, DocumentTransferReceipt, } from "./document-transfer/index.js";
|
|
14
14
|
export { applyDocumentTransfer, previewDocumentTransfer } from "./document-transfer/index.js";
|
|
15
15
|
export type { CacheDatabase, CacheStatement, OpenDatabase, SqlValue } from "./driver.js";
|
|
@@ -54,7 +54,7 @@ export type { NoteIndexState } from "./sqlite/note-index.js";
|
|
|
54
54
|
export { type NearestNote, NOTE_FIELDS, type NoteChunkToEmbed, type NoteHit, type NoteQuery, type NoteSearch, } from "./sqlite/note-search.js";
|
|
55
55
|
export type { FileNoteInput, Link, LinkInput, LinkListFilter, Note, NoteFolder, NotesStore, NoteTag, } from "./sqlite/notes.js";
|
|
56
56
|
export type { PrivateContact, PrivateContactNote } from "./sqlite/private-people.js";
|
|
57
|
-
export { PROPOSAL_STATUSES, type ProposalInput, type ProposedAction, type ProposedActionsStore, proposerOfAction, } from "./sqlite/proposed-actions.js";
|
|
57
|
+
export { assertGenericAction, PROPOSAL_STATUSES, type ProposalInput, type ProposedAction, type ProposedActionsStore, proposerOfAction, } from "./sqlite/proposed-actions.js";
|
|
58
58
|
export type { RecordKey, RecordOutcome, RecordStore, RecordsWritten, SourceRecord, } from "./sqlite/records.js";
|
|
59
59
|
export type { LinkDecision, RelationUpdate } from "./sqlite/relation-proposals.js";
|
|
60
60
|
export { fillSearchIndex, resetSearchIndex, type SearchIndexFill, type SearchIndexState, searchIndexState, } from "./sqlite/search-index.js";
|
|
@@ -62,7 +62,7 @@ export type { SyncStateSave } from "./sqlite/sync.js";
|
|
|
62
62
|
export type { TagCreationOptions, TagCreationReview } from "./sqlite/tag-creation.js";
|
|
63
63
|
export type { TagKind, TagRow } from "./sqlite/tags.js";
|
|
64
64
|
export type { TaskSort } from "./sqlite/task-board.js";
|
|
65
|
-
export { PROJECT_TASK_FIELDS, TASK_SORTS } from "./sqlite/task-board.js";
|
|
65
|
+
export { PROJECT_TASK_FIELDS, TASK_SORTS, withoutField } from "./sqlite/task-board.js";
|
|
66
66
|
export type { SimilarSubject, SimilarTask, TaskDraft } from "./sqlite/task-candidates.js";
|
|
67
67
|
export { type AgentCloseSettings, type CloseSuggestion, certaintyOf } from "./sqlite/task-close.js";
|
|
68
68
|
export type { NamedAuthor, TaskComment } from "./sqlite/task-comments.js";
|
|
@@ -70,8 +70,9 @@ export type { TaskPair, TaskPairDecision, TaskPairSide } from "./sqlite/task-pai
|
|
|
70
70
|
export { TASK_PAIR_DECISIONS } from "./sqlite/task-pairs.js";
|
|
71
71
|
export type { TaskRelation, TaskRelationKind, TaskRelationsStore } from "./sqlite/task-relations.js";
|
|
72
72
|
export { TASK_RELATION_KINDS } from "./sqlite/task-relations.js";
|
|
73
|
+
export type { TaskStatRow, TaskStatScope } from "./sqlite/task-stats.js";
|
|
73
74
|
export type { StoreTaskStore } from "./sqlite/tasks.js";
|
|
74
75
|
export type { TagPreflight, VocabularyAlias, VocabularyAliasReceipt, VocabularyAudit, VocabularyAuditOptions, VocabularyCatalog, VocabularyEntry, VocabularyKind, VocabularyOptions, VocabularyResolution, VocabularyStore, VocabularyTarget, } from "./sqlite/vocabulary.js";
|
|
75
|
-
export type { BoardCounts, BoardOptions, NewWorkTask, RevisionCheck, TaskAssignment, TaskDetails, TaskEvent, TaskFields, TaskMove, TaskMoved, TaskRole, TaskType, WorkTaskType, } from "./sqlite/work-tasks.js";
|
|
76
|
+
export type { BoardCounts, BoardOptions, EditedTask, MovedSubtask, NewWorkTask, RevisionCheck, TaskAssignment, TaskDetails, TaskEvent, TaskFields, TaskMove, TaskMoved, TaskRole, TaskType, WorkTaskType, } from "./sqlite/work-tasks.js";
|
|
76
77
|
export { TASK_EVENT_KINDS, TASK_ROLES } from "./sqlite/work-tasks.js";
|
|
77
78
|
export { type AccountAccounts, type AccountAttachments, type AccountCounters, type AccountKey, type AccountMentions, type AccountMessageFilter, type AccountMessages, type AccountPeople, type AccountsStore, type AccountVectors, type AttachmentsStore, type AttachmentTextEntry, type AttachmentView, type ChatStats, type ChatsStore, type ConversationsStore, type CountersStore, type DeletionScope, type Delta, type FileAttachment, type IdentityRef, type IngestedVia, type LinkedIdentity, type LinkOptions, MENTION_QUERY, MENTION_STATUSES, type MentionsStore, type MessageFilter, type MessageStore, type MessagesStore, OTHER_SOURCES, openStore, type PeopleStore, type PersonFacts, type PersonRecord, type QueriesStore, type Range, type RankedEvidence, type RankedStoreFound, type RankedStoreRow, type RankingEvidenceItem, type RankingEvidenceRequest, type RankingRequest, type RosterStore, type SearchCommand, type SearchesStore, type SearchIndexStore, type SearchRecord, type StoredAccount, type StoredChatFilter, type StoredHit, type StoredSearch, type StoredTag, type StoreOptions, type SyncStore, type TagFilter, type TagsStore, type TagTarget, type TaskAnalysisMark, type TaskAnalysisMarkWrite, type TaskTagTarget, type TextOrigin, type VectorsStore, } from "./store.js";
|
package/dist/store/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { createTagRedirectIndex } from "../domain/tag-redirects.js";
|
|
|
3
3
|
export { note } from "../entities/note.js";
|
|
4
4
|
export { PAYMENT_DIRECTIONS, PAYMENT_STATES, paymentTitle, validatePayment } from "../meetings/index.js";
|
|
5
5
|
export { CLOSED_STATES, isClosedState, PRIORITY_LABELS, priorityLabel, priorityOf, priorityOption, TASK_STATES, } from "../tasks/index.js";
|
|
6
|
-
export { assertMayDecide, DEFAULT_DECISION_POLICY, mayDecide, } from "./decisions.js";
|
|
6
|
+
export { assertMayDecide, DEFAULT_DECISION_POLICY, isOwnerOnlyKind, mayDecide, } from "./decisions.js";
|
|
7
7
|
export { applyDocumentTransfer, previewDocumentTransfer } from "./document-transfer/index.js";
|
|
8
8
|
export { PRAGMAS } from "./driver.js";
|
|
9
9
|
export { EVIDENCE_FINDINGS_KIND, validateEvidenceFindings, } from "./evidence-findings.js";
|
|
@@ -24,9 +24,9 @@ export { EVENT_FIELDS, } from "./sqlite/event-search.js";
|
|
|
24
24
|
export { IMPORTED_DOCUMENT_QUERY } from "./sqlite/imported-documents.js";
|
|
25
25
|
export { ORGANIZATION_KINDS, PROJECT_TYPES, SCOPES } from "./sqlite/knowledge.js";
|
|
26
26
|
export { NOTE_FIELDS, } from "./sqlite/note-search.js";
|
|
27
|
-
export { PROPOSAL_STATUSES, proposerOfAction, } from "./sqlite/proposed-actions.js";
|
|
27
|
+
export { assertGenericAction, PROPOSAL_STATUSES, proposerOfAction, } from "./sqlite/proposed-actions.js";
|
|
28
28
|
export { fillSearchIndex, resetSearchIndex, searchIndexState, } from "./sqlite/search-index.js";
|
|
29
|
-
export { PROJECT_TASK_FIELDS, TASK_SORTS } from "./sqlite/task-board.js";
|
|
29
|
+
export { PROJECT_TASK_FIELDS, TASK_SORTS, withoutField } from "./sqlite/task-board.js";
|
|
30
30
|
export { certaintyOf } from "./sqlite/task-close.js";
|
|
31
31
|
export { TASK_PAIR_DECISIONS } from "./sqlite/task-pairs.js";
|
|
32
32
|
export { TASK_RELATION_KINDS } from "./sqlite/task-relations.js";
|
package/dist/store/queries.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { QueryGroup, QueryGrouping } from "./sqlite/lucene.js";
|
|
|
7
7
|
import type { StoreContext } from "./sqlite/open.js";
|
|
8
8
|
import { type RankedEvidence, type RankingEvidenceRequest } from "./sqlite/ranking-evidence.js";
|
|
9
9
|
import { type RankedStoreFound, type RankingRequest } from "./sqlite/rankings.js";
|
|
10
|
+
import { type ChatScope, type NewestMessage, type ReplyDelay, type WaitingChat } from "./sqlite/reply-times.js";
|
|
10
11
|
import type { ScoredHit } from "./sqlite/words.js";
|
|
11
12
|
/** A resolved Lucene query run over the store: hits, counts, rankings and the statistics behind them. */
|
|
12
13
|
export interface QueriesStore {
|
|
@@ -21,5 +22,8 @@ export interface QueriesStore {
|
|
|
21
22
|
rankQuery(execution: QueryExecution, request: RankingRequest): Promise<RankedStoreFound>;
|
|
22
23
|
rankingEvidence(execution: QueryExecution, request: RankingEvidenceRequest): Promise<RankedEvidence>;
|
|
23
24
|
adminStatisticsQuery(execution: QueryExecution, request: AdminStoreRequest): Promise<AdminStoreResult>;
|
|
25
|
+
/** Every reply in one-to-one chats whose reply falls in the scope's period, with how long it took. */
|
|
26
|
+
replyDelays(scope: ChatScope, who: "me" | "others"): Promise<ReplyDelay[]>;
|
|
27
|
+
waitingChats(scope: ChatScope, waits: (newest: NewestMessage) => boolean, cap: number): Promise<WaitingChat[]>;
|
|
24
28
|
}
|
|
25
29
|
export declare const queriesStoreOver: (context: StoreContext, stemmerFor: (stemmers: Stemmers) => Stemmer) => QueriesStore;
|
package/dist/store/queries.js
CHANGED
|
@@ -4,6 +4,7 @@ import { drainEmailIndex } from "./sqlite/email-index.js";
|
|
|
4
4
|
import * as lucene from "./sqlite/lucene.js";
|
|
5
5
|
import { rankingEvidence } from "./sqlite/ranking-evidence.js";
|
|
6
6
|
import { rankQuery } from "./sqlite/rankings.js";
|
|
7
|
+
import { replyDelays, waitingChats, } from "./sqlite/reply-times.js";
|
|
7
8
|
export const queriesStoreOver = (context, stemmerFor) => {
|
|
8
9
|
const mailIndexed = (execution) => {
|
|
9
10
|
if (execution.corpus === "mail")
|
|
@@ -19,5 +20,7 @@ export const queriesStoreOver = (context, stemmerFor) => {
|
|
|
19
20
|
rankQuery: async (execution, request) => rankQuery(context, execution, request),
|
|
20
21
|
rankingEvidence: async (execution, request) => rankingEvidence(context, execution, request),
|
|
21
22
|
adminStatisticsQuery: async (execution, request) => adminStatisticsQuery(context, execution, request),
|
|
23
|
+
replyDelays: async (scope, who) => replyDelays(context, scope, who),
|
|
24
|
+
waitingChats: async (scope, waits, cap) => waitingChats(context, scope, waits, cap),
|
|
22
25
|
};
|
|
23
26
|
};
|
|
@@ -23,3 +23,20 @@ export interface ChatCompleteness {
|
|
|
23
23
|
* A start mark that a later fetch reached past was wrong, so it stops counting.
|
|
24
24
|
*/
|
|
25
25
|
export declare const chatCompleteness: ({ database }: StoreContext, accountKey: number, chatIds: Id[]) => ChatCompleteness[];
|
|
26
|
+
export interface ChatCoverage {
|
|
27
|
+
chats: number;
|
|
28
|
+
/** Held back to the first message, up to date, without gaps: `complete` above. */
|
|
29
|
+
fetched: number;
|
|
30
|
+
partial: number;
|
|
31
|
+
/** No fetch ever recorded and no message held. */
|
|
32
|
+
never: number;
|
|
33
|
+
oldestMessageAt: string | null;
|
|
34
|
+
}
|
|
35
|
+
export declare const COVERAGE_SQL = "SELECT c.last_message_at AS newest, c.message_count AS messages,\n (SELECT m.sent_at FROM messages m WHERE m.chat_id = c.id AND m.deleted_at IS NULL\n ORDER BY m.sent_at DESC LIMIT 1) AS held,\n (SELECT m.sent_at FROM messages m WHERE m.chat_id = c.id AND m.deleted_at IS NULL\n ORDER BY m.sent_at ASC LIMIT 1) AS oldest,\n (SELECT count(*) FROM sync_ranges r WHERE r.chat_id = c.id) AS stretches,\n EXISTS (SELECT 1 FROM sync_cursors s WHERE s.account_id = c.account_id AND s.key = 'history_start:' || c.external_id\n AND NOT EXISTS (SELECT 1 FROM sync_ranges r WHERE r.chat_id = c.id AND r.from_key < CAST(s.value AS INTEGER)))\n AS start,\n EXISTS (SELECT 1 FROM sync_cursors s WHERE s.account_id = c.account_id\n AND s.key IN ('history_start:' || c.external_id, 'fetched:' || c.external_id)) AS recorded\n FROM chats c WHERE c.account_id = ? AND c.searchable = 1";
|
|
36
|
+
/**
|
|
37
|
+
* `chatCompleteness` over every searchable chat of the account in one statement. A chat whose history was pushed
|
|
38
|
+
* into the store (WhatsApp) has messages and no fetch record, so it counts as partial, not never. The newest and
|
|
39
|
+
* oldest held are read with ORDER BY … LIMIT 1, so each stops at the first live row of `messages_by_time` rather than
|
|
40
|
+
* relying on the planner's min/max shortcut, which the deleted_at filter may defeat.
|
|
41
|
+
*/
|
|
42
|
+
export declare const chatCoverage: ({ database }: StoreContext, accountKey: number) => ChatCoverage;
|
|
@@ -24,18 +24,54 @@ export const chatCompleteness = ({ database }, accountKey, chatIds) => {
|
|
|
24
24
|
FROM chats c WHERE c.account_id = ? AND c.external_id IN (SELECT value FROM json_each(?))`)
|
|
25
25
|
.all(accountKey, JSON.stringify(chatIds))
|
|
26
26
|
.map((row) => {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const upToDate = row.newest === null ? null : row.held !== null && Number(row.held) >= Number(row.newest);
|
|
30
|
-
const gaps = stretches > 1;
|
|
31
|
-
const fetched = stretches > 0 || reachesStart;
|
|
32
|
-
return {
|
|
33
|
-
chatId: String(row.chat),
|
|
34
|
-
state: !fetched ? "unknown" : upToDate !== false && !gaps && reachesStart ? "complete" : "partial",
|
|
35
|
-
upToDate,
|
|
36
|
-
gaps,
|
|
37
|
-
reachesStart,
|
|
38
|
-
fetchedAt: toIso(row.fetched),
|
|
39
|
-
};
|
|
27
|
+
const facts = factsOf(row);
|
|
28
|
+
return { chatId: String(row.chat), ...facts, fetchedAt: toIso(row.fetched) };
|
|
40
29
|
});
|
|
41
30
|
};
|
|
31
|
+
const factsOf = (row) => {
|
|
32
|
+
const stretches = Number(row.stretches);
|
|
33
|
+
const reachesStart = Number(row.start) === 1;
|
|
34
|
+
const upToDate = row.newest === null ? null : row.held !== null && Number(row.held) >= Number(row.newest);
|
|
35
|
+
const gaps = stretches > 1;
|
|
36
|
+
const fetched = stretches > 0 || reachesStart;
|
|
37
|
+
const state = !fetched
|
|
38
|
+
? "unknown"
|
|
39
|
+
: upToDate !== false && !gaps && reachesStart
|
|
40
|
+
? "complete"
|
|
41
|
+
: "partial";
|
|
42
|
+
return { state, upToDate, gaps, reachesStart };
|
|
43
|
+
};
|
|
44
|
+
export const COVERAGE_SQL = `SELECT c.last_message_at AS newest, c.message_count AS messages,
|
|
45
|
+
(SELECT m.sent_at FROM messages m WHERE m.chat_id = c.id AND m.deleted_at IS NULL
|
|
46
|
+
ORDER BY m.sent_at DESC LIMIT 1) AS held,
|
|
47
|
+
(SELECT m.sent_at FROM messages m WHERE m.chat_id = c.id AND m.deleted_at IS NULL
|
|
48
|
+
ORDER BY m.sent_at ASC LIMIT 1) AS oldest,
|
|
49
|
+
(SELECT count(*) FROM sync_ranges r WHERE r.chat_id = c.id) AS stretches,
|
|
50
|
+
EXISTS (SELECT 1 FROM sync_cursors s WHERE s.account_id = c.account_id AND s.key = 'history_start:' || c.external_id
|
|
51
|
+
AND NOT EXISTS (SELECT 1 FROM sync_ranges r WHERE r.chat_id = c.id AND r.from_key < CAST(s.value AS INTEGER)))
|
|
52
|
+
AS start,
|
|
53
|
+
EXISTS (SELECT 1 FROM sync_cursors s WHERE s.account_id = c.account_id
|
|
54
|
+
AND s.key IN ('history_start:' || c.external_id, 'fetched:' || c.external_id)) AS recorded
|
|
55
|
+
FROM chats c WHERE c.account_id = ? AND c.searchable = 1`;
|
|
56
|
+
/**
|
|
57
|
+
* `chatCompleteness` over every searchable chat of the account in one statement. A chat whose history was pushed
|
|
58
|
+
* into the store (WhatsApp) has messages and no fetch record, so it counts as partial, not never. The newest and
|
|
59
|
+
* oldest held are read with ORDER BY … LIMIT 1, so each stops at the first live row of `messages_by_time` rather than
|
|
60
|
+
* relying on the planner's min/max shortcut, which the deleted_at filter may defeat.
|
|
61
|
+
*/
|
|
62
|
+
export const chatCoverage = ({ database }, accountKey) => {
|
|
63
|
+
const rows = database.prepare(COVERAGE_SQL).all(accountKey);
|
|
64
|
+
const coverage = { chats: rows.length, fetched: 0, partial: 0, never: 0, oldestMessageAt: null };
|
|
65
|
+
let oldest = null;
|
|
66
|
+
for (const row of rows) {
|
|
67
|
+
if (row.oldest !== null && (oldest === null || Number(row.oldest) < oldest))
|
|
68
|
+
oldest = Number(row.oldest);
|
|
69
|
+
if (factsOf(row).state === "complete")
|
|
70
|
+
coverage.fetched += 1;
|
|
71
|
+
else if (Number(row.stretches) === 0 && Number(row.recorded) === 0 && Number(row.messages) === 0)
|
|
72
|
+
coverage.never += 1;
|
|
73
|
+
else
|
|
74
|
+
coverage.partial += 1;
|
|
75
|
+
}
|
|
76
|
+
return { ...coverage, oldestMessageAt: toIso(oldest) };
|
|
77
|
+
};
|
|
@@ -38,6 +38,8 @@ export declare const partialEmails: (database: CacheDatabase, accountId: number,
|
|
|
38
38
|
total: number;
|
|
39
39
|
stuck: number;
|
|
40
40
|
};
|
|
41
|
+
/** One live email's thread and stored places, newest first, in the shape `partialEmails` gives; `null` if not stored. */
|
|
42
|
+
export declare const emailPlace: (database: CacheDatabase, accountId: number, externalId: string) => PartialEmail | null;
|
|
41
43
|
/** Replaces a partial email's `metadata.partial`, as a failed read records itself; a whole email is left alone. */
|
|
42
44
|
export declare const markPartial: (database: CacheDatabase, accountId: number, externalId: string, mark: Metadata) => {
|
|
43
45
|
changes: number;
|
|
@@ -48,6 +50,8 @@ export interface PartialMailStore {
|
|
|
48
50
|
total: number;
|
|
49
51
|
stuck: number;
|
|
50
52
|
}>;
|
|
53
|
+
/** One live email's thread and stored places, newest first; `null` when it is not stored. */
|
|
54
|
+
emailPlace(accountId: number, externalId: string): Promise<PartialEmail | null>;
|
|
51
55
|
/** Records a failed read of a partial email in its `metadata.partial`; a whole email is left alone. */
|
|
52
56
|
markPartial(accountId: number, externalId: string, mark: Metadata): Promise<void>;
|
|
53
57
|
}
|
|
@@ -23,6 +23,17 @@ export const folderEmails = (database, accountId, folder, uidvalidity) => databa
|
|
|
23
23
|
locations: Number(row.locations),
|
|
24
24
|
folders: Number(row.folders),
|
|
25
25
|
}));
|
|
26
|
+
const placed = (row) => ({
|
|
27
|
+
externalId: String(row.external_id),
|
|
28
|
+
threadExternalId: String(row.thread),
|
|
29
|
+
receivedAt: row.at === null ? null : Number(row.at),
|
|
30
|
+
metadata: fromJson(row.metadata),
|
|
31
|
+
locations: JSON.parse(String(row.places)).map((place) => ({
|
|
32
|
+
folder: String(place.folder),
|
|
33
|
+
uidvalidity: String(place.uidvalidity),
|
|
34
|
+
uid: Number(place.uid),
|
|
35
|
+
})),
|
|
36
|
+
});
|
|
26
37
|
export const partialEmails = (database, accountId, limit) => {
|
|
27
38
|
const rows = database
|
|
28
39
|
.prepare(`SELECT e.external_id, t.external_id AS thread, coalesce(e.received_at, e.sent_at) AS at, e.metadata,
|
|
@@ -43,19 +54,22 @@ export const partialEmails = (database, accountId, limit) => {
|
|
|
43
54
|
return {
|
|
44
55
|
total: rows.length ? Number(rows[0]?.total) : 0,
|
|
45
56
|
stuck: Number(stuck?.n ?? 0),
|
|
46
|
-
emails: rows.map(
|
|
47
|
-
externalId: String(row.external_id),
|
|
48
|
-
threadExternalId: String(row.thread),
|
|
49
|
-
receivedAt: row.at === null ? null : Number(row.at),
|
|
50
|
-
metadata: fromJson(row.metadata),
|
|
51
|
-
locations: JSON.parse(String(row.places)).map((place) => ({
|
|
52
|
-
folder: String(place.folder),
|
|
53
|
-
uidvalidity: String(place.uidvalidity),
|
|
54
|
-
uid: Number(place.uid),
|
|
55
|
-
})),
|
|
56
|
-
})),
|
|
57
|
+
emails: rows.map(placed),
|
|
57
58
|
};
|
|
58
59
|
};
|
|
60
|
+
/** One live email's thread and stored places, newest first, in the shape `partialEmails` gives; `null` if not stored. */
|
|
61
|
+
export const emailPlace = (database, accountId, externalId) => {
|
|
62
|
+
const row = database
|
|
63
|
+
.prepare(`SELECT e.external_id, t.external_id AS thread, coalesce(e.received_at, e.sent_at) AS at, e.metadata,
|
|
64
|
+
(SELECT json_group_array(json_object('folder', folder, 'uidvalidity', uidvalidity, 'uid', uid))
|
|
65
|
+
FROM (SELECT b.external_id AS folder, l.uidvalidity, l.uid
|
|
66
|
+
FROM email_locations l JOIN mailboxes b ON b.id = l.mailbox_id
|
|
67
|
+
WHERE l.email_id = e.id ORDER BY l.rowid DESC)) AS places
|
|
68
|
+
FROM emails e JOIN email_threads t ON t.id = e.email_thread_id
|
|
69
|
+
WHERE e.account_id = ? AND e.external_id = ? AND e.deleted_at IS NULL`)
|
|
70
|
+
.get(accountId, externalId);
|
|
71
|
+
return row ? placed(row) : null;
|
|
72
|
+
};
|
|
59
73
|
/** Replaces a partial email's `metadata.partial`, as a failed read records itself; a whole email is left alone. */
|
|
60
74
|
export const markPartial = (database, accountId, externalId, mark) => database
|
|
61
75
|
.prepare(`UPDATE emails SET metadata = json_set(metadata, '$.partial', json(?)) WHERE account_id = ? AND external_id = ?
|
|
@@ -63,6 +77,7 @@ export const markPartial = (database, accountId, externalId, mark) => database
|
|
|
63
77
|
.run(JSON.stringify(mark), accountId, externalId);
|
|
64
78
|
export const partialMail = (database) => ({
|
|
65
79
|
partialEmails: async (accountId, limit) => partialEmails(database, accountId, limit),
|
|
80
|
+
emailPlace: async (accountId, externalId) => emailPlace(database, accountId, externalId),
|
|
66
81
|
markPartial: async (accountId, externalId, mark) => {
|
|
67
82
|
write(database, () => markPartial(database, accountId, externalId, mark));
|
|
68
83
|
},
|
|
@@ -82,6 +82,8 @@ type Compiled = {
|
|
|
82
82
|
type FieldCompiler = (node: ResolvedPredicate, scope: FieldScope) => Compiled;
|
|
83
83
|
/** A leaf as an FTS5 match over a stem index: phrases of stems, or with `~N` their `NEAR`. */
|
|
84
84
|
export declare const stemMatch: (stemmer: Stemmer, node: ResolvedPredicate) => string | undefined;
|
|
85
|
+
/** What `kind:` matches for a chat aliased `c`: private, saved, bot, service, group, channel or unknown. */
|
|
86
|
+
export declare const PEER_KIND = "CASE WHEN json_extract(c.metadata,'$.peerKind') IN ('private','saved','bot','service','group','channel','unknown') THEN json_extract(c.metadata,'$.peerKind') WHEN c.kind='dialog' AND json_extract(c.metadata,'$.isBot')=1 THEN 'bot' WHEN c.kind='dialog' THEN 'private' ELSE c.kind END";
|
|
85
87
|
/** One compiler per field of `QUERY_FIELDS`, by its name: a leaf compiles by a lookup, never a branch. */
|
|
86
88
|
export declare const FIELD_COMPILERS: Readonly<Record<string, FieldCompiler>>;
|
|
87
89
|
export {};
|
|
@@ -344,9 +344,13 @@ const messengerOnly = (compile) => (node, scope) => {
|
|
|
344
344
|
return compile(node, scope);
|
|
345
345
|
};
|
|
346
346
|
const topic = messengerOnly(({ value }) => always(bound("m.thread_external_id = ?", value)));
|
|
347
|
-
|
|
347
|
+
/** What `kind:` matches for a chat aliased `c`: private, saved, bot, service, group, channel or unknown. */
|
|
348
|
+
export const PEER_KIND = "CASE WHEN json_extract(c.metadata,'$.peerKind') IN ('private','saved','bot','service','group','channel','unknown') THEN json_extract(c.metadata,'$.peerKind') WHEN c.kind='dialog' AND json_extract(c.metadata,'$.isBot')=1 THEN 'bot' WHEN c.kind='dialog' THEN 'private' ELSE c.kind END";
|
|
349
|
+
const kind = messengerOnly(({ value }) => always(bound(`${PEER_KIND} = ?`, value.toLowerCase())));
|
|
348
350
|
const has = ({ value }, { corpus, mail }) => {
|
|
349
351
|
const wanted = value.toLowerCase();
|
|
352
|
+
if (wanted === "transcript")
|
|
353
|
+
return always(bound(mail ? "0" : "EXISTS (SELECT 1 FROM message_transcripts t WHERE t.message_id = m.id)"));
|
|
350
354
|
return always(wanted === "text"
|
|
351
355
|
? bound("length(trim(coalesce(m.text,''))) > 0")
|
|
352
356
|
: wanted === "link"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** `_direction`: the same groups, each split by `outgoing`, for counts of received and sent mail. */
|
|
2
|
+
export type QueryGrouping = "chat" | "sender" | "time" | "sender_direction" | "time_direction";
|
|
3
|
+
export interface QueryGroup {
|
|
4
|
+
provider?: string;
|
|
5
|
+
account?: string;
|
|
6
|
+
id: string | null;
|
|
7
|
+
name: string | null;
|
|
8
|
+
outgoing?: boolean;
|
|
9
|
+
count: number;
|
|
10
|
+
}
|
|
11
|
+
/** How `countQuery` groups the matches: the GROUP BY key and the columns each group reads. */
|
|
12
|
+
export declare const GROUPS: Record<QueryGrouping, {
|
|
13
|
+
key: string;
|
|
14
|
+
select: string;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** How `countQuery` groups the matches: the GROUP BY key and the columns each group reads. */
|
|
2
|
+
export const GROUPS = {
|
|
3
|
+
chat: {
|
|
4
|
+
key: "m.chat_id",
|
|
5
|
+
select: "ac.provider AS provider, ac.external_id AS account, c.external_id AS id, c.title AS name",
|
|
6
|
+
},
|
|
7
|
+
sender: {
|
|
8
|
+
key: "coalesce(m.sender_identity_id, -m.account_id)",
|
|
9
|
+
select: "ac.provider AS provider, ac.external_id AS account, i.external_id AS id, i.name AS name, max(m.outgoing) AS outgoing",
|
|
10
|
+
},
|
|
11
|
+
// Quarter hours, so a calendar day or hour in any zone (+05:30, +05:45) is a whole number of buckets.
|
|
12
|
+
time: { key: "m.sent_at / 900000", select: "CAST(min(m.sent_at / 900000) * 900000 AS TEXT) AS id, NULL AS name" },
|
|
13
|
+
sender_direction: {
|
|
14
|
+
key: "coalesce(m.sender_identity_id, -m.account_id), coalesce(m.outgoing, 0)",
|
|
15
|
+
select: "ac.provider AS provider, ac.external_id AS account, i.external_id AS id, i.name AS name, coalesce(m.outgoing, 0) AS outgoing",
|
|
16
|
+
},
|
|
17
|
+
time_direction: {
|
|
18
|
+
key: "m.sent_at / 900000, coalesce(m.outgoing, 0)",
|
|
19
|
+
select: "CAST(min(m.sent_at / 900000) * 900000 AS TEXT) AS id, NULL AS name, coalesce(m.outgoing, 0) AS outgoing",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import type { Page } from "../../domain/models.js";
|
|
2
2
|
import type { QueryExecution } from "../../search/lucene/resolved.js";
|
|
3
3
|
import { type SqlValue } from "../driver.js";
|
|
4
|
+
import { type QueryGroup, type QueryGrouping } from "./lucene-groups.js";
|
|
4
5
|
import type { StoreContext } from "./open.js";
|
|
5
6
|
import type { ScoredHit } from "./words.js";
|
|
6
7
|
export { type Corpus, prefixOf } from "./lucene-fields.js";
|
|
7
|
-
export type
|
|
8
|
-
export interface QueryGroup {
|
|
9
|
-
provider?: string;
|
|
10
|
-
account?: string;
|
|
11
|
-
id: string | null;
|
|
12
|
-
name: string | null;
|
|
13
|
-
outgoing?: boolean;
|
|
14
|
-
count: number;
|
|
15
|
-
}
|
|
8
|
+
export type { QueryGroup, QueryGrouping } from "./lucene-groups.js";
|
|
16
9
|
export declare const matchQuery: (context: StoreContext, execution: QueryExecution) => Promise<Page<ScoredHit>>;
|
|
17
10
|
/** Every match counted once, grouped; the work and candidate budgets of a search still apply. */
|
|
18
11
|
export declare const countQuery: (context: StoreContext, execution: QueryExecution, by: QueryGrouping) => Promise<QueryGroup[]>;
|
|
@@ -6,6 +6,7 @@ import { TIME_LIMIT_FUNCTION, TimeLimitReached } from "../driver.js";
|
|
|
6
6
|
import { emailHitsByPk } from "./emails.js";
|
|
7
7
|
import { placeHits } from "./hit-places.js";
|
|
8
8
|
import { combine, FIELD_COMPILERS, ftsMatch, SPEECH, } from "./lucene-fields.js";
|
|
9
|
+
import { GROUPS } from "./lucene-groups.js";
|
|
9
10
|
import { hitsByPk } from "./search.js";
|
|
10
11
|
import { read } from "./transaction.js";
|
|
11
12
|
import { transcriptItems, transcriptTerms } from "./transcript-query.js";
|
|
@@ -32,18 +33,6 @@ const MAIL = {
|
|
|
32
33
|
item: "email",
|
|
33
34
|
thread: "email_thread",
|
|
34
35
|
};
|
|
35
|
-
const GROUPS = {
|
|
36
|
-
chat: {
|
|
37
|
-
key: "m.chat_id",
|
|
38
|
-
select: "ac.provider AS provider, ac.external_id AS account, c.external_id AS id, c.title AS name",
|
|
39
|
-
},
|
|
40
|
-
sender: {
|
|
41
|
-
key: "coalesce(m.sender_identity_id, -m.account_id)",
|
|
42
|
-
select: "ac.provider AS provider, ac.external_id AS account, i.external_id AS id, i.name AS name, max(m.outgoing) AS outgoing",
|
|
43
|
-
},
|
|
44
|
-
// Quarter hours, so a calendar day or hour in any zone (+05:30, +05:45) is a whole number of buckets.
|
|
45
|
-
time: { key: "m.sent_at / 900000", select: "CAST(min(m.sent_at / 900000) * 900000 AS TEXT) AS id, NULL AS name" },
|
|
46
|
-
};
|
|
47
36
|
export const matchQuery = (context, execution) => runQuery(context, execution);
|
|
48
37
|
/** Every match counted once, grouped; the work and candidate budgets of a search still apply. */
|
|
49
38
|
export const countQuery = (context, execution, by) => runQuery(context, execution, by);
|
|
@@ -290,7 +290,7 @@ export const mailFindingsStoreOver = (context) => {
|
|
|
290
290
|
return invalid("accept only unique IDs shown in this batch");
|
|
291
291
|
if (decision.by === undefined)
|
|
292
292
|
return invalid("a findings review names who decides: the owner");
|
|
293
|
-
assertMayDecide({ id, proposedBy: proposals.proposer(id)
|
|
293
|
+
assertMayDecide({ id, kind: MAIL_FINDINGS_KIND, proposedBy: proposals.proposer(id) }, decision.by, context.decisions);
|
|
294
294
|
const accepted = [...decision.accept].sort();
|
|
295
295
|
if (batch.receipt) {
|
|
296
296
|
if (JSON.stringify(batch.receipt.accepted) !== JSON.stringify(accepted))
|
|
@@ -179,7 +179,7 @@ export const meetingFindingsStoreOver = (context) => {
|
|
|
179
179
|
const accepted = [...decision.accept].sort();
|
|
180
180
|
if (decision.by === undefined)
|
|
181
181
|
return invalid("a findings review names who decides: the owner or an agent");
|
|
182
|
-
assertMayDecide({ id, proposedBy: proposals.proposer(id) }, decision.by, context.decisions);
|
|
182
|
+
assertMayDecide({ id, kind: MEETING_FINDINGS_KIND, proposedBy: proposals.proposer(id) }, decision.by, context.decisions);
|
|
183
183
|
if (batch.receipt) {
|
|
184
184
|
if (JSON.stringify(batch.receipt.accepted) !== JSON.stringify(accepted))
|
|
185
185
|
return invalid("this findings batch was already reviewed with a different decision");
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type MeetingStatsStore } from "../../meetings/index.js";
|
|
2
2
|
import type { StoreContext } from "./open.js";
|
|
3
|
+
/** One row per selected meeting through `meetings_by_time`; a current transcript is neither deleted nor superseded. */
|
|
4
|
+
export declare const meetingCountsSql: (where: string) => string;
|
|
3
5
|
/**
|
|
4
6
|
* One transcript per meeting — its earliest current one — so a local transcript beside the provider's does not count
|
|
5
7
|
* the talk twice. Words are counted from spaces in SQL, so no transcript text leaves the database.
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
import { aggregateMeetingStats } from "../../meetings/index.js";
|
|
2
|
+
import { aggregateMeetingCounts, aggregateMeetingStats, } from "../../meetings/index.js";
|
|
3
3
|
import { checkMeetingCancelled } from "./meeting-operation.js";
|
|
4
4
|
import { positiveReadInteger } from "./meeting-reads.js";
|
|
5
5
|
import { meetingSelectionSql } from "./meeting-selection.js";
|
|
6
6
|
import { read } from "./transaction.js";
|
|
7
7
|
const MOST_ROWS = 500_000;
|
|
8
|
+
/** One row per selected meeting through `meetings_by_time`; a current transcript is neither deleted nor superseded. */
|
|
9
|
+
export const meetingCountsSql = (where) => `SELECT m.started_at, coalesce(m.duration_ms, m.ended_at - m.started_at) AS duration_ms,
|
|
10
|
+
EXISTS (SELECT 1 FROM meeting_transcripts t WHERE t.meeting_id = m.id AND t.deleted_at IS NULL
|
|
11
|
+
AND t.superseded_at IS NULL) AS transcribed,
|
|
12
|
+
s.external_id AS series_key, CASE WHEN s.id IS NULL THEN NULL ELSE coalesce(s.title, m.title) END AS series_title
|
|
13
|
+
FROM meetings m LEFT JOIN meeting_series s ON s.id = m.meeting_series_id
|
|
14
|
+
WHERE ${where} ORDER BY m.started_at DESC`;
|
|
8
15
|
/**
|
|
9
16
|
* One transcript per meeting — its earliest current one — so a local transcript beside the provider's does not count
|
|
10
17
|
* the talk twice. Words are counted from spaces in SQL, so no transcript text leaves the database.
|
|
@@ -52,4 +59,23 @@ export const meetingStatsOver = ({ database }) => ({
|
|
|
52
59
|
})), input.timeZone);
|
|
53
60
|
});
|
|
54
61
|
},
|
|
62
|
+
async meetingCounts(input) {
|
|
63
|
+
checkMeetingCancelled(input.signal);
|
|
64
|
+
positiveReadInteger(input.accountId, "accountId");
|
|
65
|
+
return read(database, () => {
|
|
66
|
+
const { clauses, params } = meetingSelectionSql(input);
|
|
67
|
+
const meetings = database
|
|
68
|
+
.prepare(meetingCountsSql(clauses.join(" AND ")))
|
|
69
|
+
.all(...params)
|
|
70
|
+
.map((row) => ({
|
|
71
|
+
startedAt: row.started_at === null ? null : Number(row.started_at),
|
|
72
|
+
durationMs: row.duration_ms === null ? null : Number(row.duration_ms),
|
|
73
|
+
transcribed: Number(row.transcribed) === 1,
|
|
74
|
+
seriesKey: row.series_key === null ? null : String(row.series_key),
|
|
75
|
+
seriesTitle: row.series_title === null ? null : String(row.series_title),
|
|
76
|
+
}));
|
|
77
|
+
checkMeetingCancelled(input.signal);
|
|
78
|
+
return aggregateMeetingCounts(meetings, input.by, input.timeZone, input.limit);
|
|
79
|
+
});
|
|
80
|
+
},
|
|
55
81
|
});
|
|
@@ -83,5 +83,7 @@ type ProposalOperations = Omit<{
|
|
|
83
83
|
/** Who proposed an action, for `mayDecide`. */
|
|
84
84
|
export declare const proposerOfAction: ({ proposedBy, payload }: ProposedAction) => Proposer;
|
|
85
85
|
export declare const proposalOperationsOver: (context: StoreContext) => ProposalOperations;
|
|
86
|
+
/** Refuses a findings batch on a generic action path: approving one as an action would skip its own checks. */
|
|
87
|
+
export declare const assertGenericAction: (kind: string) => void;
|
|
86
88
|
export declare const proposedActionsStoreOver: (context: StoreContext) => ProposedActionsStore;
|
|
87
89
|
export {};
|