@wirecat/cli-core 0.581.0 → 0.583.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 +54 -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/entities/index.d.ts +2 -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/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/proposals/index.d.ts +1 -1
- package/dist/services/proposals/index.js +1 -1
- package/dist/services/proposals/kinds/organization.d.ts +7 -0
- package/dist/services/proposals/kinds/organization.js +22 -5
- package/dist/store/knowledge-query.js +7 -1
- package/dist/store/sqlite/finance.d.ts +2 -0
- package/dist/store/sqlite/finance.js +2 -2
- package/dist/store/sqlite/knowledge.d.ts +2 -2
- package/dist/store/sqlite/knowledge.js +2 -21
- package/dist/store/sqlite/organizations.d.ts +14 -0
- package/dist/store/sqlite/organizations.js +48 -0
- package/package.json +1 -1
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)
|
package/dist/services/index.d.ts
CHANGED
|
@@ -188,7 +188,7 @@ export { type PackageUpgradeOutcome, type PackageUpgradePorts, type PackageUpgra
|
|
|
188
188
|
export { type PersonMeetingContext, type PersonMeetingContextItem, type PersonMeetingContextOptions, type PersonMeetingReadStore, personMeetingContext, } from "./person-meeting-context.js";
|
|
189
189
|
export { privatePeopleService } from "./private-people.js";
|
|
190
190
|
export { approveAsTask, type ProposalTaskStore, proposalSource } from "./proposal-tasks.js";
|
|
191
|
-
export { type Decided, type Decision, emptyProposalRegistry, FACT_SUMMARIES, ORGANIZATION_ADD_KIND, type OrganizationProposal, type ProposalFilter, type ProposalKind, type ProposalRegistry, type ProposalRow, parseProposalIds, proposalRegistry, proposeOrganization, } from "./proposals/index.js";
|
|
191
|
+
export { type Decided, type Decision, emptyProposalRegistry, FACT_SUMMARIES, ORGANIZATION_ADD_KIND, ORGANIZATION_RENAME_KIND, type OrganizationProposal, type OrganizationRenameProposal, type ProposalFilter, type ProposalKind, type ProposalRegistry, type ProposalRow, parseProposalIds, proposalRegistry, proposeOrganization, proposeOrganizationRename, } from "./proposals/index.js";
|
|
192
192
|
export type { RetentionQuery, RetentionService } from "./retention.js";
|
|
193
193
|
export { retentionService } from "./retention.js";
|
|
194
194
|
export { RESOURCES_SEARCHED, type SearchAllFound, type SearchAllItem, type SearchAllMessenger, type SearchAllRequest, type SearchedResource, searchAll, } from "./search-all.js";
|
package/dist/services/index.js
CHANGED
|
@@ -146,7 +146,7 @@ export { upgradePackage, } from "./package-upgrade.js";
|
|
|
146
146
|
export { personMeetingContext, } from "./person-meeting-context.js";
|
|
147
147
|
export { privatePeopleService } from "./private-people.js";
|
|
148
148
|
export { approveAsTask, proposalSource } from "./proposal-tasks.js";
|
|
149
|
-
export { emptyProposalRegistry, FACT_SUMMARIES, ORGANIZATION_ADD_KIND, parseProposalIds, proposalRegistry, proposeOrganization, } from "./proposals/index.js";
|
|
149
|
+
export { emptyProposalRegistry, FACT_SUMMARIES, ORGANIZATION_ADD_KIND, ORGANIZATION_RENAME_KIND, parseProposalIds, proposalRegistry, proposeOrganization, proposeOrganizationRename, } from "./proposals/index.js";
|
|
150
150
|
export { retentionService } from "./retention.js";
|
|
151
151
|
export { RESOURCES_SEARCHED, searchAll, } from "./search-all.js";
|
|
152
152
|
export { RESOURCES_SEARCHED_WITH_MEETINGS, searchAllIncludingMeetings, searchAllWithMeetings, } from "./search-all-meetings.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { parseProposalIds } from "./ids.js";
|
|
2
2
|
export { FACT_SUMMARIES } from "./kinds/findings.js";
|
|
3
|
-
export { ORGANIZATION_ADD_KIND, type OrganizationProposal, proposeOrganization } from "./kinds/organization.js";
|
|
3
|
+
export { ORGANIZATION_ADD_KIND, ORGANIZATION_RENAME_KIND, type OrganizationProposal, type OrganizationRenameProposal, proposeOrganization, proposeOrganizationRename, } from "./kinds/organization.js";
|
|
4
4
|
export { emptyProposalRegistry, proposalRegistry } from "./registry.js";
|
|
5
5
|
export type { Decided, Decision, ProposalFilter, ProposalKind, ProposalRegistry, ProposalRow } from "./types.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { parseProposalIds } from "./ids.js";
|
|
2
2
|
export { FACT_SUMMARIES } from "./kinds/findings.js";
|
|
3
|
-
export { ORGANIZATION_ADD_KIND, proposeOrganization } from "./kinds/organization.js";
|
|
3
|
+
export { ORGANIZATION_ADD_KIND, ORGANIZATION_RENAME_KIND, proposeOrganization, proposeOrganizationRename, } from "./kinds/organization.js";
|
|
4
4
|
export { emptyProposalRegistry, proposalRegistry } from "./registry.js";
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import type { Author, MessageStore, Organization, ProposedAction } from "../../../store/index.js";
|
|
2
2
|
import type { ProposalKind } from "../types.js";
|
|
3
3
|
export declare const ORGANIZATION_ADD_KIND = "organization_add";
|
|
4
|
+
export declare const ORGANIZATION_RENAME_KIND = "organization_rename";
|
|
4
5
|
export interface OrganizationProposal {
|
|
5
6
|
name: string;
|
|
6
7
|
kind: Organization["kind"];
|
|
7
8
|
scope?: Organization["scope"];
|
|
8
9
|
}
|
|
10
|
+
export interface OrganizationRenameProposal {
|
|
11
|
+
organization: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
9
14
|
/** An agent's new organization waits as a stored proposal; the organization exists only once it is confirmed. */
|
|
10
15
|
export declare const proposeOrganization: (store: Pick<MessageStore, "proposedActions">, input: OrganizationProposal, by: Author, reason?: string) => Promise<ProposedAction>;
|
|
16
|
+
/** An agent's rename waits as a stored proposal too; the organization keeps its name until it is confirmed. */
|
|
17
|
+
export declare const proposeOrganizationRename: (store: Pick<MessageStore, "proposedActions">, input: OrganizationRenameProposal, by: Author, reason?: string) => Promise<ProposedAction>;
|
|
11
18
|
export declare const organizationKind: (store: Pick<MessageStore, "proposedActions" | "knowledge">) => ProposalKind;
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { proposerOfAction } from "../../../store/index.js";
|
|
2
2
|
import { actionStatus, kept, pushed, storedStatus } from "./shared.js";
|
|
3
3
|
export const ORGANIZATION_ADD_KIND = "organization_add";
|
|
4
|
+
export const ORGANIZATION_RENAME_KIND = "organization_rename";
|
|
4
5
|
const rowOf = (action) => {
|
|
6
|
+
const renamed = action.kind === ORGANIZATION_RENAME_KIND;
|
|
5
7
|
const { name, kind } = action.payload;
|
|
8
|
+
const { organization } = action.payload;
|
|
6
9
|
return {
|
|
7
10
|
id: action.id,
|
|
8
11
|
kind: "organization",
|
|
9
12
|
status: actionStatus(action.status),
|
|
10
13
|
project: null,
|
|
11
|
-
subject:
|
|
14
|
+
subject: renamed ? organization : "organization:new",
|
|
12
15
|
object: null,
|
|
13
|
-
summary: `add ${kind} ${name}`,
|
|
16
|
+
summary: renamed ? `rename ${organization} to ${name}` : `add ${kind} ${name}`,
|
|
14
17
|
proposedBy: proposerOfAction(action),
|
|
15
18
|
reason: action.reason,
|
|
16
19
|
evidence: [],
|
|
@@ -27,13 +30,20 @@ export const proposeOrganization = (store, input, by, reason) => store.proposedA
|
|
|
27
30
|
payload: input,
|
|
28
31
|
...(reason === undefined ? {} : { reason }),
|
|
29
32
|
});
|
|
33
|
+
/** An agent's rename waits as a stored proposal too; the organization keeps its name until it is confirmed. */
|
|
34
|
+
export const proposeOrganizationRename = (store, input, by, reason) => store.proposedActions.propose({
|
|
35
|
+
kind: ORGANIZATION_RENAME_KIND,
|
|
36
|
+
by,
|
|
37
|
+
payload: input,
|
|
38
|
+
...(reason === undefined ? {} : { reason }),
|
|
39
|
+
});
|
|
30
40
|
export const organizationKind = (store) => ({
|
|
31
41
|
kind: "organization",
|
|
32
|
-
stored: [ORGANIZATION_ADD_KIND],
|
|
42
|
+
stored: [ORGANIZATION_ADD_KIND, ORGANIZATION_RENAME_KIND],
|
|
33
43
|
list: async (filter) => {
|
|
34
44
|
const status = storedStatus(filter);
|
|
35
45
|
const actions = await store.proposedActions.list({
|
|
36
|
-
kinds: [ORGANIZATION_ADD_KIND],
|
|
46
|
+
kinds: [ORGANIZATION_ADD_KIND, ORGANIZATION_RENAME_KIND],
|
|
37
47
|
...(status === undefined ? {} : { status }),
|
|
38
48
|
...pushed(filter),
|
|
39
49
|
});
|
|
@@ -45,7 +55,14 @@ export const organizationKind = (store) => ({
|
|
|
45
55
|
await store.proposedActions.reject(id, by, reason);
|
|
46
56
|
return { id, kind: "organization", status: "rejected", made: [] };
|
|
47
57
|
}
|
|
48
|
-
const
|
|
58
|
+
const action = await store.proposedActions.get(id);
|
|
59
|
+
if (action.kind === ORGANIZATION_RENAME_KIND) {
|
|
60
|
+
const { organization, name } = action.payload;
|
|
61
|
+
const renamed = await store.knowledge.updateOrganization(organization, { name });
|
|
62
|
+
await store.proposedActions.approve(id, by, { organization: renamed.ref });
|
|
63
|
+
return { id, kind: "organization", status: "confirmed", made: [] };
|
|
64
|
+
}
|
|
65
|
+
const { name, kind, scope } = action.payload;
|
|
49
66
|
const made = await store.knowledge.addOrganization({ name, kind, ...(scope === undefined ? {} : { scope }) });
|
|
50
67
|
await store.proposedActions.approve(id, by, { organization: made.ref });
|
|
51
68
|
return { id, kind: "organization", status: "confirmed", made: [made.ref] };
|
|
@@ -6,7 +6,13 @@ export const ORGANIZATIONS_QUERY = {
|
|
|
6
6
|
backend: "sql",
|
|
7
7
|
fields: queryFieldsOf(organization, {
|
|
8
8
|
native: ["id", "kind", "scope", "created_at"],
|
|
9
|
-
examples: {
|
|
9
|
+
examples: {
|
|
10
|
+
id: "id:1",
|
|
11
|
+
kind: "kind:company",
|
|
12
|
+
name: "name:*acme*",
|
|
13
|
+
scope: "scope:work",
|
|
14
|
+
created_at: "created_at>=2026-01-01",
|
|
15
|
+
},
|
|
10
16
|
}),
|
|
11
17
|
};
|
|
12
18
|
export const PROJECTS_QUERY = {
|
|
@@ -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) => {
|