@wirecat/cli-core 0.550.0 → 0.552.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 (190) hide show
  1. package/CHANGELOG.md +235 -0
  2. package/README.md +1 -1
  3. package/THIRD_PARTY_NOTICES +41 -0
  4. package/dist/background/schedules.d.ts +12 -1
  5. package/dist/background/schedules.js +65 -24
  6. package/dist/background/units.d.ts +16 -9
  7. package/dist/background/units.js +26 -11
  8. package/dist/calendar/ics.d.ts +1 -1
  9. package/dist/calendar/ics.js +5 -2
  10. package/dist/calendar/windows-zones.generated.d.ts +1 -0
  11. package/dist/calendar/windows-zones.generated.js +146 -0
  12. package/dist/cli/compact.js +2 -0
  13. package/dist/cli/context.d.ts +2 -0
  14. package/dist/cli/index.d.ts +3 -0
  15. package/dist/cli/index.js +3 -0
  16. package/dist/cli/meeting-findings-input.js +4 -3
  17. package/dist/cli/meeting-preparation-answer.d.ts +3 -0
  18. package/dist/cli/meeting-preparation-projects.d.ts +3 -0
  19. package/dist/cli/meeting-preparation-read.d.ts +3 -0
  20. package/dist/cli/messenger/backfill-command.js +28 -1
  21. package/dist/cli/messenger/context.js +1 -0
  22. package/dist/cli/messenger/history-gaps.js +4 -4
  23. package/dist/cli/messenger/mcp-command.js +1 -0
  24. package/dist/cli/messenger/messages-delete-command.js +7 -8
  25. package/dist/cli/messenger/projects-command.js +47 -2
  26. package/dist/cli/messenger/search-answers.js +8 -1
  27. package/dist/cli/messenger/serve-catch-up.d.ts +43 -0
  28. package/dist/cli/messenger/serve-catch-up.js +90 -0
  29. package/dist/cli/messenger/serve-command.js +54 -1
  30. package/dist/cli/messenger/server-command.d.ts +4 -3
  31. package/dist/cli/messenger/server-command.js +30 -12
  32. package/dist/cli/payment-query.d.ts +8 -0
  33. package/dist/cli/payment-query.js +27 -0
  34. package/dist/cli/program.js +9 -4
  35. package/dist/cli/project-status-rows.d.ts +8 -0
  36. package/dist/cli/project-status-rows.js +102 -0
  37. package/dist/cli/project-status.d.ts +132 -0
  38. package/dist/cli/project-status.js +248 -0
  39. package/dist/cli/schedule/schedule-command.js +5 -1
  40. package/dist/cli/steps.js +4 -1
  41. package/dist/domain/knowledge.d.ts +1 -1
  42. package/dist/domain/knowledge.js +1 -1
  43. package/dist/domain/status-words.d.ts +3 -0
  44. package/dist/domain/status-words.js +44 -0
  45. package/dist/entities/fact.d.ts +12 -1
  46. package/dist/entities/fact.js +13 -0
  47. package/dist/entities/hit.d.ts +5 -0
  48. package/dist/entities/hit.js +5 -0
  49. package/dist/entities/index.d.ts +45 -2
  50. package/dist/entities/listener.d.ts +6 -0
  51. package/dist/entities/listener.js +1 -0
  52. package/dist/entities/meeting-findings.d.ts +1 -1
  53. package/dist/entities/meeting-preparation.d.ts +3 -0
  54. package/dist/entities/message-transfer.d.ts +16 -0
  55. package/dist/entities/message-transfer.js +6 -0
  56. package/dist/errors/reasons.js +1 -0
  57. package/dist/mcp/app-server.js +1 -0
  58. package/dist/mcp/http/oauth.d.ts +2 -1
  59. package/dist/mcp/http/oauth.js +2 -2
  60. package/dist/mcp/http/serve.d.ts +2 -0
  61. package/dist/mcp/http/serve.js +2 -1
  62. package/dist/mcp/meeting-preparation.d.ts +3 -0
  63. package/dist/mcp/server.js +1 -0
  64. package/dist/mcp/surface.js +1 -1
  65. package/dist/meetings/answers.d.ts +5 -0
  66. package/dist/meetings/answers.js +8 -0
  67. package/dist/meetings/cli/index.js +14 -0
  68. package/dist/meetings/findings-rules.d.ts +14 -3
  69. package/dist/meetings/findings-rules.js +16 -2
  70. package/dist/meetings/index.d.ts +5 -3
  71. package/dist/meetings/index.js +3 -1
  72. package/dist/meetings/ingest.js +2 -1
  73. package/dist/meetings/language.d.ts +5 -0
  74. package/dist/meetings/language.js +57 -0
  75. package/dist/meetings/mcp/search.d.ts +3 -0
  76. package/dist/meetings/meeting-context.d.ts +2 -3
  77. package/dist/meetings/meeting-context.js +3 -4
  78. package/dist/meetings/payment.d.ts +27 -0
  79. package/dist/meetings/payment.js +81 -0
  80. package/dist/meetings/project-evidence-answer.d.ts +3 -0
  81. package/dist/meetings/result-items.d.ts +9 -0
  82. package/dist/meetings/result-items.js +7 -0
  83. package/dist/meetings/store.d.ts +22 -0
  84. package/dist/meetings/testing/memory.js +3 -0
  85. package/dist/meetings/transcription.d.ts +2 -0
  86. package/dist/meetings/transcription.js +5 -1
  87. package/dist/search/lucene/types.d.ts +7 -0
  88. package/dist/search/lucene/types.js +11 -1
  89. package/dist/services/archive.d.ts +13 -0
  90. package/dist/services/archive.js +56 -5
  91. package/dist/services/deps.d.ts +3 -0
  92. package/dist/services/history-limits.d.ts +2 -0
  93. package/dist/services/index.d.ts +4 -1
  94. package/dist/services/index.js +4 -1
  95. package/dist/services/job-query.d.ts +5 -2
  96. package/dist/services/job-query.js +5 -2
  97. package/dist/services/link-scorers/alias-hits.d.ts +7 -0
  98. package/dist/services/link-scorers/alias-hits.js +54 -0
  99. package/dist/services/link-scorers/chat-senders.d.ts +6 -0
  100. package/dist/services/link-scorers/chat-senders.js +56 -0
  101. package/dist/services/link-scorers/chat-title.d.ts +6 -0
  102. package/dist/services/link-scorers/chat-title.js +44 -0
  103. package/dist/services/link-scorers/email-domain.d.ts +6 -0
  104. package/dist/services/link-scorers/email-domain.js +64 -0
  105. package/dist/services/link-scorers/index.d.ts +9 -0
  106. package/dist/services/link-scorers/index.js +8 -0
  107. package/dist/services/link-scorers/mail-thread.d.ts +3 -0
  108. package/dist/services/link-scorers/mail-thread.js +74 -0
  109. package/dist/services/link-scorers/member-chats.d.ts +3 -0
  110. package/dist/services/link-scorers/member-chats.js +40 -0
  111. package/dist/services/link-scorers/member-dialog.d.ts +3 -0
  112. package/dist/services/link-scorers/member-dialog.js +34 -0
  113. package/dist/services/link-scorers/scan.d.ts +41 -0
  114. package/dist/services/link-scorers/scan.js +244 -0
  115. package/dist/services/link-scorers/shared.d.ts +26 -0
  116. package/dist/services/link-scorers/shared.js +48 -0
  117. package/dist/services/link-scorers/types.d.ts +77 -0
  118. package/dist/services/link-scorers/types.js +1 -0
  119. package/dist/services/messages-archive-search.js +3 -2
  120. package/dist/services/messages-combined.js +2 -2
  121. package/dist/services/messages-discovery.js +2 -2
  122. package/dist/services/notes-search.js +4 -5
  123. package/dist/services/project-status/index.d.ts +71 -0
  124. package/dist/services/project-status/index.js +196 -0
  125. package/dist/services/project-status/items.d.ts +10 -0
  126. package/dist/services/project-status/items.js +23 -0
  127. package/dist/services/project-status/signals/amounts.d.ts +20 -0
  128. package/dist/services/project-status/signals/amounts.js +91 -0
  129. package/dist/services/project-status/signals/children.d.ts +13 -0
  130. package/dist/services/project-status/signals/children.js +28 -0
  131. package/dist/services/project-status/signals/coverage.d.ts +13 -0
  132. package/dist/services/project-status/signals/coverage.js +30 -0
  133. package/dist/services/project-status/signals/index.d.ts +3 -0
  134. package/dist/services/project-status/signals/index.js +21 -0
  135. package/dist/services/project-status/signals/last-message.d.ts +4 -0
  136. package/dist/services/project-status/signals/last-message.js +22 -0
  137. package/dist/services/project-status/signals/payments.d.ts +9 -0
  138. package/dist/services/project-status/signals/payments.js +24 -0
  139. package/dist/services/project-status/signals/promises.d.ts +17 -0
  140. package/dist/services/project-status/signals/promises.js +109 -0
  141. package/dist/services/project-status/signals/questions.d.ts +8 -0
  142. package/dist/services/project-status/signals/questions.js +38 -0
  143. package/dist/services/project-status/signals/reply-next.d.ts +19 -0
  144. package/dist/services/project-status/signals/reply-next.js +75 -0
  145. package/dist/services/project-status/signals/topics.d.ts +10 -0
  146. package/dist/services/project-status/signals/topics.js +50 -0
  147. package/dist/services/project-status/types.d.ts +84 -0
  148. package/dist/services/project-status/types.js +1 -0
  149. package/dist/services/proposals/index.d.ts +1 -0
  150. package/dist/services/proposals/index.js +1 -0
  151. package/dist/services/proposals/kinds/findings.d.ts +3 -0
  152. package/dist/services/proposals/kinds/findings.js +8 -1
  153. package/dist/services/search-all-meetings.d.ts +3 -0
  154. package/dist/services/search-all-meetings.js +6 -1
  155. package/dist/speech/local.d.ts +2 -0
  156. package/dist/speech/local.js +8 -1
  157. package/dist/speech/models.d.ts +2 -0
  158. package/dist/speech/models.js +2 -0
  159. package/dist/store/decisions.d.ts +5 -1
  160. package/dist/store/decisions.js +5 -1
  161. package/dist/store/driver.d.ts +11 -0
  162. package/dist/store/driver.js +13 -0
  163. package/dist/store/drivers/node-sqlite.d.ts +1 -1
  164. package/dist/store/drivers/node-sqlite.js +3 -0
  165. package/dist/store/evidence-findings.js +1 -0
  166. package/dist/store/index.d.ts +2 -1
  167. package/dist/store/index.js +1 -0
  168. package/dist/store/project-transfer/request.d.ts +1 -1
  169. package/dist/store/project-transfer/schema.d.ts +2 -2
  170. package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
  171. package/dist/store/sqlite/evidence-findings.js +5 -1
  172. package/dist/store/sqlite/facts.d.ts +13 -0
  173. package/dist/store/sqlite/facts.js +64 -6
  174. package/dist/store/sqlite/findings-core.js +14 -1
  175. package/dist/store/sqlite/knowledge.d.ts +16 -2
  176. package/dist/store/sqlite/knowledge.js +1 -0
  177. package/dist/store/sqlite/lucene.d.ts +1 -1
  178. package/dist/store/sqlite/lucene.js +33 -7
  179. package/dist/store/sqlite/meeting-hit-search.js +10 -3
  180. package/dist/store/sqlite/meeting-languages.d.ts +4 -0
  181. package/dist/store/sqlite/meeting-languages.js +36 -0
  182. package/dist/store/sqlite/meetings.d.ts +2 -1
  183. package/dist/store/sqlite/meetings.js +9 -3
  184. package/dist/store/sqlite/note-search.js +2 -2
  185. package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
  186. package/dist/store/sqlite/relation-proposals.d.ts +29 -2
  187. package/dist/store/sqlite/relation-proposals.js +75 -4
  188. package/dist/store/store.d.ts +1 -0
  189. package/package.json +3 -3
  190. package/parity.json +11 -0
