dsh-plugin-term-dictionary 0.0.0-stage → 1.1.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.
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ /** Common words the detector must never treat as jargon on their own. */
3
+ const EN = [
4
+ "a", "ability", "able", "about", "above", "accept", "according", "account",
5
+ "achieve", "across", "act", "action", "activity", "actually", "add", "address",
6
+ "admit", "adopt", "advance", "advantage", "affect", "after", "again", "against",
7
+ "age", "ago", "agree", "ahead", "air", "all", "allow", "almost",
8
+ "alone", "along", "already", "also", "although", "always", "among", "amount",
9
+ "analysis", "and", "animal", "another", "answer", "any", "anyone", "anything",
10
+ "appear", "apply", "approach", "area", "argue", "arm", "around", "arrive",
11
+ "art", "article", "as", "ask", "assume", "at", "attack", "attempt",
12
+ "attention", "author", "authority", "available", "avoid", "away", "baby", "back",
13
+ "bad", "bag", "ball", "bank", "bar", "basic", "basis", "be",
14
+ "bear", "beat", "beautiful", "because", "become", "bed", "before", "begin",
15
+ "behavior", "behind", "being", "believe", "below", "best", "better", "between",
16
+ "beyond", "big", "bill", "bit", "black", "blood", "blue", "board",
17
+ "body", "book", "born", "both", "box", "boy", "break", "bring",
18
+ "brother", "build", "building", "business", "but", "buy", "by", "call",
19
+ "can", "candidate", "capital", "car", "card", "care", "career", "carry",
20
+ "case", "catch", "cause", "cell", "center", "central", "century", "certain",
21
+ "certainly", "chair", "challenge", "chance", "change", "character", "charge", "check",
22
+ "child", "choice", "choose", "city", "claim", "class", "clear", "clearly",
23
+ "client", "close", "code", "cold", "college", "color", "come", "comment",
24
+ "commercial", "common", "community", "company", "compare", "computer", "concern", "condition",
25
+ "consider", "contain", "continue", "control", "conversation", "cost", "could", "country",
26
+ "couple", "course", "cover", "create", "culture", "current", "customer", "cut",
27
+ "damage", "dark", "data", "day", "dead", "deal", "death", "debate",
28
+ "decide", "decision", "deep", "degree", "describe", "design", "desire", "detail",
29
+ "determine", "develop", "development", "die", "difference", "different", "difficult", "direction",
30
+ "discover", "discuss", "discussion", "do", "doctor", "dog", "door", "down",
31
+ "draw", "dream", "drive", "drop", "during", "each", "early", "earn",
32
+ "earth", "east", "easy", "eat", "economic", "economy", "edge", "education",
33
+ "effect", "effort", "eight", "either", "else", "employee", "end", "energy",
34
+ "enjoy", "enough", "enter", "entire", "environment", "error", "especially", "establish",
35
+ "even", "evening", "event", "ever", "every", "everybody", "everyone", "everything",
36
+ "evidence", "exactly", "example", "exist", "expect", "experience", "expert", "explain",
37
+ "eye", "face", "fact", "factor", "fail", "fall", "family", "far",
38
+ "fast", "father", "fear", "feel", "feeling", "few", "field", "fight",
39
+ "figure", "file", "fill", "film", "final", "finally", "find", "fine",
40
+ "finish", "fire", "first", "five", "floor", "fly", "focus", "follow",
41
+ "food", "foot", "for", "force", "foreign", "forget", "form", "former",
42
+ "forward", "four", "free", "friend", "from", "front", "full", "fund",
43
+ "future", "game", "general", "generation", "get", "girl", "give", "glass",
44
+ "go", "goal", "good", "government", "great", "green", "ground", "group",
45
+ "grow", "growth", "guess", "guy", "half", "hand", "hang", "happen",
46
+ "happy", "hard", "have", "he", "head", "health", "hear", "heart",
47
+ "heat", "heavy", "help", "her", "here", "herself", "high", "him",
48
+ "himself", "his", "history", "hit", "hold", "home", "hope", "hot",
49
+ "hour", "house", "how", "however", "huge", "human", "hundred", "i",
50
+ "idea", "identify", "if", "image", "imagine", "impact", "important", "improve",
51
+ "in", "include", "including", "increase", "indeed", "indicate", "individual", "industry",
52
+ "information", "input", "inside", "instead", "interest", "interesting", "international", "into",
53
+ "investment", "involve", "is", "issue", "it", "item", "its", "itself",
54
+ "job", "join", "just", "keep", "key", "kid", "kill", "kind",
55
+ "know", "knowledge", "land", "language", "large", "last", "late", "later",
56
+ "law", "lead", "leader", "learn", "least", "leave", "left", "legal",
57
+ "less", "let", "letter", "level", "life", "light", "like", "likely",
58
+ "line", "list", "listen", "little", "live", "local", "long", "look",
59
+ "lose", "loss", "lot", "love", "low", "machine", "main", "maintain",
60
+ "major", "make", "man", "manage", "management", "manager", "many", "market",
61
+ "matter", "may", "maybe", "me", "mean", "measure", "media", "medical",
62
+ "meet", "meeting", "member", "memory", "mention", "message", "method", "middle",
63
+ "might", "million", "mind", "minute", "miss", "model", "modern", "moment",
64
+ "money", "month", "more", "morning", "most", "mother", "move", "movement",
65
+ "much", "music", "must", "my", "myself", "name", "nation", "national",
66
+ "natural", "nature", "near", "nearly", "necessary", "need", "network", "never",
67
+ "new", "news", "next", "nice", "night", "no", "nobody", "nor",
68
+ "north", "not", "note", "nothing", "notice", "now", "number", "of",
69
+ "off", "offer", "office", "official", "often", "oh", "old", "on",
70
+ "once", "one", "only", "open", "operation", "opportunity", "option", "or",
71
+ "order", "organization", "other", "others", "our", "ourselves", "out", "output",
72
+ "outside", "over", "own", "page", "paper", "parent", "part", "particular",
73
+ "particularly", "partner", "party", "pass", "past", "pattern", "pay", "people",
74
+ "per", "perform", "performance", "perhaps", "period", "person", "personal", "phone",
75
+ "physical", "pick", "picture", "piece", "place", "plan", "play", "please",
76
+ "point", "police", "policy", "political", "politics", "poor", "popular", "population",
77
+ "position", "possible", "power", "practice", "prepare", "present", "president", "press",
78
+ "pressure", "pretty", "prevent", "price", "private", "probably", "problem", "process",
79
+ "produce", "product", "production", "professional", "program", "project", "property", "protect",
80
+ "prove", "provide", "public", "pull", "purpose", "push", "put", "quality",
81
+ "question", "quickly", "quite", "radio", "raise", "range", "rate", "rather",
82
+ "reach", "read", "ready", "real", "reality", "realize", "really", "reason",
83
+ "receive", "recent", "recently", "recognize", "record", "red", "reduce", "relate",
84
+ "relationship", "remain", "remember", "remove", "replace", "report", "represent", "require",
85
+ "research", "resource", "respond", "response", "responsibility", "result", "return", "rich",
86
+ "right", "rise", "risk", "road", "role", "room", "rule", "run",
87
+ "safe", "same", "save", "say", "school", "science", "score", "season",
88
+ "seat", "second", "section", "security", "see", "seek", "seem", "sell",
89
+ "send", "senior", "sense", "series", "serious", "serve", "server", "service",
90
+ "set", "seven", "several", "share", "she", "short", "should", "show",
91
+ "side", "sign", "significant", "similar", "simple", "simply", "since", "single",
92
+ "sit", "site", "situation", "six", "size", "skill", "small", "so",
93
+ "social", "society", "some", "somebody", "someone", "something", "sometimes", "soon",
94
+ "sort", "sound", "source", "south", "space", "speak", "special", "specific",
95
+ "speech", "spend", "staff", "stage", "stand", "standard", "start", "state",
96
+ "statement", "stay", "step", "still", "stop", "store", "story", "strategy",
97
+ "street", "string", "strong", "structure", "student", "study", "stuff", "style",
98
+ "subject", "success", "successful", "such", "suddenly", "suggest", "summer", "support",
99
+ "sure", "system", "table", "take", "talk", "task", "teach", "teacher",
100
+ "team", "technology", "tell", "ten", "term", "test", "than", "thank",
101
+ "that", "the", "their", "them", "themselves", "then", "theory", "there",
102
+ "therefore", "these", "they", "thing", "think", "third", "this", "those",
103
+ "though", "thought", "thousand", "three", "through", "throughout", "thus", "time",
104
+ "to", "today", "together", "too", "top", "total", "toward", "town",
105
+ "trade", "traditional", "training", "travel", "treat", "tree", "trouble", "true",
106
+ "trust", "truth", "try", "turn", "type", "under", "understand", "unit",
107
+ "until", "up", "upon", "us", "use", "used", "useful", "user",
108
+ "usually", "value", "various", "very", "view", "visit", "voice", "wait",
109
+ "walk", "wall", "want", "war", "watch", "water", "way", "we",
110
+ "wear", "week", "weight", "well", "west", "what", "whatever", "when",
111
+ "where", "whether", "which", "while", "white", "who", "whole", "whom",
112
+ "whose", "why", "wide", "will", "win", "window", "with", "within",
113
+ "without", "woman", "wonder", "word", "work", "worker", "world", "worry",
114
+ "would", "write", "writer", "wrong", "yeah", "year", "yes", "yet",
115
+ "you", "young", "your", "yourself", "yourselves"
116
+ ];
117
+ const ZH = [
118
+ "一", "一下", "一个", "一些", "一样", "一点", "一直", "一般",
119
+ "一起", "上", "上面", "下", "下面", "不", "不同", "不少",
120
+ "与", "且", "东西", "个", "中", "中间", "为", "为了",
121
+ "为什么", "主要", "之前", "之后", "之间", "也", "也许", "了",
122
+ "事情", "些", "产品", "什么", "仅", "今天", "从", "从来",
123
+ "他", "他们", "代码", "以", "以前", "以及", "以后", "们",
124
+ "会", "但", "但是", "你", "你们", "使用", "例如", "全部",
125
+ "公司", "关于", "其中", "其他", "其实", "具体", "内容", "再",
126
+ "分钟", "列表", "别人", "到", "到底", "前面", "包括", "却",
127
+ "原因", "又", "及", "只", "可", "可以", "可能", "各",
128
+ "名称", "后面", "向", "和", "哪里", "因为", "困难", "在",
129
+ "地方", "基本", "复杂", "外面", "多", "大", "大家", "大概",
130
+ "大量", "太", "她", "她们", "好", "如果", "孩子", "它",
131
+ "它们", "客户端", "容易", "对", "对于", "将", "将会", "小",
132
+ "小时", "少", "尤其", "就", "工作", "差不多", "已", "已经",
133
+ "市场", "并", "应该", "当", "当然", "很", "很多", "必须",
134
+ "快速", "怎么", "总是", "情况", "意思", "慢慢", "我", "我们",
135
+ "或", "所以", "所有", "才", "把", "数据", "整个", "文件",
136
+ "方便", "方式", "方法", "时候", "时间", "明天", "昨天", "是",
137
+ "更", "曾", "最", "有", "有时", "朋友", "服务", "服务器",
138
+ "某", "模型", "正", "正在", "每", "比如", "比较", "然后",
139
+ "特别", "现在", "生活", "用户", "由于", "的", "目的", "直接",
140
+ "相同", "真", "究竟", "等等", "简单", "类型", "系统", "经常",
141
+ "结果", "给", "而", "能", "自己", "至于", "表格", "被",
142
+ "要", "让", "该", "跟", "输入", "输出", "还", "这",
143
+ "这个", "这些", "这里", "进行", "通过", "那", "那个", "那些",
144
+ "那里", "部分", "都", "里面", "重要", "错误", "问题", "间接",
145
+ "除了", "需要", "非常", "页面"
146
+ ];
147
+ module.exports = { EN, ZH };
@@ -0,0 +1,397 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * The page's view of the dictionary: one subscribable state object plus every
5
+ * mutation the UI performs.
6
+ *
7
+ * All state flows through this class so the panel, the popup and the detector
8
+ * always read the same generation. It is intentionally React-free: the bundle
9
+ * converts {@link DictionaryStore#getSnapshot} into hook state with `useSyncExternalStore`,
10
+ * which is what lets a mutation made by the transcript interaction surface
11
+ * re-render the sidebar panel.
12
+ *
13
+ * Persistence is layered rather than duplicated. The local store is written
14
+ * first and always, so an edit survives a reload even with no host; the remote
15
+ * store is written afterwards and a failure is recorded as a sync error instead
16
+ * of losing the edit.
17
+ */
18
+
19
+ const entriesModule = require("./entries.js");
20
+ const dictionary = require("./dictionary.js");
21
+
22
+ const { isUnexplained } = entriesModule;
23
+ const { emptyState, editEntry, recordSighting, removeEntry, clearAll, listEntries, summarize, mergeState, seal } = dictionary;
24
+
25
+ /** Longest context the store keeps per entry. */
26
+ const MAX_CONTEXT_CHARS = entriesModule.MAX_CONTEXT_CHARS;
27
+
28
+ /**
29
+ * @param options - `local` and `remote` stores, each with `load`/`save` (the
30
+ * remote one may be null), `now` for tests, and `logger`.
31
+ */
32
+ function createDictionaryStore(options) {
33
+ const local = options.local;
34
+ const remote = options.remote ?? null;
35
+ const now = typeof options.now === "function" ? options.now : () => Date.now();
36
+ const logger = options.logger;
37
+
38
+ /** What the host reported about itself: data directory, writability, model. */
39
+ let hostInfo = null;
40
+ let state = emptyState({ now: now() });
41
+ // The first projection runs before the initial hydration read, so it must be
42
+ // safe to build with no host information yet.
43
+ let view = buildView();
44
+ let status = "loading";
45
+ let syncError = "";
46
+ const listeners = new Set();
47
+
48
+ /** The derived, render-ready projection of the document. */
49
+ function buildView() {
50
+ return {
51
+ state,
52
+ stats: summarize(state),
53
+ list: state.entries,
54
+ hostInfo
55
+ };
56
+ }
57
+
58
+ /** Publish the current generation to subscribers. */
59
+ function emit() {
60
+ view = buildView();
61
+ for (const listener of [...listeners]) {
62
+ try {
63
+ listener();
64
+ } catch (error) {
65
+ logger?.warn?.(`term-dictionary: subscriber failed: ${error instanceof Error ? error.message : String(error)}`);
66
+ }
67
+ }
68
+ }
69
+
70
+ /** Swap in a new document, persist it locally, and notify. */
71
+ function commit(next, options) {
72
+ state = next;
73
+ emit();
74
+ const writing = options?.persist === false ? Promise.resolve() : local.save(state);
75
+ return Promise.resolve(writing).then(() => {
76
+ if (options?.sync !== false) queueRemoteSync();
77
+ });
78
+ }
79
+
80
+ let syncTimer = null;
81
+
82
+ /**
83
+ * Push the whole document to the host shortly after the last mutation.
84
+ *
85
+ * One debounced full push beats one request per keystroke: the document is
86
+ * small, the host merges by term, and a burst of edits costs one round trip.
87
+ */
88
+ function queueRemoteSync() {
89
+ if (remote === null) return;
90
+ if (syncTimer !== null) clearTimeout(syncTimer);
91
+ syncTimer = setTimeout(() => {
92
+ syncTimer = null;
93
+ flushRemote();
94
+ }, 400);
95
+ }
96
+
97
+ /**
98
+ * Send the current document to the host and adopt the answer.
99
+ *
100
+ * The host merges by term and tombstones, so its answer is the converged
101
+ * document; adopting it is what keeps the two sides identical and lets the
102
+ * host's copy of a deletion survive the page's next write.
103
+ */
104
+ function flushRemote() {
105
+ if (remote === null) return Promise.resolve();
106
+ return Promise.resolve(remote.save(state)).then(
107
+ (answer) => {
108
+ if (answer !== undefined && answer !== null && answer.ok === false) {
109
+ syncError = typeof answer.error === "string" && answer.error !== "" ? answer.error : "host rejected the dictionary";
110
+ } else {
111
+ syncError = "";
112
+ if (answer !== null && answer !== undefined && answer.state !== undefined) {
113
+ // The answer is sealed, so merging it would drop the host's
114
+ // tombstones; `mergeState` re-derives them from the document it
115
+ // receives plus the ones this side already holds.
116
+ const adopted = mergeState(state, answer.state, { now: now() });
117
+ // Only a real change is worth a repaint, and only a real change may
118
+ // loop back into another sync.
119
+ if (JSON.stringify(adopted.entries) !== JSON.stringify(state.entries) || adopted.deletedKeys.length !== state.deletedKeys.length) {
120
+ state = adopted;
121
+ void local.save(state);
122
+ }
123
+ }
124
+ }
125
+ emit();
126
+ },
127
+ (error) => {
128
+ syncError = error instanceof Error ? error.message : String(error);
129
+ emit();
130
+ }
131
+ );
132
+ }
133
+
134
+ return {
135
+ /** Subscribe to any change. @returns the unsubscribe function. */
136
+ subscribe(listener) {
137
+ listeners.add(listener);
138
+ return () => listeners.delete(listener);
139
+ },
140
+ /** The cached projection; stable between mutations. @returns the view. */
141
+ getSnapshot() {
142
+ return view;
143
+ },
144
+ /** Lifecycle status for the panel header. @returns `loading`, `ready` or `error`. */
145
+ getStatus() {
146
+ return status;
147
+ },
148
+ /** The last host sync failure, or an empty string. @returns the message. */
149
+ getSyncError() {
150
+ return syncError;
151
+ },
152
+ /** The raw document, for the detector and the export action. @returns the document. */
153
+ getState() {
154
+ return state;
155
+ },
156
+
157
+ /**
158
+ * Load local state, then adopt the host's document when it is reachable and
159
+ * newer. A host that has never seen this profile returns nothing, and the
160
+ * local document survives untouched.
161
+ * @returns a promise settling when hydration finishes.
162
+ */
163
+ async hydrate() {
164
+ status = "loading";
165
+ emit();
166
+ try {
167
+ state = local.load();
168
+ } catch (error) {
169
+ logger?.warn?.(`term-dictionary: local load failed: ${error instanceof Error ? error.message : String(error)}`);
170
+ }
171
+ if (remote !== null) {
172
+ try {
173
+ const answer = await remote.load();
174
+ if (answer !== null && answer !== undefined && answer.ok === true && answer.state !== undefined) {
175
+ state = mergeState(state, answer.state, { now: now() });
176
+ hostInfo = answer.state.location ?? null;
177
+ await local.save(state);
178
+ syncError = "";
179
+ } else if (answer !== null && answer !== undefined && answer.ok === false) {
180
+ syncError = typeof answer.error === "string" && answer.error !== "" ? answer.error : "host read failed";
181
+ }
182
+ } catch (error) {
183
+ syncError = error instanceof Error ? error.message : String(error);
184
+ }
185
+ }
186
+ status = "ready";
187
+ emit();
188
+ if (remote !== null && syncError === "") queueRemoteSync();
189
+ },
190
+
191
+ /**
192
+ * Create or update an entry from an explicit user action. The user's text is
193
+ * authoritative, so this always records `source: "user"`.
194
+ * @param term - the term being explained.
195
+ * @param patch - `definition`, `domain`, `aliases`, `pinned`.
196
+ * @returns a promise resolving to the stored entry.
197
+ */
198
+ async saveEntry(term, patch) {
199
+ const result = editEntry(state, term, patch, { now: now() });
200
+ await commit(result.state);
201
+ return result.entry;
202
+ },
203
+
204
+ /**
205
+ * Bring a deleted term back, as an explicit act.
206
+ *
207
+ * See `restoreEntry` in the core: a restoration is a decision with its own evidence and it
208
+ * withdraws the tombstone, rather than an edit that has to win a comparison on the way to the host.
209
+ * @param idOrTerm - the entry id, term or alias.
210
+ * @param patch - the text to restore it with, if any.
211
+ * @returns a promise resolving to `{ entry, restored }`.
212
+ */
213
+ async restoreEntry(idOrTerm, patch) {
214
+ const result = dictionary.restoreEntry(state, idOrTerm, patch, { now: now() });
215
+ if (result.restored) await commit(result.state);
216
+ return { entry: result.entry, restored: result.restored };
217
+ },
218
+
219
+ /**
220
+ * Record that a term appeared in a message, without overwriting a definition
221
+ * the user wrote.
222
+ *
223
+ * `deleted` is passed through because the caller needs it: a sighting of a term
224
+ * the user deleted is refused, and the collector must not announce it as newly
225
+ * collected. Dropping the flag here made the refusal invisible to the UI.
226
+ *
227
+ * @param sighting - `term`, `context`, `sessionId`, optional glossary fields.
228
+ * @returns a promise resolving to what happened, including `deleted`.
229
+ */
230
+ async noteSighting(sighting) {
231
+ const result = recordSighting(
232
+ state,
233
+ { ...sighting, context: typeof sighting.context === "string" ? sighting.context.slice(0, MAX_CONTEXT_CHARS) : "" },
234
+ { now: now() }
235
+ );
236
+ if (!result.changed) return { created: false, deleted: result.deleted === true, entry: result.entry ?? null };
237
+ await commit(result.state);
238
+ return { created: result.created, deleted: result.deleted === true, entry: result.entry };
239
+ },
240
+
241
+ /**
242
+ * Delete one entry.
243
+ * @param idOrTerm - the entry id, or its term.
244
+ * @returns a promise resolving to whether anything was deleted.
245
+ */
246
+ async deleteEntry(idOrTerm) {
247
+ const result = removeEntry(state, idOrTerm, { now: now() });
248
+ if (result.removed) await commit(result.state);
249
+ return result.removed;
250
+ },
251
+
252
+ /**
253
+ * Delete several entries, committing once.
254
+ *
255
+ * The panel's batch delete must not commit per row: every commit persists the
256
+ * whole document and pushes it to the host, so deleting twenty rows one call at a
257
+ * time would mean twenty full round trips and twenty merges. Each removal still
258
+ * leaves its own tombstone — that is what stops a union merge from restoring the
259
+ * rows from the host's copy — but the document is written once.
260
+ *
261
+ * @param idOrTerms - the entry ids, terms or aliases to remove.
262
+ * @returns a promise resolving to how many were actually removed.
263
+ */
264
+ async deleteEntries(idOrTerms) {
265
+ const list = Array.isArray(idOrTerms) ? idOrTerms : [];
266
+ let next = state;
267
+ let removed = 0;
268
+ for (const key of list) {
269
+ // Each removal needs the document the previous one produced: the tombstones
270
+ // accumulate, and a removal reads the record it is about to kill.
271
+ const result = removeEntry(next, key, { now: now() });
272
+ if (!result.removed) continue;
273
+ next = result.state;
274
+ removed++;
275
+ }
276
+ if (removed > 0) await commit(next);
277
+ return removed;
278
+ },
279
+
280
+ /**
281
+ * Toggle an entry's pinned flag.
282
+ * @param idOrTerm - the entry id, or its term.
283
+ * @returns a promise resolving to the updated entry.
284
+ */
285
+ async togglePinned(idOrTerm) {
286
+ const found = dictionary.findEntry(state, idOrTerm);
287
+ if (found === undefined) return null;
288
+ return this.saveEntry(found.term, { definition: found.definition, domain: found.domain, aliases: found.aliases, pinned: !found.pinned });
289
+ },
290
+
291
+ /**
292
+ * Forget every entry. The user asked for it explicitly, so no confirmation
293
+ * lives here; the panel owns that.
294
+ *
295
+ * Each removed entry leaves a tombstone, exactly like a single deletion: a
296
+ * bare empty document would be merged back together with the host's copy and
297
+ * the whole dictionary would return.
298
+ *
299
+ * @returns a promise settling when the clear is persisted.
300
+ */
301
+ async clearAll() {
302
+ const result = clearAll(state, { now: now() });
303
+ await commit(result.state);
304
+ return result.removed;
305
+ },
306
+
307
+ /**
308
+ * Bring a deleted entry back, as an explicit user action.
309
+ *
310
+ * The one-click counterpart to {@link DictionaryStore#deleteEntry}: retracting a deletion is
311
+ * not an edit, so it carries no patch — the entry comes back as it was, rather than back with
312
+ * the definition the editor happened to be seeded with. The revival itself is the dictionary's,
313
+ * where the merge rules it has to satisfy live.
314
+ *
315
+ * @param idOrTerm - the entry id, its term, or an alias.
316
+ * @returns a promise resolving to the revived entry, or null when there was nothing to revive.
317
+ */
318
+ async reviveEntry(idOrTerm) {
319
+ const result = dictionary.reviveEntry(state, idOrTerm, { now: now() });
320
+ if (result.revived !== true) return null;
321
+ await commit(result.state);
322
+ return result.entry;
323
+ },
324
+
325
+ /**
326
+ * Say something about an entry, or take it back.
327
+ *
328
+ * One method for both decisions the panel can make, because they travel together in the UI and
329
+ * each call persists the whole document: `{ feedback: { kind, note } }` records a remark,
330
+ * `{ feedback: null }` retracts it, and `{ untrusted: true|false }` is the verdict the automatic
331
+ * explainer obeys. A field the patch leaves out is left alone, stamp included.
332
+ *
333
+ * @param idOrTerm - the entry id, or its term.
334
+ * @param patch - `feedback` and/or `untrusted`.
335
+ * @returns a promise resolving to the updated entry, or null when there is no such entry.
336
+ */
337
+ async markEntry(idOrTerm, patch) {
338
+ const found = dictionary.findEntry(state, idOrTerm);
339
+ if (found === undefined) return null;
340
+ // Through `saveEntry`, so a remark is recorded with the same provenance and the same revival
341
+ // authority as anything else the user asked for.
342
+ return this.saveEntry(found.term, patch);
343
+ },
344
+
345
+ /**
346
+ * The groups directly inside one path, each with how many entries it holds.
347
+ * @param prefix - the path to look inside, `""` for the top level.
348
+ * @returns `[{ group, name, count }]`.
349
+ */
350
+ groups(prefix) {
351
+ return dictionary.groupsIn(state.entries, prefix);
352
+ },
353
+
354
+ /**
355
+ * The visible list for the panel.
356
+ * @param query - search text.
357
+ * @param filter - `all`, `unexplained`, `pinned`, `deleted` or `flagged`.
358
+ * @param group - the group path to look inside, `""` for the top level.
359
+ * @returns the ordered entries. For `deleted`, the tombstones.
360
+ */
361
+ list(query, filter, group) {
362
+ const at = group === undefined || group === null ? "" : String(group);
363
+ // The deleted view is a different LIST rather than a different filter: tombstones live in
364
+ // `backing`, not in `entries`, so no predicate over the live list could ever show one. The group
365
+ // still applies — standing in a folder and asking what was deleted there means that folder.
366
+ if (filter === "deleted") return dictionary.listDeleted(state, query).filter((entry) => (typeof entry.group === "string" ? entry.group : "") === at);
367
+ return listEntries(state, query, filter, at);
368
+ },
369
+
370
+ /**
371
+ * Look one entry up by term text or alias.
372
+ * @param idOrTerm - the entry id, or its term.
373
+ * @returns the entry, or null.
374
+ */
375
+ find(idOrTerm) {
376
+ return dictionary.findEntry(state, idOrTerm) ?? null;
377
+ },
378
+
379
+ /** Force an immediate host sync, used by the panel's retry action. */
380
+ retrySync() {
381
+ if (syncTimer !== null) {
382
+ clearTimeout(syncTimer);
383
+ syncTimer = null;
384
+ }
385
+ return flushRemote().then(() => this.hydrate());
386
+ },
387
+
388
+ /** Release timers. @returns nothing. */
389
+ dispose() {
390
+ if (syncTimer !== null) clearTimeout(syncTimer);
391
+ syncTimer = null;
392
+ listeners.clear();
393
+ }
394
+ };
395
+ }
396
+
397
+ module.exports = { createDictionaryStore, isUnexplained };