@wirecat/cli-core 0.557.0 → 0.559.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 (55) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/cli/index.d.ts +0 -1
  3. package/dist/cli/index.js +0 -1
  4. package/dist/domain/records.d.ts +161 -0
  5. package/dist/domain/records.js +234 -0
  6. package/dist/entities/fact.d.ts +1 -12
  7. package/dist/entities/fact.js +0 -13
  8. package/dist/entities/index.d.ts +2 -12
  9. package/dist/entities/meeting-findings.d.ts +1 -1
  10. package/dist/errors/reasons.js +10 -0
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/meetings/findings-rules.d.ts +7 -4
  14. package/dist/meetings/findings-rules.js +11 -4
  15. package/dist/meetings/payment.d.ts +10 -6
  16. package/dist/meetings/payment.js +21 -6
  17. package/dist/runs/secrets.d.ts +2 -0
  18. package/dist/runs/secrets.js +2 -0
  19. package/dist/services/index.d.ts +1 -0
  20. package/dist/services/index.js +1 -0
  21. package/dist/services/ingest.d.ts +45 -0
  22. package/dist/services/ingest.js +80 -0
  23. package/dist/services/project-status/index.d.ts +1 -1
  24. package/dist/services/project-status/index.js +7 -3
  25. package/dist/services/project-status/signals/payments.d.ts +3 -3
  26. package/dist/services/project-status/signals/payments.js +43 -17
  27. package/dist/store/evidence-findings.js +16 -8
  28. package/dist/store/index.d.ts +2 -1
  29. package/dist/store/project-transfer/request.d.ts +1 -1
  30. package/dist/store/project-transfer/schema.d.ts +2 -2
  31. package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
  32. package/dist/store/sqlite/accounts.js +4 -0
  33. package/dist/store/sqlite/facts.d.ts +0 -13
  34. package/dist/store/sqlite/facts.js +5 -58
  35. package/dist/store/sqlite/finance.d.ts +4 -0
  36. package/dist/store/sqlite/finance.js +19 -4
  37. package/dist/store/sqlite/findings-core.js +5 -14
  38. package/dist/store/sqlite/manifest.js +2 -0
  39. package/dist/store/sqlite/migrations.generated.js +18 -0
  40. package/dist/store/sqlite/open-store.js +2 -0
  41. package/dist/store/sqlite/payment-entries.d.ts +26 -0
  42. package/dist/store/sqlite/payment-entries.js +91 -0
  43. package/dist/store/sqlite/payment-facts.d.ts +12 -0
  44. package/dist/store/sqlite/payment-facts.js +92 -0
  45. package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
  46. package/dist/store/sqlite/records.d.ts +81 -0
  47. package/dist/store/sqlite/records.js +372 -0
  48. package/dist/store/sqlite/schema.d.ts +353 -0
  49. package/dist/store/sqlite/schema.js +37 -0
  50. package/dist/store/store.d.ts +3 -1
  51. package/dist/store/store.js +5 -0
  52. package/package.json +4 -2
  53. package/schemas/records-envelope.v1.schema.json +108 -0
  54. package/dist/cli/payment-query.d.ts +0 -8
  55. package/dist/cli/payment-query.js +0 -27
@@ -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
  }
@@ -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)
@@ -1,4 +1,6 @@
1
1
  export declare const secretNamed: (name: string) => boolean;
