@wirecat/cli-core 0.395.0 → 0.396.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 +30 -0
- package/dist/cli/commands-command.js +6 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/input-fields.d.ts +3 -0
- package/dist/cli/input-fields.js +27 -0
- package/dist/cli/messenger/attachments-command.js +16 -4
- package/dist/cli/messenger/conversation-answers.d.ts +5 -0
- package/dist/cli/messenger/message-answers.d.ts +5 -0
- package/dist/cli/messenger/message-writes.d.ts +5 -0
- package/dist/cli/messenger/search-answers.js +2 -0
- package/dist/cli/messenger/searches-command.d.ts +2 -0
- package/dist/cli/messenger/searches-command.js +2 -1
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/program.js +5 -2
- package/dist/entities/evidence.d.ts +40 -0
- package/dist/entities/index.d.ts +71 -1
- package/dist/entities/index.js +2 -0
- package/dist/entities/message.d.ts +9 -0
- package/dist/entities/message.js +6 -0
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +15 -0
- package/dist/search/lucene/registry.js +2 -2
- package/dist/services/messages-search.js +13 -2
- package/dist/services/messages.d.ts +5 -0
- package/dist/store/search-index.d.ts +4 -1
- package/dist/store/search-index.js +10 -1
- package/dist/store/sqlite/hit-places.d.ts +12 -0
- package/dist/store/sqlite/hit-places.js +76 -0
- package/dist/store/sqlite/lucene-fields.d.ts +8 -0
- package/dist/store/sqlite/lucene-fields.js +6 -4
- package/dist/store/sqlite/lucene.js +25 -70
- package/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/migrations.generated.js +19 -0
- package/dist/store/sqlite/note-index.d.ts +11 -1
- package/dist/store/sqlite/note-index.js +18 -6
- package/dist/store/sqlite/schema.d.ts +39 -0
- package/dist/store/sqlite/schema.js +4 -0
- package/dist/store/sqlite/transcript-query.d.ts +40 -0
- package/dist/store/sqlite/transcript-query.js +108 -0
- package/dist/store/sqlite/words.d.ts +6 -0
- package/package.json +3 -3
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { isNumberWord, widenWords } from "../../search/garbled.js";
|
|
2
|
+
import { knownRussianWord } from "../../search/russian-dictionary.js";
|
|
3
|
+
import { createStemmer, DEFAULT_STEMMERS, STEM_SEPARATOR } from "../../search/stem.js";
|
|
4
|
+
import { normalize } from "../normalize.js";
|
|
5
|
+
import { ftsMatch, quoted, SPEECH, stemMatch } from "./lucene-fields.js";
|
|
6
|
+
import { savedStemmers } from "./stems.js";
|
|
7
|
+
const predicates = (node) => node.kind === "predicate" ? [node] : node.clauses.flatMap(({ node }) => predicates(node));
|
|
8
|
+
/** A bare word and `transcript:` read transcripts; `text:` is only what was written. */
|
|
9
|
+
const readsTranscripts = (node) => (node.operator === "term" || node.operator === "phrase") &&
|
|
10
|
+
/[\p{L}\p{N}]/u.test(normalize(node.value)) &&
|
|
11
|
+
(node.field === "transcript" || (node.implicit === true && (node.field === "text" || node.field === "exact")));
|
|
12
|
+
/**
|
|
13
|
+
* Each leaf that reads transcripts, with what it matches there. A transcript always widens, not only when nothing
|
|
14
|
+
* matched as typed text does: a correct and a garbled spelling sit in one chat. Only a single word widens —
|
|
15
|
+
* FTS5 `NEAR` and phrases take no alternatives — and never under `exact:`, as meetings do.
|
|
16
|
+
*/
|
|
17
|
+
export const transcriptTerms = (database, root, item) => {
|
|
18
|
+
const leaves = predicates(root).filter(readsTranscripts);
|
|
19
|
+
const terms = new Map(leaves.map((node) => [node, { match: ftsMatch(node), widened: [], from: node.value }]));
|
|
20
|
+
const saved = savedStemmers(database);
|
|
21
|
+
if (item !== "message" || !leaves.length || saved === null)
|
|
22
|
+
return terms;
|
|
23
|
+
const stemmer = createStemmer(saved ?? DEFAULT_STEMMERS);
|
|
24
|
+
const stemsOf = (word) => [...new Set(stemmer.explain(word).map((part) => part.stem))];
|
|
25
|
+
const widening = [];
|
|
26
|
+
for (const [node, one] of terms) {
|
|
27
|
+
if (node.field === "exact")
|
|
28
|
+
continue;
|
|
29
|
+
const stems = stemMatch(stemmer, node);
|
|
30
|
+
// `NEAR` over the stems can bridge the separator between a text's Latin readings; those rows are left out.
|
|
31
|
+
if (stems)
|
|
32
|
+
Object.assign(one, { stems: node.slop === undefined ? stems : `(${stems}) NOT ${quoted(STEM_SEPARATOR)}` });
|
|
33
|
+
const words = normalize(node.value).match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
34
|
+
if (node.operator === "term" && words.length === 1 && !isNumberWord(words[0]))
|
|
35
|
+
widening.push({ node, literal: words[0], stems: stemsOf(node.value) });
|
|
36
|
+
}
|
|
37
|
+
if (!widening.length)
|
|
38
|
+
return terms;
|
|
39
|
+
const vocabulary = database
|
|
40
|
+
.prepare("SELECT term, doc FROM transcript_words_vocab WHERE col = 'normalized_text'")
|
|
41
|
+
.all()
|
|
42
|
+
.map((row) => ({ term: String(row.term), docs: Number(row.doc) }));
|
|
43
|
+
widenWords(widening, vocabulary, stemsOf, knownRussianWord).forEach((words, at) => {
|
|
44
|
+
const leaf = widening[at];
|
|
45
|
+
if (leaf)
|
|
46
|
+
Object.assign(terms.get(leaf.node), { widened: words });
|
|
47
|
+
});
|
|
48
|
+
return terms;
|
|
49
|
+
};
|
|
50
|
+
/** The attachment ids a leaf's transcripts match, as a condition on `att.id`; `part` keeps one of them. */
|
|
51
|
+
export const transcriptCondition = (terms, part = "all") => {
|
|
52
|
+
const parts = [];
|
|
53
|
+
const words = (match) => ({
|
|
54
|
+
sql: "att.id IN (SELECT rowid FROM attachment_words WHERE attachment_words MATCH ?)",
|
|
55
|
+
params: [match],
|
|
56
|
+
});
|
|
57
|
+
if (part === "all" || part === "typed")
|
|
58
|
+
parts.push(words(terms.match));
|
|
59
|
+
if ((part === "all" || part === "forms") && terms.stems)
|
|
60
|
+
parts.push({
|
|
61
|
+
sql: "att.id IN (SELECT rowid FROM transcript_stems WHERE transcript_stems MATCH ?)",
|
|
62
|
+
params: [`stems : (${terms.stems})`],
|
|
63
|
+
});
|
|
64
|
+
if ((part === "all" || part === "widened") && terms.widened.length)
|
|
65
|
+
parts.push(words(terms.widened.map(quoted).join(" OR ")));
|
|
66
|
+
return parts.length
|
|
67
|
+
? { sql: `(${parts.map(({ sql }) => sql).join(" OR ")})`, params: parts.flatMap(({ params }) => params) }
|
|
68
|
+
: { sql: "0", params: [] };
|
|
69
|
+
};
|
|
70
|
+
/** The items whose transcripts a leaf matches. */
|
|
71
|
+
export const transcriptItems = (item, terms, part) => {
|
|
72
|
+
const condition = transcriptCondition(terms, part);
|
|
73
|
+
return {
|
|
74
|
+
sql: `SELECT att.attachable_id FROM attachments att WHERE att.attachable_type='${item}' AND ${SPEECH} AND ${condition.sql}`,
|
|
75
|
+
params: condition.params,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Of the items `keys` names, the transcript the leaves match — one they match as typed or in another form first —
|
|
80
|
+
* and, when only a spelling recognition garbled matched it, which: as meetings say it.
|
|
81
|
+
*/
|
|
82
|
+
export const spokenTranscripts = (database, item, speaking, keys) => {
|
|
83
|
+
const any = speaking.map((terms) => transcriptCondition(terms));
|
|
84
|
+
if (!any.length)
|
|
85
|
+
return new Map();
|
|
86
|
+
const plain = speaking
|
|
87
|
+
.flatMap((terms) => [transcriptCondition(terms, "typed"), transcriptCondition(terms, "forms")])
|
|
88
|
+
.filter(({ sql }) => sql !== "0");
|
|
89
|
+
const rows = database
|
|
90
|
+
.prepare(`SELECT att.attachable_id AS id, att.text AS text, ${plain.length ? plain.map(({ sql }) => sql).join(" OR ") : "0"} AS plain FROM attachments att WHERE att.attachable_type='${item}' AND ${SPEECH} AND att.attachable_id IN (SELECT value FROM json_each(?)) AND (${any.map(({ sql }) => sql).join(" OR ")}) ORDER BY plain DESC, att.position`)
|
|
91
|
+
.all(...plain.flatMap(({ params }) => params), keys, ...any.flatMap(({ params }) => params));
|
|
92
|
+
const found = new Map();
|
|
93
|
+
for (const row of rows) {
|
|
94
|
+
const id = Number(row.id);
|
|
95
|
+
if (found.has(id))
|
|
96
|
+
continue;
|
|
97
|
+
const text = String(row.text);
|
|
98
|
+
const tokens = new Set(normalize(text).match(/[\p{L}\p{N}]+/gu) ?? []);
|
|
99
|
+
const corrections = Number(row.plain) === 1
|
|
100
|
+
? []
|
|
101
|
+
: speaking.flatMap(({ from, widened }) => {
|
|
102
|
+
const to = widened.filter((word) => tokens.has(word));
|
|
103
|
+
return to.length ? [{ from, to }] : [];
|
|
104
|
+
});
|
|
105
|
+
found.set(id, { text, corrections });
|
|
106
|
+
}
|
|
107
|
+
return found;
|
|
108
|
+
};
|
|
@@ -9,6 +9,12 @@ export type ScoredHit = StoredHit & {
|
|
|
9
9
|
matchedIn?: MatchPlace;
|
|
10
10
|
/** The text of that place, for the excerpt; never part of an answer. */
|
|
11
11
|
matchedText?: string;
|
|
12
|
+
/** `corrected`: found only through a spelling speech recognition garbled, in a voice note's transcript. */
|
|
13
|
+
match?: "words" | "beginnings" | "corrected";
|
|
14
|
+
corrections?: {
|
|
15
|
+
from: string;
|
|
16
|
+
to: string[];
|
|
17
|
+
}[];
|
|
12
18
|
};
|
|
13
19
|
export type MatchPlace = "text" | "transcript" | "filename" | "file";
|
|
14
20
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.396.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",
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
"@types/node": "^22.20.4",
|
|
208
208
|
"@types/pngjs": "6.0.5",
|
|
209
209
|
"@vitest/coverage-v8": "^5.0.2",
|
|
210
|
-
"@wirecat/cli-base": "0.50.
|
|
210
|
+
"@wirecat/cli-base": "0.50.1",
|
|
211
211
|
"commander": "15.0.0",
|
|
212
212
|
"cspell": "10.3.6",
|
|
213
213
|
"drizzle-kit": "1.0.0-rc.4",
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
},
|
|
228
228
|
"peerDependencies": {
|
|
229
229
|
"@napi-rs/canvas": ">=0.1.69 <2",
|
|
230
|
-
"@wirecat/cli-base": ">=0.
|
|
230
|
+
"@wirecat/cli-base": ">=0.50.1 <0.51.0",
|
|
231
231
|
"commander": ">=15"
|
|
232
232
|
},
|
|
233
233
|
"bin": {
|