@wirecat/cli-core 0.556.0 → 0.558.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/calendar/mcp/index.d.ts +12 -12
  3. package/dist/cli/index.d.ts +0 -1
  4. package/dist/cli/index.js +0 -1
  5. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  6. package/dist/domain/finance.d.ts +40 -0
  7. package/dist/domain/finance.js +39 -0
  8. package/dist/domain/money.d.ts +29 -0
  9. package/dist/domain/money.js +98 -0
  10. package/dist/domain/ref-summaries.js +2 -0
  11. package/dist/domain/references.d.ts +1 -1
  12. package/dist/domain/references.js +2 -0
  13. package/dist/entities/fact.d.ts +1 -12
  14. package/dist/entities/fact.js +0 -13
  15. package/dist/entities/index.d.ts +2 -12
  16. package/dist/entities/meeting-findings.d.ts +1 -1
  17. package/dist/errors/reasons.js +16 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.js +2 -0
  20. package/dist/mail/cli/review.js +0 -1
  21. package/dist/meetings/findings-rules.d.ts +7 -4
  22. package/dist/meetings/findings-rules.js +11 -4
  23. package/dist/meetings/mcp/index.d.ts +15 -15
  24. package/dist/meetings/payment.d.ts +10 -6
  25. package/dist/meetings/payment.js +21 -6
  26. package/dist/services/project-status/index.d.ts +1 -1
  27. package/dist/services/project-status/index.js +7 -3
  28. package/dist/services/project-status/signals/payments.d.ts +3 -3
  29. package/dist/services/project-status/signals/payments.js +43 -17
  30. package/dist/services/proposals/kinds/finance.d.ts +4 -0
  31. package/dist/services/proposals/kinds/finance.js +40 -0
  32. package/dist/services/proposals/registry.js +3 -0
  33. package/dist/store/evidence-findings.js +16 -8
  34. package/dist/store/index.d.ts +2 -1
  35. package/dist/store/project-transfer/request.d.ts +1 -1
  36. package/dist/store/project-transfer/schema.d.ts +2 -2
  37. package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
  38. package/dist/store/sqlite/entity-types.d.ts +2 -0
  39. package/dist/store/sqlite/entity-types.js +2 -0
  40. package/dist/store/sqlite/facts.d.ts +0 -13
  41. package/dist/store/sqlite/facts.js +5 -58
  42. package/dist/store/sqlite/finance.d.ts +203 -0
  43. package/dist/store/sqlite/finance.js +529 -0
  44. package/dist/store/sqlite/findings-core.js +5 -14
  45. package/dist/store/sqlite/knowledge.js +3 -1
  46. package/dist/store/sqlite/manifest.js +3 -0
  47. package/dist/store/sqlite/migrations.generated.js +43 -0
  48. package/dist/store/sqlite/open-store.js +2 -0
  49. package/dist/store/sqlite/payment-entries.d.ts +26 -0
  50. package/dist/store/sqlite/payment-entries.js +91 -0
  51. package/dist/store/sqlite/payment-facts.d.ts +12 -0
  52. package/dist/store/sqlite/payment-facts.js +92 -0
  53. package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
  54. package/dist/store/sqlite/ref-summaries.js +33 -0
  55. package/dist/store/sqlite/schema.d.ts +1043 -0
  56. package/dist/store/sqlite/schema.js +98 -0
  57. package/dist/store/sqlite/things.js +2 -0
  58. package/dist/store/store.d.ts +3 -1
  59. package/dist/store/store.js +5 -0
  60. package/package.json +1 -1
  61. package/dist/cli/payment-query.d.ts +0 -8
  62. package/dist/cli/payment-query.js +0 -27
@@ -14,16 +14,19 @@ interface FindingBase {
14
14
  }
15
15
  export interface FactFinding extends FindingBase {
16
16
  kind: "fact";
17
- type: Exclude<(typeof FACT_TYPES)[number], "payment">;
17
+ type: (typeof FACT_TYPES)[number];
18
18
  statement: string;
19
19
  }
20
- /** A payment an agent read; confirming it records a `payment` fact. A paid one supersedes the expected one. */
20
+ /**
21
+ * A payment an agent read. Confirming it writes money through `store.finance`, never a fact: a paid one a `payments`
22
+ * row, an expected one an invoice; a paid one that supersedes an invoice settles it, a cancelled one voids it.
23
+ */
21
24
  export interface PaymentFinding extends FindingBase {
22
25
  kind: "payment";
23
- /** The fact's full sentence; without one, the payment's title. */
26
+ /** The full sentence, kept as the proposal's reason; without one, the payment's title. */
24
27
  statement?: string;
25
28
  payment: Payment;
26
- /** The `fact:` of the expected payment this one replaces, once it is paid or cancelled. */
29
+ /** The `invoice:` of the expected payment this one settles (paid) or voids (cancelled). */
27
30
  supersedes?: string;
28
31
  }
