@wirecat/cli-core 0.549.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +223 -22
  2. package/THIRD_PARTY_NOTICES +41 -0
  3. package/dist/calendar/ics.d.ts +1 -1
  4. package/dist/calendar/ics.js +5 -2
  5. package/dist/calendar/windows-zones.generated.d.ts +1 -0
  6. package/dist/calendar/windows-zones.generated.js +146 -0
  7. package/dist/cli/compact.js +2 -0
  8. package/dist/cli/index.d.ts +3 -0
  9. package/dist/cli/index.js +3 -0
  10. package/dist/cli/meeting-findings-input.js +4 -3
  11. package/dist/cli/messenger/backfill-command.js +28 -1
  12. package/dist/cli/messenger/context.js +1 -0
  13. package/dist/cli/messenger/history-gaps.js +4 -4
  14. package/dist/cli/messenger/mcp-command.js +1 -0
  15. package/dist/cli/messenger/messages-delete-command.js +7 -8
  16. package/dist/cli/messenger/projects-command.js +47 -2
  17. package/dist/cli/payment-query.d.ts +8 -0
  18. package/dist/cli/payment-query.js +27 -0
  19. package/dist/cli/project-status-rows.d.ts +8 -0
  20. package/dist/cli/project-status-rows.js +102 -0
  21. package/dist/cli/project-status.d.ts +132 -0
  22. package/dist/cli/project-status.js +248 -0
  23. package/dist/cli/steps.js +4 -1
  24. package/dist/domain/knowledge.d.ts +1 -1
  25. package/dist/domain/knowledge.js +1 -1
  26. package/dist/domain/status-words.d.ts +3 -0
  27. package/dist/domain/status-words.js +44 -0
  28. package/dist/entities/fact.d.ts +12 -1
  29. package/dist/entities/fact.js +13 -0
  30. package/dist/entities/index.d.ts +18 -2
  31. package/dist/entities/meeting-findings.d.ts +1 -1
  32. package/dist/entities/message-transfer.d.ts +16 -0
  33. package/dist/entities/message-transfer.js +6 -0
  34. package/dist/errors/reasons.js +1 -0
  35. package/dist/mail/partial.d.ts +9 -3
  36. package/dist/mail/partial.js +35 -10
  37. package/dist/mail/source.d.ts +7 -1
  38. package/dist/mail/sync.js +1 -1
  39. package/dist/mcp/app-server.js +1 -0
  40. package/dist/mcp/http/oauth.d.ts +2 -1
  41. package/dist/mcp/http/oauth.js +2 -2
  42. package/dist/mcp/http/serve.d.ts +2 -0
  43. package/dist/mcp/http/serve.js +2 -1
  44. package/dist/mcp/server.js +1 -0
  45. package/dist/mcp/surface.js +1 -1
  46. package/dist/meetings/findings-rules.d.ts +14 -3
  47. package/dist/meetings/findings-rules.js +16 -2
  48. package/dist/meetings/index.d.ts +3 -2
  49. package/dist/meetings/index.js +2 -1
  50. package/dist/meetings/meeting-context.d.ts +2 -3
  51. package/dist/meetings/meeting-context.js +3 -4
  52. package/dist/meetings/payment.d.ts +27 -0
  53. package/dist/meetings/payment.js +81 -0
  54. package/dist/services/archive.d.ts +13 -0
  55. package/dist/services/archive.js +56 -5
  56. package/dist/services/deps.d.ts +3 -0
  57. package/dist/services/history-limits.d.ts +2 -0
  58. package/dist/services/index.d.ts +4 -1
  59. package/dist/services/index.js +4 -1
  60. package/dist/services/job-query.d.ts +5 -2
  61. package/dist/services/job-query.js +5 -2
  62. package/dist/services/link-scorers/alias-hits.d.ts +7 -0
  63. package/dist/services/link-scorers/alias-hits.js +54 -0
  64. package/dist/services/link-scorers/chat-senders.d.ts +6 -0
  65. package/dist/services/link-scorers/chat-senders.js +56 -0
  66. package/dist/services/link-scorers/chat-title.d.ts +6 -0
  67. package/dist/services/link-scorers/chat-title.js +44 -0
  68. package/dist/services/link-scorers/email-domain.d.ts +6 -0
  69. package/dist/services/link-scorers/email-domain.js +64 -0
  70. package/dist/services/link-scorers/index.d.ts +9 -0
  71. package/dist/services/link-scorers/index.js +8 -0
  72. package/dist/services/link-scorers/mail-thread.d.ts +3 -0
  73. package/dist/services/link-scorers/mail-thread.js +74 -0
  74. package/dist/services/link-scorers/member-chats.d.ts +3 -0
  75. package/dist/services/link-scorers/member-chats.js +40 -0
  76. package/dist/services/link-scorers/member-dialog.d.ts +3 -0
  77. package/dist/services/link-scorers/member-dialog.js +34 -0
  78. package/dist/services/link-scorers/scan.d.ts +41 -0
  79. package/dist/services/link-scorers/scan.js +244 -0
  80. package/dist/services/link-scorers/shared.d.ts +26 -0
  81. package/dist/services/link-scorers/shared.js +48 -0
  82. package/dist/services/link-scorers/types.d.ts +77 -0
  83. package/dist/services/link-scorers/types.js +1 -0
  84. package/dist/services/project-status/index.d.ts +71 -0
  85. package/dist/services/project-status/index.js +196 -0
  86. package/dist/services/project-status/items.d.ts +10 -0
  87. package/dist/services/project-status/items.js +23 -0
  88. package/dist/services/project-status/signals/amounts.d.ts +20 -0
  89. package/dist/services/project-status/signals/amounts.js +91 -0
  90. package/dist/services/project-status/signals/children.d.ts +13 -0
  91. package/dist/services/project-status/signals/children.js +28 -0
  92. package/dist/services/project-status/signals/coverage.d.ts +13 -0
  93. package/dist/services/project-status/signals/coverage.js +30 -0
  94. package/dist/services/project-status/signals/index.d.ts +3 -0
  95. package/dist/services/project-status/signals/index.js +21 -0
  96. package/dist/services/project-status/signals/last-message.d.ts +4 -0
  97. package/dist/services/project-status/signals/last-message.js +22 -0
  98. package/dist/services/project-status/signals/payments.d.ts +9 -0
  99. package/dist/services/project-status/signals/payments.js +24 -0
  100. package/dist/services/project-status/signals/promises.d.ts +17 -0
  101. package/dist/services/project-status/signals/promises.js +109 -0
  102. package/dist/services/project-status/signals/questions.d.ts +8 -0
  103. package/dist/services/project-status/signals/questions.js +38 -0
  104. package/dist/services/project-status/signals/reply-next.d.ts +19 -0
  105. package/dist/services/project-status/signals/reply-next.js +75 -0
  106. package/dist/services/project-status/signals/topics.d.ts +10 -0
  107. package/dist/services/project-status/signals/topics.js +50 -0
  108. package/dist/services/project-status/types.d.ts +84 -0
  109. package/dist/services/project-status/types.js +1 -0
  110. package/dist/services/proposals/index.d.ts +1 -0
  111. package/dist/services/proposals/index.js +1 -0
  112. package/dist/services/proposals/kinds/findings.d.ts +3 -0
  113. package/dist/services/proposals/kinds/findings.js +8 -1
  114. package/dist/store/decisions.d.ts +5 -1
  115. package/dist/store/decisions.js +5 -1
  116. package/dist/store/evidence-findings.js +1 -0
  117. package/dist/store/index.d.ts +2 -1
  118. package/dist/store/index.js +1 -0
  119. package/dist/store/project-transfer/request.d.ts +1 -1
  120. package/dist/store/project-transfer/schema.d.ts +2 -2
  121. package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
  122. package/dist/store/sqlite/email-locations.d.ts +22 -1
  123. package/dist/store/sqlite/email-locations.js +29 -2
  124. package/dist/store/sqlite/emails.d.ts +3 -7
  125. package/dist/store/sqlite/emails.js +4 -2
  126. package/dist/store/sqlite/evidence-findings.js +5 -1
  127. package/dist/store/sqlite/facts.d.ts +13 -0
  128. package/dist/store/sqlite/facts.js +64 -6
  129. package/dist/store/sqlite/findings-core.js +14 -1
  130. package/dist/store/sqlite/knowledge.d.ts +16 -2
  131. package/dist/store/sqlite/knowledge.js +1 -0
  132. package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
  133. package/dist/store/sqlite/relation-proposals.d.ts +29 -2
  134. package/dist/store/sqlite/relation-proposals.js +75 -4
  135. package/dist/store/store.d.ts +1 -0
  136. package/package.json +3 -3
  137. package/parity.json +11 -0