2
+ /** A credential in a shape vendors publish so scanners find it: a known token prefix, a private key, a `Bearer` value. */
3
+ export declare const holdsCredential: (text: string) => boolean;
2
4
  /**
3
5
  * Whether a run of token characters is a random string rather than words: long, mixed case, high Shannon entropy,
4
6
  * cut into short pieces (`Kz9vQ2mX`) where a name is cut into words (`InlineQueryResultMpeg4Gif`).
@@ -54,6 +54,8 @@ const PRIVATE_KEY = /-----BEGIN[A-Z ]* PRIVATE KEY-----[\s\S]*?(?:-----END[A-Z ]
54
54
  const SCHEME = /(?<![A-Za-z])(Bearer|Basic)(\s+)(?![A-Z]?[a-z]+(?![A-Za-z0-9]))[A-Za-z0-9._~+/=-]{8,}/g;
55
55
  // After a bare space only a value that is not a plain word: `pin 1234` is a secret, `pin this message` is not.
56
56
  const NAMED_PAIR = /(?<![\p{L}\p{N}])([\p{L}\p{N}_.-]*(?:token|pass|pwd|pin|otp|2fa|secret|credential|cookie|authorization|key|contrase|clave|пароль|пин|код)[\p{L}\p{N}_.-]*)(["']?\s*(?:=>|[:=])\s*["']?|\s+(?![\p{L}]+(?![\p{L}\p{N}])))((?:Bearer\s+|Basic\s+)?)([^\s"'&,;]+)/giu;
57
+ /** A credential in a shape vendors publish so scanners find it: a known token prefix, a private key, a `Bearer` value. */
58
+ export const holdsCredential = (text) => text.search(PRIVATE_KEY) >= 0 || text.search(KNOWN_TOKENS) >= 0 || text.search(SCHEME) >= 0;
57
59
  /**
58
60
  * Whether a run of token characters is a random string rather than words: long, mixed case, high Shannon entropy,
59
61
  * cut into short pieces (`Kz9vQ2mX`) where a name is cut into words (`InlineQueryResultMpeg4Gif`).
@@ -6,6 +6,7 @@ export { type ChatCoverage, chatCoverage } from "./chat-coverage.js";
6
6
  export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextEvidenceSender, ContextIndex, ContextReadLimits, ContextReadUsage, ContextRecordReader, ContextSource, ScopedContextEvidenceReader, } from "./context-evidence.js";
7
7
  export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
8
8
  export { excerpt } from "./excerpt.js";
9
+ export { type IngestOptions, type IngestReceipt, ingest } from "./ingest.js";
9
10
  export { aliasHits, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, type LinkCandidate, type LinkScorer, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, type ScanInput as LinkScanInput, type ScannedProject, type ScannedProposal, type ScanResult as LinkScanResult, type ScorerInput, type Signal as LinkSignal, scanProjectLinks, } from "./link-scorers/index.js";
10
11
  export type { MediaArchiveOptions, MediaArchivePorts } from "./media-archive.js";
11
12
  export { archiveMedia, checkMediaArchive } from "./media-archive.js";
@@ -5,6 +5,7 @@ export { AGENDA_DEFAULTS } from "./agenda.js";
5
5
  export { chatCoverage } from "./chat-coverage.js";
6
6
  export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
7
7
  export { excerpt } from "./excerpt.js";
8
+ export { ingest } from "./ingest.js";
8
9
  export { aliasHits, chatSenders, chatTitle, combinedScore, DEFAULT_LIMIT as DEFAULT_LINK_SCAN_LIMIT, DEFAULT_THRESHOLD as DEFAULT_LINK_SCAN_THRESHOLD, emailDomain, mailThread, memberChats, memberDialog, PERSON_SCORERS, PERSON_THRESHOLD as PERSON_LINK_SCAN_THRESHOLD, SCAN_BOT, SCORERS, scanProjectLinks, } from "./link-scorers/index.js";
9
10
  export { archiveMedia, checkMediaArchive } from "./media-archive.js";
10
11
  export { moveMediaArchive } from "./media-archive-move.js";
@@ -0,0 +1,45 @@
1
+ import { type IngestError } from "../domain/records.js";
2
+ import type { MessageStore } from "../store/store.js";
3
+ export interface IngestOptions {
4
+ /** The account the caller means; the envelope's `source.account` must be this one. */
5
+ account: string;
6
+ /** Checks and plans everything, then writes nothing. */
7
+ dryRun?: boolean;
8
+ /** Records per transaction, 1000 by default. */
9
+ chunk?: number;
10
+ }
11
+ export interface IngestReceipt {
12
+ dryRun: boolean;
13
+ source: {
14
+ provider: string;
15
+ account: string;
16
+ batchId: string;
17
+ } | null;
18
+ created: number;
19
+ updated: number;
20
+ unchanged: number;
21
+ deleted: number;
22
+ refused: number;
23
+ /** `invoice:` and `payment:` proposals this import made; the owner confirms them in review. */
24
+ proposed: string[];
25
+ /** Kept, with no entity yet: each named role points at a record not imported, or one with no entity. */
26
+ waiting: {
27
+ key: string;
28
+ pointer: string;
29
+ missing: string[];
30
+ }[];
31
+ /** Changed or deleted at the source after the entity was made from it: the entity was not touched. */
32
+ flagged: {
33
+ key: string;
34
+ pointer: string;
35
+ entity: string;
36
+ }[];
37
+ errors: IngestError[];
38
+ }
39
+ /**
40
+ * Takes a v1 records envelope (or `envelopeOfJsonl` of a JSONL file) into the store: checks it, then writes per
41
+ * chunk in one transaction. The same import twice writes nothing new. A record with a lower revision than the stored
42
+ * one is refused, a delete is soft, a reference to a record not yet imported waits for it, and nothing is linked by
43
+ * name. An invoice or a payment becomes a proposal: an import never writes confirmed money.
44
+ */
45
+ export declare const ingest: (store: Pick<MessageStore, "records">, envelope: unknown, { account, dryRun, chunk }: IngestOptions) => Promise<IngestReceipt>;
@@ -0,0 +1,80 @@
1
+ import { envelopeErrors, pointerOf, recordErrors, } from "../domain/records.js";
2
+ const emptyReceipt = (dryRun) => ({
3
+ dryRun,
4
+ source: null,
5
+ created: 0,
6
+ updated: 0,
7
+ unchanged: 0,
8
+ deleted: 0,
9
+ refused: 0,
10
+ proposed: [],
11
+ waiting: [],
12
+ flagged: [],
13
+ errors: [],
14
+ });
15
+ /**
16
+ * Takes a v1 records envelope (or `envelopeOfJsonl` of a JSONL file) into the store: checks it, then writes per
17
+ * chunk in one transaction. The same import twice writes nothing new. A record with a lower revision than the stored
18
+ * one is refused, a delete is soft, a reference to a record not yet imported waits for it, and nothing is linked by
19
+ * name. An invoice or a payment becomes a proposal: an import never writes confirmed money.
20
+ */
21
+ export const ingest = async (store, envelope, { account, dryRun = false, chunk = 1000 }) => {
22
+ const receipt = emptyReceipt(dryRun);
23
+ const broken = envelopeErrors(envelope, account);
24
+ if (broken.length > 0)
25
+ return { ...receipt, errors: broken };
26
+ const { source, records } = envelope;
27
+ receipt.source = { provider: source.provider, account: source.account, batchId: source.batch_id };
28
+ const taken = [];
29
+ const keys = new Set();
30
+ const identities = new Set();
31
+ for (const [index, record] of records.entries()) {
32
+ const errors = recordErrors(record, index);
33
+ const valid = record;
34
+ if (errors.length === 0 && keys.has(valid.key))
35
+ errors.push({
36
+ pointer: pointerOf("records", index, "key"),
37
+ code: "record.duplicate-key",
38
+ message: "another record of this envelope has this key",
39
+ key: valid.key,
40
+ });
41
+ const identity = errors.length === 0 ? `${valid.type}\0${valid.external_id}` : "";
42
+ if (errors.length === 0 && identities.has(identity))
43
+ errors.push({
44
+ pointer: pointerOf("records", index, "external_id"),
45
+ code: "record.duplicate",
46
+ message: `another record of this envelope is ${valid.type} ${valid.external_id}`,
47
+ key: valid.key,
48
+ });
49
+ if (errors.length > 0) {
50
+ receipt.refused += 1;
51
+ for (const error of errors)
52
+ receipt.errors.push(error);
53
+ continue;
54
+ }
55
+ keys.add(valid.key);
56
+ identities.add(identity);
57
+ taken.push({ index, record: valid });
58
+ }
59
+ const written = await store.records.write(source, taken, { dryRun, chunk });
60
+ for (const outcome of written.outcomes) {
61
+ receipt[outcome.status] += 1;
62
+ if (outcome.error)
63
+ receipt.errors.push(outcome.error);
64
+ }
65
+ for (const error of written.laterErrors)
66
+ receipt.errors.push(error);
67
+ receipt.errors.sort((a, b) => a.pointer.localeCompare(b.pointer, "en", { numeric: true }));
68
+ receipt.proposed = written.proposed;
69
+ receipt.waiting = written.waiting.map(({ index, key, missing }) => ({
70
+ key,
71
+ pointer: pointerOf("records", index, "relations"),
72
+ missing,
73
+ }));
74
+ receipt.flagged = written.flagged.map(({ index, key, entity }) => ({
75
+ key,
76
+ pointer: pointerOf("records", index),
77
+ entity,
78
+ }));
79
+ return receipt;
80
+ };
@@ -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
  };
