@wirecat/cli-core 0.551.0 → 0.553.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 +83 -0
- package/README.md +1 -1
- package/dist/background/schedules.d.ts +12 -1
- package/dist/background/schedules.js +65 -24
- package/dist/background/units.d.ts +16 -9
- package/dist/background/units.js +26 -11
- package/dist/cli/compact.d.ts +4 -0
- package/dist/cli/compact.js +65 -7
- package/dist/cli/context.d.ts +2 -0
- package/dist/cli/meeting-preparation-answer.d.ts +3 -0
- package/dist/cli/meeting-preparation-projects.d.ts +3 -0
- package/dist/cli/meeting-preparation-read.d.ts +3 -0
- package/dist/cli/messenger/context.d.ts +3 -0
- package/dist/cli/messenger/projects-command.js +38 -11
- package/dist/cli/messenger/search-answers.js +8 -1
- package/dist/cli/messenger/serve-catch-up.d.ts +43 -0
- package/dist/cli/messenger/serve-catch-up.js +90 -0
- package/dist/cli/messenger/serve-command.js +54 -1
- package/dist/cli/messenger/serve-relay.d.ts +1 -0
- package/dist/cli/messenger/server-command.d.ts +4 -3
- package/dist/cli/messenger/server-command.js +30 -12
- package/dist/cli/program.js +9 -4
- package/dist/cli/project-evidence.d.ts +33 -4
- package/dist/cli/project-evidence.js +38 -4
- package/dist/cli/project-status-rows.js +11 -1
- package/dist/cli/project-status.d.ts +24 -0
- package/dist/cli/project-status.js +52 -9
- package/dist/cli/schedule/schedule-command.js +5 -1
- package/dist/entities/hit.d.ts +5 -0
- package/dist/entities/hit.js +5 -0
- package/dist/entities/index.d.ts +27 -0
- package/dist/entities/listener.d.ts +6 -0
- package/dist/entities/listener.js +1 -0
- package/dist/entities/meeting-preparation.d.ts +3 -0
- package/dist/errors/reasons.js +1 -0
- package/dist/mcp/meeting-preparation.d.ts +3 -0
- package/dist/meetings/answers.d.ts +5 -0
- package/dist/meetings/answers.js +8 -0
- package/dist/meetings/cli/index.js +14 -0
- package/dist/meetings/index.d.ts +2 -1
- package/dist/meetings/index.js +1 -0
- package/dist/meetings/ingest.js +2 -1
- package/dist/meetings/language.d.ts +5 -0
- package/dist/meetings/language.js +57 -0
- package/dist/meetings/mcp/search.d.ts +3 -0
- package/dist/meetings/project-evidence-answer.d.ts +3 -0
- package/dist/meetings/result-items.d.ts +9 -0
- package/dist/meetings/result-items.js +7 -0
- package/dist/meetings/store.d.ts +22 -0
- package/dist/meetings/testing/memory.js +3 -0
- package/dist/meetings/transcription.d.ts +2 -0
- package/dist/meetings/transcription.js +5 -1
- package/dist/search/lucene/types.d.ts +7 -0
- package/dist/search/lucene/types.js +11 -1
- package/dist/services/index.d.ts +4 -3
- package/dist/services/index.js +4 -3
- package/dist/services/link-scorers/index.d.ts +1 -1
- package/dist/services/link-scorers/index.js +1 -1
- package/dist/services/link-scorers/scan.d.ts +0 -1
- package/dist/services/link-scorers/scan.js +1 -1
- package/dist/services/messages-archive-search.js +3 -2
- package/dist/services/messages-combined.js +2 -2
- package/dist/services/messages-discovery.js +2 -2
- package/dist/services/notes-search.js +4 -5
- package/dist/services/project-purposes.d.ts +28 -0
- package/dist/services/project-purposes.js +41 -0
- package/dist/services/project-status/index.d.ts +22 -8
- package/dist/services/project-status/index.js +17 -9
- package/dist/services/search-all-meetings.d.ts +3 -0
- package/dist/services/search-all-meetings.js +6 -1
- package/dist/services/stored-project-gathering.d.ts +79 -4
- package/dist/services/stored-project-gathering.js +171 -22
- package/dist/speech/local.d.ts +2 -0
- package/dist/speech/local.js +8 -1
- package/dist/speech/models.d.ts +2 -0
- package/dist/speech/models.js +2 -0
- package/dist/store/driver.d.ts +11 -0
- package/dist/store/driver.js +13 -0
- package/dist/store/drivers/node-sqlite.d.ts +1 -1
- package/dist/store/drivers/node-sqlite.js +3 -0
- package/dist/store/messages.d.ts +2 -1
- package/dist/store/messages.js +2 -2
- package/dist/store/sqlite/lucene.d.ts +1 -1
- package/dist/store/sqlite/lucene.js +33 -7
- package/dist/store/sqlite/meeting-hit-search.js +10 -3
- package/dist/store/sqlite/meeting-languages.d.ts +4 -0
- package/dist/store/sqlite/meeting-languages.js +36 -0
- package/dist/store/sqlite/meetings.d.ts +2 -1
- package/dist/store/sqlite/meetings.js +9 -3
- package/dist/store/sqlite/note-search.js +2 -2
- package/dist/store/sqlite/reads.d.ts +1 -1
- package/dist/store/sqlite/reads.js +2 -2
- package/package.json +1 -1
- package/parity.json +10 -0
|
@@ -139,6 +139,9 @@ export declare const callMeetingSearchTool: (search: MeetingHitSearch, input: un
|
|
|
139
139
|
id: string;
|
|
140
140
|
excerpt: string;
|
|
141
141
|
offset_ms: number | null;
|
|
142
|
+
language?: string | null | undefined;
|
|
143
|
+
line?: number | undefined;
|
|
144
|
+
lines_total?: number | undefined;
|
|
142
145
|
match?: "corrected" | "words" | undefined;
|
|
143
146
|
corrections?: {
|
|
144
147
|
from: string;
|
|
@@ -24,6 +24,9 @@ export declare const meetingProjectEvidence: v.StrictObjectSchema<{
|
|
|
24
24
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
25
25
|
readonly excerpt: v.StringSchema<undefined>;
|
|
26
26
|
readonly offset_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
27
|
+
readonly language: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
28
|
+
readonly line: v.OptionalSchema<v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, v.MinValueAction<number, 1, undefined>]>, undefined>;
|
|
29
|
+
readonly lines_total: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
27
30
|
readonly match: v.OptionalSchema<v.PicklistSchema<["words", "corrected"], undefined>, undefined>;
|
|
28
31
|
readonly corrections: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
29
32
|
readonly from: v.StringSchema<undefined>;
|
|
@@ -124,4 +124,13 @@ export declare const MEETING_RESULT_ENTITIES: {
|
|
|
124
124
|
readonly next_transcript_id: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
|
|
125
125
|
}, undefined>;
|
|
126
126
|
}, undefined>], undefined>;
|
|
127
|
+
readonly meeting_repair: v.StrictObjectSchema<{
|
|
128
|
+
readonly account_id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
129
|
+
readonly applied: v.BooleanSchema<undefined>;
|
|
130
|
+
readonly transcripts: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
131
|
+
readonly languages: v.ArraySchema<v.StrictObjectSchema<{
|
|
132
|
+
readonly language: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
133
|
+
readonly transcripts: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
134
|
+
}, undefined>, undefined>;
|
|
135
|
+
}, undefined>;
|
|
127
136
|
};
|
|
@@ -116,10 +116,17 @@ const embedding = v.union([
|
|
|
116
116
|
}),
|
|
117
117
|
}),
|
|
118
118
|
]);
|
|
119
|
+
const repair = v.strictObject({
|
|
120
|
+
account_id: identifier,
|
|
121
|
+
applied: flag,
|
|
122
|
+
transcripts: count,
|
|
123
|
+
languages: v.array(v.strictObject({ language: nullableText, transcripts: count })),
|
|
124
|
+
});
|
|
119
125
|
export const MEETING_RESULT_ENTITIES = {
|
|
120
126
|
meeting_ingestion: run,
|
|
121
127
|
meeting_export: exported,
|
|
122
128
|
meeting_association: meetingChange,
|
|
123
129
|
meeting_transcription: transcription,
|
|
124
130
|
meeting_embedding: embedding,
|
|
131
|
+
meeting_repair: repair,
|
|
125
132
|
};
|
package/dist/meetings/store.d.ts
CHANGED
|
@@ -90,6 +90,11 @@ export interface SearchHit {
|
|
|
90
90
|
startMs: number | null;
|
|
91
91
|
/** Who spoke a transcript hit's cue, as the transcript names them; absent for chat and summary hits. */
|
|
92
92
|
speakerName?: string | null;
|
|
93
|
+
/** A transcript hit's transcript language (`ru`, `en`…), `null` when unknown; absent for chat and summary hits. */
|
|
94
|
+
language?: string | null;
|
|
95
|
+
/** A transcript hit's line, from 1, of `linesTotal` lines in its transcript: where in the meeting it was said. */
|
|
96
|
+
line?: number;
|
|
97
|
+
linesTotal?: number;
|
|
93
98
|
/** `corrected` when a query word matched only as a stored word recognition may have garbled. */
|
|
94
99
|
match?: "words" | "corrected";
|
|
95
100
|
/** For a `corrected` hit, the stored words each such query word matched in this passage. */
|
|
@@ -126,4 +131,21 @@ export interface MeetingStore {
|
|
|
126
131
|
reviewMeetingLink?(meetingId: number, eventIds: number[], now: number): Promise<void>;
|
|
127
132
|
cursor(accountId: number): Promise<string | null>;
|
|
128
133
|
setCursor(accountId: number, value: string, now: number): Promise<void>;
|
|
134
|
+
/** Detects the language of the account's transcripts that have none; stores it only with `apply`. */
|
|
135
|
+
fillTranscriptLanguages?(input: {
|
|
136
|
+
accountId: number;
|
|
137
|
+
apply: boolean;
|
|
138
|
+
now: number;
|
|
139
|
+
}): Promise<TranscriptLanguageFill>;
|
|
140
|
+
}
|
|
141
|
+
export interface TranscriptLanguageFill {
|
|
142
|
+
accountId: number;
|
|
143
|
+
applied: boolean;
|
|
144
|
+
/** Transcripts that had no language. */
|
|
145
|
+
transcripts: number;
|
|
146
|
+
/** How many of them each language was found in; `null` where the text did not show one. */
|
|
147
|
+
languages: {
|
|
148
|
+
language: string | null;
|
|
149
|
+
transcripts: number;
|
|
150
|
+
}[];
|
|
129
151
|
}
|
|
@@ -121,6 +121,9 @@ export const memoryMeetingStore = () => {
|
|
|
121
121
|
text: r.text,
|
|
122
122
|
startMs: r.startMs,
|
|
123
123
|
speakerName: r.speakerName,
|
|
124
|
+
language: t.transcript.language,
|
|
125
|
+
line: r.position + 1,
|
|
126
|
+
linesTotal: t.rows.length,
|
|
124
127
|
})));
|
|
125
128
|
rows.push(...d.chat.map((r) => ({
|
|
126
129
|
meetingId: d.meeting.id,
|
|
@@ -13,6 +13,8 @@ export interface MeetingMediaDescriptor {
|
|
|
13
13
|
model: string;
|
|
14
14
|
version?: string;
|
|
15
15
|
timing?: "fixed-windows" | "recognition-segments";
|
|
16
|
+
/** The one language the model writes (`ru`); without it the language is read from the recognized text. */
|
|
17
|
+
language?: string;
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
export interface MeetingRecognitionSegment {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { contentHash } from "./ingest.js";
|
|
3
|
+
import { transcriptLanguage } from "./language.js";
|
|
3
4
|
import { dispatchMeetingMutation } from "./meeting-management.js";
|
|
4
5
|
const bounded = (value, max, name) => {
|
|
5
6
|
if (!Number.isSafeInteger(value) || value < 1 || value > max)
|
|
@@ -46,6 +47,9 @@ export const transcribeMeeting = async (source, store, target, options = {}) =>
|
|
|
46
47
|
const timing = recognition.timing ?? "recognition-segments";
|
|
47
48
|
if (timing !== "fixed-windows" && timing !== "recognition-segments")
|
|
48
49
|
throw new CliError("invalid_response", "unsupported recognition timing");
|
|
50
|
+
const modelLanguage = recognition.language;
|
|
51
|
+
if (modelLanguage !== undefined && (typeof modelLanguage !== "string" || !/^[a-z]{2,3}$/.test(modelLanguage)))
|
|
52
|
+
throw new CliError("invalid_response", "recognition language must be a two- or three-letter code");
|
|
49
53
|
const media = Object.freeze({
|
|
50
54
|
id: identifier(raw.id),
|
|
51
55
|
contentHash: raw.contentHash,
|
|
@@ -137,7 +141,7 @@ export const transcribeMeeting = async (source, store, target, options = {}) =>
|
|
|
137
141
|
source: `local/${encodeURIComponent(media.recognition.engine)}/${encodeURIComponent(media.recognition.model)}`,
|
|
138
142
|
format: "local-transcription",
|
|
139
143
|
}),
|
|
140
|
-
language:
|
|
144
|
+
language: modelLanguage ?? transcriptLanguage(rows.map((row) => row.text)),
|
|
141
145
|
contentHash: hash,
|
|
142
146
|
externalCreatedAt: null,
|
|
143
147
|
metadata: provenance,
|
|
@@ -22,4 +22,11 @@ export declare const MESSAGE_PARSING: {
|
|
|
22
22
|
/** Further apart than this, two words are rarely about one thing, even in a long message. */
|
|
23
23
|
export declare const MOST_WORDS_APART = 50;
|
|
24
24
|
export declare function searchError(reason: string, span: Span, alternative?: string): never;
|
|
25
|
+
/**
|
|
26
|
+
* The search ran out of time, not the query out of bounds: a valid query over a large store. `timeout`, so a caller
|
|
27
|
+
* does not tell the person to fix what they typed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function outOfTime(limitMs?: number): never;
|
|
30
|
+
/** A search limit that leaves the answer without a part, such as its total, rather than failing it. */
|
|
31
|
+
export declare const isQueryLimit: (error: unknown) => boolean;
|
|
25
32
|
export declare function exhausted(budget: string): never;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliError } from "@wirecat/cli-base";
|
|
1
|
+
import { CliError, isCliError } from "@wirecat/cli-base";
|
|
2
2
|
import { QUERY_LIMITS as PARSER_LIMITS, queryError } from "@wirecat/cli-base/query";
|
|
3
3
|
export const QUERY_VERSION = 1;
|
|
4
4
|
export const QUERY_LIMITS = {
|
|
@@ -22,6 +22,16 @@ export const MOST_WORDS_APART = 50;
|
|
|
22
22
|
export function searchError(reason, span, alternative) {
|
|
23
23
|
queryError(reason, span, alternative, SEARCH_QUERY);
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The search ran out of time, not the query out of bounds: a valid query over a large store. `timeout`, so a caller
|
|
27
|
+
* does not tell the person to fix what they typed.
|
|
28
|
+
*/
|
|
29
|
+
export function outOfTime(limitMs = QUERY_LIMITS.milliseconds) {
|
|
30
|
+
throw new CliError("timeout", `search stopped at its time limit of ${limitMs / 1000}s — the query is valid; ` +
|
|
31
|
+
"add chat: or date: to search less of the store", { reason: "query.time-limit", budget: "time", limit_ms: limitMs, complete: false });
|
|
32
|
+
}
|
|
33
|
+
/** A search limit that leaves the answer without a part, such as its total, rather than failing it. */
|
|
34
|
+
export const isQueryLimit = (error) => isCliError(error) && (error.details.reason === "query.too-large" || error.details.reason === "query.time-limit");
|
|
25
35
|
export function exhausted(budget) {
|
|
26
36
|
throw new CliError("validation_error", `search exceeded its ${budget} budget — narrow the chat or date range`, {
|
|
27
37
|
reason: "query.too-large",
|
package/dist/services/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { type ChatCoverage, chatCoverage } from "./chat-coverage.js";
|
|
|
6
6
|
export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextEvidenceSender, ContextIndex, ContextReadLimits, ContextReadUsage, ContextRecordReader, ContextSource, ScopedContextEvidenceReader, } from "./context-evidence.js";
|
|
7
7
|
export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
|
|
8
8
|
export { excerpt } from "./excerpt.js";
|
|
9
|
-
export { aliasHits,
|
|
9
|
+
export { aliasHits, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, type LinkCandidate, type LinkScorer, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput as LinkScanInput, type ScannedProject, type ScannedProposal, type ScanResult as LinkScanResult, type ScorerInput, type Signal as LinkSignal, scanProjectLinks, } from "./link-scorers/index.js";
|
|
10
10
|
export type { MediaArchiveOptions, MediaArchivePorts } from "./media-archive.js";
|
|
11
11
|
export { archiveMedia, checkMediaArchive } from "./media-archive.js";
|
|
12
12
|
export type { MediaArchiveEntry, MediaIdentity } from "./media-archive-manifest.js";
|
|
@@ -21,9 +21,10 @@ export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./pr
|
|
|
21
21
|
export type { EvidenceMention, EvidenceMentions } from "./project-gathering-mentions.js";
|
|
22
22
|
export { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
23
23
|
export type { ProjectChatFetched, ProjectGatheringBudgets, ProjectGatheringRequest, ProjectLane, ProjectLaneKind, ProjectLinkedChat, ProjectPerson, ProjectSender, ProjectUnitState, } from "./project-gathering-request.js";
|
|
24
|
-
export {
|
|
24
|
+
export { BUILT_IN_PURPOSE_CONFIG, BUILT_IN_PURPOSES, DEFAULT_PURPOSES, LINK_PURPOSE, type PurposeChoice, type PurposeConfig, purposeList, purposeRead, purposesOf, } from "./project-purposes.js";
|
|
25
|
+
export { type MoneyTotal, type OutOfScope, type ProjectStatus, type ProjectStatusInput, projectStatus, STATUS_CHATS, STATUS_SIGNALS, STATUS_WINDOW, type StatusAttention, type StatusChat, type StatusChatSummary, type StatusContext, type StatusItem, type StatusSignal, } from "./project-status/index.js";
|
|
25
26
|
export { questions } from "./questions.js";
|
|
26
|
-
export { canonicalProjectRef, gatherStoredProject, type LinkedChat, linkedChatsOf, type StoredProjectGathering, type StoredProjectInput, } from "./stored-project-gathering.js";
|
|
27
|
+
export { type ChatCount, type ChatCounter, canonicalProjectRef, gatherStoredProject, type LeftOutChat, type LinkedChat, linkedChatsOf, type ProjectMember, type StoredProjectGathering, type StoredProjectInput, storedChatCount, } from "./stored-project-gathering.js";
|
|
27
28
|
import { type AccountService } from "./account.js";
|
|
28
29
|
import { type AdminService } from "./admin.js";
|
|
29
30
|
import { type AdminStatisticsService } from "./admin-statistics.js";
|
package/dist/services/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { AGENDA_DEFAULTS } from "./agenda.js";
|
|
|
5
5
|
export { chatCoverage } from "./chat-coverage.js";
|
|
6
6
|
export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
|
|
7
7
|
export { excerpt } from "./excerpt.js";
|
|
8
|
-
export { aliasHits,
|
|
8
|
+
export { aliasHits, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./link-scorers/index.js";
|
|
9
9
|
export { archiveMedia, checkMediaArchive } from "./media-archive.js";
|
|
10
10
|
export { moveMediaArchive } from "./media-archive-move.js";
|
|
11
11
|
export { downloadImageModel, downloadTextModel, } from "./model-downloads.js";
|
|
@@ -13,9 +13,10 @@ export { MONEY_DONE, MONEY_REQUEST, MONEY_WORDS, moneyClassesOf, PROMISE } from
|
|
|
13
13
|
export { gatherProjectEvidence } from "./project-gathering.js";
|
|
14
14
|
export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
|
|
15
15
|
export { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
16
|
-
export { DEFAULT_PURPOSES,
|
|
16
|
+
export { BUILT_IN_PURPOSE_CONFIG, BUILT_IN_PURPOSES, DEFAULT_PURPOSES, LINK_PURPOSE, purposeList, purposeRead, purposesOf, } from "./project-purposes.js";
|
|
17
|
+
export { projectStatus, STATUS_CHATS, STATUS_SIGNALS, STATUS_WINDOW, } from "./project-status/index.js";
|
|
17
18
|
export { questions } from "./questions.js";
|
|
18
|
-
export { canonicalProjectRef, gatherStoredProject, linkedChatsOf, } from "./stored-project-gathering.js";
|
|
19
|
+
export { canonicalProjectRef, gatherStoredProject, linkedChatsOf, storedChatCount, } from "./stored-project-gathering.js";
|
|
19
20
|
import { accountService } from "./account.js";
|
|
20
21
|
import { adminService } from "./admin.js";
|
|
21
22
|
import { adminStatisticsService } from "./admin-statistics.js";
|
|
@@ -5,5 +5,5 @@ export { emailDomain } from "./email-domain.js";
|
|
|
5
5
|
export { mailThread } from "./mail-thread.js";
|
|
6
6
|
export { memberChats } from "./member-chats.js";
|
|
7
7
|
export { memberDialog } from "./member-dialog.js";
|
|
8
|
-
export {
|
|
8
|
+
export { combinedScore, DEFAULT_LIMIT, DEFAULT_THRESHOLD, PERSON_SCORERS, PERSON_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput, scanProjectLinks, } from "./scan.js";
|
|
9
9
|
export type { LinkCandidate, LinkScorer, ScannedProject, ScannedProposal, ScanResult, ScorerInput, Signal, } from "./types.js";
|
|
@@ -5,4 +5,4 @@ export { emailDomain } from "./email-domain.js";
|
|
|
5
5
|
export { mailThread } from "./mail-thread.js";
|
|
6
6
|
export { memberChats } from "./member-chats.js";
|
|
7
7
|
export { memberDialog } from "./member-dialog.js";
|
|
8
|
-
export {
|
|
8
|
+
export { combinedScore, DEFAULT_LIMIT, DEFAULT_THRESHOLD, PERSON_SCORERS, PERSON_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./scan.js";
|
|
@@ -10,7 +10,6 @@ export declare const SCAN_BOT: {
|
|
|
10
10
|
readonly bot: "memo-scan";
|
|
11
11
|
readonly kind: "rule";
|
|
12
12
|
};
|
|
13
|
-
export declare const BUILT_IN_PURPOSES: readonly ["work", "sales", "support", "other"];
|
|
14
13
|
export declare const DEFAULT_THRESHOLD = 0.5;
|
|
15
14
|
/** A person ↔ person scan keeps the old rule's reach: one weak signal is enough to ask the owner. */
|
|
16
15
|
export declare const PERSON_THRESHOLD = 0.3;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { parseReference } from "../../domain/references.js";
|
|
3
|
+
import { BUILT_IN_PURPOSES } from "../project-purposes.js";
|
|
3
4
|
import { canonicalProjectRef } from "../stored-project-gathering.js";
|
|
4
5
|
import { aliasHits } from "./alias-hits.js";
|
|
5
6
|
import { chatSenders } from "./chat-senders.js";
|
|
@@ -15,7 +16,6 @@ export const SCORERS = [chatTitle, aliasHits, memberChats, memberDialog, chatSen
|
|
|
15
16
|
export const PERSON_SCORERS = [emailDomain];
|
|
16
17
|
/** The rule bot every scan proposal is written by: only the owner decides its proposals. */
|
|
17
18
|
export const SCAN_BOT = { bot: "memo-scan", kind: "rule" };
|
|
18
|
-
export const BUILT_IN_PURPOSES = ["work", "sales", "support", "other"];
|
|
19
19
|
export const DEFAULT_THRESHOLD = 0.5;
|
|
20
20
|
/** A person ↔ person scan keeps the old rule's reach: one weak signal is enough to ask the owner. */
|
|
21
21
|
export const PERSON_THRESHOLD = 0.3;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { CliError
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { parseLocator } from "../domain/locator.js";
|
|
3
|
+
import { isQueryLimit } from "../search/lucene/types.js";
|
|
3
4
|
import { TOTAL_CAP, totalOf } from "../search/total.js";
|
|
4
5
|
import { discoveryRequest, searchDiscovery } from "./messages-discovery.js";
|
|
5
6
|
import { countLucene, searchLucene, statsLucene } from "./messages-search.js";
|
|
@@ -53,7 +54,7 @@ export const searchStore = async (store, account, request, messenger = {}, meani
|
|
|
53
54
|
const total = discovery
|
|
54
55
|
? totalOf(discovery.candidates, found.items.length, found.hasMore, !discovery.truncated)
|
|
55
56
|
: totalOf(await countLucene(store, account, { ...(request.discover ? discoveryRequest(request) : request), limit: request.limit + offset }, messenger, TOTAL_CAP).catch((error) => {
|
|
56
|
-
if (
|
|
57
|
+
if (isQueryLimit(error))
|
|
57
58
|
return undefined;
|
|
58
59
|
throw error;
|
|
59
60
|
}), found.items.length, found.hasMore);
|
|
@@ -4,7 +4,7 @@ import { parseLocator } from "../domain/locator.js";
|
|
|
4
4
|
import { correctWords } from "../search/correct.js";
|
|
5
5
|
import { PRESET_VERSION } from "../search/lucene/presets.js";
|
|
6
6
|
import { FIELD_VERSION } from "../search/lucene/registry.js";
|
|
7
|
-
import { exhausted, QUERY_LIMITS } from "../search/lucene/types.js";
|
|
7
|
+
import { exhausted, outOfTime, QUERY_LIMITS } from "../search/lucene/types.js";
|
|
8
8
|
import { normalize } from "../store/normalize.js";
|
|
9
9
|
import { withExcerpt } from "./excerpt.js";
|
|
10
10
|
import { coverageOf, prepareLucene } from "./messages-search.js";
|
|
@@ -31,7 +31,7 @@ export const searchCombined = async (store, account, request, messenger = {}, op
|
|
|
31
31
|
if (request.signal?.aborted)
|
|
32
32
|
throw new CliError("validation_error", "search was aborted", { reason: "query.aborted", complete: false });
|
|
33
33
|
if (performance.now() - started > QUERY_LIMITS.milliseconds)
|
|
34
|
-
|
|
34
|
+
outOfTime();
|
|
35
35
|
};
|
|
36
36
|
check();
|
|
37
37
|
const spans = request.ast === undefined && !request.exact && request.text !== undefined
|
|
@@ -3,7 +3,7 @@ import { implicitTextTerms, parseLucene, walkQuery } from "@wirecat/cli-base/que
|
|
|
3
3
|
import { parseLocator } from "../domain/locator.js";
|
|
4
4
|
import { fieldsOfQuery, meaningWords } from "../search/lucene/meaning-parts.js";
|
|
5
5
|
import { validateFields } from "../search/lucene/registry.js";
|
|
6
|
-
import { exhausted, MESSAGE_PARSING, QUERY_LIMITS } from "../search/lucene/types.js";
|
|
6
|
+
import { exhausted, MESSAGE_PARSING, outOfTime, QUERY_LIMITS } from "../search/lucene/types.js";
|
|
7
7
|
import { planQuestion, words } from "../search/question-plan.js";
|
|
8
8
|
import { searchCombined } from "./messages-combined.js";
|
|
9
9
|
import { prepareLucene, searchLucene } from "./messages-search.js";
|
|
@@ -101,7 +101,7 @@ export const searchDiscovery = async (store, account, asked, messenger = {}, mea
|
|
|
101
101
|
if (request.signal?.aborted)
|
|
102
102
|
throw new CliError("validation_error", "search was aborted", { reason: "query.aborted", complete: false });
|
|
103
103
|
if (performance.now() - started > QUERY_LIMITS.milliseconds)
|
|
104
|
-
|
|
104
|
+
outOfTime();
|
|
105
105
|
};
|
|
106
106
|
const union = new Map();
|
|
107
107
|
let truncated = false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliError
|
|
1
|
+
import { CliError } 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";
|
|
@@ -9,7 +9,7 @@ import { entriesOfList, fitting } from "../search/account-list.js";
|
|
|
9
9
|
import { fieldsOfQuery as fieldsOf, meaningWords } from "../search/lucene/meaning-parts.js";
|
|
10
10
|
import { validateFields } from "../search/lucene/registry.js";
|
|
11
11
|
import { hasStems } from "../search/lucene/resolved.js";
|
|
12
|
-
import { SEARCH_QUERY } from "../search/lucene/types.js";
|
|
12
|
+
import { isQueryLimit, SEARCH_QUERY } from "../search/lucene/types.js";
|
|
13
13
|
import { createStemmer, DEFAULT_STEMMERS } from "../search/stem.js";
|
|
14
14
|
import { TOTAL_CAP, totalOf } from "../search/total.js";
|
|
15
15
|
import { openPictureEmbedderFor } from "./document-pictures.js";
|
|
@@ -286,12 +286,11 @@ const filterRefs = async (store, narrowing, scope) => {
|
|
|
286
286
|
return found.hasMore ? undefined : found.refs;
|
|
287
287
|
}
|
|
288
288
|
catch (error) {
|
|
289
|
-
if (
|
|
289
|
+
if (isQueryLimit(error))
|
|
290
290
|
return undefined;
|
|
291
291
|
throw error;
|
|
292
292
|
}
|
|
293
293
|
};
|
|
294
|
-
const tooLarge = (error) => isCliError(error) && error.details.reason === "query.too-large";
|
|
295
294
|
/**
|
|
296
295
|
* The notes the words find, with those found by meaning: every note the search would list, paged to the end. Never
|
|
297
296
|
* below what the page already showed; a count the store refuses as too costly is that lower bound.
|
|
@@ -313,7 +312,7 @@ const notesTotal = async (store, text, { exact, scope, shown, hasMore, }, others
|
|
|
313
312
|
});
|
|
314
313
|
}
|
|
315
314
|
catch (error) {
|
|
316
|
-
if (
|
|
315
|
+
if (isQueryLimit(error))
|
|
317
316
|
return totalOf(undefined, shown, hasMore);
|
|
318
317
|
throw error;
|
|
319
318
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** What a chat can be for in a project, when the host's config names none. */
|
|
2
|
+
export declare const BUILT_IN_PURPOSES: readonly string[];
|
|
3
|
+
/** What `projects evidence` and `projects status` read when no purpose is asked for (built in; memo's config may change it). */
|
|
4
|
+
export declare const DEFAULT_PURPOSES: readonly string[];
|
|
5
|
+
/** A link with no purpose is a work chat. */
|
|
6
|
+
export declare const LINK_PURPOSE = "work";
|
|
7
|
+
/** The purposes a host's config lists; core knows only the built-in ones and takes the rest as given. */
|
|
8
|
+
export interface PurposeConfig {
|
|
9
|
+
purposes: readonly string[];
|
|
10
|
+
/** Read when no `--purpose` is given. */
|
|
11
|
+
defaults: readonly string[];
|
|
12
|
+
/** A project's own defaults, by its key or `project:<id>`. */
|
|
13
|
+
projects?: Readonly<Record<string, readonly string[]>>;
|
|
14
|
+
}
|
|
15
|
+
export declare const BUILT_IN_PURPOSE_CONFIG: PurposeConfig;
|
|
16
|
+
/** The purposes one run reads: `all`, or a set. */
|
|
17
|
+
export type PurposeChoice = "all" | ReadonlySet<string>;
|
|
18
|
+
/**
|
|
19
|
+
* The purposes a run reads: the asked ones, each checked against the config's list, else the project's own defaults,
|
|
20
|
+
* else the config's. `all` reads every purpose, a link's unlisted one too.
|
|
21
|
+
*/
|
|
22
|
+
export declare const purposesOf: (asked: readonly string[] | undefined, config?: PurposeConfig, project?: {
|
|
23
|
+
ref: string | null;
|
|
24
|
+
key: string | null;
|
|
25
|
+
}) => PurposeChoice;
|
|
26
|
+
export declare const purposeRead: (choice: PurposeChoice, purpose: string | null) => boolean;
|
|
27
|
+
/** `a,b` and repeated flags as one list. */
|
|
28
|
+
export declare const purposeList: (value: string, previous?: string[]) => string[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
/** What a chat can be for in a project, when the host's config names none. */
|
|
3
|
+
export const BUILT_IN_PURPOSES = ["work", "sales", "support", "other"];
|
|
4
|
+
/** What `projects evidence` and `projects status` read when no purpose is asked for (built in; memo's config may change it). */
|
|
5
|
+
export const DEFAULT_PURPOSES = ["work"];
|
|
6
|
+
/** A link with no purpose is a work chat. */
|
|
7
|
+
export const LINK_PURPOSE = "work";
|
|
8
|
+
export const BUILT_IN_PURPOSE_CONFIG = { purposes: BUILT_IN_PURPOSES, defaults: DEFAULT_PURPOSES };
|
|
9
|
+
const unknown = (asked, config) => {
|
|
10
|
+
const wrong = asked.filter((one) => !config.purposes.includes(one));
|
|
11
|
+
if (wrong.length)
|
|
12
|
+
throw new CliError("validation_error", `unknown purpose ${wrong.join(", ")}; the configured ones are ${config.purposes.join(", ")}, or all`, { param: "purpose", purposes: [...config.purposes] });
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The purposes a run reads: the asked ones, each checked against the config's list, else the project's own defaults,
|
|
16
|
+
* else the config's. `all` reads every purpose, a link's unlisted one too.
|
|
17
|
+
*/
|
|
18
|
+
export const purposesOf = (asked, config = BUILT_IN_PURPOSE_CONFIG, project) => {
|
|
19
|
+
const words = (asked ?? []).map((one) => one.trim()).filter(Boolean);
|
|
20
|
+
if (words.includes("all")) {
|
|
21
|
+
if (words.length > 1)
|
|
22
|
+
throw new CliError("validation_error", "--purpose all reads every purpose; give it alone");
|
|
23
|
+
return "all";
|
|
24
|
+
}
|
|
25
|
+
if (words.length) {
|
|
26
|
+
unknown(words, config);
|
|
27
|
+
return new Set(words);
|
|
28
|
+
}
|
|
29
|
+
const own = (project?.key ? config.projects?.[project.key] : undefined) ??
|
|
30
|
+
(project?.ref ? config.projects?.[project.ref] : undefined);
|
|
31
|
+
return new Set(own ?? config.defaults);
|
|
32
|
+
};
|
|
33
|
+
export const purposeRead = (choice, purpose) => choice === "all" || choice.has(purpose ?? LINK_PURPOSE);
|
|
34
|
+
/** `a,b` and repeated flags as one list. */
|
|
35
|
+
export const purposeList = (value, previous = []) => [
|
|
36
|
+
...previous,
|
|
37
|
+
...value
|
|
38
|
+
.split(",")
|
|
39
|
+
.map((one) => one.trim())
|
|
40
|
+
.filter(Boolean),
|
|
41
|
+
];
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { Id } from "../../domain/models.js";
|
|
1
2
|
import type { AccountKey, MessageStore } from "../../store/store.js";
|
|
3
|
+
import { type PurposeConfig } from "../project-purposes.js";
|
|
4
|
+
import { type ChatCount, type ChatCounter } from "../stored-project-gathering.js";
|
|
2
5
|
import { STATUS_SIGNALS } from "./signals/index.js";
|
|
3
6
|
import type { StatusAttention, StatusChat, StatusItem, StatusProject, StatusSignal } from "./types.js";
|
|
4
7
|
export type * from "./types.js";
|
|
@@ -7,8 +10,6 @@ export { STATUS_SIGNALS };
|
|
|
7
10
|
export declare const STATUS_WINDOW = 300;
|
|
8
11
|
/** Linked chats read at most, as `projects evidence` reads. */
|
|
9
12
|
export declare const STATUS_CHATS = 16;
|
|
10
|
-
/** What `projects status` reads when no purpose is asked for (built in; memo's config may change it). */
|
|
11
|
-
export declare const DEFAULT_PURPOSES: readonly string[];
|
|
12
13
|
export interface ProjectStatusInput {
|
|
13
14
|
/** An id, key, name or alias, as `canonicalProjectRef` takes it. */
|
|
14
15
|
project: string;
|
|
@@ -18,12 +19,27 @@ export interface ProjectStatusInput {
|
|
|
18
19
|
after?: number;
|
|
19
20
|
asOf?: number;
|
|
20
21
|
contextQueries?: string[];
|
|
21
|
-
/** The chat purposes read; `all` reads every one.
|
|
22
|
+
/** The chat purposes read; `["all"]` reads every one. Left out, the config's defaults. */
|
|
22
23
|
purposes?: readonly string[];
|
|
24
|
+
/** The host's purposes; core's built-in ones when left out. */
|
|
25
|
+
purposeConfig?: PurposeConfig;
|
|
26
|
+
/** How many messages a chat left out by purpose holds in the window; the store's own count when left out. */
|
|
27
|
+
count?: ChatCounter;
|
|
23
28
|
/** Default `STATUS_SIGNALS`; tests pass their own. */
|
|
24
29
|
signals?: readonly StatusSignal[];
|
|
25
30
|
}
|
|
26
31
|
export type StatusChatSummary = Omit<StatusChat, "messages" | "account">;
|
|
32
|
+
/** A linked chat not read; one left out by its purpose says how much it holds in the window. */
|
|
33
|
+
export type OutOfScope = {
|
|
34
|
+
chat: string;
|
|
35
|
+
reason: "other-account" | "linked-chat-limit";
|
|
36
|
+
} | ({
|
|
37
|
+
chat: string;
|
|
38
|
+
reason: "purpose";
|
|
39
|
+
purpose: string;
|
|
40
|
+
account: AccountKey;
|
|
41
|
+
chatId: Id;
|
|
42
|
+
} & ChatCount);
|
|
27
43
|
export interface MoneyTotal {
|
|
28
44
|
direction: string;
|
|
29
45
|
currency: string | null;
|
|
@@ -48,11 +64,9 @@ export interface ProjectStatus {
|
|
|
48
64
|
money: {
|
|
49
65
|
open: MoneyTotal[];
|
|
50
66
|
};
|
|
51
|
-
outOfScope:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
purpose?: string;
|
|
55
|
-
}[];
|
|
67
|
+
outOfScope: OutOfScope[];
|
|
68
|
+
/** The purposes read: `all`, or the list. */
|
|
69
|
+
purposes: "all" | string[];
|
|
56
70
|
failed: {
|
|
57
71
|
signal: string;
|
|
58
72
|
why: string;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { formatReference } from "../../domain/references.js";
|
|
3
3
|
import { chatCoverage } from "../chat-coverage.js";
|
|
4
|
-
import {
|
|
4
|
+
import { LINK_PURPOSE, purposeRead, purposesOf } from "../project-purposes.js";
|
|
5
|
+
import { canonicalProjectRef, linkedChatsOf, storedChatCount, } from "../stored-project-gathering.js";
|
|
5
6
|
import { centsText } from "./signals/amounts.js";
|
|
6
7
|
import { STATUS_SIGNALS } from "./signals/index.js";
|
|
7
8
|
export { STATUS_SIGNALS };
|
|
@@ -12,8 +13,6 @@ export const STATUS_WINDOW = 300;
|
|
|
12
13
|
export const STATUS_CHATS = 16;
|
|
13
14
|
const CHILD_DEPTH = 2;
|
|
14
15
|
const CHILDREN = 8;
|
|
15
|
-
/** What `projects status` reads when no purpose is asked for (built in; memo's config may change it). */
|
|
16
|
-
export const DEFAULT_PURPOSES = ["work"];
|
|
17
16
|
const projectsCatalog = async (store) => new Map((await store.knowledge.vocabulary({ kind: "project" })).entries.map((entry) => [`project:${entry.id}`, entry]));
|
|
18
17
|
/** Confirmed `member-of` links from another project; depth two, eight at most, a project seen twice skipped. */
|
|
19
18
|
const childrenOf = async (store, root, catalog) => {
|
|
@@ -92,13 +91,14 @@ export const projectStatus = async (store, input) => {
|
|
|
92
91
|
if (!(since < now))
|
|
93
92
|
throw new CliError("validation_error", "the window must start before it ends");
|
|
94
93
|
const signals = input.signals ?? STATUS_SIGNALS;
|
|
95
|
-
const purposes = new Set(input.purposes ?? DEFAULT_PURPOSES);
|
|
96
94
|
const catalog = await projectsCatalog(store);
|
|
97
95
|
const ref = await canonicalProjectRef(store, input.project);
|
|
98
96
|
const entry = catalog.get(ref);
|
|
99
97
|
if (!entry)
|
|
100
98
|
throw new CliError("not_found", "the owner project is unavailable");
|
|
101
99
|
const root = { ref, key: entry.key, name: entry.name };
|
|
100
|
+
const purposes = purposesOf(input.purposes, input.purposeConfig, root);
|
|
101
|
+
const count = input.count ?? storedChatCount(store);
|
|
102
102
|
const { children, cut } = await childrenOf(store, root, catalog);
|
|
103
103
|
const outOfScope = [];
|
|
104
104
|
const linked = new Map();
|
|
@@ -110,10 +110,17 @@ export const projectStatus = async (store, input) => {
|
|
|
110
110
|
}
|
|
111
111
|
const chosen = [];
|
|
112
112
|
for (const [chat, link] of linked) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
if (!purposeRead(purposes, link.purpose)) {
|
|
114
|
+
const account = { provider: link.provider, account: link.account };
|
|
115
|
+
outOfScope.push({
|
|
116
|
+
chat,
|
|
117
|
+
reason: "purpose",
|
|
118
|
+
purpose: link.purpose ?? LINK_PURPOSE,
|
|
119
|
+
account,
|
|
120
|
+
chatId: link.chat,
|
|
121
|
+
...(await count(account, link.chat, { since, until: now })),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
117
124
|
else if (!inAccounts(input.accounts, link.provider, link.account))
|
|
118
125
|
outOfScope.push({ chat, reason: "other-account" });
|
|
119
126
|
else if (chosen.length >= STATUS_CHATS)
|
|
@@ -134,7 +141,7 @@ export const projectStatus = async (store, input) => {
|
|
|
134
141
|
title: coverage?.title ?? null,
|
|
135
142
|
kind: (await store.chats.chatKind(account, link.chat)) ?? "unknown",
|
|
136
143
|
role: link.role,
|
|
137
|
-
purpose:
|
|
144
|
+
purpose: link.purpose ?? LINK_PURPOSE,
|
|
138
145
|
project: link.project,
|
|
139
146
|
coverage: coverage,
|
|
140
147
|
messages: page.items.filter((message) => Date.parse(message.timestamp) <= now),
|
|
@@ -189,6 +196,7 @@ export const projectStatus = async (store, input) => {
|
|
|
189
196
|
items,
|
|
190
197
|
money: { open: totalsOf(items) },
|
|
191
198
|
outOfScope,
|
|
199
|
+
purposes: purposes === "all" ? "all" : [...purposes],
|
|
192
200
|
failed,
|
|
193
201
|
attention,
|
|
194
202
|
signals: signals.map(({ id, version }) => ({ id, version })),
|
|
@@ -22,6 +22,9 @@ export interface MeetingSearchItem {
|
|
|
22
22
|
/** A transcript hit's cue, `meeting:<account>/<meeting>/<transcript>/<cue>`, which `meetings passage` opens. */
|
|
23
23
|
cueRef?: string;
|
|
24
24
|
speakerName?: string | null;
|
|
25
|
+
language?: string | null;
|
|
26
|
+
line?: number;
|
|
27
|
+
linesTotal?: number;
|
|
25
28
|
}
|
|
26
29
|
export interface MeetingSearchCursor {
|
|
27
30
|
accountId: number;
|
|
@@ -105,7 +105,12 @@ const collectMeetings = async (store, account, request, query) => {
|
|
|
105
105
|
id: hit.id,
|
|
106
106
|
startMs: hit.startMs,
|
|
107
107
|
...(hit.scope === "transcript" && hit.ref !== undefined
|
|
108
|
-
? {
|
|
108
|
+
? {
|
|
109
|
+
cueRef: hit.ref,
|
|
110
|
+
speakerName: hit.speakerName ?? null,
|
|
111
|
+
language: hit.language ?? null,
|
|
112
|
+
...(hit.line === undefined ? {} : { line: hit.line, linesTotal: hit.linesTotal }),
|
|
113
|
+
}
|
|
109
114
|
: {}),
|
|
110
115
|
},
|
|
111
116
|
});
|