@wirecat/cli-core 0.379.0 → 0.380.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 +52 -0
- package/dist/cli/meeting-preparation-answer.d.ts +5 -0
- package/dist/cli/meeting-preparation-projects.d.ts +5 -0
- package/dist/cli/meeting-preparation-read.d.ts +5 -0
- package/dist/cli/messenger/chats-stats-command.js +5 -1
- package/dist/cli/messenger/contacts-command.js +8 -4
- package/dist/cli/messenger/stats-answers.d.ts +2 -1
- package/dist/cli/messenger/stats-answers.js +1 -1
- package/dist/cli/messenger/stats-command.js +4 -9
- package/dist/entities/index.d.ts +15 -0
- package/dist/entities/meeting-preparation.d.ts +5 -0
- package/dist/mcp/meeting-preparation.d.ts +5 -0
- package/dist/mcp/tools/stats.js +9 -6
- package/dist/meetings/answers.d.ts +6 -0
- package/dist/meetings/answers.js +5 -0
- package/dist/meetings/cli/index.d.ts +2 -0
- package/dist/meetings/cli/index.js +5 -2
- package/dist/meetings/cli/search.js +1 -1
- package/dist/meetings/hit-query.d.ts +12 -0
- package/dist/meetings/hit-query.js +58 -0
- package/dist/meetings/import.d.ts +2 -0
- package/dist/meetings/import.js +8 -2
- package/dist/meetings/index.d.ts +2 -0
- package/dist/meetings/index.js +2 -0
- package/dist/meetings/mcp/search.d.ts +6 -0
- package/dist/meetings/mcp/search.js +5 -1
- package/dist/meetings/project-evidence-answer.d.ts +5 -0
- package/dist/meetings/pull.d.ts +2 -0
- package/dist/meetings/recording-schedule.js +6 -5
- package/dist/meetings/search-store.d.ts +3 -1
- package/dist/meetings/store.d.ts +7 -0
- package/dist/meetings/transcript-text.d.ts +20 -0
- package/dist/meetings/transcript-text.js +59 -0
- package/dist/render/markdown.js +2 -2
- package/dist/search/garbled.d.ts +29 -0
- package/dist/search/garbled.js +125 -0
- package/dist/search/russian-dictionary.d.ts +5 -0
- package/dist/search/russian-dictionary.js +66 -0
- package/dist/services/meeting-project-discovery.js +2 -2
- package/dist/services/moment.d.ts +21 -0
- package/dist/services/moment.js +19 -0
- package/dist/services/retention.js +6 -3
- package/dist/store/sqlite/meeting-hit-query.d.ts +60 -0
- package/dist/store/sqlite/meeting-hit-query.js +262 -0
- package/dist/store/sqlite/meeting-hit-search.js +28 -45
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,58 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.380.0 — 21.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Meetings import text transcripts.** `parseTranscriptText` reads the text form zoom-searcher wrote: the header
|
|
15
|
+
(`Встреча:` title, `Дата:` start as written, `Zoom ID:`, `Говорят:` speakers) and `[hh:mm:ss] Speaker: text`
|
|
16
|
+
blocks, a block running over several lines. `ImportFile.format: "text"` makes `importFiles` use it; `vtt` stays
|
|
17
|
+
the default. The file keeps start times only, so a line ends where the next begins.
|
|
18
|
+
- **An import says the zone it read file times in, and a host can explain its file names.** `RunReport.timezone` is
|
|
19
|
+
set when every imported file's meeting carries one zone; `MeetingCommandDeps.importHelp` adds the host's note
|
|
20
|
+
(file name patterns, how their time is read) under `meetings import --help`.
|
|
21
|
+
- **Meeting search finds words spread over neighbouring lines.** `"циан риелтор"~3` in a meeting query finds a
|
|
22
|
+
transcript line with one of the words when the others are at most three lines away in the same transcript; `~0`
|
|
23
|
+
is one line, any order; at most 20. The words widen to garbled spellings as single words do. It needs
|
|
24
|
+
`@wirecat/cli-base` 0.49.0 (`parseLucene({ proximity: true })`), now the pinned version and the peer range.
|
|
25
|
+
See [query language](docs/search/query-language.md#встречи).
|
|
26
|
+
|
|
27
|
+
- **Meeting search finds words speech recognition garbled.** `meetings.searchHits` widens each query word of four
|
|
28
|
+
letters or more to stored words one stem edit away that a Russian dictionary does not know, and to stems one side
|
|
29
|
+
extends, so «риелтор» finds «реелтора», «рилторов» and «дриелтора», «циана» finds «цианом», and «договор» does not
|
|
30
|
+
find «поговорить». Hits come typed form first, then other forms, then garbled spellings; a widened hit carries `match: "corrected"` and `corrections`, and `meeting_hit` items show both. Number words never
|
|
31
|
+
widen, and match only their own forms («семь» finds «семи», not «семьдесят»). See [meetings](docs/dev/MEETINGS.md).
|
|
32
|
+
- **Answers say which zone a typed date was read in, and the UTC window it gave** (cli-base `WC-TIME-3`, `WC-TIME-5`).
|
|
33
|
+
`stats charts`, the MCP `stats` tool and `contacts timeline` put `meta.timezone` (only when a value had no zone of
|
|
34
|
+
its own) and `meta.window: { since?, until? }` in UTC in their answers. `dateWindow` in `./cli`'s moment helpers
|
|
35
|
+
builds both from `--since-time`/`--until-time`. Other date filters follow.
|
|
36
|
+
|
|
37
|
+
### Changed — may break callers
|
|
38
|
+
|
|
39
|
+
- **Meeting search reads the messages query language.** `meetings.searchHits`, `search meetings` and MCP
|
|
40
|
+
`search_meetings` take `OR`, `(groups)`, `NOT`/`-word`, `"phrases"` (neighbouring words, any form), `exact:` and
|
|
41
|
+
`from:"Name"` — who said the line or wrote the chat message, a person through all their linked identities; an
|
|
42
|
+
ambiguous name is an error with the candidates. `:`, `(`, `"`, `-` and `AND`/`OR`/`NOT` in a meeting query are
|
|
43
|
+
now syntax, and a query of punctuation alone is a `validation_error`. `searchHits` also takes a `QueryAst`;
|
|
44
|
+
`meetingWordsQuery(text)` builds one from text that must stay plain words, as project discovery now does.
|
|
45
|
+
`MEETING_HIT_QUERY` and `parseMeetingQuery` are exported. See [query language](docs/search/query-language.md#встречи).
|
|
46
|
+
|
|
47
|
+
- **Meeting search reads `exact:` and has new cursors.** `exact:word` and `exact:"two words"` in a
|
|
48
|
+
`meetings.searchHits` query keep those words to their typed form, where `exact` was a search word before. Exact
|
|
49
|
+
hits now come before widened ones, so cursors from earlier versions are refused; start the search again.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **A recording archive schedule counts "today" in its own zone.** The window's end, its ten-year floor and its
|
|
54
|
+
look-back started from the UTC date, so a run between local midnight and UTC midnight worked on yesterday. The
|
|
55
|
+
schedule's `timeZone` was stored and never used for this.
|
|
56
|
+
- **A Markdown chat archive says which zone its times are in.** The file printed local wall times with no zone; it
|
|
57
|
+
now has `Times are in <zone>.` under the title.
|
|
58
|
+
- **`chats stats show` and `stats chats retention` read `--since-time`/`--until-time` in their `--timezone`.** They
|
|
59
|
+
used the computer's zone even when `--timezone` was given, and retention's `--until-time` dropped the day it named;
|
|
60
|
+
it now covers that whole local day.
|
|
61
|
+
|
|
10
62
|
## 0.379.0 — 21.10.2026
|
|
11
63
|
|
|
12
64
|
### Changed — may break callers
|
|
@@ -309,6 +309,11 @@ export declare const meetingPreparationAnswer: (packet: MeetingPreparation) => i
|
|
|
309
309
|
id: string;
|
|
310
310
|
excerpt: string;
|
|
311
311
|
offset_ms: number | null;
|
|
312
|
+
match?: "corrected" | "words" | undefined;
|
|
313
|
+
corrections?: {
|
|
314
|
+
from: string;
|
|
315
|
+
to: string[];
|
|
316
|
+
}[] | undefined;
|
|
312
317
|
source?: {
|
|
313
318
|
ref: string;
|
|
314
319
|
title: string | null;
|
|
@@ -29,6 +29,11 @@ export declare const mapPreparationProjectEvidence: (section: NonNullable<Meetin
|
|
|
29
29
|
id: string;
|
|
30
30
|
excerpt: string;
|
|
31
31
|
offset_ms: number | null;
|
|
32
|
+
match?: "corrected" | "words" | undefined;
|
|
33
|
+
corrections?: {
|
|
34
|
+
from: string;
|
|
35
|
+
to: string[];
|
|
36
|
+
}[] | undefined;
|
|
32
37
|
source?: {
|
|
33
38
|
ref: string;
|
|
34
39
|
title: string | null;
|
|
@@ -338,6 +338,11 @@ export declare const readMeetingPreparation: (read: MeetingPreparationReader, in
|
|
|
338
338
|
id: string;
|
|
339
339
|
excerpt: string;
|
|
340
340
|
offset_ms: number | null;
|
|
341
|
+
match?: "corrected" | "words" | undefined;
|
|
342
|
+
corrections?: {
|
|
343
|
+
from: string;
|
|
344
|
+
to: string[];
|
|
345
|
+
}[] | undefined;
|
|
341
346
|
source?: {
|
|
342
347
|
ref: string;
|
|
343
348
|
title: string | null;
|
|
@@ -21,7 +21,11 @@ export const statsCommand = (messenger) => annotate(new Command("show"), { effec
|
|
|
21
21
|
refuseJsonl(context.format);
|
|
22
22
|
const { stats, account } = await context.withServices(async (services) => ({
|
|
23
23
|
stats: await services.chats.stats(chat, {
|
|
24
|
-
...(sinceTime === undefined
|
|
24
|
+
...(sinceTime === undefined
|
|
25
|
+
? {}
|
|
26
|
+
: {
|
|
27
|
+
since: momentOf(sinceTime, "--since-time", Date.now(), timezone === undefined ? {} : { zone: timezone }),
|
|
28
|
+
}),
|
|
25
29
|
...(by ? { by } : {}),
|
|
26
30
|
...(timezone === undefined ? {} : { timezone }),
|
|
27
31
|
}),
|
|
@@ -4,7 +4,7 @@ import { Command } from "commander";
|
|
|
4
4
|
import { REGISTRIES, registriesCover } from "../../botcheck/registries.js";
|
|
5
5
|
import { levelFor } from "../../sends/permissions.js";
|
|
6
6
|
import { phoneOf } from "../../services/index.js";
|
|
7
|
-
import { momentOf } from "../../services/moment.js";
|
|
7
|
+
import { dateWindow, momentOf } from "../../services/moment.js";
|
|
8
8
|
import { maskedAccount } from "../../services/people.js";
|
|
9
9
|
import { CHAT_MESSAGES, CONTEXT_BYTES, CONTEXT_MESSAGES } from "../../services/person-context.js";
|
|
10
10
|
import { SCOPES, TIMELINE_ITEMS } from "../../services/person-timeline.js";
|
|
@@ -150,10 +150,14 @@ export const contactsCommand = (messenger) => {
|
|
|
150
150
|
if (scope !== undefined && !SCOPES.includes(scope))
|
|
151
151
|
throw new CliError("validation_error", `--scope is personal or work, not "${scope}"`);
|
|
152
152
|
const context = messengerContext(this, messenger);
|
|
153
|
+
const window = dateWindow({
|
|
154
|
+
...(sinceTime === undefined ? {} : { sinceTime }),
|
|
155
|
+
...(untilTime === undefined ? {} : { untilTime }),
|
|
156
|
+
});
|
|
153
157
|
const found = await context.withServices((services) => services.people.timeline(person, {
|
|
154
158
|
...(scope === undefined ? {} : { scope: scope }),
|
|
155
|
-
...(
|
|
156
|
-
...(
|
|
159
|
+
...(window.since === undefined ? {} : { since: window.since }),
|
|
160
|
+
...(window.until === undefined ? {} : { until: window.until }),
|
|
157
161
|
...(limit === undefined ? {} : { limit }),
|
|
158
162
|
...(cursor === undefined ? {} : { cursor }),
|
|
159
163
|
}));
|
|
@@ -170,7 +174,7 @@ export const contactsCommand = (messenger) => {
|
|
|
170
174
|
...(sinceTime === undefined ? {} : { since_time: sinceTime }),
|
|
171
175
|
...(untilTime === undefined ? {} : { until_time: untilTime }),
|
|
172
176
|
},
|
|
173
|
-
...(pending > 0 ? {
|
|
177
|
+
meta: { ...window.meta, ...(pending > 0 ? { truncated: true } : {}) },
|
|
174
178
|
}));
|
|
175
179
|
});
|
|
176
180
|
annotate(contacts.command("check"), { effects: REMOTE_READ })
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Meta } from "@wirecat/cli-base/standards";
|
|
1
2
|
import type { InferOutput } from "valibot";
|
|
2
3
|
import type { ChartData } from "../../charts/model.js";
|
|
3
4
|
import type { CounterState } from "../../domain/counters.js";
|
|
@@ -62,7 +63,7 @@ export declare const chartAnswer: (chart: ChartData, file?: {
|
|
|
62
63
|
format: "svg" | "png";
|
|
63
64
|
width: number;
|
|
64
65
|
height: number;
|
|
65
|
-
}) => ItemBody<Of<"chart">>;
|
|
66
|
+
}, meta?: Partial<Meta>) => ItemBody<Of<"chart">>;
|
|
66
67
|
/** A report or a chart is one object: `--jsonl` would print one line pretending to be a stream. */
|
|
67
68
|
export declare const refuseJsonl: (format: string) => void;
|
|
68
69
|
/** `stats chats show`, CLI and MCP alike; a chat the store does not hold whole names the sync that fills it. */
|
|
@@ -326,7 +326,7 @@ export const officialAnswer = (stats, provider) => {
|
|
|
326
326
|
graphs: stats.graphs,
|
|
327
327
|
}));
|
|
328
328
|
};
|
|
329
|
-
export const chartAnswer = (chart, file) => itemOf(defined({ ...chart, file }));
|
|
329
|
+
export const chartAnswer = (chart, file, meta) => itemOf(defined({ ...chart, file }), meta ? { meta } : {});
|
|
330
330
|
/** A report or a chart is one object: `--jsonl` would print one line pretending to be a stream. */
|
|
331
331
|
export const refuseJsonl = (format) => {
|
|
332
332
|
if (format === "jsonl")
|
|
@@ -9,7 +9,7 @@ import { CHART_SIZE } from "../../charts/model.js";
|
|
|
9
9
|
import { chartPng } from "../../charts/png.js";
|
|
10
10
|
import { chartRenderer } from "../../charts/render.js";
|
|
11
11
|
import { EVENTS_DAYS } from "../../services/chats.js";
|
|
12
|
-
import {
|
|
12
|
+
import { dateWindow } from "../../services/moment.js";
|
|
13
13
|
import { destroysNothing, LOCAL_WRITE } from "../effects.js";
|
|
14
14
|
import { renderAnswer } from "../envelope.js";
|
|
15
15
|
import { adminStatisticsCommand } from "./admin-statistics-command.js";
|
|
@@ -61,13 +61,8 @@ export const statsCommand = (messenger, loadRenderer = chartRenderer) => {
|
|
|
61
61
|
throw new CliError("validation_error", "--output takes a .svg or .png file; image output to stdout is unavailable");
|
|
62
62
|
}
|
|
63
63
|
const timezone = timezoneOf(given);
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
timezone,
|
|
67
|
-
...(sinceTime === undefined
|
|
68
|
-
? {}
|
|
69
|
-
: { since: momentOf(sinceTime, "--since-time", Date.now(), { zone: timezone }) }),
|
|
70
|
-
}));
|
|
64
|
+
const window = dateWindow({ ...(sinceTime === undefined ? {} : { sinceTime }), zone: timezone });
|
|
65
|
+
const found = await context.withServices((services) => services.chats.stats(chat, { by, timezone, ...(window.since === undefined ? {} : { since: window.since }) }));
|
|
71
66
|
const chart = chatChart(found, { kind: chartKind, by, timezone });
|
|
72
67
|
let chartFile;
|
|
73
68
|
if (output !== undefined) {
|
|
@@ -87,7 +82,7 @@ export const statsCommand = (messenger, loadRenderer = chartRenderer) => {
|
|
|
87
82
|
}
|
|
88
83
|
if (chart.partial)
|
|
89
84
|
context.renderer.note("partial data — the chart shows lower bounds");
|
|
90
|
-
renderAnswer(this, context, chartAnswer(chart, chartFile));
|
|
85
|
+
renderAnswer(this, context, chartAnswer(chart, chartFile, window.meta));
|
|
91
86
|
});
|
|
92
87
|
return stats;
|
|
93
88
|
};
|
package/dist/entities/index.d.ts
CHANGED
|
@@ -2106,6 +2106,11 @@ export declare const ENTITIES: {
|
|
|
2106
2106
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
2107
2107
|
readonly excerpt: v.StringSchema<undefined>;
|
|
2108
2108
|
readonly offset_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
2109
|
+
readonly match: v.OptionalSchema<v.PicklistSchema<["words", "corrected"], undefined>, undefined>;
|
|
2110
|
+
readonly corrections: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
2111
|
+
readonly from: v.StringSchema<undefined>;
|
|
2112
|
+
readonly to: v.ArraySchema<v.StringSchema<undefined>, undefined>;
|
|
2113
|
+
}, undefined>, undefined>, undefined>;
|
|
2109
2114
|
readonly source: v.OptionalSchema<v.StrictObjectSchema<{
|
|
2110
2115
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
2111
2116
|
readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
@@ -2257,6 +2262,11 @@ export declare const ENTITIES: {
|
|
|
2257
2262
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
2258
2263
|
readonly excerpt: v.StringSchema<undefined>;
|
|
2259
2264
|
readonly offset_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
2265
|
+
readonly match: v.OptionalSchema<v.PicklistSchema<["words", "corrected"], undefined>, undefined>;
|
|
2266
|
+
readonly corrections: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
2267
|
+
readonly from: v.StringSchema<undefined>;
|
|
2268
|
+
readonly to: v.ArraySchema<v.StringSchema<undefined>, undefined>;
|
|
2269
|
+
}, undefined>, undefined>, undefined>;
|
|
2260
2270
|
readonly source: v.OptionalSchema<v.StrictObjectSchema<{
|
|
2261
2271
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
2262
2272
|
readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
@@ -2614,6 +2624,11 @@ export declare const ENTITIES: {
|
|
|
2614
2624
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
2615
2625
|
readonly excerpt: v.StringSchema<undefined>;
|
|
2616
2626
|
readonly offset_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
2627
|
+
readonly match: v.OptionalSchema<v.PicklistSchema<["words", "corrected"], undefined>, undefined>;
|
|
2628
|
+
readonly corrections: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
2629
|
+
readonly from: v.StringSchema<undefined>;
|
|
2630
|
+
readonly to: v.ArraySchema<v.StringSchema<undefined>, undefined>;
|
|
2631
|
+
}, undefined>, undefined>, undefined>;
|
|
2617
2632
|
readonly source: v.OptionalSchema<v.StrictObjectSchema<{
|
|
2618
2633
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
2619
2634
|
readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
@@ -309,6 +309,11 @@ export declare const meetingPreparation: v.StrictObjectSchema<{
|
|
|
309
309
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
310
310
|
readonly excerpt: v.StringSchema<undefined>;
|
|
311
311
|
readonly offset_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
312
|
+
readonly match: v.OptionalSchema<v.PicklistSchema<["words", "corrected"], undefined>, undefined>;
|
|
313
|
+
readonly corrections: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
314
|
+
readonly from: v.StringSchema<undefined>;
|
|
315
|
+
readonly to: v.ArraySchema<v.StringSchema<undefined>, undefined>;
|
|
316
|
+
}, undefined>, undefined>, undefined>;
|
|
312
317
|
readonly source: v.OptionalSchema<v.StrictObjectSchema<{
|
|
313
318
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
314
319
|
readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
@@ -376,6 +376,11 @@ export declare const callMeetingPreparationTool: (read: MeetingPreparationReader
|
|
|
376
376
|
id: string;
|
|
377
377
|
excerpt: string;
|
|
378
378
|
offset_ms: number | null;
|
|
379
|
+
match?: "corrected" | "words" | undefined;
|
|
380
|
+
corrections?: {
|
|
381
|
+
from: string;
|
|
382
|
+
to: string[];
|
|
383
|
+
}[] | undefined;
|
|
379
384
|
source?: {
|
|
380
385
|
ref: string;
|
|
381
386
|
title: string | null;
|
package/dist/mcp/tools/stats.js
CHANGED
|
@@ -7,7 +7,7 @@ import { chartRenderer } from "../../charts/render.js";
|
|
|
7
7
|
import { sealed } from "../../cli/envelope.js";
|
|
8
8
|
import { chartAnswer } from "../../cli/messenger/stats-answers.js";
|
|
9
9
|
import { servicesFor, storedDeps } from "../../services/index.js";
|
|
10
|
-
import {
|
|
10
|
+
import { dateWindow } from "../../services/moment.js";
|
|
11
11
|
import { chatOf, Picture, READ, tool } from "../tool.js";
|
|
12
12
|
export const statsTools = (messenger) => ({
|
|
13
13
|
stats_charts: tool({
|
|
@@ -29,18 +29,21 @@ export const statsTools = (messenger) => ({
|
|
|
29
29
|
stored: async (store, account, args, defaults) => {
|
|
30
30
|
const timezone = timezoneOf(args.timezone);
|
|
31
31
|
const by = args.by ?? "day";
|
|
32
|
+
const window = dateWindow({
|
|
33
|
+
...(args.since_time === undefined ? {} : { sinceTime: args.since_time }),
|
|
34
|
+
zone: timezone,
|
|
35
|
+
names: ["since_time", "until_time"],
|
|
36
|
+
});
|
|
32
37
|
const stats = await servicesFor(storedDeps(messenger, store, account, defaults.guard)).chats.stats(args.chat, {
|
|
33
38
|
by,
|
|
34
39
|
timezone,
|
|
35
|
-
...(
|
|
36
|
-
? {}
|
|
37
|
-
: { since: momentOf(args.since_time, "since_time", Date.now(), { zone: timezone }) }),
|
|
40
|
+
...(window.since === undefined ? {} : { since: window.since }),
|
|
38
41
|
});
|
|
39
42
|
const chart = chatChart(stats, { kind: args.chart_kind ?? "messages", by, timezone });
|
|
40
43
|
if (args.format !== "png")
|
|
41
|
-
return chartAnswer(chart);
|
|
44
|
+
return chartAnswer(chart, undefined, window.meta);
|
|
42
45
|
const image = await chartPng(await (await chartRenderer()).render(chart, CHART_SIZE));
|
|
43
|
-
return new Picture(image.bytes, image.mimeType, sealed(chartAnswer(chart)));
|
|
46
|
+
return new Picture(image.bytes, image.mimeType, sealed(chartAnswer(chart, undefined, window.meta)));
|
|
44
47
|
},
|
|
45
48
|
}),
|
|
46
49
|
});
|
|
@@ -15,6 +15,12 @@ export declare const meetingHit: v.StrictObjectSchema<{
|
|
|
15
15
|
readonly excerpt: v.StringSchema<undefined>;
|
|
16
16
|
/** How far into the meeting the passage is. */
|
|
17
17
|
readonly offset_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
18
|
+
/** `corrected`: a query word matched only a garbled form of itself, listed in `corrections`. */
|
|
19
|
+
readonly match: v.OptionalSchema<v.PicklistSchema<["words", "corrected"], undefined>, undefined>;
|
|
20
|
+
readonly corrections: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
21
|
+
readonly from: v.StringSchema<undefined>;
|
|
22
|
+
readonly to: v.ArraySchema<v.StringSchema<undefined>, undefined>;
|
|
23
|
+
}, undefined>, undefined>, undefined>;
|
|
18
24
|
readonly source: v.OptionalSchema<v.StrictObjectSchema<{
|
|
19
25
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
20
26
|
readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
package/dist/meetings/answers.js
CHANGED
|
@@ -16,6 +16,9 @@ export const meetingHit = v.strictObject({
|
|
|
16
16
|
excerpt: text,
|
|
17
17
|
/** How far into the meeting the passage is. */
|
|
18
18
|
offset_ms: v.nullable(count),
|
|
19
|
+
/** `corrected`: a query word matched only a garbled form of itself, listed in `corrections`. */
|
|
20
|
+
match: v.optional(v.picklist(["words", "corrected"])),
|
|
21
|
+
corrections: v.optional(v.array(v.strictObject({ from: text, to: v.array(text) }))),
|
|
19
22
|
source: v.optional(v.strictObject({
|
|
20
23
|
ref: reference,
|
|
21
24
|
title: v.nullable(text),
|
|
@@ -185,6 +188,8 @@ export const meetingHitItem = (hit) => ({
|
|
|
185
188
|
id: String(hit.id),
|
|
186
189
|
excerpt: hit.text,
|
|
187
190
|
offset_ms: hit.startMs,
|
|
191
|
+
...(hit.match === undefined ? {} : { match: hit.match }),
|
|
192
|
+
...(hit.corrections === undefined ? {} : { corrections: hit.corrections }),
|
|
188
193
|
...(hit.source === undefined
|
|
189
194
|
? {}
|
|
190
195
|
: {
|
|
@@ -7,6 +7,8 @@ export interface MeetingCommandDeps {
|
|
|
7
7
|
accountId: number;
|
|
8
8
|
source?: MeetingSource;
|
|
9
9
|
readFiles?: (folder: string) => Promise<ImportFile[]>;
|
|
10
|
+
/** The host's file names and how it reads their time, shown in `meetings import --help`. */
|
|
11
|
+
importHelp?: string;
|
|
10
12
|
writeFile?: (path: string, content: string) => Promise<void>;
|
|
11
13
|
write: (value: unknown, format: "json" | "jsonl" | "text") => void;
|
|
12
14
|
now?: () => number;
|
|
@@ -122,9 +122,12 @@ export const addMeetingCommands = (program, deps) => {
|
|
|
122
122
|
lookbackMs: opts.lookbackDays * 86400000,
|
|
123
123
|
}), command);
|
|
124
124
|
});
|
|
125
|
-
|
|
125
|
+
const importCommand = annotate(group.command("import"), { effects: LOCAL_WRITE })
|
|
126
126
|
.description("import downloaded meeting transcripts")
|
|
127
|
-
.argument("<folder>", "folder of transcripts downloaded from the provider")
|
|
127
|
+
.argument("<folder>", "folder of transcripts downloaded from the provider");
|
|
128
|
+
if (deps.importHelp)
|
|
129
|
+
importCommand.addHelpText("after", `\n${deps.importHelp}`);
|
|
130
|
+
output(importCommand).action(async (folder, _opts, command) => {
|
|
128
131
|
if (!deps.readFiles)
|
|
129
132
|
throw new MeetingError("configuration_error", "file reader is unavailable");
|
|
130
133
|
const files = await deps.readFiles(folder);
|
|
@@ -28,7 +28,7 @@ export const createMeetingSearchCommand = (program) => {
|
|
|
28
28
|
const command = search
|
|
29
29
|
.command("meetings")
|
|
30
30
|
.description("find words in stored meetings, indexing new ones first")
|
|
31
|
-
.argument("<query>",
|
|
31
|
+
.argument("<query>", 'words, OR, (groups), -word, "a phrase", "a b"~N (within N lines), exact:word, from:"Name" (who said the line); a word also matches its other forms and spellings speech recognition garbled')
|
|
32
32
|
.option("--meeting-id <id>", "only this stored meeting", integer)
|
|
33
33
|
.option("--identity-id <id>", "meetings with this participant identity; does not say who spoke a hit", integer)
|
|
34
34
|
.option("--person-id <id>", "meetings with a participant linked to this person", integer)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type QueryAst } from "@wirecat/cli-base/query";
|
|
2
|
+
/** A transcript line holds a sentence or two; further than this is a different part of the meeting. */
|
|
3
|
+
export declare const MOST_LINES_APART = 20;
|
|
4
|
+
/** The messages query language over meeting text: the same fields, a narrower set of operators. */
|
|
5
|
+
export declare const MEETING_HIT_QUERY: import("@wirecat/cli-base/query").QueryRegistry;
|
|
6
|
+
/** What a person typed: the CLI, MCP and batch queries. */
|
|
7
|
+
export declare const parseMeetingQuery: (query: string) => QueryAst;
|
|
8
|
+
/**
|
|
9
|
+
* Words from a name or a note, never parsed: a project called «C++ (beta)» must not become syntax. Each word is
|
|
10
|
+
* required, in any form.
|
|
11
|
+
*/
|
|
12
|
+
export declare const meetingWordsQuery: (text: string) => QueryAst;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defineQuery, parseLucene, queryError, validateQuery, } from "@wirecat/cli-base/query";
|
|
2
|
+
const context = {
|
|
3
|
+
command: "search meetings",
|
|
4
|
+
guide: "https://github.com/WireCatLabs/cli-core/blob/main/docs/search/query-language.md#встречи",
|
|
5
|
+
};
|
|
6
|
+
const words = ["term", "phrase"];
|
|
7
|
+
/** A transcript line holds a sentence or two; further than this is a different part of the meeting. */
|
|
8
|
+
export const MOST_LINES_APART = 20;
|
|
9
|
+
const near = (node) => {
|
|
10
|
+
if (node.slop !== undefined && node.slop > MOST_LINES_APART)
|
|
11
|
+
queryError("query.invalid-number", node.span, `words at most ${MOST_LINES_APART} lines apart: "a b"~${MOST_LINES_APART}`, context);
|
|
12
|
+
};
|
|
13
|
+
/** The messages query language over meeting text: the same fields, a narrower set of operators. */
|
|
14
|
+
export const MEETING_HIT_QUERY = defineQuery({
|
|
15
|
+
entity: "meeting_hit",
|
|
16
|
+
backend: "sql",
|
|
17
|
+
fields: [
|
|
18
|
+
{
|
|
19
|
+
name: "text",
|
|
20
|
+
type: "txt",
|
|
21
|
+
operators: words,
|
|
22
|
+
example: "text:budget",
|
|
23
|
+
relation: "a passage with the word in any form, or a spelling speech recognition garbled",
|
|
24
|
+
validate: near,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "exact",
|
|
28
|
+
type: "txt",
|
|
29
|
+
operators: words,
|
|
30
|
+
example: "exact:budget",
|
|
31
|
+
relation: "a passage with the word as typed: no other forms, no garbled spellings",
|
|
32
|
+
validate: near,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "from",
|
|
36
|
+
type: "kw",
|
|
37
|
+
operators: words,
|
|
38
|
+
example: 'from:"Alice Synthetic"',
|
|
39
|
+
relation: "a line said or a chat message written by this person; attendance is --identity-id",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
/** What a person typed: the CLI, MCP and batch queries. */
|
|
44
|
+
export const parseMeetingQuery = (query) => validateQuery(parseLucene(query, { defaultField: "text", proximity: true, ...context }), MEETING_HIT_QUERY, context);
|
|
45
|
+
/**
|
|
46
|
+
* Words from a name or a note, never parsed: a project called «C++ (beta)» must not become syntax. Each word is
|
|
47
|
+
* required, in any form.
|
|
48
|
+
*/
|
|
49
|
+
export const meetingWordsQuery = (text) => {
|
|
50
|
+
const found = text.match(/[\p{L}\p{N}\p{M}]+/gu) ?? [];
|
|
51
|
+
const span = { start: 0, end: text.length };
|
|
52
|
+
const clauses = found.map((value) => ({
|
|
53
|
+
occur: "must",
|
|
54
|
+
node: { kind: "predicate", field: "text", operator: "term", value, span },
|
|
55
|
+
}));
|
|
56
|
+
const root = clauses.length === 1 && clauses[0] ? clauses[0].node : { kind: "boolean", clauses, span };
|
|
57
|
+
return { version: 1, language: "lucene-v1", root };
|
|
58
|
+
};
|
|
@@ -5,6 +5,8 @@ export interface ImportFile {
|
|
|
5
5
|
content: string;
|
|
6
6
|
participants?: ParticipantInput[];
|
|
7
7
|
source?: string;
|
|
8
|
+
/** `vtt` (the default) or `text`, the form `parseTranscriptText` reads. */
|
|
9
|
+
format?: "vtt" | "text";
|
|
8
10
|
}
|
|
9
11
|
/** The caller reads files and supplies provider-neutral occurrence metadata. */
|
|
10
12
|
export declare const importFiles: (files: ImportFile[], store: MeetingStore, now?: number) => Promise<RunReport>;
|
package/dist/meetings/import.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { linkEvent } from "./events.js";
|
|
2
2
|
import { transcriptInput } from "./ingest.js";
|
|
3
|
+
import { parseTranscriptText } from "./transcript-text.js";
|
|
3
4
|
import { parseVtt } from "./vtt.js";
|
|
4
5
|
/** The caller reads files and supplies provider-neutral occurrence metadata. */
|
|
5
6
|
export const importFiles = async (files, store, now = Date.now()) => {
|
|
6
7
|
const report = { meetings: 0, participants: 0, transcriptRows: 0, summaries: 0, warnings: [] };
|
|
7
8
|
for (const file of files) {
|
|
8
9
|
try {
|
|
9
|
-
const
|
|
10
|
+
const format = file.format ?? "vtt";
|
|
11
|
+
const lines = format === "text" ? parseTranscriptText(file.content).lines : parseVtt(file.content);
|
|
10
12
|
const participants = file.participants ?? [];
|
|
11
13
|
const details = await store.saveMeeting({
|
|
12
14
|
meeting: file.meeting,
|
|
13
15
|
participants: file.participants,
|
|
14
|
-
transcripts: [await transcriptInput(lines, participants, file.source ?? "file", file.content,
|
|
16
|
+
transcripts: [await transcriptInput(lines, participants, file.source ?? "file", file.content, format)],
|
|
15
17
|
now,
|
|
16
18
|
});
|
|
17
19
|
await linkEvent(store, details, now);
|
|
@@ -26,5 +28,9 @@ export const importFiles = async (files, store, now = Date.now()) => {
|
|
|
26
28
|
});
|
|
27
29
|
}
|
|
28
30
|
}
|
|
31
|
+
const zones = new Set(files.map((file) => file.meeting.timezone).filter((zone) => zone !== null && zone !== undefined));
|
|
32
|
+
const [zone] = zones;
|
|
33
|
+
if (zones.size === 1 && zone)
|
|
34
|
+
report.timezone = zone;
|
|
29
35
|
return report;
|
|
30
36
|
};
|
package/dist/meetings/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { linkEvent } from "./events.js";
|
|
|
6
6
|
export { exportMeeting, type MeetingExport, type MeetingExportFormat, type MeetingExportOptions, serializeVtt, } from "./export.js";
|
|
7
7
|
export { type FindingEvidence, type FindingOutcome, findingHash, MEETING_FINDINGS_KIND, type MeetingFactFinding, type MeetingFinding, type MeetingFindingsInput, type MeetingFindingsReceipt, type MeetingTaskFinding, meetingFindingFingerprint, validateMeetingFindings, } from "./findings.js";
|
|
8
8
|
export { type Finding, type FindingsInput, type FindingsReceipt, type FindingsRules, findingFingerprint, validateFindings, } from "./findings-rules.js";
|
|
9
|
+
export { MEETING_HIT_QUERY, meetingWordsQuery, parseMeetingQuery } from "./hit-query.js";
|
|
9
10
|
export { type ImportFile, importFiles } from "./import.js";
|
|
10
11
|
export { MEETING_QUERY, type MeetingItem, type MeetingOwner, meeting, meetingItem } from "./item.js";
|
|
11
12
|
export { createMeetingContext, MEETING_FACT_TYPES, type MeetingContextAction, type MeetingContextChange, type MeetingContextOperation, type MeetingContextPorts, type MeetingContextReceipt, validateMeetingContextChange, } from "./meeting-context.js";
|
|
@@ -41,6 +42,7 @@ export { validateMeetingSeriesInventoryCursor } from "./series-inventory-validat
|
|
|
41
42
|
export type { MeetingSource, SourceParticipant } from "./source.js";
|
|
42
43
|
export type { ChatInput, ContractCase, EventCandidateFilter, IdentityInput, MeetingDetails, MeetingFilter, MeetingHitSource, MeetingInput, MeetingSave, MeetingStore, NewRecord, ParticipantInput, SearchHit, TranscriptInput, TranscriptRowInput, } from "./store.js";
|
|
43
44
|
export type { MeetingTranscriptReceiptStore, MeetingTranscriptStore, TranscriptAppend, TranscriptAppendReceipt, UnlinkedTranscriptInput, } from "./transcript-store.js";
|
|
45
|
+
export { parseTranscriptText, TranscriptTextError, type TranscriptTextHeader } from "./transcript-text.js";
|
|
44
46
|
export { type MeetingMediaDescriptor, type MeetingRecognitionSegment, type MeetingTranscriptionContext, type MeetingTranscriptionOptions, type MeetingTranscriptionReceipt, type MeetingTranscriptionSource, transcribeMeeting, } from "./transcription.js";
|
|
45
47
|
export { createUpcomingMeetingReader } from "./upcoming.js";
|
|
46
48
|
export type { MeetingOccurrence, MeetingOccurrencePort, MeetingOccurrenceRead, MeetingOccurrenceSchedule, UpcomingMeetingRequest, UpcomingMeetingResult, } from "./upcoming-types.js";
|
package/dist/meetings/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { linkEvent } from "./events.js";
|
|
|
4
4
|
export { exportMeeting, serializeVtt, } from "./export.js";
|
|
5
5
|
export { findingHash, MEETING_FINDINGS_KIND, meetingFindingFingerprint, validateMeetingFindings, } from "./findings.js";
|
|
6
6
|
export { findingFingerprint, validateFindings, } from "./findings-rules.js";
|
|
7
|
+
export { MEETING_HIT_QUERY, meetingWordsQuery, parseMeetingQuery } from "./hit-query.js";
|
|
7
8
|
export { importFiles } from "./import.js";
|
|
8
9
|
export { MEETING_QUERY, meeting, meetingItem } from "./item.js";
|
|
9
10
|
export { createMeetingContext, MEETING_FACT_TYPES, validateMeetingContextChange, } from "./meeting-context.js";
|
|
@@ -27,6 +28,7 @@ export { validateMeetingHitSearch } from "./search-validation.js";
|
|
|
27
28
|
export { validateMeetingSeriesHistory, } from "./series-history.js";
|
|
28
29
|
export { createMeetingSeriesInventoryReader } from "./series-inventory.js";
|
|
29
30
|
export { validateMeetingSeriesInventoryCursor } from "./series-inventory-validation.js";
|
|
31
|
+
export { parseTranscriptText, TranscriptTextError } from "./transcript-text.js";
|
|
30
32
|
export { transcribeMeeting, } from "./transcription.js";
|
|
31
33
|
export { createUpcomingMeetingReader } from "./upcoming.js";
|
|
32
34
|
export { parseVtt, VttError } from "./vtt.js";
|
|
@@ -15,6 +15,7 @@ export declare const meetingSearchTool: {
|
|
|
15
15
|
query: {
|
|
16
16
|
type: string;
|
|
17
17
|
minLength: number;
|
|
18
|
+
description: string;
|
|
18
19
|
};
|
|
19
20
|
meeting_id: {
|
|
20
21
|
type: string;
|
|
@@ -138,6 +139,11 @@ export declare const callMeetingSearchTool: (search: MeetingHitSearch, input: un
|
|
|
138
139
|
id: string;
|
|
139
140
|
excerpt: string;
|
|
140
141
|
offset_ms: number | null;
|
|
142
|
+
match?: "corrected" | "words" | undefined;
|
|
143
|
+
corrections?: {
|
|
144
|
+
from: string;
|
|
145
|
+
to: string[];
|
|
146
|
+
}[] | undefined;
|
|
141
147
|
source?: {
|
|
142
148
|
ref: string;
|
|
143
149
|
title: string | null;
|
|
@@ -11,7 +11,11 @@ export const meetingSearchTool = {
|
|
|
11
11
|
inputSchema: {
|
|
12
12
|
type: "object",
|
|
13
13
|
properties: {
|
|
14
|
-
query: {
|
|
14
|
+
query: {
|
|
15
|
+
type: "string",
|
|
16
|
+
minLength: 1,
|
|
17
|
+
description: 'The messages query language: words (all required), OR, (groups), NOT or -word, "a phrase", "a b"~N for words within N transcript lines (at most 20), exact:word, from:"Name" for who said the line',
|
|
18
|
+
},
|
|
15
19
|
meeting_id: integer,
|
|
16
20
|
identity_id: integer,
|
|
17
21
|
person_id: integer,
|
|
@@ -24,6 +24,11 @@ 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 match: v.OptionalSchema<v.PicklistSchema<["words", "corrected"], undefined>, undefined>;
|
|
28
|
+
readonly corrections: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
29
|
+
readonly from: v.StringSchema<undefined>;
|
|
30
|
+
readonly to: v.ArraySchema<v.StringSchema<undefined>, undefined>;
|
|
31
|
+
}, undefined>, undefined>, undefined>;
|
|
27
32
|
readonly source: v.OptionalSchema<v.StrictObjectSchema<{
|
|
28
33
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
29
34
|
readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
package/dist/meetings/pull.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface RunReport {
|
|
|
9
9
|
externalId: string;
|
|
10
10
|
message: string;
|
|
11
11
|
}[];
|
|
12
|
+
/** An import whose files all read their start time in one zone names it; files carry no zone of their own. */
|
|
13
|
+
timezone?: string;
|
|
12
14
|
}
|
|
13
15
|
export interface PullOptions {
|
|
14
16
|
accountId: number;
|
|
@@ -12,6 +12,8 @@ const clock = (now) => {
|
|
|
12
12
|
return new Date(now);
|
|
13
13
|
};
|
|
14
14
|
const day = (instant) => instant.toISOString().slice(0, 10);
|
|
15
|
+
/** The calendar day it is at `instant` in the schedule's own zone: the day the owner's daily run belongs to. */
|
|
16
|
+
const localDay = (instant, timeZone) => new Intl.DateTimeFormat("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).format(instant);
|
|
15
17
|
const identity = (schedule) => createHash("sha256").update(JSON.stringify(schedule)).digest("hex");
|
|
16
18
|
export const planRecordingArchiveSchedule = (input, options) => {
|
|
17
19
|
const now = clock(options.now);
|
|
@@ -29,8 +31,8 @@ export const planRecordingArchiveSchedule = (input, options) => {
|
|
|
29
31
|
fail("archive schedule requires a valid IANA time zone");
|
|
30
32
|
}
|
|
31
33
|
recordingWindows(input.from, input.from);
|
|
32
|
-
if (input.from <=
|
|
33
|
-
recordingWindows(input.from,
|
|
34
|
+
if (input.from <= localDay(now, input.timeZone))
|
|
35
|
+
recordingWindows(input.from, localDay(now, input.timeZone));
|
|
34
36
|
const lookbackDays = input.lookbackDays ?? 7;
|
|
35
37
|
const maxBytes = input.maxBytes ?? 20 * 1024 ** 3;
|
|
36
38
|
if (!Number.isSafeInteger(lookbackDays) || lookbackDays < 0 || lookbackDays > 31)
|
|
@@ -76,11 +78,10 @@ export const parseRecordingArchiveSchedule = (value) => {
|
|
|
76
78
|
export const recordingArchiveScheduleWindow = (value, options) => {
|
|
77
79
|
const schedule = parseRecordingArchiveSchedule(value);
|
|
78
80
|
const now = clock(options.now);
|
|
79
|
-
const through =
|
|
81
|
+
const through = localDay(now, schedule.timeZone);
|
|
80
82
|
if (schedule.from > through)
|
|
81
83
|
return null;
|
|
82
|
-
const floor = new Date(
|
|
83
|
-
floor.setUTCHours(0, 0, 0, 0);
|
|
84
|
+
const floor = new Date(`${through}T00:00:00Z`);
|
|
84
85
|
// A running job ages beyond its original ten-year discovery interval; saved pending work survives separately.
|
|
85
86
|
const month = floor.getUTCMonth();
|
|
86
87
|
const date = floor.getUTCDate();
|