@wirecat/cli-core 0.546.0 → 0.548.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 +173 -0
- package/dist/calendar/cli/answers.d.ts +7 -2
- package/dist/calendar/cli/answers.js +12 -4
- package/dist/calendar/cli/index.js +9 -8
- package/dist/calendar/feeds.d.ts +19 -0
- package/dist/calendar/feeds.js +52 -9
- package/dist/calendar/source.d.ts +10 -2
- package/dist/calendar/sync.d.ts +2 -0
- package/dist/calendar/sync.js +10 -6
- package/dist/calendar/testing/memory.js +39 -0
- package/dist/cli/messenger/history-query.d.ts +10 -4
- package/dist/cli/messenger/history-query.js +87 -31
- package/dist/cli/messenger/messages-command.js +10 -4
- package/dist/cli/messenger/messages-comments-command.js +4 -2
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/option-hints.d.ts +6 -0
- package/dist/cli/option-hints.js +15 -0
- package/dist/errors/reasons.js +13 -1
- package/dist/mail/cli/index.js +4 -4
- package/dist/mail/index.d.ts +2 -0
- package/dist/mail/index.js +2 -0
- package/dist/mail/partial.d.ts +34 -0
- package/dist/mail/partial.js +94 -0
- package/dist/mail/server-search.js +17 -9
- package/dist/mail/source.d.ts +13 -2
- package/dist/mail/source.js +7 -1
- package/dist/mail/sync-types.d.ts +112 -0
- package/dist/mail/sync-types.js +1 -0
- package/dist/mail/sync.d.ts +4 -106
- package/dist/mail/sync.js +49 -4
- package/dist/mail/testing/fake.d.ts +3 -1
- package/dist/mail/testing/fake.js +10 -2
- package/dist/meetings/event-store.d.ts +10 -0
- package/dist/meetings/testing/event-store-contract.js +36 -0
- package/dist/services/deps.d.ts +0 -2
- package/dist/services/deps.js +0 -2
- package/dist/services/documents/ingestion.js +1 -1
- package/dist/services/documents/knowledge.d.ts +10 -6
- package/dist/services/documents/knowledge.js +25 -11
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/messages.d.ts +2 -0
- package/dist/services/messages.js +25 -23
- package/dist/services/proposal-tasks.d.ts +3 -2
- package/dist/services/proposal-tasks.js +5 -2
- package/dist/services/proposals/ids.d.ts +5 -0
- package/dist/services/proposals/ids.js +43 -0
- package/dist/services/proposals/index.d.ts +3 -0
- package/dist/services/proposals/index.js +2 -0
- package/dist/services/proposals/kinds/action.d.ts +7 -0
- package/dist/services/proposals/kinds/action.js +45 -0
- package/dist/services/proposals/kinds/fact.d.ts +4 -0
- package/dist/services/proposals/kinds/fact.js +43 -0
- package/dist/services/proposals/kinds/findings.d.ts +7 -0
- package/dist/services/proposals/kinds/findings.js +75 -0
- package/dist/services/proposals/kinds/key-document.d.ts +9 -0
- package/dist/services/proposals/kinds/key-document.js +75 -0
- package/dist/services/proposals/kinds/memory.d.ts +4 -0
- package/dist/services/proposals/kinds/memory.js +42 -0
- package/dist/services/proposals/kinds/relationship.d.ts +10 -0
- package/dist/services/proposals/kinds/relationship.js +82 -0
- package/dist/services/proposals/kinds/shared.d.ts +25 -0
- package/dist/services/proposals/kinds/shared.js +49 -0
- package/dist/services/proposals/registry.d.ts +9 -0
- package/dist/services/proposals/registry.js +129 -0
- package/dist/services/proposals/types.d.ts +85 -0
- package/dist/services/proposals/types.js +1 -0
- package/dist/services/server-search.d.ts +7 -4
- package/dist/services/server-search.js +5 -2
- package/dist/services/tasks.js +8 -4
- package/dist/store/decisions.d.ts +65 -0
- package/dist/store/decisions.js +55 -0
- package/dist/store/evidence-findings.d.ts +6 -6
- package/dist/store/index.d.ts +4 -3
- package/dist/store/index.js +2 -1
- package/dist/store/meeting-findings.d.ts +2 -0
- package/dist/store/messages.d.ts +4 -9
- package/dist/store/sqlite/accounts.d.ts +8 -0
- package/dist/store/sqlite/accounts.js +2 -2
- package/dist/store/sqlite/actors.d.ts +13 -2
- package/dist/store/sqlite/actors.js +24 -1
- package/dist/store/sqlite/calendar-forget.d.ts +17 -0
- package/dist/store/sqlite/calendar-forget.js +68 -0
- package/dist/store/sqlite/calendar.js +8 -0
- package/dist/store/sqlite/email-locations.d.ts +16 -0
- package/dist/store/sqlite/email-locations.js +26 -0
- package/dist/store/sqlite/emails.d.ts +6 -2
- package/dist/store/sqlite/emails.js +2 -1
- package/dist/store/sqlite/evidence-findings.js +13 -3
- package/dist/store/sqlite/facts.d.ts +16 -8
- package/dist/store/sqlite/facts.js +48 -15
- package/dist/store/sqlite/findings-core.d.ts +2 -1
- package/dist/store/sqlite/findings-core.js +4 -4
- package/dist/store/sqlite/knowledge.d.ts +43 -7
- package/dist/store/sqlite/knowledge.js +55 -15
- package/dist/store/sqlite/meeting-findings.js +6 -6
- package/dist/store/sqlite/memories.d.ts +12 -8
- package/dist/store/sqlite/memories.js +33 -10
- package/dist/store/sqlite/notes.d.ts +5 -1
- package/dist/store/sqlite/notes.js +18 -2
- package/dist/store/sqlite/open-store.d.ts +4 -1
- package/dist/store/sqlite/open-store.js +8 -2
- package/dist/store/sqlite/open.d.ts +3 -0
- package/dist/store/sqlite/proposed-actions.d.ts +21 -8
- package/dist/store/sqlite/proposed-actions.js +29 -9
- package/dist/store/sqlite/reads.d.ts +13 -2
- package/dist/store/sqlite/reads.js +8 -4
- package/dist/store/sqlite/relation-proposals.d.ts +34 -0
- package/dist/store/sqlite/relation-proposals.js +114 -0
- package/dist/store/store.d.ts +3 -0
- package/dist/store/store.js +2 -0
- package/package.json +1 -1
|
@@ -132,7 +132,7 @@ const referencesTo = (database, table, skip) => {
|
|
|
132
132
|
const pointers = pointerPairs(database).map(({ table: tbl, pointer }) => `SELECT ${pointer}_id AS id FROM "${tbl}" WHERE ${pointer}_type = '${type}'`);
|
|
133
133
|
return [...keys, ...pointers].join(" UNION ");
|
|
134
134
|
};
|
|
135
|
-
const unreferenced = (database, table) => new Set(database
|
|
135
|
+
export const unreferenced = (database, table) => new Set(database
|
|
136
136
|
.prepare(`SELECT id FROM ${table} WHERE id NOT IN (SELECT id FROM (${referencesTo(database, table, OWN)}) WHERE id IS NOT NULL)`)
|
|
137
137
|
.all()
|
|
138
138
|
.map(({ id }) => Number(id)));
|
|
@@ -141,7 +141,7 @@ const unreferenced = (database, table) => new Set(database
|
|
|
141
141
|
* (another account, a tag, a note, a link), or while its person is kept: the owner, a person something
|
|
142
142
|
* points at, or one linked to an identity that stays.
|
|
143
143
|
*/
|
|
144
|
-
const dropOrphanPeople = (database, unseenBefore) => {
|
|
144
|
+
export const dropOrphanPeople = (database, unseenBefore) => {
|
|
145
145
|
const orphans = [...unreferenced(database, "identities")].filter((id) => !unseenBefore.has(id));
|
|
146
146
|
if (orphans.length === 0)
|
|
147
147
|
return;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Proposer } from "../decisions.js";
|
|
1
2
|
import type { CacheDatabase } from "../driver.js";
|
|
2
3
|
/** Who made or owns something: the store's polymorphic actor pair. */
|
|
3
4
|
export interface Actor {
|
|
@@ -6,7 +7,7 @@ export interface Actor {
|
|
|
6
7
|
}
|
|
7
8
|
/** How the task package names who acted; each maps to one stable actor row. */
|
|
8
9
|
export type Origin = "owner" | "agent" | "rule";
|
|
9
|
-
declare const BOT_KINDS: readonly ["agent", "script", "integration"];
|
|
10
|
+
export declare const BOT_KINDS: readonly ["agent", "rule", "script", "integration"];
|
|
10
11
|
/** The owner's person row, which the initial migration seeds. */
|
|
11
12
|
export declare const ownerPerson: (database: CacheDatabase) => Actor;
|
|
12
13
|
/** A bot by its unique handle, registered on first use. */
|
|
@@ -17,4 +18,14 @@ export declare const botNamed: (database: CacheDatabase, name: string, now: numb
|
|
|
17
18
|
*/
|
|
18
19
|
export declare const actorOfOrigin: (database: CacheDatabase, origin: Origin) => Actor;
|
|
19
20
|
export declare const originOfActor: (database: CacheDatabase, type: unknown, id: unknown) => Origin | undefined;
|
|
20
|
-
|
|
21
|
+
/** Who proposed a stored row, from its actor pair; the seeded bot `rule` and a bot of kind `rule` are rules. */
|
|
22
|
+
export declare const proposerOf: (database: CacheDatabase, type: unknown, id: unknown) => Proposer;
|
|
23
|
+
/** Who proposed something, as a list filter: any agent, any rule, or one bot by its handle. */
|
|
24
|
+
export type ProposedByFilter = "agent" | "rule" | {
|
|
25
|
+
bot: string;
|
|
26
|
+
};
|
|
27
|
+
/** The SQL that keeps rows whose actor pair matches `by`; the seeded bot `rule` counts as a rule. */
|
|
28
|
+
export declare const proposedBySql: (type: string, id: string, by: ProposedByFilter | undefined) => {
|
|
29
|
+
sql: string;
|
|
30
|
+
params: string[];
|
|
31
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
const BOT_KINDS = ["agent", "script", "integration"];
|
|
2
|
+
export const BOT_KINDS = ["agent", "rule", "script", "integration"];
|
|
3
3
|
/** The owner's person row, which the initial migration seeds. */
|
|
4
4
|
export const ownerPerson = (database) => {
|
|
5
5
|
const found = database.prepare("SELECT id FROM persons WHERE owner = 1 ORDER BY id LIMIT 1").get();
|
|
@@ -41,3 +41,26 @@ export const originOfActor = (database, type, id) => {
|
|
|
41
41
|
return "owner";
|
|
42
42
|
return database.prepare("SELECT name FROM bots WHERE id = ?").get(Number(id))?.name === "rule" ? "rule" : "agent";
|
|
43
43
|
};
|
|
44
|
+
/** Who proposed a stored row, from its actor pair; the seeded bot `rule` and a bot of kind `rule` are rules. */
|
|
45
|
+
export const proposerOf = (database, type, id) => {
|
|
46
|
+
if (type === "person" && id != null)
|
|
47
|
+
return { type: "person", id: String(id) };
|
|
48
|
+
if (type !== "bot" || id == null)
|
|
49
|
+
return { type: "unknown" };
|
|
50
|
+
const bot = database.prepare("SELECT name, kind FROM bots WHERE id = ?").get(Number(id));
|
|
51
|
+
if (!bot)
|
|
52
|
+
return { type: "unknown" };
|
|
53
|
+
const name = String(bot.name);
|
|
54
|
+
return { type: "bot", id: String(id), name, kind: bot.kind === "rule" || name === "rule" ? "rule" : "agent" };
|
|
55
|
+
};
|
|
56
|
+
/** The SQL that keeps rows whose actor pair matches `by`; the seeded bot `rule` counts as a rule. */
|
|
57
|
+
export const proposedBySql = (type, id, by) => {
|
|
58
|
+
if (by === undefined)
|
|
59
|
+
return { sql: "1", params: [] };
|
|
60
|
+
const bot = (condition) => `${type} = 'bot' AND EXISTS (SELECT 1 FROM bots pb WHERE pb.id = ${id} AND ${condition})`;
|
|
61
|
+
if (by === "rule")
|
|
62
|
+
return { sql: bot("(pb.kind = 'rule' OR pb.name = 'rule')"), params: [] };
|
|
63
|
+
if (by === "agent")
|
|
64
|
+
return { sql: bot("pb.kind <> 'rule' AND pb.name <> 'rule'"), params: [] };
|
|
65
|
+
return { sql: bot("pb.name = ?"), params: [by.bot] };
|
|
66
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CacheDatabase } from "../driver.js";
|
|
2
|
+
/**
|
|
3
|
+
* `EventStore.forgetCalendar` inside its transaction: children before parents, since the foreign keys are enforced.
|
|
4
|
+
* `refresh` gives a kept event or series its fields again from the copies left.
|
|
5
|
+
*/
|
|
6
|
+
export declare const forgetCalendarRows: (database: CacheDatabase, { calendarId, accountId, cursorKey, now }: {
|
|
7
|
+
calendarId: number;
|
|
8
|
+
accountId: number;
|
|
9
|
+
cursorKey: string;
|
|
10
|
+
now: number;
|
|
11
|
+
}, refresh: {
|
|
12
|
+
event: (id: number, now: number) => void;
|
|
13
|
+
series: (id: number, now: number) => void;
|
|
14
|
+
}) => {
|
|
15
|
+
entries: number;
|
|
16
|
+
events: number;
|
|
17
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { dropOrphanPeople, unreferenced } from "./accounts.js";
|
|
2
|
+
import { dropUnusedVectors } from "./vector-index.js";
|
|
3
|
+
const distinctIds = (rows) => [...new Set(rows.map((row) => Number(row.id)))];
|
|
4
|
+
/**
|
|
5
|
+
* `EventStore.forgetCalendar` inside its transaction: children before parents, since the foreign keys are enforced.
|
|
6
|
+
* `refresh` gives a kept event or series its fields again from the copies left.
|
|
7
|
+
*/
|
|
8
|
+
export const forgetCalendarRows = (database, { calendarId, accountId, cursorKey, now }, refresh) => {
|
|
9
|
+
const unseenBefore = unreferenced(database, "identities");
|
|
10
|
+
const ids = (sql) => distinctIds(database.prepare(sql).all(calendarId));
|
|
11
|
+
const entryIds = ids("SELECT id FROM calendar_entries WHERE calendar_id = ?");
|
|
12
|
+
const eventIds = ids("SELECT event_id AS id FROM calendar_entries WHERE calendar_id = ? AND event_id IS NOT NULL");
|
|
13
|
+
const seriesIds = ids("SELECT event_series_id AS id FROM calendar_entries WHERE calendar_id = ? AND event_series_id IS NOT NULL");
|
|
14
|
+
const hashes = [];
|
|
15
|
+
const dropAround = (type, list) => {
|
|
16
|
+
const json = JSON.stringify(list);
|
|
17
|
+
const inList = "IN (SELECT value FROM json_each(?))";
|
|
18
|
+
for (const row of database
|
|
19
|
+
.prepare(`SELECT DISTINCT content_hash FROM chunks WHERE chunkable_type = ? AND chunkable_id ${inList}`)
|
|
20
|
+
.all(type, json))
|
|
21
|
+
hashes.push(String(row.content_hash));
|
|
22
|
+
database.prepare(`DELETE FROM chunks WHERE chunkable_type = ? AND chunkable_id ${inList}`).run(type, json);
|
|
23
|
+
database
|
|
24
|
+
.prepare(`DELETE FROM links WHERE (from_type = ? AND from_id ${inList}) OR (to_type = ? AND to_id ${inList})`)
|
|
25
|
+
.run(type, json, type, json);
|
|
26
|
+
for (const [table, pointer] of [
|
|
27
|
+
["taggings", "taggable"],
|
|
28
|
+
["aliases", "aliasable"],
|
|
29
|
+
["attachments", "attachable"],
|
|
30
|
+
["link_reviews", "linkable"],
|
|
31
|
+
["entity_link_events", "linkable"],
|
|
32
|
+
])
|
|
33
|
+
database.prepare(`DELETE FROM ${table} WHERE ${pointer}_type = ? AND ${pointer}_id ${inList}`).run(type, json);
|
|
34
|
+
};
|
|
35
|
+
const entries = JSON.stringify(entryIds);
|
|
36
|
+
for (const table of ["calendar_entry_participants", "calendar_entry_revisions"])
|
|
37
|
+
database.prepare(`DELETE FROM ${table} WHERE entry_id IN (SELECT value FROM json_each(?))`).run(entries);
|
|
38
|
+
dropAround("calendar_entry", entryIds);
|
|
39
|
+
// Occurrences point at their series in the same table: the pointers go first, so the order of rows is free.
|
|
40
|
+
database.prepare("UPDATE calendar_entries SET series_entry_id = NULL WHERE calendar_id = ?").run(calendarId);
|
|
41
|
+
database.prepare("DELETE FROM calendar_entries WHERE calendar_id = ?").run(calendarId);
|
|
42
|
+
const held = (column, id) => database.prepare(`SELECT 1 FROM calendar_entries WHERE ${column} = ? LIMIT 1`).get(id) !== undefined;
|
|
43
|
+
const goneSeries = seriesIds.filter((id) => !held("event_series_id", id));
|
|
44
|
+
const goneEvents = eventIds.filter((id) => !held("event_id", id));
|
|
45
|
+
const gone = (list) => JSON.stringify(list);
|
|
46
|
+
database
|
|
47
|
+
.prepare("UPDATE meetings SET event_id = NULL, updated_at = ? WHERE event_id IN (SELECT value FROM json_each(?))")
|
|
48
|
+
.run(now, gone(goneEvents));
|
|
49
|
+
dropAround("event", goneEvents);
|
|
50
|
+
database.prepare("DELETE FROM events WHERE id IN (SELECT value FROM json_each(?))").run(gone(goneEvents));
|
|
51
|
+
for (const table of ["meeting_series", "events"])
|
|
52
|
+
database
|
|
53
|
+
.prepare(`UPDATE ${table} SET event_series_id = NULL, updated_at = ? WHERE event_series_id IN (SELECT value FROM json_each(?))`)
|
|
54
|
+
.run(now, gone(goneSeries));
|
|
55
|
+
dropAround("event_series", goneSeries);
|
|
56
|
+
database.prepare("DELETE FROM event_series WHERE id IN (SELECT value FROM json_each(?))").run(gone(goneSeries));
|
|
57
|
+
for (const id of seriesIds)
|
|
58
|
+
if (!goneSeries.includes(id))
|
|
59
|
+
refresh.series(id, now);
|
|
60
|
+
for (const id of eventIds)
|
|
61
|
+
if (!goneEvents.includes(id))
|
|
62
|
+
refresh.event(id, now);
|
|
63
|
+
database.prepare("DELETE FROM sync_cursors WHERE account_id = ? AND key = ?").run(accountId, cursorKey);
|
|
64
|
+
database.prepare("DELETE FROM calendars WHERE id = ?").run(calendarId);
|
|
65
|
+
dropOrphanPeople(database, unseenBefore);
|
|
66
|
+
dropUnusedVectors(database, hashes);
|
|
67
|
+
return { entries: entryIds.length, events: goneEvents.length };
|
|
68
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { calendarText, entryInWindow, entryMetadata, joinKeyOf, joinUrlWithoutQuery, } from "../../meetings/index.js";
|
|
3
3
|
import { listEvents } from "./calendar-events.js";
|
|
4
|
+
import { forgetCalendarRows } from "./calendar-forget.js";
|
|
4
5
|
import { bool, eventOf, fromJson, int, str, toJson } from "./events.js";
|
|
5
6
|
import { driveDocumentOf, driveFileTarget, logFileLink } from "./files.js";
|
|
6
7
|
import { identityOf } from "./identities.js";
|
|
@@ -524,6 +525,13 @@ export const eventStoreOver = (context) => {
|
|
|
524
525
|
.prepare("UPDATE calendars SET synced = ?, updated_at = ? WHERE id = ?")
|
|
525
526
|
.run(flag(synced), now, calendarId);
|
|
526
527
|
}),
|
|
528
|
+
forgetCalendar: async (calendarId, now) => write(database, () => {
|
|
529
|
+
const accountId = Number(calendarRow(calendarId).account_id);
|
|
530
|
+
return forgetCalendarRows(database, { calendarId, accountId, cursorKey: cursorKey(calendarId), now }, {
|
|
531
|
+
event: refreshEvent,
|
|
532
|
+
series: refreshSeries,
|
|
533
|
+
});
|
|
534
|
+
}),
|
|
527
535
|
saveEntries: async (calendarId, entries, now) => write(database, () => saveAll(calendarId, entries, now)),
|
|
528
536
|
removeEntries: async (calendarId, externalIds, now) => write(database, () => removeAll(calendarId, externalIds, now)),
|
|
529
537
|
markUnseen: async (calendarId, seenBefore, window, now) => write(database, () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Metadata } from "../../meetings/index.js";
|
|
1
2
|
import type { CacheDatabase } from "../driver.js";
|
|
2
3
|
/** Where an IMAP server holds an email: `folder` is the mailbox's external id. */
|
|
3
4
|
export interface EmailLocationInput {
|
|
@@ -20,5 +21,20 @@ export declare const locate: (database: CacheDatabase, accountId: number, emailI
|
|
|
20
21
|
changes: number;
|
|
21
22
|
};
|
|
22
23
|
export declare const folderEmails: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string) => FolderEmail[];
|
|
24
|
+
/**
|
|
25
|
+
* An email stored from its first bytes only (`metadata.partial`): a sync reads it whole and clears the mark.
|
|
26
|
+
* `partialEmails` lists an account's live ones newest first, with how many there are in all.
|
|
27
|
+
*/
|
|
28
|
+
export interface PartialEmail {
|
|
29
|
+
externalId: string;
|
|
30
|
+
threadExternalId: string;
|
|
31
|
+
receivedAt: number | null;
|
|
32
|
+
metadata: Metadata;
|
|
33
|
+
locations: EmailLocationInput[];
|
|
34
|
+
}
|
|
35
|
+
export declare const partialEmails: (database: CacheDatabase, accountId: number, limit: number) => {
|
|
36
|
+
emails: PartialEmail[];
|
|
37
|
+
total: number;
|
|
38
|
+
};
|
|
23
39
|
export declare const setLocations: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string, found: ReadonlyMap<string, number[]>) => number;
|
|
24
40
|
export declare const forgetLocations: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string, uids: number[]) => void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fromJson } from "./events.js";
|
|
1
2
|
export const locate = (database, accountId, emailId, where) => database
|
|
2
3
|
.prepare(`INSERT OR REPLACE INTO email_locations (mailbox_id, uidvalidity, uid, email_id)
|
|
3
4
|
SELECT id, ?, ?, ? FROM mailboxes WHERE account_id = ? AND external_id = ?`)
|
|
@@ -21,6 +22,31 @@ export const folderEmails = (database, accountId, folder, uidvalidity) => databa
|
|
|
21
22
|
locations: Number(row.locations),
|
|
22
23
|
folders: Number(row.folders),
|
|
23
24
|
}));
|
|
25
|
+
export const partialEmails = (database, accountId, limit) => {
|
|
26
|
+
const rows = database
|
|
27
|
+
.prepare(`SELECT e.external_id, t.external_id AS thread, coalesce(e.received_at, e.sent_at) AS at, e.metadata,
|
|
28
|
+
count(*) OVER () AS total,
|
|
29
|
+
(SELECT json_group_array(json_object('folder', b.external_id, 'uidvalidity', l.uidvalidity, 'uid', l.uid))
|
|
30
|
+
FROM email_locations l JOIN mailboxes b ON b.id = l.mailbox_id WHERE l.email_id = e.id) AS places
|
|
31
|
+
FROM emails e JOIN email_threads t ON t.id = e.email_thread_id
|
|
32
|
+
WHERE e.account_id = ? AND e.deleted_at IS NULL AND json_extract(e.metadata, '$.partial') IS NOT NULL
|
|
33
|
+
ORDER BY at DESC, e.id DESC LIMIT ?`)
|
|
34
|
+
.all(accountId, limit);
|
|
35
|
+
return {
|
|
36
|
+
total: rows.length ? Number(rows[0]?.total) : 0,
|
|
37
|
+
emails: rows.map((row) => ({
|
|
38
|
+
externalId: String(row.external_id),
|
|
39
|
+
threadExternalId: String(row.thread),
|
|
40
|
+
receivedAt: row.at === null ? null : Number(row.at),
|
|
41
|
+
metadata: fromJson(row.metadata),
|
|
42
|
+
locations: JSON.parse(String(row.places)).map((place) => ({
|
|
43
|
+
folder: String(place.folder),
|
|
44
|
+
uidvalidity: String(place.uidvalidity),
|
|
45
|
+
uid: Number(place.uid),
|
|
46
|
+
})),
|
|
47
|
+
})),
|
|
48
|
+
};
|
|
49
|
+
};
|
|
24
50
|
export const setLocations = (database, accountId, folder, uidvalidity, found) => {
|
|
25
51
|
database
|
|
26
52
|
.prepare(`DELETE FROM email_locations WHERE uidvalidity <> ?
|
|
@@ -3,13 +3,13 @@ import type { Attachment, Metadata } from "../../meetings/index.js";
|
|
|
3
3
|
import type { Stemmer, Stemmers } from "../../search/stem.js";
|
|
4
4
|
import type { CacheDatabase } from "../driver.js";
|
|
5
5
|
import type { ConversationSummary, StoredHit } from "../types.js";
|
|
6
|
-
import { type EmailLocationInput, type FolderEmail } from "./email-locations.js";
|
|
6
|
+
import { type EmailLocationInput, type FolderEmail, type PartialEmail } from "./email-locations.js";
|
|
7
7
|
import { type MailRulesStore } from "./mail-rules.js";
|
|
8
8
|
import { type AttachmentInput } from "./meetings.js";
|
|
9
9
|
import type { StoreContext } from "./open.js";
|
|
10
10
|
import { type SyncStateSave } from "./sync.js";
|
|
11
11
|
import { type ThreadFilter } from "./thread-columns.js";
|
|
12
|
-
export type { EmailLocationInput, FolderEmail };
|
|
12
|
+
export type { EmailLocationInput, FolderEmail, PartialEmail };
|
|
13
13
|
export interface EmailAddress {
|
|
14
14
|
address: string;
|
|
15
15
|
name: string | null;
|
|
@@ -217,6 +217,10 @@ export interface MailStore {
|
|
|
217
217
|
emails(filter?: EmailFilter): Promise<Email[]>;
|
|
218
218
|
/** Each matching email's Message-ID and mailboxes, without text or people: cheap over a whole mailbox. */
|
|
219
219
|
emailRefs(filter?: Omit<EmailFilter, "limit" | "offset">): Promise<EmailRef[]>;
|
|
220
|
+
partialEmails(accountId: number, limit: number): Promise<{
|
|
221
|
+
emails: PartialEmail[];
|
|
222
|
+
total: number;
|
|
223
|
+
}>;
|
|
220
224
|
/** Each matching email's sender, recipients, list, size, flag and mailboxes, oldest first; no text. */
|
|
221
225
|
emailFacts(filter?: Omit<EmailFilter, "limit" | "offset">): Promise<EmailFacts[]>;
|
|
222
226
|
/**
|
|
@@ -2,7 +2,7 @@ import { formatLocator } from "../../domain/locator.js";
|
|
|
2
2
|
import { formatReference } from "../../domain/references.js";
|
|
3
3
|
import { fold, normalize } from "../normalize.js";
|
|
4
4
|
import { chunkAttachments, drainEmailIndex, dropAttachmentChunks, MAIL_PIECES, queueThreads, threadPieces, titleOf, } from "./email-index.js";
|
|
5
|
-
import { folderEmails, forgetLocations, locate, setLocations, } from "./email-locations.js";
|
|
5
|
+
import { folderEmails, forgetLocations, locate, partialEmails, setLocations, } from "./email-locations.js";
|
|
6
6
|
import { markEmailsDeleted, markEmailsRestored, recountEmailThread as recount, setEmailFlags } from "./email-state.js";
|
|
7
7
|
import { bool, flag, fromJson, int, page, str, toJson } from "./events.js";
|
|
8
8
|
import { folded } from "./folded.js";
|
|
@@ -469,6 +469,7 @@ export const mailStoreOver = ({ database, orm, now }, stemmerFor = stemmerCache(
|
|
|
469
469
|
mailboxes: JSON.parse(String(row.boxes)),
|
|
470
470
|
}));
|
|
471
471
|
},
|
|
472
|
+
partialEmails: async (accountId, limit) => partialEmails(database, accountId, limit),
|
|
472
473
|
async emailFacts(filter = {}) {
|
|
473
474
|
const [where, params] = emailWhere(filter);
|
|
474
475
|
return database
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { contextEvidenceFingerprint } from "../../context/evidence.js";
|
|
3
3
|
import { findingFingerprint, findingHash } from "../../meetings/index.js";
|
|
4
|
+
import { assertMayDecide } from "../decisions.js";
|
|
4
5
|
import { EVIDENCE_FINDINGS_KIND, validateEvidenceFindings, } from "../evidence-findings.js";
|
|
5
6
|
import { contextRecordOf } from "./context-evidence.js";
|
|
6
7
|
import { sourcesOfReferences } from "./evidence-sources.js";
|
|
@@ -80,8 +81,10 @@ export const evidenceFindingsStoreOver = (context) => {
|
|
|
80
81
|
};
|
|
81
82
|
const snapshot = (input) => core.snapshot(input, cite());
|
|
82
83
|
const decider = (by) => {
|
|
83
|
-
if (by ===
|
|
84
|
+
if (by === "owner")
|
|
84
85
|
return { author: "owner", decision: { by: "owner" } };
|
|
86
|
+
if (by === undefined)
|
|
87
|
+
return invalid("a findings review names who decides: the owner or an agent with a reason");
|
|
85
88
|
const raw = by;
|
|
86
89
|
const agent = findingsAgent({
|
|
87
90
|
bot: raw.bot,
|
|
@@ -157,7 +160,14 @@ export const evidenceFindingsStoreOver = (context) => {
|
|
|
157
160
|
decision.accept.some((candidate) => !batch.input.findings.some((finding) => finding.id === candidate)))
|
|
158
161
|
return invalid("accept only unique IDs shown in this batch");
|
|
159
162
|
const accepted = [...decision.accept].sort();
|
|
160
|
-
const { author, decision:
|
|
163
|
+
const { author, decision: checked } = decider(decision.by);
|
|
164
|
+
const reason = typeof decision.reason === "string" ? decision.reason.trim().slice(0, 2000) : "";
|
|
165
|
+
const decidedBy = reason && checked.by === "owner"
|
|
166
|
+
? { ...checked, reason }
|
|
167
|
+
: reason && checked.by === "agent"
|
|
168
|
+
? { ...checked, reason: `${checked.reason}: ${reason}` }
|
|
169
|
+
: checked;
|
|
170
|
+
assertMayDecide({ id, proposedBy: proposals.proposer(id) }, decision.by, context.decisions);
|
|
161
171
|
if (batch.receipt) {
|
|
162
172
|
if (JSON.stringify(batch.receipt.accepted) !== JSON.stringify(accepted))
|
|
163
173
|
return invalid("this findings batch was already reviewed with a different decision");
|
|
@@ -187,7 +197,7 @@ export const evidenceFindingsStoreOver = (context) => {
|
|
|
187
197
|
bot: proposal.proposedBy.name,
|
|
188
198
|
...(input.agentModel === null ? {} : { model: input.agentModel }),
|
|
189
199
|
};
|
|
190
|
-
const items = core.outcomes(null, batch.input, accepted, agent);
|
|
200
|
+
const items = core.outcomes(null, batch.input, accepted, agent, decision.by);
|
|
191
201
|
return view(proposals.approve(id, {
|
|
192
202
|
previewFingerprint: batch.fingerprint,
|
|
193
203
|
accepted,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type FACT_STATUSES, FACT_TYPES, MEMORY_SCOPES } from "../../domain/knowledge.js";
|
|
2
|
-
import type
|
|
2
|
+
import { type Decider } from "../decisions.js";
|
|
3
|
+
import { type Actor, type ProposedByFilter } from "./actors.js";
|
|
3
4
|
import { type EvidenceSource } from "./evidence-sources.js";
|
|
4
5
|
import { type Author } from "./memories.js";
|
|
5
6
|
import type { StoreContext } from "./open.js";
|
|
@@ -18,11 +19,12 @@ export interface Fact {
|
|
|
18
19
|
subjectIsOwner: boolean;
|
|
19
20
|
occurredAt: string | null;
|
|
20
21
|
supersedes: string | null;
|
|
21
|
-
/** `name` is a bot's handle; `null` for a person. */
|
|
22
|
+
/** `name` is a bot's handle and `kind` whether it is an agent or a rule; both `null` for a person. */
|
|
22
23
|
author: {
|
|
23
24
|
type: Actor["type"];
|
|
24
25
|
id: string;
|
|
25
26
|
name: string | null;
|
|
27
|
+
kind: "agent" | "rule" | null;
|
|
26
28
|
};
|
|
27
29
|
model: string | null;
|
|
28
30
|
confirmedBy: {
|
|
@@ -59,19 +61,22 @@ export interface FactsStore {
|
|
|
59
61
|
project?: string;
|
|
60
62
|
subject?: string;
|
|
61
63
|
status?: Fact["status"];
|
|
64
|
+
proposedBy?: ProposedByFilter;
|
|
65
|
+
/** `created` lists the newest written first; by default the newest by when it happened. */
|
|
66
|
+
order?: "occurred" | "created";
|
|
62
67
|
limit?: number;
|
|
63
68
|
}): Promise<Fact[]>;
|
|
64
|
-
/** The owner accepts a proposed fact; the fact it supersedes stops holding. */
|
|
65
|
-
accept(reference: string): Promise<Fact>;
|
|
69
|
+
/** The owner, or an agent for the owner, accepts a proposed fact; the fact it supersedes stops holding. */
|
|
70
|
+
accept(reference: string, by: Decider, reason?: string): Promise<Fact>;
|
|
66
71
|
/** An accepted decision no longer holds and nothing replaced it. */
|
|
67
|
-
reverse(reference: string): Promise<Fact>;
|
|
68
|
-
/** The owner turns down a proposed fact; it stays, marked `rejected`. */
|
|
69
|
-
reject(reference: string): Promise<Fact>;
|
|
72
|
+
reverse(reference: string, by: Decider): Promise<Fact>;
|
|
73
|
+
/** The owner, or an agent for the owner, turns down a proposed fact; it stays, marked `rejected`. */
|
|
74
|
+
reject(reference: string, by: Decider, reason?: string): Promise<Fact>;
|
|
70
75
|
/**
|
|
71
76
|
* Erases the fact: its row, its links, its tags, and the memory it was migrated from, which still held its text.
|
|
72
77
|
* A decision that replaced it no longer names it. A backup made before keeps it.
|
|
73
78
|
*/
|
|
74
|
-
remove(reference: string): Promise<{
|
|
79
|
+
remove(reference: string, by: Decider): Promise<{
|
|
75
80
|
ref: string;
|
|
76
81
|
removed: true;
|
|
77
82
|
links: number;
|
|
@@ -81,6 +86,9 @@ export interface FactsStore {
|
|
|
81
86
|
}
|
|
82
87
|
type FactOperations = {
|
|
83
88
|
[K in keyof FactsStore]: FactsStore[K] extends (...args: infer A) => Promise<infer R> ? (...args: A) => R : never;
|
|
89
|
+
} & {
|
|
90
|
+
/** Accepts as part of a decision already checked, such as a reviewed findings batch. */
|
|
91
|
+
acceptChecked(reference: string, by: Decider): Fact;
|
|
84
92
|
};
|
|
85
93
|
export declare const factOperationsOver: (context: StoreContext) => FactOperations;
|
|
86
94
|
export declare const factsStoreOver: (context: StoreContext) => FactsStore;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { FACT_TYPES, MEMORY_SCOPES } from "../../domain/knowledge.js";
|
|
3
3
|
import { calendarMoment } from "../../domain/moment.js";
|
|
4
|
+
import { assertMayDecide, assertMayWithdraw, deciderAuthor } from "../decisions.js";
|
|
5
|
+
import { proposedBySql, proposerOf } from "./actors.js";
|
|
4
6
|
import { evidenceSourcesOf } from "./evidence-sources.js";
|
|
5
7
|
import { linkKind } from "./link-kinds.js";
|
|
6
8
|
import { authorActor, authorOf, dropLinksOf, erase, evidenceOf, keepHighestId, linkEvidence, nextId, projectOf, subjectOf, } from "./memories.js";
|
|
7
9
|
import { requiredThing } from "./notes.js";
|
|
8
10
|
import { write } from "./transaction.js";
|
|
9
|
-
const NAMED = "SELECT f.*, b.name AS author_name FROM facts f LEFT JOIN bots b ON f.author_type = 'bot' AND b.id = f.author_id";
|
|
11
|
+
const NAMED = "SELECT f.*, b.name AS author_name, b.kind AS author_kind FROM facts f LEFT JOIN bots b ON f.author_type = 'bot' AND b.id = f.author_id";
|
|
10
12
|
const iso = (value) => new Date(Number(value)).toISOString();
|
|
11
13
|
const oneOf = (value, allowed, what) => {
|
|
12
14
|
if (typeof value !== "string" || !allowed.includes(value))
|
|
@@ -42,15 +44,32 @@ export const factOperationsOver = (context) => {
|
|
|
42
44
|
throw new CliError("validation_error", `${reference} is not a fact`);
|
|
43
45
|
return database.prepare(`${NAMED} WHERE f.id = ?`).get(thing.id);
|
|
44
46
|
};
|
|
45
|
-
const
|
|
47
|
+
const withdrawn = (row, by) => assertMayWithdraw({
|
|
48
|
+
id: `fact:${row.id}`,
|
|
49
|
+
proposedBy: proposerOf(database, row.author_type, row.author_id),
|
|
50
|
+
proposed: row.status === "proposed",
|
|
51
|
+
}, by);
|
|
52
|
+
const decided = (row, by) => assertMayDecide({ id: `fact:${row.id}`, proposedBy: proposerOf(database, row.author_type, row.author_id) }, by, context.decisions);
|
|
53
|
+
/** Keeps who decided for the owner and why in `metadata.decision`; the decider's actor is in `confirmed_by`. */
|
|
54
|
+
const keepDecision = (id, by, reason) => {
|
|
55
|
+
const why = [by === "owner" ? undefined : by.reason.trim(), reason?.trim() || undefined].filter(Boolean).join(": ");
|
|
56
|
+
if (by === "owner" && !why)
|
|
57
|
+
return;
|
|
58
|
+
const decision = by === "owner" ? { by: "owner", reason: why } : { by: "agent", bot: by.bot, reason: why };
|
|
59
|
+
database
|
|
60
|
+
.prepare("UPDATE facts SET metadata = json_set(coalesce(metadata, '{}'), '$.decision', json(?)) WHERE id = ?")
|
|
61
|
+
.run(JSON.stringify(decision), id);
|
|
62
|
+
};
|
|
63
|
+
const accept = (id, by = "owner", reason) => {
|
|
46
64
|
const row = database.prepare("SELECT * FROM facts WHERE id = ?").get(id);
|
|
47
65
|
if (row.status !== "proposed" && row.status !== "accepted")
|
|
48
66
|
throw new CliError("validation_error", `a ${String(row.status)} fact cannot be accepted`);
|
|
49
|
-
const { actor } = authorActor(context,
|
|
67
|
+
const { actor } = authorActor(context, deciderAuthor(by));
|
|
50
68
|
const at = now();
|
|
51
69
|
database
|
|
52
70
|
.prepare("UPDATE facts SET status = 'accepted', confirmed_by_type = ?, confirmed_by_id = ?, updated_at = ? WHERE id = ?")
|
|
53
71
|
.run(actor.type, actor.id, at, id);
|
|
72
|
+
keepDecision(id, by, reason);
|
|
54
73
|
if (row.supersedes_id != null)
|
|
55
74
|
database
|
|
56
75
|
.prepare("UPDATE facts SET status = 'superseded', updated_at = ? WHERE id = ? AND status IN ('proposed', 'accepted')")
|
|
@@ -100,20 +119,29 @@ export const factOperationsOver = (context) => {
|
|
|
100
119
|
list: (options = {}) => {
|
|
101
120
|
const project = options.project === undefined ? undefined : requiredThing(database, options.project, "the project");
|
|
102
121
|
const subject = options.subject === undefined ? undefined : requiredThing(database, options.subject);
|
|
122
|
+
const by = proposedBySql("f.author_type", "f.author_id", options.proposedBy);
|
|
123
|
+
const order = options.order === "created" ? "f.created_at" : "coalesce(f.occurred_at, f.created_at)";
|
|
103
124
|
return database
|
|
104
125
|
.prepare(`${NAMED} WHERE f.deleted_at IS NULL AND (? IS NULL OR f.type = ?) AND (? IS NULL OR f.project_id = ?) ` +
|
|
105
126
|
"AND (? IS NULL OR (f.subject_type = ? AND f.subject_id = ?)) AND (? IS NULL OR f.status = ?) " +
|
|
106
|
-
|
|
107
|
-
.all(options.type ?? null, options.type ?? null, project?.id ?? null, project?.id ?? null, subject?.type ?? null, subject?.type ?? null, subject?.id ?? null, options.status ?? null, options.status ?? null, Math.min(Math.max(options.limit ?? 100, 1), 500))
|
|
127
|
+
`AND ${by.sql} ORDER BY ${order} DESC, f.id DESC LIMIT ?`)
|
|
128
|
+
.all(options.type ?? null, options.type ?? null, project?.id ?? null, project?.id ?? null, subject?.type ?? null, subject?.type ?? null, subject?.id ?? null, options.status ?? null, options.status ?? null, ...by.params, Math.min(Math.max(options.limit ?? 100, 1), 500))
|
|
108
129
|
.map(factOf);
|
|
109
130
|
},
|
|
110
|
-
accept: (reference) => {
|
|
131
|
+
accept: (reference, by, reason) => {
|
|
132
|
+
const row = rowOf(reference);
|
|
133
|
+
decided(row, by);
|
|
134
|
+
write(database, () => accept(Number(row.id), by, reason));
|
|
135
|
+
return factOf(rowOf(reference));
|
|
136
|
+
},
|
|
137
|
+
acceptChecked: (reference, by) => {
|
|
111
138
|
const id = Number(rowOf(reference).id);
|
|
112
|
-
write(database, () => accept(id));
|
|
139
|
+
write(database, () => accept(id, by));
|
|
113
140
|
return factOf(rowOf(reference));
|
|
114
141
|
},
|
|
115
|
-
reverse: (reference) => {
|
|
142
|
+
reverse: (reference, by) => {
|
|
116
143
|
const row = rowOf(reference);
|
|
144
|
+
assertMayWithdraw({ id: `fact:${row.id}`, proposedBy: { type: "unknown" }, proposed: false }, by);
|
|
117
145
|
if (row.type !== "decision")
|
|
118
146
|
throw new CliError("validation_error", "only a decision can be reversed");
|
|
119
147
|
if (row.status !== "accepted")
|
|
@@ -121,15 +149,20 @@ export const factOperationsOver = (context) => {
|
|
|
121
149
|
database.prepare("UPDATE facts SET status = 'reversed', updated_at = ? WHERE id = ?").run(now(), Number(row.id));
|
|
122
150
|
return factOf(rowOf(reference));
|
|
123
151
|
},
|
|
124
|
-
reject: (reference) => {
|
|
152
|
+
reject: (reference, by, reason) => {
|
|
125
153
|
const row = rowOf(reference);
|
|
154
|
+
decided(row, by);
|
|
126
155
|
if (row.status !== "proposed")
|
|
127
156
|
throw new CliError("validation_error", `only a proposed fact can be rejected; this one is ${String(row.status)}`);
|
|
128
|
-
database
|
|
157
|
+
write(database, () => {
|
|
158
|
+
database.prepare("UPDATE facts SET status = 'rejected', updated_at = ? WHERE id = ?").run(now(), Number(row.id));
|
|
159
|
+
keepDecision(Number(row.id), by, reason);
|
|
160
|
+
});
|
|
129
161
|
return factOf(rowOf(reference));
|
|
130
162
|
},
|
|
131
|
-
remove: (reference) => {
|
|
163
|
+
remove: (reference, by) => {
|
|
132
164
|
const row = rowOf(reference);
|
|
165
|
+
withdrawn(row, by);
|
|
133
166
|
const id = Number(row.id);
|
|
134
167
|
const twin = row.source === "memory" && row.metadata != null ? migratedFrom(String(row.metadata)) : undefined;
|
|
135
168
|
const links = erase(database, () => {
|
|
@@ -165,10 +198,10 @@ export const factsStoreOver = (context) => {
|
|
|
165
198
|
add: async (input) => operations.add(input),
|
|
166
199
|
get: async (reference) => operations.get(reference),
|
|
167
200
|
list: async (options) => operations.list(options),
|
|
168
|
-
accept: async (reference) => operations.accept(reference),
|
|
169
|
-
reverse: async (reference) => operations.reverse(reference),
|
|
170
|
-
reject: async (reference) => operations.reject(reference),
|
|
171
|
-
remove: async (reference) => operations.remove(reference),
|
|
201
|
+
accept: async (reference, by, reason) => operations.accept(reference, by, reason),
|
|
202
|
+
reverse: async (reference, by) => operations.reverse(reference, by),
|
|
203
|
+
reject: async (reference, by, reason) => operations.reject(reference, by, reason),
|
|
204
|
+
remove: async (reference, by) => operations.remove(reference, by),
|
|
172
205
|
sources: async (reference) => operations.sources(reference),
|
|
173
206
|
};
|
|
174
207
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Finding, type FindingEvidence, type FindingOutcome, type FindingsInput } from "../../meetings/index.js";
|
|
2
|
+
import type { Decider } from "../decisions.js";
|
|
2
3
|
import type { StoreContext } from "./open.js";
|
|
3
4
|
export interface FindingsAgent {
|
|
4
5
|
bot: string;
|
|
@@ -14,5 +15,5 @@ export declare const findingsCoreOver: (context: StoreContext, kind: string) =>
|
|
|
14
15
|
snapshot: (input: FindingsInput, cite: (evidence: FindingEvidence, finding: Finding, projectId: string | null, seen: Map<string, unknown>) => void) => string;
|
|
15
16
|
snapshotParts: (input: FindingsInput, cite: (evidence: FindingEvidence, finding: Finding, projectId: string | null, seen: Map<string, unknown>) => void) => Record<string, string>;
|
|
16
17
|
previous: (account: number | null, fingerprint: string) => FindingOutcome | undefined;
|
|
17
|
-
outcomes: (account: number | null, input: FindingsInput, accepted: string[], agent: FindingsAgent) => FindingOutcome[];
|
|
18
|
+
outcomes: (account: number | null, input: FindingsInput, accepted: string[], agent: FindingsAgent, decidedBy: Decider) => FindingOutcome[];
|
|
18
19
|
};
|
|
@@ -68,7 +68,7 @@ export const findingsCoreOver = (context, kind) => {
|
|
|
68
68
|
.get(account, kind, fingerprint);
|
|
69
69
|
return row ? JSON.parse(String(row.item)) : undefined;
|
|
70
70
|
};
|
|
71
|
-
const create = (finding, agent) => {
|
|
71
|
+
const create = (finding, agent, decidedBy) => {
|
|
72
72
|
const evidence = finding.evidence.map((item) => item.reference);
|
|
73
73
|
if (finding.kind === "fact") {
|
|
74
74
|
const fact = facts.add({
|
|
@@ -79,7 +79,7 @@ export const findingsCoreOver = (context, kind) => {
|
|
|
79
79
|
evidence,
|
|
80
80
|
by: agent,
|
|
81
81
|
});
|
|
82
|
-
return facts.
|
|
82
|
+
return facts.acceptChecked(fact.ref, decidedBy).ref;
|
|
83
83
|
}
|
|
84
84
|
if (finding.project === undefined)
|
|
85
85
|
return invalid("a task finding names the project the task goes in");
|
|
@@ -99,7 +99,7 @@ export const findingsCoreOver = (context, kind) => {
|
|
|
99
99
|
return task.ref;
|
|
100
100
|
};
|
|
101
101
|
/** Reuses an earlier decision on an unchanged candidate; refuses to accept one that was rejected. */
|
|
102
|
-
const outcomes = (account, input, accepted, agent) => input.findings.map((finding) => {
|
|
102
|
+
const outcomes = (account, input, accepted, agent, decidedBy) => input.findings.map((finding) => {
|
|
103
103
|
const fingerprint = findingFingerprint(finding);
|
|
104
104
|
const prior = previous(account, fingerprint);
|
|
105
105
|
if (prior) {
|
|
@@ -112,7 +112,7 @@ export const findingsCoreOver = (context, kind) => {
|
|
|
112
112
|
id: finding.id,
|
|
113
113
|
fingerprint,
|
|
114
114
|
status,
|
|
115
|
-
reference: status === "accepted" ? create(finding, agent) : null,
|
|
115
|
+
reference: status === "accepted" ? create(finding, agent, decidedBy) : null,
|
|
116
116
|
reused: false,
|
|
117
117
|
};
|
|
118
118
|
});
|