@@ -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,7 +36,7 @@ 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
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";
42
42
  export { IMPORTED_DOCUMENT_QUERY, type ImportedDocuments } from "./sqlite/imported-documents.js";
@@ -54,6 +54,7 @@ export { type NearestNote, NOTE_FIELDS, type NoteChunkToEmbed, type NoteHit, typ
54
54
  export type { FileNoteInput, Link, LinkInput, LinkListFilter, Note, NoteFolder, NotesStore, NoteTag, } from "./sqlite/notes.js";
55
55
  export type { PrivateContact, PrivateContactNote } from "./sqlite/private-people.js";
56
56
  export { PROPOSAL_STATUSES, type ProposalInput, type ProposedAction, type ProposedActionsStore, proposerOfAction, } from "./sqlite/proposed-actions.js";
57
+ export type { RecordKey, RecordOutcome, RecordStore, RecordsWritten, SourceRecord, } from "./sqlite/records.js";
57
58
  export type { LinkDecision, RelationUpdate } from "./sqlite/relation-proposals.js";
58
59
  export { fillSearchIndex, resetSearchIndex, type SearchIndexFill, type SearchIndexState, searchIndexState, } from "./sqlite/search-index.js";
59
60
  export type { SyncStateSave } from "./sqlite/sync.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;
@@ -114,6 +114,10 @@ export const purgeAccount = ({ orm, database }, accountKey) => {
114
114
  // The inbox project and its tasks are the owner's; only the tie to the account goes.
115
115
  orm.update(projects).set({ accountId: null }).where(eq(projects.accountId, accountKey)).run();
116
116
  orm.run(sql `DELETE FROM files WHERE account_id = ${accountKey}`);
117
+ // An entity made from a record is the owner's and stays; only what the source said goes.
118
+ const recordsOf = sql `(SELECT id FROM records WHERE account_id = ${accountKey})`;
119
+ orm.run(sql `DELETE FROM record_relations WHERE record_id IN ${recordsOf} OR target_id IN ${recordsOf}`);
120
+ orm.run(sql `DELETE FROM records WHERE account_id = ${accountKey}`);
117
121
  orm.delete(accounts).where(eq(accounts.id, accountKey)).run();
118
122
  dropOrphanPeople(database, unseenBefore);
119
123
  dropUnusedVectors(database, hashes);
@@ -1,5 +1,4 @@
1
1
  import { type FACT_STATUSES, FACT_TYPES, MEMORY_SCOPES } from "../../domain/knowledge.js";
2
- import { type PAYMENT_DIRECTIONS, type PAYMENT_STATES, type Payment } from "../../meetings/index.js";
3
2
  import { type Decider } from "../decisions.js";
4
3
  import { type Actor, type ProposedByFilter } from "./actors.js";
5
4
  import { type EvidenceSource } from "./evidence-sources.js";
@@ -35,8 +34,6 @@ export interface Fact {
35
34
  scope: (typeof MEMORY_SCOPES)[number] | null;
36
35
  source: string;
37
36
  evidence: string[];
38
- /** Set on a `payment` fact only. */
39
- payment: Payment | null;
40
37
  createdAt: string;
41
38
  updatedAt: string;
42
39
  }
@@ -55,14 +52,6 @@ export interface FactInput {
55
52
  supersedes?: string;
56
53
  /** The memory an agent drew it from: a `created-from` link. */
57
54
  createdFrom?: string;
58
- /** Required on a `payment` fact, refused on any other; checked as `validatePayment` does. */
59
- payment?: Payment;
60
- }
61
- /** `open` is an expected payment that still holds: not superseded, rejected or cancelled. */
62
- export interface PaymentFilter {
63
- direction?: (typeof PAYMENT_DIRECTIONS)[number];
64
- state?: (typeof PAYMENT_STATES)[number];
65
- open?: boolean;
66
55
  }
67
56
  export interface FactsStore {
68
57
  add(input: FactInput): Promise<Fact>;
@@ -73,8 +62,6 @@ export interface FactsStore {
73
62
  subject?: string;
74
63
  status?: Fact["status"];
75
64
  proposedBy?: ProposedByFilter;
76
- /** Narrows to payment facts by their fields. */
77
- payment?: PaymentFilter;
78
65
  /** `created` lists the newest written first; by default the newest by when it happened. */
79
66
  order?: "occurred" | "created";
80
67
  limit?: number;