@wirecat/cli-core 0.362.0 → 0.364.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 (56) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cli/index.d.ts +1 -1
  3. package/dist/cli/index.js +1 -1
  4. package/dist/cli/meeting-findings-input.d.ts +2 -1
  5. package/dist/cli/meeting-findings-input.js +10 -7
  6. package/dist/cli/messenger/context.d.ts +2 -2
  7. package/dist/cli/messenger/context.js +6 -2
  8. package/dist/cli/messenger/flooded.d.ts +2 -1
  9. package/dist/cli/messenger/flooded.js +32 -12
  10. package/dist/cli/messenger/replies-command.js +7 -5
  11. package/dist/cli/messenger/replies-edit-command.js +3 -2
  12. package/dist/cli/messenger/serve-command.js +3 -2
  13. package/dist/cli/messenger/serve-relay.d.ts +1 -1
  14. package/dist/cli/messenger/stored.js +5 -1
  15. package/dist/context/evidence.d.ts +6 -0
  16. package/dist/context/evidence.js +4 -0
  17. package/dist/errors/reasons.js +2 -0
  18. package/dist/meetings/findings-rules.d.ts +60 -0
  19. package/dist/meetings/findings-rules.js +117 -0
  20. package/dist/meetings/findings.d.ts +10 -48
  21. package/dist/meetings/findings.js +10 -116
  22. package/dist/meetings/index.d.ts +1 -0
  23. package/dist/meetings/index.js +1 -0
  24. package/dist/replies/rendering.d.ts +2 -1
  25. package/dist/replies/rendering.js +2 -2
  26. package/dist/replies/rules.d.ts +6 -1
  27. package/dist/replies/rules.js +12 -4
  28. package/dist/replies/serve.d.ts +2 -1
  29. package/dist/replies/serve.js +2 -2
  30. package/dist/replies/template.d.ts +3 -0
  31. package/dist/replies/template.js +14 -2
  32. package/dist/sends/guard.d.ts +3 -2
  33. package/dist/sends/guard.js +5 -1
  34. package/dist/services/context-evidence.d.ts +2 -1
  35. package/dist/services/context-evidence.js +1 -0
  36. package/dist/services/index.d.ts +3 -2
  37. package/dist/services/index.js +2 -1
  38. package/dist/services/project-gathering.d.ts +1 -0
  39. package/dist/services/project-gathering.js +3 -0
  40. package/dist/services/proposal-tasks.js +5 -0
  41. package/dist/store/evidence-findings.d.ts +63 -0
  42. package/dist/store/evidence-findings.js +15 -0
  43. package/dist/store/index.d.ts +1 -0
  44. package/dist/store/index.js +1 -0
  45. package/dist/store/sqlite/context-evidence.d.ts +9 -1
  46. package/dist/store/sqlite/context-evidence.js +43 -1
  47. package/dist/store/sqlite/evidence-findings.d.ts +3 -0
  48. package/dist/store/sqlite/evidence-findings.js +172 -0
  49. package/dist/store/sqlite/findings-core.d.ts +17 -0
  50. package/dist/store/sqlite/findings-core.js +108 -0
  51. package/dist/store/sqlite/meeting-findings.js +47 -121
  52. package/dist/store/sqlite/proposed-actions.d.ts +4 -1
  53. package/dist/store/sqlite/proposed-actions.js +12 -4
  54. package/dist/store/store.d.ts +5 -1
  55. package/dist/store/store.js +10 -1
  56. package/package.json +1 -1
@@ -1,119 +1,13 @@
1
- import { createHash } from "node:crypto";
2
1
  import { CliError } from "@wirecat/cli-base";
3
- import { MEETING_FACT_TYPES } from "./meeting-context.js";
2
+ import { findingFingerprint, validateFindings, } from "./findings-rules.js";
4
3
  import { canonicalMeetingReference, parseMeetingReference } from "./reference.js";
5
- import { meetingTimestamp } from "./timestamp.js";
6
- const MEETING_TASK_TYPES = ["bug", "feature", "chore"];
4
+ export { findingHash } from "./findings-rules.js";
7
5
  export const MEETING_FINDINGS_KIND = "meeting_findings";
