@wirecat/cli-core 0.417.0 → 0.418.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/cli/meeting-communications.d.ts +1 -0
- package/dist/context/evidence-response.js +9 -0
- package/dist/context/evidence.d.ts +13 -0
- package/dist/documents/markdown-blocks.js +4 -1
- package/dist/services/context-evidence.d.ts +1 -1
- package/dist/services/index.d.ts +2 -2
- package/dist/services/meeting-communications-preparation.js +2 -1
- package/dist/services/project-gathering.d.ts +15 -1
- package/dist/services/project-gathering.js +32 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/sqlite/context-evidence.js +28 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.418.0 — 25.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Project evidence says who wrote it.** `gatherProjectEvidence` returns `people`: who sent the run's message and mail items, person by person from the stored contacts (identities a person holds across messengers and mail count as one), with an item count split by context, the owner left out, at most 20 entries and `peopleOmitted` for the rest. It never matches names in the text. The context reader's message and mail items carry `sender` (identity, linked person, `owner`) to make that possible; project and meeting preparation items do not repeat it. See `docs/dev/PROJECT-GATHERING.md`.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **YAML front matter at the top of a Markdown file is no longer read as a rule and a heading.** `markdownBlocks` leaves it out, so a mirrored Doc does not show it, and a Doc edit written back to the file keeps it byte for byte. A `---` line further down is still a rule.
|
|
19
|
+
|
|
10
20
|
## 0.417.0 — 25.10.2026
|
|
11
21
|
|
|
12
22
|
### Fixed
|
|
@@ -39,6 +39,7 @@ export declare const mapPreparationCommunications: ({ items, ...section }: Meeti
|
|
|
39
39
|
text: string;
|
|
40
40
|
timeKind: "sent" | "source-modified" | "stored-update" | "meeting-start";
|
|
41
41
|
revision: number | null;
|
|
42
|
+
sender?: import("../context/evidence.js").ContextEvidenceSender | null;
|
|
42
43
|
selectionId: string;
|
|
43
44
|
textTruncated: boolean;
|
|
44
45
|
at: string;
|
|
@@ -62,6 +62,15 @@ export const validateContextEvidencePage = (page, request) => {
|
|
|
62
62
|
? !["source-modified", "stored-update"].includes(item.timeKind)
|
|
63
63
|
: item.timeKind !== (source.kind === "meeting" ? "meeting-start" : "sent"))
|
|
64
64
|
fail();
|
|
65
|
+
const sender = item.sender;
|
|
66
|
+
if (sender != null &&
|
|
67
|
+
(typeof sender !== "object" ||
|
|
68
|
+
typeof sender.provider !== "string" ||
|
|
69
|
+
typeof sender.id !== "string" ||
|
|
70
|
+
typeof sender.owner !== "boolean" ||
|
|
71
|
+
[sender.name, sender.person, sender.personName].some((v) => v !== null && typeof v !== "string") ||
|
|
72
|
+
(sender.person !== null && !/^person:[1-9]\d*$/.test(sender.person))))
|
|
73
|
+
fail();
|
|
65
74
|
const text = [item.text, item.title, item.url, item.provider, item.account, item.timeKind];
|
|
66
75
|
if (text.some((value) => value !== null && typeof value !== "string"))
|
|
67
76
|
fail();
|
|
@@ -36,6 +36,19 @@ export interface ContextEvidenceItem {
|
|
|
36
36
|
timeKind: "sent" | "source-modified" | "stored-update" | "meeting-start";
|
|
37
37
|
updatedAt: number;
|
|
38
38
|
revision: number | null;
|
|
39
|
+
/** Who wrote a message or email, as stored; absent for documents and meeting cues. */
|
|
40
|
+
sender?: ContextEvidenceSender | null;
|
|
41
|
+
}
|
|
42
|
+
export interface ContextEvidenceSender {
|
|
43
|
+
provider: string;
|
|
44
|
+
/** The identity's id in its provider; an email address for mail. */
|
|
45
|
+
id: string;
|
|
46
|
+
name: string | null;
|
|
47
|
+
/** `person:<id>` when the store links the identity to a person. */
|
|
48
|
+
person: string | null;
|
|
49
|
+
personName: string | null;
|
|
50
|
+
/** The identity is the owner's: linked to the owner person, or the account's own. */
|
|
51
|
+
owner: boolean;
|
|
39
52
|
}
|
|
40
53
|
export interface ContextEvidencePage {
|
|
41
54
|
source: ContextSource;
|
|
@@ -8,8 +8,11 @@ const loadParser = () => (parser ??= Promise.all([
|
|
|
8
8
|
]).then(([{ fromMarkdown }, { gfmFromMarkdown }, { gfm }]) => (markdown) => fromMarkdown(markdown, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] })));
|
|
9
9
|
/** Markdown (CommonMark + GFM tables, task lists and strikethrough) as the blocks a provider can write natively. */
|
|
10
10
|
export const markdownBlocks = async (markdown) => (await placedMarkdownBlocks(markdown)).map((placed) => placed.block);
|
|
11
|
+
// YAML front matter is the file's metadata, not its text: CommonMark alone reads it as a rule and a heading.
|
|
12
|
+
const FRONT_MATTER = /^---[ \t]*\r?\n(?:[^\n]*\n)*?---[ \t]*(?=\r?\n|$)/;
|
|
11
13
|
export const placedMarkdownBlocks = async (markdown) => {
|
|
12
|
-
|
|
14
|
+
// Blanked, not cut, so every offset and column still points into the file as written.
|
|
15
|
+
const tree = (await loadParser())(markdown.replace(FRONT_MATTER, (matter) => matter.replace(/[^\n]/g, " ")));
|
|
13
16
|
const definitions = new Map();
|
|
14
17
|
const collect = (node) => {
|
|
15
18
|
if (node.type === "definition")
|
|
@@ -5,6 +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, ContextRecordReader, ContextSource, } from "../context/evidence.js";
|
|
8
|
+
export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextEvidenceSender, ContextIndex, ContextRecordReader, ContextSource, } from "../context/evidence.js";
|
|
9
9
|
export { contextEvidenceFingerprint } from "../context/evidence.js";
|
|
10
10
|
export type { ContextReadLimits, ContextReadUsage } from "../context/read-budget.js";
|
package/dist/services/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type BotCheckService } from "../botcheck/service.js";
|
|
2
|
-
export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextIndex, ContextReadLimits, ContextReadUsage, ContextRecordReader, ContextSource, ScopedContextEvidenceReader, } from "./context-evidence.js";
|
|
2
|
+
export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextEvidenceSender, ContextIndex, ContextReadLimits, ContextReadUsage, ContextRecordReader, ContextSource, ScopedContextEvidenceReader, } from "./context-evidence.js";
|
|
3
3
|
export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
|
|
4
4
|
export { excerpt } from "./excerpt.js";
|
|
5
5
|
export type { MediaArchiveOptions, MediaArchivePorts } from "./media-archive.js";
|
|
@@ -8,7 +8,7 @@ export type { MediaArchiveEntry, MediaIdentity } from "./media-archive-manifest.
|
|
|
8
8
|
export type { MoveMediaArchiveOptions } from "./media-archive-move.js";
|
|
9
9
|
export { moveMediaArchive } from "./media-archive-move.js";
|
|
10
10
|
export { type DownloadedModel, downloadImageModel, downloadTextModel, type ModelDownloadOptions, } from "./model-downloads.js";
|
|
11
|
-
export type { ProjectEvidenceContext, ProjectEvidenceItem, ProjectGathering, ProjectGatheringPorts, ProjectGatheringStop, ProjectIndexCatchUp, ProjectSourceFetcher, } from "./project-gathering.js";
|
|
11
|
+
export type { ProjectEvidenceContext, ProjectEvidenceItem, ProjectEvidencePerson, ProjectGathering, ProjectGatheringPorts, ProjectGatheringStop, ProjectIndexCatchUp, ProjectSourceFetcher, } from "./project-gathering.js";
|
|
12
12
|
export { gatherProjectEvidence } from "./project-gathering.js";
|
|
13
13
|
export type { ProjectNeighbour, ProjectNeighbourReader } from "./project-gathering-around.js";
|
|
14
14
|
export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
|
|
@@ -55,7 +55,8 @@ export const readPreparationCommunications = async (plan, signal) => {
|
|
|
55
55
|
const text = excerpt(item.text, plan.input.query, 300, { literal: true });
|
|
56
56
|
// excerpt() puts every text on one line, so only a shorter result means words were cut.
|
|
57
57
|
const whole = item.text.replace(/\s+/g, " ").trim();
|
|
58
|
-
|
|
58
|
+
const { sender: _, ...rest } = item;
|
|
59
|
+
items.push({ ...rest, text, selectionId: read.id, textTruncated: text !== whole });
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
catch (error) {
|
|
@@ -26,7 +26,7 @@ export interface ProjectIndexCatchUp {
|
|
|
26
26
|
documents: number;
|
|
27
27
|
code: string | null;
|
|
28
28
|
}
|
|
29
|
-
export interface ProjectEvidenceItem extends Omit<ContextEvidenceItem, "text"> {
|
|
29
|
+
export interface ProjectEvidenceItem extends Omit<ContextEvidenceItem, "text" | "sender"> {
|
|
30
30
|
kind: ContextSource["kind"];
|
|
31
31
|
source: number;
|
|
32
32
|
text: string;
|
|
@@ -44,6 +44,18 @@ export interface ProjectEvidenceItem extends Omit<ContextEvidenceItem, "text"> {
|
|
|
44
44
|
* naming only a person.
|
|
45
45
|
*/
|
|
46
46
|
export type ProjectEvidenceContext = "project" | "linked-chat" | "sender" | "person-only";
|
|
47
|
+
/** Who wrote this run's message and mail items: one stored person, or one identity linked to none. */
|
|
48
|
+
export interface ProjectEvidencePerson {
|
|
49
|
+
person: string | null;
|
|
50
|
+
name: string;
|
|
51
|
+
identities: {
|
|
52
|
+
provider: string;
|
|
53
|
+
account: string;
|
|
54
|
+
id: string;
|
|
55
|
+
}[];
|
|
56
|
+
items: number;
|
|
57
|
+
contexts: Partial<Record<ProjectEvidenceContext, number>>;
|
|
58
|
+
}
|
|
47
59
|
export type ProjectGatheringStop = "reads" | "read-bytes" | "elapsed" | "items" | null;
|
|
48
60
|
export declare const gatherProjectEvidence: (ports: ProjectGatheringPorts, request: ProjectGatheringRequest) => Promise<{
|
|
49
61
|
schemaVersion: 1;
|
|
@@ -97,6 +109,8 @@ export declare const gatherProjectEvidence: (ports: ProjectGatheringPorts, reque
|
|
|
97
109
|
issue: string | null;
|
|
98
110
|
}[];
|
|
99
111
|
items: ProjectEvidenceItem[];
|
|
112
|
+
people: ProjectEvidencePerson[];
|
|
113
|
+
peopleOmitted: number;
|
|
100
114
|
oversized: {
|
|
101
115
|
ref: string;
|
|
102
116
|
lanes: string[];
|
|
@@ -6,6 +6,7 @@ import { attachNeighbours, documentMatches, } from "./project-gathering-around.j
|
|
|
6
6
|
import { decodeCheckpoint, encodeCheckpoint, projectGatheringRequest, projectUnits, seenKey, } from "./project-gathering-request.js";
|
|
7
7
|
const terminal = new Set(["done", "unavailable", "failed"]);
|
|
8
8
|
const PAGE = 20;
|
|
9
|
+
const PEOPLE = 20;
|
|
9
10
|
const CONTEXTS = ["person-only", "sender", "linked-chat", "project"];
|
|
10
11
|
const contextOf = (lane) => lane.kinds.includes("linked-chat")
|
|
11
12
|
? "linked-chat"
|
|
@@ -81,6 +82,7 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
81
82
|
const usage = { reads: 0, readBytes: 0, aroundBytes: 0, items: 0 };
|
|
82
83
|
const items = [];
|
|
83
84
|
const byRef = new Map();
|
|
85
|
+
const senderOf = new Map();
|
|
84
86
|
const matchedAgain = new Map();
|
|
85
87
|
const oversized = new Map();
|
|
86
88
|
let stopped = null;
|
|
@@ -117,8 +119,11 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
117
119
|
seen.add(seenKey(item.ref));
|
|
118
120
|
const short = excerpt(item.text, lane.query ?? "", 300, { literal: true });
|
|
119
121
|
const fingerprint = contextEvidenceFingerprint(item);
|
|
122
|
+
const { sender, ...stored } = item;
|
|
123
|
+
if (sender)
|
|
124
|
+
senderOf.set(item.ref, sender);
|
|
120
125
|
const entry = {
|
|
121
|
-
...
|
|
126
|
+
...stored,
|
|
122
127
|
kind,
|
|
123
128
|
source,
|
|
124
129
|
text: short,
|
|
@@ -258,6 +263,7 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
258
263
|
usage.aroundBytes += bytes;
|
|
259
264
|
},
|
|
260
265
|
}, (ref) => byRef.has(ref) || seen.has(seenKey(ref)), signal);
|
|
266
|
+
const people = peopleOf(items, senderOf);
|
|
261
267
|
const totals = {
|
|
262
268
|
reads: (previous?.totals.reads ?? 0) + usage.reads,
|
|
263
269
|
readBytes: (previous?.totals.readBytes ?? 0) + usage.readBytes,
|
|
@@ -321,6 +327,8 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
321
327
|
issue: progress[index]?.code ?? null,
|
|
322
328
|
})),
|
|
323
329
|
items,
|
|
330
|
+
people: people.slice(0, PEOPLE),
|
|
331
|
+
peopleOmitted: Math.max(0, people.length - PEOPLE),
|
|
324
332
|
oversized: [...oversized.values()],
|
|
325
333
|
matchedAgain: [...matchedAgain].map(([ref, lanes]) => ({ ref, lanes: [...lanes] })),
|
|
326
334
|
usage: { ...usage, elapsedMs: now() - started },
|
|
@@ -331,3 +339,26 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
331
339
|
checkpoint: resumable ? encodeCheckpoint(checkpoint) : null,
|
|
332
340
|
};
|
|
333
341
|
};
|
|
342
|
+
/** The owner's own messages say nothing about who else is involved, so they are left out. */
|
|
343
|
+
const peopleOf = (items, senderOf) => {
|
|
344
|
+
const byKey = new Map();
|
|
345
|
+
for (const item of items) {
|
|
346
|
+
const sender = senderOf.get(item.ref);
|
|
347
|
+
if (!sender || sender.owner)
|
|
348
|
+
continue;
|
|
349
|
+
const key = sender.person ?? JSON.stringify([sender.provider, sender.id]);
|
|
350
|
+
const found = byKey.get(key) ?? {
|
|
351
|
+
person: sender.person,
|
|
352
|
+
name: sender.personName ?? sender.name ?? sender.id,
|
|
353
|
+
identities: [],
|
|
354
|
+
items: 0,
|
|
355
|
+
contexts: {},
|
|
356
|
+
};
|
|
357
|
+
byKey.set(key, found);
|
|
358
|
+
if (!found.identities.some((i) => i.provider === sender.provider && i.account === item.account && i.id === sender.id))
|
|
359
|
+
found.identities.push({ provider: sender.provider, account: item.account, id: sender.id });
|
|
360
|
+
found.items++;
|
|
361
|
+
found.contexts[item.context] = (found.contexts[item.context] ?? 0) + 1;
|
|
362
|
+
}
|
|
363
|
+
return [...byKey.values()].sort((a, b) => b.items - a.items || a.name.localeCompare(b.name));
|
|
364
|
+
};
|
package/dist/store/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextSource, } from "../context/evidence.js";
|
|
1
|
+
export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextEvidenceSender, ContextSource, } from "../context/evidence.js";
|
|
2
2
|
export { FACT_STATUSES, FACT_TYPES, MEMORY_KINDS, MEMORY_SCOPES, MEMORY_STATUSES, TASK_TYPES, WORK_TASK_TYPES, } from "../domain/knowledge.js";
|
|
3
3
|
export type { TagConsolidationOptions, TagConsolidationPreview, TagConsolidationStore, } from "../domain/tag-consolidation.js";
|
|
4
4
|
export type { TagRedirectIndex, TagRedirectResolution, TagRedirectSnapshot } from "../domain/tag-redirects.js";
|
|
@@ -100,9 +100,19 @@ export const contextEvidenceOver = ({ database }) => ({
|
|
|
100
100
|
const bySender = senders === undefined
|
|
101
101
|
? ""
|
|
102
102
|
: ` AND ${corpus.sender} IN (SELECT id FROM identities WHERE provider=a.provider AND external_id IN (${senders.map(() => "?").join(",")}))`;
|
|
103
|
-
const
|
|
104
|
-
|
|
103
|
+
const from = `FROM ${corpus.table} n JOIN accounts a ON a.id=n.account_id ${corpus.join}`;
|
|
104
|
+
const where = `WHERE n.account_id=? AND a.provider=? AND a.external_id=? AND n.deleted_at IS NULL
|
|
105
105
|
AND ${byChat}${bySender}`;
|
|
106
|
+
const base = `${from} ${where}`;
|
|
107
|
+
const author = corpus.sender === null
|
|
108
|
+
? { join: "", columns: "NULL AS sender_id" }
|
|
109
|
+
: {
|
|
110
|
+
join: `LEFT JOIN identities si ON si.id=${corpus.sender} LEFT JOIN identity_links sl ON sl.identity_id=si.id
|
|
111
|
+
LEFT JOIN persons sp ON sp.id=sl.person_id`,
|
|
112
|
+
columns: `si.provider AS sender_provider,si.external_id AS sender_id,si.name AS sender_name,
|
|
113
|
+
sp.id AS sender_person,sp.name AS sender_person_name,
|
|
114
|
+
coalesce(sp.owner=1,0) OR (si.provider=a.provider AND si.external_id=a.external_id) AS sender_owner`,
|
|
115
|
+
};
|
|
106
116
|
// A chat or sender read uses no word index, so words still waiting for it hide nothing.
|
|
107
117
|
if (!searched)
|
|
108
118
|
coverage.index = "ready";
|
|
@@ -135,7 +145,8 @@ export const contextEvidenceOver = ({ database }) => ({
|
|
|
135
145
|
}
|
|
136
146
|
const selection = `SELECT n.id,n.external_id,${corpus.chat} AS chat,a.provider,a.external_id AS account,
|
|
137
147
|
${corpus.title} AS title,${corpus.body} AS body,${corpus.url} AS url,${corpus.at} AS at,
|
|
138
|
-
n.updated_at,${corpus.revision} AS revision,${corpus.timeKind} AS time_kind
|
|
148
|
+
n.updated_at,${corpus.revision} AS revision,${corpus.timeKind} AS time_kind,${author.columns}
|
|
149
|
+
${from} ${author.join} ${where}
|
|
139
150
|
AND ${corpus.at}>=? AND ${corpus.at}<?
|
|
140
151
|
${!searched ? "" : `AND n.id IN (SELECT rowid FROM ${corpus.index} WHERE ${corpus.index} MATCH ?)`}
|
|
141
152
|
${!searched ? "" : source.kind === "message" ? `AND n.normalized_text IS NOT NULL AND n.normalizer_version=${NORMALIZER_VERSION}` : `AND NOT EXISTS(SELECT 1 FROM ${source.kind === "mail" ? "email" : "document"}_index_pending p WHERE p.id=n.id AND p.indexable_type='${source.kind === "mail" ? "email" : "document"}')`}
|
|
@@ -198,6 +209,20 @@ export const contextEvidenceOver = ({ database }) => ({
|
|
|
198
209
|
timeKind: row.time_kind,
|
|
199
210
|
updatedAt: Number(row.updated_at),
|
|
200
211
|
revision: row.revision == null ? null : Number(row.revision),
|
|
212
|
+
...(corpus.sender === null
|
|
213
|
+
? {}
|
|
214
|
+
: {
|
|
215
|
+
sender: row.sender_id == null
|
|
216
|
+
? null
|
|
217
|
+
: {
|
|
218
|
+
provider: String(row.sender_provider),
|
|
219
|
+
id: String(row.sender_id),
|
|
220
|
+
name: row.sender_name == null ? null : String(row.sender_name),
|
|
221
|
+
person: row.sender_person == null ? null : `person:${row.sender_person}`,
|
|
222
|
+
personName: row.sender_person_name == null ? null : String(row.sender_person_name),
|
|
223
|
+
owner: Boolean(row.sender_owner),
|
|
224
|
+
},
|
|
225
|
+
}),
|
|
201
226
|
})),
|
|
202
227
|
};
|
|
203
228
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.418.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|