@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
package/CHANGELOG.md CHANGED
@@ -7,6 +7,33 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.364.0 — 19.10.2026
11
+
12
+ ### Fixed
13
+
14
+ - **More warnings in `serve`'s log now carry a code.** A flood wait (`flood_wait`, with `counts.seconds`),
15
+ a long wait for the profile's pace (`pace_wait`, with `counts.seconds`), a held account (`account_frozen`,
16
+ `account_limited`, `account_banned`), a wait that could not be remembered (`wait_not_remembered`), a send missing
17
+ from the send journal (`send_not_journaled`), a recipient list that could not be re-keyed or read
18
+ (`recipients_not_rekeyed`, `recipients_not_read`), the reply audience warnings (`audience_person_on_both_lists`,
19
+ `audience_chat_on_both_lists`, `audience_allow_unused`, `audience_allow_empty`) and the reply template warnings
20
+ (`reply_legacy_placeholders`, `reply_legacy_may_reword`, `reply_template_warning`) each log their code and
21
+ operation instead of a bare `serve.warning`. The record never holds the sentence, a chat id, a name or an error
22
+ message; the sentence at a terminal is unchanged.
23
+
24
+ ## 0.363.0 — 19.10.2026
25
+
26
+ ### Added
27
+
28
+ - **`store.evidenceFindings`: cited facts and follow-ups about one project from stored messages, mail and
29
+ documents, staged by an agent and reviewed** — reviewed meeting findings' rules over any stored reference. A
30
+ review is the owner's or an agent's deciding for the owner (`by: { bot, reason }`), recorded in the receipt.
31
+ Evidence is checked against the current text by fingerprint: project evidence items carry `fingerprint`, and
32
+ `store.contextRecords.read(ref)` reproduces it. Meeting and evidence findings share their validation and review
33
+ code (`validateFindings`); meeting findings behave as before. A host reads a staged batch from snake_case JSON
34
+ with `parseEvidenceFindingsJson` from `./cli`. `./services` also exports `contextEvidenceFingerprint`,
35
+ `ContextRecordReader` and `excerpt`, for a host that shows a cited record's text.
36
+
10
37
  ## 0.362.0 — 19.10.2026
11
38
 
12
39
  ### Changed — may break callers