8
- const invalid = (message) => {
9
- throw new CliError("validation_error", message);
10
- };
11
- const text = (value, limit) => {
12
- if (typeof value !== "string" || !value.trim() || value.includes("\0") || value.length > limit)
13
- return invalid(`finding text must contain 1–${limit} characters without NUL`);
14
- return value.trim();
15
- };
16
- export const findingHash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
17
- const fields = (value, allowed) => {
18
- if (Object.keys(value).some((key) => !allowed.includes(key)))
19
- invalid("unknown meeting finding field");
20
- };
21
- export const validateMeetingFindings = (input) => {
22
- if (!input || typeof input !== "object" || Buffer.byteLength(JSON.stringify(input), "utf8") > 65536)
23
- return invalid("meeting findings take at most 65536 UTF-8 bytes");
24
- fields(input, ["schemaVersion", "key", "extractor", "findings"]);
25
- if (input.schemaVersion !== 1 ||
26
- !Array.isArray(input.findings) ||
27
- input.findings.length < 1 ||
28
- input.findings.length > 16)
29
- return invalid("schemaVersion 1 takes 1–16 meeting findings");
30
- const key = text(input.key, 64);
31
- if (!/^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/.test(key))
32
- return invalid("a findings key uses letters, digits, dot, colon, underscore or hyphen");
33
- const ids = new Set();
34
- const findings = input.findings.map((finding) => {
35
- if (!finding || typeof finding !== "object")
36
- return invalid("a finding is an object");
37
- const id = text(finding.id, 64);
38
- if (!/^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/.test(id) || ids.has(id))
39
- return invalid("finding IDs must be unique simple keys");
40
- ids.add(id);
41
- const project = text(finding.project, 64);
42
- if (!/^project:[1-9]\d*$/.test(project))
43
- return invalid("a finding names a project reference");
44
- if (!Array.isArray(finding.evidence) || finding.evidence.length < 1 || finding.evidence.length > 8)
45
- return invalid("a finding requires 1–8 cited cues");
46
- const evidence = finding.evidence
47
- .map((item) => {
48
- if (!item || typeof item !== "object")
49
- return invalid("finding evidence is a reference and fingerprint");
50
- fields(item, ["reference", "fingerprint"]);
51
- if (typeof item.reference !== "string" || typeof item.fingerprint !== "string")
52
- return invalid("evidence requires a reference and fingerprint");
53
- const reference = canonicalMeetingReference(item.reference);
54
- if (parseMeetingReference(reference).cuePosition === undefined || !/^[a-f0-9]{64}$/.test(item.fingerprint))
55
- return invalid("finding evidence requires exact cue references and fingerprints");
56
- return { reference, fingerprint: item.fingerprint };
57
- })
58
- .sort((a, b) => a.reference.localeCompare(b.reference));
59
- if (new Set(evidence.map((item) => item.reference)).size !== evidence.length)
60
- return invalid("duplicate evidence cue");
61
- const base = { id, project, evidence };
62
- if (finding.kind === "fact") {
63
- fields(finding, ["id", "project", "evidence", "kind", "type", "statement"]);
64
- if (!MEETING_FACT_TYPES.includes(finding.type))
65
- return invalid("unknown fact finding type");
66
- return { ...base, kind: "fact", type: finding.type, statement: text(finding.statement, 2000) };
67
- }
68
- if (finding.kind !== "task")
69
- return invalid("a finding is a fact or task");
70
- fields(finding, [
71
- "id",
72
- "project",
73
- "evidence",
74
- "kind",
75
- "type",
76
- "title",
77
- "reason",
78
- "commitment",
79
- "dueAt",
80
- "dueEvidence",
81
- "assignee",
82
- "assigneeEvidence",
83
- ]);
84
- if (!MEETING_TASK_TYPES.includes(finding.type))
85
- return invalid("a task finding type is bug, chore or feature");
86
- if (!["explicit", "inferred"].includes(finding.commitment))
87
- return invalid("task commitment must be explicit or inferred");
88
- const support = (value) => {
89
- if (!Array.isArray(value) ||
90
- value.length < 1 ||
91
- value.length > 8 ||
92
- value.some((ref) => typeof ref !== "string" || !evidence.some((item) => item.reference === ref)))
93
- return invalid("date and assignee support must cite the finding's evidence");
94
- return [...new Set(value)].sort();
95
- };
96
- if ((finding.dueAt === undefined) !== (finding.dueEvidence === undefined) ||
97
- (finding.assignee === undefined) !== (finding.assigneeEvidence === undefined))
98
- return invalid("optional dates and assignees require their supporting evidence");
99
- if (finding.assignee !== undefined &&
100
- (typeof finding.assignee !== "string" || !/^person:[1-9]\d*$/.test(finding.assignee)))
101
- return invalid("an assignee must be a person reference");
102
- return {
103
- ...base,
104
- kind: "task",
105
- type: finding.type,
106
- title: text(finding.title, 300),
107
- reason: text(finding.reason, 2000),
108
- commitment: finding.commitment,
109
- ...(finding.dueAt === undefined
110
- ? {}
111
- : { dueAt: meetingTimestamp(finding.dueAt, "finding due date"), dueEvidence: support(finding.dueEvidence) }),
112
- ...(finding.assignee === undefined
113
- ? {}
114
- : { assignee: finding.assignee, assigneeEvidence: support(finding.assigneeEvidence) }),
115
- };
116
- });
117
- return { schemaVersion: 1, key, extractor: text(input.extractor, 200), findings };
118
- };
119
- export const meetingFindingFingerprint = ({ id: _id, ...finding }) => findingHash(finding);
6
+ const cue = (raw) => {
7
+ const reference = canonicalMeetingReference(raw);
8
+ if (parseMeetingReference(reference).cuePosition === undefined)
9
+ throw new CliError("validation_error", "finding evidence requires exact cue references and fingerprints");
10
+ return reference;
11
+ };
12
+ export const validateMeetingFindings = (input) => validateFindings(input, { plural: "meeting findings", singular: "meeting finding", cited: "cue", reference: cue });
13
+ export const meetingFindingFingerprint = findingFingerprint;
@@ -5,6 +5,7 @@ export { type MeetingCueIdentity, meetingCueFingerprint } from "./cue-fingerprin
5
5
  export { linkEvent } from "./events.js";
