@wirecat/cli-core 0.550.0 → 0.551.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 +176 -0
- package/THIRD_PARTY_NOTICES +41 -0
- package/dist/calendar/ics.d.ts +1 -1
- package/dist/calendar/ics.js +5 -2
- package/dist/calendar/windows-zones.generated.d.ts +1 -0
- package/dist/calendar/windows-zones.generated.js +146 -0
- package/dist/cli/compact.js +2 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/meeting-findings-input.js +4 -3
- package/dist/cli/messenger/backfill-command.js +28 -1
- package/dist/cli/messenger/context.js +1 -0
- package/dist/cli/messenger/history-gaps.js +4 -4
- package/dist/cli/messenger/mcp-command.js +1 -0
- package/dist/cli/messenger/messages-delete-command.js +7 -8
- package/dist/cli/messenger/projects-command.js +47 -2
- package/dist/cli/payment-query.d.ts +8 -0
- package/dist/cli/payment-query.js +27 -0
- package/dist/cli/project-status-rows.d.ts +8 -0
- package/dist/cli/project-status-rows.js +102 -0
- package/dist/cli/project-status.d.ts +132 -0
- package/dist/cli/project-status.js +248 -0
- package/dist/cli/steps.js +4 -1
- package/dist/domain/knowledge.d.ts +1 -1
- package/dist/domain/knowledge.js +1 -1
- package/dist/domain/status-words.d.ts +3 -0
- package/dist/domain/status-words.js +44 -0
- package/dist/entities/fact.d.ts +12 -1
- package/dist/entities/fact.js +13 -0
- package/dist/entities/index.d.ts +18 -2
- package/dist/entities/meeting-findings.d.ts +1 -1
- package/dist/entities/message-transfer.d.ts +16 -0
- package/dist/entities/message-transfer.js +6 -0
- package/dist/mcp/app-server.js +1 -0
- package/dist/mcp/http/oauth.d.ts +2 -1
- package/dist/mcp/http/oauth.js +2 -2
- package/dist/mcp/http/serve.d.ts +2 -0
- package/dist/mcp/http/serve.js +2 -1
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/surface.js +1 -1
- package/dist/meetings/findings-rules.d.ts +14 -3
- package/dist/meetings/findings-rules.js +16 -2
- package/dist/meetings/index.d.ts +3 -2
- package/dist/meetings/index.js +2 -1
- package/dist/meetings/meeting-context.d.ts +2 -3
- package/dist/meetings/meeting-context.js +3 -4
- package/dist/meetings/payment.d.ts +27 -0
- package/dist/meetings/payment.js +81 -0
- package/dist/services/archive.d.ts +13 -0
- package/dist/services/archive.js +56 -5
- package/dist/services/deps.d.ts +3 -0
- package/dist/services/history-limits.d.ts +2 -0
- package/dist/services/index.d.ts +4 -1
- package/dist/services/index.js +4 -1
- package/dist/services/job-query.d.ts +5 -2
- package/dist/services/job-query.js +5 -2
- package/dist/services/link-scorers/alias-hits.d.ts +7 -0
- package/dist/services/link-scorers/alias-hits.js +54 -0
- package/dist/services/link-scorers/chat-senders.d.ts +6 -0
- package/dist/services/link-scorers/chat-senders.js +56 -0
- package/dist/services/link-scorers/chat-title.d.ts +6 -0
- package/dist/services/link-scorers/chat-title.js +44 -0
- package/dist/services/link-scorers/email-domain.d.ts +6 -0
- package/dist/services/link-scorers/email-domain.js +64 -0
- package/dist/services/link-scorers/index.d.ts +9 -0
- package/dist/services/link-scorers/index.js +8 -0
- package/dist/services/link-scorers/mail-thread.d.ts +3 -0
- package/dist/services/link-scorers/mail-thread.js +74 -0
- package/dist/services/link-scorers/member-chats.d.ts +3 -0
- package/dist/services/link-scorers/member-chats.js +40 -0
- package/dist/services/link-scorers/member-dialog.d.ts +3 -0
- package/dist/services/link-scorers/member-dialog.js +34 -0
- package/dist/services/link-scorers/scan.d.ts +41 -0
- package/dist/services/link-scorers/scan.js +244 -0
- package/dist/services/link-scorers/shared.d.ts +26 -0
- package/dist/services/link-scorers/shared.js +48 -0
- package/dist/services/link-scorers/types.d.ts +77 -0
- package/dist/services/link-scorers/types.js +1 -0
- package/dist/services/project-status/index.d.ts +71 -0
- package/dist/services/project-status/index.js +196 -0
- package/dist/services/project-status/items.d.ts +10 -0
- package/dist/services/project-status/items.js +23 -0
- package/dist/services/project-status/signals/amounts.d.ts +20 -0
- package/dist/services/project-status/signals/amounts.js +91 -0
- package/dist/services/project-status/signals/children.d.ts +13 -0
- package/dist/services/project-status/signals/children.js +28 -0
- package/dist/services/project-status/signals/coverage.d.ts +13 -0
- package/dist/services/project-status/signals/coverage.js +30 -0
- package/dist/services/project-status/signals/index.d.ts +3 -0
- package/dist/services/project-status/signals/index.js +21 -0
- package/dist/services/project-status/signals/last-message.d.ts +4 -0
- package/dist/services/project-status/signals/last-message.js +22 -0
- package/dist/services/project-status/signals/payments.d.ts +9 -0
- package/dist/services/project-status/signals/payments.js +24 -0
- package/dist/services/project-status/signals/promises.d.ts +17 -0
- package/dist/services/project-status/signals/promises.js +109 -0
- package/dist/services/project-status/signals/questions.d.ts +8 -0
- package/dist/services/project-status/signals/questions.js +38 -0
- package/dist/services/project-status/signals/reply-next.d.ts +19 -0
- package/dist/services/project-status/signals/reply-next.js +75 -0
- package/dist/services/project-status/signals/topics.d.ts +10 -0
- package/dist/services/project-status/signals/topics.js +50 -0
- package/dist/services/project-status/types.d.ts +84 -0
- package/dist/services/project-status/types.js +1 -0
- package/dist/services/proposals/index.d.ts +1 -0
- package/dist/services/proposals/index.js +1 -0
- package/dist/services/proposals/kinds/findings.d.ts +3 -0
- package/dist/services/proposals/kinds/findings.js +8 -1
- package/dist/store/decisions.d.ts +5 -1
- package/dist/store/decisions.js +5 -1
- package/dist/store/evidence-findings.js +1 -0
- package/dist/store/index.d.ts +2 -1
- package/dist/store/index.js +1 -0
- package/dist/store/project-transfer/request.d.ts +1 -1
- package/dist/store/project-transfer/schema.d.ts +2 -2
- package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
- package/dist/store/sqlite/evidence-findings.js +5 -1
- package/dist/store/sqlite/facts.d.ts +13 -0
- package/dist/store/sqlite/facts.js +64 -6
- package/dist/store/sqlite/findings-core.js +14 -1
- package/dist/store/sqlite/knowledge.d.ts +16 -2
- package/dist/store/sqlite/knowledge.js +1 -0
- package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
- package/dist/store/sqlite/relation-proposals.d.ts +29 -2
- package/dist/store/sqlite/relation-proposals.js +75 -4
- package/dist/store/store.d.ts +1 -0
- package/package.json +3 -3
- package/parity.json +11 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { questions as questionsOf } from "../../questions.js";
|
|
2
|
+
import { evidenceOf, excerpt, whoOf } from "../items.js";
|
|
3
|
+
const PER_SIDE = 5;
|
|
4
|
+
const WEEK = 7 * 86_400_000;
|
|
5
|
+
const NOBODY = new Set();
|
|
6
|
+
/** Open questions both ways, newest first, at most five a side. A question answered in another chat stays open. */
|
|
7
|
+
export const questions = {
|
|
8
|
+
id: "questions",
|
|
9
|
+
version: 1,
|
|
10
|
+
scope: "chat",
|
|
11
|
+
read: async (_context, chat) => {
|
|
12
|
+
if (!chat)
|
|
13
|
+
return [];
|
|
14
|
+
const open = (asker) => questionsOf(chat.messages, { answerers: NOBODY, asker })
|
|
15
|
+
.filter(({ answer }) => answer === undefined)
|
|
16
|
+
.map(({ question }) => question)
|
|
17
|
+
.reverse()
|
|
18
|
+
.slice(0, PER_SIDE);
|
|
19
|
+
return [
|
|
20
|
+
["theirs", open("others")],
|
|
21
|
+
["yours", open("owner")],
|
|
22
|
+
].flatMap(([side, asked]) => asked.map((question) => ({
|
|
23
|
+
signal: "questions",
|
|
24
|
+
chat: chat.ref,
|
|
25
|
+
at: question.timestamp,
|
|
26
|
+
who: whoOf(question),
|
|
27
|
+
evidence: evidenceOf(chat, question),
|
|
28
|
+
basis: "store",
|
|
29
|
+
data: { side, excerpt: excerpt(question.text) },
|
|
30
|
+
})));
|
|
31
|
+
},
|
|
32
|
+
attention: (items, context) => {
|
|
33
|
+
const stale = new Set(items.flatMap((item) => item.chat !== null && item.data.side === "yours" && item.at && context.now - Date.parse(item.at) > WEEK
|
|
34
|
+
? [item.chat]
|
|
35
|
+
: []));
|
|
36
|
+
return [...stale].map((chat) => ({ chat, weight: 1, why: "your question unanswered for a week" }));
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { StatusSignal } from "../types.js";
|
|
2
|
+
export interface ReplyNextData {
|
|
3
|
+
next: "owner" | "them" | "named" | "nobody";
|
|
4
|
+
/** Since when the one named has been the one to answer. */
|
|
5
|
+
waitingSince: string | null;
|
|
6
|
+
/** In a group: the person the newest message replies to or mentions. */
|
|
7
|
+
addressed: {
|
|
8
|
+
ref: string | null;
|
|
9
|
+
name: string | null;
|
|
10
|
+
} | null;
|
|
11
|
+
/** Waited longer than `tasks agenda` does before it lists the chat (a day for you, a week for them). */
|
|
12
|
+
overdue: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whose move it is, by five rules in order: the owner wrote last → them; a dialog → the owner; in a group a message
|
|
16
|
+
* that replies to or mentions the owner, or a question right after the owner spoke → the owner; one that names
|
|
17
|
+
* another member → that member; else nobody. A name typed without a mention is not seen.
|
|
18
|
+
*/
|
|
19
|
+
export declare const replyNext: StatusSignal<ReplyNextData>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { AGENDA_DEFAULTS } from "../../agenda.js";
|
|
2
|
+
import { evidenceOf, fromOwner, whoOf } from "../items.js";
|
|
3
|
+
/** Whom a message answers or names: the quoted message's sender first, then the first mention. */
|
|
4
|
+
const targetOf = (message, chat, owners) => {
|
|
5
|
+
const byId = (id) => chat.messages.find((one) => one.id === id);
|
|
6
|
+
const quoted = message.replyTo ?? (message.replyToId === undefined ? undefined : byId(message.replyToId));
|
|
7
|
+
if (quoted)
|
|
8
|
+
return {
|
|
9
|
+
owner: quoted.outgoing === true || (quoted.senderId !== null && owners.has(quoted.senderId)),
|
|
10
|
+
id: quoted.senderId,
|
|
11
|
+
name: quoted.senderName,
|
|
12
|
+
};
|
|
13
|
+
const [mentioned] = message.mentions ?? [];
|
|
14
|
+
if (mentioned === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (owners.has(mentioned))
|
|
17
|
+
return { owner: true, id: mentioned, name: null };
|
|
18
|
+
const named = chat.messages.find((one) => one.senderId === mentioned);
|
|
19
|
+
return { owner: false, id: mentioned, name: named?.senderName ?? null };
|
|
20
|
+
};
|
|
21
|
+
const asksSomething = (message) => message.text.includes("?");
|
|
22
|
+
/**
|
|
23
|
+
* Whose move it is, by five rules in order: the owner wrote last → them; a dialog → the owner; in a group a message
|
|
24
|
+
* that replies to or mentions the owner, or a question right after the owner spoke → the owner; one that names
|
|
25
|
+
* another member → that member; else nobody. A name typed without a mention is not seen.
|
|
26
|
+
*/
|
|
27
|
+
export const replyNext = {
|
|
28
|
+
id: "reply-next",
|
|
29
|
+
version: 1,
|
|
30
|
+
scope: "chat",
|
|
31
|
+
read: async (context, chat) => {
|
|
32
|
+
const last = chat?.messages.at(-1);
|
|
33
|
+
if (!chat || !last)
|
|
34
|
+
return [];
|
|
35
|
+
const { messages } = chat;
|
|
36
|
+
const owners = new Set(messages.flatMap((one) => (fromOwner(one) && one.senderId !== null ? [one.senderId] : [])));
|
|
37
|
+
const lastOwn = messages.findLastIndex(fromOwner);
|
|
38
|
+
const firstAfterOwn = messages[lastOwn + 1] ?? messages[0];
|
|
39
|
+
const target = targetOf(last, chat, owners);
|
|
40
|
+
const decide = () => {
|
|
41
|
+
if (last.outgoing === null)
|
|
42
|
+
return { next: "nobody", addressed: null, since: null };
|
|
43
|
+
if (fromOwner(last))
|
|
44
|
+
return {
|
|
45
|
+
next: "them",
|
|
46
|
+
addressed: chat.kind !== "dialog" && target && !target.owner ? { ref: null, name: target.name } : null,
|
|
47
|
+
since: last.timestamp,
|
|
48
|
+
};
|
|
49
|
+
const since = firstAfterOwn?.timestamp ?? last.timestamp;
|
|
50
|
+
if (chat.kind === "dialog")
|
|
51
|
+
return { next: "owner", addressed: null, since };
|
|
52
|
+
if (target?.owner || (asksSomething(last) && lastOwn === messages.length - 2))
|
|
53
|
+
return { next: "owner", addressed: null, since };
|
|
54
|
+
if (target)
|
|
55
|
+
return { next: "named", addressed: { ref: null, name: target.name }, since };
|
|
56
|
+
return { next: "nobody", addressed: null, since };
|
|
57
|
+
};
|
|
58
|
+
const { next, addressed, since } = decide();
|
|
59
|
+
const waited = since === null ? 0 : context.now - Date.parse(since);
|
|
60
|
+
const overdue = (next === "owner" && waited >= AGENDA_DEFAULTS.unanswered) ||
|
|
61
|
+
(next === "them" && waited >= AGENDA_DEFAULTS.waiting);
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
signal: "reply-next",
|
|
65
|
+
chat: chat.ref,
|
|
66
|
+
at: last.timestamp,
|
|
67
|
+
who: whoOf(last),
|
|
68
|
+
evidence: evidenceOf(chat, last),
|
|
69
|
+
basis: "store",
|
|
70
|
+
data: { next, waitingSince: since, addressed, overdue },
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
},
|
|
74
|
+
attention: (items) => items.flatMap((item) => item.chat !== null && item.data.next === "owner" ? [{ chat: item.chat, weight: 2, why: "your move" }] : []),
|
|
75
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { StatusSignal } from "../types.js";
|
|
2
|
+
export interface TopicsData {
|
|
3
|
+
query: string;
|
|
4
|
+
yoursLast: string | null;
|
|
5
|
+
theirsLast: string | null;
|
|
6
|
+
/** Their newest mention, when the owner spoke of it later: the other side went quiet on it. */
|
|
7
|
+
silentSince: string | null;
|
|
8
|
+
}
|
|
9
|
+
/** Per `--context-query`, the newest message of each side that holds every one of its words, in the window. */
|
|
10
|
+
export declare const topics: StatusSignal<TopicsData>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { normalize } from "../../../store/normalize.js";
|
|
2
|
+
import { evidenceOf, fromOwner } from "../items.js";
|
|
3
|
+
const escaped = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4
|
+
const wholeWord = (word) => new RegExp(`(?<![\\p{L}\\p{N}])${escaped(word)}(?![\\p{L}\\p{N}])`, "u");
|
|
5
|
+
/** Every word of the query, folded, as a whole word somewhere in the text: `projects evidence`'s reading. */
|
|
6
|
+
const matcherOf = (query) => {
|
|
7
|
+
const words = normalize(query).split(" ").filter(Boolean).map(wholeWord);
|
|
8
|
+
return (text) => {
|
|
9
|
+
const folded = normalize(text);
|
|
10
|
+
return words.length > 0 && words.every((word) => word.test(folded));
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/** Per `--context-query`, the newest message of each side that holds every one of its words, in the window. */
|
|
14
|
+
export const topics = {
|
|
15
|
+
id: "topics",
|
|
16
|
+
version: 1,
|
|
17
|
+
scope: "chat",
|
|
18
|
+
read: async (context, chat) => {
|
|
19
|
+
if (!chat)
|
|
20
|
+
return [];
|
|
21
|
+
return context.contextQueries.flatMap((query) => {
|
|
22
|
+
const matches = matcherOf(query);
|
|
23
|
+
const naming = chat.messages.filter((message) => matches(message.text));
|
|
24
|
+
const yours = naming.findLast(fromOwner);
|
|
25
|
+
const theirs = naming.findLast((message) => !fromOwner(message));
|
|
26
|
+
if (!yours && !theirs)
|
|
27
|
+
return [];
|
|
28
|
+
const silent = theirs && yours && Date.parse(yours.timestamp) > Date.parse(theirs.timestamp);
|
|
29
|
+
const newest = [yours, theirs]
|
|
30
|
+
.filter((one) => one !== undefined)
|
|
31
|
+
.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
signal: "topics",
|
|
35
|
+
chat: chat.ref,
|
|
36
|
+
at: newest[0]?.timestamp ?? null,
|
|
37
|
+
who: null,
|
|
38
|
+
evidence: evidenceOf(chat, ...newest),
|
|
39
|
+
basis: "store",
|
|
40
|
+
data: {
|
|
41
|
+
query,
|
|
42
|
+
yoursLast: yours?.timestamp ?? null,
|
|
43
|
+
theirsLast: theirs?.timestamp ?? null,
|
|
44
|
+
silentSince: silent ? theirs.timestamp : null,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ChatKind, Id, Message } from "../../domain/models.js";
|
|
2
|
+
import type { AccountKey, MessageStore } from "../../store/store.js";
|
|
3
|
+
import type { ChatCoverage } from "../chat-coverage.js";
|
|
4
|
+
/** What every chat signal reads; loaded once per chat by the pipeline. Never printed. */
|
|
5
|
+
export interface StatusChat {
|
|
6
|
+
/** `chat:<provider>/<account>/<chat>` */
|
|
7
|
+
ref: string;
|
|
8
|
+
account: AccountKey;
|
|
9
|
+
chatId: Id;
|
|
10
|
+
title: string | null;
|
|
11
|
+
kind: ChatKind;
|
|
12
|
+
/** The link's role: "client", "contractor", or null. */
|
|
13
|
+
role: string | null;
|
|
14
|
+
/** The chat's purpose in the project; `work` until links carry one. */
|
|
15
|
+
purpose: string;
|
|
16
|
+
/** `project:<id>`; a child's id when rolled up. */
|
|
17
|
+
project: string;
|
|
18
|
+
coverage: ChatCoverage;
|
|
19
|
+
/** Oldest first, the window, at most `STATUS_WINDOW`. */
|
|
20
|
+
messages: Message[];
|
|
21
|
+
}
|
|
22
|
+
export interface StatusProject {
|
|
23
|
+
ref: string;
|
|
24
|
+
key: string | null;
|
|
25
|
+
name: string;
|
|
26
|
+
}
|
|
27
|
+
export interface StatusChild extends StatusProject {
|
|
28
|
+
parent: string;
|
|
29
|
+
depth: number;
|
|
30
|
+
}
|
|
31
|
+
export interface StatusContext {
|
|
32
|
+
now: number;
|
|
33
|
+
/** Window start, ms. */
|
|
34
|
+
since: number;
|
|
35
|
+
chats: StatusChat[];
|
|
36
|
+
project: StatusProject & {
|
|
37
|
+
children: StatusChild[];
|
|
38
|
+
/** Children not read: too deep, seen twice, or past the limit. */
|
|
39
|
+
cut: {
|
|
40
|
+
ref: string;
|
|
41
|
+
reason: "depth" | "cycle" | "limit";
|
|
42
|
+
}[];
|
|
43
|
+
};
|
|
44
|
+
/** Read only: facts, tasks, chats. */
|
|
45
|
+
store: MessageStore;
|
|
46
|
+
/** `--context-query` texts: every word of one must appear in one message. */
|
|
47
|
+
contextQueries: string[];
|
|
48
|
+
}
|
|
49
|
+
export interface StatusWho {
|
|
50
|
+
ref: string | null;
|
|
51
|
+
name: string | null;
|
|
52
|
+
owner: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** One finding of a signal. Stable keys; the signal adds its own under `data`. */
|
|
55
|
+
export interface StatusItem<D = Record<string, unknown>> {
|
|
56
|
+
signal: string;
|
|
57
|
+
/** Null for a project level finding. */
|
|
58
|
+
chat: string | null;
|
|
59
|
+
/** ISO, when it happened. */
|
|
60
|
+
at: string | null;
|
|
61
|
+
who: StatusWho | null;
|
|
62
|
+
/** `msg:`, `fact:`, `task:` refs. */
|
|
63
|
+
evidence: {
|
|
64
|
+
ref: string;
|
|
65
|
+
fingerprint: string | null;
|
|
66
|
+
}[];
|
|
67
|
+
/** How sure: `pattern` is a word match, to check before it is trusted. */
|
|
68
|
+
basis: "store" | "fact" | "task" | "pattern";
|
|
69
|
+
data: D;
|
|
70
|
+
}
|
|
71
|
+
export interface StatusAttention {
|
|
72
|
+
chat: string;
|
|
73
|
+
weight: number;
|
|
74
|
+
why: string;
|
|
75
|
+
}
|
|
76
|
+
export interface StatusSignal<D = Record<string, unknown>> {
|
|
77
|
+
id: string;
|
|
78
|
+
/** Bumped when the logic changes, so a saved answer says which rules made it. */
|
|
79
|
+
version: number;
|
|
80
|
+
scope: "chat" | "project";
|
|
81
|
+
read(context: StatusContext, chat?: StatusChat): Promise<StatusItem<D>[]>;
|
|
82
|
+
/** Why a chat should be read next, weight 0 to 3; the pipeline sums them per chat. */
|
|
83
|
+
attention?(items: StatusItem<D>[], context: StatusContext): StatusAttention[];
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { parseProposalIds } from "./ids.js";
|
|
2
|
+
export { FACT_SUMMARIES } from "./kinds/findings.js";
|
|
2
3
|
export { emptyProposalRegistry, proposalRegistry } from "./registry.js";
|
|
3
4
|
export type { Decided, Decision, ProposalFilter, ProposalKind, ProposalRegistry, ProposalRow } from "./types.js";
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { Finding } from "../../../meetings/index.js";
|
|
1
2
|
import { type MessageStore } from "../../../store/index.js";
|
|
2
3
|
import type { ProposalKind } from "../types.js";
|
|
4
|
+
/** A finding's one line in the inbox, per kind; a new fact kind adds its row here. */
|
|
5
|
+
export declare const FACT_SUMMARIES: Record<string, (finding: Finding) => string>;
|
|
3
6
|
/**
|
|
4
7
|
* An agent's batch of cited facts and tasks from stored messages, mail and documents. Confirming accepts the parts
|
|
5
8
|
* named, or every finding not rejected before; the rest are rejected. Meeting findings stay with their meeting.
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import { paymentTitle } from "../../../meetings/index.js";
|
|
1
2
|
import { EVIDENCE_FINDINGS_KIND } from "../../../store/index.js";
|
|
2
3
|
import { kept, proposerOfAuthor } from "./shared.js";
|
|
4
|
+
/** A finding's one line in the inbox, per kind; a new fact kind adds its row here. */
|
|
5
|
+
export const FACT_SUMMARIES = {
|
|
6
|
+
fact: (finding) => finding.statement,
|
|
7
|
+
task: (finding) => finding.title,
|
|
8
|
+
payment: (finding) => paymentTitle(finding),
|
|
9
|
+
};
|
|
3
10
|
const statusOf = (batch) => batch.status === "proposed" ? "proposed" : batch.receipt?.accepted.length ? "confirmed" : "rejected";
|
|
4
11
|
const rowOf = (batch, createdAt) => {
|
|
5
12
|
const findings = batch.input.findings;
|
|
@@ -20,7 +27,7 @@ const rowOf = (batch, createdAt) => {
|
|
|
20
27
|
parts: findings.map((finding) => ({
|
|
21
28
|
id: finding.id,
|
|
22
29
|
kind: finding.kind,
|
|
23
|
-
summary: finding.kind
|
|
30
|
+
summary: FACT_SUMMARIES[finding.kind]?.(finding) ?? finding.kind,
|
|
24
31
|
outcome: outcome(finding.id),
|
|
25
32
|
})),
|
|
26
33
|
createdAt,
|
|
@@ -38,9 +38,12 @@ export interface Refusal {
|
|
|
38
38
|
* The one check every decide path runs before it confirms or rejects a proposal. `null` means allowed. Every Claude
|
|
39
39
|
* Code session is the bot `claude-code`, so comparing bot names keeps one session from deciding another's proposal.
|
|
40
40
|
*/
|
|
41
|
-
export declare const mayDecide: (
|
|
41
|
+
export declare const mayDecide: (
|
|
42
|
+
/** `ownerOnly`: what only the owner may say, such as that a payment was made. */
|
|
43
|
+
row: {
|
|
42
44
|
id: string;
|
|
43
45
|
proposedBy: Proposer;
|
|
46
|
+
ownerOnly?: boolean;
|
|
44
47
|
}, by: Decider, policy?: DecisionPolicy) => Refusal | null;
|
|
45
48
|
/** One bot whatever its letter case or spacing: `Claude-Code` is `claude-code`. */
|
|
46
49
|
export declare const botKey: (name: string) => string;
|
|
@@ -59,6 +62,7 @@ export declare const assertMayWithdraw: (row: {
|
|
|
59
62
|
export declare const assertMayDecide: (row: {
|
|
60
63
|
id: string;
|
|
61
64
|
proposedBy: Proposer;
|
|
65
|
+
ownerOnly?: boolean;
|
|
62
66
|
}, by: Decider, policy?: DecisionPolicy) => void;
|
|
63
67
|
/** The author a decision is recorded under. */
|
|
64
68
|
export declare const deciderAuthor: (by: Decider) => "owner" | {
|
package/dist/store/decisions.js
CHANGED
|
@@ -4,7 +4,9 @@ export const DEFAULT_DECISION_POLICY = { agentDecisions: true, ruleProposalsOwne
|
|
|
4
4
|
* The one check every decide path runs before it confirms or rejects a proposal. `null` means allowed. Every Claude
|
|
5
5
|
* Code session is the bot `claude-code`, so comparing bot names keeps one session from deciding another's proposal.
|
|
6
6
|
*/
|
|
7
|
-
export const mayDecide = (
|
|
7
|
+
export const mayDecide = (
|
|
8
|
+
/** `ownerOnly`: what only the owner may say, such as that a payment was made. */
|
|
9
|
+
row, by, policy = DEFAULT_DECISION_POLICY) => {
|
|
8
10
|
if (by === "owner")
|
|
9
11
|
return null;
|
|
10
12
|
const refuse = (code, reason, message) => ({
|
|
@@ -13,6 +15,8 @@ export const mayDecide = (row, by, policy = DEFAULT_DECISION_POLICY) => {
|
|
|
13
15
|
reason,
|
|
14
16
|
message,
|
|
15
17
|
});
|
|
18
|
+
if (row.ownerOnly)
|
|
19
|
+
return refuse("permission_error", "proposal.owner-only", `only the owner decides ${row.id}`);
|
|
16
20
|
if (!policy.agentDecisions)
|
|
17
21
|
return refuse("permission_error", "proposals.agent-decisions-off", "agent decisions are turned off; the owner decides");
|
|
18
22
|
if (typeof by.reason !== "string" || !by.reason.trim() || by.reason.length > 2000)
|
package/dist/store/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type { TagRedirectIndex, TagRedirectResolution, TagRedirectSnapshot } fro
|
|
|
6
6
|
export { createTagRedirectIndex } from "../domain/tag-redirects.js";
|
|
7
7
|
export { note } from "../entities/note.js";
|
|
8
8
|
export type { FindingEvidence, FindingOutcome, MeetingFactFinding, MeetingFinding, MeetingFindingsInput, MeetingFindingsReceipt, MeetingTaskFinding, } from "../meetings/index.js";
|
|
9
|
+
export { PAYMENT_DIRECTIONS, PAYMENT_STATES, type Payment, paymentTitle, validatePayment } from "../meetings/index.js";
|
|
9
10
|
export { CLOSED_STATES, isClosedState, PRIORITY_LABELS, type PriorityLabel, priorityLabel, priorityOf, priorityOption, TASK_STATES, type TaskState, } from "../tasks/index.js";
|
|
10
11
|
export type { AccountMail, AccountMeetings, AccountStore } from "./account-store.js";
|
|
11
12
|
export { assertMayDecide, DEFAULT_DECISION_POLICY, type Decider, type DecisionPolicy, mayDecide, type Proposer, type Refusal, } from "./decisions.js";
|
|
@@ -35,7 +36,7 @@ export type { ChatMetadata } from "./sqlite/chats.js";
|
|
|
35
36
|
export type { Email, EmailAddress, EmailAttachment, EmailFacts, EmailFilter, EmailInput, EmailInThread, EmailLocationInput, EmailRecipient, EmailRef, EmailThread, EmailVectorHit, FolderEmail, Mailbox, MailboxInput, MailFilter, MailStore, PartialEmail, SenderScore, ThreadDetails, ThreadSave, } from "./sqlite/emails.js";
|
|
36
37
|
export { EVENT_FIELDS, type EventChunkToEmbed, type EventHit, type EventQuery, type EventSearch, type NearestEvent, } from "./sqlite/event-search.js";
|
|
37
38
|
export type { EvidenceSource } from "./sqlite/evidence-sources.js";
|
|
38
|
-
export type { Fact, FactInput, FactsStore } from "./sqlite/facts.js";
|
|
39
|
+
export type { Fact, FactInput, FactsStore, PaymentFilter } from "./sqlite/facts.js";
|
|
39
40
|
export type { FileListQuery, FilesStore, StoredFile } from "./sqlite/files.js";
|
|
40
41
|
export { IMPORTED_DOCUMENT_QUERY, type ImportedDocuments } from "./sqlite/imported-documents.js";
|
|
41
42
|
export type { Involvement, InvolvementStore } from "./sqlite/involvements.js";
|
package/dist/store/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { FACT_STATUSES, FACT_TYPES, MEMORY_KINDS, MEMORY_SCOPES, MEMORY_STATUSES, TASK_TYPES, WORK_TASK_TYPES, } from "../domain/knowledge.js";
|
|
2
2
|
export { createTagRedirectIndex } from "../domain/tag-redirects.js";
|
|
3
3
|
export { note } from "../entities/note.js";
|
|
4
|
+
export { PAYMENT_DIRECTIONS, PAYMENT_STATES, paymentTitle, validatePayment } from "../meetings/index.js";
|
|
4
5
|
export { CLOSED_STATES, isClosedState, PRIORITY_LABELS, priorityLabel, priorityOf, priorityOption, TASK_STATES, } from "../tasks/index.js";
|
|
5
6
|
export { assertMayDecide, DEFAULT_DECISION_POLICY, mayDecide, } from "./decisions.js";
|
|
6
7
|
export { applyDocumentTransfer, previewDocumentTransfer } from "./document-transfer/index.js";
|
|
@@ -4,7 +4,7 @@ export declare const projectAnnotationRequest: (request: ProjectAnnotationTarget
|
|
|
4
4
|
format: "wirecat.project-annotations";
|
|
5
5
|
facts: {
|
|
6
6
|
ref: string;
|
|
7
|
-
type: "decision" | "fact" | "promise" | "question" | "statement";
|
|
7
|
+
type: "decision" | "fact" | "payment" | "promise" | "question" | "statement";
|
|
8
8
|
statement: string;
|
|
9
9
|
status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
|
|
10
10
|
subject: string | null;
|
|
@@ -23,7 +23,7 @@ export declare const projectAnnotationsSchema: v.StrictObjectSchema<{
|
|
|
23
23
|
readonly aliases: v.SchemaWithPipe<readonly [v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>, v.MaxLengthAction<string[], 1000, undefined>]>;
|
|
24
24
|
readonly facts: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
|
|
25
25
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
26
|
-
readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
|
|
26
|
+
readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise", "payment"], undefined>;
|
|
27
27
|
readonly statement: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
28
28
|
readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed", "rejected"], undefined>;
|
|
29
29
|
readonly subject: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
@@ -38,7 +38,7 @@ export declare const projectAnnotationsSchema: v.StrictObjectSchema<{
|
|
|
38
38
|
readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
39
39
|
}, undefined>, undefined>, v.MaxLengthAction<{
|
|
40
40
|
ref: string;
|
|
41
|
-
type: "decision" | "fact" | "promise" | "question" | "statement";
|
|
41
|
+
type: "decision" | "fact" | "payment" | "promise" | "question" | "statement";
|
|
42
42
|
statement: string;
|
|
43
43
|
status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
|
|
44
44
|
subject: string | null;
|
|
@@ -4,7 +4,7 @@ export declare const projectAnnotationSnapshotSchema: v.StrictObjectSchema<{
|
|
|
4
4
|
readonly format: v.LiteralSchema<"wirecat.project-annotations", undefined>;
|
|
5
5
|
readonly facts: v.SchemaWithPipe<readonly [v.ArraySchema<v.StrictObjectSchema<{
|
|
6
6
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
7
|
-
readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
|
|
7
|
+
readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise", "payment"], undefined>;
|
|
8
8
|
readonly statement: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>;
|
|
9
9
|
readonly status: v.PicklistSchema<readonly ["proposed", "accepted", "superseded", "reversed", "rejected"], undefined>;
|
|
10
10
|
readonly subject: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, number, undefined>]>, undefined>;
|
|
@@ -19,7 +19,7 @@ export declare const projectAnnotationSnapshotSchema: v.StrictObjectSchema<{
|
|
|
19
19
|
readonly updated_at: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.SafeIntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
20
20
|
}, undefined>, undefined>, v.MaxLengthAction<{
|
|
21
21
|
ref: string;
|
|
22
|
-
type: "decision" | "fact" | "promise" | "question" | "statement";
|
|
22
|
+
type: "decision" | "fact" | "payment" | "promise" | "question" | "statement";
|
|
23
23
|
statement: string;
|
|
24
24
|
status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
|
|
25
25
|
subject: string | null;
|
|
@@ -167,7 +167,11 @@ export const evidenceFindingsStoreOver = (context) => {
|
|
|
167
167
|
: reason && checked.by === "agent"
|
|
168
168
|
? { ...checked, reason: `${checked.reason}: ${reason}` }
|
|
169
169
|
: checked;
|
|
170
|
-
assertMayDecide({
|
|
170
|
+
assertMayDecide({
|
|
171
|
+
id,
|
|
172
|
+
proposedBy: proposals.proposer(id),
|
|
173
|
+
ownerOnly: batch.input.findings.some((finding) => finding.kind === "payment"),
|
|
174
|
+
}, decision.by, context.decisions);
|
|
171
175
|
if (batch.receipt) {
|
|
172
176
|
if (JSON.stringify(batch.receipt.accepted) !== JSON.stringify(accepted))
|
|
173
177
|
return invalid("this findings batch was already reviewed with a different decision");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type FACT_STATUSES, FACT_TYPES, MEMORY_SCOPES } from "../../domain/knowledge.js";
|
|
2
|
+
import { type PAYMENT_DIRECTIONS, type PAYMENT_STATES, type Payment } from "../../meetings/index.js";
|
|
2
3
|
import { type Decider } from "../decisions.js";
|
|
3
4
|
import { type Actor, type ProposedByFilter } from "./actors.js";
|
|
4
5
|
import { type EvidenceSource } from "./evidence-sources.js";
|
|
@@ -34,6 +35,8 @@ export interface Fact {
|
|
|
34
35
|
scope: (typeof MEMORY_SCOPES)[number] | null;
|
|
35
36
|
source: string;
|
|
36
37
|
evidence: string[];
|
|
38
|
+
/** Set on a `payment` fact only. */
|
|
39
|
+
payment: Payment | null;
|
|
37
40
|
createdAt: string;
|
|
38
41
|
updatedAt: string;
|
|
39
42
|
}
|
|
@@ -52,6 +55,14 @@ export interface FactInput {
|
|
|
52
55
|
supersedes?: string;
|
|
53
56
|
/** The memory an agent drew it from: a `created-from` link. */
|
|
54
57
|
createdFrom?: string;
|
|
58
|
+
/** Required on a `payment` fact, refused on any other; checked as `validatePayment` does. */
|
|
59
|
+
payment?: Payment;
|
|
60
|
+
}
|
|
61
|
+
/** `open` is an expected payment that still holds: not superseded, rejected or cancelled. */
|
|
62
|
+
export interface PaymentFilter {
|
|
63
|
+
direction?: (typeof PAYMENT_DIRECTIONS)[number];
|
|
64
|
+
state?: (typeof PAYMENT_STATES)[number];
|
|
65
|
+
open?: boolean;
|
|
55
66
|
}
|
|
56
67
|
export interface FactsStore {
|
|
57
68
|
add(input: FactInput): Promise<Fact>;
|
|
@@ -62,6 +73,8 @@ export interface FactsStore {
|
|
|
62
73
|
subject?: string;
|
|
63
74
|
status?: Fact["status"];
|
|
64
75
|
proposedBy?: ProposedByFilter;
|
|
76
|
+
/** Narrows to payment facts by their fields. */
|
|
77
|
+
payment?: PaymentFilter;
|
|
65
78
|
/** `created` lists the newest written first; by default the newest by when it happened. */
|
|
66
79
|
order?: "occurred" | "created";
|
|
67
80
|
limit?: number;
|
|
@@ -1,6 +1,7 @@
|
|
|
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 { validatePayment } from "../../meetings/index.js";
|
|
4
5
|
import { assertMayDecide, assertMayWithdraw, deciderAuthor } from "../decisions.js";
|
|
5
6
|
import { proposedBySql, proposerOf } from "./actors.js";
|
|
6
7
|
import { evidenceSourcesOf } from "./evidence-sources.js";
|
|
@@ -35,6 +36,7 @@ export const factOperationsOver = (context) => {
|
|
|
35
36
|
scope: row.scope == null ? null : row.scope,
|
|
36
37
|
source: String(row.source),
|
|
37
38
|
evidence: evidenceOf(context, { type: "fact", id: Number(row.id) }),
|
|
39
|
+
payment: row.type === "payment" ? paymentOf(row.metadata) : null,
|
|
38
40
|
createdAt: iso(row.created_at),
|
|
39
41
|
updatedAt: iso(row.updated_at),
|
|
40
42
|
});
|
|
@@ -49,7 +51,11 @@ export const factOperationsOver = (context) => {
|
|
|
49
51
|
proposedBy: proposerOf(database, row.author_type, row.author_id),
|
|
50
52
|
proposed: row.status === "proposed",
|
|
51
53
|
}, by);
|
|
52
|
-
const decided = (row, by) => assertMayDecide({
|
|
54
|
+
const decided = (row, by) => assertMayDecide({
|
|
55
|
+
id: `fact:${row.id}`,
|
|
56
|
+
proposedBy: proposerOf(database, row.author_type, row.author_id),
|
|
57
|
+
ownerOnly: row.type === "payment",
|
|
58
|
+
}, by, context.decisions);
|
|
53
59
|
/** Keeps who decided for the owner and why in `metadata.decision`; the decider's actor is in `confirmed_by`. */
|
|
54
60
|
const keepDecision = (id, by, reason) => {
|
|
55
61
|
const why = [by === "owner" ? undefined : by.reason.trim(), reason?.trim() || undefined].filter(Boolean).join(": ");
|
|
@@ -85,6 +91,7 @@ export const factOperationsOver = (context) => {
|
|
|
85
91
|
const occurredAt = input.occurredAt === undefined ? null : calendarMoment(input.occurredAt, "occurredAt");
|
|
86
92
|
if (occurredAt !== null && !Number.isFinite(occurredAt))
|
|
87
93
|
throw new CliError("validation_error", "occurredAt takes an ISO timestamp");
|
|
94
|
+
const payment = paymentInput(type, input);
|
|
88
95
|
const id = write(database, () => {
|
|
89
96
|
const project = input.project === undefined ? undefined : requiredThing(database, input.project, "the project");
|
|
90
97
|
if (project && project.type !== "project")
|
|
@@ -93,14 +100,18 @@ export const factOperationsOver = (context) => {
|
|
|
93
100
|
const replaced = input.supersedes === undefined ? undefined : rowOf(input.supersedes);
|
|
94
101
|
if (replaced && replaced.status !== "proposed" && replaced.status !== "accepted")
|
|
95
102
|
throw new CliError("validation_error", `${input.supersedes} is ${String(replaced.status)}; only a proposed or accepted fact can be corrected`);
|
|
103
|
+
if (replaced && (replaced.type === "payment") !== (type === "payment"))
|
|
104
|
+
throw new CliError("validation_error", "a payment fact corrects a payment fact, and only one");
|
|
105
|
+
if (payment?.counterparty)
|
|
106
|
+
requiredThing(database, payment.counterparty, "the counterparty");
|
|
96
107
|
const owner = input.by === "owner";
|
|
97
108
|
const { actor, model } = authorActor(context, input.by);
|
|
98
109
|
const at = now();
|
|
99
110
|
const made = Number(database
|
|
100
111
|
.prepare("INSERT INTO facts (id, type, statement, status, project_id, subject_type, subject_id, occurred_at, supersedes_id, " +
|
|
101
|
-
"author_type, author_id, model, scope, source, created_at, updated_at) " +
|
|
102
|
-
"VALUES (?, ?, ?, 'proposed', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id")
|
|
103
|
-
.get(nextId(database, "facts"), type, statement, project?.id ?? null, subject?.type ?? null, subject?.id ?? null, occurredAt, replaced === undefined ? null : Number(replaced.id), actor.type, actor.id, model, scope, owner ? "owner" : "agent", at, at)?.id);
|
|
112
|
+
"author_type, author_id, model, scope, source, metadata, created_at, updated_at) " +
|
|
113
|
+
"VALUES (?, ?, ?, 'proposed', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id")
|
|
114
|
+
.get(nextId(database, "facts"), type, statement, project?.id ?? null, subject?.type ?? null, subject?.id ?? null, occurredAt, replaced === undefined ? null : Number(replaced.id), actor.type, actor.id, model, scope, owner ? "owner" : "agent", payment ? JSON.stringify({ payment }) : null, at, at)?.id);
|
|
104
115
|
linkEvidence(context, { type: "fact", id: made }, input.evidence ?? [], owner ? "owner" : "agent");
|
|
105
116
|
if (input.createdFrom !== undefined) {
|
|
106
117
|
const from = requiredThing(database, input.createdFrom, "the source");
|
|
@@ -121,11 +132,12 @@ export const factOperationsOver = (context) => {
|
|
|
121
132
|
const subject = options.subject === undefined ? undefined : requiredThing(database, options.subject);
|
|
122
133
|
const by = proposedBySql("f.author_type", "f.author_id", options.proposedBy);
|
|
123
134
|
const order = options.order === "created" ? "f.created_at" : "coalesce(f.occurred_at, f.created_at)";
|
|
135
|
+
const paid = paymentSql(options.payment);
|
|
124
136
|
return database
|
|
125
137
|
.prepare(`${NAMED} WHERE f.deleted_at IS NULL AND (? IS NULL OR f.type = ?) AND (? IS NULL OR f.project_id = ?) ` +
|
|
126
138
|
"AND (? IS NULL OR (f.subject_type = ? AND f.subject_id = ?)) AND (? IS NULL OR f.status = ?) " +
|
|
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))
|
|
139
|
+
`AND ${by.sql}${paid.sql} ORDER BY ${order} DESC, f.id DESC LIMIT ?`)
|
|
140
|
+
.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, ...paid.params, Math.min(Math.max(options.limit ?? 100, 1), 500))
|
|
129
141
|
.map(factOf);
|
|
130
142
|
},
|
|
131
143
|
accept: (reference, by, reason) => {
|
|
@@ -183,6 +195,52 @@ export const factOperationsOver = (context) => {
|
|
|
183
195
|
sources: (reference) => evidenceSourcesOf(database, { type: "fact", id: Number(rowOf(reference).id) }),
|
|
184
196
|
};
|
|
185
197
|
};
|
|
198
|
+
const paymentInput = (type, input) => {
|
|
199
|
+
if (type !== "payment") {
|
|
200
|
+
if (input.payment !== undefined)
|
|
201
|
+
throw new CliError("validation_error", "only a payment fact takes a payment");
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
if (input.payment === undefined)
|
|
205
|
+
throw new CliError("validation_error", "a payment fact needs its payment: amount, currency, direction and state");
|
|
206
|
+
const evidence = input.evidence?.length ?? 0;
|
|
207
|
+
if (evidence < 1 || evidence > 8)
|
|
208
|
+
throw new CliError("validation_error", "a payment fact cites 1–8 references: the ask, the transfer or a sheet row");
|
|
209
|
+
return validatePayment(input.payment);
|
|
210
|
+
};
|
|
211
|
+
const paymentOf = (metadata) => {
|
|
212
|
+
if (metadata == null)
|
|
213
|
+
return null;
|
|
214
|
+
try {
|
|
215
|
+
const { payment } = JSON.parse(String(metadata));
|
|
216
|
+
return payment ?? null;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const paymentSql = (filter) => {
|
|
223
|
+
if (filter === undefined)
|
|
224
|
+
return { sql: "", params: [] };
|
|
225
|
+
const parts = ["f.type = 'payment'"];
|
|
226
|
+
const params = [];
|
|
227
|
+
const state = filter.open ? "expected" : filter.state;
|
|
228
|
+
if (filter.open && filter.state !== undefined && filter.state !== "expected")
|
|
229
|
+
parts.push("0");
|
|
230
|
+
if (filter.direction !== undefined) {
|
|
231
|
+
parts.push("json_extract(f.metadata, '$.payment.direction') = ?");
|
|
232
|
+
params.push(filter.direction);
|
|
233
|
+
}
|
|
234
|
+
if (state !== undefined) {
|
|
235
|
+
parts.push("json_extract(f.metadata, '$.payment.state') = ?");
|
|
236
|
+
params.push(state);
|
|
237
|
+
}
|
|
238
|
+
if (filter.open)
|
|
239
|
+
parts.push("f.status IN ('proposed', 'accepted')");
|
|
240
|
+
if (filter.open === false)
|
|
241
|
+
parts.push("NOT (json_extract(f.metadata, '$.payment.state') = 'expected' AND f.status IN ('proposed', 'accepted'))");
|
|
242
|
+
return { sql: ` AND ${parts.join(" AND ")}`, params };
|
|
243
|
+
};
|
|
186
244
|
const migratedFrom = (metadata) => {
|
|
187
245
|
try {
|
|
188
246
|
const { memory } = JSON.parse(metadata);
|