@@ -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({ id: `fact:${row.id}`, proposedBy: proposerOf(database, row.author_type, row.author_id) }, by, context.decisions);
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);
@@ -1,5 +1,5 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
- import { findingFingerprint, findingHash, } from "../../meetings/index.js";
2
+ import { findingFingerprint, findingHash, paymentTitle, } from "../../meetings/index.js";
3
3
  import { factOperationsOver } from "./facts.js";
4
4
  import { linkEvidence } from "./memories.js";
5
5
  import { requiredThing } from "./notes.js";
@@ -81,6 +81,19 @@ export const findingsCoreOver = (context, kind) => {
81
81
  });
82
82
  return facts.acceptChecked(fact.ref, decidedBy).ref;
83
83
  }
84
+ if (finding.kind === "payment") {
85
+ const fact = facts.add({
86
+ type: "payment",
87
+ statement: finding.statement ?? paymentTitle(finding),
88
+ payment: finding.payment,
89
+ ...(finding.project === undefined ? {} : { project: finding.project }),
90
+ ...(finding.subject === undefined ? {} : { subject: finding.subject }),
91
+ ...(finding.supersedes === undefined ? {} : { supersedes: finding.supersedes }),
92
+ evidence,
93
+ by: agent,
94
+ });
95
+ return facts.acceptChecked(fact.ref, decidedBy).ref;
96
+ }
84
97
  if (finding.project === undefined)
