@wirecat/cli-core 0.558.0 → 0.560.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 +60 -0
- package/dist/background/schedules.d.ts +2 -0
- package/dist/cli/effects.d.ts +13 -2
- package/dist/cli/effects.js +23 -2
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +2 -2
- package/dist/cli/meeting-findings-input.d.ts +3 -0
- package/dist/cli/meeting-findings-input.js +4 -0
- package/dist/cli/messenger/conversation-answers.d.ts +3 -0
- package/dist/cli/messenger/message-answers.d.ts +3 -0
- package/dist/cli/messenger/message-writes.d.ts +6 -0
- package/dist/cli/schedule/schedule-command.d.ts +6 -1
- package/dist/cli/schedule/schedule-command.js +24 -2
- package/dist/domain/records.d.ts +161 -0
- package/dist/domain/records.js +234 -0
- package/dist/entities/contact-context.d.ts +6 -0
- package/dist/entities/conversation-results.d.ts +6 -0
- package/dist/entities/evidence.d.ts +24 -0
- package/dist/entities/index.d.ts +190 -0
- package/dist/entities/index.js +3 -0
- package/dist/entities/listener.d.ts +12 -0
- package/dist/entities/mail-findings.d.ts +116 -0
- package/dist/entities/mail-findings.js +68 -0
- package/dist/entities/message-context.d.ts +9 -0
- package/dist/entities/message-write.d.ts +12 -0
- package/dist/entities/message.d.ts +6 -0
- package/dist/entities/message.js +6 -0
- package/dist/entities/store-records.d.ts +3 -0
- package/dist/errors/reasons.js +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mail/cli/marks.d.ts +1 -0
- package/dist/mail/cli/marks.js +11 -4
- package/dist/mail/cli/review.js +10 -2
- package/dist/mail/findings.d.ts +55 -0
- package/dist/mail/findings.js +140 -0
- package/dist/mail/index.d.ts +2 -1
- package/dist/mail/index.js +2 -1
- package/dist/mail/marks.d.ts +10 -3
- package/dist/mail/marks.js +34 -5
- package/dist/meetings/findings-rules.d.ts +24 -0
- package/dist/meetings/findings-rules.js +136 -116
- package/dist/meetings/index.d.ts +2 -1
- package/dist/meetings/index.js +2 -1
- package/dist/meetings/mcp/index.d.ts +15 -15
- 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/proposals/kinds/mail-findings.d.ts +8 -0
- package/dist/services/proposals/kinds/mail-findings.js +87 -0
- package/dist/services/proposals/registry.js +3 -1
- package/dist/services/proposals/types.d.ts +2 -0
- package/dist/store/decisions.d.ts +1 -1
- package/dist/store/decisions.js +1 -1
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.js +1 -0
- package/dist/store/mail-findings.d.ts +51 -0
- package/dist/store/mail-findings.js +1 -0
- package/dist/store/path.d.ts +2 -0
- package/dist/store/path.js +21 -1
- package/dist/store/sqlite/accounts.js +4 -0
- package/dist/store/sqlite/email-activity.d.ts +8 -0
- package/dist/store/sqlite/email-activity.js +31 -5
- package/dist/store/sqlite/findings-core.d.ts +1 -0
- package/dist/store/sqlite/findings-core.js +1 -1
- package/dist/store/sqlite/mail-findings.d.ts +3 -0
- package/dist/store/sqlite/mail-findings.js +333 -0
- package/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/memories.d.ts +4 -1
- package/dist/store/sqlite/memories.js +48 -43
- package/dist/store/sqlite/migrations.generated.js +16 -0
- package/dist/store/sqlite/open-store.d.ts +1 -1
- package/dist/store/sqlite/open-store.js +4 -1
- package/dist/store/sqlite/open.js +2 -1
- package/dist/store/sqlite/proposed-actions.js +2 -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/sqlite/tasks.d.ts +2 -0
- package/dist/store/sqlite/tasks.js +26 -24
- package/dist/store/sqlite/test-accounts.d.ts +8 -0
- package/dist/store/sqlite/test-accounts.js +31 -0
- package/dist/store/sqlite/work-tasks.d.ts +1 -0
- package/dist/store/sqlite/work-tasks.js +6 -0
- package/dist/store/store.d.ts +6 -0
- package/dist/store/store.js +10 -0
- package/package.json +4 -2
- package/schemas/records-envelope.v1.schema.json +108 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { contextEvidenceFingerprint } from "../../context/evidence.js";
|
|
3
|
+
import { parseLocator } from "../../domain/locator.js";
|
|
4
|
+
import { MAIL_FINDINGS_PER_SENDER, mailFindingFingerprint, validateMailFindings, } from "../../mail/findings.js";
|
|
5
|
+
import { findingHash } from "../../meetings/index.js";
|
|
6
|
+
import { assertMayDecide } from "../decisions.js";
|
|
7
|
+
import { MAIL_FINDINGS_KIND } from "../mail-findings.js";
|
|
8
|
+
import { findingsAgent, findingsCoreOver } from "./findings-core.js";
|
|
9
|
+
import { authorActor, memoriesStoreOver } from "./memories.js";
|
|
10
|
+
import { insertLink, requiredThing } from "./notes.js";
|
|
11
|
+
import { linkIdentityToPerson } from "./person-links.js";
|
|
12
|
+
import { proposalOperationsOver } from "./proposed-actions.js";
|
|
13
|
+
import { addComment } from "./task-comments.js";
|
|
14
|
+
import { inboxProject } from "./tasks.js";
|
|
15
|
+
import { taskRowOf } from "./things.js";
|
|
16
|
+
import { write } from "./transaction.js";
|
|
17
|
+
import { workTaskOperationsOver } from "./work-tasks.js";
|
|
18
|
+
const stale = () => {
|
|
19
|
+
throw new CliError("validation_error", "mail findings cite mail that changed since it was read; stage a new batch", {
|
|
20
|
+
reason: "mail-findings.stale",
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
const invalid = (message, reason) => {
|
|
24
|
+
throw new CliError("validation_error", message, reason ? { reason } : undefined);
|
|
25
|
+
};
|
|
26
|
+
const TEXT_BUDGET = 262144;
|
|
27
|
+
const EMAIL = `SELECT e.id, e.account_id, e.subject, coalesce(e.body_text, '') AS body,
|
|
28
|
+
json_extract(e.metadata, '$.partial') IS NOT NULL AS partial, lower(e.from_address) AS sender,
|
|
29
|
+
coalesce(e.outgoing, lower(e.from_address) = lower(a.external_id)) AS outgoing
|
|
30
|
+
FROM emails e JOIN email_threads t ON t.id = e.email_thread_id JOIN accounts a ON a.id = e.account_id
|
|
31
|
+
WHERE a.provider = 'email' AND a.external_id = ? AND t.external_id = ? AND e.external_id = ? AND e.deleted_at IS NULL`;
|
|
32
|
+
export const mailFindingsStoreOver = (context) => {
|
|
33
|
+
const { database, now } = context;
|
|
34
|
+
const proposals = proposalOperationsOver(context);
|
|
35
|
+
const core = findingsCoreOver(context, MAIL_FINDINGS_KIND);
|
|
36
|
+
const memories = memoriesStoreOver(context);
|
|
37
|
+
const tasks = workTaskOperationsOver(context);
|
|
38
|
+
const mailbox = (key) => {
|
|
39
|
+
if (key?.provider !== "email" || typeof key.account !== "string" || !key.account.trim())
|
|
40
|
+
return invalid("mail findings belong to one email account");
|
|
41
|
+
const row = database
|
|
42
|
+
.prepare("SELECT id FROM accounts WHERE provider = 'email' AND external_id = ?")
|
|
43
|
+
.get(key.account);
|
|
44
|
+
if (!row)
|
|
45
|
+
throw new CliError("not_found", `no mail of ${key.account} is stored`);
|
|
46
|
+
return Number(row.id);
|
|
47
|
+
};
|
|
48
|
+
const load = (id) => {
|
|
49
|
+
if (typeof id !== "string" || !/^[1-9]\d*$/.test(id) || !Number.isSafeInteger(Number(id)))
|
|
50
|
+
return invalid("a findings batch needs a stored ID");
|
|
51
|
+
const row = database
|
|
52
|
+
.prepare("SELECT length(CAST(payload AS BLOB)) AS bytes, length(CAST(result AS BLOB)) AS result_bytes FROM proposed_actions WHERE id = ? AND kind = ?")
|
|
53
|
+
.get(Number(id), MAIL_FINDINGS_KIND);
|
|
54
|
+
if (!row)
|
|
55
|
+
throw new CliError("not_found", "the mail findings batch is unavailable");
|
|
56
|
+
if (Number(row.bytes) > 100000 || Number(row.result_bytes) > 65536)
|
|
57
|
+
return invalid("stored findings exceed the read budget");
|
|
58
|
+
return proposals.get(id);
|
|
59
|
+
};
|
|
60
|
+
const accountOf = (proposal) => mailbox(proposal.account);
|
|
61
|
+
const view = (proposal) => {
|
|
62
|
+
const input = proposal.payload;
|
|
63
|
+
if (!input || !proposal.account || !["proposed", "approved", "rejected"].includes(proposal.status))
|
|
64
|
+
return invalid("invalid stored findings batch");
|
|
65
|
+
return {
|
|
66
|
+
id: proposal.id,
|
|
67
|
+
account: proposal.account,
|
|
68
|
+
input: { schemaVersion: 1, key: input.key, extractor: input.extractor, findings: input.findings },
|
|
69
|
+
fingerprint: findingHash({ input, author: proposal.proposedBy.id }),
|
|
70
|
+
status: proposal.status === "proposed" ? "proposed" : "reviewed",
|
|
71
|
+
author: proposal.proposedBy,
|
|
72
|
+
decidedBy: proposal.decidedBy,
|
|
73
|
+
decidedAt: proposal.decidedAt,
|
|
74
|
+
previous: proposal.result
|
|
75
|
+
? proposal.result.items
|
|
76
|
+
: input.findings.flatMap((finding) => {
|
|
77
|
+
const held = core.previous(accountOf(proposal), mailFindingFingerprint(finding));
|
|
78
|
+
return held ? [{ ...held, id: finding.id, reused: true }] : [];
|
|
79
|
+
}),
|
|
80
|
+
receipt: proposal.result,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Reads each cited email once: it must be stored, whole, and hash to the fingerprint given. Every finding cites
|
|
85
|
+
* mail of the batch's own mailbox, and no sender other than the owner is behind more than five findings.
|
|
86
|
+
*/
|
|
87
|
+
const cite = (findings, account) => {
|
|
88
|
+
const read = new Map();
|
|
89
|
+
const perSender = new Map();
|
|
90
|
+
let bytes = 0;
|
|
91
|
+
for (const finding of findings) {
|
|
92
|
+
const senders = new Set();
|
|
93
|
+
let own = false;
|
|
94
|
+
for (const { reference, fingerprint } of finding.evidence) {
|
|
95
|
+
let row = read.get(reference);
|
|
96
|
+
if (!row) {
|
|
97
|
+
const at = parseLocator(reference);
|
|
98
|
+
row = database.prepare(EMAIL).get(at.account, at.chat, at.message);
|
|
99
|
+
if (!row)
|
|
100
|
+
return stale();
|
|
101
|
+
bytes += Buffer.byteLength(String(row.body)) + Buffer.byteLength(String(row.subject ?? ""));
|
|
102
|
+
if (bytes > TEXT_BUDGET)
|
|
103
|
+
return invalid(`finding evidence exceeds the ${TEXT_BUDGET}-byte stored text budget`);
|
|
104
|
+
if (row.partial)
|
|
105
|
+
return invalid(`${reference} is not stored whole yet; cite it once it is`, "mail-findings.partial");
|
|
106
|
+
const title = row.subject == null ? null : String(row.subject);
|
|
107
|
+
row.fingerprint = contextEvidenceFingerprint({
|
|
108
|
+
ref: reference,
|
|
109
|
+
title,
|
|
110
|
+
text: String(row.body),
|
|
111
|
+
revision: null,
|
|
112
|
+
});
|
|
113
|
+
read.set(reference, row);
|
|
114
|
+
}
|
|
115
|
+
if (row.fingerprint !== fingerprint)
|
|
116
|
+
return stale();
|
|
117
|
+
if (Number(row.account_id) === account)
|
|
118
|
+
own = true;
|
|
119
|
+
if (!row.outgoing && row.sender != null)
|
|
120
|
+
senders.add(String(row.sender));
|
|
121
|
+
}
|
|
122
|
+
if (!own)
|
|
123
|
+
return invalid(`finding ${finding.id} cites no email of this mailbox`);
|
|
124
|
+
for (const sender of senders) {
|
|
125
|
+
const count = (perSender.get(sender) ?? 0) + 1;
|
|
126
|
+
if (count > MAIL_FINDINGS_PER_SENDER)
|
|
127
|
+
return invalid(`more than ${MAIL_FINDINGS_PER_SENDER} findings from ${sender} in one batch`);
|
|
128
|
+
perSender.set(sender, count);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return read;
|
|
132
|
+
};
|
|
133
|
+
const identityOfAddress = (address) => database
|
|
134
|
+
.prepare("SELECT i.id, l.person_id FROM identities i LEFT JOIN identity_links l ON l.identity_id = i.id " +
|
|
135
|
+
"WHERE i.provider = 'email' AND lower(i.external_id) = ?")
|
|
136
|
+
.get(address.toLowerCase());
|
|
137
|
+
/** Whatever a finding names must be stored: a task to update, a subject, a link's ends, a contact's address. */
|
|
138
|
+
const targets = (finding, read) => {
|
|
139
|
+
const thing = (reference, what) => {
|
|
140
|
+
if (reference !== undefined)
|
|
141
|
+
requiredThing(database, reference, what);
|
|
142
|
+
};
|
|
143
|
+
switch (finding.kind) {
|
|
144
|
+
case "task":
|
|
145
|
+
thing(finding.project, "the project");
|
|
146
|
+
thing(finding.assignee, "the assignee");
|
|
147
|
+
return;
|
|
148
|
+
case "fact":
|
|
149
|
+
case "memory":
|
|
150
|
+
thing(finding.subject, "the subject");
|
|
151
|
+
thing(finding.project, "the project");
|
|
152
|
+
return;
|
|
153
|
+
case "task_update":
|
|
154
|
+
if (taskRowOf(database, finding.task.slice("task:".length)) === undefined)
|
|
155
|
+
throw new CliError("not_found", `no task ${finding.task}`);
|
|
156
|
+
return;
|
|
157
|
+
case "link":
|
|
158
|
+
thing(finding.from, "the link's source");
|
|
159
|
+
thing(finding.to, "the link's target");
|
|
160
|
+
return;
|
|
161
|
+
case "new_contact": {
|
|
162
|
+
const ids = finding.evidence.map(({ reference }) => Number(read.get(reference)?.id));
|
|
163
|
+
const marks = ids.map(() => "?").join(",");
|
|
164
|
+
const named = database
|
|
165
|
+
.prepare(`SELECT 1 FROM emails WHERE id IN (${marks}) AND lower(from_address) = ? UNION ALL ` +
|
|
166
|
+
`SELECT 1 FROM email_recipients WHERE email_id IN (${marks}) AND lower(address) = ? LIMIT 1`)
|
|
167
|
+
.get(...ids, finding.address, ...ids, finding.address);
|
|
168
|
+
if (!named)
|
|
169
|
+
return invalid(`${finding.address} neither sent nor received the mail finding ${finding.id} cites`);
|
|
170
|
+
if (!identityOfAddress(finding.address))
|
|
171
|
+
throw new CliError("not_found", `no stored contact ${finding.address}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
/** What accepting one finding makes, inside the decision's write; returns the reference of what it made. */
|
|
177
|
+
const create = (finding, account, agent) => {
|
|
178
|
+
const evidence = finding.evidence.map((one) => one.reference);
|
|
179
|
+
switch (finding.kind) {
|
|
180
|
+
case "fact":
|
|
181
|
+
return core.create(finding, agent, "owner");
|
|
182
|
+
case "task": {
|
|
183
|
+
const project = finding.project ?? `project:${inboxProject(context, `email:${account.account}`, true)}`;
|
|
184
|
+
return core.create({ ...finding, project }, agent, "owner");
|
|
185
|
+
}
|
|
186
|
+
case "task_update": {
|
|
187
|
+
if (finding.change === "close")
|
|
188
|
+
tasks.editNow(finding.task, { status: "done", reason: finding.reason }, "owner");
|
|
189
|
+
else if (finding.change === "due")
|
|
190
|
+
tasks.editNow(finding.task, { dueAt: new Date(finding.dueAt) }, "owner");
|
|
191
|
+
else {
|
|
192
|
+
const row = taskRowOf(database, finding.task.slice("task:".length));
|
|
193
|
+
addComment(context, row, `${finding.detail}\n\n${evidence.join("\n")}`, agent);
|
|
194
|
+
}
|
|
195
|
+
return finding.task;
|
|
196
|
+
}
|
|
197
|
+
case "memory": {
|
|
198
|
+
const made = memories.addNow({
|
|
199
|
+
kind: finding.memoryKind,
|
|
200
|
+
body: finding.body,
|
|
201
|
+
scope: finding.scope,
|
|
202
|
+
evidence,
|
|
203
|
+
author: agent,
|
|
204
|
+
...(finding.subject === undefined ? {} : { subject: finding.subject }),
|
|
205
|
+
...(finding.project === undefined ? {} : { project: finding.project }),
|
|
206
|
+
});
|
|
207
|
+
return memories.confirmNow(made.ref, "owner").ref;
|
|
208
|
+
}
|
|
209
|
+
case "link": {
|
|
210
|
+
if (finding.from.startsWith("contact:")) {
|
|
211
|
+
const identity = requiredThing(database, finding.from, "the contact");
|
|
212
|
+
const person = requiredThing(database, finding.to, "the person");
|
|
213
|
+
linkIdentityToPerson(context, identity.id, person.id, { method: "mail-finding", by: "owner" });
|
|
214
|
+
return finding.to;
|
|
215
|
+
}
|
|
216
|
+
const link = insertLink(context, {
|
|
217
|
+
from: finding.from,
|
|
218
|
+
to: finding.to,
|
|
219
|
+
kind: "member-of",
|
|
220
|
+
origin: "owner",
|
|
221
|
+
evidence: evidence[0],
|
|
222
|
+
provenance: `${agent.bot}: ${finding.reason}`,
|
|
223
|
+
});
|
|
224
|
+
if (!link.confirmed)
|
|
225
|
+
database.prepare("UPDATE links SET confirmed = 1, updated_at = ? WHERE id = ?").run(now(), Number(link.id));
|
|
226
|
+
return `link:${link.id}`;
|
|
227
|
+
}
|
|
228
|
+
case "new_contact": {
|
|
229
|
+
const person = Number(identityOfAddress(finding.address)?.person_id);
|
|
230
|
+
database
|
|
231
|
+
.prepare("UPDATE persons SET name = ?, updated_at = ? WHERE id = ? AND name IS NULL")
|
|
232
|
+
.run(finding.name, now(), person);
|
|
233
|
+
return `person:${person}`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
stage: async (key, raw, rawAgent) => {
|
|
239
|
+
const input = validateMailFindings(raw);
|
|
240
|
+
const by = findingsAgent(rawAgent);
|
|
241
|
+
return write(database, () => {
|
|
242
|
+
const account = mailbox(key);
|
|
243
|
+
const { actor } = authorActor(context, by);
|
|
244
|
+
const inputHash = findingHash({ input, model: by.model ?? null });
|
|
245
|
+
const held = database
|
|
246
|
+
.prepare("SELECT id FROM proposed_actions WHERE account_id = ? AND kind = ? AND proposed_by_type = ? AND proposed_by_id = ? AND json_extract(payload, '$.key') = ? ORDER BY id LIMIT 1")
|
|
247
|
+
.get(account, MAIL_FINDINGS_KIND, actor.type, actor.id, input.key);
|
|
248
|
+
if (held) {
|
|
249
|
+
const proposal = load(String(held.id));
|
|
250
|
+
if (proposal.payload.inputHash !== inputHash)
|
|
251
|
+
return invalid("the findings batch key was already used for different input");
|
|
252
|
+
return view(proposal);
|
|
253
|
+
}
|
|
254
|
+
const read = cite(input.findings, account);
|
|
255
|
+
for (const finding of input.findings)
|
|
256
|
+
targets(finding, read);
|
|
257
|
+
const fingerprints = input.findings.map(mailFindingFingerprint);
|
|
258
|
+
if (new Set(fingerprints).size !== fingerprints.length)
|
|
259
|
+
return invalid("the batch contains duplicate findings");
|
|
260
|
+
return view(proposals.propose({
|
|
261
|
+
kind: MAIL_FINDINGS_KIND,
|
|
262
|
+
by,
|
|
263
|
+
account: { provider: "email", account: key.account },
|
|
264
|
+
payload: { ...input, inputHash, agentModel: by.model ?? null },
|
|
265
|
+
}));
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
get: async (id) => view(load(id)),
|
|
269
|
+
list: async ({ account, status, limit = 20, offset = 0 } = {}) => {
|
|
270
|
+
if (status !== undefined && status !== "proposed" && status !== "reviewed")
|
|
271
|
+
return invalid("finding status is proposed or reviewed");
|
|
272
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 50 || !Number.isSafeInteger(offset) || offset < 0)
|
|
273
|
+
return invalid("findings lists take 1–50 items and a nonnegative offset");
|
|
274
|
+
const id = account === undefined ? null : mailbox(account);
|
|
275
|
+
const rows = database
|
|
276
|
+
.prepare("SELECT id FROM proposed_actions WHERE kind = ? AND (? IS NULL OR account_id = ?) AND (? IS NULL OR status = ?) " +
|
|
277
|
+
"ORDER BY id DESC LIMIT ? OFFSET ?")
|
|
278
|
+
.all(MAIL_FINDINGS_KIND, id, id, status ?? null, status === "reviewed" ? "approved" : (status ?? null), limit + 1, offset);
|
|
279
|
+
return { items: rows.slice(0, limit).map((row) => view(load(String(row.id)))), hasMore: rows.length > limit };
|
|
280
|
+
},
|
|
281
|
+
review: async (id, decision) => write(database, () => {
|
|
282
|
+
const proposal = load(id);
|
|
283
|
+
const batch = view(proposal);
|
|
284
|
+
if (!decision || typeof decision.fingerprint !== "string" || decision.fingerprint !== batch.fingerprint)
|
|
285
|
+
return stale();
|
|
286
|
+
if (!Array.isArray(decision.accept) ||
|
|
287
|
+
decision.accept.length > batch.input.findings.length ||
|
|
288
|
+
new Set(decision.accept).size !== decision.accept.length ||
|
|
289
|
+
decision.accept.some((candidate) => !batch.input.findings.some((finding) => finding.id === candidate)))
|
|
290
|
+
return invalid("accept only unique IDs shown in this batch");
|
|
291
|
+
if (decision.by === undefined)
|
|
292
|
+
return invalid("a findings review names who decides: the owner");
|
|
293
|
+
assertMayDecide({ id, proposedBy: proposals.proposer(id), ownerOnly: true }, decision.by, context.decisions);
|
|
294
|
+
const accepted = [...decision.accept].sort();
|
|
295
|
+
if (batch.receipt) {
|
|
296
|
+
if (JSON.stringify(batch.receipt.accepted) !== JSON.stringify(accepted))
|
|
297
|
+
return invalid("this findings batch was already reviewed with a different decision");
|
|
298
|
+
return batch;
|
|
299
|
+
}
|
|
300
|
+
const input = proposal.payload;
|
|
301
|
+
const account = accountOf(proposal);
|
|
302
|
+
// Rejecting creates nothing, so a batch whose mail changed can still be closed; accepting needs the
|
|
303
|
+
// accepted findings' own mail unchanged.
|
|
304
|
+
const chosen = batch.input.findings.filter((finding) => accepted.includes(finding.id));
|
|
305
|
+
if (chosen.length)
|
|
306
|
+
cite(chosen, account);
|
|
307
|
+
if (proposal.proposedBy.type !== "bot" || !proposal.proposedBy.name)
|
|
308
|
+
return invalid("a findings author must be an agent");
|
|
309
|
+
const agent = {
|
|
310
|
+
bot: proposal.proposedBy.name,
|
|
311
|
+
...(input.agentModel === null ? {} : { model: input.agentModel }),
|
|
312
|
+
};
|
|
313
|
+
const items = batch.input.findings.map((finding) => {
|
|
314
|
+
const fingerprint = mailFindingFingerprint(finding);
|
|
315
|
+
const prior = core.previous(account, fingerprint);
|
|
316
|
+
if (prior) {
|
|
317
|
+
if (accepted.includes(finding.id) && prior.status === "rejected")
|
|
318
|
+
return invalid("this unchanged finding was already rejected; changed evidence needs a new candidate");
|
|
319
|
+
return { ...prior, id: finding.id, reused: true };
|
|
320
|
+
}
|
|
321
|
+
const status = accepted.includes(finding.id) ? "accepted" : "rejected";
|
|
322
|
+
return {
|
|
323
|
+
id: finding.id,
|
|
324
|
+
fingerprint,
|
|
325
|
+
status,
|
|
326
|
+
reference: status === "accepted" ? create(finding, batch.account, agent) : null,
|
|
327
|
+
reused: false,
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
return view(proposals.approve(id, { previewFingerprint: batch.fingerprint, accepted, items }, "owner"));
|
|
331
|
+
}),
|
|
332
|
+
};
|
|
333
|
+
};
|
|
@@ -62,6 +62,8 @@ export const MANIFEST = [
|
|
|
62
62
|
{ name: "20261102173102_version-45-finance", version: 45, minCompatible: 34 },
|
|
63
63
|
{ name: "20261102173113_version-45-finance-rules", version: 45, minCompatible: 34 },
|
|
64
64
|
{ name: "20261102185954_version-46-payment-facts", version: 46, minCompatible: 34 },
|
|
65
|
+
{ name: "20261102192959_version-47-records", version: 47, minCompatible: 34 },
|
|
66
|
+
{ name: "20261102222340_version-48-test-accounts", version: 48, minCompatible: 34 },
|
|
65
67
|
];
|
|
66
68
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
67
69
|
const migrations = [];
|
|
@@ -141,5 +141,8 @@ export declare const dropLinksOf: (database: StoreContext["database"], thing: {
|
|
|
141
141
|
* segment still holds the erased words, and the write-ahead log emptied when no reader holds it.
|
|
142
142
|
*/
|
|
143
143
|
export declare const erase: <T>(database: StoreContext["database"], work: () => T) => T;
|
|
144
|
-
export declare const memoriesStoreOver: (context: StoreContext) => MemoriesStore
|
|
144
|
+
export declare const memoriesStoreOver: (context: StoreContext) => MemoriesStore & {
|
|
145
|
+
addNow(input: MemoryInput): Memory;
|
|
146
|
+
confirmNow(reference: string, by: Decider): Memory;
|
|
147
|
+
};
|
|
145
148
|
export {};
|
|
@@ -167,46 +167,54 @@ export const memoriesStoreOver = (context) => {
|
|
|
167
167
|
.run(...(verified ? [status, at, at, Number(row.id)] : [status, at, Number(row.id)]));
|
|
168
168
|
return memoryOf(rowOf(reference));
|
|
169
169
|
};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
});
|
|
177
|
-
const scope = oneOf(input.scope, MEMORY_SCOPES, "a memory's scope");
|
|
178
|
-
if (!Array.isArray(input.evidence) || input.evidence.length === 0)
|
|
179
|
-
throw new CliError("validation_error", "a memory needs at least one piece of evidence it rests on", {
|
|
180
|
-
reason: "memory.evidence-required",
|
|
181
|
-
});
|
|
182
|
-
const body = input.body?.trim() ?? "";
|
|
183
|
-
if (!body || body.length > 20_000)
|
|
184
|
-
throw new CliError("validation_error", "a memory's body takes 1–20000 characters");
|
|
185
|
-
if (input.confidence !== undefined && !(input.confidence >= 0 && input.confidence <= 1))
|
|
186
|
-
throw new CliError("validation_error", "confidence takes 0–1");
|
|
187
|
-
const status = oneOf(input.status ?? "proposed", ["proposed", "confirmed"], "a new memory's status");
|
|
188
|
-
if (status === "confirmed" && input.author !== "owner")
|
|
189
|
-
throw new CliError("permission_error", "an agent's memory waits as proposed; only the owner confirms it", {
|
|
190
|
-
reason: "proposal.owner-only",
|
|
191
|
-
});
|
|
192
|
-
const id = write(database, () => {
|
|
193
|
-
const { actor, model } = authorActor(context, input.author);
|
|
194
|
-
const subject = input.subject === undefined ? undefined : requiredThing(database, input.subject, "the subject");
|
|
195
|
-
const project = input.project === undefined ? null : projectId(database, input.project);
|
|
196
|
-
const replaced = input.supersedes === undefined ? undefined : Number(rowOf(input.supersedes).id);
|
|
197
|
-
const at = now();
|
|
198
|
-
const made = Number(database
|
|
199
|
-
.prepare("INSERT INTO memories (id, kind, body, subject_type, subject_id, author_type, author_id, model, confidence, status, " +
|
|
200
|
-
"last_verified_at, supersedes_id, scope, project_id, created_at, updated_at) " +
|
|
201
|
-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id")
|
|
202
|
-
.get(nextId(database, "memories"), kind, body, subject?.type ?? null, subject?.id ?? null, actor.type, actor.id, model, input.confidence ?? null, status, status === "confirmed" ? at : null, replaced ?? null, scope, project, at, at)?.id);
|
|
203
|
-
linkEvidence(context, { type: "memory", id: made }, input.evidence, input.author === "owner" ? "owner" : "agent");
|
|
204
|
-
if (replaced !== undefined)
|
|
205
|
-
database.prepare("UPDATE memories SET status = 'superseded', updated_at = ? WHERE id = ?").run(at, replaced);
|
|
206
|
-
return made;
|
|
170
|
+
/** `add` inside the caller's write, so a decision that adds a memory rolls back with it. */
|
|
171
|
+
const addNow = (input) => {
|
|
172
|
+
const kind = oneOf(input.kind, MEMORY_KINDS, "a memory's kind");
|
|
173
|
+
if (input.scope === undefined || input.scope === null)
|
|
174
|
+
throw new CliError("validation_error", "a memory needs its scope — personal or work; there is no default", {
|
|
175
|
+
reason: "memory.scope-required",
|
|
207
176
|
});
|
|
208
|
-
|
|
209
|
-
|
|
177
|
+
const scope = oneOf(input.scope, MEMORY_SCOPES, "a memory's scope");
|
|
178
|
+
if (!Array.isArray(input.evidence) || input.evidence.length === 0)
|
|
179
|
+
throw new CliError("validation_error", "a memory needs at least one piece of evidence it rests on", {
|
|
180
|
+
reason: "memory.evidence-required",
|
|
181
|
+
});
|
|
182
|
+
const body = input.body?.trim() ?? "";
|
|
183
|
+
if (!body || body.length > 20_000)
|
|
184
|
+
throw new CliError("validation_error", "a memory's body takes 1–20000 characters");
|
|
185
|
+
if (input.confidence !== undefined && !(input.confidence >= 0 && input.confidence <= 1))
|
|
186
|
+
throw new CliError("validation_error", "confidence takes 0–1");
|
|
187
|
+
const status = oneOf(input.status ?? "proposed", ["proposed", "confirmed"], "a new memory's status");
|
|
188
|
+
if (status === "confirmed" && input.author !== "owner")
|
|
189
|
+
throw new CliError("permission_error", "an agent's memory waits as proposed; only the owner confirms it", {
|
|
190
|
+
reason: "proposal.owner-only",
|
|
191
|
+
});
|
|
192
|
+
const id = write(database, () => {
|
|
193
|
+
const { actor, model } = authorActor(context, input.author);
|
|
194
|
+
const subject = input.subject === undefined ? undefined : requiredThing(database, input.subject, "the subject");
|
|
195
|
+
const project = input.project === undefined ? null : projectId(database, input.project);
|
|
196
|
+
const replaced = input.supersedes === undefined ? undefined : Number(rowOf(input.supersedes).id);
|
|
197
|
+
const at = now();
|
|
198
|
+
const made = Number(database
|
|
199
|
+
.prepare("INSERT INTO memories (id, kind, body, subject_type, subject_id, author_type, author_id, model, confidence, status, " +
|
|
200
|
+
"last_verified_at, supersedes_id, scope, project_id, created_at, updated_at) " +
|
|
201
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id")
|
|
202
|
+
.get(nextId(database, "memories"), kind, body, subject?.type ?? null, subject?.id ?? null, actor.type, actor.id, model, input.confidence ?? null, status, status === "confirmed" ? at : null, replaced ?? null, scope, project, at, at)?.id);
|
|
203
|
+
linkEvidence(context, { type: "memory", id: made }, input.evidence, input.author === "owner" ? "owner" : "agent");
|
|
204
|
+
if (replaced !== undefined)
|
|
205
|
+
database.prepare("UPDATE memories SET status = 'superseded', updated_at = ? WHERE id = ?").run(at, replaced);
|
|
206
|
+
return made;
|
|
207
|
+
});
|
|
208
|
+
return memoryOf(rowOf(`memory:${id}`));
|
|
209
|
+
};
|
|
210
|
+
const confirmNow = (reference, by) => {
|
|
211
|
+
decided(reference, by);
|
|
212
|
+
return setStatus(reference, "confirmed", true);
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
addNow,
|
|
216
|
+
confirmNow,
|
|
217
|
+
add: async (input) => addNow(input),
|
|
210
218
|
get: async (reference) => memoryOf(rowOf(reference)),
|
|
211
219
|
list: async (options = {}) => {
|
|
212
220
|
const subject = options.subject === undefined ? undefined : requiredThing(database, options.subject);
|
|
@@ -225,10 +233,7 @@ export const memoriesStoreOver = (context) => {
|
|
|
225
233
|
database.prepare("UPDATE memories SET project_id = ?, updated_at = ? WHERE id = ?").run(project, now(), id);
|
|
226
234
|
return memoryOf(rowOf(`memory:${id}`));
|
|
227
235
|
}),
|
|
228
|
-
confirm: async (reference, by) =>
|
|
229
|
-
decided(reference, by);
|
|
230
|
-
return setStatus(reference, "confirmed", true);
|
|
231
|
-
},
|
|
236
|
+
confirm: async (reference, by) => confirmNow(reference, by),
|
|
232
237
|
markStale: async (reference, by) => {
|
|
233
238
|
decided(reference, by);
|
|
234
239
|
return setStatus(reference, "stale", false);
|
|
@@ -932,5 +932,21 @@ export const GENERATED = [
|
|
|
932
932
|
"CREATE TRIGGER facts_no_payment_bu BEFORE UPDATE OF type ON facts WHEN new.type = 'payment' BEGIN\n SELECT RAISE(ABORT, 'facts: a payment is a payments row in store.finance, not a fact; upgrade the CLI');\nEND;",
|
|
933
933
|
"INSERT OR REPLACE INTO store_settings (key, value, at) VALUES ('payment_facts_to_move', '1', unixepoch() * 1000);"
|
|
934
934
|
]
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
"name": "20261102192959_version-47-records",
|
|
938
|
+
"statements": [
|
|
939
|
+
"CREATE TABLE `record_relations` (\n\t`id` integer PRIMARY KEY,\n\t`record_id` integer NOT NULL,\n\t`role` text NOT NULL,\n\t`target_type` text NOT NULL,\n\t`target_external_id` text NOT NULL,\n\t`target_id` integer,\n\t`created_at` integer NOT NULL,\n\tCONSTRAINT `fk_record_relations_record_id_records_id_fk` FOREIGN KEY (`record_id`) REFERENCES `records`(`id`),\n\tCONSTRAINT `fk_record_relations_target_id_records_id_fk` FOREIGN KEY (`target_id`) REFERENCES `records`(`id`),\n\tCONSTRAINT `record_relations_record_id_role_target_type_target_external_id_unique` UNIQUE(`record_id`,`role`,`target_type`,`target_external_id`)\n);",
|
|
940
|
+
"CREATE TABLE `records` (\n\t`id` integer PRIMARY KEY,\n\t`account_id` integer NOT NULL,\n\t`type` text NOT NULL,\n\t`external_id` text NOT NULL,\n\t`fields` text NOT NULL,\n\t`revision` integer,\n\t`fingerprint` text NOT NULL,\n\t`entity_type` text,\n\t`entity_id` integer,\n\t`projected_fingerprint` text,\n\t`batch_id` text NOT NULL,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL,\n\t`deleted_at` integer,\n\tCONSTRAINT `fk_records_account_id_accounts_id_fk` FOREIGN KEY (`account_id`) REFERENCES `accounts`(`id`),\n\tCONSTRAINT `records_account_id_type_external_id_unique` UNIQUE(`account_id`,`type`,`external_id`)\n);",
|
|
941
|
+
"CREATE INDEX `record_relations_by_target_type_target_external_id` ON `record_relations` (`target_type`,`target_external_id`);",
|
|
942
|
+
"CREATE INDEX `record_relations_by_target_id` ON `record_relations` (`target_id`);",
|
|
943
|
+
"CREATE INDEX `records_by_entity_type_entity_id` ON `records` (`entity_type`,`entity_id`);"
|
|
944
|
+
]
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
"name": "20261102222340_version-48-test-accounts",
|
|
948
|
+
"statements": [
|
|
949
|
+
"-- Tests wrote the fake `chat` messenger of src/testing/shared-program.ts into real stores until a test could no longer\n-- open one (#2438). Deleting an account is the store's own purge, in code (every child row, the people only it saw,\n-- the vectors only it held), so the next writable open removes every `chat` account and deletes this key\n-- (purgeTestAccounts). A store without one gets no key, so a test's own `chat` rows written after this stay.\nINSERT OR REPLACE INTO store_settings (key, value, at)\nSELECT 'test_accounts_to_purge', '1', unixepoch() * 1000 WHERE EXISTS (SELECT 1 FROM accounts WHERE provider = 'chat');"
|
|
950
|
+
]
|
|
935
951
|
}
|
|
936
952
|
];
|
|
@@ -12,7 +12,7 @@ export interface StoreOptions {
|
|
|
12
12
|
version?: string;
|
|
13
13
|
/** Where the notice of an upgrade that locks other CLIs out goes; stderr, since stdout may carry JSON or MCP. */
|
|
14
14
|
warn?: (line: string) => void;
|
|
15
|
-
/** The host's decision policy (its `agent_decisions`);
|
|
15
|
+
/** The host's decision policy (its `agent_decisions`); off unless the host passes `agentDecisions: true`. */
|
|
16
16
|
decisions?: DecisionPolicy;
|
|
17
17
|
}
|
|
18
18
|
export declare const BACKFILL_ON_OPEN = 5000;
|
|
@@ -4,7 +4,7 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
4
4
|
import { DEFAULT_DECISION_POLICY } from "../decisions.js";
|
|
5
5
|
import { migrate } from "../migrations.js";
|
|
6
6
|
import { storeCapable } from "../open.js";
|
|
7
|
-
import { storePath } from "../path.js";
|
|
7
|
+
import { refuseOwnerStoreInTests, storePath } from "../path.js";
|
|
8
8
|
import { sharedConfigPath } from "../shared-config.js";
|
|
9
9
|
import { backfillNormalized, pendingNormalization } from "./backfill.js";
|
|
10
10
|
import { fillFiles } from "./files.js";
|
|
@@ -18,6 +18,7 @@ import { assertReadCompatible } from "./read-compatibility.js";
|
|
|
18
18
|
import { stemsState } from "./stems.js";
|
|
19
19
|
import { fillStems, followDefaultStemmers } from "./stems-fill.js";
|
|
20
20
|
import { moveSuggestedLinks } from "./suggested-links.js";
|
|
21
|
+
import { purgeTestAccounts } from "./test-accounts.js";
|
|
21
22
|
import { fillVectorIndexes } from "./vector-index.js";
|
|
22
23
|
export const BACKFILL_ON_OPEN = 5_000;
|
|
23
24
|
const NOT_WRITABLE = new Set(["EACCES", "EPERM", "EROFS"]);
|
|
@@ -39,6 +40,7 @@ export const openStoreContext = async ({ path, env, now = Date.now, command, ver
|
|
|
39
40
|
throw new CliError("validation_error", "readOnly must be a boolean");
|
|
40
41
|
await storeCapable();
|
|
41
42
|
const file = path ?? storePath(env);
|
|
43
|
+
refuseOwnerStoreInTests(file);
|
|
42
44
|
if (readOnly) {
|
|
43
45
|
if (!statSync(file, { throwIfNoEntry: false })?.isFile())
|
|
44
46
|
throw new CliError("not_found", "The store does not exist as a file; read-only opening does not create it");
|
|
@@ -59,6 +61,7 @@ export const openStoreContext = async ({ path, env, now = Date.now, command, ver
|
|
|
59
61
|
fillJoinKeys(database);
|
|
60
62
|
moveSuggestedLinks(database, now);
|
|
61
63
|
movePaymentFacts({ ...opened, now, decisions });
|
|
64
|
+
purgeTestAccounts({ ...opened, now, decisions }, warn);
|
|
62
65
|
const pending = pendingNormalization(database);
|
|
63
66
|
if (pending > 0 && pending <= BACKFILL_ON_OPEN)
|
|
64
67
|
backfillNormalized(database);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// biome-ignore-all lint/plugin/no-sqlite-driver: cli-core owns the store, so it opens the driver itself
|
|
2
1
|
import { PRAGMAS } from "../driver.js";
|
|
2
|
+
import { refuseOwnerStoreInTests } from "../path.js";
|
|
3
3
|
import { quietSqliteWarning } from "../sqlite-warning.js";
|
|
4
4
|
/**
|
|
5
5
|
* One SQLite connection, seen two ways: the `CacheDatabase` seam the hand-written SQL and the
|
|
@@ -8,6 +8,7 @@ import { quietSqliteWarning } from "../sqlite-warning.js";
|
|
|
8
8
|
* `../open.ts` for why a static import breaks the other runtime.
|
|
9
9
|
*/
|
|
10
10
|
export const openSqlite = async (path, { readOnly = false } = {}) => {
|
|
11
|
+
refuseOwnerStoreInTests(path);
|
|
11
12
|
const opened = "Bun" in globalThis ? await openUnderBun(path, readOnly) : await openUnderNode(path, readOnly);
|
|
12
13
|
try {
|
|
13
14
|
opened.database.exec(readOnly ? "PRAGMA busy_timeout=5000; PRAGMA foreign_keys=ON; PRAGMA query_only=ON" : PRAGMAS);
|
|
@@ -2,6 +2,7 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
2
2
|
import { MEETING_FINDINGS_KIND } from "../../meetings/index.js";
|
|
3
3
|
import { assertMayDecide, deciderAuthor } from "../decisions.js";
|
|
4
4
|
import { EVIDENCE_FINDINGS_KIND } from "../evidence-findings.js";
|
|
5
|
+
import { MAIL_FINDINGS_KIND } from "../mail-findings.js";
|
|
5
6
|
import { proposedBySql } from "./actors.js";
|
|
6
7
|
import { authorActor } from "./memories.js";
|
|
7
8
|
import { requiredThing } from "./notes.js";
|
|
@@ -15,7 +16,7 @@ const json = (value) => (value == null ? null : JSON.parse(String(value)));
|
|
|
15
16
|
* A findings batch closed with nothing accepted is stored `approved`, as older builds read it; it reads as
|
|
16
17
|
* `rejected`, the decision the owner made.
|
|
17
18
|
*/
|
|
18
|
-
const STATUS = `CASE WHEN p.status = 'approved' AND p.kind IN ('${MEETING_FINDINGS_KIND}', '${EVIDENCE_FINDINGS_KIND}') ` +
|
|
19
|
+
const STATUS = `CASE WHEN p.status = 'approved' AND p.kind IN ('${MEETING_FINDINGS_KIND}', '${EVIDENCE_FINDINGS_KIND}', '${MAIL_FINDINGS_KIND}') ` +
|
|
19
20
|
"AND json_array_length(p.result, '$.accepted') = 0 THEN 'rejected' ELSE p.status END";
|
|
20
21
|
const NAMED = `SELECT p.*, ${STATUS} AS shown_status, b.name AS proposer_name, b.kind AS proposer_kind FROM proposed_actions p ` +
|
|
21
22
|
"LEFT JOIN bots b ON p.proposed_by_type = 'bot' AND b.id = p.proposed_by_id";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type EnvelopeRecord, type EnvelopeSource, type IngestError } from "../../domain/records.js";
|
|
2
|
+
import type { Author } from "./memories.js";
|
|
3
|
+
import type { StoreContext } from "./open.js";
|
|
4
|
+
export interface RecordKey {
|
|
5
|
+
provider: string;
|
|
6
|
+
account: string;
|
|
7
|
+
type: string;
|
|
8
|
+
externalId: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SourceRecord extends RecordKey {
|
|
11
|
+
fields: Record<string, unknown>;
|
|
12
|
+
revision: number | null;
|
|
13
|
+
fingerprint: string;
|
|
14
|
+
relations: {
|
|
15
|
+
role: string;
|
|
16
|
+
type: string;
|
|
17
|
+
externalId: string;
|
|
18
|
+
resolved: boolean;
|
|
19
|
+
}[];
|
|
20
|
+
/** The entity made from it or mapped to it by the owner; `null` while it waits or for a type without a table. */
|
|
21
|
+
entity: string | null;
|
|
22
|
+
/** The source changed or deleted it after its entity was made: the owner should look at the entity. */
|
|
23
|
+
flagged: boolean;
|
|
24
|
+
batchId: string;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
updatedAt: string;
|
|
27
|
+
deletedAt: string | null;
|
|
28
|
+
}
|
|
29
|
+
export interface RecordOutcome {
|
|
30
|
+
/** The record's place in the envelope's `records`. */
|
|
31
|
+
index: number;
|
|
32
|
+
key: string;
|
|
33
|
+
status: "created" | "updated" | "unchanged" | "deleted" | "refused";
|
|
34
|
+
error?: IngestError;
|
|
35
|
+
}
|
|
36
|
+
export interface RecordsWritten {
|
|
37
|
+
outcomes: RecordOutcome[];
|
|
38
|
+
/** `invoice:` and `payment:` refs this import proposed, also for records of earlier imports it completed. */
|
|
39
|
+
proposed: string[];
|
|
40
|
+
/** Kept, with no entity yet: the roles whose target is not imported or has no entity. */
|
|
41
|
+
waiting: {
|
|
42
|
+
index: number;
|
|
43
|
+
key: string;
|
|
44
|
+
missing: string[];
|
|
45
|
+
}[];
|
|
46
|
+
/** Changed or deleted at the source after its entity was made. */
|
|
47
|
+
flagged: {
|
|
48
|
+
index: number;
|
|
49
|
+
key: string;
|
|
50
|
+
entity: string;
|
|
51
|
+
}[];
|
|
52
|
+
/** A record of an earlier import this one completed, whose entity was still refused. */
|
|
53
|
+
laterErrors: IngestError[];
|
|
54
|
+
}
|
|
55
|
+
export interface RecordStore {
|
|
56
|
+
/** Writes validated records of one source, per chunk in one transaction; a dry run rolls everything back. */
|
|
57
|
+
write(source: EnvelopeSource, records: {
|
|
58
|
+
index: number;
|
|
59
|
+
record: EnvelopeRecord;
|
|
60
|
+
}[], options?: {
|
|
61
|
+
dryRun?: boolean;
|
|
62
|
+
chunk?: number;
|
|
63
|
+
}): Promise<RecordsWritten>;
|
|
64
|
+
get(key: RecordKey): Promise<SourceRecord | undefined>;
|
|
65
|
+
list(filter: {
|
|
66
|
+
provider: string;
|
|
67
|
+
account: string;
|
|
68
|
+
type?: string;
|
|
69
|
+
/** Only records whose entity waits for a reference. */
|
|
70
|
+
waiting?: boolean;
|
|
71
|
+
flagged?: boolean;
|
|
72
|
+
/** 1–500, 100 by default. */
|
|
73
|
+
limit?: number;
|
|
74
|
+
}): Promise<SourceRecord[]>;
|
|
75
|
+
/**
|
|
76
|
+
* The owner says which entity a record is: `person:4` for a payer a sheet names. Never done by name. Records that
|
|
77
|
+
* waited for it get their entity, as proposals.
|
|
78
|
+
*/
|
|
79
|
+
map(key: RecordKey, entity: string, by: Author): Promise<SourceRecord>;
|
|
80
|
+
}
|
|
81
|
+
export declare const recordStoreOver: (context: StoreContext) => RecordStore;
|