@@ -65,7 +65,7 @@ export { bufferedInput, type InputPolicy, inputPolicy, MAX_BUFFERED_INPUT, MAX_S
65
65
  export { answerFields, checkJson, type EntityFields, entityFields, type JsonFieldsResolver, type JsonRequest, liftJson, } from "./json-fields.js";
66
66
  export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTerms, pagedList, pageOf, type QueryWords, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
67
67
  export { addMeetingBatchCommand, formatMeetingBatch, prepareMeetingBatch } from "./meeting-batch.js";
68
- export { parseMeetingFindingsJson } from "./meeting-findings-input.js";
68
+ export { parseEvidenceFindingsJson, parseMeetingFindingsJson } from "./meeting-findings-input.js";
69
69
  export { meetingPreparationAnswer } from "./meeting-preparation-answer.js";
70
70
  export { addMeetingPreparationCommand } from "./meeting-preparation-command.js";
71
71
  export { type MeetingPreparationReader, meetingPreparationInput, readMeetingPreparation, } from "./meeting-preparation-read.js";
package/dist/cli/index.js CHANGED
@@ -60,7 +60,7 @@ export { bufferedInput, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provi
60
60
  export { answerFields, checkJson, entityFields, liftJson, } from "./json-fields.js";
61
61
  export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTerms, pagedList, pageOf, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
62
62
  export { addMeetingBatchCommand, formatMeetingBatch, prepareMeetingBatch } from "./meeting-batch.js";
63
- export { parseMeetingFindingsJson } from "./meeting-findings-input.js";
63
+ export { parseEvidenceFindingsJson, parseMeetingFindingsJson } from "./meeting-findings-input.js";
64
64
  export { meetingPreparationAnswer } from "./meeting-preparation-answer.js";
65
65
  export { addMeetingPreparationCommand } from "./meeting-preparation-command.js";
66
66
  export { meetingPreparationInput, readMeetingPreparation, } from "./meeting-preparation-read.js";
@@ -1,2 +1,3 @@
1
- import { type MeetingFindingsInput } from "../meetings/index.js";
1
+ import { type FindingsInput, type MeetingFindingsInput } from "../meetings/index.js";
2
2
  export declare const parseMeetingFindingsJson: (text: string) => MeetingFindingsInput;
3
+ export declare const parseEvidenceFindingsJson: (text: string) => FindingsInput;
@@ -1,24 +1,25 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { validateMeetingFindings } from "../meetings/index.js";
3
+ import { validateEvidenceFindings } from "../store/evidence-findings.js";
3
4
  const keys = {
4
5
  schema_version: "schemaVersion",
5
6
  due_at: "dueAt",
6
7
  due_evidence: "dueEvidence",
7
8
  assignee_evidence: "assigneeEvidence",
8
9
  };
9
- export const parseMeetingFindingsJson = (text) => {
10
+ const parseFindingsJson = (text, label, validate) => {
10
11
  if (Buffer.byteLength(text, "utf8") > 65536)
11
- throw new CliError("validation_error", "meeting findings take at most 65536 UTF-8 bytes");
12
+ throw new CliError("validation_error", `${label} take at most 65536 UTF-8 bytes`);
12
13
  let raw;
13
14
  try {
14
15
  raw = JSON.parse(text);
15
16
  }
16
17
  catch {
17
- throw new CliError("validation_error", "meeting findings input must be valid JSON");
18
+ throw new CliError("validation_error", `${label} input must be valid JSON`);
18
19
  }
19
20
  const decode = (value, depth = 0) => {
20
21
  if (depth > 16)
21
- throw new CliError("validation_error", "meeting findings JSON is nested too deeply");
22
+ throw new CliError("validation_error", `${label} JSON is nested too deeply`);
22
23
  if (Array.isArray(value))
23
24
  return value.map((item) => decode(item, depth + 1));
24
25
  if (value === null || typeof value !== "object")
@@ -26,13 +27,15 @@ export const parseMeetingFindingsJson = (text) => {
26
27
  const output = {};
27
28
  for (const [key, item] of Object.entries(value)) {
28
29
  if (/[A-Z]/.test(key))
29
- throw new CliError("validation_error", "meeting findings JSON fields use snake_case");
30
+ throw new CliError("validation_error", `${label} JSON fields use snake_case`);
30
31
  const name = keys[key] ?? key;
31
32
  if (Object.hasOwn(output, name))
32
- throw new CliError("validation_error", "duplicate meeting findings field");
33
+ throw new CliError("validation_error", `duplicate ${label} field`);
33
34
  Object.defineProperty(output, name, { value: decode(item, depth + 1), enumerable: true });
34
35
  }
35
36
  return output;
36
37
  };
37
- return validateMeetingFindings(decode(raw));
38
+ return validate(decode(raw));
38
39
  };
40
+ export const parseMeetingFindingsJson = (text) => parseFindingsJson(text, "meeting findings", validateMeetingFindings);
41
+ export const parseEvidenceFindingsJson = (text) => parseFindingsJson(text, "evidence findings", validateEvidenceFindings);
@@ -1,7 +1,7 @@
1
1
  import type { Command } from "commander";
2
2
  import type { AdminRight, GroupSettingKey, Id, Message, PollVote, PrivacyKey, Provider } from "../../domain/models.js";
3
3
  import type { MessengerAdapter } from "../../ports/messenger.js";
4
- import type { EventSink } from "../../runs/events.js";
4
+ import type { EventSink, Warn } from "../../runs/events.js";
5
5
  import { type Asker, type SendGuard } from "../../sends/guard.js";
6
6
  import { type PaceRate } from "../../sends/pace.js";
7
7
  import { type PermissionKey } from "../../sends/permissions.js";
@@ -186,7 +186,7 @@ export interface Messenger extends ServiceMessenger {
186
186
  * `ask`, when given, answers a write at level `ask` instead of the messenger's own prompt: the MCP
187
187
  * server has no terminal to ask at.
188
188
  */
189
- guard?: (command: Command, settings: Settings, warn: (message: string) => void, ask?: Asker) => SendGuard;
189
+ guard?: (command: Command, settings: Settings, warn: Warn, ask?: Asker) => SendGuard;
190
190
  /** The permission key of one of this CLI's own commands, when `keyForCommand` does not know it. */
191
191
  permissionKey?: PermissionKeyOf;
192
192
  /** The CLI's SKILL.md, which the MCP server also serves as `<command>://skill`. */
@@ -169,7 +169,11 @@ const rekeyRecipients = async (app, profile, env, warn, from, to) => {
169
169
  await new RecipientList(recipientsPathFor(app, profile, env), app.command, REKEY_LOCK_WAIT_MS).rekey(from, to);
170
170
  }
171
171
  catch (error) {
172
- warn(`the recipient list still names ${from}: ${error instanceof Error ? error.message : String(error)}`);
172
+ warn(`the recipient list still names ${from}: ${error instanceof Error ? error.message : String(error)}`, {
173
+ event: "warning",
174
+ code: "recipients_not_rekeyed",
175
+ operation: "recipients.rekey",
176
+ });
173
177
  }
174
178
  };
175
179
  const waitsOf = (command) => {
@@ -344,7 +348,7 @@ export const messengerContext = (command, messenger, resolving = {}) => {
344
348
  base.renderer.warn(`permissions.${key} names no command and does nothing — \`config unset permissions.${key}\``);
345
349
  }
346
350
  // Read when warning, not now: serve swaps the renderer's methods for log records after this.
347
- const warn = (text) => base.renderer.warn(text);
351
+ const warn = (text, warning) => base.renderer.warn(text, warning);
348
352
  const accountFor = () => {
349
353
  const account = messenger.accountOf?.(command) ?? recalledAccount(app, provider, profile, base.env);
350
354
  if (!account)
@@ -1,4 +1,5 @@
1
1
  import { type MessengerAdapter } from "../../ports/messenger.js";
2
+ import { type Warn } from "../../runs/events.js";
2
3
  import { type FloodMemory } from "../../sends/flood.js";
3
4
  import type { Pacer } from "../../sends/pace.js";
4
5
  /**
@@ -9,6 +10,6 @@ import type { Pacer } from "../../sends/pace.js";
9
10
  */
10
11
  export declare const flooded: (messenger: MessengerAdapter, memory: FloodMemory, { name, warn, pacer }: {
11
12
  name: string;
12
- warn: (message: string) => void;
13
+ warn: Warn;
13
14
  pacer?: Pacer;
14
15
  }) => MessengerAdapter;
@@ -19,12 +19,16 @@ const chatOf = (args) => typeof args[0] === "string" && /^-?\d+$/.test(args[0])
19
19
  * waits for its turn in the profile's pace, and a wait the messenger asks for holds the whole profile.
20
20
  */
21
21
  export const flooded = (messenger, memory, { name, warn, pacer }) => {
22
- const kept = async (what) => {
22
+ const kept = async (operation, what) => {
23
23
  try {
24
24
  await what();
25
25
  }
26
26
  catch (error) {
27
- warn(`could not remember ${name}'s wait (${error instanceof Error ? error.message : String(error)})`);
27
+ warn(`could not remember ${name}'s wait (${error instanceof Error ? error.message : String(error)})`, {
28
+ event: "warning",
29
+ code: "wait_not_remembered",
30
+ operation,
31
+ });
28
32
  }
29
33
  };
30
34
  const noted = async (operation, chatId, error) => {
@@ -33,13 +37,17 @@ export const flooded = (messenger, memory, { name, warn, pacer }) => {
33
37
  const details = error.details ?? {};
34
38
  const standing = details.standing;
35
39
  if (standing && BLOCKING.has(standing.state)) {
36
- await kept(() => {
40
+ await kept(operation, () => {
37
41
  const block = memory.block({
38
42
  state: standing.state,
39
43
  hint: standing.hint ?? error.message,
40
44
  ...(standing.until ? { until: standing.until } : {}),
41
45
  });
42
- warn(`writes from this profile are held ${holdEnd(block)}: ${block.hint}`);
46
+ warn(`writes from this profile are held ${holdEnd(block)}: ${block.hint}`, {
47
+ event: "warning",
48
+ code: `account_${block.state}`,
49
+ operation,
50
+ });
43
51
  });
44
52
  return;
45
53
  }
@@ -47,23 +55,28 @@ export const flooded = (messenger, memory, { name, warn, pacer }) => {
47
55
  if (error.code !== "rate_limited" || details.remembered === true || !Number.isFinite(waitMs) || waitMs <= 0)
48
56
  return;
49
57
  const providerError = providerErrorKey(details.provider_error);
50
- await kept(() => {
58
+ await kept(operation, () => {
51
59
  const deadline = memory.remember({
52
60
  operation,
53
61
  ...(chatId ? { chatId } : {}),
54
62
  ...(providerError ? { providerError } : {}),
55
63
  waitMs,
56
64
  });
57
- warn(`${name} asked to wait before ${operation} again — until ${deadline.until}`);
65
+ warn(`${name} asked to wait before ${operation} again — until ${deadline.until}`, {
66
+ event: "warning",
67
+ code: "flood_wait",
68
+ operation,
69
+ counts: { seconds: Math.ceil(waitMs / 1000) },
70
+ });
58
71
  });
59
72
  if (pacer)
60
- await kept(() => pacer.holdFor(waitMs));
73
+ await kept(operation, () => pacer.holdFor(waitMs));
61
74
  };
62
- const turn = async () => {
75
+ const turn = async (operation) => {
63
76
  if (!pacer)
64
77
  return;
65
78
  let at = Date.now();
66
- await kept(async () => {
79
+ await kept(operation, async () => {
67
80
  at = await pacer.reserve();
68
81
  });
69
82
  const waitMs = at - Date.now();
@@ -72,8 +85,15 @@ export const flooded = (messenger, memory, { name, warn, pacer }) => {
72
85
  if (waitMs > LONGEST_PACE_WAIT_MS) {
73
86
  throw new CliError("rate_limited", `${name} asked this profile to wait, ${Math.ceil(waitMs / 1000)} s are left — nothing was sent to ${name}`, { retry_after_ms: waitMs, retry_at: new Date(at).toISOString(), remembered: true });
74
87
  }
75
- if (waitMs >= ANNOUNCE_WAIT_MS)
76
- warn(`waiting ${Math.ceil(waitMs / 1000)} s to keep this profile's pace with ${name}`);
88
+ if (waitMs >= ANNOUNCE_WAIT_MS) {
89
+ const seconds = Math.ceil(waitMs / 1000);
90
+ warn(`waiting ${seconds} s to keep this profile's pace with ${name}`, {
91
+ event: "warning",
92
+ code: "pace_wait",
93
+ operation,
94
+ counts: { seconds },
95
+ });
96
+ }
77
97
  await new Promise((resolve) => setTimeout(resolve, waitMs));
78
98
  };
79
99
  return throughWrapper(messenger, {}, (operation, call) => {
@@ -101,7 +121,7 @@ export const flooded = (messenger, memory, { name, warn, pacer }) => {
101
121
  ...(owed.providerError ? { provider_error: owed.providerError } : {}),
102
122
  });
103
123
  }
104
- await turn();
124
+ await turn(operation);
105
125
  try {
106
126
  return await call(...args);
107
127
  }
@@ -43,8 +43,9 @@ export const repliesCommand = (messenger) => {
43
43
  if (rules.length === 0) {
44
44
  renderer.note(`no reply rules yet — they live in ${path}`);
45
45
  }
46
- for (const warning of audienceWarnings(audience))
47
- renderer.warn(warning);
46
+ const warn = renderer.warn;
47
+ for (const { text, warning } of audienceWarnings(audience))
48
+ warn(text, warning);
48
49
  const found = await context.withStore((store, account) => dryRun(store, account, rules, {
49
50
  since,
50
51
  audience,
@@ -103,8 +104,9 @@ export const repliesCommand = (messenger) => {
103
104
  const { settings, renderer, env } = context;
104
105
  const { rules, audience } = readReplies(repliesPathFor(messenger.app, settings.profile, env), messenger.provider);
105
106
  const warnings = audienceWarnings(audience);
106
- for (const warning of warnings)
107
- renderer.warn(warning);
107
+ const warn = renderer.warn;
108
+ for (const { text, warning } of warnings)
109
+ warn(text, warning);
108
110
  const state = readRepliesState(repliesStatePathFor(messenger.app, settings.profile, env));
109
111
  const level = levelFor(settings.permissions ?? {}, "replies.send").level;
110
112
  if (level !== "allow")
@@ -117,7 +119,7 @@ export const repliesCommand = (messenger) => {
117
119
  allow: audience.allow.people.length + audience.allow.chats.length,
118
120
  deny: audience.deny.people.length + audience.deny.chats.length,
119
121
  },
120
- warnings,
122
+ warnings: warnings.map(({ text }) => text),
121
123
  rules: rules.map(({ id, on }) => ({ id, on })),
122
124
  }));
123
125
  });
@@ -78,8 +78,9 @@ export const addReplyEditors = (replies, messenger) => {
78
78
  const context = messengerContext(this, messenger);
79
79
  const { settings, renderer, env } = context;
80
80
  const answer = editAudience(repliesPathFor(messenger.app, settings.profile, env), messenger.provider, this.opts());
81
- for (const warning of audienceWarnings(answer))
82
- renderer.warn(warning);
81
+ const warn = renderer.warn;
82
+ for (const { text, warning } of audienceWarnings(answer))
83
+ warn(text, warning);
83
84
  renderItem(this, context, answer);
84
85
  });
85
86
  replies.addCommand(audience);
@@ -162,9 +162,10 @@ const replying = (context, messenger, since) => {
162
162
  // A file that does not read is said on each message instead; it must not stop serve.
163
163
  try {
164
164
  const { rules, audience } = readReplies(repliesPathFor(app, profile, env), provider);
165
+ const warn = renderer.warn;
165
166
  if (rules.length > 0)
166
- for (const warning of audienceWarnings(audience))
167
- renderer.warn(warning);
167
+ for (const { text, warning } of audienceWarnings(audience))
168
+ warn(text, warning);
168
169
  }
169
170
  catch { }
170
171
  const handle = async (message) => {
@@ -85,7 +85,7 @@ export declare const serveRelay: (command: Command, messenger: Messenger) => {
85
85
  officialStats?: boolean;
86
86
  tracksMembers?: boolean;
87
87
  speechModels?: readonly string[];
88
- guard?: (command: Command, settings: import("../settings.js").Settings, warn: (message: string) => void, ask?: import("../../sends/guard.js").Asker) => import("../../sends/guard.js").SendGuard;
88
+ guard?: (command: Command, settings: import("../settings.js").Settings, warn: import("../../runs/events.js").Warn, ask?: import("../../sends/guard.js").Asker) => import("../../sends/guard.js").SendGuard;
89
89
  permissionKey?: import("../permission-keys.js").PermissionKeyOf;
90
90
  skill?: URL;
91
91
  diagnose?: (command: Command, context: import("../context.js").BaseContext) => Promise<Record<string, unknown>>;
@@ -461,7 +461,11 @@ const announceStoredChats = async (messenger, store, account, events, warn, reci
461
461
  listed = recipientIds?.() ?? [];
462
462
  }
463
463
  catch (error) {
464
- warn(`the messenger was not told the recipient list: ${error instanceof Error ? error.message : String(error)}`);
464
+ warn(`the messenger was not told the recipient list: ${error instanceof Error ? error.message : String(error)}`, {
465
+ event: "warning",
466
+ code: "recipients_not_read",
467
+ operation: "history.stored_chats",
468
+ });
465
469
  }
466
470
  try {
467
471
  const opened = await store();
@@ -63,6 +63,12 @@ export interface ContextIndex {
63
63
  documents: number;
64
64
  }>;
65
65
  }
66
+ /** Changes whenever the cited text, its title or its revision does; a staged finding citing an older one is stale. */
67
+ export declare const contextEvidenceFingerprint: (item: Pick<ContextEvidenceItem, "ref" | "title" | "text" | "revision">) => string;
68
+ /** One stored message, email or document as the context reader returns it, by its reference. */
69
+ export interface ContextRecordReader {
70
+ read(ref: string): Promise<Pick<ContextEvidenceItem, "ref" | "title" | "text" | "revision"> | null>;
71
+ }
66
72
  export declare const contextCursorScope: (request: Pick<ContextEvidenceRequest, "source" | "query" | "after" | "before">) => string;
67
73
  export declare const contextSource: (value: ContextSource) => ContextSource;
68
74
  export declare const contextEvidenceRequest: (input: ContextEvidenceRequest) => {
@@ -1,6 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { CliError } from "@wirecat/cli-base";
3
3
  export const CONTEXT_SOURCE_KINDS = ["message", "mail", "document", "meeting"];
4
+ /** Changes whenever the cited text, its title or its revision does; a staged finding citing an older one is stale. */
5
+ export const contextEvidenceFingerprint = (item) => createHash("sha256")
6
+ .update(JSON.stringify(["context-evidence-v1", item.ref, item.title, item.text, item.revision]))
7
+ .digest("hex");
4
8
  export const contextCursorScope = (request) => createHash("sha256")
5
9
  .update(JSON.stringify([
6
10
  "context-evidence-v1",
@@ -18,6 +18,8 @@ const OTHERS = [
18
18
  validation("context.read-budget", "the selected context reads exceed their text or request allowance"),
19
19
  validation("meeting-findings.stale", "meeting findings changed since they were read; stage a new batch"),
20
20
  validation("meeting-findings.dedicated-review", "review meeting findings through the dedicated findings command"),
21
+ validation("evidence-findings.stale", "evidence findings changed since they were read; stage a new batch"),
22
+ validation("evidence-findings.dedicated-review", "review evidence findings through the dedicated findings command"),
21
23
  validation("document.changed", "the document changed while it was read; nothing was saved"),
22
24
  ["document.not-imported", "not_found", "the document is not imported in this source account"],
23
25
  ["extraction.invalid-proposals", "invalid_response", "the model's memory proposals could not be used"],
@@ -0,0 +1,60 @@
1
+ import { MEETING_FACT_TYPES as FACT_TYPES } from "./meeting-context.js";
2
+ declare const TASK_TYPES: readonly ["bug", "feature", "chore"];
3
+ export interface FindingEvidence {
4
+ reference: string;
5
+ fingerprint: string;
6
+ }
7
+ interface FindingBase {
8
+ id: string;
9
+ project: string;
10
+ evidence: FindingEvidence[];
11
+ }
12
+ export interface FactFinding extends FindingBase {
13
+ kind: "fact";
14
+ type: (typeof FACT_TYPES)[number];
15
+ statement: string;
16
+ }
17
+ export interface TaskFinding extends FindingBase {
18
+ kind: "task";
19
+ type: (typeof TASK_TYPES)[number];
20
+ title: string;
21
+ reason: string;
22
+ commitment: "explicit" | "inferred";
23
+ dueAt?: string;
24
+ dueEvidence?: string[];
25
+ assignee?: string;
26
+ assigneeEvidence?: string[];
27
+ }
28
+ export type Finding = FactFinding | TaskFinding;
29
+ export interface FindingsInput {
30
+ schemaVersion: 1;
31
+ key: string;
32
+ extractor: string;
33
+ findings: Finding[];
34
+ }
35
+ export interface FindingOutcome {
36
+ id: string;
37
+ fingerprint: string;
38
+ status: "accepted" | "rejected";
39
+ reference: string | null;
40
+ reused: boolean;
41
+ }
42
+ export interface FindingsReceipt {
43
+ previewFingerprint: string;
44
+ accepted: string[];
45
+ items: FindingOutcome[];
46
+ }
47
+ export declare const findingHash: (value: unknown) => string;
48
+ /** What differs between findings kinds: their wording and what one cited reference must look like. */
49
+ export interface FindingsRules {
50
+ /** "meeting findings" */
51
+ plural: string;
52
+ /** "meeting finding" */
53
+ singular: string;
54
+ /** "cue" */
55
+ cited: string;
56
+ reference: (raw: string) => string;
57
+ }
58
+ export declare const validateFindings: (input: FindingsInput, rules: FindingsRules) => FindingsInput;
59
+ export declare const findingFingerprint: ({ id: _id, ...finding }: Finding) => string;
60
+ export {};
@@ -0,0 +1,117 @@
1
+ import { createHash } from "node:crypto";
2
+ import { CliError } from "@wirecat/cli-base";
3
+ import { MEETING_FACT_TYPES as FACT_TYPES } from "./meeting-context.js";
4
+ import { meetingTimestamp } from "./timestamp.js";
5
+ const TASK_TYPES = ["bug", "feature", "chore"];
6
+ const invalid = (message) => {
7
+ throw new CliError("validation_error", message);
8
+ };
9
+ const text = (value, limit) => {
10
+ if (typeof value !== "string" || !value.trim() || value.includes("\0") || value.length > limit)
11
+ return invalid(`finding text must contain 1–${limit} characters without NUL`);
12
+ return value.trim();
13
+ };
14
+ export const findingHash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
15
+ const fields = (value, allowed, noun) => {
16
+ if (Object.keys(value).some((key) => !allowed.includes(key)))
17
+ invalid(`unknown ${noun} field`);
18
+ };
19
+ export const validateFindings = (input, rules) => {
20
+ if (!input || typeof input !== "object" || Buffer.byteLength(JSON.stringify(input), "utf8") > 65536)
21
+ return invalid(`${rules.plural} take at most 65536 UTF-8 bytes`);
22
+ fields(input, ["schemaVersion", "key", "extractor", "findings"], rules.singular);
23
+ if (input.schemaVersion !== 1 ||
24
+ !Array.isArray(input.findings) ||
25
+ input.findings.length < 1 ||
26
+ input.findings.length > 16)
27
+ return invalid(`schemaVersion 1 takes 1–16 ${rules.plural}`);
28
+ const key = text(input.key, 64);
29
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/.test(key))
30
+ return invalid("a findings key uses letters, digits, dot, colon, underscore or hyphen");
31
+ const ids = new Set();
32
+ const findings = input.findings.map((finding) => {
33
+ if (!finding || typeof finding !== "object")
34
+ return invalid("a finding is an object");
35
+ const id = text(finding.id, 64);
36
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/.test(id) || ids.has(id))
37
+ return invalid("finding IDs must be unique simple keys");
38
+ ids.add(id);
39
+ const project = text(finding.project, 64);
40
+ if (!/^project:[1-9]\d*$/.test(project))
41
+ return invalid("a finding names a project reference");
42
+ if (!Array.isArray(finding.evidence) || finding.evidence.length < 1 || finding.evidence.length > 8)
43
+ return invalid(`a finding requires 1–8 cited ${rules.cited}s`);
44
+ const evidence = finding.evidence
45
+ .map((item) => {
46
+ if (!item || typeof item !== "object")
47
+ return invalid("finding evidence is a reference and fingerprint");
48
+ fields(item, ["reference", "fingerprint"], rules.singular);
49
+ if (typeof item.reference !== "string" || typeof item.fingerprint !== "string")
50
+ return invalid("evidence requires a reference and fingerprint");
51
+ const reference = rules.reference(item.reference);
52
+ if (!/^[a-f0-9]{64}$/.test(item.fingerprint))
53
+ return invalid(`finding evidence requires exact ${rules.cited} references and fingerprints`);
54
+ return { reference, fingerprint: item.fingerprint };
55
+ })
56
+ .sort((a, b) => a.reference.localeCompare(b.reference));
57
+ if (new Set(evidence.map((item) => item.reference)).size !== evidence.length)
58
+ return invalid(`duplicate evidence ${rules.cited}`);
59
+ const base = { id, project, evidence };
60
+ if (finding.kind === "fact") {
61
+ fields(finding, ["id", "project", "evidence", "kind", "type", "statement"], rules.singular);
62
+ if (!FACT_TYPES.includes(finding.type))
63
+ return invalid("unknown fact finding type");
64
+ return { ...base, kind: "fact", type: finding.type, statement: text(finding.statement, 2000) };
65
+ }
66
+ if (finding.kind !== "task")
67
+ return invalid("a finding is a fact or task");
68
+ fields(finding, [
69
+ "id",
70
+ "project",
71
+ "evidence",
72
+ "kind",
73
+ "type",
74
+ "title",
75
+ "reason",
76
+ "commitment",
77
+ "dueAt",
78
+ "dueEvidence",
79
+ "assignee",
80
+ "assigneeEvidence",
81
+ ], rules.singular);
82
+ if (!TASK_TYPES.includes(finding.type))
83
+ return invalid("a task finding type is bug, chore or feature");
84
+ if (!["explicit", "inferred"].includes(finding.commitment))
85
+ return invalid("task commitment must be explicit or inferred");
86
+ const support = (value) => {
87
+ if (!Array.isArray(value) ||
88
+ value.length < 1 ||
89
+ value.length > 8 ||
90
+ value.some((ref) => typeof ref !== "string" || !evidence.some((item) => item.reference === ref)))
91
+ return invalid("date and assignee support must cite the finding's evidence");
92
+ return [...new Set(value)].sort();
93
+ };
94
+ if ((finding.dueAt === undefined) !== (finding.dueEvidence === undefined) ||
95
+ (finding.assignee === undefined) !== (finding.assigneeEvidence === undefined))
96
+ return invalid("optional dates and assignees require their supporting evidence");
97
+ if (finding.assignee !== undefined &&
98
+ (typeof finding.assignee !== "string" || !/^person:[1-9]\d*$/.test(finding.assignee)))
99
+ return invalid("an assignee must be a person reference");
100
+ return {
101
+ ...base,
102
+ kind: "task",
103
+ type: finding.type,
104
+ title: text(finding.title, 300),
105
+ reason: text(finding.reason, 2000),
106
+ commitment: finding.commitment,
107
+ ...(finding.dueAt === undefined
108
+ ? {}
109
+ : { dueAt: meetingTimestamp(finding.dueAt, "finding due date"), dueEvidence: support(finding.dueEvidence) }),
110
+ ...(finding.assignee === undefined
111
+ ? {}
112
+ : { assignee: finding.assignee, assigneeEvidence: support(finding.assigneeEvidence) }),
113
+ };
114
+ });
115
+ return { schemaVersion: 1, key, extractor: text(input.extractor, 200), findings };
116
+ };
117
+ export const findingFingerprint = ({ id: _id, ...finding }) => findingHash(finding);
@@ -1,51 +1,13 @@
1
- import { MEETING_FACT_TYPES } from "./meeting-context.js";
2
- declare const MEETING_TASK_TYPES: readonly ["bug", "feature", "chore"];
1
+ import { type FactFinding, type Finding, type FindingsInput, type FindingsReceipt, findingFingerprint } from "./findings-rules.js";
2
+ export type { FindingEvidence, FindingOutcome } from "./findings-rules.js";
3
+ export { findingHash } from "./findings-rules.js";
3
4
  export declare const MEETING_FINDINGS_KIND = "meeting_findings";
4
- export interface FindingEvidence {
5
- reference: string;
6
- fingerprint: string;
7
- }
8
- interface FindingBase {
9
- id: string;
10
- project: string;
11
- evidence: FindingEvidence[];
12
- }
13
- export interface MeetingFactFinding extends FindingBase {
14
- kind: "fact";
15
- type: (typeof MEETING_FACT_TYPES)[number];
16
- statement: string;
17
- }
18
- export interface MeetingTaskFinding extends FindingBase {
5
+ export type MeetingFactFinding = FactFinding;
6
+ export type MeetingTaskFinding = Extract<Finding, {
19
7
  kind: "task";
20
- type: (typeof MEETING_TASK_TYPES)[number];
21
- title: string;
22
- reason: string;
23
- commitment: "explicit" | "inferred";
24
- dueAt?: string;
25
- dueEvidence?: string[];
26
- assignee?: string;
27
- assigneeEvidence?: string[];
28
- }
29
- export type MeetingFinding = MeetingFactFinding | MeetingTaskFinding;
30
- export interface MeetingFindingsInput {
31
- schemaVersion: 1;
32
- key: string;
33
- extractor: string;
34
- findings: MeetingFinding[];
35
- }
36
- export interface FindingOutcome {
37
- id: string;
38
- fingerprint: string;
39
- status: "accepted" | "rejected";
40
- reference: string | null;
41
- reused: boolean;
42
- }
43
- export interface MeetingFindingsReceipt {
44
- previewFingerprint: string;
45
- accepted: string[];
46
- items: FindingOutcome[];
47
- }
48
- export declare const findingHash: (value: unknown) => string;
8
+ }>;
9
+ export type MeetingFinding = Finding;
10
+ export type MeetingFindingsInput = FindingsInput;
11
+ export type MeetingFindingsReceipt = FindingsReceipt;
49
12
  export declare const validateMeetingFindings: (input: MeetingFindingsInput) => MeetingFindingsInput;
50
- export declare const meetingFindingFingerprint: ({ id: _id, ...finding }: MeetingFinding) => string;
51
- export {};
13
+ export declare const meetingFindingFingerprint: typeof findingFingerprint;