@@ -8,6 +8,7 @@ export const MODELS = [
8
8
  id: "gigaam-v3",
9
9
  title: "GigaAM v3, with punctuation",
10
10
  languages: "Russian — the best of the three for Russian",
11
+ language: "ru",
11
12
  featureDim: 64,
12
13
  files: [
13
14
  {
@@ -45,6 +46,7 @@ export const MODELS = [
45
46
  id: "gigaam-v3-ctc",
46
47
  title: "GigaAM v3 CTC, with punctuation — a little faster",
47
48
  languages: "Russian — a little faster, rougher with capital letters",
49
+ language: "ru",
48
50
  featureDim: 64,
49
51
  files: [
50
52
  {
@@ -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: (row: {
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" | {
@@ -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 = (row, by, policy = DEFAULT_DECISION_POLICY) => {
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)
@@ -23,7 +23,18 @@ export interface CacheDatabase {
23
23
  exec(sql: string): void;
24
24
  prepare(sql: string): CacheStatement;
25
25
  close(): void;
26
+ /**
27
+ * Set when the connection has `TIME_LIMIT_FUNCTION`. A statement runs to its end before any timer can fire, so a
28
+ * long one is stopped from inside: `WHERE … AND wc_time_limit(<epoch ms>)` throws `TimeLimitReached` once past it.
29
+ */
30
+ timeLimit?: true;
26
31
  }
32
+ export declare const TIME_LIMIT_FUNCTION = "wc_time_limit";
33
+ export declare class TimeLimitReached extends Error {
34
+ constructor();
35
+ }
36
+ /** The SQL function's body: 1 until `until` (epoch milliseconds) has passed. */
37
+ export declare const beforeTimeLimit: (until: unknown) => number;
27
38
  export type OpenDatabase = (path: string) => CacheDatabase;
28
39
  /**
29
40
  * **Write-ahead mode, and a timeout rather than an instant failure.**
@@ -9,6 +9,19 @@
9
9
  * The surface is deliberately tiny. Everything above it is ordinary SQL, so replacing the driver —
10
10
  * or adding a third — is writing thirty lines, not learning an abstraction.
11
11
  */
12
+ export const TIME_LIMIT_FUNCTION = "wc_time_limit";
13
+ export class TimeLimitReached extends Error {
14
+ constructor() {
15
+ super("the statement ran past its time limit");
16
+ this.name = "TimeLimitReached";
17
+ }
18
+ }
19
+ /** The SQL function's body: 1 until `until` (epoch milliseconds) has passed. */
20
+ export const beforeTimeLimit = (until) => {
21
+ if (Date.now() > Number(until))
22
+ throw new TimeLimitReached();
23
+ return 1;
24
+ };
12
25
  /**
13
26
  * **Write-ahead mode, and a timeout rather than an instant failure.**
14
27
  *
@@ -1,5 +1,5 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
- import type { CacheDatabase } from "../driver.js";
2
+ import { type CacheDatabase } from "../driver.js";
3
3
  /**
4
4
  * Import this module only through a dynamic import that follows `quietSqliteWarning()`: a static import loads
5
5
  * `node:sqlite`, and on Node 22 its ExperimentalWarning, before the filter can run.
@@ -1,5 +1,6 @@
1
1
  // biome-ignore-all lint/plugin/no-sqlite-driver: cli-core owns the store, so it opens the driver itself
2
2
  import { DatabaseSync } from "node:sqlite";
3
+ import { beforeTimeLimit, TIME_LIMIT_FUNCTION, } from "../driver.js";
3
4
  import { loadVectorExtension, vectorExtension } from "../vector-extension.js";
4
5
  /**
5
6
  * Import this module only through a dynamic import that follows `quietSqliteWarning()`: a static import loads
@@ -29,6 +30,7 @@ export const nodeClient = async (path, { readOnly = false } = {}) => {
29
30
  export const STATEMENT_CACHE_SIZE = 1000;
30
31
  export const cacheOverNodeSqlite = (database, cacheSize = STATEMENT_CACHE_SIZE) => {
31
32
  const statements = new Map();
33
+ database.function(TIME_LIMIT_FUNCTION, { deterministic: false, directOnly: true }, beforeTimeLimit);
32
34
  const prepare = (sql) => {
33
35
  const statement = database.prepare(sql);
34
36
  return {
@@ -38,6 +40,7 @@ export const cacheOverNodeSqlite = (database, cacheSize = STATEMENT_CACHE_SIZE)
38
40
  };
39
41
  };
40
42
  return {
43
+ timeLimit: true,
41
44
  exec: (sql) => database.exec(sql),
42
45
  prepare: (sql) => {
43
46
  const cached = statements.get(sql);
@@ -18,4 +18,5 @@ export const validateEvidenceFindings = (input) => validateFindings(input, {
18
18
  cited: "reference",
19
19
  reference,
20
20
  anySubject: true,
21
+ payments: true,
21
22
  });
@@ -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";
@@ -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({ id, proposedBy: proposals.proposer(id) }, decision.by, context.decisions);
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({ 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));
@@ -1,6 +1,6 @@
1
1
  import type { Page } from "../../domain/models.js";
2
2
  import type { QueryExecution } from "../../search/lucene/resolved.js";
3
- import type { SqlValue } from "../driver.js";
3
+ import { type SqlValue } from "../driver.js";
4
4
  import type { StoreContext } from "./open.js";
5
5
  import type { ScoredHit } from "./words.js";
6
6
  export { type Corpus, prefixOf } from "./lucene-fields.js";
@@ -1,7 +1,8 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { compileAutomaton } from "../../search/lucene/automaton.js";
3
3
  import { isStemmed } from "../../search/lucene/resolved.js";
4
- import { exhausted, QUERY_LIMITS, searchError } from "../../search/lucene/types.js";
4
+ import { exhausted, outOfTime, QUERY_LIMITS, searchError } from "../../search/lucene/types.js";
5
+ import { TIME_LIMIT_FUNCTION, TimeLimitReached } from "../driver.js";
5
6
  import { emailHitsByPk } from "./emails.js";
6
7
  import { placeHits } from "./hit-places.js";
7
8
  import { combine, FIELD_COMPILERS, ftsMatch, SPEECH, } from "./lucene-fields.js";
@@ -54,7 +55,7 @@ export const countMatches = (context, execution, cap) => matchingPks(context, ex
54
55
  * The keys of the messages that match, no more than `cap + 1`: one SQL query, unranked. `undefined` when a leaf is
55
56
  * checked row by row (a preset, a pattern), where it would cost as much as the search itself.
56
57
  */
57
- export const matchingPks = (context, execution, cap) => {
58
+ export const matchingPks = (context, execution, cap) => inTime(() => {
58
59
  const { database, check, where, from, withRanking, ranked, leaves } = compileQuery(context, {
59
60
  ...execution,
60
61
  newest: true,
@@ -66,6 +67,19 @@ export const matchingPks = (context, execution, cap) => {
66
67
  .all(...ranked.params, ...where.params, cap + 1);
67
68
  check();
68
69
  return rows.map(({ id }) => Number(id));
70
+ });
71
+ const timeLimited = (error) => {
72
+ if (error instanceof TimeLimitReached)
73
+ outOfTime();
74
+ throw error;
75
+ };
76
+ const inTime = (work) => {
77
+ try {
78
+ return work();
79
+ }
80
+ catch (error) {
81
+ return timeLimited(error);
82
+ }
69
83
  };
70
84
  const compileQuery = (context, execution, boundedAttachments = false) => {
71
85
  const { database } = context;
@@ -74,6 +88,7 @@ const compileQuery = (context, execution, boundedAttachments = false) => {
74
88
  if (mail && (execution.conversationIds || execution.conversationSince !== undefined))
75
89
  throw new CliError("validation_error", "mail has no conversations to search within");
76
90
  const started = context.now();
91
+ const until = Date.now() + QUERY_LIMITS.milliseconds;
77
92
  const budget = { work: 0 };
78
93
  let states = 0, expansions = 0;
79
94
  const patterns = new Map();
@@ -82,7 +97,7 @@ const compileQuery = (context, execution, boundedAttachments = false) => {
82
97
  if (execution.signal?.aborted)
83
98
  throw new CliError("validation_error", "search was aborted", { reason: "query.aborted", complete: false });
84
99
  if (context.now() - started > QUERY_LIMITS.milliseconds)
85
- exhausted("time");
100
+ outOfTime();
86
101
  };
87
102
  const patternOf = (pattern, node) => {
88
103
  check();
@@ -197,7 +212,10 @@ const compileQuery = (context, execution, boundedAttachments = false) => {
197
212
  exact: true,
198
213
  })), "OR"),
199
214
  ], "AND");
200
- let where = combine([{ sql: "m.deleted_at IS NULL", params: [], exact: true }, scope, expression], "AND");
215
+ const timeLimit = database.timeLimit
216
+ ? [{ sql: `${TIME_LIMIT_FUNCTION}(?)`, params: [until], exact: true }]
217
+ : [];
218
+ let where = combine([{ sql: "m.deleted_at IS NULL", params: [], exact: true }, scope, expression, ...timeLimit], "AND");
201
219
  if (execution.conversationIds !== undefined || execution.conversationSince !== undefined) {
202
220
  const ids = execution.conversationIds;
203
221
  where = combine([
@@ -404,7 +422,7 @@ const compileQuery = (context, execution, boundedAttachments = false) => {
404
422
  /** The matcher and its consumer run synchronously in one read snapshot; exact keys stay in SQL. */
405
423
  export const withQuerySelection = (context, execution, consume) => {
406
424
  const { database } = context;
407
- return read(database, () => {
425
+ return inTime(() => read(database, () => {
408
426
  const query = compileQuery(context, execution, true);
409
427
  const { check, leaves, where, from, withRanking, ranked } = query;
410
428
  check();
@@ -448,9 +466,17 @@ export const withQuerySelection = (context, execution, consume) => {
448
466
  throw new Error("query selection consumers must run synchronously");
449
467
  check();
450
468
  return result;
451
- });
469
+ }));
470
+ };
471
+ const runQuery = (context, execution, by) => {
472
+ try {
473
+ return runCompiled(context, execution, by).catch(timeLimited);
474
+ }
475
+ catch (error) {
476
+ return timeLimited(error);
477
+ }
452
478
  };
453
- const runQuery = async (context, execution, by) => {
479
+ const runCompiled = async (context, execution, by) => {
454
480
  const { corpus, rowsFrom, hits, database, check, grouped, where, from, withRanking, ranked, leaves, stemmed, exactTier, driver, baseFrom, filtered, ranking, relevance, rankedFrom, exactColumn, order, placeTier, projection, projectionParams, joinedFrom, evaluate, } = compileQuery(context, execution);
455
481
  if (by === "pks" && leaves.every(({ test }) => !test)) {
456
482
  const rows = database
@@ -121,9 +121,9 @@ export function meetingHitSearchOver({ database }) {
121
121
  if (after)
122
122
  params.push(after.tier, after.tier, after.started, after.started, after.meeting, after.meeting, after.kind, after.kind, after.id);
123
123
  const sql = `WITH matches AS (${matching}), matching AS (SELECT rid,tier FROM matches h WHERE NOT EXISTS(SELECT 1 FROM meeting_index_pending p WHERE p.id=h.rid/4 AND p.indexable_type=CASE h.rid%4 WHEN 1 THEN 'meeting_transcript_row' WHEN 2 THEN 'meeting_chat_message' ELSE 'meeting_summary' END)), selected AS (SELECT m.id,coalesce(m.started_at,0) AS started FROM meetings m WHERE ${clauses.join(" AND ")}), hits AS (
124
- SELECT h.tier,m.started,m.id AS meeting_id,0 AS kind,r.id,r.start_ms,${bytes("r", ["text"])} AS bytes,t.id AS transcript_id,r.position FROM matching h JOIN meeting_transcript_rows r ON h.rid%4=1 AND r.id=h.rid/4 JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id AND t.superseded_at IS NULL AND t.deleted_at IS NULL JOIN selected m ON m.id=t.meeting_id
125
- UNION ALL SELECT h.tier,m.started,m.id,1,c.id,NULL,${bytes("c", ["text"])},NULL,NULL FROM matching h JOIN meeting_chat_messages c ON h.rid%4=2 AND c.id=h.rid/4 JOIN selected m ON m.id=c.meeting_id
126
- UNION ALL SELECT h.tier,m.started,m.id,2,s.id,NULL,${bytes("s", fields)},NULL,NULL FROM matching h JOIN meeting_summaries s ON h.rid%4=3 AND s.id=h.rid/4 JOIN selected m ON m.id=s.meeting_id)
124
+ SELECT h.tier,m.started,m.id AS meeting_id,0 AS kind,r.id,r.start_ms,${bytes("r", ["text"])} AS bytes,t.id AS transcript_id,r.position,t.language,(SELECT count(*) FROM meeting_transcript_rows n WHERE n.meeting_transcript_id=t.id) AS lines_total FROM matching h JOIN meeting_transcript_rows r ON h.rid%4=1 AND r.id=h.rid/4 JOIN meeting_transcripts t ON t.id=r.meeting_transcript_id AND t.superseded_at IS NULL AND t.deleted_at IS NULL JOIN selected m ON m.id=t.meeting_id
125
+ UNION ALL SELECT h.tier,m.started,m.id,1,c.id,NULL,${bytes("c", ["text"])},NULL,NULL,NULL,NULL FROM matching h JOIN meeting_chat_messages c ON h.rid%4=2 AND c.id=h.rid/4 JOIN selected m ON m.id=c.meeting_id
126
+ UNION ALL SELECT h.tier,m.started,m.id,2,s.id,NULL,${bytes("s", fields)},NULL,NULL,NULL,NULL FROM matching h JOIN meeting_summaries s ON h.rid%4=3 AND s.id=h.rid/4 JOIN selected m ON m.id=s.meeting_id)
127
127
  SELECT h.*,${bytes("m", ["title"])}+${bytes("s", ["doc_url"])} AS context_bytes FROM hits h
128
128
  JOIN meetings m ON m.id=h.meeting_id LEFT JOIN meeting_summaries s ON h.kind=2 AND s.id=h.id
129
129
  ${seek} ORDER BY h.tier,h.started DESC,h.meeting_id,h.kind,h.id LIMIT ?`;
@@ -176,6 +176,13 @@ export function meetingHitSearchOver({ database }) {
176
176
  id: Number(row.id),
177
177
  text,
178
178
  startMs: row.start_ms === null ? null : Number(row.start_ms),
179
+ ...(kind === 0
180
+ ? {
181
+ language: row.language === null ? null : String(row.language),
182
+ line: Number(row.position) + 1,
183
+ linesTotal: Number(row.lines_total),
184
+ }
185
+ : {}),
179
186
  };
180
187
  });
181
188
  const fresh = items.flatMap((item) => {
@@ -0,0 +1,4 @@
1
+ import { type MeetingStore } from "../../meetings/index.js";
2
+ import type { StoreContext } from "./open.js";
3
+ export type MeetingLanguageStore = Required<Pick<MeetingStore, "fillTranscriptLanguages">>;
4
+ export declare const meetingLanguagesOver: ({ database }: StoreContext) => MeetingLanguageStore;