@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.
- package/CHANGELOG.md +33 -0
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -1
- package/dist/domain/records.d.ts +161 -0
- package/dist/domain/records.js +234 -0
- package/dist/entities/fact.d.ts +1 -12
- package/dist/entities/fact.js +0 -13
- package/dist/entities/index.d.ts +2 -12
- package/dist/entities/meeting-findings.d.ts +1 -1
- package/dist/errors/reasons.js +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/meetings/findings-rules.d.ts +7 -4
- package/dist/meetings/findings-rules.js +11 -4
- package/dist/meetings/payment.d.ts +10 -6
- package/dist/meetings/payment.js +21 -6
- package/dist/runs/secrets.d.ts +2 -0
- package/dist/runs/secrets.js +2 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/ingest.d.ts +45 -0
- package/dist/services/ingest.js +80 -0
- package/dist/services/project-status/index.d.ts +1 -1
- package/dist/services/project-status/index.js +7 -3
- package/dist/services/project-status/signals/payments.d.ts +3 -3
- package/dist/services/project-status/signals/payments.js +43 -17
- package/dist/store/evidence-findings.js +16 -8
- package/dist/store/index.d.ts +2 -1
- package/dist/store/project-transfer/request.d.ts +1 -1
- package/dist/store/project-transfer/schema.d.ts +2 -2
- package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
- package/dist/store/sqlite/accounts.js +4 -0
- package/dist/store/sqlite/facts.d.ts +0 -13
- package/dist/store/sqlite/facts.js +5 -58
- package/dist/store/sqlite/finance.d.ts +4 -0
- package/dist/store/sqlite/finance.js +19 -4
- package/dist/store/sqlite/findings-core.js +5 -14
- package/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/migrations.generated.js +18 -0
- package/dist/store/sqlite/open-store.js +2 -0
- package/dist/store/sqlite/payment-entries.d.ts +26 -0
- package/dist/store/sqlite/payment-entries.js +91 -0
- package/dist/store/sqlite/payment-facts.d.ts +12 -0
- package/dist/store/sqlite/payment-facts.js +92 -0
- package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
- package/dist/store/sqlite/records.d.ts +81 -0
- package/dist/store/sqlite/records.js +372 -0
- package/dist/store/sqlite/schema.d.ts +353 -0
- package/dist/store/sqlite/schema.js +37 -0
- package/dist/store/store.d.ts +3 -1
- package/dist/store/store.js +5 -0
- package/package.json +4 -2
- package/schemas/records-envelope.v1.schema.json +108 -0
- package/dist/cli/payment-query.d.ts +0 -8
- package/dist/cli/payment-query.js +0 -27
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,39 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.559.0 — 02.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Imports through one envelope (store version 47).** `ingest(store, envelope, { account, dryRun })` in
|
|
15
|
+
`./services` takes the v1 records envelope (`schema_version`, `source { provider, account, batch_id }`, `records[]`
|
|
16
|
+
with `key`, `type`, `external_id`, `operation` `upsert` or `delete`, `fields`, `revision`, `relations[]`), or a JSONL
|
|
17
|
+
file through `envelopeOfJsonl`. Its JSON Schema (Draft 2020-12) is `RECORDS_ENVELOPE_SCHEMA` and
|
|
18
|
+
`@wirecat/cli-core/schemas/records-envelope.v1.schema.json`. Each record is kept in the new `records` table, unique
|
|
19
|
+
per account, type and external id: the same import twice writes nothing, a lower revision is refused, a delete is
|
|
20
|
+
soft, and a field that looks like a secret is refused. The receipt counts created, updated, unchanged, deleted and
|
|
21
|
+
refused records, and each error carries a JSON pointer and a stable code.
|
|
22
|
+
- **An import never writes confirmed money.** An `invoice` or `payment` record becomes a proposal by the bot `import`,
|
|
23
|
+
decided in the proposal inbox. A reference to a record not yet imported waits for it, and a person or organization
|
|
24
|
+
record is linked only by the owner (`store.records.map`), never by name. `store.records` lists and reads records,
|
|
25
|
+
waiting and flagged ones too.
|
|
26
|
+
|
|
27
|
+
## 0.558.0 — 02.11.2026
|
|
28
|
+
|
|
29
|
+
### Changed — may break callers
|
|
30
|
+
|
|
31
|
+
- **A payment is no longer a fact (store version 46).** Money lives only in `store.finance`. `facts.add` refuses type
|
|
32
|
+
`payment` with reason `fact.payment-retired`, naming `store.finance.addPayment`, and a trigger refuses one from an
|
|
33
|
+
older build. `FACT_TYPES`, the `fact` entity and `Fact` lose `payment`; `PaymentFilter`, `facts.list({ payment })`,
|
|
34
|
+
`PAYMENT_QUERY_FIELDS` and `paymentFilterOf` are gone. The first writable open moves stored payment facts into rows.
|
|
35
|
+
- **An accepted payment finding writes a finance row, not a fact.** A paid one is a `payment:` row, an expected one
|
|
36
|
+
an `invoice:` of one line, proposed by the agent and confirmed by the owner's acceptance. `supersedes` now names
|
|
37
|
+
the `invoice:` a paid one settles or a cancelled one voids, not a `fact:`. An expected one needs a `person:` or
|
|
38
|
+
`organization:` counterparty, and the amount must fit the currency's decimals. New optional field `paid_at`.
|
|
39
|
+
- **`projects status` reads money from `store.finance`.** The `payments` signal (version 3) lists payments and open
|
|
40
|
+
invoices by their `payment:` and `invoice:` refs, `status` being `proposed` or `confirmed`; the open total counts
|
|
41
|
+
confirmed invoices only. Finance rows gain `evidence`, and `addInvoice` and `addPayment` take it.
|
|
42
|
+
|
|
10
43
|
## 0.557.0 — 02.11.2026
|
|
11
44
|
|
|
12
45
|
### Added
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -111,7 +111,6 @@ export { type Saving, stored } from "./messenger/stored.js";
|
|
|
111
111
|
export { dateOrNone } from "./messenger/tasks-command.js";
|
|
112
112
|
export { MODEL_CATALOGUE_QUERY, type ModelCatalogueOptions } from "./model-catalogue-command.js";
|
|
113
113
|
export { positiveCount, window, withPaging } from "./paging.js";
|
|
114
|
-
export { PAYMENT_QUERY_FIELDS, paymentFilterOf } from "./payment-query.js";
|
|
115
114
|
export { type CommandPermissions, commandPathKey, commandPermissions, knownBeside, knownPermissionKeys, type PermissionKeyOf, type PermissionKeySource, refuseUnknownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
|
|
116
115
|
export { asFirstWord, commandWords, liftProfile, profileFree, refuseCommandName, rootOf, takesProfile, usableProfileName, } from "./profile.js";
|
|
117
116
|
export { createProgram, type ProgramDefinition, type ProgramOptions, type RunOptions, run } from "./program.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -101,7 +101,6 @@ export { stored } from "./messenger/stored.js";
|
|
|
101
101
|
export { dateOrNone } from "./messenger/tasks-command.js";
|
|
102
102
|
export { MODEL_CATALOGUE_QUERY } from "./model-catalogue-command.js";
|
|
103
103
|
export { positiveCount, window, withPaging } from "./paging.js";
|
|
104
|
-
export { PAYMENT_QUERY_FIELDS, paymentFilterOf } from "./payment-query.js";
|
|
105
104
|
export { commandPathKey, commandPermissions, knownBeside, knownPermissionKeys, refuseUnknownPermissionKeys, unknownPermissionIssues, unknownPermissionKeys, } from "./permission-keys.js";
|
|
106
105
|
export { asFirstWord, commandWords, liftProfile, profileFree, refuseCommandName, rootOf, takesProfile, usableProfileName, } from "./profile.js";
|
|
107
106
|
export { createProgram, run } from "./program.js";
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
export declare const RECORDS_SCHEMA_VERSION = 1;
|
|
2
|
+
export declare const RECORD_OPERATIONS: readonly ["upsert", "delete"];
|
|
3
|
+
/** Types with a table of their own; anything else a source sends is `custom:<name>` and stays a record only. */
|
|
4
|
+
export declare const RECORD_ENTITY_TYPES: readonly ["invoice", "payment", "person", "organization", "project"];
|
|
5
|
+
export type RecordOperation = (typeof RECORD_OPERATIONS)[number];
|
|
6
|
+
export type RecordEntityType = (typeof RECORD_ENTITY_TYPES)[number];
|
|
7
|
+
export interface RecordRelation {
|
|
8
|
+
/** What the target is to this record: `counterparty`, `payer`, `settles`, or a source's own word. */
|
|
9
|
+
role: string;
|
|
10
|
+
type: string;
|
|
11
|
+
external_id: string;
|
|
12
|
+
}
|
|
13
|
+
export interface EnvelopeRecord {
|
|
14
|
+
/** The caller's name for this line, unique in the envelope; the receipt cites it. */
|
|
15
|
+
key: string;
|
|
16
|
+
type: string;
|
|
17
|
+
external_id: string;
|
|
18
|
+
operation: RecordOperation;
|
|
19
|
+
fields?: Record<string, unknown>;
|
|
20
|
+
/** Goes up with each change at the source; a lower one than the store holds is refused. */
|
|
21
|
+
revision?: number;
|
|
22
|
+
relations?: RecordRelation[];
|
|
23
|
+
}
|
|
24
|
+
export interface EnvelopeSource {
|
|
25
|
+
provider: string;
|
|
26
|
+
account: string;
|
|
27
|
+
batch_id: string;
|
|
28
|
+
}
|
|
29
|
+
export interface RecordsEnvelope {
|
|
30
|
+
schema_version: 1;
|
|
31
|
+
source: EnvelopeSource;
|
|
32
|
+
records: EnvelopeRecord[];
|
|
33
|
+
}
|
|
34
|
+
/** A refusal: `pointer` is an RFC 6901 JSON pointer into the envelope, `code` a stable word callers may branch on. */
|
|
35
|
+
export interface IngestError {
|
|
36
|
+
pointer: string;
|
|
37
|
+
code: string;
|
|
38
|
+
message: string;
|
|
39
|
+
key?: string;
|
|
40
|
+
}
|
|
41
|
+
/** The v1 envelope as JSON Schema (Draft 2020-12); `schemas/records-envelope.v1.schema.json` is this, written out. */
|
|
42
|
+
export declare const RECORDS_ENVELOPE_SCHEMA: {
|
|
43
|
+
readonly $schema: "https://json-schema.org/draft/2020-12/schema";
|
|
44
|
+
readonly title: "WireCat records envelope, version 1";
|
|
45
|
+
readonly description: string;
|
|
46
|
+
readonly type: "object";
|
|
47
|
+
readonly required: readonly ["schema_version", "source", "records"];
|
|
48
|
+
readonly additionalProperties: false;
|
|
49
|
+
readonly properties: {
|
|
50
|
+
readonly schema_version: {
|
|
51
|
+
readonly const: 1;
|
|
52
|
+
};
|
|
53
|
+
readonly source: {
|
|
54
|
+
readonly $ref: "#/$defs/source";
|
|
55
|
+
};
|
|
56
|
+
readonly records: {
|
|
57
|
+
readonly type: "array";
|
|
58
|
+
readonly items: {
|
|
59
|
+
readonly $ref: "#/$defs/record";
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
readonly $defs: {
|
|
64
|
+
readonly source: {
|
|
65
|
+
readonly type: "object";
|
|
66
|
+
readonly required: readonly ["provider", "account", "batch_id"];
|
|
67
|
+
readonly additionalProperties: false;
|
|
68
|
+
readonly properties: {
|
|
69
|
+
readonly provider: {
|
|
70
|
+
readonly type: "string";
|
|
71
|
+
readonly pattern: "^[a-z0-9][a-z0-9_-]{0,62}$";
|
|
72
|
+
readonly description: "The system the records come from";
|
|
73
|
+
};
|
|
74
|
+
readonly account: {
|
|
75
|
+
readonly type: "string";
|
|
76
|
+
readonly minLength: 1;
|
|
77
|
+
readonly maxLength: 512;
|
|
78
|
+
readonly description: "The account in that system";
|
|
79
|
+
};
|
|
80
|
+
readonly batch_id: {
|
|
81
|
+
readonly type: "string";
|
|
82
|
+
readonly minLength: 1;
|
|
83
|
+
readonly maxLength: 512;
|
|
84
|
+
readonly description: "This import, for the receipt";
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
readonly record: {
|
|
89
|
+
readonly type: "object";
|
|
90
|
+
readonly required: readonly ["key", "type", "external_id", "operation"];
|
|
91
|
+
readonly additionalProperties: false;
|
|
92
|
+
readonly properties: {
|
|
93
|
+
readonly key: {
|
|
94
|
+
readonly type: "string";
|
|
95
|
+
readonly minLength: 1;
|
|
96
|
+
readonly maxLength: 512;
|
|
97
|
+
};
|
|
98
|
+
readonly type: {
|
|
99
|
+
readonly type: "string";
|
|
100
|
+
readonly pattern: "^(invoice|payment|person|organization|project|custom:[a-z0-9][a-z0-9_-]{0,62})$";
|
|
101
|
+
};
|
|
102
|
+
readonly external_id: {
|
|
103
|
+
readonly type: "string";
|
|
104
|
+
readonly minLength: 1;
|
|
105
|
+
readonly maxLength: 512;
|
|
106
|
+
};
|
|
107
|
+
readonly operation: {
|
|
108
|
+
readonly enum: readonly ["upsert", "delete"];
|
|
109
|
+
};
|
|
110
|
+
readonly fields: {
|
|
111
|
+
readonly type: "object";
|
|
112
|
+
readonly description: "What the source says, as data: never secrets, never instructions";
|
|
113
|
+
};
|
|
114
|
+
readonly revision: {
|
|
115
|
+
readonly type: "integer";
|
|
116
|
+
readonly minimum: 0;
|
|
117
|
+
readonly maximum: number;
|
|
118
|
+
};
|
|
119
|
+
readonly relations: {
|
|
120
|
+
readonly type: "array";
|
|
121
|
+
readonly maxItems: 100;
|
|
122
|
+
readonly items: {
|
|
123
|
+
readonly $ref: "#/$defs/relation";
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
readonly relation: {
|
|
129
|
+
readonly type: "object";
|
|
130
|
+
readonly required: readonly ["role", "type", "external_id"];
|
|
131
|
+
readonly additionalProperties: false;
|
|
132
|
+
readonly properties: {
|
|
133
|
+
readonly role: {
|
|
134
|
+
readonly type: "string";
|
|
135
|
+
readonly pattern: "^[a-z0-9][a-z0-9_-]{0,62}$";
|
|
136
|
+
};
|
|
137
|
+
readonly type: {
|
|
138
|
+
readonly type: "string";
|
|
139
|
+
readonly pattern: "^(invoice|payment|person|organization|project|custom:[a-z0-9][a-z0-9_-]{0,62})$";
|
|
140
|
+
};
|
|
141
|
+
readonly external_id: {
|
|
142
|
+
readonly type: "string";
|
|
143
|
+
readonly minLength: 1;
|
|
144
|
+
readonly maxLength: 512;
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
export declare const pointerOf: (...segments: (string | number)[]) => string;
|
|
151
|
+
/** Why one record cannot be taken, or nothing; `index` is its place in `records`. */
|
|
152
|
+
export declare const recordErrors: (record: unknown, index: number) => IngestError[];
|
|
153
|
+
/** The envelope's own errors (not its records'): when there is one, no record is taken. */
|
|
154
|
+
export declare const envelopeErrors: (envelope: unknown, account: string) => IngestError[];
|
|
155
|
+
/** The same fields and relations in any key or relation order give the same fingerprint. */
|
|
156
|
+
export declare const recordFingerprint: (fields: Record<string, unknown>, relations: RecordRelation[]) => string;
|
|
157
|
+
/**
|
|
158
|
+
* A JSONL import as the envelope it stands for: the first line holds `schema_version` and `source`, each further
|
|
159
|
+
* non-empty line one record. A line that is not JSON comes back as a record the envelope check refuses, at its index.
|
|
160
|
+
*/
|
|
161
|
+
export declare const envelopeOfJsonl: (text: string) => unknown;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { holdsCredential, secretNamed } from "../runs/secrets.js";
|
|
3
|
+
export const RECORDS_SCHEMA_VERSION = 1;
|
|
4
|
+
export const RECORD_OPERATIONS = ["upsert", "delete"];
|
|
5
|
+
/** Types with a table of their own; anything else a source sends is `custom:<name>` and stays a record only. */
|
|
6
|
+
export const RECORD_ENTITY_TYPES = ["invoice", "payment", "person", "organization", "project"];
|
|
7
|
+
const WORD = "^[a-z0-9][a-z0-9_-]{0,62}$";
|
|
8
|
+
const TYPE = "^(invoice|payment|person|organization|project|custom:[a-z0-9][a-z0-9_-]{0,62})$";
|
|
9
|
+
const MAX_ID = 512;
|
|
10
|
+
const MAX_FIELDS_BYTES = 65_536;
|
|
11
|
+
const MAX_RELATIONS = 100;
|
|
12
|
+
/** The v1 envelope as JSON Schema (Draft 2020-12); `schemas/records-envelope.v1.schema.json` is this, written out. */
|
|
13
|
+
export const RECORDS_ENVELOPE_SCHEMA = {
|
|
14
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
15
|
+
title: "WireCat records envelope, version 1",
|
|
16
|
+
description: "Records one source sends to the store. JSONL is the same model: the first line holds schema_version and " +
|
|
17
|
+
"source, each further line one record.",
|
|
18
|
+
type: "object",
|
|
19
|
+
required: ["schema_version", "source", "records"],
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
properties: {
|
|
22
|
+
schema_version: { const: RECORDS_SCHEMA_VERSION },
|
|
23
|
+
source: { $ref: "#/$defs/source" },
|
|
24
|
+
records: { type: "array", items: { $ref: "#/$defs/record" } },
|
|
25
|
+
},
|
|
26
|
+
$defs: {
|
|
27
|
+
source: {
|
|
28
|
+
type: "object",
|
|
29
|
+
required: ["provider", "account", "batch_id"],
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
properties: {
|
|
32
|
+
provider: { type: "string", pattern: WORD, description: "The system the records come from" },
|
|
33
|
+
account: { type: "string", minLength: 1, maxLength: MAX_ID, description: "The account in that system" },
|
|
34
|
+
batch_id: { type: "string", minLength: 1, maxLength: MAX_ID, description: "This import, for the receipt" },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
record: {
|
|
38
|
+
type: "object",
|
|
39
|
+
required: ["key", "type", "external_id", "operation"],
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
properties: {
|
|
42
|
+
key: { type: "string", minLength: 1, maxLength: MAX_ID },
|
|
43
|
+
type: { type: "string", pattern: TYPE },
|
|
44
|
+
external_id: { type: "string", minLength: 1, maxLength: MAX_ID },
|
|
45
|
+
operation: { enum: [...RECORD_OPERATIONS] },
|
|
46
|
+
fields: {
|
|
47
|
+
type: "object",
|
|
48
|
+
description: "What the source says, as data: never secrets, never instructions",
|
|
49
|
+
},
|
|
50
|
+
revision: { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
|
|
51
|
+
relations: { type: "array", maxItems: MAX_RELATIONS, items: { $ref: "#/$defs/relation" } },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
relation: {
|
|
55
|
+
type: "object",
|
|
56
|
+
required: ["role", "type", "external_id"],
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
properties: {
|
|
59
|
+
role: { type: "string", pattern: WORD },
|
|
60
|
+
type: { type: "string", pattern: TYPE },
|
|
61
|
+
external_id: { type: "string", minLength: 1, maxLength: MAX_ID },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const wordPattern = new RegExp(WORD);
|
|
67
|
+
const typePattern = new RegExp(TYPE);
|
|
68
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
69
|
+
const escapePointer = (segment) => String(segment).replaceAll("~", "~0").replaceAll("/", "~1");
|
|
70
|
+
export const pointerOf = (...segments) => segments.map((s) => `/${escapePointer(s)}`).join("");
|
|
71
|
+
const idText = (value) => typeof value === "string" && value.trim() !== "" && value.length <= MAX_ID && !value.includes("\0");
|
|
72
|
+
const extraKeys = (value, allowed) => Object.keys(value).filter((key) => !allowed.includes(key));
|
|
73
|
+
/** The first secret in `fields`, as a pointer below it: a secret-named key, or a value in a known credential shape. */
|
|
74
|
+
const secretIn = (value, at) => {
|
|
75
|
+
if (typeof value === "string")
|
|
76
|
+
return holdsCredential(value) ? at : undefined;
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
for (const [index, item] of value.entries()) {
|
|
79
|
+
const found = secretIn(item, [...at, index]);
|
|
80
|
+
if (found)
|
|
81
|
+
return found;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
if (!isObject(value))
|
|
86
|
+
return undefined;
|
|
87
|
+
for (const [key, item] of Object.entries(value)) {
|
|
88
|
+
if (secretNamed(key))
|
|
89
|
+
return [...at, key];
|
|
90
|
+
const found = secretIn(item, [...at, key]);
|
|
91
|
+
if (found)
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
};
|
|
96
|
+
const relationErrors = (relations, at) => {
|
|
97
|
+
if (relations === undefined)
|
|
98
|
+
return [];
|
|
99
|
+
if (!Array.isArray(relations) || relations.length > MAX_RELATIONS)
|
|
100
|
+
return [invalid(at, `relations is a list of at most ${MAX_RELATIONS}`)];
|
|
101
|
+
return relations.flatMap((relation, index) => {
|
|
102
|
+
const here = [...at, index];
|
|
103
|
+
if (!isObject(relation))
|
|
104
|
+
return [invalid(here, "a relation is an object")];
|
|
105
|
+
const extra = extraKeys(relation, ["role", "type", "external_id"]);
|
|
106
|
+
if (extra.length > 0)
|
|
107
|
+
return [invalid([...here, extra[0]], "a relation holds role, type and external_id")];
|
|
108
|
+
if (typeof relation.role !== "string" || !wordPattern.test(relation.role))
|
|
109
|
+
return [invalid([...here, "role"], "a role is lowercase letters, digits, - and _")];
|
|
110
|
+
if (typeof relation.type !== "string" || !typePattern.test(relation.type))
|
|
111
|
+
return [invalid([...here, "type"], `a type is one of ${RECORD_ENTITY_TYPES.join(", ")} or custom:<name>`)];
|
|
112
|
+
if (!idText(relation.external_id))
|
|
113
|
+
return [invalid([...here, "external_id"], `an external id takes 1–${MAX_ID} characters`)];
|
|
114
|
+
return [];
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
const invalid = (at, message, code = "record.invalid") => ({
|
|
118
|
+
pointer: pointerOf(...at),
|
|
119
|
+
code,
|
|
120
|
+
message,
|
|
121
|
+
});
|
|
122
|
+
/** Why one record cannot be taken, or nothing; `index` is its place in `records`. */
|
|
123
|
+
export const recordErrors = (record, index) => {
|
|
124
|
+
const at = ["records", index];
|
|
125
|
+
if (!isObject(record))
|
|
126
|
+
return [invalid(at, "a record is an object")];
|
|
127
|
+
const key = typeof record.key === "string" ? record.key : undefined;
|
|
128
|
+
const withKey = (errors) => errors.map((error) => (key === undefined ? error : { ...error, key }));
|
|
129
|
+
const extra = extraKeys(record, ["key", "type", "external_id", "operation", "fields", "revision", "relations"]);
|
|
130
|
+
if (extra.length > 0)
|
|
131
|
+
return withKey([invalid([...at, extra[0]], `a record has no ${extra[0]}`)]);
|
|
132
|
+
if (!idText(record.key))
|
|
133
|
+
return [invalid([...at, "key"], `a key takes 1–${MAX_ID} characters`)];
|
|
134
|
+
if (typeof record.type !== "string" || !typePattern.test(record.type))
|
|
135
|
+
return withKey([
|
|
136
|
+
invalid([...at, "type"], `a type is one of ${RECORD_ENTITY_TYPES.join(", ")} or custom:<name>`, "record.type"),
|
|
137
|
+
]);
|
|
138
|
+
if (!idText(record.external_id))
|
|
139
|
+
return withKey([invalid([...at, "external_id"], `an external id takes 1–${MAX_ID} characters`)]);
|
|
140
|
+
if (!RECORD_OPERATIONS.includes(record.operation))
|
|
141
|
+
return withKey([invalid([...at, "operation"], "an operation is upsert or delete")]);
|
|
142
|
+
if (record.revision !== undefined &&
|
|
143
|
+
(typeof record.revision !== "number" || !Number.isSafeInteger(record.revision) || record.revision < 0))
|
|
144
|
+
return withKey([invalid([...at, "revision"], "a revision is a whole number, 0 or more")]);
|
|
145
|
+
if (record.operation === "upsert" && !isObject(record.fields))
|
|
146
|
+
return withKey([invalid([...at, "fields"], "an upsert carries its fields as an object")]);
|
|
147
|
+
if (record.fields !== undefined && !isObject(record.fields))
|
|
148
|
+
return withKey([invalid([...at, "fields"], "fields is an object")]);
|
|
149
|
+
if (record.fields !== undefined && Buffer.byteLength(JSON.stringify(record.fields)) > MAX_FIELDS_BYTES)
|
|
150
|
+
return withKey([invalid([...at, "fields"], `fields take at most ${MAX_FIELDS_BYTES} bytes`, "record.too-large")]);
|
|
151
|
+
const secret = record.fields === undefined ? undefined : secretIn(record.fields, [...at, "fields"]);
|
|
152
|
+
if (secret)
|
|
153
|
+
return withKey([
|
|
154
|
+
invalid(secret, "a secret never goes into the store: drop this field at the source", "record.secret-field"),
|
|
155
|
+
]);
|
|
156
|
+
return withKey(relationErrors(record.relations, [...at, "relations"]));
|
|
157
|
+
};
|
|
158
|
+
/** The envelope's own errors (not its records'): when there is one, no record is taken. */
|
|
159
|
+
export const envelopeErrors = (envelope, account) => {
|
|
160
|
+
if (!isObject(envelope))
|
|
161
|
+
return [invalid([], "an envelope is an object", "envelope.invalid")];
|
|
162
|
+
const extra = extraKeys(envelope, ["schema_version", "source", "records"]);
|
|
163
|
+
if (extra.length > 0)
|
|
164
|
+
return [invalid([extra[0]], `an envelope has no ${extra[0]}`, "envelope.invalid")];
|
|
165
|
+
if (envelope.schema_version !== RECORDS_SCHEMA_VERSION)
|
|
166
|
+
return [
|
|
167
|
+
invalid(["schema_version"], `this build reads schema_version ${RECORDS_SCHEMA_VERSION}, not ${JSON.stringify(envelope.schema_version)}`, "envelope.schema-version"),
|
|
168
|
+
];
|
|
169
|
+
const { source } = envelope;
|
|
170
|
+
if (!isObject(source))
|
|
171
|
+
return [invalid(["source"], "source is an object", "envelope.invalid")];
|
|
172
|
+
const sourceExtra = extraKeys(source, ["provider", "account", "batch_id"]);
|
|
173
|
+
if (sourceExtra.length > 0)
|
|
174
|
+
return [
|
|
175
|
+
invalid(["source", sourceExtra[0]], "source holds provider, account and batch_id", "envelope.invalid"),
|
|
176
|
+
];
|
|
177
|
+
if (typeof source.provider !== "string" || !wordPattern.test(source.provider))
|
|
178
|
+
return [invalid(["source", "provider"], "a provider is lowercase letters, digits, - and _", "envelope.invalid")];
|
|
179
|
+
if (!idText(source.account))
|
|
180
|
+
return [invalid(["source", "account"], `an account takes 1–${MAX_ID} characters`, "envelope.invalid")];
|
|
181
|
+
if (!idText(source.batch_id))
|
|
182
|
+
return [invalid(["source", "batch_id"], `a batch id takes 1–${MAX_ID} characters`, "envelope.invalid")];
|
|
183
|
+
if (source.account !== account)
|
|
184
|
+
return [
|
|
185
|
+
invalid(["source", "account"], `the envelope is for account ${JSON.stringify(source.account)}, the import for ${JSON.stringify(account)}`, "envelope.account-mismatch"),
|
|
186
|
+
];
|
|
187
|
+
if (!Array.isArray(envelope.records))
|
|
188
|
+
return [invalid(["records"], "records is a list", "envelope.invalid")];
|
|
189
|
+
return [];
|
|
190
|
+
};
|
|
191
|
+
const canonical = (value) => {
|
|
192
|
+
if (Array.isArray(value))
|
|
193
|
+
return value.map(canonical);
|
|
194
|
+
if (!isObject(value))
|
|
195
|
+
return value;
|
|
196
|
+
return Object.fromEntries(Object.keys(value)
|
|
197
|
+
.sort()
|
|
198
|
+
.map((key) => [key, canonical(value[key])]));
|
|
199
|
+
};
|
|
200
|
+
/** The same fields and relations in any key or relation order give the same fingerprint. */
|
|
201
|
+
export const recordFingerprint = (fields, relations) => createHash("sha256")
|
|
202
|
+
.update(JSON.stringify(canonical({
|
|
203
|
+
fields,
|
|
204
|
+
relations: relations
|
|
205
|
+
.map((relation) => [relation.role, relation.type, relation.external_id])
|
|
206
|
+
.sort((a, b) => a.join("\0").localeCompare(b.join("\0"))),
|
|
207
|
+
})))
|
|
208
|
+
.digest("hex");
|
|
209
|
+
/**
|
|
210
|
+
* A JSONL import as the envelope it stands for: the first line holds `schema_version` and `source`, each further
|
|
211
|
+
* non-empty line one record. A line that is not JSON comes back as a record the envelope check refuses, at its index.
|
|
212
|
+
*/
|
|
213
|
+
export const envelopeOfJsonl = (text) => {
|
|
214
|
+
const lines = text.split(/\r?\n/);
|
|
215
|
+
const first = lines.findIndex((line) => line.trim() !== "");
|
|
216
|
+
if (first < 0)
|
|
217
|
+
return {};
|
|
218
|
+
const parse = (line) => {
|
|
219
|
+
try {
|
|
220
|
+
return JSON.parse(line);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return line;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const header = parse(lines[first]);
|
|
227
|
+
const records = [];
|
|
228
|
+
for (let at = first + 1; at < lines.length; at++) {
|
|
229
|
+
const line = lines[at];
|
|
230
|
+
if (line.trim() !== "")
|
|
231
|
+
records.push(parse(line));
|
|
232
|
+
}
|
|
233
|
+
return isObject(header) ? { ...header, records } : header;
|
|
234
|
+
};
|
package/dist/entities/fact.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as v from "valibot";
|
|
|
2
2
|
export declare const fact: v.StrictObjectSchema<{
|
|
3
3
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
4
4
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
5
|
-
readonly kind: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"
|
|
5
|
+
readonly kind: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
|
|
6
6
|
readonly text: v.StringSchema<undefined>;
|
|
7
7
|
readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed", "rejected"], undefined>;
|
|
8
8
|
readonly project: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
@@ -29,17 +29,6 @@ export declare const fact: v.StrictObjectSchema<{
|
|
|
29
29
|
/** Who recorded it: the owner, or an agent whose fact waits as proposed. */
|
|
30
30
|
readonly origin: v.PicklistSchema<["owner", "agent"], undefined>;
|
|
31
31
|
readonly evidence: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
32
|
-
/** A `payment` fact's fields. */
|
|
33
|
-
readonly payment: v.OptionalSchema<v.NullableSchema<v.StrictObjectSchema<{
|
|
34
|
-
readonly amount: v.StringSchema<undefined>;
|
|
35
|
-
readonly currency: v.StringSchema<undefined>;
|
|
36
|
-
readonly direction: v.PicklistSchema<readonly ["to_us", "from_us"], undefined>;
|
|
37
|
-
readonly counterparty: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
38
|
-
readonly counterparty_name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
39
|
-
readonly due_at: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
40
|
-
readonly state: v.PicklistSchema<readonly ["expected", "paid", "cancelled"], undefined>;
|
|
41
|
-
readonly item: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
42
|
-
}, undefined>, undefined>, undefined>;
|
|
43
32
|
/** With `--sources`: each piece of evidence read back. */
|
|
44
33
|
readonly sources: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
45
34
|
readonly ref: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
package/dist/entities/fact.js
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import * as v from "valibot";
|
|
2
2
|
import { FACT_STATUSES, FACT_TYPES, MEMORY_SCOPES } from "../domain/knowledge.js";
|
|
3
|
-
import { PAYMENT_DIRECTIONS, PAYMENT_STATES } from "../meetings/index.js";
|
|
4
3
|
import { author, evidenceSource, id, reference, text, time } from "./fields.js";
|
|
5
|
-
const payment = v.strictObject({
|
|
6
|
-
amount: text,
|
|
7
|
-
currency: text,
|
|
8
|
-
direction: v.picklist(PAYMENT_DIRECTIONS),
|
|
9
|
-
counterparty: v.nullable(reference),
|
|
10
|
-
counterparty_name: v.nullable(text),
|
|
11
|
-
due_at: v.nullable(text),
|
|
12
|
-
state: v.picklist(PAYMENT_STATES),
|
|
13
|
-
item: v.nullable(text),
|
|
14
|
-
});
|
|
15
4
|
export const fact = v.strictObject({
|
|
16
5
|
ref: reference,
|
|
17
6
|
id,
|
|
@@ -35,8 +24,6 @@ export const fact = v.strictObject({
|
|
|
35
24
|
/** Who recorded it: the owner, or an agent whose fact waits as proposed. */
|
|
36
25
|
origin: v.picklist(["owner", "agent"]),
|
|
37
26
|
evidence: v.array(reference),
|
|
38
|
-
/** A `payment` fact's fields. */
|
|
39
|
-
payment: v.optional(v.nullable(payment)),
|
|
40
27
|
/** With `--sources`: each piece of evidence read back. */
|
|
41
28
|
sources: v.optional(v.array(evidenceSource)),
|
|
42
29
|
created_at: time,
|
package/dist/entities/index.d.ts
CHANGED
|
@@ -5761,7 +5761,7 @@ export declare const ENTITIES: {
|
|
|
5761
5761
|
readonly fact: v.StrictObjectSchema<{
|
|
5762
5762
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
|
5763
5763
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
5764
|
-
readonly kind: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"
|
|
5764
|
+
readonly kind: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
|
|
5765
5765
|
readonly text: v.StringSchema<undefined>;
|
|
5766
5766
|
readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed", "rejected"], undefined>;
|
|
5767
5767
|
readonly project: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
@@ -5784,16 +5784,6 @@ export declare const ENTITIES: {
|
|
|
5784
5784
|
readonly scope: v.NullableSchema<v.PicklistSchema<readonly ["personal", "work"], undefined>, undefined>;
|
|
5785
5785
|
readonly origin: v.PicklistSchema<["owner", "agent"], undefined>;
|
|
5786
5786
|
readonly evidence: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
5787
|
-
readonly payment: v.OptionalSchema<v.NullableSchema<v.StrictObjectSchema<{
|
|
5788
|
-
readonly amount: v.StringSchema<undefined>;
|
|
5789
|
-
readonly currency: v.StringSchema<undefined>;
|
|
5790
|
-
readonly direction: v.PicklistSchema<readonly ["to_us", "from_us"], undefined>;
|
|
5791
|
-
readonly counterparty: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
5792
|
-
readonly counterparty_name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
5793
|
-
readonly due_at: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
5794
|
-
readonly state: v.PicklistSchema<readonly ["expected", "paid", "cancelled"], undefined>;
|
|
5795
|
-
readonly item: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
5796
|
-
}, undefined>, undefined>, undefined>;
|
|
5797
5787
|
readonly sources: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
5798
5788
|
readonly ref: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
5799
5789
|
readonly kind: v.StringSchema<undefined>;
|
|
@@ -6589,7 +6579,7 @@ export declare const ENTITIES: {
|
|
|
6589
6579
|
readonly fingerprint: v.StringSchema<undefined>;
|
|
6590
6580
|
}, undefined>, undefined>;
|
|
6591
6581
|
readonly kind: v.LiteralSchema<"fact", undefined>;
|
|
6592
|
-
readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"
|
|
6582
|
+
readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
|
|
6593
6583
|
readonly statement: v.StringSchema<undefined>;
|
|
6594
6584
|
}, undefined>, v.StrictObjectSchema<{
|
|
6595
6585
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
@@ -17,7 +17,7 @@ export declare const meetingFindings: v.StrictObjectSchema<{
|
|
|
17
17
|
readonly fingerprint: v.StringSchema<undefined>;
|
|
18
18
|
}, undefined>, undefined>;
|
|
19
19
|
readonly kind: v.LiteralSchema<"fact", undefined>;
|
|
20
|
-
readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"
|
|
20
|
+
readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
|
|
21
21
|
readonly statement: v.StringSchema<undefined>;
|
|
22
22
|
}, undefined>, v.StrictObjectSchema<{
|
|
23
23
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
package/dist/errors/reasons.js
CHANGED
|
@@ -138,6 +138,7 @@ const OTHERS = [
|
|
|
138
138
|
validation("input.required", "the command needs an answer and cannot ask for one"),
|
|
139
139
|
validation("input.too-large", "the input is over its size limit"),
|
|
140
140
|
["meeting-project.stale", "configuration_error", "the meeting's project changed meanwhile"],
|
|
141
|
+
validation("fact.payment-retired", "a payment is a payments row in store.finance, not a fact"),
|
|
141
142
|
validation("finance.allocate", "the invoice or payment cannot take an allocation in its state"),
|
|
142
143
|
validation("finance.currency", "the payment and the invoice are in different currencies; nothing is converted"),
|
|
143
144
|
validation("finance.field", "a field of the invoice or payment is not valid"),
|
|
@@ -150,6 +151,15 @@ const OTHERS = [
|
|
|
150
151
|
validation("finance.ref", "the reference names no invoice or payment"),
|
|
151
152
|
validation("finance.status", "the invoice's status does not allow this"),
|
|
152
153
|
validation("finance.total", "the invoice's total is not the sum of its lines"),
|
|
154
|
+
validation("record.entity", "the invoice or payment the record stands for was refused"),
|
|
155
|
+
["record.map-owner-only", "permission_error", "only the owner says which entity a source record is"],
|
|
156
|
+
validation("record.map-type", "a record maps only to an entity of its own type"),
|
|
157
|
+
validation("record.mapped", "the record already stands for another entity"),
|
|
158
|
+
validation("record.not-found", "the store holds no such record to delete"),
|
|
159
|
+
validation("record.invalid", "a field of the record is not the kind the entity needs"),
|
|
160
|
+
validation("record.reference-not-found", "a reference in the record's fields is not in the store"),
|
|
161
|
+
validation("record.relation-ambiguous", "the record names more than one target for a role that takes one"),
|
|
162
|
+
validation("record.stale-revision", "the store holds a newer revision of the record"),
|
|
153
163
|
validation("memory.evidence-required", "a memory needs its evidence"),
|
|
154
164
|
validation("memory.scope-required", "a memory needs its scope"),
|
|
155
165
|
validation("money.amount", "the amount is not a decimal string in the currency's scale"),
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { FOLDER_PROVIDERS, isFolderProvider, isKind, KINDS, type Kind, kindIn, k
|
|
|
14
14
|
export type { RankingGraphEvidence, RankingGraphLink } from "./domain/rankings-graph.js";
|
|
15
15
|
export type { RankingComponent, RankingInput, RankingMeasure, RankingOptions, RankingTarget, RankingWeights, ScorePreset, } from "./domain/rankings-options.js";
|
|
16
16
|
export { rankingOptions } from "./domain/rankings-options.js";
|
|
17
|
+
export { type EnvelopeRecord, type EnvelopeSource, envelopeOfJsonl, type IngestError, RECORD_ENTITY_TYPES, RECORD_OPERATIONS, RECORDS_ENVELOPE_SCHEMA, RECORDS_SCHEMA_VERSION, type RecordEntityType, type RecordOperation, type RecordRelation, type RecordsEnvelope, } from "./domain/records.js";
|
|
17
18
|
export { canonicalReference, formatReference, looksLikeReference, parseReference, REF_KINDS, type Reference, type RefKindName, type RefKindSpec, } from "./domain/references.js";
|
|
18
19
|
export { normalizeTag, TAG_TYPES, type TagType } from "./domain/tags.js";
|
|
19
20
|
export { type AppErrors, DETAILS as CORE_DETAILS, failureContext, REASONS as CORE_REASONS } from "./errors/reasons.js";
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export { CURRENCY_SCALES, currencyOf, minorUnits, moneyLabel, moneyOf, } from ".
|
|
|
9
9
|
export { countedPoll } from "./domain/polls.js";
|
|
10
10
|
export { FOLDER_PROVIDERS, isFolderProvider, isKind, KINDS, kindIn, kindOf, PROVIDERS, providerNamed, providers, UNKNOWN_PROVIDER, useProviders, } from "./domain/providers.js";
|
|
11
11
|
export { rankingOptions } from "./domain/rankings-options.js";
|
|
12
|
+
export { envelopeOfJsonl, RECORD_ENTITY_TYPES, RECORD_OPERATIONS, RECORDS_ENVELOPE_SCHEMA, RECORDS_SCHEMA_VERSION, } from "./domain/records.js";
|
|
12
13
|
export { canonicalReference, formatReference, looksLikeReference, parseReference, REF_KINDS, } from "./domain/references.js";
|
|
13
14
|
export { normalizeTag, TAG_TYPES } from "./domain/tags.js";
|
|
14
15
|
export { DETAILS as CORE_DETAILS, failureContext, REASONS as CORE_REASONS } from "./errors/reasons.js";
|
|
@@ -14,16 +14,19 @@ interface FindingBase {
|
|
|
14
14
|
}
|
|
15
15
|
export interface FactFinding extends FindingBase {
|
|
16
16
|
kind: "fact";
|
|
17
|
-
type:
|
|
17
|
+
type: (typeof FACT_TYPES)[number];
|
|
18
18
|
statement: string;
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
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
|
|
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 `
|
|
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 {
|