29
32
  export interface TaskFinding extends FindingBase {
@@ -70,20 +70,27 @@ export const validateFindings = (input, rules) => {
70
70
  if (finding.kind === "fact") {
71
71
  fields(finding, ["id", "project", "subject", "evidence", "kind", "type", "statement"], rules.singular);
72
72
  if (finding.type === "payment")
73
- return invalid("a payment is a finding of kind payment");
73
+ return invalid("a payment is a finding of kind payment; money is a payments row, not a fact");
74
74
  if (!FACT_TYPES.includes(finding.type))
75
75
  return invalid("unknown fact finding type");
76
76
  return { ...base, kind: "fact", type: finding.type, statement: text(finding.statement, 2000) };
77
77
  }
78
78
  if (finding.kind === "payment" && rules.payments) {
79
79
  fields(finding, ["id", "project", "subject", "evidence", "kind", "statement", "payment", "supersedes"], rules.singular);
80
- if (finding.supersedes !== undefined && !/^fact:[1-9]\d*$/.test(String(finding.supersedes)))
81
- return invalid("a payment finding supersedes a fact: reference");
80
+ if (finding.supersedes !== undefined && !/^invoice:[1-9]\d*$/.test(String(finding.supersedes)))
81
+ return invalid("a payment finding supersedes an invoice: reference, the expected payment it settles or voids");
82
+ const payment = validatePayment(finding.payment);
83
+ if (payment.state === "expected" && finding.supersedes !== undefined)
84
+ return invalid("an expected payment supersedes nothing; a paid or cancelled one supersedes its invoice");
85
+ if (payment.state === "cancelled" && finding.supersedes === undefined)
86
+ return invalid("a cancelled payment names the invoice: it cancels in supersedes");
87
+ if (payment.state === "expected" && !/^(person|organization):/.test(payment.counterparty ?? ""))
88
+ return invalid("an expected payment becomes an invoice, which names its counterparty as a person: or organization:");
82
89
  return {
83
90
  ...base,
84
91
  kind: "payment",
85
92
  ...(finding.statement === undefined ? {} : { statement: text(finding.statement, 2000) }),
86
- payment: validatePayment(finding.payment),
93
+ payment,
87
94
  ...(finding.supersedes === undefined ? {} : { supersedes: finding.supersedes }),
88
95
  };
89
96
  }
@@ -38,6 +38,7 @@ export declare const meetingTools: ({
38
38
  max_read_bytes?: undefined;
39
39
  cursor?: undefined;
40
40
  } | {
41
+ limit?: undefined;
41
42
  event_id?: undefined;
42
43
  series_id?: undefined;
43
44
  page?: undefined;
@@ -53,11 +54,11 @@ export declare const meetingTools: ({
53
54
  format?: undefined;
54
55
  transcript_id?: undefined;
55
56
  query?: undefined;
56
- limit?: undefined;
57
57
  scan_limit?: undefined;
58
58
  max_read_bytes?: undefined;
59
59
  cursor?: undefined;
60
60
  } | {
61
+ limit?: undefined;
61
62
  event_id?: undefined;
62
63
  series_id?: undefined;
63
64
  page?: undefined;
@@ -75,11 +76,11 @@ export declare const meetingTools: ({
75
76
  format?: undefined;
76
77
  transcript_id?: undefined;
77
78
  query?: undefined;
78
- limit?: undefined;
79
79
  scan_limit?: undefined;
80
80
  max_read_bytes?: undefined;
81
81
  cursor?: undefined;
82
82
  } | {
83
+ limit?: undefined;
83
84
  event_id?: undefined;
84
85
  series_id?: undefined;
85
86
  page?: undefined;
@@ -101,7 +102,6 @@ export declare const meetingTools: ({
101
102
  minimum: number;
102
103
  };
103
104
  query?: undefined;
104
- limit?: undefined;
105
105
  scan_limit?: undefined;
106
106
  max_read_bytes?: undefined;
107
107
  cursor?: undefined;
@@ -188,6 +188,7 @@ export declare const meetingTools: ({
188
188
  max_read_bytes?: undefined;
189
189
  cursor?: undefined;
190
190
  } | {
191
+ limit?: undefined;
191
192
  event_id?: undefined;
192
193
  series_id?: undefined;
193
194
  page?: undefined;
@@ -203,11 +204,11 @@ export declare const meetingTools: ({
203
204
  format?: undefined;
204
205
  transcript_id?: undefined;
205
206
  query?: undefined;
206
- limit?: undefined;
207
207
  scan_limit?: undefined;
208
208
  max_read_bytes?: undefined;
209
209
  cursor?: undefined;
210
210
  } | {
211
+ limit?: undefined;
211
212
  event_id?: undefined;
212
213
  series_id?: undefined;
213
214
  page?: undefined;
@@ -225,11 +226,11 @@ export declare const meetingTools: ({
225
226
  format?: undefined;
226
227
  transcript_id?: undefined;
227
228
  query?: undefined;
228
- limit?: undefined;
229
229
  scan_limit?: undefined;
230
230
  max_read_bytes?: undefined;
231
231
  cursor?: undefined;
232
232
  } | {
233
+ limit?: undefined;
233
234
  event_id?: undefined;
234
235
  series_id?: undefined;
235
236
  page?: undefined;
@@ -251,7 +252,6 @@ export declare const meetingTools: ({
251
252
  minimum: number;
252
253
  };
253
254
  query?: undefined;
254
- limit?: undefined;
255
255
  scan_limit?: undefined;
256
256
  max_read_bytes?: undefined;
257
257
  cursor?: undefined;
@@ -338,6 +338,7 @@ export declare const meetingTools: ({
338
338
  max_read_bytes?: undefined;
339
339
  cursor?: undefined;
340
340
  } | {
341
+ limit?: undefined;
341
342
  event_id?: undefined;
342
343
  series_id?: undefined;
343
344
  page?: undefined;
@@ -353,11 +354,11 @@ export declare const meetingTools: ({
353
354
  format?: undefined;
354
355
  transcript_id?: undefined;
355
356
  query?: undefined;
356
- limit?: undefined;
357
357
  scan_limit?: undefined;
358
358
  max_read_bytes?: undefined;
359
359
  cursor?: undefined;
360
360
  } | {
361
+ limit?: undefined;
361
362
  event_id?: undefined;
362
363
  series_id?: undefined;
363
364
  page?: undefined;
@@ -375,11 +376,11 @@ export declare const meetingTools: ({
375
376
  format?: undefined;
376
377
  transcript_id?: undefined;
377
378
  query?: undefined;
378
- limit?: undefined;
379
379
  scan_limit?: undefined;
380
380
  max_read_bytes?: undefined;
381
381
  cursor?: undefined;
382
382
  } | {
383
+ limit?: undefined;
383
384
  event_id?: undefined;
384
385
  series_id?: undefined;
385
386
  page?: undefined;
@@ -401,7 +402,6 @@ export declare const meetingTools: ({
401
402
  minimum: number;
402
403
  };
403
404
  query?: undefined;
404
- limit?: undefined;
405
405
  scan_limit?: undefined;
406
406
  max_read_bytes?: undefined;
407
407
  cursor?: undefined;
@@ -488,6 +488,7 @@ export declare const meetingTools: ({
488
488
  max_read_bytes?: undefined;
489
489
  cursor?: undefined;
490
490
  } | {
491
+ limit?: undefined;
491
492
  event_id?: undefined;
492
493
  series_id?: undefined;
493
494
  page?: undefined;
@@ -503,11 +504,11 @@ export declare const meetingTools: ({
503
504
  format?: undefined;
504
505
  transcript_id?: undefined;
505
506
  query?: undefined;
506
- limit?: undefined;
507
507
  scan_limit?: undefined;
508
508
  max_read_bytes?: undefined;
509
509
  cursor?: undefined;
510
510
  } | {
511
+ limit?: undefined;
511
512
  event_id?: undefined;
512
513
  series_id?: undefined;
513
514
  page?: undefined;
@@ -525,11 +526,11 @@ export declare const meetingTools: ({
525
526
  format?: undefined;
526
527
  transcript_id?: undefined;
527
528
  query?: undefined;
528
- limit?: undefined;
529
529
  scan_limit?: undefined;
530
530
  max_read_bytes?: undefined;
531
531
  cursor?: undefined;
532
532
  } | {
533
+ limit?: undefined;
533
534
  event_id?: undefined;
534
535
  series_id?: undefined;
535
536
  page?: undefined;
@@ -551,7 +552,6 @@ export declare const meetingTools: ({
551
552
  minimum: number;
552
553
  };
553
554
  query?: undefined;
554
- limit?: undefined;
555
555
  scan_limit?: undefined;
556
556
  max_read_bytes?: undefined;
557
557
  cursor?: undefined;
@@ -638,6 +638,7 @@ export declare const meetingTools: ({
638
638
  max_read_bytes?: undefined;
639
639
  cursor?: undefined;
640
640
  } | {
641
+ limit?: undefined;
641
642
  event_id?: undefined;
642
643
  series_id?: undefined;
643
644
  page?: undefined;
@@ -653,11 +654,11 @@ export declare const meetingTools: ({
653
654
  format?: undefined;
654
655
  transcript_id?: undefined;
655
656
  query?: undefined;
656
- limit?: undefined;
657
657
  scan_limit?: undefined;
658
658
  max_read_bytes?: undefined;
659
659
  cursor?: undefined;
660
660
  } | {
661
+ limit?: undefined;
661
662
  event_id?: undefined;
662
663
  series_id?: undefined;
663
664
  page?: undefined;
@@ -675,11 +676,11 @@ export declare const meetingTools: ({
675
676
  format?: undefined;
676
677
  transcript_id?: undefined;
677
678
  query?: undefined;
678
- limit?: undefined;
679
679
  scan_limit?: undefined;
680
680
  max_read_bytes?: undefined;
681
681
  cursor?: undefined;
682
682
  } | {
683
+ limit?: undefined;
683
684
  event_id?: undefined;
684
685
  series_id?: undefined;
685
686
  page?: undefined;
@@ -701,7 +702,6 @@ export declare const meetingTools: ({
701
702
  minimum: number;
702
703
  };
703
704
  query?: undefined;
704
- limit?: undefined;
705
705
  scan_limit?: undefined;
706
706
  max_read_bytes?: undefined;
707
707
  cursor?: undefined;
@@ -1,8 +1,11 @@
1
- /** Every fact type the store takes; findings and meeting changes check against the same list. */
2
- export declare const FACT_TYPES: readonly ["fact", "statement", "question", "decision", "promise", "payment"];
1
+ /** Every fact type the store takes; findings and meeting changes check against the same list. Money is not a fact. */
2
+ export declare const FACT_TYPES: readonly ["fact", "statement", "question", "decision", "promise"];
3
3
  export declare const PAYMENT_DIRECTIONS: readonly ["to_us", "from_us"];
4
4
  export declare const PAYMENT_STATES: readonly ["expected", "paid", "cancelled"];
5
- /** A payment fact's fields, kept in `facts.metadata.payment`. */
5
+ /**
6
+ * A payment finding's fields, as an agent reads them from evidence. Confirmed by the owner, a paid one becomes a
7
+ * `payments` row, an expected one an invoice, a cancelled one voids the invoice it supersedes.
8
+ */
6
9
  export interface Payment {
7
10
  /** A decimal as text with two places, "50000.00": money is never a float. */
8
11
  amount: string;
@@ -14,14 +17,15 @@ export interface Payment {
14
17
  counterparty_name: string | null;
15
18
  /** An ISO date. */
16
19
  due_at: string | null;
17
- /** A change of state is a new fact that supersedes this one, so the history and its evidence stay. */
18
20
  state: (typeof PAYMENT_STATES)[number];
19
- /** What it is for, short: "support, September". The fact's statement is the full sentence. */
21
+ /** What it is for, short: "support, September". The finding's statement is the full sentence. */
20
22
  item: string | null;
23
+ /** When a paid one was paid, an ISO date; without it the due date, else the day the owner confirms it. */
24
+ paid_at?: string;
21
25
  }
22
26
  /** A payment as stored: the amount with two decimals, the currency upper case; refuses what is not one. */
23
27
  export declare const validatePayment: (input: unknown) => Payment;
24
- /** One line for a payment, as a list or an inbox shows it: "1 200 EUR to us from Ana, due 31.10 (expected)". */
28
+ /** One line for a payment finding, as a list or an inbox shows it: "1 200 EUR to us from Ana, due 31.10 (expected)". */
25
29
  export declare const paymentTitle: (fact: {
26
30
  payment: Payment | null;
27
31
  }) => string;
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
- /** Every fact type the store takes; findings and meeting changes check against the same list. */
3
- export const FACT_TYPES = ["fact", "statement", "question", "decision", "promise", "payment"];
2
+ /** Every fact type the store takes; findings and meeting changes check against the same list. Money is not a fact. */
3
+ export const FACT_TYPES = ["fact", "statement", "question", "decision", "promise"];
4
4
  export const PAYMENT_DIRECTIONS = ["to_us", "from_us"];
5
5
  export const PAYMENT_STATES = ["expected", "paid", "cancelled"];
6
6
  const invalid = (message) => {
@@ -21,7 +21,7 @@ const amountOf = (value) => {
21
21
  const dateOf = (value) => {
22
22
  const date = typeof value === "string" ? value.trim() : "";
23
23
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date)
24
- return invalid("a payment's due date is an ISO date, such as 2026-10-31");
24
+ return invalid("a payment's dates are ISO dates, such as 2026-10-31");
25
25
  return date;
26
26
  };
27
27
  const optionalText = (value, what, limit) => {
@@ -36,7 +36,17 @@ const oneOf = (value, allowed, what) => {
36
36
  return invalid(`a payment's ${what} is one of ${allowed.join(", ")}`);
37
37
  return value;
38
38
  };
39
- const FIELDS = ["amount", "currency", "direction", "counterparty", "counterparty_name", "due_at", "state", "item"];
39
+ const FIELDS = [
40
+ "amount",
41
+ "currency",
42
+ "direction",
43
+ "counterparty",
44
+ "counterparty_name",
45
+ "due_at",
46
+ "state",
47
+ "item",
48
+ "paid_at",
49
+ ];
40
50
  /** A payment as stored: the amount with two decimals, the currency upper case; refuses what is not one. */
41
51
  export const validatePayment = (input) => {
42
52
  if (!input || typeof input !== "object" || Array.isArray(input))
@@ -51,6 +61,9 @@ export const validatePayment = (input) => {
51
61
  const counterparty = optionalText(raw.counterparty, "counterparty", 80);
52
62
  if (counterparty !== null && !COUNTERPARTY.test(counterparty))
53
63
  return invalid("a payment's counterparty is a person:, organization: or project: reference");
64
+ const state = oneOf(raw.state, PAYMENT_STATES, "state");
65
+ if (raw.paid_at != null && state !== "paid")
66
+ return invalid("only a paid payment has paid_at");
54
67
  return {
55
68
  amount: amountOf(raw.amount),
56
69
  currency,
@@ -58,8 +71,10 @@ export const validatePayment = (input) => {
58
71
  counterparty,
59
72
  counterparty_name: optionalText(raw.counterparty_name, "counterparty name", 200),
60
73
  due_at: raw.due_at === undefined || raw.due_at === null ? null : dateOf(raw.due_at),
61
- state: oneOf(raw.state, PAYMENT_STATES, "state"),
74
+ state,
62
75
  item: optionalText(raw.item, "item", 200),
76
+ // Left out when absent, so a finding staged before the field keeps its fingerprint.
77
+ ...(raw.paid_at == null ? {} : { paid_at: dateOf(raw.paid_at) }),
63
78
  };
64
79
  };
65
80
  const grouped = (amount) => {
@@ -67,7 +82,7 @@ const grouped = (amount) => {
67
82
  const digits = whole.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
68
83
  return cents === "00" ? digits : `${digits}.${cents}`;
69
84
  };
70
- /** One line for a payment, as a list or an inbox shows it: "1 200 EUR to us from Ana, due 31.10 (expected)". */
85
+ /** One line for a payment finding, as a list or an inbox shows it: "1 200 EUR to us from Ana, due 31.10 (expected)". */
71
86
  export const paymentTitle = (fact) => {
72
87
  const payment = fact.payment;
73
88
  if (!payment)
@@ -60,7 +60,7 @@ export interface ProjectStatus {
60
60
  chats: StatusChatSummary[];
61
61
  /** Every signal's items, in signal order, then chat order. */
62
62
  items: StatusItem[];
63
- /** Open amounts and expected payment facts, summed per direction and currency. */
63
+ /** Open amounts and what confirmed open invoices still owe, summed per direction and currency. */
64
64
  money: {
65
65
  open: MoneyTotal[];
66
66
  };
@@ -53,11 +53,15 @@ const childrenOf = async (store, root, catalog) => {
53
53
  };
54
54
  const inAccounts = (accounts, provider, account) => accounts === undefined || accounts.some((one) => one.provider === provider && one.account === account);
55
55
  const failure = (error) => (error instanceof Error ? error.message : String(error));
56
- /** An open ask, or an expected payment fact; an ask that a payment fact cites is counted once, as the fact. */
56
+ /**
57
+ * An open ask, or a confirmed open invoice; an ask that a confirmed row cites is counted once, as the row. A proposed
58
+ * row promises nothing yet, so the ask it cites still counts.
59
+ */
57
60
  const counted = (items) => {
58
- const recorded = new Set(items.filter((item) => item.signal === "payments").flatMap((item) => item.evidence.slice(1).map((one) => one.ref)));
61
+ const confirmed = (item) => item.signal === "payments" && item.data.status === "confirmed";
62
+ const recorded = new Set(items.filter(confirmed).flatMap((item) => item.evidence.slice(1).map((one) => one.ref)));
59
63
  return (item, state) => item.signal === "payments"
60
- ? state === "expected"
64
+ ? state === "expected" && confirmed(item)
61
65
  : item.signal === "amounts" && state === "open" && !item.evidence.some((one) => recorded.has(one.ref));
62
66
  };
63
67
  const totalsOf = (items) => {
@@ -1,9 +1,9 @@
1
- import { type Payment } from "../../../meetings/index.js";
1
+ import type { Payment } from "../../../meetings/index.js";
2
2
  import type { StatusSignal } from "../types.js";
3
3
  export interface PaymentsData extends Payment {
4
4
  title: string;
5
- /** The fact's review status: proposed or accepted. */
5
+ /** The row's review: proposed or confirmed. */
6
6
  status: string;
7
7
  }
8
- /** Payment facts of the project and its children, proposed or accepted. Only what someone recorded. */
8
+ /** Payments and open invoices of the project and its children from `store.finance`, proposed or confirmed. */
9
9
  export declare const payments: StatusSignal<PaymentsData>;
@@ -1,24 +1,50 @@
1
- import { paymentTitle } from "../../../meetings/index.js";
2
- const HOLDING = new Set(["proposed", "accepted"]);
3
- /** Payment facts of the project and its children, proposed or accepted. Only what someone recorded. */
1
+ const holding = (row) => row.review !== "rejected";
2
+ const item = (row, at, data) => ({
3
+ signal: "payments",
4
+ chat: null,
5
+ at,
6
+ who: null,
7
+ // The row first, then what it cites, so an amount read from a cited message is not counted twice.
8
+ evidence: [row.ref, ...row.evidence].map((ref) => ({ ref, fingerprint: null })),
9
+ basis: "store",
10
+ data: { ...data, title: row.title, status: row.review },
11
+ });
12
+ const paid = (row) => {
13
+ const party = row.direction === "in" ? row.payer : row.payee;
14
+ return item(row, `${row.date}T00:00:00.000Z`, {
15
+ amount: row.amount,
16
+ currency: row.currency,
17
+ direction: row.direction === "in" ? "to_us" : "from_us",
18
+ counterparty: party?.ref ?? null,
19
+ counterparty_name: party?.name ?? null,
20
+ due_at: null,
21
+ state: "paid",
22
+ item: row.reference,
23
+ paid_at: row.date,
24
+ });
25
+ };
26
+ /** What an open invoice still owes is the expected payment. */
27
+ const expected = (row) => item(row, row.issueDate === null ? row.createdAt : `${row.issueDate}T00:00:00.000Z`, {
28
+ amount: row.open,
29
+ currency: row.currency,
30
+ direction: row.direction === "receivable" ? "to_us" : "from_us",
31
+ counterparty: row.counterparty.ref,
32
+ counterparty_name: row.counterparty.name,
33
+ due_at: row.dueDate,
34
+ state: "expected",
35
+ item: row.number,
36
+ });
37
+ /** Payments and open invoices of the project and its children from `store.finance`, proposed or confirmed. */
4
38
  export const payments = {
5
39
  id: "payments",
6
- version: 2,
40
+ version: 3,
7
41
  scope: "project",
8
42
  read: async (context) => {
9
43
  const projects = [context.project.ref, ...context.project.children.map((child) => child.ref)];
10
- const facts = (await Promise.all(projects.map((project) => context.store.facts.list({ project, type: "payment", limit: 500 })))).flat();
11
- return facts
12
- .filter((fact) => fact.payment !== null && HOLDING.has(fact.status))
13
- .map((fact) => ({
14
- signal: "payments",
15
- chat: null,
16
- at: fact.occurredAt ?? fact.createdAt,
17
- who: null,
18
- // The fact first, then what it cites, so an amount read from a cited message is not counted twice.
19
- evidence: [fact.ref, ...fact.evidence].map((ref) => ({ ref, fingerprint: null })),
20
- basis: "fact",
21
- data: { ...fact.payment, title: paymentTitle(fact), status: fact.status },
22
- }));
44
+ const rows = await Promise.all(projects.map(async (project) => [
45
+ ...(await context.store.finance.payments({ project, limit: 500 })).filter(holding).map(paid),
46
+ ...(await context.store.finance.invoices({ project, open: true, limit: 500 })).filter(holding).map(expected),
47
+ ]));
48
+ return rows.flat();
23
49
  },
24
50
  };
@@ -0,0 +1,4 @@
1
+ import type { MessageStore } from "../../../store/index.js";
2
+ import type { ProposalKind } from "../types.js";
3
+ export declare const invoiceKind: (store: Pick<MessageStore, "finance">) => ProposalKind;
4
+ export declare const paymentKind: (store: Pick<MessageStore, "finance">) => ProposalKind;
@@ -0,0 +1,40 @@
1
+ import { kept, proposerOfAuthor, pushed } from "./shared.js";
2
+ const rowOf = (kind, entry) => ({
3
+ id: entry.ref,
4
+ kind,
5
+ status: entry.review,
6
+ project: entry.project,
7
+ subject: entry.ref,
8
+ object: "counterparty" in entry
9
+ ? entry.counterparty.ref
10
+ : ((entry.direction === "in" ? entry.payer : entry.payee)?.ref ?? null),
11
+ summary: entry.title,
12
+ proposedBy: proposerOfAuthor(entry.author),
13
+ reason: entry.reason,
14
+ evidence: [],
15
+ score: null,
16
+ signals: null,
17
+ parts: null,
18
+ createdAt: entry.createdAt,
19
+ });
20
+ /**
21
+ * An agent's or an import's invoice or payment. One kind per entity, because the registry routes an id by its prefix
22
+ * and the id is the entry's own ref (`payment:12`), as a fact's is. Only the owner decides money.
23
+ */
24
+ const financeKind = (store, kind) => ({
25
+ kind,
26
+ prefix: kind,
27
+ list: async (filter) => {
28
+ const status = filter.status ?? "proposed";
29
+ const options = { ...pushed(filter), ...(status === "all" ? {} : { review: status }) };
30
+ const entries = kind === "invoice" ? await store.finance.invoices(options) : await store.finance.payments(options);
31
+ return kept(entries.map((entry) => rowOf(kind, entry)), filter);
32
+ },
33
+ get: async (id) => rowOf(kind, kind === "invoice" ? await store.finance.invoice(id) : await store.finance.payment(id)),
34
+ decide: async (id, { confirm, by, reason }) => {
35
+ const entry = confirm ? await store.finance.confirm(id, by, reason) : await store.finance.reject(id, by, reason);
36
+ return { id, kind, status: confirm ? "confirmed" : "rejected", made: confirm ? [entry.ref] : [] };
37
+ },
38
+ });
39
+ export const invoiceKind = (store) => financeKind(store, "invoice");
40
+ export const paymentKind = (store) => financeKind(store, "payment");
@@ -2,6 +2,7 @@ import { CliError } from "@wirecat/cli-base";
2
2
  import { mayDecide } from "../../store/decisions.js";
3
3
  import { actionKind } from "./kinds/action.js";
4
4
  import { factKind } from "./kinds/fact.js";
5
+ import { invoiceKind, paymentKind } from "./kinds/finance.js";
5
6
  import { findingsKind } from "./kinds/findings.js";
6
7
  import { keyDocumentKind } from "./kinds/key-document.js";
7
8
  import { memoryKind } from "./kinds/memory.js";
@@ -122,6 +123,8 @@ export const proposalRegistry = (store) => {
122
123
  relationshipUpdateKind(store),
123
124
  keyDocumentKind(store),
124
125
  factKind(store),
126
+ invoiceKind(store),
127
+ paymentKind(store),
125
128
  memoryKind(store),
126
129
  findingsKind(store),
127
130
  ])
@@ -1,6 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { formatLocator, parseLocator } from "../domain/locator.js";
3
3
  import { validateFindings } from "../meetings/index.js";
4
+ import { entryAmount } from "./sqlite/payment-entries.js";
4
5
  export const EVIDENCE_FINDINGS_KIND = "evidence_findings";
5
6
  const reference = (raw) => {
6
7
  if (/^(document|note):[1-9]\d*$/.test(raw))
@@ -12,11 +13,18 @@ const reference = (raw) => {
12
13
  throw new CliError("validation_error", "finding evidence is a msg:, document: or note: reference");
13
14
  }
14
15
  };
15
- export const validateEvidenceFindings = (input) => validateFindings(input, {
16
- plural: "evidence findings",
17
- singular: "evidence finding",
18
- cited: "reference",
19
- reference,
20
- anySubject: true,
21
- payments: true,
22
- });
16
+ export const validateEvidenceFindings = (input) => {
17
+ const valid = validateFindings(input, {
18
+ plural: "evidence findings",
19
+ singular: "evidence finding",
20
+ cited: "reference",
21
+ reference,
22
+ anySubject: true,
23
+ payments: true,
24
+ });
25
+ // Refused at staging rather than when the owner accepts, which would fail the whole batch.
26
+ for (const finding of valid.findings)
27
+ if (finding.kind === "payment")
28
+ entryAmount(finding.payment);
29
+ return valid;
30
+ };
@@ -36,8 +36,9 @@ export type { ChatMetadata } from "./sqlite/chats.js";
36
36
  export type { Email, EmailAddress, EmailAttachment, EmailFacts, EmailFilter, EmailInput, EmailInThread, EmailLocationInput, EmailRecipient, EmailRef, EmailThread, EmailVectorHit, FolderEmail, Mailbox, MailboxInput, MailFilter, MailStore, PartialEmail, SenderScore, ThreadDetails, ThreadSave, } from "./sqlite/emails.js";
37
37
  export { EVENT_FIELDS, type EventChunkToEmbed, type EventHit, type EventQuery, type EventSearch, type NearestEvent, } from "./sqlite/event-search.js";
38
38
  export type { EvidenceSource } from "./sqlite/evidence-sources.js";
39
- export type { Fact, FactInput, FactsStore, PaymentFilter } from "./sqlite/facts.js";
39
+ export type { Fact, FactInput, FactsStore } from "./sqlite/facts.js";
40
40
  export type { FileListQuery, FilesStore, StoredFile } from "./sqlite/files.js";
41
+ export type { Allocation, FinanceStore, Invoice, InvoiceDetail, InvoiceInput, InvoiceLine, InvoiceLineInput, OpenTotal, Party, Payment as FinancePayment, PaymentDetail as FinancePaymentDetail, PaymentInput as FinancePaymentInput, } from "./sqlite/finance.js";
41
42
  export { IMPORTED_DOCUMENT_QUERY, type ImportedDocuments } from "./sqlite/imported-documents.js";
42
43
  export type { Involvement, InvolvementStore } from "./sqlite/involvements.js";
43
44
  export type { Annotation, KnowledgeRelation, KnowledgeStore, KnowledgeTarget, LabelledType, Organization, Project, Reminder, } from "./sqlite/knowledge.js";
@@ -4,7 +4,7 @@ export declare const projectAnnotationRequest: (request: ProjectAnnotationTarget
4
4
  format: "wirecat.project-annotations";
5
5
  facts: {
6
6
  ref: string;
7
- type: "decision" | "fact" | "payment" | "promise" | "question" | "statement";
7
+ type: "decision" | "fact" | "promise" | "question" | "statement";
8
8
  statement: string;
9
9
  status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
10
10
  subject: string | null;
@@ -23,7 +23,7 @@ export declare const projectAnnotationsSchema: v.StrictObjectSchema<{
23
23
  readonly aliases: v.SchemaWithPipe<readonly [v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>, v.MaxLengthAction<string[], 1000, undefined>]>;
24
24
  readonly facts: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
25
25
  readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
26
- readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise", "payment"], undefined>;
26
+ readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
27
27
  readonly statement: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
28
28
  readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed", "rejected"], undefined>;
29
29
  readonly subject: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
@@ -38,7 +38,7 @@ export declare const projectAnnotationsSchema: v.StrictObjectSchema<{
38
38
  readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
39
39
  }, undefined>, undefined>, v.MaxLengthAction<{
40
40
  ref: string;
41
- type: "decision" | "fact" | "payment" | "promise" | "question" | "statement";
41
+ type: "decision" | "fact" | "promise" | "question" | "statement";
42
42
  statement: string;
43
43
  status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
44
44
  subject: string | null;
@@ -4,7 +4,7 @@ export declare const projectAnnotationSnapshotSchema: v.StrictObjectSchema<{
4
4
  readonly format: v.LiteralSchema<"wirecat.project-annotations", undefined>;
5
5
  readonly facts: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
6
6
  readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
7
- readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise", "payment"], undefined>;
7
+ readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
8
8
  readonly statement: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
9
9
  readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed", "rejected"], undefined>;
10
10
  readonly subject: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
@@ -19,7 +19,7 @@ export declare const projectAnnotationSnapshotSchema: v.StrictObjectSchema<{
19
19
  readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
20
20
  }, undefined>, undefined>, v.MaxLengthAction<{
21
21
  ref: string;
22
- type: "decision" | "fact" | "payment" | "promise" | "question" | "statement";
22
+ type: "decision" | "fact" | "promise" | "question" | "statement";
23
23
  statement: string;
24
24
  status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
25
25
  subject: string | null;