6
6
  export { exportMeeting, type MeetingExport, type MeetingExportFormat, type MeetingExportOptions, serializeVtt, } from "./export.js";
7
7
  export { type FindingEvidence, type FindingOutcome, findingHash, MEETING_FINDINGS_KIND, type MeetingFactFinding, type MeetingFinding, type MeetingFindingsInput, type MeetingFindingsReceipt, type MeetingTaskFinding, meetingFindingFingerprint, validateMeetingFindings, } from "./findings.js";
8
+ export { type Finding, type FindingsInput, type FindingsReceipt, type FindingsRules, findingFingerprint, validateFindings, } from "./findings-rules.js";
8
9
  export { type ImportFile, importFiles } from "./import.js";
9
10
  export { MEETING_QUERY, type MeetingItem, type MeetingOwner, meeting, meetingItem } from "./item.js";
10
11
  export { createMeetingContext, MEETING_FACT_TYPES, type MeetingContextAction, type MeetingContextChange, type MeetingContextOperation, type MeetingContextPorts, type MeetingContextReceipt, validateMeetingContextChange, } from "./meeting-context.js";
@@ -3,6 +3,7 @@ export { meetingCueFingerprint } from "./cue-fingerprint.js";
3
3
  export { linkEvent } from "./events.js";
4
4
  export { exportMeeting, serializeVtt, } from "./export.js";
5
5
  export { findingHash, MEETING_FINDINGS_KIND, meetingFindingFingerprint, validateMeetingFindings, } from "./findings.js";
6
+ export { findingFingerprint, validateFindings, } from "./findings-rules.js";
6
7
  export { importFiles } from "./import.js";
7
8
  export { MEETING_QUERY, meeting, meetingItem } from "./item.js";
8
9
  export { createMeetingContext, MEETING_FACT_TYPES, validateMeetingContextChange, } from "./meeting-context.js";
@@ -2,6 +2,7 @@ import { type KeyringStore } from "@wirecat/cli-base";
2
2
  import type { Chat, Message } from "../domain/models.js";
3
3
  import { type ModelSettings } from "../models/index.js";
4
4
  import type { AppIdentity } from "../ports/app.js";
5
+ import type { Warn } from "../runs/events.js";
5
6
  import type { ReplyRule } from "./rules.js";
6
7
  import { type TemplateResult } from "./template.js";
