@wirecat/cli-core 0.545.0 → 0.547.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 +228 -0
- package/dist/calendar/busy.js +4 -1
- package/dist/calendar/cli/answers.d.ts +31 -0
- package/dist/calendar/cli/answers.js +8 -0
- package/dist/calendar/cli/index.d.ts +6 -0
- package/dist/calendar/cli/index.js +43 -4
- package/dist/calendar/cli/remind.js +2 -1
- package/dist/calendar/expand.d.ts +17 -0
- package/dist/calendar/expand.js +340 -0
- package/dist/calendar/feeds.d.ts +96 -0
- package/dist/calendar/feeds.js +324 -0
- package/dist/calendar/ics.d.ts +15 -4
- package/dist/calendar/ics.js +18 -7
- package/dist/calendar/index.d.ts +3 -1
- package/dist/calendar/index.js +2 -0
- package/dist/calendar/server-search.js +3 -1
- package/dist/calendar/writes.js +15 -4
- package/dist/cli/after-dashes.d.ts +16 -0
- package/dist/cli/after-dashes.js +41 -0
- package/dist/cli/bot/context.d.ts +1 -1
- package/dist/cli/bot/context.js +9 -1
- package/dist/cli/bot/messages.js +2 -0
- package/dist/cli/bot/setup.js +1 -0
- package/dist/cli/contract-templates.js +2 -2
- package/dist/cli/entry.d.ts +2 -0
- package/dist/cli/entry.js +3 -2
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/messenger/admin-commands.js +5 -1
- package/dist/cli/messenger/admin-contacts-command.js +5 -1
- package/dist/cli/messenger/admin-folders-command.js +5 -1
- package/dist/cli/messenger/complete-command.js +1 -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 +12 -23
- package/dist/cli/messenger/messages-comments-command.js +4 -2
- package/dist/cli/messenger/messages-edit-command.js +1 -0
- package/dist/cli/messenger/messages-press-command.js +1 -0
- package/dist/cli/messenger/messages-send-command.js +1 -0
- package/dist/cli/messenger/observed.js +2 -2
- package/dist/cli/messenger/polls-command.js +1 -0
- package/dist/cli/messenger/private-people-command.js +5 -1
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/messenger/stored.js +56 -3
- package/dist/cli/messenger/topics-command.js +1 -1
- package/dist/cli/option-hints.d.ts +6 -0
- package/dist/cli/option-hints.js +15 -0
- package/dist/cli/program.js +17 -6
- package/dist/cli/schedule/schedule-command.js +1 -0
- package/dist/domain/message-link.d.ts +3 -0
- package/dist/domain/message-link.js +9 -14
- package/dist/domain/money-words.d.ts +11 -0
- package/dist/domain/money-words.js +38 -0
- package/dist/domain/providers.d.ts +2 -0
- package/dist/domain/providers.js +15 -3
- package/dist/domain/ref-arguments.d.ts +31 -0
- package/dist/domain/ref-arguments.js +79 -0
- package/dist/domain/references.d.ts +20 -2
- package/dist/domain/references.js +132 -47
- package/dist/errors/reasons.js +31 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -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/mcp/index.d.ts +15 -15
- package/dist/oauth/client-file.d.ts +14 -0
- package/dist/oauth/client-file.js +28 -0
- package/dist/oauth/index.d.ts +1 -0
- package/dist/oauth/index.js +1 -0
- package/dist/ports/messenger.d.ts +3 -2
- package/dist/resolve.d.ts +3 -1
- package/dist/resolve.js +8 -0
- package/dist/search/task-signature.d.ts +5 -1
- package/dist/search/task-signature.js +27 -9
- package/dist/services/agenda.d.ts +5 -0
- package/dist/services/agenda.js +3 -1
- 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 +6 -1
- package/dist/services/index.js +6 -1
- package/dist/services/message-resolution.d.ts +4 -1
- package/dist/services/message-resolution.js +19 -3
- package/dist/services/messages.d.ts +2 -0
- package/dist/services/messages.js +40 -36
- package/dist/services/money-words.d.ts +3 -0
- package/dist/services/money-words.js +5 -0
- 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/questions.d.ts +7 -2
- package/dist/services/questions.js +13 -4
- package/dist/services/server-search.d.ts +7 -4
- package/dist/services/server-search.js +5 -2
- package/dist/services/stored-project-gathering.d.ts +9 -0
- package/dist/services/stored-project-gathering.js +9 -4
- package/dist/services/task-categories.d.ts +2 -0
- package/dist/services/task-categories.js +19 -8
- 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/actors.d.ts +13 -2
- package/dist/store/sqlite/actors.js +24 -1
- 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/evidence-sources.js +2 -2
- 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/identities.js +1 -1
- package/dist/store/sqlite/knowledge.d.ts +43 -7
- package/dist/store/sqlite/knowledge.js +62 -17
- 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 +22 -6
- 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/sqlite/things.d.ts +0 -1
- package/dist/store/sqlite/things.js +25 -24
- package/dist/store/store.d.ts +3 -0
- package/dist/store/store.js +2 -0
- package/package.json +1 -1
|
@@ -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,5 @@
|
|
|
1
1
|
import { linkKind } from "./link-kinds.js";
|
|
2
|
-
import {
|
|
2
|
+
import { referenceOfThing, stateOfThing, storedThing, thingOf } from "./things.js";
|
|
3
3
|
const EXCERPT_CHARS = 280;
|
|
4
4
|
const excerptOf = (text) => {
|
|
5
5
|
if (text == null)
|
|
@@ -59,7 +59,7 @@ export const evidenceSourcesOf = (database, from) => database
|
|
|
59
59
|
.map((link) => sourceOf(database, storedThing(link.to_type, link.to_id)));
|
|
60
60
|
const thingNamed = (database, reference) => {
|
|
61
61
|
try {
|
|
62
|
-
return thingOf(database,
|
|
62
|
+
return thingOf(database, reference);
|
|
63
63
|
}
|
|
64
64
|
catch {
|
|
65
65
|
return undefined;
|
|
@@ -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
|
});
|
|
@@ -205,7 +205,7 @@ export const people = ({ orm, database }, provider, { account, accounts: native
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
const byId = new Map(everyone.map((person) => [person.id, person]));
|
|
208
|
-
return { get: (id) => byId.get(id), all: () => everyone };
|
|
208
|
+
return { provider, get: (id) => byId.get(id), all: () => everyone };
|
|
209
209
|
};
|
|
210
210
|
const contactsWhere = (accountKey, key, { query, notes }) => {
|
|
211
211
|
if (query !== undefined && query.trim().length < 3) {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { type QueryAst } from "@wirecat/cli-base/query";
|
|
2
|
+
import { type Decider } from "../decisions.js";
|
|
2
3
|
import type { AccountKey } from "../types.js";
|
|
3
4
|
import { RELATION_KINDS } from "./link-kinds.js";
|
|
4
5
|
import { type MeetingProjectsStore } from "./meeting-projects.js";
|
|
6
|
+
import type { Author } from "./memories.js";
|
|
7
|
+
import { type Link } from "./notes.js";
|
|
5
8
|
import type { StoreContext } from "./open.js";
|
|
9
|
+
import type { ProposedAction } from "./proposed-actions.js";
|
|
10
|
+
import { type LinkDecision } from "./relation-proposals.js";
|
|
6
11
|
import type { TagCreationOptions } from "./tag-creation.js";
|
|
7
12
|
import { type Label, type TagKind, type TagRow } from "./tags.js";
|
|
8
13
|
import { type ThingState } from "./things.js";
|
|
@@ -177,21 +182,43 @@ export interface KnowledgeStore extends VocabularyStore, MeetingProjectsStore {
|
|
|
177
182
|
description?: string;
|
|
178
183
|
}): Promise<Project>;
|
|
179
184
|
projects(query?: QueryAst): Promise<Project[]>;
|
|
180
|
-
|
|
185
|
+
/** The owner's relationship is a link; `confirmed: false` keeps it a weak suggestion only the owner decides. */
|
|
186
|
+
relate(key: KnowledgeScope, input: RelateInput & {
|
|
187
|
+
by: "owner";
|
|
188
|
+
}): Promise<KnowledgeRelation>;
|
|
189
|
+
/** An agent's relationship is a `relationship` proposal for the owner, never a link. */
|
|
190
|
+
relate(key: KnowledgeScope, input: RelateInput & {
|
|
191
|
+
by: Exclude<Author, "owner">;
|
|
192
|
+
}): Promise<ProposedAction>;
|
|
193
|
+
/**
|
|
194
|
+
* An agent's or a rule's relationship, written as a `relationship` proposal for the owner instead of a link. The
|
|
195
|
+
* same pending pair returns the proposal already held.
|
|
196
|
+
*/
|
|
197
|
+
proposeRelation(key: KnowledgeScope, input: {
|
|
181
198
|
from: string;
|
|
182
199
|
to: string;
|
|
183
200
|
kind: KnowledgeRelation["kind"];
|
|
184
201
|
role?: string;
|
|
185
|
-
evidence?: string;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
202
|
+
evidence?: string[];
|
|
203
|
+
reason?: string;
|
|
204
|
+
}, by: Exclude<Author, "owner">): Promise<ProposedAction>;
|
|
205
|
+
/** Decides a stored `relationship` or `key_document` proposal; the link and the decision land together. */
|
|
206
|
+
decideLinkProposal(id: string, decision: LinkDecision): Promise<{
|
|
207
|
+
proposal: ProposedAction;
|
|
208
|
+
link: Link | null;
|
|
209
|
+
}>;
|
|
210
|
+
/** Decides an unconfirmed link written before proposals were stored. Its author is unknown, so only the owner may. */
|
|
211
|
+
decideSuggestedLink(id: string, decision: LinkDecision): Promise<Link | null>;
|
|
189
212
|
relations(key: KnowledgeScope, reference?: string): Promise<KnowledgeRelation[]>;
|
|
190
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Removing an unconfirmed relationship rejects it, so `by` must be allowed to decide it. Removing a link to a
|
|
215
|
+
* project, or rejecting any suggestion, leaves a rejected `relationship` proposal so a scan never proposes it again.
|
|
216
|
+
*/
|
|
217
|
+
removeRelation(key: KnowledgeScope, id: string, by: Decider): Promise<{
|
|
191
218
|
id: string;
|
|
192
219
|
removed: true;
|
|
193
220
|
}>;
|
|
194
|
-
confirmRelation(key: KnowledgeScope, id: string): Promise<KnowledgeRelation>;
|
|
221
|
+
confirmRelation(key: KnowledgeScope, id: string, by: Decider): Promise<KnowledgeRelation>;
|
|
195
222
|
schedule(key: AccountKey, task: string, dueAt: string, timezone: string): Promise<Reminder>;
|
|
196
223
|
reminders(key: AccountKey, query?: QueryAst): Promise<Reminder[]>;
|
|
197
224
|
cancelReminder(key: AccountKey, id: string): Promise<Reminder>;
|
|
@@ -202,4 +229,13 @@ export interface KnowledgeStore extends VocabularyStore, MeetingProjectsStore {
|
|
|
202
229
|
}): Promise<Reminder[]>;
|
|
203
230
|
acknowledgeReminder(key: AccountKey, id: string, receipt: string): Promise<Reminder>;
|
|
204
231
|
}
|
|
232
|
+
export interface RelateInput {
|
|
233
|
+
from: string;
|
|
234
|
+
to: string;
|
|
235
|
+
kind: KnowledgeRelation["kind"];
|
|
236
|
+
role?: string;
|
|
237
|
+
evidence?: string;
|
|
238
|
+
confirmed?: boolean;
|
|
239
|
+
provenance?: string;
|
|
240
|
+
}
|
|
205
241
|
export declare const knowledgeStoreOver: (context: StoreContext) => KnowledgeStore;
|
|
@@ -6,6 +6,7 @@ import { normalizeTag } from "../../domain/tags.js";
|
|
|
6
6
|
import { normalizeVocabularyName } from "../../domain/vocabulary.js";
|
|
7
7
|
import { formatMeetingReference } from "../../meetings/index.js";
|
|
8
8
|
import { isClosedState } from "../../tasks/index.js";
|
|
9
|
+
import { assertMayDecide } from "../decisions.js";
|
|
9
10
|
import { fold } from "../normalize.js";
|
|
10
11
|
import { ulid } from "../ulid.js";
|
|
11
12
|
import { ownerPerson } from "./actors.js";
|
|
@@ -14,6 +15,7 @@ import { meetingProjectsStoreOver } from "./meeting-projects.js";
|
|
|
14
15
|
import { holdingText } from "./note-index.js";
|
|
15
16
|
import { insertLink, linkOf } from "./notes.js";
|
|
16
17
|
import { queryFilter } from "./query-filter.js";
|
|
18
|
+
import { relationProposalsOver } from "./relation-proposals.js";
|
|
17
19
|
import { addTags as addThingTags, canonicalTagNamed, createTag, labelsOf, liveTagging, removeTags as removeThingTags, setMainTopic, } from "./tags.js";
|
|
18
20
|
import { INBOX_KEY_LENGTH, TASK_KEY_PREFIXES } from "./task-keys.js";
|
|
19
21
|
import { inboxKey, taskAccountOf, taskIdOf, taskRowOf } from "./tasks.js";
|
|
@@ -24,6 +26,11 @@ const TYPED_KEY_PREFIXES = new Set(Object.values(TASK_KEY_PREFIXES));
|
|
|
24
26
|
export const ORGANIZATION_KINDS = ["company", "team", "family", "community"];
|
|
25
27
|
export const PROJECT_TYPES = ["work", "client", "personal", "oss", "other"];
|
|
26
28
|
export const SCOPES = ["personal", "work"];
|
|
29
|
+
/**
|
|
30
|
+
* A link records no actor, only free-text provenance anyone could write, so whoever proposed one is unverified and
|
|
31
|
+
* only the owner decides it.
|
|
32
|
+
*/
|
|
33
|
+
const UNVERIFIED = { type: "unknown" };
|
|
27
34
|
const iso = (value) => new Date(Number(value)).toISOString();
|
|
28
35
|
const bounded = (limit = 100, offset = 0) => {
|
|
29
36
|
if (!Number.isInteger(limit) ||
|
|
@@ -127,7 +134,7 @@ export const knowledgeStoreOver = (context) => {
|
|
|
127
134
|
if (thing.type === "meeting")
|
|
128
135
|
return { type: "meeting", id: String(thing.id) };
|
|
129
136
|
const reference = referenceOfThing(database, thing);
|
|
130
|
-
if (reference === undefined
|
|
137
|
+
if (reference === undefined)
|
|
131
138
|
return undefined;
|
|
132
139
|
const parsed = parseReference(reference);
|
|
133
140
|
if (parsed.type === "message")
|
|
@@ -136,7 +143,12 @@ export const knowledgeStoreOver = (context) => {
|
|
|
136
143
|
return { type: "chat", id: parsed.chat };
|
|
137
144
|
if (parsed.type === "folder")
|
|
138
145
|
return { type: "folder", id: parsed.id, path: parsed.path };
|
|
139
|
-
if (parsed.type === "entity" ||
|
|
146
|
+
if (parsed.type === "entity" ||
|
|
147
|
+
parsed.type === "memory" ||
|
|
148
|
+
parsed.type === "fact" ||
|
|
149
|
+
parsed.type === "bot" ||
|
|
150
|
+
parsed.type === "event" ||
|
|
151
|
+
parsed.type === "meeting")
|
|
140
152
|
return undefined;
|
|
141
153
|
return { type: parsed.type, id: parsed.id };
|
|
142
154
|
};
|
|
@@ -221,6 +233,7 @@ export const knowledgeStoreOver = (context) => {
|
|
|
221
233
|
confidenceCategory: link.confirmed ? "owner-confirmed" : "weak",
|
|
222
234
|
createdAt: link.createdAt,
|
|
223
235
|
});
|
|
236
|
+
const proposals = relationProposalsOver(context);
|
|
224
237
|
const RELATIONS = `SELECT * FROM links WHERE kind IN (${RELATION_KINDS.map(linkKind).join(",")}) AND to_id IS NOT NULL`;
|
|
225
238
|
const relation = (id) => {
|
|
226
239
|
const row = /^\d+$/.test(id) ? database.prepare(`${RELATIONS} AND id = ?`).get(Number(id)) : undefined;
|
|
@@ -228,6 +241,19 @@ export const knowledgeStoreOver = (context) => {
|
|
|
228
241
|
throw new CliError("not_found", "no relationship with that id");
|
|
229
242
|
return row;
|
|
230
243
|
};
|
|
244
|
+
/** The two ends of a relationship, checked, and the link that already joins them. */
|
|
245
|
+
const pair = (key, input) => {
|
|
246
|
+
const from = related(key, input.from, true);
|
|
247
|
+
const to = related(key, input.to);
|
|
248
|
+
if ((from.type === to.type && from.id === to.id) || !RELATION_KINDS.includes(input.kind))
|
|
249
|
+
throw new CliError("validation_error", "a relationship needs distinct references and a supported kind");
|
|
250
|
+
if ((from.type === "chat" || from.type === "email_thread") && (input.kind !== "member-of" || to.type !== "project"))
|
|
251
|
+
throw new CliError("validation_error", "a chat is related only as member-of a project");
|
|
252
|
+
const existing = database
|
|
253
|
+
.prepare(`${RELATIONS} AND from_type=? AND from_id=? AND to_type=? AND to_id=? AND kind=?`)
|
|
254
|
+
.get(from.type, from.id, to.type, to.id, input.kind);
|
|
255
|
+
return { from, to, existing };
|
|
256
|
+
};
|
|
231
257
|
/** A chat may only be the member: its reference names its own account, so it is never another account's. */
|
|
232
258
|
const related = (key, reference, member = false) => {
|
|
233
259
|
checked(key);
|
|
@@ -512,17 +538,19 @@ export const knowledgeStoreOver = (context) => {
|
|
|
512
538
|
.all(...(where?.params ?? []))
|
|
513
539
|
.map(projectOf);
|
|
514
540
|
},
|
|
515
|
-
relate: async (key, input) => {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
541
|
+
relate: (async (key, input) => {
|
|
542
|
+
if (input.by === undefined)
|
|
543
|
+
throw new CliError("validation_error", "a relationship names who writes it: the owner or an agent", {
|
|
544
|
+
reason: "proposal.decider-required",
|
|
545
|
+
});
|
|
546
|
+
const { from, existing } = pair(key, input);
|
|
547
|
+
if (input.by !== "owner") {
|
|
548
|
+
if (existing)
|
|
549
|
+
throw new CliError("validation_error", `relationship ${String(existing.id)} already links them`, {
|
|
550
|
+
reason: "relationship.exists",
|
|
551
|
+
});
|
|
552
|
+
return proposals.propose(from, { ...input, ...(input.evidence === undefined ? {} : { reason: input.evidence }), evidence: [] }, input.by);
|
|
553
|
+
}
|
|
526
554
|
if (existing)
|
|
527
555
|
return relationOf(linkOf(database, existing));
|
|
528
556
|
const confirmed = input.confirmed !== false;
|
|
@@ -536,7 +564,17 @@ export const knowledgeStoreOver = (context) => {
|
|
|
536
564
|
...(input.evidence === undefined ? {} : { evidence: input.evidence }),
|
|
537
565
|
...(input.provenance === undefined ? {} : { provenance: input.provenance }),
|
|
538
566
|
})));
|
|
567
|
+
}),
|
|
568
|
+
proposeRelation: async (key, input, by) => {
|
|
569
|
+
const { from, existing } = pair(key, input);
|
|
570
|
+
if (existing)
|
|
571
|
+
throw new CliError("validation_error", `relationship ${String(existing.id)} already links them`, {
|
|
572
|
+
reason: "relationship.exists",
|
|
573
|
+
});
|
|
574
|
+
return proposals.propose(from, input, by);
|
|
539
575
|
},
|
|
576
|
+
decideLinkProposal: async (id, decision) => proposals.decideProposal(id, decision),
|
|
577
|
+
decideSuggestedLink: async (id, decision) => proposals.decideLink(id, decision),
|
|
540
578
|
relations: async (key, reference) => {
|
|
541
579
|
checked(key);
|
|
542
580
|
const thing = reference === undefined ? undefined : thingOf(database, reference);
|
|
@@ -547,16 +585,23 @@ export const knowledgeStoreOver = (context) => {
|
|
|
547
585
|
.all(thing?.type ?? null, thing?.type ?? null, thing?.id ?? null, thing?.type ?? null, thing?.id ?? null)
|
|
548
586
|
.map((row) => relationOf(linkOf(database, row)));
|
|
549
587
|
},
|
|
550
|
-
removeRelation: async (key, id) => {
|
|
588
|
+
removeRelation: async (key, id, by) => {
|
|
551
589
|
checked(key);
|
|
552
|
-
relation(id);
|
|
553
|
-
|
|
590
|
+
const row = relation(id);
|
|
591
|
+
const link = linkOf(database, row);
|
|
592
|
+
assertMayDecide({ id: `link:${id}`, proposedBy: UNVERIFIED }, by, context.decisions);
|
|
593
|
+
write(database, () => {
|
|
594
|
+
if (!link.confirmed || String(row.to_type) === "project")
|
|
595
|
+
proposals.rejected(link, by);
|
|
596
|
+
database.prepare("DELETE FROM links WHERE id = ?").run(Number(id));
|
|
597
|
+
});
|
|
554
598
|
return { id, removed: true };
|
|
555
599
|
},
|
|
556
|
-
confirmRelation: async (key, id) => {
|
|
600
|
+
confirmRelation: async (key, id, by) => {
|
|
557
601
|
checked(key);
|
|
558
602
|
const row = relation(id);
|
|
559
603
|
const link = linkOf(database, row);
|
|
604
|
+
assertMayDecide({ id: `link:${id}`, proposedBy: UNVERIFIED }, by, context.decisions);
|
|
560
605
|
related(key, link.from, true);
|
|
561
606
|
related(key, String(link.to));
|
|
562
607
|
database
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { findingHash, MEETING_FINDINGS_KIND, meetingCueFingerprint, meetingFindingFingerprint, parseMeetingReference, validateMeetingFindings, } from "../../meetings/index.js";
|
|
3
|
+
import { assertMayDecide, deciderAuthor } from "../decisions.js";
|
|
3
4
|
import { findingsCoreOver } from "./findings-core.js";
|
|
4
5
|
import { authorActor } from "./memories.js";
|
|
5
6
|
import { proposalOperationsOver } from "./proposed-actions.js";
|
|
@@ -176,6 +177,9 @@ export const meetingFindingsStoreOver = (context) => {
|
|
|
176
177
|
decision.accept.some((candidate) => !batch.input.findings.some((finding) => finding.id === candidate)))
|
|
177
178
|
return invalid("accept only unique IDs shown in this batch");
|
|
178
179
|
const accepted = [...decision.accept].sort();
|
|
180
|
+
if (decision.by === undefined)
|
|
181
|
+
return invalid("a findings review names who decides: the owner or an agent");
|
|
182
|
+
assertMayDecide({ id, proposedBy: proposals.proposer(id) }, decision.by, context.decisions);
|
|
179
183
|
if (batch.receipt) {
|
|
180
184
|
if (JSON.stringify(batch.receipt.accepted) !== JSON.stringify(accepted))
|
|
181
185
|
return invalid("this findings batch was already reviewed with a different decision");
|
|
@@ -191,12 +195,8 @@ export const meetingFindingsStoreOver = (context) => {
|
|
|
191
195
|
bot: proposal.proposedBy.name,
|
|
192
196
|
...(input.agentModel === null ? {} : { model: input.agentModel }),
|
|
193
197
|
};
|
|
194
|
-
const items = core.outcomes(account, batch.input, accepted, agent);
|
|
195
|
-
return view(proposals.approve(id, {
|
|
196
|
-
previewFingerprint: batch.fingerprint,
|
|
197
|
-
accepted,
|
|
198
|
-
items,
|
|
199
|
-
}));
|
|
198
|
+
const items = core.outcomes(account, batch.input, accepted, agent, decision.by);
|
|
199
|
+
return view(proposals.approve(id, { previewFingerprint: batch.fingerprint, accepted, items }, deciderAuthor(decision.by)));
|
|
200
200
|
}),
|
|
201
201
|
};
|
|
202
202
|
};
|