@wirecat/cli-core 0.377.0 → 0.378.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 +17 -1
- package/dist/context/evidence-response.js +2 -2
- package/dist/context/evidence.d.ts +5 -2
- package/dist/context/evidence.js +21 -6
- package/dist/services/context-evidence.js +1 -0
- package/dist/services/index.d.ts +1 -1
- package/dist/services/meeting-communications-selection.d.ts +1 -0
- package/dist/services/messages-archive-search.js +1 -1
- package/dist/services/project-gathering-request.d.ts +30 -8
- package/dist/services/project-gathering-request.js +61 -8
- package/dist/services/project-gathering.d.ts +5 -2
- package/dist/services/project-gathering.js +11 -5
- package/dist/store/sqlite/context-evidence.js +24 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,23 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## 0.378.0 — 21.10.2026
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`gatherProjectEvidence` reads what a person wrote.** A person given with `senders: [{ provider, id }]` (their
|
|
13
|
+
stored identities, at most 16; an email identity's id is its address) and a `reference` gets a lane of its own
|
|
14
|
+
(`kinds: ["sender"]`, `query: null`, `senders`): every message or mail one of those identities wrote in the window,
|
|
15
|
+
read only through a selected source of that identity's provider. `name` becomes optional for such a person. An
|
|
16
|
+
item found only that way has `context: "sender"`, ranked between `linked-chat` and `person-only`. The context reader
|
|
17
|
+
takes the same filter as `senders` (`coverage.matching: "sender"`). Requests without senders keep their
|
|
18
|
+
fingerprint, so their checkpoints still resume. See [project gathering](docs/dev/PROJECT-GATHERING.md).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **Discovery search accepts thread context.** `search messages --discover --thread` expands the ranked hits
|
|
23
|
+
through the shared bounded thread reader instead of refusing inside candidate search. Reply provenance,
|
|
24
|
+
time context and thread limits are preserved.
|
|
9
25
|
|
|
10
26
|
## 0.377.0 — 20.10.2026
|
|
11
27
|
|
|
@@ -8,7 +8,7 @@ export const validateContextEvidencePage = (page, request) => {
|
|
|
8
8
|
throw new CliError("invalid_response", "context reader returned invalid or out-of-scope evidence");
|
|
9
9
|
};
|
|
10
10
|
try {
|
|
11
|
-
const { source, chat, after, before, limit, maxReadBytes } = contextEvidenceRequest(request);
|
|
11
|
+
const { source, chat, senders, after, before, limit, maxReadBytes } = contextEvidenceRequest(request);
|
|
12
12
|
if (!page || contextSourceKey(contextSource(page.source)) !== contextSourceKey(source))
|
|
13
13
|
fail();
|
|
14
14
|
if (!Array.isArray(page.items) ||
|
|
@@ -23,7 +23,7 @@ export const validateContextEvidencePage = (page, request) => {
|
|
|
23
23
|
typeof c.hasMore !== "boolean" ||
|
|
24
24
|
typeof c.hasUndated !== "boolean" ||
|
|
25
25
|
!["ready", "pending", "missing", "unknown"].includes(c.index) ||
|
|
26
|
-
c.matching !== (chat
|
|
26
|
+
c.matching !== (chat !== undefined ? "chat" : senders !== undefined ? "sender" : "literal-words") ||
|
|
27
27
|
c.completeness !== "unknown" ||
|
|
28
28
|
c.consistency !== "snapshot-per-page")
|
|
29
29
|
fail();
|
|
@@ -12,6 +12,8 @@ export interface ContextEvidenceRequest {
|
|
|
12
12
|
query?: string;
|
|
13
13
|
/** One stored chat or mail thread of the source's account, by its external id; refused for documents and meetings. */
|
|
14
14
|
chat?: string;
|
|
15
|
+
/** Messages or mail written by these identities of the source's provider (mail: addresses); no query or chat. */
|
|
16
|
+
senders?: string[];
|
|
15
17
|
after: number;
|
|
16
18
|
before: number;
|
|
17
19
|
limit?: number;
|
|
@@ -55,7 +57,7 @@ export interface ContextEvidencePage {
|
|
|
55
57
|
hasMore: boolean;
|
|
56
58
|
hasUndated: boolean;
|
|
57
59
|
completeness: "unknown";
|
|
58
|
-
matching: "literal-words" | "chat";
|
|
60
|
+
matching: "literal-words" | "chat" | "sender";
|
|
59
61
|
consistency: "snapshot-per-page";
|
|
60
62
|
};
|
|
61
63
|
}
|
|
@@ -83,12 +85,13 @@ export declare const fittingRows: (sizes: readonly number[], limit: number, maxR
|
|
|
83
85
|
export interface ContextRecordReader {
|
|
84
86
|
read(ref: string): Promise<Pick<ContextEvidenceItem, "ref" | "title" | "text" | "revision"> | null>;
|
|
85
87
|
}
|
|
86
|
-
export declare const contextCursorScope: (request: Pick<ContextEvidenceRequest, "source" | "query" | "chat" | "after" | "before">) => string;
|
|
88
|
+
export declare const contextCursorScope: (request: Pick<ContextEvidenceRequest, "source" | "query" | "chat" | "senders" | "after" | "before">) => string;
|
|
87
89
|
export declare const contextSource: (value: ContextSource) => ContextSource;
|
|
88
90
|
export declare const contextEvidenceRequest: (input: ContextEvidenceRequest) => {
|
|
89
91
|
source: ContextSource;
|
|
90
92
|
query: string | undefined;
|
|
91
93
|
chat: string | undefined;
|
|
94
|
+
senders: string[] | undefined;
|
|
92
95
|
words: RegExpMatchArray | [];
|
|
93
96
|
after: number;
|
|
94
97
|
before: number;
|
package/dist/context/evidence.js
CHANGED
|
@@ -29,6 +29,7 @@ export const contextCursorScope = (request) => createHash("sha256")
|
|
|
29
29
|
request.before,
|
|
30
30
|
// Only a chat read hashes its chat, so cursors issued before chat reads existed stay valid.
|
|
31
31
|
...(request.chat === undefined ? [] : [request.chat]),
|
|
32
|
+
...(request.senders === undefined ? [] : [["senders", ...request.senders]]),
|
|
32
33
|
]))
|
|
33
34
|
.digest("hex");
|
|
34
35
|
const fields = (value, allowed, prefix) => {
|
|
@@ -53,7 +54,7 @@ export const contextSource = (value) => {
|
|
|
53
54
|
};
|
|
54
55
|
export const contextEvidenceRequest = (input) => {
|
|
55
56
|
const source = contextSource(input?.source);
|
|
56
|
-
fields(input, ["source", "query", "chat", "after", "before", "limit", "maxReadBytes", "cursor", "signal"], "");
|
|
57
|
+
fields(input, ["source", "query", "chat", "senders", "after", "before", "limit", "maxReadBytes", "cursor", "signal"], "");
|
|
57
58
|
const integer = (value, name, low, high) => {
|
|
58
59
|
if (!Number.isSafeInteger(value) || value < low || value > high)
|
|
59
60
|
throw new CliError("validation_error", `${name} must be a whole number from ${low} to ${high}`);
|
|
@@ -72,11 +73,25 @@ export const contextEvidenceRequest = (input) => {
|
|
|
72
73
|
if (input.query !== undefined)
|
|
73
74
|
throw new CliError("validation_error", "a chat read takes no query");
|
|
74
75
|
}
|
|
76
|
+
const senders = input.senders;
|
|
77
|
+
if (senders !== undefined) {
|
|
78
|
+
if (!Array.isArray(senders) ||
|
|
79
|
+
!senders.length ||
|
|
80
|
+
senders.length > 16 ||
|
|
81
|
+
senders.some((id) => typeof id !== "string" || !id.trim() || Buffer.byteLength(id) > 1024) ||
|
|
82
|
+
new Set(senders).size !== senders.length)
|
|
83
|
+
throw new CliError("validation_error", "senders needs 1–16 distinct identity ids");
|
|
84
|
+
if (source.kind !== "message" && source.kind !== "mail")
|
|
85
|
+
throw new CliError("validation_error", "only a message or mail source reads by sender");
|
|
86
|
+
if (input.query !== undefined || chat !== undefined)
|
|
87
|
+
throw new CliError("validation_error", "a sender read takes no query or chat");
|
|
88
|
+
}
|
|
89
|
+
const bare = chat === undefined && senders === undefined;
|
|
75
90
|
const query = input.query;
|
|
76
|
-
if (
|
|
91
|
+
if (bare && (typeof query !== "string" || !query.trim() || Buffer.byteLength(query) > 2048))
|
|
77
92
|
throw new CliError("validation_error", "query needs 1–2048 UTF-8 bytes of literal words");
|
|
78
93
|
const words = query?.match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
79
|
-
if (
|
|
94
|
+
if (bare && (!words.length || words.length > 16))
|
|
80
95
|
throw new CliError("validation_error", "query needs 1–16 literal words");
|
|
81
96
|
const limit = integer(input.limit ?? 10, "limit", 1, 20);
|
|
82
97
|
const maxReadBytes = integer(input.maxReadBytes ?? 65536, "maxReadBytes", 1, CONTEXT_MAX_READ_BYTES);
|
|
@@ -91,7 +106,7 @@ export const contextEvidenceRequest = (input) => {
|
|
|
91
106
|
id: integer(input.cursor.id, "cursor.id", 1, Number.MAX_SAFE_INTEGER),
|
|
92
107
|
scope: input.cursor.scope,
|
|
93
108
|
};
|
|
94
|
-
if (cursor && cursor.scope !== contextCursorScope({ source, query, chat, after, before }))
|
|
95
|
-
throw new CliError("validation_error", "cursor belongs to another source, query, chat or time window");
|
|
96
|
-
return { source, query, chat, words, after, before, limit, maxReadBytes, cursor, signal: input.signal };
|
|
109
|
+
if (cursor && cursor.scope !== contextCursorScope({ source, query, chat, senders, after, before }))
|
|
110
|
+
throw new CliError("validation_error", "cursor belongs to another source, query, chat, senders or time window");
|
|
111
|
+
return { source, query, chat, senders, words, after, before, limit, maxReadBytes, cursor, signal: input.signal };
|
|
97
112
|
};
|
|
@@ -23,6 +23,7 @@ export const createContextEvidenceReader = (port, allowed, limits) => {
|
|
|
23
23
|
source: Object.freeze({ ...source }),
|
|
24
24
|
...(validated.query === undefined ? {} : { query: validated.query }),
|
|
25
25
|
...(validated.chat === undefined ? {} : { chat: validated.chat }),
|
|
26
|
+
...(validated.senders === undefined ? {} : { senders: Object.freeze([...validated.senders]) }),
|
|
26
27
|
after: validated.after,
|
|
27
28
|
before: validated.before,
|
|
28
29
|
limit: validated.limit,
|
package/dist/services/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type { ProjectNeighbour, ProjectNeighbourReader } from "./project-gatheri
|
|
|
9
9
|
export { documentPassage, storedMessageNeighbours, storedNeighbours } from "./project-gathering-around.js";
|
|
10
10
|
export type { EvidenceMention, EvidenceMentions } from "./project-gathering-mentions.js";
|
|
11
11
|
export { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
12
|
-
export type { ProjectFetchState, ProjectGatheringBudgets, ProjectGatheringRequest, ProjectLane, ProjectLaneKind, ProjectLinkedChat, ProjectUnitState, } from "./project-gathering-request.js";
|
|
12
|
+
export type { ProjectFetchState, ProjectGatheringBudgets, ProjectGatheringRequest, ProjectLane, ProjectLaneKind, ProjectLinkedChat, ProjectPerson, ProjectSender, ProjectUnitState, } from "./project-gathering-request.js";
|
|
13
13
|
import { type AccountService } from "./account.js";
|
|
14
14
|
import { type AdminService } from "./admin.js";
|
|
15
15
|
import { type AdminStatisticsService } from "./admin-statistics.js";
|
|
@@ -16,6 +16,7 @@ export declare const captureCommunicationSelections: (port: MeetingCommunication
|
|
|
16
16
|
source: import("./context-evidence.js").ContextSource;
|
|
17
17
|
query: string | undefined;
|
|
18
18
|
chat: string | undefined;
|
|
19
|
+
senders: string[] | undefined;
|
|
19
20
|
after: number;
|
|
20
21
|
before: number;
|
|
21
22
|
limit: number;
|
|
@@ -65,7 +65,7 @@ const searchPage = async (store, account, request, messenger) => {
|
|
|
65
65
|
throw new CliError("validation_error", "discovery searches the local archive; use backend archive");
|
|
66
66
|
await topUp(store);
|
|
67
67
|
const found = request.discover
|
|
68
|
-
? await searchDiscovery(store, account, request, messenger)
|
|
68
|
+
? await searchDiscovery(store, account, { ...request, thread: undefined }, messenger)
|
|
69
69
|
: await searchLucene(store, account, request, messenger);
|
|
70
70
|
return withThreads(store, found, request);
|
|
71
71
|
};
|
|
@@ -7,10 +7,7 @@ export interface ProjectGatheringRequest {
|
|
|
7
7
|
aliases?: string[];
|
|
8
8
|
};
|
|
9
9
|
keywords?: string[];
|
|
10
|
-
people?:
|
|
11
|
-
name: string;
|
|
12
|
-
reference?: string;
|
|
13
|
-
}[];
|
|
10
|
+
people?: ProjectPerson[];
|
|
14
11
|
sources: ContextSource[];
|
|
15
12
|
/** Chats the host confirmed as the project's; each is read whole, through the source of its own account. */
|
|
16
13
|
linkedChats?: ProjectLinkedChat[];
|
|
@@ -30,6 +27,20 @@ export interface ProjectGatheringRequest {
|
|
|
30
27
|
checkpoint?: string;
|
|
31
28
|
signal?: AbortSignal;
|
|
32
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* `name` is searched as words; `senders` are the person's stored identities, and every message or mail one of them
|
|
32
|
+
* wrote in the window is read. A sender lane needs the person's `reference`.
|
|
33
|
+
*/
|
|
34
|
+
export interface ProjectPerson {
|
|
35
|
+
name?: string;
|
|
36
|
+
reference?: string;
|
|
37
|
+
senders?: ProjectSender[];
|
|
38
|
+
}
|
|
39
|
+
/** One stored identity by its provider and the provider's own id; an email identity's id is its address. */
|
|
40
|
+
export interface ProjectSender {
|
|
41
|
+
provider: string;
|
|
42
|
+
id: string;
|
|
43
|
+
}
|
|
33
44
|
export interface ProjectLinkedChat {
|
|
34
45
|
provider: string;
|
|
35
46
|
account: string;
|
|
@@ -42,13 +53,15 @@ export interface ProjectGatheringBudgets {
|
|
|
42
53
|
maxElapsedMs?: number;
|
|
43
54
|
maxItems?: number;
|
|
44
55
|
}
|
|
45
|
-
export type ProjectLaneKind = "project" | "key" | "alias" | "keyword" | "person" | "linked-chat";
|
|
56
|
+
export type ProjectLaneKind = "project" | "key" | "alias" | "keyword" | "person" | "sender" | "linked-chat";
|
|
46
57
|
export interface ProjectLane {
|
|
47
58
|
id: string;
|
|
48
59
|
kinds: ProjectLaneKind[];
|
|
49
|
-
/** Null for a linked chat's lane, which
|
|
60
|
+
/** Null for a linked chat's or a sender's lane, which read every message of the chat or of the sender. */
|
|
50
61
|
query: string | null;
|
|
51
62
|
people: string[];
|
|
63
|
+
/** A sender lane only: the identities whose messages and mail it reads. */
|
|
64
|
+
senders?: ProjectSender[];
|
|
52
65
|
/** A linked chat's lane only: the chat's reference. */
|
|
53
66
|
chat?: string;
|
|
54
67
|
}
|
|
@@ -96,10 +109,18 @@ export declare const projectGatheringRequest: (input: ProjectGatheringRequest, n
|
|
|
96
109
|
aliases: string[];
|
|
97
110
|
};
|
|
98
111
|
keywords: string[];
|
|
99
|
-
people: {
|
|
112
|
+
people: ({
|
|
100
113
|
name: string;
|
|
101
114
|
reference: string | null;
|
|
102
|
-
|
|
115
|
+
senders?: undefined;
|
|
116
|
+
} | {
|
|
117
|
+
name: string | null;
|
|
118
|
+
reference: string | null;
|
|
119
|
+
senders: {
|
|
120
|
+
provider: string;
|
|
121
|
+
id: string;
|
|
122
|
+
}[];
|
|
123
|
+
})[];
|
|
103
124
|
lanes: ProjectLane[];
|
|
104
125
|
sources: ContextSource[];
|
|
105
126
|
linkedChats: {
|
|
@@ -148,6 +169,7 @@ export declare const projectUnits: (plan: ProjectGatheringPlan, window: {
|
|
|
148
169
|
before: number;
|
|
149
170
|
};
|
|
150
171
|
chat: string | undefined;
|
|
172
|
+
senders: string[] | undefined;
|
|
151
173
|
}[];
|
|
152
174
|
export declare const seenKey: (ref: string) => string;
|
|
153
175
|
export declare const encodeCheckpoint: (checkpoint: ProjectGatheringCheckpoint) => string;
|
|
@@ -6,7 +6,15 @@ import { formatReference } from "../domain/references.js";
|
|
|
6
6
|
const DAY = 86400000;
|
|
7
7
|
const MAX_TIME = 8_640_000_000_000_000;
|
|
8
8
|
export const CONTEXT_CHUNK_DAYS = 90;
|
|
9
|
-
const LIMITS = {
|
|
9
|
+
const LIMITS = {
|
|
10
|
+
lanes: 16,
|
|
11
|
+
sources: 32,
|
|
12
|
+
units: 512,
|
|
13
|
+
lookbackDays: 730,
|
|
14
|
+
people: 16,
|
|
15
|
+
linkedChats: 16,
|
|
16
|
+
senders: 16,
|
|
17
|
+
};
|
|
10
18
|
const BUDGETS = {
|
|
11
19
|
maxReads: [48, 512],
|
|
12
20
|
maxReadBytes: [2097152, 16777216],
|
|
@@ -77,11 +85,29 @@ export const projectGatheringRequest = (input, now) => {
|
|
|
77
85
|
const people = list(input.people, "people", LIMITS.people).map((person, i) => {
|
|
78
86
|
if (!person || typeof person !== "object")
|
|
79
87
|
return fail(`people.${i} needs a name`, `people.${i}`);
|
|
80
|
-
known(person, ["name", "reference"], `people.${i}.`);
|
|
88
|
+
known(person, ["name", "reference", "senders"], `people.${i}.`);
|
|
81
89
|
const p = person;
|
|
90
|
+
const reference = p.reference === undefined ? null : text(p.reference, `people.${i}.reference`, 1024);
|
|
91
|
+
if (p.senders === undefined)
|
|
92
|
+
return { name: laneQuery(p.name, `people.${i}.name`), reference };
|
|
93
|
+
const senders = list(p.senders, `people.${i}.senders`, LIMITS.senders).map((value, j) => {
|
|
94
|
+
const name = `people.${i}.senders.${j}`;
|
|
95
|
+
if (!value || typeof value !== "object")
|
|
96
|
+
return fail(`${name} needs provider and id`, name);
|
|
97
|
+
known(value, ["provider", "id"], `${name}.`);
|
|
98
|
+
const given = value;
|
|
99
|
+
return { provider: text(given.provider, `${name}.provider`, 128), id: text(given.id, `${name}.id`, 1024) };
|
|
100
|
+
});
|
|
101
|
+
if (!senders.length)
|
|
102
|
+
fail(`people.${i}.senders needs at least one identity`, `people.${i}.senders`);
|
|
103
|
+
if (new Set(senders.map((one) => JSON.stringify([one.provider, one.id]))).size !== senders.length)
|
|
104
|
+
fail(`people.${i}.senders lists one identity twice`, `people.${i}.senders`);
|
|
105
|
+
if (reference === null)
|
|
106
|
+
fail(`people.${i} needs a reference to read by sender`, `people.${i}.reference`);
|
|
82
107
|
return {
|
|
83
|
-
name: laneQuery(p.name, `people.${i}.name`),
|
|
84
|
-
reference
|
|
108
|
+
name: p.name === undefined ? null : laneQuery(p.name, `people.${i}.name`),
|
|
109
|
+
reference,
|
|
110
|
+
senders,
|
|
85
111
|
};
|
|
86
112
|
});
|
|
87
113
|
if (input.scope !== "explicit" && input.scope !== "all-connected")
|
|
@@ -139,7 +165,17 @@ export const projectGatheringRequest = (input, now) => {
|
|
|
139
165
|
for (const k of keywords)
|
|
140
166
|
lane("keyword", k);
|
|
141
167
|
for (const p of people)
|
|
142
|
-
|
|
168
|
+
if (p.name !== null)
|
|
169
|
+
lane("person", p.name, p.reference ?? p.name);
|
|
170
|
+
for (const p of people)
|
|
171
|
+
if (p.senders && p.reference !== null)
|
|
172
|
+
lanes.push({
|
|
173
|
+
id: `l${lanes.length + 1}`,
|
|
174
|
+
kinds: ["sender"],
|
|
175
|
+
query: null,
|
|
176
|
+
people: [p.reference],
|
|
177
|
+
senders: p.senders,
|
|
178
|
+
});
|
|
143
179
|
if (lanes.length > LIMITS.lanes)
|
|
144
180
|
fail(`at most ${LIMITS.lanes} distinct search lanes`, "keywords");
|
|
145
181
|
for (const linked of linkedChats) {
|
|
@@ -248,16 +284,33 @@ export const projectUnits = (plan, window) => {
|
|
|
248
284
|
sourceIndex,
|
|
249
285
|
chunk,
|
|
250
286
|
chat: undefined,
|
|
287
|
+
senders: undefined,
|
|
251
288
|
}))));
|
|
252
|
-
//
|
|
289
|
+
// Only the sources of a sender's own provider: an identity of one messenger never wrote in another.
|
|
290
|
+
const bySender = chunks.flatMap((chunk) => plan.lanes.flatMap((lane) => plan.sources.flatMap((source, sourceIndex) => {
|
|
291
|
+
if (!lane.senders || (source.kind !== "message" && source.kind !== "mail"))
|
|
292
|
+
return [];
|
|
293
|
+
const ids = lane.senders.filter((one) => one.provider === source.provider).map((one) => one.id);
|
|
294
|
+
return ids.length ? [{ lane, source, sourceIndex, chunk, chat: undefined, senders: ids }] : [];
|
|
295
|
+
})));
|
|
296
|
+
// Appended after every literal and sender unit: a checkpoint from a run without links keeps its unit order, and in each
|
|
253
297
|
// round a literal lane labels a shared hit before the linked chat does.
|
|
254
298
|
const linked = chunks.flatMap((chunk) => plan.linkedChats.flatMap(({ lane, source, chat }) => {
|
|
255
299
|
const found = plan.lanes.find((l) => l.id === lane);
|
|
256
300
|
return found && source !== null
|
|
257
|
-
? [
|
|
301
|
+
? [
|
|
302
|
+
{
|
|
303
|
+
lane: found,
|
|
304
|
+
source: plan.sources[source],
|
|
305
|
+
sourceIndex: source,
|
|
306
|
+
chunk,
|
|
307
|
+
chat,
|
|
308
|
+
senders: undefined,
|
|
309
|
+
},
|
|
310
|
+
]
|
|
258
311
|
: [];
|
|
259
312
|
}));
|
|
260
|
-
const units = [...literal, ...linked];
|
|
313
|
+
const units = [...literal, ...bySender, ...linked];
|
|
261
314
|
if (units.length > LIMITS.units)
|
|
262
315
|
fail(`${units.length} lane × source × 90-day reads exceed ${LIMITS.units}; narrow the lanes, sources or window`);
|
|
263
316
|
return units;
|
|
@@ -39,8 +39,11 @@ export interface ProjectEvidenceItem extends Omit<ContextEvidenceItem, "text"> {
|
|
|
39
39
|
/** A document hit with `around`: how many paragraphs after the one it centres on also name a lane word. */
|
|
40
40
|
moreMatches?: number;
|
|
41
41
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Strongest first: a hit that names the project, then one only from a linked chat, then one a person wrote, then one
|
|
44
|
+
* naming only a person.
|
|
45
|
+
*/
|
|
46
|
+
export type ProjectEvidenceContext = "project" | "linked-chat" | "sender" | "person-only";
|
|
44
47
|
export type ProjectGatheringStop = "reads" | "read-bytes" | "elapsed" | "items" | null;
|
|
45
48
|
export declare const gatherProjectEvidence: (ports: ProjectGatheringPorts, request: ProjectGatheringRequest) => Promise<{
|
|
46
49
|
schemaVersion: 1;
|
|
@@ -5,12 +5,14 @@ import { excerpt } from "./excerpt.js";
|
|
|
5
5
|
import { attachNeighbours, documentMatches, } from "./project-gathering-around.js";
|
|
6
6
|
import { decodeCheckpoint, encodeCheckpoint, projectGatheringRequest, projectUnits, seenKey, } from "./project-gathering-request.js";
|
|
7
7
|
const terminal = new Set(["done", "unavailable", "failed"]);
|
|
8
|
-
const CONTEXTS = ["person-only", "linked-chat", "project"];
|
|
8
|
+
const CONTEXTS = ["person-only", "sender", "linked-chat", "project"];
|
|
9
9
|
const contextOf = (lane) => lane.kinds.includes("linked-chat")
|
|
10
10
|
? "linked-chat"
|
|
11
|
-
: lane.kinds.
|
|
12
|
-
? "
|
|
13
|
-
: "
|
|
11
|
+
: lane.kinds.includes("sender")
|
|
12
|
+
? "sender"
|
|
13
|
+
: lane.kinds.every((k) => k === "person")
|
|
14
|
+
? "person-only"
|
|
15
|
+
: "project";
|
|
14
16
|
export const gatherProjectEvidence = async (ports, request) => {
|
|
15
17
|
if (!ports?.evidence || typeof ports.evidence.read !== "function")
|
|
16
18
|
throw new CliError("configuration_error", "stored context reads are unavailable in this store");
|
|
@@ -164,7 +166,11 @@ export const gatherProjectEvidence = async (ports, request) => {
|
|
|
164
166
|
try {
|
|
165
167
|
const page = await reader.read({
|
|
166
168
|
source: unit.source,
|
|
167
|
-
...(unit.chat
|
|
169
|
+
...(unit.chat !== undefined
|
|
170
|
+
? { chat: unit.chat }
|
|
171
|
+
: unit.senders !== undefined
|
|
172
|
+
? { senders: unit.senders }
|
|
173
|
+
: { query: unit.lane.query ?? undefined }),
|
|
168
174
|
after: unit.chunk.after,
|
|
169
175
|
before: unit.chunk.before,
|
|
170
176
|
limit,
|
|
@@ -20,6 +20,7 @@ const corpora = {
|
|
|
20
20
|
join: "JOIN chats c ON c.id=n.chat_id AND c.account_id=n.account_id",
|
|
21
21
|
where: "c.searchable=1",
|
|
22
22
|
chats: "chats",
|
|
23
|
+
sender: "n.sender_identity_id",
|
|
23
24
|
// A read that names the chat sees it even when it is hidden from search (searchable=0).
|
|
24
25
|
named: "c.external_id=?",
|
|
25
26
|
},
|
|
@@ -36,6 +37,7 @@ const corpora = {
|
|
|
36
37
|
join: "JOIN email_threads c ON c.id=n.email_thread_id AND c.account_id=n.account_id",
|
|
37
38
|
where: "a.provider='email'",
|
|
38
39
|
chats: "email_threads",
|
|
40
|
+
sender: "n.from_identity_id",
|
|
39
41
|
named: "a.provider='email' AND c.external_id=?",
|
|
40
42
|
},
|
|
41
43
|
document: {
|
|
@@ -51,6 +53,7 @@ const corpora = {
|
|
|
51
53
|
join: "",
|
|
52
54
|
where: "n.body IS NOT NULL AND CASE WHEN json_valid(n.metadata) THEN coalesce(json_extract(n.metadata, '$.catalog.active'),1) ELSE 1 END=1",
|
|
53
55
|
chats: null,
|
|
56
|
+
sender: null,
|
|
54
57
|
named: null,
|
|
55
58
|
},
|
|
56
59
|
};
|
|
@@ -58,11 +61,12 @@ const corpora = {
|
|
|
58
61
|
export const contextEvidenceOver = ({ database }) => ({
|
|
59
62
|
async read(original) {
|
|
60
63
|
const request = contextEvidenceRequest(original);
|
|
61
|
-
const { source, words, chat, after, before, limit, maxReadBytes, cursor, signal } = request;
|
|
64
|
+
const { source, words, chat, senders, after, before, limit, maxReadBytes, cursor, signal } = request;
|
|
62
65
|
if (source.kind === "meeting")
|
|
63
66
|
return readMeetingContextEvidence(database, request);
|
|
64
67
|
const corpus = corpora[source.kind];
|
|
65
|
-
const scope = contextCursorScope({ source, query: request.query, chat, after, before });
|
|
68
|
+
const scope = contextCursorScope({ source, query: request.query, chat, senders, after, before });
|
|
69
|
+
const searched = chat === undefined && senders === undefined;
|
|
66
70
|
signal?.throwIfAborted();
|
|
67
71
|
return read(database, () => {
|
|
68
72
|
const coverage = {
|
|
@@ -71,7 +75,7 @@ export const contextEvidenceOver = ({ database }) => ({
|
|
|
71
75
|
hasMore: false,
|
|
72
76
|
hasUndated: false,
|
|
73
77
|
completeness: "unknown",
|
|
74
|
-
matching: chat
|
|
78
|
+
matching: chat !== undefined ? "chat" : senders !== undefined ? "sender" : "literal-words",
|
|
75
79
|
consistency: "snapshot-per-page",
|
|
76
80
|
};
|
|
77
81
|
const empty = () => ({ source, items: [], next: null, readBytes: 0, coverage });
|
|
@@ -87,19 +91,27 @@ export const contextEvidenceOver = ({ database }) => ({
|
|
|
87
91
|
.get(source.accountId, chat))
|
|
88
92
|
return empty();
|
|
89
93
|
coverage.available = true;
|
|
90
|
-
const scopeParams = chat
|
|
94
|
+
const scopeParams = chat !== undefined
|
|
95
|
+
? [...accountParams, chat]
|
|
96
|
+
: senders !== undefined
|
|
97
|
+
? [...accountParams, ...senders]
|
|
98
|
+
: accountParams;
|
|
99
|
+
const byChat = chat === undefined ? corpus.where : corpus.named;
|
|
100
|
+
const bySender = senders === undefined
|
|
101
|
+
? ""
|
|
102
|
+
: ` AND ${corpus.sender} IN (SELECT id FROM identities WHERE provider=a.provider AND external_id IN (${senders.map(() => "?").join(",")}))`;
|
|
91
103
|
const base = `FROM ${corpus.table} n JOIN accounts a ON a.id=n.account_id ${corpus.join}
|
|
92
104
|
WHERE n.account_id=? AND a.provider=? AND a.external_id=? AND n.deleted_at IS NULL
|
|
93
|
-
AND ${
|
|
94
|
-
// A chat read uses no word index, so words still waiting for it hide nothing.
|
|
95
|
-
if (
|
|
105
|
+
AND ${byChat}${bySender}`;
|
|
106
|
+
// A chat or sender read uses no word index, so words still waiting for it hide nothing.
|
|
107
|
+
if (!searched)
|
|
96
108
|
coverage.index = "ready";
|
|
97
109
|
else if (!database.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(corpus.index)) {
|
|
98
110
|
coverage.index = "missing";
|
|
99
111
|
return empty();
|
|
100
112
|
}
|
|
101
113
|
coverage.hasUndated = Boolean(database.prepare(`SELECT 1 ${base} AND ${corpus.at} IS NULL LIMIT 1`).get(...scopeParams));
|
|
102
|
-
if (
|
|
114
|
+
if (searched && source.kind === "message") {
|
|
103
115
|
const state = database
|
|
104
116
|
.prepare("SELECT watermark,filled_through FROM search_index_state WHERE name='message_words'")
|
|
105
117
|
.get();
|
|
@@ -112,7 +124,7 @@ export const contextEvidenceOver = ({ database }) => ({
|
|
|
112
124
|
? "pending"
|
|
113
125
|
: "ready";
|
|
114
126
|
}
|
|
115
|
-
else if (
|
|
127
|
+
else if (searched) {
|
|
116
128
|
coverage.index = database
|
|
117
129
|
.prepare(`SELECT 1 ${base} AND ${corpus.at}>=? AND ${corpus.at}<?
|
|
118
130
|
AND EXISTS(SELECT 1 FROM ${source.kind === "mail" ? "email" : "document"}_index_pending p
|
|
@@ -125,12 +137,12 @@ export const contextEvidenceOver = ({ database }) => ({
|
|
|
125
137
|
${corpus.title} AS title,${corpus.body} AS body,${corpus.url} AS url,${corpus.at} AS at,
|
|
126
138
|
n.updated_at,${corpus.revision} AS revision,${corpus.timeKind} AS time_kind ${base}
|
|
127
139
|
AND ${corpus.at}>=? AND ${corpus.at}<?
|
|
128
|
-
${
|
|
129
|
-
${
|
|
140
|
+
${!searched ? "" : `AND n.id IN (SELECT rowid FROM ${corpus.index} WHERE ${corpus.index} MATCH ?)`}
|
|
141
|
+
${!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"}')`}
|
|
130
142
|
${cursor ? `AND (${corpus.at}<? OR (${corpus.at}=? AND n.id<?))` : ""}
|
|
131
143
|
ORDER BY ${corpus.at} DESC,n.id DESC LIMIT ?`;
|
|
132
144
|
const params = [...scopeParams, after, before];
|
|
133
|
-
if (
|
|
145
|
+
if (searched)
|
|
134
146
|
params.push(words.map((w) => `normalized_text:"${normalize(w)}"`).join(" AND "));
|
|
135
147
|
if (cursor)
|
|
136
148
|
params.push(cursor.at, cursor.at, cursor.id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.378.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",
|