7
8
  export type ReplyChat = Pick<Chat, "id" | "kind"> & {
@@ -10,7 +11,7 @@ export type ReplyChat = Pick<Chat, "id" | "kind"> & {
10
11
  export type ReplyRender = (rule: ReplyRule, message: Message, chat: ReplyChat, now: number) => Promise<TemplateResult>;
11
12
  export declare const replyRenderer: (app: AppIdentity, profile: string, settings: () => {
12
13
  models?: ModelSettings;
13
- }, env: NodeJS.ProcessEnv, warn: (message: string) => void, options?: {
14
+ }, env: NodeJS.ProcessEnv, warn: Warn, options?: {
14
15
  ai?: boolean;
15
16
  preview?: boolean;
16
17
  signal?: AbortSignal;
@@ -2,7 +2,7 @@ import { CliError } from "@wirecat/cli-base";
2
2
  import { embeddingKeys, endpointKeyName } from "../embeddings/keys.js";
3
3
  import { modelGateway, modelTarget } from "../models/index.js";
4
4
  import { mayModelReply, replyConsentPathFor, replyModelIdentity } from "./consents.js";
5
- import { renderReplyTemplate } from "./template.js";
5
+ import { renderReplyTemplate, templateWarning } from "./template.js";
6
6
  export const replyRenderer = (app, profile, settings, env, warn, options = {
7
7
  ai: true,
8
8
  }) => async (rule, message, chat, now) => {
@@ -48,6 +48,6 @@ export const replyRenderer = (app, profile, settings, env, warn, options = {
48
48
  },
49
49
  });
50
50
  for (const warning of result.warnings)
51
- warn(warning);
51
+ warn(warning, templateWarning(warning));
52
52
  return result;
53
53
  };
@@ -1,5 +1,6 @@
1
1
  import * as v from "valibot";
2
2
  import type { AppIdentity } from "../ports/app.js";
3
+ import type { WarningEvent } from "../runs/events.js";
3
4
  export declare const REPLY_ACTIONS: readonly ["reply", "task"];
4
5
  export declare const REPLY_MODELS: readonly ["fill-only", "may-reword"];
5
6
  export type ReplyAction = (typeof REPLY_ACTIONS)[number];
@@ -199,8 +200,12 @@ export interface Replies {
199
200
  export declare const EVERYONE: Audience;
200
201
  /** Why nobody may be answered here, or `null` when the audience lets the reply go. */
201
202
  export declare const outsideAudience: (audience: Audience, person: string | null, chat: string) => string | null;
203
+ export interface AudienceWarning {
204
+ text: string;
205
+ warning: WarningEvent;
206
+ }
202
207
  /** What in the audience does not do what it seems to say — said, never refused. */
203
- export declare const audienceWarnings: ({ reply, allow, deny }: Audience) => string[];
208
+ export declare const audienceWarnings: ({ reply, allow, deny }: Audience) => AudienceWarning[];
204
209
  /** The owner's rules, in file order. No file is no rules; a file that does not check out refuses, naming the field. */
205
210
  export declare const readReplyRules: (path: string) => ReplyRule[];
206
211
  /** `provider`: the messenger reading the file, which an older file's `testers` may have named. */
@@ -112,19 +112,27 @@ export const outsideAudience = (audience, person, chat) => {
112
112
  const listed = (person !== null && audience.allow.people.includes(person)) || audience.allow.chats.includes(chat);
113
113
  return listed ? null : "not on the allow list";
114
114
  };
115
+ const audienceWarning = (code, text) => ({
116
+ text,
117
+ warning: { event: "warning", code, operation: "replies.audience" },
118
+ });
115
119
  /** What in the audience does not do what it seems to say — said, never refused. */
116
120
  export const audienceWarnings = ({ reply, allow, deny }) => [
117
121
  ...allow.people
118
122
  .filter((id) => deny.people.includes(id))
119
- .map((id) => `person ${id} is on both lists: deny wins, nobody answers them`),
123
+ .map((id) => audienceWarning("audience_person_on_both_lists", `person ${id} is on both lists: deny wins, nobody answers them`)),
120
124
  ...allow.chats
121
125
  .filter((id) => deny.chats.includes(id))
122
- .map((id) => `chat ${id} is on both lists: deny wins, nothing is answered there`),
126
+ .map((id) => audienceWarning("audience_chat_on_both_lists", `chat ${id} is on both lists: deny wins, nothing is answered there`)),
123
127
  ...(reply === "all" && allow.people.length + allow.chats.length > 0
124
- ? ['the allow list does nothing while audience.reply is "all" — set it to "listed" to answer only those']
128
+ ? [
129
+ audienceWarning("audience_allow_unused", 'the allow list does nothing while audience.reply is "all" — set it to "listed" to answer only those'),
130
+ ]
125
131
  : []),
126
132
  ...(reply === "listed" && allow.people.length + allow.chats.length === 0
127
- ? ['audience.reply is "listed" and the allow list is empty: nobody is answered']
133
+ ? [
134
+ audienceWarning("audience_allow_empty", 'audience.reply is "listed" and the allow list is empty: nobody is answered'),
135
+ ]
128
136
  : []),
129
137
  ];
130
138
  /** The owner's rules, in file order. No file is no rules; a file that does not check out refuses, naming the field. */
@@ -1,4 +1,5 @@
1
1
  import type { Id, Message } from "../domain/models.js";
2
+ import type { Warn } from "../runs/events.js";
2
3
  import type { ReplyChat, ReplyRender } from "./rendering.js";
3
4
  export declare const NOT_ALLOWED = "replies.send is not allow";
4
5
  export declare const NO_RULES = "no rules";
@@ -27,7 +28,7 @@ export interface Replier {
27
28
  allowed: () => boolean;
28
29
  chatOf: (chat: Id) => Promise<ReplyChat>;
29
30
  render?: ReplyRender;
30
- warn?: (message: string) => void;
31
+ warn?: Warn;
31
32
  senderOf: (person: Id) => Promise<{
32
33
  isBot: boolean;
33
34
  isContact: boolean;
@@ -2,7 +2,7 @@ import { codeOf } from "../sends/guarded.js";
2
2
  import { decide } from "./decide.js";
3
3
  import { outsideAudience, readReplies } from "./rules.js";
4
4
  import { readRepliesState, recordReply, writeRepliesState } from "./state.js";
5
- import { renderReplyTemplate } from "./template.js";
5
+ import { renderReplyTemplate, templateWarning } from "./template.js";
6
6
  export const NOT_ALLOWED = "replies.send is not allow";
7
7
  export const NO_RULES = "no rules";
8
8
  export const ONLY_TASK = "opened a task, sent nothing";
@@ -69,7 +69,7 @@ export const replyTo = async (deps, message) => {
69
69
  data: message.text,
70
70
  });
71
71
  for (const warning of rendered.warnings)
72
- deps.warn?.(warning);
72
+ deps.warn?.(warning, templateWarning(warning));
73
73
  if (rendered.text === null)
74
74
  return { skip: rendered.reason ?? "reply template produced no reply", ...opened };
75
75
  // Pause and consent can be revoked while an async model request is in flight.
@@ -1,3 +1,4 @@
1
+ import type { WarningEvent } from "../runs/events.js";
1
2
  import type { ReplyRule } from "./rules.js";
2
3
  export declare const TEMPLATE_LIMITS: {
3
4
  parse: number;
@@ -34,4 +35,6 @@ export interface TemplateInput {
34
35
  }>;
35
36
  limits?: Partial<typeof TEMPLATE_LIMITS>;
36
37
  }
38
+ /** A rendering warning as a log keeps it: a renderer a CLI hands in may warn in words this module does not know. */
39
+ export declare const templateWarning: (text: string) => WarningEvent;
37
40
  export declare const renderReplyTemplate: (reply: ReplyRule["reply"], input: TemplateInput) => Promise<TemplateResult>;
@@ -9,6 +9,18 @@ export const TEMPLATE_LIMITS = {
9
9
  aiOutput: 1_024,
10
10
  calls: 4,
11
11
  };
12
+ const LEGACY_PLACEHOLDERS = "legacy reply placeholders: use {{ sender.firstName }} and {{ sender.name }}";
13
+ const LEGACY_REWORD = "legacy may-reword: use an ai block with an explicit fallback";
14
+ const WARNING_CODES = {
15
+ [LEGACY_PLACEHOLDERS]: "reply_legacy_placeholders",
16
+ [LEGACY_REWORD]: "reply_legacy_may_reword",
17
+ };
18
+ /** A rendering warning as a log keeps it: a renderer a CLI hands in may warn in words this module does not know. */
19
+ export const templateWarning = (text) => ({
20
+ event: "warning",
21
+ code: WARNING_CODES[text] ?? "reply_template_warning",
22
+ operation: "replies.render",
23
+ });
12
24
  export const renderReplyTemplate = async (reply, input) => {
13
25
  const limits = { ...TEMPLATE_LIMITS, ...input.limits };
14
26
  const blocks = [];
@@ -16,11 +28,11 @@ export const renderReplyTemplate = async (reply, input) => {
16
28
  let source = reply.template;
17
29
  const legacy = /(?<!\{)\{(firstName|name)\}(?!\})/g;
18
30
  if (legacy.test(source)) {
19
- warnings.push("legacy reply placeholders: use {{ sender.firstName }} and {{ sender.name }}");
31
+ warnings.push(LEGACY_PLACEHOLDERS);
20
32
  source = source.replace(legacy, (_, name) => `{{ sender.${name} }}`);
21
33
  }
22
34
  if (reply.model === "may-reword") {
23
- warnings.push("legacy may-reword: use an ai block with an explicit fallback");
35
+ warnings.push(LEGACY_REWORD);
24
36
  if (!/\{%[-]?\s*ai\b/.test(source))
25
37
  source = `{% ai %}${source}{% else %}${source}{% endai %}`;
26
38
  }
@@ -1,6 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import type { Id } from "../domain/models.js";
3
3
  import { type AppIdentity } from "../ports/app.js";
4
+ import type { Warn } from "../runs/events.js";
4
5
  import { FloodMemory } from "./flood.js";
5
6
  import { type AccountAction, type ChatAction, type SendEntry, SendJournal, type SendKind } from "./journal.js";
6
7
  import { type Level, type PermissionKey } from "./permissions.js";
@@ -66,7 +67,7 @@ export interface SendGuardOptions {
66
67
  recipients: RecipientList;
67
68
  /** Where a messenger's "this account may not write" is remembered; without it nothing is held. */
68
69
  flood?: FloodMemory;
69
- warn: (message: string) => void;
70
+ warn: Warn;
70
71
  now?: () => Date;
71
72
  }
72
73
  /** A write the profile's level refuses, worded alike by the guard and by the check before connecting. */
@@ -96,7 +97,7 @@ export interface GuardSettings {
96
97
  * The guard a profile's configuration asks for — the command's, and a background server's for every
97
98
  * write it forwards. Built per request there, so `config set permissions.messages readonly` needs no restart.
98
99
  */
99
- export declare const guardFor: (app: AppIdentity, settings: GuardSettings, warn: (message: string) => void, env?: NodeJS.ProcessEnv, ask?: Asker) => SendGuard;
100
+ export declare const guardFor: (app: AppIdentity, settings: GuardSettings, warn: Warn, env?: NodeJS.ProcessEnv, ask?: Asker) => SendGuard;
100
101
  /**
101
102
  * Over a background server, the server journals what it forwards, with the outcome it saw; the command
102
103
  * writes only the refusals of its own check, which never reached the server.
@@ -227,7 +227,11 @@ export const sendGuard = ({ profile, recipients, command = recipients.command, r
227
227
  journal.append({ at: now().toISOString(), profile, ...kept, ...(settles ? { reservation: settles } : {}) });
228
228
  }
229
229
  catch (error) {
230
- warn(`this send is not in the send journal: ${journalFailure(error)}`);
230
+ warn(`this send is not in the send journal: ${journalFailure(error)}`, {
231
+ event: "warning",
232
+ code: "send_not_journaled",
233
+ operation: "sends.journal",
234
+ });
231
235
  }
232
236
  },
233
237
  };
@@ -5,5 +5,6 @@ export interface ScopedContextEvidenceReader extends ContextEvidenceReader {
5
5
  usage(): ContextReadUsage;
6
6
  }
7
7
  export declare const createContextEvidenceReader: (port: ContextEvidenceReader, allowed: readonly ContextSource[], limits?: ContextReadLimits) => ScopedContextEvidenceReader;
8
- export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextIndex, ContextSource, } from "../context/evidence.js";
8
+ export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextIndex, ContextRecordReader, ContextSource, } from "../context/evidence.js";
9
+ export { contextEvidenceFingerprint } from "../context/evidence.js";
9
10
  export type { ContextReadLimits, ContextReadUsage } from "../context/read-budget.js";
@@ -43,3 +43,4 @@ export const createContextEvidenceReader = (port, allowed, limits) => {
43
43
  },
44
44
  };
45
45
  };
46
+ export { contextEvidenceFingerprint } from "../context/evidence.js";
@@ -1,6 +1,7 @@
1
1
  import { type BotCheckService } from "../botcheck/service.js";
2
- export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextIndex, ContextReadLimits, ContextReadUsage, ContextSource, ScopedContextEvidenceReader, } from "./context-evidence.js";
3
- export { createContextEvidenceReader } from "./context-evidence.js";
2
+ export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextIndex, ContextReadLimits, ContextReadUsage, ContextRecordReader, ContextSource, ScopedContextEvidenceReader, } from "./context-evidence.js";
3
+ export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
4
+ export { excerpt } from "./excerpt.js";
4
5
  export { type DownloadedModel, downloadImageModel, downloadTextModel, type ModelDownloadOptions, } from "./model-downloads.js";
5
6
  export type { ProjectEvidenceItem, ProjectGathering, ProjectGatheringPorts, ProjectGatheringStop, ProjectIndexCatchUp, ProjectSourceFetcher, } from "./project-gathering.js";
6
7
  export { gatherProjectEvidence } from "./project-gathering.js";
@@ -1,5 +1,6 @@
1
1
  import { botCheckService } from "../botcheck/service.js";
2
- export { createContextEvidenceReader } from "./context-evidence.js";
2
+ export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
3
+ export { excerpt } from "./excerpt.js";
3
4
  export { downloadImageModel, downloadTextModel, } from "./model-downloads.js";
4
5
  export { gatherProjectEvidence } from "./project-gathering.js";
5
6
  export { storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
@@ -33,6 +33,7 @@ export interface ProjectEvidenceItem extends Omit<ContextEvidenceItem, "text"> {
33
33
  textTruncated: boolean;
34
34
  lanes: string[];
35
35
  context: "project" | "person-only";
36
+ fingerprint: string;
36
37
  around?: ProjectNeighbour[] | null;
37
38
  aroundIssue?: string | null;
38
39
  }
@@ -1,4 +1,5 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
+ import { contextEvidenceFingerprint } from "../context/evidence.js";
2
3
  import { createContextEvidenceReader } from "./context-evidence.js";
3
4
  import { excerpt } from "./excerpt.js";
4
5
  import { attachNeighbours } from "./project-gathering-around.js";
@@ -89,12 +90,14 @@ export const gatherProjectEvidence = async (ports, request) => {
89
90
  }
90
91
  seen.add(key);
91
92
  const short = excerpt(item.text, lane.query, 300, { literal: true });
93
+ const fingerprint = contextEvidenceFingerprint(item);
92
94
  const entry = {
93
95
  ...item,
94
96
  kind,
95
97
  source,
96
98
  text: short,
97
99
  textTruncated: short !== item.text.replace(/\s+/g, " ").trim(),
100
+ fingerprint,
98
101
  lanes: [lane.id],
99
102
  context: lane.kinds.every((k) => k === "person") ? "person-only" : "project",
100
103
  };
@@ -1,5 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { MEETING_FINDINGS_KIND } from "../meetings/index.js";
3
+ import { EVIDENCE_FINDINGS_KIND } from "../store/evidence-findings.js";
3
4
  import { createTaskService } from "../tasks/index.js";
4
5
  import { taskAccount } from "./task-rules.js";
5
6
  /** The source a proposal's task keeps: found again on a retry, so one proposal never makes two tasks. */
@@ -14,6 +15,10 @@ export const approveAsTask = async (store, id) => {
14
15
  throw new CliError("validation_error", "review meeting findings through the dedicated findings command", {
15
16
  reason: "meeting-findings.dedicated-review",
16
17
  });
18
+ if (proposal.kind === EVIDENCE_FINDINGS_KIND)
19
+ throw new CliError("validation_error", "review evidence findings through the dedicated findings command", {
20
+ reason: "evidence-findings.dedicated-review",
21
+ });
17
22
  if (proposal.status !== "proposed")
18
23
  throw new CliError("validation_error", `a ${proposal.status} action cannot be approved`, {
19
24
  reason: "proposal.wrong-state",
@@ -0,0 +1,63 @@
1
+ import { type FindingOutcome, type FindingsInput, type FindingsReceipt } from "../meetings/index.js";
2
+ export declare const EVIDENCE_FINDINGS_KIND = "evidence_findings";
3
+ export declare const validateEvidenceFindings: (input: FindingsInput) => FindingsInput;
4
+ /** Who decided: the owner, or an agent the host let decide for the owner, with its reason. */
5
+ export type EvidenceFindingsDecider = "owner" | {
6
+ bot: string;
7
+ model?: string;
8
+ reason: string;
9
+ };
10
+ export interface EvidenceFindingsReceipt extends FindingsReceipt {
11
+ decision: {
12
+ by: "owner";
13
+ } | {
14
+ by: "agent";
15
+ bot: string;
16
+ model: string | null;
17
+ reason: string;
18
+ };
19
+ }
20
+ export interface EvidenceFindingsBatch {
21
+ id: string;
22
+ project: string;
23
+ input: FindingsInput;
24
+ fingerprint: string;
25
+ status: "proposed" | "reviewed";
26
+ author: {
27
+ type: "person" | "bot";
28
+ id: string;
29
+ name: string | null;
30
+ };
31
+ decidedBy: {
32
+ type: "person" | "bot";
33
+ id: string;
34
+ } | null;
35
+ decidedAt: string | null;
36
+ previous: FindingOutcome[];
37
+ receipt: EvidenceFindingsReceipt | null;
38
+ }
39
+ /**
40
+ * Cited facts and follow-ups about one project from stored messages, mail and documents, staged by an agent for
41
+ * review. A sibling of meeting findings: the same rules, evidence checked against the current stored text.
42
+ */
43
+ export interface EvidenceFindingsStore {
44
+ stage(input: FindingsInput, agent: {
45
+ bot: string;
46
+ model?: string;
47
+ }): Promise<EvidenceFindingsBatch>;
48
+ get(id: string): Promise<EvidenceFindingsBatch>;
49
+ list(options?: {
50
+ project?: string;
51
+ limit?: number;
52
+ offset?: number;
53
+ status?: "proposed" | "reviewed";
54
+ }): Promise<{
55
+ items: EvidenceFindingsBatch[];
56
+ hasMore: boolean;
57
+ }>;
58
+ review(id: string, input: {
59
+ fingerprint: string;
60
+ accept: string[];
61
+ by?: EvidenceFindingsDecider;
62
+ }): Promise<EvidenceFindingsBatch>;
63
+ }
@@ -0,0 +1,15 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { formatLocator, parseLocator } from "../domain/locator.js";
3
+ import { validateFindings } from "../meetings/index.js";
4
+ export const EVIDENCE_FINDINGS_KIND = "evidence_findings";
5
+ const reference = (raw) => {
6
+ if (/^document:[1-9]\d*$/.test(raw))
7
+ return raw;
8
+ try {
9
+ return formatLocator(parseLocator(raw));
10
+ }
11
+ catch {
12
+ throw new CliError("validation_error", "finding evidence is a msg: or document: reference");
13
+ }
14
+ };
15
+ export const validateEvidenceFindings = (input) => validateFindings(input, { plural: "evidence findings", singular: "evidence finding", cited: "reference", reference });
@@ -9,6 +9,7 @@ export type { DocumentTransferOptions, DocumentTransferPreview, DocumentTransfer
9
9
  export { applyDocumentTransfer, previewDocumentTransfer } from "./document-transfer/index.js";
10
10
  export type { CacheDatabase, CacheStatement, OpenDatabase, SqlValue } from "./driver.js";
11
11
  export { PRAGMAS } from "./driver.js";
12
+ export { EVIDENCE_FINDINGS_KIND, type EvidenceFindingsBatch, type EvidenceFindingsDecider, type EvidenceFindingsReceipt, type EvidenceFindingsStore, validateEvidenceFindings, } from "./evidence-findings.js";
12
13
  export type { MeetingFindingsBatch, MeetingFindingsStore } from "./meeting-findings.js";
13
14
  export { MIGRATIONS, type Migration, migrate } from "./migrations.js";
14
15
  export { openCache } from "./open.js";
@@ -2,6 +2,7 @@ export { FACT_STATUSES, FACT_TYPES, MEMORY_KINDS, MEMORY_SCOPES, MEMORY_STATUSES
2
2
  export { createTagRedirectIndex } from "../domain/tag-redirects.js";
3
3
  export { applyDocumentTransfer, previewDocumentTransfer } from "./document-transfer/index.js";
4
4
  export { PRAGMAS } from "./driver.js";
5
+ export { EVIDENCE_FINDINGS_KIND, validateEvidenceFindings, } from "./evidence-findings.js";
5
6
  export { MIGRATIONS, migrate } from "./migrations.js";
6
7
  export { openCache } from "./open.js";
7
8
  export { storePath } from "./path.js";
@@ -1,7 +1,15 @@
1
- import { type ContextEvidenceReader, type ContextIndex } from "../../context/evidence.js";
1
+ import { type ContextEvidenceReader, type ContextIndex, type ContextRecordReader } from "../../context/evidence.js";
2
2
  import type { Stemmer, Stemmers } from "../../search/stem.js";
3
3
  import type { StoreContext } from "./open.js";
4
4
  /** No index fills, model loads or record hydration outside the read transaction. */
5
5
  export declare const contextEvidenceOver: ({ database }: Pick<StoreContext, "database">) => ContextEvidenceReader;
6
6
  /** The same fills a mail search and the notes search run before reading; a message is indexed when it is saved. */
7
7
  export declare const contextIndexOver: ({ database }: Pick<StoreContext, "database">, stemmerFor: (stemmers: Stemmers) => Stemmer) => ContextIndex;
8
+ /** The cited record by reference, read with the context reader's own columns, so fingerprints agree. */
9
+ export declare const contextRecordOf: (database: StoreContext["database"], ref: string) => {
10
+ ref: string;
11
+ title: string | null;
12
+ text: string;
13
+ revision: number | null;
14
+ } | null;
15
+ export declare const contextRecordsOver: ({ database }: Pick<StoreContext, "database">) => ContextRecordReader;
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { contextCursorScope, contextEvidenceRequest, } from "../../context/evidence.js";
3
- import { formatLocator } from "../../domain/locator.js";
3
+ import { formatLocator, parseLocator } from "../../domain/locator.js";
4
4
  import { NORMALIZER_VERSION, normalize } from "../normalize.js";
5
5
  import { contextDocumentTime, contextDocumentTimeKind, contextDocumentUrl } from "./context-document-source.js";
6
6
  import { readMeetingContextEvidence } from "./context-meeting-source.js";
@@ -178,3 +178,45 @@ export const contextIndexOver = ({ database }, stemmerFor) => ({
178
178
  return { mail, documents };
179
179
  },
180
180
  });
181
+ /** The cited record by reference, read with the context reader's own columns, so fingerprints agree. */
182
+ export const contextRecordOf = (database, ref) => {
183
+ if (typeof ref !== "string")
184
+ return null;
185
+ const document = /^document:([1-9]\d*)$/.exec(ref);
186
+ if (document) {
187
+ const corpus = corpora.document;
188
+ const row = database
189
+ .prepare(`SELECT ${corpus.title} AS title,${corpus.body} AS body,${corpus.revision} AS revision FROM documents n
190
+ WHERE n.id=? AND n.deleted_at IS NULL AND ${corpus.where}`)
191
+ .get(Number(document[1]));
192
+ return row
193
+ ? {
194
+ ref,
195
+ title: row.title == null ? null : String(row.title),
196
+ text: String(row.body),
197
+ revision: row.revision == null ? null : Number(row.revision),
198
+ }
199
+ : null;
200
+ }
201
+ let at;
202
+ try {
203
+ at = parseLocator(ref);
204
+ }
205
+ catch {
206
+ return null;
207
+ }
208
+ const corpus = at.provider === "email" ? corpora.mail : corpora.message;
209
+ const row = database
210
+ .prepare(`SELECT ${corpus.title} AS title,${corpus.body} AS body FROM ${corpus.table} n JOIN accounts a ON a.id=n.account_id
211
+ ${corpus.join} WHERE a.provider=? AND a.external_id=? AND c.external_id=? AND n.external_id=?
212
+ AND n.deleted_at IS NULL AND ${corpus.where}`)
213
+ .get(at.provider, at.account, at.chat, at.message);
214
+ return row
215
+ ? { ref, title: row.title == null ? null : String(row.title), text: String(row.body), revision: null }
216
+ : null;
217
+ };
218
+ export const contextRecordsOver = ({ database }) => ({
219
+ async read(ref) {
220
+ return read(database, () => contextRecordOf(database, ref));
221
+ },
222
+ });
@@ -0,0 +1,3 @@
1
+ import { type EvidenceFindingsStore } from "../evidence-findings.js";
2
+ import type { StoreContext } from "./open.js";
3
+ export declare const evidenceFindingsStoreOver: (context: StoreContext) => EvidenceFindingsStore;