@wirecat/cli-core 0.582.0 → 0.584.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 +53 -0
- package/dist/cli/compact.js +46 -9
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/project-status-rows.js +1 -1
- package/dist/cli/project-status.d.ts +4 -12
- package/dist/cli/project-status.js +35 -16
- package/dist/cli/schedule/schedule-command.js +28 -12
- package/dist/entities/index.d.ts +10 -0
- package/dist/entities/list-results.d.ts +12 -0
- package/dist/entities/list-results.js +9 -0
- package/dist/entities/mail-findings.d.ts +3 -0
- package/dist/entities/mail-findings.js +3 -0
- package/dist/errors/reasons.js +1 -0
- package/dist/mail/cli/marks.js +9 -3
- package/dist/mail/cli/review.js +2 -2
- package/dist/mail/findings.d.ts +56 -2
- package/dist/mail/findings.js +98 -81
- package/dist/mail/index.d.ts +1 -1
- package/dist/mail/index.js +1 -1
- package/dist/mail/marks.js +5 -6
- package/dist/meetings/findings-rules.d.ts +4 -1
- package/dist/meetings/findings-rules.js +36 -19
- package/dist/meetings/index.d.ts +1 -1
- package/dist/meetings/index.js +1 -1
- package/dist/meetings/timestamp.js +1 -1
- package/dist/store/sqlite/finance.d.ts +2 -0
- package/dist/store/sqlite/finance.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,59 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## 0.584.0 — 06.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`schedule list` gives an agent schedule's cost caps.** `max_cost_usd` is the cap on one run (`--max-cost`, null
|
|
15
|
+
when none) and `max_cost_per_day_usd` the daily cap in force, the schedule's `--max-cost-per-day` or the config's
|
|
16
|
+
(0 for none), so a screen can show `$0.84 of $5.00`; the text line says `today $0.84 of $5.00`.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **`schedule list`'s output schema describes what it prints.** `spent_today_usd`, and the last run's `cost_usd`
|
|
21
|
+
and `skipped`, were printed but missing from the strict `scheduled_job` schema.
|
|
22
|
+
|
|
23
|
+
## 0.583.0 — 05.11.2026
|
|
24
|
+
|
|
25
|
+
### Changed — may break callers
|
|
26
|
+
|
|
27
|
+
- **`--compact` says a day once above its rows in a list in time order.** When a list's rows are sorted by time and
|
|
28
|
+
a day holds three rows on average, a `# YYYY-MM-DD` line opens each day and its rows print only `HH:MM`. A
|
|
29
|
+
1,000-message `messages list` drops from 43 % to 41 % of JSON's tokens (53 % to 52 % of its bytes). A reader that
|
|
30
|
+
parsed `MM-DD HH:MM` from such a row reads the day from the line above it. Searches and other unsorted lists are
|
|
31
|
+
unchanged.
|
|
32
|
+
- **An item answer's compact sections share one span line.** The days and zone are printed once, as
|
|
33
|
+
`# <first>…<last> · <zone>` above the first section, not on every section's header (`projects status`,
|
|
34
|
+
`projects evidence`).
|
|
35
|
+
- **`projects status` leaves out what the answer already says.** `item.window` is gone (it is `meta.window`). A
|
|
36
|
+
listed item drops `chat` when its `ref` is a message of that chat, drops `basis` when it is `store`, and a promise
|
|
37
|
+
drops `data.source`, which equals its `basis`. `who.name` and a chat row's `role` are left out instead of `null`.
|
|
38
|
+
The promise row of `--compact` prints `basis`. On the E7 fixture JSON goes from 3,998 to 3,676 bytes and compact
|
|
39
|
+
from 1,511 to 1,387.
|
|
40
|
+
- **Every mail finding kind needs `reason`.** A fact, a memory and a new contact had none or an optional one; the
|
|
41
|
+
owner reads it beside each finding when he decides. A missing field is refused with its name, the finding and what
|
|
42
|
+
that kind needs (`finding.missing-field`, `details.fields`, `details.needs`). Batches staged before keep theirs.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **`invoices` takes `number`.** `finance.invoices({ number })` lists the invoices whose printed number is exactly
|
|
47
|
+
that, as `orders.list({ number })` does for orders.
|
|
48
|
+
- **`MAIL_FINDING_FIELDS` and `mailFindingFieldsHelp()`.** The fields each mail finding kind needs and takes, the
|
|
49
|
+
table the validator itself reads, so a host's `--help` lists them without drifting from the checks.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **A `messages reset` that leaves the mark where it is still counts.** It adds to `resets`, so the holder's next
|
|
54
|
+
findings key is new, and says "mark unchanged … counted as a reset"; the answer gives `resets`. Before, it wrote
|
|
55
|
+
nothing, and a run after it was refused with `mail-findings.key-used`.
|
|
56
|
+
- **`review --holder` gives no `mail-findings.add` step when it gives no emails.**
|
|
57
|
+
- **A task finding's `due_at` refusals say what is needed.** A date without a time and timezone, `due_at` without
|
|
58
|
+
`due_evidence`, and `due_evidence` given as evidence objects each name the field and show the right form, such as
|
|
59
|
+
`"due_at":"2026-11-09T09:00:00Z","due_evidence":["<reference>"]`.
|
|
60
|
+
|
|
8
61
|
## 0.582.0 — 05.11.2026
|
|
9
62
|
|
|
10
63
|
### Added
|
package/dist/cli/compact.js
CHANGED
|
@@ -200,7 +200,9 @@ const cellOf = (item, column, clock) => {
|
|
|
200
200
|
if (!parts)
|
|
201
201
|
return { text: clean(iso) };
|
|
202
202
|
const day = parts.date.slice(5);
|
|
203
|
-
return
|
|
203
|
+
return column.as === "day"
|
|
204
|
+
? { text: day, date: parts.date }
|
|
205
|
+
: { text: `${day} ${parts.time}`, date: parts.date, time: parts.time };
|
|
204
206
|
}
|
|
205
207
|
case "count":
|
|
206
208
|
return typeof value === "number" && value !== 0
|
|
@@ -255,6 +257,22 @@ const sameEverywhere = (rows, index) => {
|
|
|
255
257
|
const first = rows[0]?.cells[index]?.text;
|
|
256
258
|
return first !== undefined && rows.every(({ cells }) => cells[index]?.text === first);
|
|
257
259
|
};
|
|
260
|
+
const ordered = (dates) => dates.every((one, at) => at === 0 || one >= dates[at - 1]) ||
|
|
261
|
+
dates.every((one, at) => at === 0 || one <= dates[at - 1]);
|
|
262
|
+
/**
|
|
263
|
+
* The column whose date is said once on a `# YYYY-MM-DD` line above its rows: a list in time order, such as a chat's
|
|
264
|
+
* messages. Only when days hold three rows on average: with fewer, the day lines cost more than they save.
|
|
265
|
+
*/
|
|
266
|
+
const dayColumn = (rows, shape, hoisted) => {
|
|
267
|
+
const index = shape.columns.findIndex((column, at) => column.as === "time" && !hoisted[at]);
|
|
268
|
+
if (index < 0)
|
|
269
|
+
return -1;
|
|
270
|
+
const dates = rows.map(({ cells }) => cells[index]?.date);
|
|
271
|
+
if (dates.some((one) => one === undefined))
|
|
272
|
+
return -1;
|
|
273
|
+
const days = dates;
|
|
274
|
+
return ordered(days) && new Set(days).size * 3 <= days.length ? index : -1;
|
|
275
|
+
};
|
|
258
276
|
const rowsOfItems = (items, shape, clock, chars) => {
|
|
259
277
|
const dates = [];
|
|
260
278
|
const rows = items.map((item) => {
|
|
@@ -277,17 +295,30 @@ const rowsOfItems = (items, shape, clock, chars) => {
|
|
|
277
295
|
return [];
|
|
278
296
|
return [`${sourceOf(item, column) ?? fieldsOfColumn(column.field)[0]}: ${rows[0]?.cells[index]?.text}`];
|
|
279
297
|
});
|
|
280
|
-
const
|
|
298
|
+
const byDay = dayColumn(rows, shape, hoisted);
|
|
299
|
+
let shownDay;
|
|
300
|
+
const lines = rows.flatMap(({ cells, text, item }) => {
|
|
281
301
|
const parts = cells.map((cell, index) => (hoisted[index] ? undefined : cell?.text));
|
|
302
|
+
const dayLine = [];
|
|
303
|
+
const cell = cells[byDay];
|
|
304
|
+
if (byDay >= 0 && cell?.date && cell.time) {
|
|
305
|
+
if (cell.date !== shownDay)
|
|
306
|
+
dayLine.push(`# ${cell.date}`);
|
|
307
|
+
shownDay = cell.date;
|
|
308
|
+
parts[byDay] = cell.time;
|
|
309
|
+
}
|
|
282
310
|
const shortened = prefix ? clean(prefix) : undefined;
|
|
283
311
|
if (shortened && parts[0]?.startsWith(shortened))
|
|
284
312
|
parts[0] = `#${parts[0].slice(shortened.length)}`;
|
|
285
313
|
const shown = [...parts, text].filter((part) => part !== undefined && part !== "");
|
|
286
|
-
return guarded(shown.length ? shown.join(" · ") : item ? blockLine(item, chars) : "");
|
|
314
|
+
return [...dayLine, guarded(shown.length ? shown.join(" · ") : item ? blockLine(item, chars) : "")];
|
|
287
315
|
});
|
|
316
|
+
const header = [...(prefix ? [`ref: ${clean(prefix)}<id>`] : []), ...named];
|
|
317
|
+
return { header, lines, dates };
|
|
318
|
+
};
|
|
319
|
+
const spanOf = (dates, clock) => {
|
|
288
320
|
const when = span(dates);
|
|
289
|
-
|
|
290
|
-
return { header, lines };
|
|
321
|
+
return when ? [when, clock.zone] : [];
|
|
291
322
|
};
|
|
292
323
|
const blockLine = (fields, chars) => blockOf(fields, chars).join(" ");
|
|
293
324
|
/** A record as `field: value` lines; a nested record is indented under its field, a list of records counted. */
|
|
@@ -352,20 +383,26 @@ export const compactText = (body, shape, options, rowsOf = rowsOfEntity()) => {
|
|
|
352
383
|
const chars = options.textChars ?? TEXT_CHARS;
|
|
353
384
|
let lines = [];
|
|
354
385
|
if ("items" in body) {
|
|
355
|
-
const { header, lines: rows } = rowsOfItems(body.items, shape, clock, chars);
|
|
356
|
-
|
|
386
|
+
const { header, lines: rows, dates } = rowsOfItems(body.items, shape, clock, chars);
|
|
387
|
+
const head = [...header, ...spanOf(dates, clock)];
|
|
388
|
+
lines = (head.length ? [`# ${head.join(" · ")}`] : []).concat(rows.length ? rows : ["# nothing found"]);
|
|
357
389
|
}
|
|
358
390
|
else if (isFields(body.item)) {
|
|
359
391
|
const sections = shape.sections ?? {};
|
|
360
392
|
const rest = Object.fromEntries(Object.entries(body.item).filter(([name]) => !Object.hasOwn(sections, name) && !shape.hidden?.includes(name)));
|
|
361
393
|
lines = blockOf(rest, chars, "", shape.whole).map(guarded);
|
|
394
|
+
let dates = [];
|
|
395
|
+
let drawn = [];
|
|
362
396
|
for (const [name, entity] of Object.entries(sections)) {
|
|
363
397
|
const list = body.item[name];
|
|
364
398
|
if (!Array.isArray(list))
|
|
365
399
|
continue;
|
|
366
|
-
const { header, lines: rows } = rowsOfItems(list, rowsOf(entity), clock, chars);
|
|
367
|
-
|
|
400
|
+
const { header, lines: rows, dates: more } = rowsOfItems(list, rowsOf(entity), clock, chars);
|
|
401
|
+
dates = dates.concat(more);
|
|
402
|
+
drawn = drawn.concat([`# ${[`${clean(name)}: ${list.length}`, ...header].join(" · ")}`], rows);
|
|
368
403
|
}
|
|
404
|
+
const when = spanOf(dates, clock);
|
|
405
|
+
lines = lines.concat(when.length ? [`# ${when.join(" · ")}`] : [], drawn);
|
|
369
406
|
}
|
|
370
407
|
else if (present(body.item))
|
|
371
408
|
lines = [guarded(cut(clean(String(body.item)), chars))];
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { botTemplate } from "../entities/list-results.js";
|
|
|
6
6
|
export { syncProgress } from "../entities/store-records.js";
|
|
7
7
|
export { isCliFailure, isCommanderFailure } from "../errors/failures.js";
|
|
8
8
|
export { type AppErrors, failureContext } from "../errors/reasons.js";
|
|
9
|
+
export { mailFindingFieldsHelp } from "../mail/findings.js";
|
|
9
10
|
export { botInstructions } from "../mcp/bot/instructions.js";
|
|
10
11
|
export type { BotServerOptions, RunBotCommand } from "../mcp/bot/server.js";
|
|
11
12
|
export type { HttpOptions } from "../mcp/http/serve.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { botTemplate } from "../entities/list-results.js";
|
|
|
5
5
|
export { syncProgress } from "../entities/store-records.js";
|
|
6
6
|
export { isCliFailure, isCommanderFailure } from "../errors/failures.js";
|
|
7
7
|
export { failureContext } from "../errors/reasons.js";
|
|
8
|
+
export { mailFindingFieldsHelp } from "../mail/findings.js";
|
|
8
9
|
export { botInstructions } from "../mcp/bot/instructions.js";
|
|
9
10
|
export { callMeetingPreparationTool, meetingPreparationTool } from "../mcp/meeting-preparation.js";
|
|
10
11
|
export { callMeetingProjectCandidatesTool, meetingProjectCandidatesTool } from "../mcp/meeting-project-candidates.js";
|
|
@@ -27,10 +27,6 @@ export declare const projectStatusSchema: v.LooseObjectSchema<{
|
|
|
27
27
|
readonly project: v.StringSchema<undefined>;
|
|
28
28
|
readonly key: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
29
29
|
readonly name: v.StringSchema<undefined>;
|
|
30
|
-
readonly window: v.StrictObjectSchema<{
|
|
31
|
-
readonly since: v.StringSchema<undefined>;
|
|
32
|
-
readonly until: v.StringSchema<undefined>;
|
|
33
|
-
}, undefined>;
|
|
34
30
|
/** Each signal's version, so a saved answer says which rules made it. */
|
|
35
31
|
readonly signals: v.RecordSchema<v.StringSchema<undefined>, v.NumberSchema<undefined>, undefined>;
|
|
36
32
|
readonly money: v.ArraySchema<v.StrictObjectSchema<{
|
|
@@ -43,7 +39,7 @@ export declare const projectStatusSchema: v.LooseObjectSchema<{
|
|
|
43
39
|
readonly chat: v.StringSchema<undefined>;
|
|
44
40
|
readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
45
41
|
readonly kind: v.StringSchema<undefined>;
|
|
46
|
-
readonly role: v.
|
|
42
|
+
readonly role: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
47
43
|
readonly purpose: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
48
44
|
readonly project: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
49
45
|
readonly coverage: v.StringSchema<undefined>;
|
|
@@ -54,7 +50,7 @@ export declare const projectStatusSchema: v.LooseObjectSchema<{
|
|
|
54
50
|
readonly at: v.StringSchema<undefined>;
|
|
55
51
|
readonly who: v.StrictObjectSchema<{
|
|
56
52
|
readonly ref: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
57
|
-
readonly name: v.
|
|
53
|
+
readonly name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
58
54
|
readonly owner: v.BooleanSchema<undefined>;
|
|
59
55
|
}, undefined>;
|
|
60
56
|
}, undefined>, undefined>;
|
|
@@ -100,10 +96,6 @@ export declare const projectStatusAnswer: (found: ProjectStatus, hints: ProjectS
|
|
|
100
96
|
project: string;
|
|
101
97
|
key: string | null;
|
|
102
98
|
name: string;
|
|
103
|
-
window: {
|
|
104
|
-
since: string;
|
|
105
|
-
until: string;
|
|
106
|
-
};
|
|
107
99
|
signals: {
|
|
108
100
|
[x: string]: number;
|
|
109
101
|
};
|
|
@@ -117,7 +109,7 @@ export declare const projectStatusAnswer: (found: ProjectStatus, hints: ProjectS
|
|
|
117
109
|
chat: string;
|
|
118
110
|
title: string | null;
|
|
119
111
|
kind: string;
|
|
120
|
-
role
|
|
112
|
+
role?: string | undefined;
|
|
121
113
|
purpose?: string | undefined;
|
|
122
114
|
project?: string | undefined;
|
|
123
115
|
coverage: string;
|
|
@@ -128,7 +120,7 @@ export declare const projectStatusAnswer: (found: ProjectStatus, hints: ProjectS
|
|
|
128
120
|
at: string;
|
|
129
121
|
who: {
|
|
130
122
|
ref?: string | undefined;
|
|
131
|
-
name
|
|
123
|
+
name?: string | undefined;
|
|
132
124
|
owner: boolean;
|
|
133
125
|
};
|
|
134
126
|
} | undefined;
|
|
@@ -13,21 +13,23 @@ const SECTIONS = {
|
|
|
13
13
|
/** The `project_status` answer drawn by `--compact`: a row per chat, then each listed signal with its own row. */
|
|
14
14
|
export const projectStatusRow = (signals) => ({
|
|
15
15
|
columns: [],
|
|
16
|
-
hidden: ["project", "
|
|
16
|
+
hidden: ["project", "purposes", "signals"],
|
|
17
17
|
sections: {
|
|
18
18
|
...SECTIONS,
|
|
19
19
|
...Object.fromEntries(signals.flatMap(({ id }) => (ON_CHAT_ROW.has(id) ? [] : [[listKey(id), `status.${id}`]]))),
|
|
20
20
|
},
|
|
21
21
|
});
|
|
22
|
-
const who = v.strictObject({ ref: v.optional(v.string()), name: v.
|
|
22
|
+
const who = v.strictObject({ ref: v.optional(v.string()), name: v.optional(v.string()), owner: v.boolean() });
|
|
23
23
|
const statusItem = v.looseObject({
|
|
24
24
|
ref: v.nullable(v.string()),
|
|
25
|
-
chat
|
|
25
|
+
/** Left out when `ref` is a message of that chat. */
|
|
26
|
+
chat: v.optional(v.nullable(v.string())),
|
|
26
27
|
at: v.nullable(v.string()),
|
|
27
28
|
who: v.optional(who),
|
|
28
29
|
/** Every ref the item cites, when it cites more than `ref`. */
|
|
29
30
|
evidence: v.optional(v.array(v.string())),
|
|
30
|
-
|
|
31
|
+
/** Left out for `store`, what most items are read from. */
|
|
32
|
+
basis: v.optional(v.picklist(["fact", "task", "pattern"])),
|
|
31
33
|
data: v.record(v.string(), v.unknown()),
|
|
32
34
|
});
|
|
33
35
|
/** Where a project stands: per chat signals, open money, and what was left out. Empty lists are left out. */
|
|
@@ -35,7 +37,6 @@ export const projectStatusSchema = v.looseObject({
|
|
|
35
37
|
project: v.string(),
|
|
36
38
|
key: v.nullable(v.string()),
|
|
37
39
|
name: v.string(),
|
|
38
|
-
window: v.strictObject({ since: v.string(), until: v.string() }),
|
|
39
40
|
/** Each signal's version, so a saved answer says which rules made it. */
|
|
40
41
|
signals: v.record(v.string(), v.number()),
|
|
41
42
|
money: v.array(v.strictObject({ direction: v.string(), currency: v.nullable(v.string()), amount: v.string(), rows: v.number() })),
|
|
@@ -43,7 +44,7 @@ export const projectStatusSchema = v.looseObject({
|
|
|
43
44
|
chat: v.string(),
|
|
44
45
|
title: v.nullable(v.string()),
|
|
45
46
|
kind: v.string(),
|
|
46
|
-
role: v.
|
|
47
|
+
role: v.optional(v.string()),
|
|
47
48
|
purpose: v.optional(v.string()),
|
|
48
49
|
project: v.optional(v.string()),
|
|
49
50
|
coverage: v.string(),
|
|
@@ -139,14 +140,33 @@ const missingOf = (found, hints) => {
|
|
|
139
140
|
...found.failed.map((one) => ({ field: `signal:${one.signal}`, why: one.why })),
|
|
140
141
|
];
|
|
141
142
|
};
|
|
142
|
-
const whoOf = (one) => one === null
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
143
|
+
const whoOf = (one) => one === null
|
|
144
|
+
? {}
|
|
145
|
+
: {
|
|
146
|
+
who: {
|
|
147
|
+
...(one.ref === null ? {} : { ref: one.ref }),
|
|
148
|
+
...(one.name === null ? {} : { name: one.name }),
|
|
149
|
+
owner: one.owner,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
const inChat = (ref, chat) => ref !== null && chat !== null && (ref === chat || ref.startsWith(`msg:${chat.slice("chat:".length)}/`));
|
|
153
|
+
/**
|
|
154
|
+
* An item as listed: its first ref, every ref only when there are more. What the rest of the item already says is
|
|
155
|
+
* left out: the chat its message ref names, the `store` basis, and a promise's `source`, which is its basis.
|
|
156
|
+
*/
|
|
157
|
+
const listed = ({ signal: _signal, who, evidence, chat, basis, data, ...one }) => {
|
|
158
|
+
const ref = evidence[0]?.ref ?? chat;
|
|
159
|
+
const { source, ...rest } = data;
|
|
160
|
+
return {
|
|
161
|
+
...(inChat(ref, chat) ? {} : { chat }),
|
|
162
|
+
...one,
|
|
163
|
+
...(basis === "store" ? {} : { basis }),
|
|
164
|
+
data: source === basis ? rest : data,
|
|
165
|
+
ref,
|
|
166
|
+
...whoOf(who),
|
|
167
|
+
...(evidence.length > 1 ? { evidence: evidence.map((each) => each.ref) } : {}),
|
|
168
|
+
};
|
|
169
|
+
};
|
|
150
170
|
/**
|
|
151
171
|
* Low signal items, counted on their chat's row instead of listed: questions others ask in a group with no role (a
|
|
152
172
|
* leads group's requests), promises found by words that others wrote in a group, and past the newest one per chat.
|
|
@@ -194,7 +214,7 @@ const chatRow = (found, hints, chat, unlisted) => {
|
|
|
194
214
|
chat: chat.ref,
|
|
195
215
|
title: chat.title,
|
|
196
216
|
kind: chat.kind,
|
|
197
|
-
role: chat.role,
|
|
217
|
+
...(chat.role === null ? {} : { role: chat.role }),
|
|
198
218
|
...(Array.isArray(found.purposes) && found.purposes.length === 1 ? {} : { purpose: chat.purpose }),
|
|
199
219
|
...(chat.project === found.project.ref ? {} : { project: chat.project }),
|
|
200
220
|
coverage: chat.coverage.completeness.state,
|
|
@@ -225,7 +245,6 @@ export const projectStatusAnswer = (found, hints) => {
|
|
|
225
245
|
project: found.project.ref,
|
|
226
246
|
key: found.project.key,
|
|
227
247
|
name: found.project.name,
|
|
228
|
-
window: found.window,
|
|
229
248
|
signals: Object.fromEntries(found.signals.map(({ id, version }) => [id, version])),
|
|
230
249
|
money: found.money.open,
|
|
231
250
|
chats: found.chats.map((chat) => chatRow(found, hints, chat, [...unlisted].filter((one) => one.chat === chat.ref))),
|
|
@@ -138,6 +138,12 @@ export const scheduleLimitsOf = (config) => {
|
|
|
138
138
|
const given = (limits) => Object.fromEntries(Object.entries(limits).filter(([, value]) => value !== undefined));
|
|
139
139
|
const shown = (argv) => argv.map((arg) => (arg === "" ? '""' : arg)).join(" ");
|
|
140
140
|
const usd = (value) => `$${value.toFixed(2)}`;
|
|
141
|
+
/** The per-run cap an agent schedule was made with (`--max-cost`, passed to claude as --max-budget-usd), or null. */
|
|
142
|
+
const runCapOf = (argv) => {
|
|
143
|
+
const at = argv.indexOf("--max-budget-usd");
|
|
144
|
+
const cap = at < 0 ? Number.NaN : Number(argv[at + 1]);
|
|
145
|
+
return Number.isFinite(cap) ? cap : null;
|
|
146
|
+
};
|
|
141
147
|
/**
|
|
142
148
|
* Runs the CLI's own commands, or an agent, later or on a timer — through systemd (Linux), launchd
|
|
143
149
|
* (macOS) or Task Scheduler (Windows). Every timer runs `schedule exec <file>`; the schedule file holds
|
|
@@ -184,19 +190,21 @@ export const scheduleCommandFor = ({ app, context: contextOf, newMail, limits: c
|
|
|
184
190
|
* Why a run does not start now, or nothing: an agent whose runs reached the day's cost cap waits for tomorrow, and
|
|
185
191
|
* one gated on mail waits for mail past its holder's mark — on its timer, until enough of it waits.
|
|
186
192
|
*/
|
|
193
|
+
/** A schedule's own limits, else the config file's, else the defaults. */
|
|
194
|
+
const limitsOf = (self, schedule) => ({
|
|
195
|
+
...SCHEDULE_DEFAULTS,
|
|
196
|
+
...given(configured?.(self) ?? {}),
|
|
197
|
+
...given({
|
|
198
|
+
mailWaitMinutes: schedule.mailWaitMinutes,
|
|
199
|
+
mailBatch: schedule.mailBatch,
|
|
200
|
+
maxCostPerDay: schedule.maxCostPerDay,
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
187
203
|
const waits = async (self, file, { timed }) => {
|
|
188
204
|
if (!existsSync(file))
|
|
189
205
|
return undefined;
|
|
190
206
|
const schedule = readSchedule(file);
|
|
191
|
-
const { mailWaitMinutes, mailBatch, maxCostPerDay } =
|
|
192
|
-
...SCHEDULE_DEFAULTS,
|
|
193
|
-
...given(configured?.(self) ?? {}),
|
|
194
|
-
...given({
|
|
195
|
-
mailWaitMinutes: schedule.mailWaitMinutes,
|
|
196
|
-
mailBatch: schedule.mailBatch,
|
|
197
|
-
maxCostPerDay: schedule.maxCostPerDay,
|
|
198
|
-
}),
|
|
199
|
-
};
|
|
207
|
+
const { mailWaitMinutes, mailBatch, maxCostPerDay } = limitsOf(self, schedule);
|
|
200
208
|
if (schedule.kind === "agent" && maxCostPerDay > 0) {
|
|
201
209
|
const spent = spentToday(file);
|
|
202
210
|
if (spent >= maxCostPerDay) {
|
|
@@ -342,7 +350,13 @@ export const scheduleCommandFor = ({ app, context: contextOf, newMail, limits: c
|
|
|
342
350
|
kind: schedule.kind,
|
|
343
351
|
when: describeWhen(schedule.when),
|
|
344
352
|
last: lastRun(scheduleFile(state, schedule.name)) ?? null,
|
|
345
|
-
...(schedule.kind === "agent"
|
|
353
|
+
...(schedule.kind === "agent"
|
|
354
|
+
? {
|
|
355
|
+
spentTodayUsd: spentToday(scheduleFile(state, schedule.name)),
|
|
356
|
+
maxCostUsd: runCapOf(schedule.argv),
|
|
357
|
+
maxCostPerDayUsd: limitsOf(this, schedule).maxCostPerDay,
|
|
358
|
+
}
|
|
359
|
+
: {}),
|
|
346
360
|
}));
|
|
347
361
|
const answer = listOf("schedule list", {
|
|
348
362
|
items: rows.map((row) => snakeKeys(row)),
|
|
@@ -355,7 +369,7 @@ export const scheduleCommandFor = ({ app, context: contextOf, newMail, limits: c
|
|
|
355
369
|
return "";
|
|
356
370
|
}
|
|
357
371
|
return rows
|
|
358
|
-
.map(({ name, kind, when, last, spentTodayUsd }) => [
|
|
372
|
+
.map(({ name, kind, when, last, spentTodayUsd, maxCostPerDayUsd }) => [
|
|
359
373
|
name,
|
|
360
374
|
kind,
|
|
361
375
|
when,
|
|
@@ -366,7 +380,9 @@ export const scheduleCommandFor = ({ app, context: contextOf, newMail, limits: c
|
|
|
366
380
|
`${last.failures} in a row${last.reason ? `: ${last.reason}` : ""} — ${last.output}`
|
|
367
381
|
: "")
|
|
368
382
|
: "not run yet",
|
|
369
|
-
...(spentTodayUsd
|
|
383
|
+
...(spentTodayUsd
|
|
384
|
+
? [`today ${usd(spentTodayUsd)}${maxCostPerDayUsd ? ` of ${usd(maxCostPerDayUsd)}` : ""}`]
|
|
385
|
+
: []),
|
|
370
386
|
...(last?.skipped
|
|
371
387
|
? [`skipped ${last.skipped.at.slice(0, 16).replace("T", " ")}: ${last.skipped.why}`]
|
|
372
388
|
: []),
|
package/dist/entities/index.d.ts
CHANGED
|
@@ -3979,7 +3979,15 @@ export declare const ENTITIES: {
|
|
|
3979
3979
|
readonly error_code: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
3980
3980
|
readonly failing_since: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
3981
3981
|
readonly failures: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
3982
|
+
readonly cost_usd: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
3983
|
+
readonly skipped: v.OptionalSchema<v.StrictObjectSchema<{
|
|
3984
|
+
readonly at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
|
|
3985
|
+
readonly why: v.StringSchema<undefined>;
|
|
3986
|
+
}, undefined>, undefined>;
|
|
3982
3987
|
}, undefined>, undefined>;
|
|
3988
|
+
readonly spent_today_usd: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
3989
|
+
readonly max_cost_usd: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
3990
|
+
readonly max_cost_per_day_usd: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
3983
3991
|
}, undefined>;
|
|
3984
3992
|
readonly call_record: v.StrictObjectSchema<{
|
|
3985
3993
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
@@ -5936,6 +5944,7 @@ export declare const ENTITIES: {
|
|
|
5936
5944
|
readonly subject: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
5937
5945
|
readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
|
|
5938
5946
|
readonly statement: v.StringSchema<undefined>;
|
|
5947
|
+
readonly reason: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
5939
5948
|
}, undefined>, v.StrictObjectSchema<{
|
|
5940
5949
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
5941
5950
|
readonly evidence: v.ArraySchema<v.StrictObjectSchema<{
|
|
@@ -5960,6 +5969,7 @@ export declare const ENTITIES: {
|
|
|
5960
5969
|
readonly body: v.StringSchema<undefined>;
|
|
5961
5970
|
readonly subject: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
5962
5971
|
readonly project: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
5972
|
+
readonly reason: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
5963
5973
|
}, undefined>, v.StrictObjectSchema<{
|
|
5964
5974
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
5965
5975
|
readonly evidence: v.ArraySchema<v.StrictObjectSchema<{
|
|
@@ -57,7 +57,19 @@ export declare const scheduledJob: v.StrictObjectSchema<{
|
|
|
57
57
|
readonly error_code: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
58
58
|
readonly failing_since: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
59
59
|
readonly failures: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
60
|
+
readonly cost_usd: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
61
|
+
/** The last time a run did not start, and why: the day's cost cap, or too little new mail. */
|
|
62
|
+
readonly skipped: v.OptionalSchema<v.StrictObjectSchema<{
|
|
63
|
+
readonly at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
|
|
64
|
+
readonly why: v.StringSchema<undefined>;
|
|
65
|
+
}, undefined>, undefined>;
|
|
60
66
|
}, undefined>, undefined>;
|
|
67
|
+
/** Agent schedules: what today's runs cost, in US dollars. */
|
|
68
|
+
readonly spent_today_usd: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
69
|
+
/** Agent schedules: the cap on one run (`--max-cost`), null when none. */
|
|
70
|
+
readonly max_cost_usd: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
71
|
+
/** Agent schedules: the daily cap in force, the schedule's `--max-cost-per-day` or the config's; 0 for none. */
|
|
72
|
+
readonly max_cost_per_day_usd: v.OptionalSchema<v.NumberSchema<undefined>, undefined>;
|
|
61
73
|
}, undefined>;
|
|
62
74
|
export declare const botTemplate: v.StrictObjectSchema<{
|
|
63
75
|
readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
@@ -41,7 +41,16 @@ export const scheduledJob = v.strictObject({
|
|
|
41
41
|
error_code: v.optional(text),
|
|
42
42
|
failing_since: v.optional(time),
|
|
43
43
|
failures: v.optional(v.number()),
|
|
44
|
+
cost_usd: v.optional(v.number()),
|
|
45
|
+
/** The last time a run did not start, and why: the day's cost cap, or too little new mail. */
|
|
46
|
+
skipped: v.optional(v.strictObject({ at: time, why: text })),
|
|
44
47
|
})),
|
|
48
|
+
/** Agent schedules: what today's runs cost, in US dollars. */
|
|
49
|
+
spent_today_usd: v.optional(v.number()),
|
|
50
|
+
/** Agent schedules: the cap on one run (`--max-cost`), null when none. */
|
|
51
|
+
max_cost_usd: v.optional(v.nullable(v.number())),
|
|
52
|
+
/** Agent schedules: the daily cap in force, the schedule's `--max-cost-per-day` or the config's; 0 for none. */
|
|
53
|
+
max_cost_per_day_usd: v.optional(v.number()),
|
|
45
54
|
});
|
|
46
55
|
export const botTemplate = v.strictObject({
|
|
47
56
|
name: v.nullable(text),
|
|
@@ -41,6 +41,8 @@ export declare const mailFindings: v.StrictObjectSchema<{
|
|
|
41
41
|
readonly subject: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
42
42
|
readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
|
|
43
43
|
readonly statement: v.StringSchema<undefined>;
|
|
44
|
+
/** Every new finding gives one; a batch staged before reason was needed on every kind has none. */
|
|
45
|
+
readonly reason: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
44
46
|
}, undefined>, v.StrictObjectSchema<{
|
|
45
47
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
46
48
|
readonly evidence: v.ArraySchema<v.StrictObjectSchema<{
|
|
@@ -65,6 +67,7 @@ export declare const mailFindings: v.StrictObjectSchema<{
|
|
|
65
67
|
readonly body: v.StringSchema<undefined>;
|
|
66
68
|
readonly subject: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
67
69
|
readonly project: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
70
|
+
readonly reason: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
68
71
|
}, undefined>, v.StrictObjectSchema<{
|
|
69
72
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
70
73
|
readonly evidence: v.ArraySchema<v.StrictObjectSchema<{
|
|
@@ -30,6 +30,8 @@ const finding = v.variant("kind", [
|
|
|
30
30
|
subject: v.optional(reference),
|
|
31
31
|
type: v.picklist(FACT_TYPES),
|
|
32
32
|
statement: text,
|
|
33
|
+
/** Every new finding gives one; a batch staged before reason was needed on every kind has none. */
|
|
34
|
+
reason: v.optional(text),
|
|
33
35
|
}),
|
|
34
36
|
v.strictObject({
|
|
35
37
|
...base,
|
|
@@ -48,6 +50,7 @@ const finding = v.variant("kind", [
|
|
|
48
50
|
body: text,
|
|
49
51
|
subject: v.optional(reference),
|
|
50
52
|
project: v.optional(reference),
|
|
53
|
+
reason: v.optional(text),
|
|
51
54
|
}),
|
|
52
55
|
v.strictObject({ ...base, kind: v.literal("link"), from: reference, to: reference, reason: text }),
|
|
53
56
|
v.strictObject({ ...base, kind: v.literal("new_contact"), address: text, name: text, reason: v.optional(text) }),
|
package/dist/errors/reasons.js
CHANGED
|
@@ -65,6 +65,7 @@ const OTHERS = [
|
|
|
65
65
|
validation("mail-findings.invalid", "a checked mail findings batch would be refused; details.problems names why, finding by finding"),
|
|
66
66
|
validation("mail-findings.key-used", "the findings batch key was already used for different input"),
|
|
67
67
|
validation("finding.unknown-field", "a finding has a field its kind does not take"),
|
|
68
|
+
validation("finding.missing-field", "a finding leaves out a field its kind needs"),
|
|
68
69
|
validation("document.changed", "the document changed while it was read; nothing was saved"),
|
|
69
70
|
["document.not-imported", "not_found", "the document is not imported in this source account"],
|
|
70
71
|
["extraction.invalid-proposals", "invalid_response", "the model's memory proposals could not be used"],
|
package/dist/mail/cli/marks.js
CHANGED
|
@@ -78,6 +78,8 @@ const mailMarkReset = v.strictObject({
|
|
|
78
78
|
bulk: v.number(),
|
|
79
79
|
/** Emails between the two marks it skips again: drafts, envelopes, suspicious senders, partial mail. */
|
|
80
80
|
skipped: v.number(),
|
|
81
|
+
/** How often the owner moved this mark back, this reset counted even when the mark stays: the findings key names it. */
|
|
82
|
+
resets: v.number(),
|
|
81
83
|
/** Nothing moved: `--dry-run` only counts. */
|
|
82
84
|
dry_run: v.boolean(),
|
|
83
85
|
});
|
|
@@ -102,7 +104,8 @@ export const resetMarkCommand = (mail) => answerEntity(annotate(new Command("res
|
|
|
102
104
|
.summary("move a holder's processing mark back, so it reads that mail again (owner only)")
|
|
103
105
|
.description("the owner moves a holder's mark back past the mail received since --since-time, or past the last --last " +
|
|
104
106
|
"emails it was given; its next reviews give that mail again. The answer says how many emails it reads " +
|
|
105
|
-
"again.
|
|
107
|
+
"again. Every reset counts, even one that leaves the mark where it is, so the holder's next findings key " +
|
|
108
|
+
"is new. An agent cannot: a holder only moves its own mark forward, with `messages acknowledge`")
|
|
106
109
|
.requiredOption("--holder <name>", "whose mark: lower-case letters, digits and dashes, such as mail-triage")
|
|
107
110
|
.option("--since-time <time>", "read again what was received since this ISO 8601 time, or 2d ago")
|
|
108
111
|
.option("--last <n>", "read again the last n emails the holder was given", wholeNumber)
|
|
@@ -125,12 +128,15 @@ export const resetMarkCommand = (mail) => answerEntity(annotate(new Command("res
|
|
|
125
128
|
again: reset.again,
|
|
126
129
|
bulk: reset.bulk,
|
|
127
130
|
skipped: reset.skipped,
|
|
131
|
+
resets: reset.mark.resets,
|
|
128
132
|
dry_run: reset.dry,
|
|
129
133
|
};
|
|
130
134
|
renderAnswer(this, outputFor(this), itemOf(item), () => {
|
|
131
135
|
const mark = reset.mark.throughId === reset.fromId
|
|
132
|
-
? `
|
|
133
|
-
|
|
136
|
+
? `mark unchanged at ${reset.fromId}, ${reset.dry ? "would be counted" : "counted"} as a reset ` +
|
|
137
|
+
`(resets ${reset.mark.resets}): the next run gets a new findings key`
|
|
138
|
+
: `the mark ${reset.dry ? "would move" : "moved"} from ${reset.fromId} to ${reset.mark.throughId} ` +
|
|
139
|
+
`(resets ${reset.mark.resets})`;
|
|
134
140
|
return (`${holder} ${reset.dry ? "would read" : "reads"} ${reset.again} emails again, and ${reset.bulk} bulk ` +
|
|
135
141
|
`ones as excerpts unless they are newsletters; ${reset.skipped} are skipped again; ${mark}\n`);
|
|
136
142
|
});
|
package/dist/mail/cli/review.js
CHANGED
|
@@ -208,7 +208,7 @@ const holderReview = async (command, mail, holder, query) => {
|
|
|
208
208
|
const step = stepsFor(mail.mount, mailbox);
|
|
209
209
|
const answer = reviewAnswer(found, mailbox, true, step);
|
|
210
210
|
const skipped = Object.entries(read.skipped).filter(([, count]) => count > 0);
|
|
211
|
-
const stage = read.findingsKey === undefined ? [] : step("mail-findings.add", { key: read.findingsKey });
|
|
211
|
+
const stage = read.findingsKey === undefined || emails.length === 0 ? [] : step("mail-findings.add", { key: read.findingsKey });
|
|
212
212
|
answer.next = [
|
|
213
213
|
...stage,
|
|
214
214
|
...step("mail-messages.acknowledge", { holder, cursor: read.cursor }),
|
|
@@ -233,7 +233,7 @@ const holderReview = async (command, mail, holder, query) => {
|
|
|
233
233
|
const output = outputFor(command);
|
|
234
234
|
renderAnswer(command, output, answer, () => {
|
|
235
235
|
output.renderer.note(read.mark
|
|
236
|
-
? `${holder}: past ${read.mark.throughId}
|
|
236
|
+
? `${holder}: past ${read.mark.throughId}${emails.length ? `; findings key ${read.findingsKey}` : ""}; acknowledge with --cursor ${read.cursor}${read.more ? "; more after these" : ""}`
|
|
237
237
|
: `${holder} has no mark yet: it starts from now — acknowledge with --cursor ${read.cursor}`);
|
|
238
238
|
return emails.map((hit) => `${hitText(hit)}${markText(marks.get(hit.id), hit.excerptOnly)}\n`).join("");
|
|
239
239
|
});
|
package/dist/mail/findings.d.ts
CHANGED
|
@@ -11,7 +11,10 @@ interface Cited {
|
|
|
11
11
|
evidence: FindingEvidence[];
|
|
12
12
|
}
|
|
13
13
|
export type MailTaskFinding = TaskFinding;
|
|
14
|
-
|
|
14
|
+
/** The owner reads `reason` beside every finding when he decides it, so every kind gives one. */
|
|
15
|
+
export type MailFactFinding = FactFinding & {
|
|
16
|
+
reason: string;
|
|
17
|
+
};
|
|
15
18
|
export interface MailTaskUpdateFinding extends Cited {
|
|
16
19
|
kind: "task_update";
|
|
17
20
|
task: string;
|
|
@@ -27,6 +30,7 @@ export interface MailMemoryFinding extends Cited {
|
|
|
27
30
|
body: string;
|
|
28
31
|
subject?: string;
|
|
29
32
|
project?: string;
|
|
33
|
+
reason: string;
|
|
30
34
|
}
|
|
31
35
|
/** Address → person, person → organization, or thread → project. */
|
|
32
36
|
export interface MailLinkFinding extends Cited {
|
|
@@ -39,7 +43,7 @@ export interface MailNewContactFinding extends Cited {
|
|
|
39
43
|
kind: "new_contact";
|
|
40
44
|
address: string;
|
|
41
45
|
name: string;
|
|
42
|
-
reason
|
|
46
|
+
reason: string;
|
|
43
47
|
}
|
|
44
48
|
/** An invoice the mail carries; accepting it records the invoice through `store.finance`, confirmed. */
|
|
45
49
|
export interface MailInvoiceFinding extends Cited {
|
|
@@ -139,6 +143,56 @@ export interface MailFindingsInput {
|
|
|
139
143
|
extractor: string;
|
|
140
144
|
findings: MailFinding[];
|
|
141
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* What each kind takes besides `id`, `kind` and `evidence`, in the order the validator reads them: `needs` it refuses
|
|
148
|
+
* without, `may` it takes. `--help`, `details.allowed` and `details.needs` all read this table, so they cannot drift
|
|
149
|
+
* from the checks. Some fields go together (due_at with due_evidence) or replace each other (counterparty or
|
|
150
|
+
* counterparty_name); the check of each kind says which.
|
|
151
|
+
*/
|
|
152
|
+
export declare const MAIL_FINDING_FIELDS: {
|
|
153
|
+
readonly task: {
|
|
154
|
+
readonly needs: readonly ["type", "title", "reason", "commitment"];
|
|
155
|
+
readonly may: readonly ["project", "dueAt", "dueEvidence", "assignee", "assigneeEvidence", "externalId", "sourceUrl"];
|
|
156
|
+
};
|
|
157
|
+
readonly task_update: {
|
|
158
|
+
readonly needs: readonly ["task", "change", "reason"];
|
|
159
|
+
readonly may: readonly ["dueAt", "detail"];
|
|
160
|
+
};
|
|
161
|
+
readonly fact: {
|
|
162
|
+
readonly needs: readonly ["type", "statement", "reason"];
|
|
163
|
+
readonly may: readonly ["subject", "project"];
|
|
164
|
+
};
|
|
165
|
+
readonly memory: {
|
|
166
|
+
readonly needs: readonly ["memoryKind", "scope", "body", "reason"];
|
|
167
|
+
readonly may: readonly ["subject", "project"];
|
|
168
|
+
};
|
|
169
|
+
readonly link: {
|
|
170
|
+
readonly needs: readonly ["from", "to", "reason"];
|
|
171
|
+
readonly may: readonly [];
|
|
172
|
+
};
|
|
173
|
+
readonly new_contact: {
|
|
174
|
+
readonly needs: readonly ["address", "name", "reason"];
|
|
175
|
+
readonly may: readonly [];
|
|
176
|
+
};
|
|
177
|
+
readonly invoice: {
|
|
178
|
+
readonly needs: readonly ["direction", "currency", "lines", "reason"];
|
|
179
|
+
readonly may: readonly ["counterparty", "counterpartyName", "counterpartyEmail", "total", "number", "issueDate", "dueDate", "project"];
|
|
180
|
+
};
|
|
181
|
+
readonly payment: {
|
|
182
|
+
readonly needs: readonly ["direction", "amount", "currency", "date", "reason"];
|
|
183
|
+
readonly may: readonly ["payer", "payerName", "payerEmail", "payee", "payeeName", "payeeEmail", "reference", "allocations", "project"];
|
|
184
|
+
};
|
|
185
|
+
readonly order: {
|
|
186
|
+
readonly needs: readonly ["purpose", "supplierName", "reason"];
|
|
187
|
+
readonly may: readonly ["customer", "customerName", "customerEmail", "supplier", "number", "total", "currency", "status", "orderDate", "fulfilledDate", "invoice", "payment", "project", "sourceUrl"];
|
|
188
|
+
};
|
|
189
|
+
readonly order_update: {
|
|
190
|
+
readonly needs: readonly ["supplierName", "number", "status", "reason"];
|
|
191
|
+
readonly may: readonly ["date"];
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
/** One line per kind, `kind: needed fields; optional ones`, for `mail findings add --help`. */
|
|
195
|
+
export declare const mailFindingFieldsHelp: () => string;
|
|
142
196
|
export declare const validateMailFindings: (input: MailFindingsInput) => MailFindingsInput;
|
|
143
197
|
/** Why one finding, or the batch when `finding` is null, would be refused. */
|
|
144
198
|
export interface MailFindingProblem {
|
package/dist/mail/findings.js
CHANGED
|
@@ -5,7 +5,7 @@ import { MEMORY_KINDS, MEMORY_SCOPES } from "../domain/knowledge.js";
|
|
|
5
5
|
import { formatLocator, parseLocator } from "../domain/locator.js";
|
|
6
6
|
import { ORDER_PURPOSES, ORDER_STATUSES } from "../domain/orders.js";
|
|
7
7
|
import { parseReference } from "../domain/references.js";
|
|
8
|
-
import { findingFields, findingHash, findingId, findingText, meetingTimestamp, validateBatch, validateFinding, validEvidence, } from "../meetings/index.js";
|
|
8
|
+
import { findingFields, findingHash, findingId, findingNeeds, findingText, meetingTimestamp, snakeCase, validateBatch, validateFinding, validEvidence, } from "../meetings/index.js";
|
|
9
9
|
export { MAIL_FINDINGS_KIND } from "../store/mail-findings.js";
|
|
10
10
|
/** At most this many findings in one run's batch, and per sender in it. */
|
|
11
11
|
export const MAIL_FINDINGS_PER_RUN = 30;
|
|
@@ -30,6 +30,90 @@ const RULES = {
|
|
|
30
30
|
anySubject: true,
|
|
31
31
|
inboxTasks: true,
|
|
32
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* What each kind takes besides `id`, `kind` and `evidence`, in the order the validator reads them: `needs` it refuses
|
|
35
|
+
* without, `may` it takes. `--help`, `details.allowed` and `details.needs` all read this table, so they cannot drift
|
|
36
|
+
* from the checks. Some fields go together (due_at with due_evidence) or replace each other (counterparty or
|
|
37
|
+
* counterparty_name); the check of each kind says which.
|
|
38
|
+
*/
|
|
39
|
+
export const MAIL_FINDING_FIELDS = {
|
|
40
|
+
task: {
|
|
41
|
+
needs: ["type", "title", "reason", "commitment"],
|
|
42
|
+
may: ["project", "dueAt", "dueEvidence", "assignee", "assigneeEvidence", "externalId", "sourceUrl"],
|
|
43
|
+
},
|
|
44
|
+
task_update: { needs: ["task", "change", "reason"], may: ["dueAt", "detail"] },
|
|
45
|
+
fact: { needs: ["type", "statement", "reason"], may: ["subject", "project"] },
|
|
46
|
+
memory: { needs: ["memoryKind", "scope", "body", "reason"], may: ["subject", "project"] },
|
|
47
|
+
link: { needs: ["from", "to", "reason"], may: [] },
|
|
48
|
+
new_contact: { needs: ["address", "name", "reason"], may: [] },
|
|
49
|
+
invoice: {
|
|
50
|
+
needs: ["direction", "currency", "lines", "reason"],
|
|
51
|
+
may: [
|
|
52
|
+
"counterparty",
|
|
53
|
+
"counterpartyName",
|
|
54
|
+
"counterpartyEmail",
|
|
55
|
+
"total",
|
|
56
|
+
"number",
|
|
57
|
+
"issueDate",
|
|
58
|
+
"dueDate",
|
|
59
|
+
"project",
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
payment: {
|
|
63
|
+
needs: ["direction", "amount", "currency", "date", "reason"],
|
|
64
|
+
may: [
|
|
65
|
+
"payer",
|
|
66
|
+
"payerName",
|
|
67
|
+
"payerEmail",
|
|
68
|
+
"payee",
|
|
69
|
+
"payeeName",
|
|
70
|
+
"payeeEmail",
|
|
71
|
+
"reference",
|
|
72
|
+
"allocations",
|
|
73
|
+
"project",
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
order: {
|
|
77
|
+
needs: ["purpose", "supplierName", "reason"],
|
|
78
|
+
may: [
|
|
79
|
+
"customer",
|
|
80
|
+
"customerName",
|
|
81
|
+
"customerEmail",
|
|
82
|
+
"supplier",
|
|
83
|
+
"number",
|
|
84
|
+
"total",
|
|
85
|
+
"currency",
|
|
86
|
+
"status",
|
|
87
|
+
"orderDate",
|
|
88
|
+
"fulfilledDate",
|
|
89
|
+
"invoice",
|
|
90
|
+
"payment",
|
|
91
|
+
"project",
|
|
92
|
+
"sourceUrl",
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
order_update: { needs: ["supplierName", "number", "status", "reason"], may: ["date"] },
|
|
96
|
+
};
|
|
97
|
+
const MAIL_FINDING_KINDS = Object.keys(MAIL_FINDING_FIELDS);
|
|
98
|
+
/** The fixed values a field takes, shown in `--help` beside it; the checks read the same lists. */
|
|
99
|
+
const VALUES = {
|
|
100
|
+
task: { type: ["bug", "feature", "chore"], commitment: ["explicit", "inferred"] },
|
|
101
|
+
task_update: { change: ["close", "due", "detail"] },
|
|
102
|
+
memory: { memoryKind: MEMORY_KINDS, scope: MEMORY_SCOPES },
|
|
103
|
+
invoice: { direction: INVOICE_DIRECTIONS },
|
|
104
|
+
payment: { direction: MONEY_DIRECTIONS },
|
|
105
|
+
order: { purpose: ORDER_PURPOSES, status: ORDER_STATUSES },
|
|
106
|
+
order_update: { status: ORDER_STATUSES },
|
|
107
|
+
};
|
|
108
|
+
/** One line per kind, `kind: needed fields; optional ones`, for `mail findings add --help`. */
|
|
109
|
+
export const mailFindingFieldsHelp = () => MAIL_FINDING_KINDS.map((kind) => {
|
|
110
|
+
const { needs, may } = MAIL_FINDING_FIELDS[kind];
|
|
111
|
+
const named = (field) => {
|
|
112
|
+
const values = VALUES[kind]?.[field];
|
|
113
|
+
return values ? `${snakeCase(field)} ${values.join("|")}` : snakeCase(field);
|
|
114
|
+
};
|
|
115
|
+
return ` ${kind}: ${needs.map(named).join(", ")}${may.length ? `; optional ${may.map(named).join(", ")}` : ""}`;
|
|
116
|
+
}).join("\n");
|
|
33
117
|
const typeOf = (reference) => {
|
|
34
118
|
try {
|
|
35
119
|
const parsed = parseReference(reference);
|
|
@@ -60,23 +144,18 @@ const optional = (key, value, read) => (value === undefined ? {} : { [key]: read
|
|
|
60
144
|
const one = (raw, ids) => {
|
|
61
145
|
if (!raw || typeof raw !== "object")
|
|
62
146
|
return invalid("a finding is an object");
|
|
63
|
-
if (raw.kind
|
|
64
|
-
return
|
|
147
|
+
if (!MAIL_FINDING_KINDS.includes(raw.kind))
|
|
148
|
+
return invalid(`a mail finding is a ${MAIL_FINDING_KINDS.join(", ")}`);
|
|
149
|
+
const { needs, may } = MAIL_FINDING_FIELDS[raw.kind];
|
|
150
|
+
findingFields(raw, ["id", "kind", ...needs, ...may, "evidence"], RULES.singular);
|
|
151
|
+
findingNeeds(raw, needs, RULES.singular);
|
|
152
|
+
if (raw.kind === "fact") {
|
|
153
|
+
const { reason, ...fact } = raw;
|
|
154
|
+
return { ...validateFinding(fact, RULES, ids), reason: findingText(reason, 2000) };
|
|
155
|
+
}
|
|
65
156
|
if (raw.kind === "task") {
|
|
66
157
|
const { externalId, sourceUrl, ...task } = raw;
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
checked = validateFinding(task, RULES, ids);
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
if (!isCliError(error) || error.details.reason !== "finding.unknown-field")
|
|
73
|
-
throw error;
|
|
74
|
-
const allowed = [...error.details.allowed, "external_id", "source_url"];
|
|
75
|
-
throw new CliError("validation_error", error.message.replace(/; it takes .*$/, `; it takes ${allowed.join(", ")}`), {
|
|
76
|
-
...error.details,
|
|
77
|
-
allowed,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
158
|
+
const checked = validateFinding(task, RULES, ids);
|
|
80
159
|
return {
|
|
81
160
|
...checked,
|
|
82
161
|
...optional("externalId", externalId, (value) => {
|
|
@@ -112,7 +191,6 @@ const one = (raw, ids) => {
|
|
|
112
191
|
};
|
|
113
192
|
switch (raw.kind) {
|
|
114
193
|
case "task_update": {
|
|
115
|
-
findingFields(raw, ["id", "kind", "task", "change", "dueAt", "detail", "reason", "evidence"], RULES.singular);
|
|
116
194
|
const task = reference(raw.task, /^task:[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/, "a task update's task");
|
|
117
195
|
const change = raw.change;
|
|
118
196
|
if (change !== "close" && change !== "due" && change !== "detail")
|
|
@@ -135,7 +213,6 @@ const one = (raw, ids) => {
|
|
|
135
213
|
};
|
|
136
214
|
}
|
|
137
215
|
case "memory": {
|
|
138
|
-
findingFields(raw, ["id", "kind", "memoryKind", "scope", "body", "subject", "project", "evidence"], RULES.singular);
|
|
139
216
|
if (!MEMORY_KINDS.includes(raw.memoryKind))
|
|
140
217
|
return invalid(`a memory's memory_kind is ${MEMORY_KINDS.join(", ")}`);
|
|
141
218
|
if (!MEMORY_SCOPES.includes(raw.scope))
|
|
@@ -152,11 +229,11 @@ const one = (raw, ids) => {
|
|
|
152
229
|
? {}
|
|
153
230
|
: { subject: reference(raw.subject, /^(person|organization|project):[1-9]\d*$/, "a memory's subject") }),
|
|
154
231
|
...(raw.project === undefined ? {} : { project: reference(raw.project, /^project:[1-9]\d*$/, "a project") }),
|
|
232
|
+
reason: findingText(raw.reason, 2000),
|
|
155
233
|
evidence,
|
|
156
234
|
};
|
|
157
235
|
}
|
|
158
236
|
case "link": {
|
|
159
|
-
findingFields(raw, ["id", "kind", "from", "to", "reason", "evidence"], RULES.singular);
|
|
160
237
|
const from = findingText(raw.from, 400);
|
|
161
238
|
const to = findingText(raw.to, 80);
|
|
162
239
|
const wanted = LINKS[typeOf(from) ?? ""];
|
|
@@ -165,7 +242,6 @@ const one = (raw, ids) => {
|
|
|
165
242
|
return { id, kind: "link", from, to, reason: findingText(raw.reason, 2000), evidence };
|
|
166
243
|
}
|
|
167
244
|
case "new_contact": {
|
|
168
|
-
findingFields(raw, ["id", "kind", "address", "name", "reason", "evidence"], RULES.singular);
|
|
169
245
|
const address = findingText(raw.address, 320).toLowerCase();
|
|
170
246
|
if (!ADDRESS.test(address))
|
|
171
247
|
return invalid("a new contact's address is an email address");
|
|
@@ -174,28 +250,11 @@ const one = (raw, ids) => {
|
|
|
174
250
|
kind: "new_contact",
|
|
175
251
|
address,
|
|
176
252
|
name: findingText(raw.name, 200),
|
|
177
|
-
|
|
253
|
+
reason: findingText(raw.reason, 2000),
|
|
178
254
|
evidence,
|
|
179
255
|
};
|
|
180
256
|
}
|
|
181
257
|
case "invoice": {
|
|
182
|
-
findingFields(raw, [
|
|
183
|
-
"id",
|
|
184
|
-
"kind",
|
|
185
|
-
"direction",
|
|
186
|
-
"counterparty",
|
|
187
|
-
"counterpartyName",
|
|
188
|
-
"counterpartyEmail",
|
|
189
|
-
"currency",
|
|
190
|
-
"lines",
|
|
191
|
-
"total",
|
|
192
|
-
"number",
|
|
193
|
-
"issueDate",
|
|
194
|
-
"dueDate",
|
|
195
|
-
"project",
|
|
196
|
-
"reason",
|
|
197
|
-
"evidence",
|
|
198
|
-
], RULES.singular);
|
|
199
258
|
if (!Array.isArray(raw.lines) || raw.lines.length < 1 || raw.lines.length > 50)
|
|
200
259
|
return invalid("an invoice finding has 1–50 lines");
|
|
201
260
|
const lines = raw.lines.map((line) => {
|
|
@@ -229,25 +288,6 @@ const one = (raw, ids) => {
|
|
|
229
288
|
};
|
|
230
289
|
}
|
|
231
290
|
case "payment": {
|
|
232
|
-
findingFields(raw, [
|
|
233
|
-
"id",
|
|
234
|
-
"kind",
|
|
235
|
-
"direction",
|
|
236
|
-
"payer",
|
|
237
|
-
"payerName",
|
|
238
|
-
"payerEmail",
|
|
239
|
-
"payee",
|
|
240
|
-
"payeeName",
|
|
241
|
-
"payeeEmail",
|
|
242
|
-
"amount",
|
|
243
|
-
"currency",
|
|
244
|
-
"date",
|
|
245
|
-
"reference",
|
|
246
|
-
"allocations",
|
|
247
|
-
"project",
|
|
248
|
-
"reason",
|
|
249
|
-
"evidence",
|
|
250
|
-
], RULES.singular);
|
|
251
291
|
let allocations;
|
|
252
292
|
if (raw.allocations !== undefined) {
|
|
253
293
|
if (!Array.isArray(raw.allocations) || raw.allocations.length < 1 || raw.allocations.length > 8)
|
|
@@ -280,28 +320,6 @@ const one = (raw, ids) => {
|
|
|
280
320
|
};
|
|
281
321
|
}
|
|
282
322
|
case "order": {
|
|
283
|
-
findingFields(raw, [
|
|
284
|
-
"id",
|
|
285
|
-
"kind",
|
|
286
|
-
"purpose",
|
|
287
|
-
"customer",
|
|
288
|
-
"customerName",
|
|
289
|
-
"customerEmail",
|
|
290
|
-
"supplier",
|
|
291
|
-
"supplierName",
|
|
292
|
-
"number",
|
|
293
|
-
"total",
|
|
294
|
-
"currency",
|
|
295
|
-
"status",
|
|
296
|
-
"orderDate",
|
|
297
|
-
"fulfilledDate",
|
|
298
|
-
"invoice",
|
|
299
|
-
"payment",
|
|
300
|
-
"project",
|
|
301
|
-
"sourceUrl",
|
|
302
|
-
"reason",
|
|
303
|
-
"evidence",
|
|
304
|
-
], RULES.singular);
|
|
305
323
|
return {
|
|
306
324
|
id,
|
|
307
325
|
kind: "order",
|
|
@@ -324,7 +342,6 @@ const one = (raw, ids) => {
|
|
|
324
342
|
};
|
|
325
343
|
}
|
|
326
344
|
case "order_update": {
|
|
327
|
-
findingFields(raw, ["id", "kind", "supplierName", "number", "status", "date", "reason", "evidence"], RULES.singular);
|
|
328
345
|
const status = oneOf(raw.status, ORDER_STATUSES, "an order update's status");
|
|
329
346
|
if (raw.date !== undefined && status !== "delivered")
|
|
330
347
|
return invalid("only a delivered order update gives date");
|
|
@@ -340,7 +357,7 @@ const one = (raw, ids) => {
|
|
|
340
357
|
};
|
|
341
358
|
}
|
|
342
359
|
default:
|
|
343
|
-
return invalid(
|
|
360
|
+
return invalid(`a mail finding is a ${MAIL_FINDING_KINDS.join(", ")}`);
|
|
344
361
|
}
|
|
345
362
|
};
|
|
346
363
|
/**
|
package/dist/mail/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { mailAccountKey } from "./account-key.js";
|
|
|
3
3
|
export { DEFAULT_ATTACHMENT_POLICY, type MailAttachmentPolicy, POLICY_SKIPS } from "./attachments.js";
|
|
4
4
|
export { BULK, bulkBox } from "./bulk.js";
|
|
5
5
|
export { factsOf } from "./facts.js";
|
|
6
|
-
export { MAIL_FINDINGS_KIND, MAIL_FINDINGS_PER_RUN, MAIL_FINDINGS_PER_SENDER, type MailFinding, type MailFindingsInput, mailFindingFingerprint, validateMailFindings, } from "./findings.js";
|
|
6
|
+
export { MAIL_FINDING_FIELDS, MAIL_FINDINGS_KIND, MAIL_FINDINGS_PER_RUN, MAIL_FINDINGS_PER_SENDER, type MailFinding, type MailFindingsInput, mailFindingFieldsHelp, mailFindingFingerprint, validateMailFindings, } from "./findings.js";
|
|
7
7
|
export { GMAIL_ALL_MAIL, mailFolders } from "./folders.js";
|
|
8
8
|
export { dropMissing, type Gone, tooManyGone } from "./gone.js";
|
|
9
9
|
export { type AuthVerdicts, HEADER_FIELDS, headersOf, type MailHeaders, mailHeadersOf } from "./headers.js";
|
package/dist/mail/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { mailAccountKey } from "./account-key.js";
|
|
|
3
3
|
export { DEFAULT_ATTACHMENT_POLICY, POLICY_SKIPS } from "./attachments.js";
|
|
4
4
|
export { BULK, bulkBox } from "./bulk.js";
|
|
5
5
|
export { factsOf } from "./facts.js";
|
|
6
|
-
export { MAIL_FINDINGS_KIND, MAIL_FINDINGS_PER_RUN, MAIL_FINDINGS_PER_SENDER, mailFindingFingerprint, validateMailFindings, } from "./findings.js";
|
|
6
|
+
export { MAIL_FINDING_FIELDS, MAIL_FINDINGS_KIND, MAIL_FINDINGS_PER_RUN, MAIL_FINDINGS_PER_SENDER, mailFindingFieldsHelp, mailFindingFingerprint, validateMailFindings, } from "./findings.js";
|
|
7
7
|
export { GMAIL_ALL_MAIL, mailFolders } from "./folders.js";
|
|
8
8
|
export { dropMissing, tooManyGone } from "./gone.js";
|
|
9
9
|
export { HEADER_FIELDS, headersOf, mailHeadersOf } from "./headers.js";
|
package/dist/mail/marks.js
CHANGED
|
@@ -398,13 +398,12 @@ export const rewindMark = async (store, mailbox, holder, to, { now = Date.now(),
|
|
|
398
398
|
skipped: count((said) => said !== "read" && said !== "held" && said !== "bulk"),
|
|
399
399
|
dry,
|
|
400
400
|
};
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const [last] = through > 0 ? await store.mail.emailsBefore(account.id, through + 1, 1) : [];
|
|
404
|
-
const at = last?.id === through ? last.at : null;
|
|
401
|
+
// A reset that lands where the mark already is still counts: the owner asked for a fresh findings key.
|
|
402
|
+
const stays = through >= mark.throughId;
|
|
403
|
+
const [last] = !stays && through > 0 ? await store.mail.emailsBefore(account.id, through + 1, 1) : [];
|
|
405
404
|
const next = {
|
|
406
|
-
throughId: through,
|
|
407
|
-
throughAt: at,
|
|
405
|
+
throughId: stays ? mark.throughId : through,
|
|
406
|
+
throughAt: stays ? mark.throughAt : last?.id === through ? last.at : null,
|
|
408
407
|
held: mark.held.filter((id) => id <= through),
|
|
409
408
|
suspicious: mark.suspicious.filter((id) => id <= through),
|
|
410
409
|
read: mark.read,
|
|
@@ -65,6 +65,7 @@ export interface FindingsReceipt {
|
|
|
65
65
|
}
|
|
66
66
|
export declare const findingText: (value: unknown, limit: number) => string;
|
|
67
67
|
export declare const findingHash: (value: unknown) => string;
|
|
68
|
+
export declare const snakeCase: (key: string) => string;
|
|
68
69
|
/**
|
|
69
70
|
* Refuses a field `allowed` does not list, naming it, the finding it is in and what that kind takes: an agent fixes
|
|
70
71
|
* the batch from the message alone. `of` is the finding a nested object (an invoice line) belongs to.
|
|
@@ -73,6 +74,8 @@ export declare const findingFields: (value: object, allowed: string[], noun: str
|
|
|
73
74
|
id?: unknown;
|
|
74
75
|
kind?: unknown;
|
|
75
76
|
}) => void;
|
|
77
|
+
/** Refuses a finding that leaves out a field `needed` lists, naming each one missing and all it needs. */
|
|
78
|
+
export declare const findingNeeds: (value: Record<string, unknown>, needed: readonly string[], noun: string) => void;
|
|
76
79
|
/** What differs between findings kinds: their wording and what one cited reference must look like. */
|
|
77
80
|
export interface FindingsRules {
|
|
78
81
|
/** "meeting findings" */
|
|
@@ -96,7 +99,7 @@ export declare const findingId: (raw: unknown, ids: Set<string>) => string;
|
|
|
96
99
|
/** One fact, payment or task finding, its ID added to `ids`. */
|
|
97
100
|
export declare const validateFinding: (finding: Finding, rules: FindingsRules, ids: Set<string>) => Finding;
|
|
98
101
|
/** A date's or assignee's support: references the finding's own evidence cites. */
|
|
99
|
-
export declare const support: (value: unknown, evidence: readonly FindingEvidence[]) => string[];
|
|
102
|
+
export declare const support: (value: unknown, evidence: readonly FindingEvidence[], field: string) => string[];
|
|
100
103
|
/** A batch's envelope — schema version, key, extractor — around 1–`most` findings each `one` checks. */
|
|
101
104
|
export declare const validateBatch: <F>(input: {
|
|
102
105
|
schemaVersion: 1;
|
|
@@ -12,7 +12,15 @@ export const findingText = (value, limit) => {
|
|
|
12
12
|
return value.trim();
|
|
13
13
|
};
|
|
14
14
|
export const findingHash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
15
|
-
const snakeCase = (key) => key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
15
|
+
export const snakeCase = (key) => key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
16
|
+
const findingWhere = (of) => {
|
|
17
|
+
const id = typeof of.id === "string" ? of.id : null;
|
|
18
|
+
const kind = typeof of.kind === "string" ? of.kind : null;
|
|
19
|
+
return {
|
|
20
|
+
where: id || kind ? ` in ${[id, kind && `(${kind})`].filter(Boolean).join(" ")}` : "",
|
|
21
|
+
named: { ...(id ? { finding: id } : {}), ...(kind ? { kind } : {}) },
|
|
22
|
+
};
|
|
23
|
+
};
|
|
16
24
|
/**
|
|
17
25
|
* Refuses a field `allowed` does not list, naming it, the finding it is in and what that kind takes: an agent fixes
|
|
18
26
|
* the batch from the message alone. `of` is the finding a nested object (an invoice line) belongs to.
|
|
@@ -23,16 +31,18 @@ export const findingFields = (value, allowed, noun, of = value) => {
|
|
|
23
31
|
return;
|
|
24
32
|
const fields = unknown.map(snakeCase);
|
|
25
33
|
const takes = allowed.map(snakeCase);
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
const { where, named } = findingWhere(of);
|
|
35
|
+
throw new CliError("validation_error", `unknown ${noun} field${fields.length > 1 ? "s" : ""} ${fields.join(", ")}${where}; it takes ${takes.join(", ")}`, { reason: "finding.unknown-field", fields, allowed: takes, ...named });
|
|
36
|
+
};
|
|
37
|
+
/** Refuses a finding that leaves out a field `needed` lists, naming each one missing and all it needs. */
|
|
38
|
+
export const findingNeeds = (value, needed, noun) => {
|
|
39
|
+
const missing = needed.filter((key) => value[key] === undefined);
|
|
40
|
+
if (!missing.length)
|
|
41
|
+
return;
|
|
42
|
+
const fields = missing.map(snakeCase);
|
|
43
|
+
const needs = needed.map(snakeCase);
|
|
44
|
+
const { where, named } = findingWhere(value);
|
|
45
|
+
throw new CliError("validation_error", `missing ${noun} field${fields.length > 1 ? "s" : ""} ${fields.join(", ")}${where}; it needs ${needs.join(", ")}`, { reason: "finding.missing-field", fields, needs, ...named });
|
|
36
46
|
};
|
|
37
47
|
const SUBJECT = /^(person|organization|project):[1-9]\d*$|^task:[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
38
48
|
/** A finding's cited evidence: 1–8 distinct references in `rules`' form, each with a 64-hex fingerprint. */
|
|
@@ -130,9 +140,12 @@ export const validateFinding = (finding, rules, ids) => {
|
|
|
130
140
|
return invalid("a task finding type is bug, chore or feature");
|
|
131
141
|
if (!["explicit", "inferred"].includes(finding.commitment))
|
|
132
142
|
return invalid("task commitment must be explicit or inferred");
|
|
133
|
-
if ((finding.dueAt === undefined) !== (finding.dueEvidence === undefined)
|
|
134
|
-
(
|
|
135
|
-
|
|
143
|
+
if ((finding.dueAt === undefined) !== (finding.dueEvidence === undefined))
|
|
144
|
+
return invalid(`due_at comes with due_evidence: the ${rules.cited} references of this finding's evidence that give the date, ` +
|
|
145
|
+
`as a list of strings such as ["<reference>"]`);
|
|
146
|
+
if ((finding.assignee === undefined) !== (finding.assigneeEvidence === undefined))
|
|
147
|
+
return invalid(`assignee comes with assignee_evidence: the ${rules.cited} references of this finding's evidence that name ` +
|
|
148
|
+
`them, as a list of strings such as ["<reference>"]`);
|
|
136
149
|
if (finding.assignee !== undefined &&
|
|
137
150
|
(typeof finding.assignee !== "string" || !/^person:[1-9]\d*$/.test(finding.assignee)))
|
|
138
151
|
return invalid("an assignee must be a person reference");
|
|
@@ -146,21 +159,25 @@ export const validateFinding = (finding, rules, ids) => {
|
|
|
146
159
|
...(finding.dueAt === undefined
|
|
147
160
|
? {}
|
|
148
161
|
: {
|
|
149
|
-
dueAt: meetingTimestamp(finding.dueAt, "
|
|
150
|
-
dueEvidence: support(finding.dueEvidence, evidence),
|
|
162
|
+
dueAt: meetingTimestamp(finding.dueAt, "a task's due_at"),
|
|
163
|
+
dueEvidence: support(finding.dueEvidence, evidence, "due_evidence"),
|
|
151
164
|
}),
|
|
152
165
|
...(finding.assignee === undefined
|
|
153
166
|
? {}
|
|
154
|
-
: {
|
|
167
|
+
: {
|
|
168
|
+
assignee: finding.assignee,
|
|
169
|
+
assigneeEvidence: support(finding.assigneeEvidence, evidence, "assignee_evidence"),
|
|
170
|
+
}),
|
|
155
171
|
};
|
|
156
172
|
};
|
|
157
173
|
/** A date's or assignee's support: references the finding's own evidence cites. */
|
|
158
|
-
export const support = (value, evidence) => {
|
|
174
|
+
export const support = (value, evidence, field) => {
|
|
159
175
|
if (!Array.isArray(value) ||
|
|
160
176
|
value.length < 1 ||
|
|
161
177
|
value.length > 8 ||
|
|
162
178
|
value.some((ref) => typeof ref !== "string" || !evidence.some((item) => item.reference === ref)))
|
|
163
|
-
return invalid(
|
|
179
|
+
return invalid(`${field} lists 1–8 references this finding's evidence cites, as plain strings such as ["<reference>"], ` +
|
|
180
|
+
"not evidence objects");
|
|
164
181
|
return [...new Set(value)].sort();
|
|
165
182
|
};
|
|
166
183
|
/** A batch's envelope — schema version, key, extractor — around 1–`most` findings each `one` checks. */
|
package/dist/meetings/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { entryInWindow, eventChange } from "./event-store.js";
|
|
|
9
9
|
export { linkEvent } from "./events.js";
|
|
10
10
|
export { exportMeeting, type MeetingExport, type MeetingExportFormat, type MeetingExportOptions, serializeVtt, } from "./export.js";
|
|
11
11
|
export { type FindingEvidence, type FindingOutcome, findingHash, MEETING_FINDINGS_KIND, type MeetingFactFinding, type MeetingFinding, type MeetingFindingsInput, type MeetingFindingsReceipt, type MeetingTaskFinding, meetingFindingFingerprint, validateMeetingFindings, } from "./findings.js";
|
|
12
|
-
export { type FactFinding, type Finding, type FindingsInput, type FindingsReceipt, type FindingsRules, findingFields, findingFingerprint, findingId, findingText, type PaymentFinding, type TaskFinding, validateBatch, validateFinding, validateFindings, validEvidence, } from "./findings-rules.js";
|
|
12
|
+
export { type FactFinding, type Finding, type FindingsInput, type FindingsReceipt, type FindingsRules, findingFields, findingFingerprint, findingId, findingNeeds, findingText, type PaymentFinding, snakeCase, type TaskFinding, validateBatch, validateFinding, validateFindings, validEvidence, } from "./findings-rules.js";
|
|
13
13
|
export { liftMeetingFields, MEETING_HIT_QUERY, meetingWordsQuery, parseMeetingQuery } from "./hit-query.js";
|
|
14
14
|
export { type ImportFile, importFiles } from "./import.js";
|
|
15
15
|
export { MEETING_ISSUES, MEETING_QUERY, type MeetingIssue, type MeetingItem, type MeetingOwner, meeting, meetingIssuesOf, meetingItem, } from "./item.js";
|
package/dist/meetings/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { entryInWindow, eventChange } from "./event-store.js";
|
|
|
6
6
|
export { linkEvent } from "./events.js";
|
|
7
7
|
export { exportMeeting, serializeVtt, } from "./export.js";
|
|
8
8
|
export { findingHash, MEETING_FINDINGS_KIND, meetingFindingFingerprint, validateMeetingFindings, } from "./findings.js";
|
|
9
|
-
export { findingFields, findingFingerprint, findingId, findingText, validateBatch, validateFinding, validateFindings, validEvidence, } from "./findings-rules.js";
|
|
9
|
+
export { findingFields, findingFingerprint, findingId, findingNeeds, findingText, snakeCase, validateBatch, validateFinding, validateFindings, validEvidence, } from "./findings-rules.js";
|
|
10
10
|
export { liftMeetingFields, MEETING_HIT_QUERY, meetingWordsQuery, parseMeetingQuery } from "./hit-query.js";
|
|
11
11
|
export { importFiles } from "./import.js";
|
|
12
12
|
export { MEETING_ISSUES, MEETING_QUERY, meeting, meetingIssuesOf, meetingItem, } from "./item.js";
|
|
@@ -3,7 +3,7 @@ export const meetingTimestamp = (value, name) => {
|
|
|
3
3
|
if (typeof value !== "string" ||
|
|
4
4
|
!/^\d{4}-\d{2}-\d{2}T.+(?:Z|[+-]\d{2}:\d{2})$/i.test(value) ||
|
|
5
5
|
!Number.isFinite(Date.parse(value)))
|
|
6
|
-
throw new CliError("validation_error", `${name} must be an ISO timestamp with a timezone`);
|
|
6
|
+
throw new CliError("validation_error", `${name} must be an ISO timestamp with a timezone, such as 2026-11-09T09:00:00Z`);
|
|
7
7
|
const date = value.slice(0, 10);
|
|
8
8
|
const instant = Date.parse(`${date}T00:00:00Z`);
|
|
9
9
|
if (!Number.isFinite(instant) || new Date(instant).toISOString().slice(0, 10) !== date)
|
|
@@ -159,6 +159,8 @@ export interface FinanceStore {
|
|
|
159
159
|
/** Issued or partly paid. */
|
|
160
160
|
open?: boolean;
|
|
161
161
|
counterparty?: string;
|
|
162
|
+
/** The number printed on it, exactly. */
|
|
163
|
+
number?: string;
|
|
162
164
|
}): Promise<Invoice[]>;
|
|
163
165
|
/** The owner sends a draft: its status then follows its allocations. */
|
|
164
166
|
issueInvoice(reference: string, by: Author): Promise<InvoiceDetail>;
|
|
@@ -402,10 +402,10 @@ export const financeOperationsOver = (context) => {
|
|
|
402
402
|
return database
|
|
403
403
|
.prepare(`${INVOICES} WHERE i.deleted_at IS NULL AND (? IS NULL OR i.direction = ?) AND (? IS NULL OR i.status = ?) ` +
|
|
404
404
|
"AND (? IS NULL OR i.review = ?) AND (? IS NULL OR i.project_id = ?) " +
|
|
405
|
-
"AND (? IS NULL OR (i.counterparty_type = ? AND i.counterparty_id = ?)) " +
|
|
405
|
+
"AND (? IS NULL OR (i.counterparty_type = ? AND i.counterparty_id = ?)) AND (? IS NULL OR i.number = ?) " +
|
|
406
406
|
`${filter.open ? "AND i.status IN ('issued', 'partly_paid') " : ""}AND ${by.sql} ` +
|
|
407
407
|
"ORDER BY i.created_at DESC, i.id DESC LIMIT ?")
|
|
408
|
-
.all(filter.direction ?? null, filter.direction ?? null, status, status, review, review, project, project, counterparty?.type ?? null, counterparty?.type ?? null, counterparty?.id ?? null, ...by.params, limitOf(filter.limit))
|
|
408
|
+
.all(filter.direction ?? null, filter.direction ?? null, status, status, review, review, project, project, counterparty?.type ?? null, counterparty?.type ?? null, counterparty?.id ?? null, filter.number ?? null, filter.number ?? null, ...by.params, limitOf(filter.limit))
|
|
409
409
|
.map(invoiceOf);
|
|
410
410
|
},
|
|
411
411
|
issueInvoice: (reference, by) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.584.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|