85
98
  return invalid("a task finding names the project the task goes in");
86
99
  const task = tasks.create({
@@ -6,7 +6,7 @@ import type { Author } from "./memories.js";
6
6
  import { type Link } from "./notes.js";
7
7
  import type { StoreContext } from "./open.js";
8
8
  import type { ProposedAction } from "./proposed-actions.js";
9
- import { type KnowledgeRelation, type LinkDecision, type RelateInput, type RelationUpdate } from "./relation-proposals.js";
9
+ import { type KnowledgeRelation, type LinkDecision, type RelateInput, type RelationUpdate, type Scored } from "./relation-proposals.js";
10
10
  import type { TagCreationOptions } from "./tag-creation.js";
11
11
  import { type Label, type TagKind, type TagRow } from "./tags.js";
12
12
  import { type ThingState } from "./things.js";
@@ -190,7 +190,21 @@ export interface KnowledgeStore extends VocabularyStore, MeetingProjectsStore {
190
190
  purpose?: string;
191
191
  evidence?: string[];
192
192
  reason?: string;
193
- }, by: Exclude<Author, "owner">): Promise<ProposedAction>;
193
+ } & Scored, by: Exclude<Author, "owner">): Promise<ProposedAction>;
194
+ /**
195
+ * Where a pair stands for a scan: `linked` when a link already joins it, `rejected` when any `relationship`
196
+ * proposal for it was rejected, and the pending proposal if one waits. `undefined` when either end is not stored.
197
+ */
198
+ relationState(key: KnowledgeScope, input: {
199
+ from: string;
200
+ to: string;
201
+ kind: KnowledgeRelation["kind"];
202
+ }): Promise<{
203
+ linked: boolean;
204
+ confirmed: boolean;
205
+ rejected: boolean;
206
+ pending: ProposedAction | null;
207
+ } | undefined>;
194
208
  /** The owner sets a relationship's role and purpose; `null` clears one. */
195
209
  updateRelation(key: KnowledgeScope, id: string, input: RelationUpdate & {
196
210
  by: "owner";
@@ -562,6 +562,7 @@ export const knowledgeStoreOver = (context) => {
562
562
  });
563
563
  return proposals.propose(from, input, by);
564
564
  },
565
+ relationState: async (key, input) => proposals.state(() => pair(key, input), input),
565
566
  updateRelation: (async (key, id, input) => {
566
567
  checked(key);
567
568
  const link = linkOf(database, relation(id));
@@ -5,7 +5,7 @@ export declare const projectAnnotationSnapshot: (database: CacheDatabase, projec
5
5
  format: "wirecat.project-annotations";
6
6
  facts: {
7
7
  ref: string;
8
- type: "decision" | "fact" | "promise" | "question" | "statement";
8
+ type: "decision" | "fact" | "payment" | "promise" | "question" | "statement";
9
9
  statement: string;
10
10
  status: "accepted" | "proposed" | "rejected" | "reversed" | "superseded";
11
11
  subject: string | null;
@@ -5,6 +5,7 @@ import { type Link } from "./notes.js";
5
5
  import type { StoreContext } from "./open.js";
6
6
  import { type ProposedAction } from "./proposed-actions.js";
7
7
  import { type Thing } from "./things.js";
8
+ type Row = Record<string, unknown>;
8
9
  export interface KnowledgeRelation {
9
10
  id: string;
10
11
  from: string;
@@ -32,7 +33,23 @@ export interface RelateInput {
32
33
  export declare const relationOf: (link: Link) => KnowledgeRelation;
33
34
  /** `relationship` proposals in `proposed_actions`: an agent's or a rule's, and the rejected ones a scan skips. */
34
35
  export declare const relationProposalsOver: (context: StoreContext) => {
35
- /** The same pending pair returns the proposal already held. */
36
+ /**
37
+ * Where a pair stands before a scan proposes it: a link joins it, a `relationship` proposal for it was rejected,
38
+ * or one is pending. A rejection is matched by the pair alone, whoever proposed it and with whatever scorer.
39
+ */
40
+ state: (paired: () => {
41
+ from: Thing;
42
+ existing: Row | undefined;
43
+ }, { to, kind }: {
44
+ to: string;
45
+ kind: string;
46
+ }) => {
47
+ linked: boolean;
48
+ confirmed: boolean;
49
+ rejected: boolean;
50
+ pending: ProposedAction | null;
51
+ } | undefined;
52
+ /** The same pending pair returns the proposal already held; a scored one updates its score in place. */
36
53
  propose: (from: Thing, input: {
37
54
  from: string;
38
55
  to: string;
@@ -41,7 +58,7 @@ export declare const relationProposalsOver: (context: StoreContext) => {
41
58
  purpose?: string;
42
59
  evidence?: string[];
43
60
  reason?: string;
44
- }, by: Exclude<Author, "owner">) => ProposedAction;
61
+ } & Scored, by: Exclude<Author, "owner">) => ProposedAction;
45
62
  rejected: (link: Link, by: Decider, reason?: string) => void;
46
63
  update: (link: Link, input: RelationUpdate) => Link;
47
64
  proposeUpdate: (link: Link, input: RelationUpdate & {
@@ -62,6 +79,15 @@ export declare const relationProposalsOver: (context: StoreContext) => {
62
79
  /** Decides an unconfirmed link written before proposals were stored; only the owner may, as its author is unknown. */
63
80
  decideLink: (id: string, decision: LinkDecision) => Link | null;
64
81
  };
82
+ /** What a scan adds to a relationship proposal: the combined score and each scorer's part in it. */
83
+ export interface Scored {
84
+ score?: number;
85
+ signals?: {
86
+ scorer: string;
87
+ weight: number;
88
+ why: string;
89
+ }[];
90
+ }
65
91
  /** A relationship's role and purpose to set; `null` clears one. */
66
92
  export interface RelationUpdate {
67
93
  role?: string | null;
@@ -72,3 +98,4 @@ export interface LinkDecision {
72
98
  by: Decider;
73
99
  reason?: string;
74
100
  }
101
+ export {};
@@ -1,5 +1,5 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
- import { assertMayDecide, deciderAuthor } from "../decisions.js";
2
+ import { assertMayDecide, botKey, deciderAuthor } from "../decisions.js";
3
3
  import { actorOfOrigin, botNamed, ownerPerson } from "./actors.js";
4
4
  import { insertLink, linkOf, purposeOf } from "./notes.js";
5
5
  import { proposalOperationsOver, proposerOfAction } from "./proposed-actions.js";
@@ -47,19 +47,74 @@ export const relationProposalsOver = (context) => {
47
47
  .run(Number("role" in changes), changes.role ?? null, Number("purpose" in changes), changes.purpose ?? null, now(), Number(link.id));
48
48
  return linkOf(database, database.prepare("SELECT * FROM links WHERE id = ?").get(Number(link.id)));
49
49
  };
50
+ /**
51
+ * A scan's newer score on a pending proposal, in place under the same id. A rule's own row takes the new score,
52
+ * signals, evidence and purpose; another proposer's row keeps its author, evidence and purpose and gains the signals.
53
+ */
54
+ const rescored = (action, input, scored, by) => {
55
+ const proposer = proposerOfAction(action);
56
+ const own = proposer.type === "bot" && proposer.kind === "rule" && botKey(proposer.name) === botKey(by.bot);
57
+ const before = action.payload;
58
+ const payload = own
59
+ ? {
60
+ ...before,
61
+ ...scored,
62
+ evidence: input.evidence ?? [],
63
+ ...(input.purpose === undefined ? {} : { purpose: purposeOf(input.purpose) }),
64
+ }
65
+ : { ...before, ...scored };
66
+ const reason = own && input.reason !== undefined ? input.reason : action.reason;
67
+ if (JSON.stringify(payload) === JSON.stringify(before) && reason === action.reason)
68
+ return action;
69
+ database
70
+ .prepare("UPDATE proposed_actions SET payload = ?, reason = ?, updated_at = ? WHERE id = ? AND status = 'proposed'")
71
+ .run(JSON.stringify(payload), reason, now(), Number(action.id));
72
+ return operations.get(action.id);
73
+ };
50
74
  return {
51
- /** The same pending pair returns the proposal already held. */
75
+ /**
76
+ * Where a pair stands before a scan proposes it: a link joins it, a `relationship` proposal for it was rejected,
77
+ * or one is pending. A rejection is matched by the pair alone, whoever proposed it and with whatever scorer.
78
+ */
79
+ state: (paired, { to, kind }) => {
80
+ let found;
81
+ try {
82
+ found = paired();
83
+ }
84
+ catch (error) {
85
+ if (error instanceof CliError && (error.code === "not_found" || error.code === "validation_error"))
86
+ return;
87
+ throw error;
88
+ }
89
+ const { from, existing } = found;
90
+ const rows = database
91
+ .prepare("SELECT id, status FROM proposed_actions WHERE kind='relationship' AND status IN ('proposed','rejected') " +
92
+ "AND target_type=? AND target_id=? AND json_extract(payload,'$.to')=? AND json_extract(payload,'$.kind')=? " +
93
+ "ORDER BY id")
94
+ .all(from.type, from.id, to, kind);
95
+ const pending = rows.find((row) => row.status === "proposed");
96
+ return {
97
+ linked: existing !== undefined,
98
+ confirmed: existing !== undefined && Number(existing.confirmed) === 1,
99
+ rejected: rows.some((row) => row.status === "rejected"),
100
+ pending: pending ? operations.get(String(pending.id)) : null,
101
+ };
102
+ },
103
+ /** The same pending pair returns the proposal already held; a scored one updates its score in place. */
52
104
  propose: (from, input, by) => {
53
105
  const evidence = input.evidence ?? [];
54
106
  if (!Array.isArray(evidence) || evidence.length > 5 || evidence.some((ref) => typeof ref !== "string"))
55
107
  throw new CliError("validation_error", "a relationship proposal cites up to 5 references");
108
+ const scored = scoredOf(input);
56
109
  return write(database, () => {
57
110
  const held = database
58
111
  .prepare("SELECT id FROM proposed_actions WHERE kind='relationship' AND status='proposed' AND target_type=? AND target_id=? " +
59
112
  "AND json_extract(payload,'$.to')=? AND json_extract(payload,'$.kind')=? ORDER BY id LIMIT 1")
60
113
  .get(from.type, from.id, input.to, input.kind);
61
- if (held)
62
- return operations.get(String(held.id));
114
+ if (held) {
115
+ const action = operations.get(String(held.id));
116
+ return scored ? rescored(action, input, scored, by) : action;
117
+ }
63
118
  return operations.propose({
64
119
  kind: "relationship",
65
120
  by,
@@ -71,6 +126,7 @@ export const relationProposalsOver = (context) => {
71
126
  ...(input.role === undefined ? {} : { role: input.role }),
72
127
  ...(input.purpose === undefined ? {} : { purpose: purposeOf(input.purpose) }),
73
128
  evidence,
129
+ ...scored,
74
130
  },
75
131
  ...(input.reason === undefined ? {} : { reason: input.reason }),
76
132
  });
@@ -174,6 +230,21 @@ export const relationProposalsOver = (context) => {
174
230
  }),
175
231
  };
176
232
  };
233
+ const scoredOf = (input) => {
234
+ if (input.score === undefined && input.signals === undefined)
235
+ return undefined;
236
+ const { score, signals } = input;
237
+ if (typeof score !== "number" || !(score >= 0 && score <= 1) || !Array.isArray(signals) || signals.length > 20)
238
+ throw new CliError("validation_error", "a scored proposal takes a score of 0–1 and up to 20 signals");
239
+ for (const signal of signals)
240
+ if (typeof signal?.scorer !== "string" ||
241
+ typeof signal.why !== "string" ||
242
+ signal.why.length > 500 ||
243
+ typeof signal.weight !== "number" ||
244
+ !(signal.weight >= 0 && signal.weight <= 1))
245
+ throw new CliError("validation_error", "a signal names its scorer, a weight of 0–1 and why, up to 500 characters");
246
+ return { score, signals: signals.map(({ scorer, weight, why }) => ({ scorer, weight, why })) };
247
+ };
177
248
  /** What a relationship update sets, checked; `null` clears a field and an absent one is left alone. */
178
249
  const changesOf = (input) => {
179
250
  const changes = {};
@@ -112,6 +112,7 @@ export interface MessageStore {
112
112
  forAccount(key: AccountKey): AccountStore;
113
113
  close(): Promise<void>;
114
114
  }
115
+ export type { PaymentFilter } from "./sqlite/facts.js";
115
116
  export type { ChatHistoryShape, StoredHistorySkip } from "./sqlite/history-skips.js";
116
117
  export { BACKFILL_ON_OPEN, type StoreOptions } from "./sqlite/open-store.js";
117
118
  export { fileFacts, readStoreTables, type TableKind } from "./sqlite/store-tables.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.549.0",
3
+ "version": "0.551.0",
4
4
  "description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",
@@ -251,7 +251,7 @@
251
251
  "@types/node": "^22.20.5",
252
252
  "@types/pngjs": "6.0.5",
253
253
  "@vitest/coverage-v8": "^5.0.5",
254
- "@wirecat/cli-base": "0.74.0",
254
+ "@wirecat/cli-base": "0.75.0",
255
255
  "@wirecat/cli-core-legacy-tags": "npm:@wirecat/cli-core@0.414.0",
256
256
  "commander": "15.0.0",
257
257
  "cspell": "10.4.0",
@@ -271,7 +271,7 @@
271
271
  },
272
272
  "peerDependencies": {
273
273
  "@napi-rs/canvas": ">=0.1.69 <2",
274
- "@wirecat/cli-base": ">=0.74.0 <0.75.0",
274
+ "@wirecat/cli-base": ">=0.75.0 <0.76.0",
275
275
  "commander": ">=15"
276
276
  },
277
277
  "bin": {
package/parity.json CHANGED
@@ -3715,6 +3715,17 @@
3715
3715
  "--sync-time": "all"
3716
3716
  }
3717
3717
  },
3718
+ "projects status": {
3719
+ "in": ["max", "tg"],
3720
+ "planned": {
3721
+ "wa": "wa backlog: mount projectsCommand once wa is on cli-core 0.504.0"
3722
+ },
3723
+ "options": {
3724
+ "--before-time": "all",
3725
+ "--context-query": "all",
3726
+ "--since-time": "all"
3727
+ }
3728
+ },
3718
3729
  "reactions": {
3719
3730
  "in": "all"
3720
3731
  },