@atlaskit/editor-plugin-autocomplete 0.1.0 → 0.3.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 (58) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/afm-cc/tsconfig.json +2 -1
  3. package/afm-jira/tsconfig.json +2 -1
  4. package/afm-products/tsconfig.json +2 -1
  5. package/build/tsconfig.json +20 -0
  6. package/build/url-module.d.ts +8 -0
  7. package/dist/cjs/autocompletePlugin.js +18 -5
  8. package/dist/cjs/autocompletePluginType.js +5 -1
  9. package/dist/cjs/pm-plugins/autocomplete-plugin.js +368 -0
  10. package/dist/cjs/pm-plugins/ghost-text-decoration.js +39 -0
  11. package/dist/cjs/pm-plugins/scoring-pipeline.js +256 -0
  12. package/dist/cjs/pm-plugins/slow-lane-client.js +199 -0
  13. package/dist/cjs/pm-plugins/text-predictor.js +796 -0
  14. package/dist/es2019/autocompletePlugin.js +20 -6
  15. package/dist/es2019/autocompletePluginType.js +1 -0
  16. package/dist/es2019/pm-plugins/autocomplete-plugin.js +368 -0
  17. package/dist/es2019/pm-plugins/ghost-text-decoration.js +33 -0
  18. package/dist/es2019/pm-plugins/scoring-pipeline.js +221 -0
  19. package/dist/es2019/pm-plugins/slow-lane-client.js +157 -0
  20. package/dist/es2019/pm-plugins/text-predictor.js +631 -0
  21. package/dist/esm/autocompletePlugin.js +18 -5
  22. package/dist/esm/autocompletePluginType.js +1 -0
  23. package/dist/esm/pm-plugins/autocomplete-plugin.js +362 -0
  24. package/dist/esm/pm-plugins/ghost-text-decoration.js +33 -0
  25. package/dist/esm/pm-plugins/scoring-pipeline.js +252 -0
  26. package/dist/esm/pm-plugins/slow-lane-client.js +192 -0
  27. package/dist/esm/pm-plugins/text-predictor.js +793 -0
  28. package/dist/types/autocompletePluginType.d.ts +6 -3
  29. package/dist/types/pm-plugins/autocomplete-plugin.d.ts +36 -0
  30. package/dist/types/pm-plugins/ghost-text-decoration.d.ts +7 -0
  31. package/dist/types/pm-plugins/scoring-pipeline.d.ts +33 -0
  32. package/dist/types/pm-plugins/slow-lane-client.d.ts +46 -0
  33. package/dist/types/pm-plugins/text-predictor.d.ts +90 -0
  34. package/dist/types-ts4.5/autocompletePluginType.d.ts +6 -3
  35. package/dist/types-ts4.5/pm-plugins/autocomplete-plugin.d.ts +36 -0
  36. package/dist/types-ts4.5/pm-plugins/ghost-text-decoration.d.ts +7 -0
  37. package/dist/types-ts4.5/pm-plugins/scoring-pipeline.d.ts +33 -0
  38. package/dist/types-ts4.5/pm-plugins/slow-lane-client.d.ts +46 -0
  39. package/dist/types-ts4.5/pm-plugins/text-predictor.d.ts +90 -0
  40. package/package.json +2 -2
  41. package/src/autocompletePlugin.tsx +25 -5
  42. package/src/autocompletePluginType.ts +14 -3
  43. package/src/pm-plugins/autocomplete-plugin/package.json +15 -0
  44. package/src/pm-plugins/autocomplete-plugin.ts +443 -0
  45. package/src/pm-plugins/data/combined_l2_l3_pos_tags.json +73571 -3
  46. package/src/pm-plugins/data/ghost_pos_tags.json +43 -3
  47. package/src/pm-plugins/data/grammar_transitions_10k.json +46 -3
  48. package/src/pm-plugins/data/l3_vocabulary.json +20002 -3
  49. package/src/pm-plugins/data/vocabulary_10k.json +38794 -3
  50. package/src/pm-plugins/data/word_index_10k.json +7760 -3
  51. package/src/pm-plugins/ghost-text-decoration.ts +44 -0
  52. package/src/pm-plugins/scoring-pipeline.ts +294 -0
  53. package/src/pm-plugins/slow-lane-client/package.json +15 -0
  54. package/src/pm-plugins/slow-lane-client.ts +222 -0
  55. package/src/pm-plugins/text-predictor/package.json +15 -0
  56. package/src/pm-plugins/text-predictor.ts +780 -0
  57. package/tsconfig.app.json +12 -3
  58. package/tsconfig.json +4 -1
@@ -0,0 +1,793 @@
1
+ import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
2
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
+ import _createClass from "@babel/runtime/helpers/createClass";
4
+ import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
5
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
6
+ import _regeneratorRuntime from "@babel/runtime/regenerator";
7
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
8
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
9
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
10
+ /**
11
+ * Fast Lane Predictor: Local autocomplete using weighted trie + frequency + semantic scoring.
12
+ *
13
+ * Two prediction modes:
14
+ * 1. Word boundary → bigram-based next-word suggestion (grammar-filtered)
15
+ * 2. Mid-word (≥3 chars) → trie prefix search → scoring pipeline → top result
16
+ *
17
+ * Scoring is delegated to scoring-pipeline.ts which handles:
18
+ * Stage 1 (semantic + frequency), grammar filter, Stage 2 (optional LM re-ranking).
19
+ *
20
+ * Context vector: average of word vectors from text before cursor (last N words).
21
+ * Falls back to cold mode (freq-only) when vectors not yet loaded.
22
+ *
23
+ * Session personalization (L1): words the user types are incrementally boosted
24
+ * via incrementSessionFreq(), called on word boundaries from the plugin.
25
+ */
26
+
27
+ // import bigramsData from './data/bigrams.json';
28
+ import l3VocabularyData from './data/l3_vocabulary.json';
29
+ import vocabularyData from './data/vocabulary_10k.json';
30
+ import wordIndexData from './data/word_index_10k.json';
31
+ // import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
32
+ import { rankCandidates } from './scoring-pipeline';
33
+ import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
34
+
35
+ // ─── Constants ───────────────────────────────────────────────────────────────
36
+
37
+ // eslint-disable-next-line require-unicode-regexp
38
+ var PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/g;
39
+ var MIN_PREFIX_LENGTH = 3;
40
+ var MAX_CANDIDATES = 200;
41
+ var CONTEXT_WORDS = 10;
42
+ var MIN_SCORE_THRESHOLD = 0.2;
43
+ var L3_BASELINE_FREQ = 0.001;
44
+
45
+ // ─── Types ───────────────────────────────────────────────────────────────────
46
+ var TrieNode = /*#__PURE__*/_createClass(function TrieNode() {
47
+ _classCallCheck(this, TrieNode);
48
+ _defineProperty(this, "children", new Map());
49
+ _defineProperty(this, "word", null);
50
+ _defineProperty(this, "tenantFreq", 0);
51
+ _defineProperty(this, "docFreq", 0);
52
+ _defineProperty(this, "authorFreq", 0);
53
+ _defineProperty(this, "sessionFreq", 0);
54
+ });
55
+ var WeightedWordTrie = /*#__PURE__*/function () {
56
+ function WeightedWordTrie() {
57
+ _classCallCheck(this, WeightedWordTrie);
58
+ _defineProperty(this, "root", new TrieNode());
59
+ /** Highest tenantFreq seen — used to normalize freq scores at query time */
60
+ _defineProperty(this, "maxTenantFreq", 1);
61
+ }
62
+ return _createClass(WeightedWordTrie, [{
63
+ key: "insert",
64
+ value: function insert(word, tenantFreq, docFreq, authorFreq) {
65
+ var node = this.root;
66
+ var _iterator = _createForOfIteratorHelper(word.toLowerCase()),
67
+ _step;
68
+ try {
69
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
70
+ var char = _step.value;
71
+ var next = node.children.get(char);
72
+ if (!next) {
73
+ next = new TrieNode();
74
+ node.children.set(char, next);
75
+ }
76
+ node = next;
77
+ }
78
+ } catch (err) {
79
+ _iterator.e(err);
80
+ } finally {
81
+ _iterator.f();
82
+ }
83
+ node.word = word;
84
+ node.tenantFreq = tenantFreq;
85
+ node.docFreq = docFreq;
86
+ node.authorFreq = authorFreq;
87
+ if (tenantFreq > this.maxTenantFreq) {
88
+ this.maxTenantFreq = tenantFreq;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Return all words matching this prefix, up to maxResults.
94
+ * O(prefix_length + results) — traverses to the prefix node then collects subtree.
95
+ */
96
+ }, {
97
+ key: "getCandidates",
98
+ value: function getCandidates(prefix) {
99
+ var maxResults = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : MAX_CANDIDATES;
100
+ var node = this.root;
101
+ var _iterator2 = _createForOfIteratorHelper(prefix.toLowerCase()),
102
+ _step2;
103
+ try {
104
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
105
+ var char = _step2.value;
106
+ var next = node.children.get(char);
107
+ if (!next) {
108
+ return [];
109
+ }
110
+ node = next;
111
+ }
112
+ } catch (err) {
113
+ _iterator2.e(err);
114
+ } finally {
115
+ _iterator2.f();
116
+ }
117
+ var candidates = [];
118
+ var stack = [node];
119
+ while (stack.length > 0 && candidates.length < maxResults) {
120
+ var current = stack.pop();
121
+ if (!current) {
122
+ continue;
123
+ }
124
+
125
+ // Only add candidates that are longer than the prefix
126
+ if (current.word && current.word.length > prefix.length) {
127
+ candidates.push({
128
+ word: current.word,
129
+ node: current
130
+ });
131
+ }
132
+ var _iterator3 = _createForOfIteratorHelper(current.children.values()),
133
+ _step3;
134
+ try {
135
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
136
+ var child = _step3.value;
137
+ stack.push(child);
138
+ }
139
+ } catch (err) {
140
+ _iterator3.e(err);
141
+ } finally {
142
+ _iterator3.f();
143
+ }
144
+ }
145
+ return candidates;
146
+ }
147
+ }, {
148
+ key: "findNode",
149
+ value: function findNode(word) {
150
+ var node = this.root;
151
+ var _iterator4 = _createForOfIteratorHelper(word.toLowerCase()),
152
+ _step4;
153
+ try {
154
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
155
+ var char = _step4.value;
156
+ var next = node.children.get(char);
157
+ if (!next) {
158
+ return null;
159
+ }
160
+ node = next;
161
+ }
162
+ } catch (err) {
163
+ _iterator4.e(err);
164
+ } finally {
165
+ _iterator4.f();
166
+ }
167
+ return node.word !== null ? node : null;
168
+ }
169
+
170
+ /**
171
+ * Set the session frequency for a word.
172
+ * Returns true if the word exists in the trie.
173
+ */
174
+ }, {
175
+ key: "updateSessionFreq",
176
+ value: function updateSessionFreq(word, count) {
177
+ var node = this.findNode(word);
178
+ if (!node) {
179
+ return false;
180
+ }
181
+ node.sessionFreq = count;
182
+ return true;
183
+ }
184
+
185
+ /**
186
+ * Increment the session frequency for a word by 1.
187
+ * Returns true if the word exists in the trie.
188
+ */
189
+ }, {
190
+ key: "incrementSessionFreq",
191
+ value: function incrementSessionFreq(word) {
192
+ var node = this.findNode(word);
193
+ if (!node) {
194
+ return false;
195
+ }
196
+ node.sessionFreq += 1;
197
+ return true;
198
+ }
199
+ }]);
200
+ }(); // L1/L2 Trie (Session + Atlassian Domain)
201
+ var wordTrie = new WeightedWordTrie();
202
+
203
+ // L3 Trie (General English Fallback)
204
+ var l3Trie = new WeightedWordTrie();
205
+
206
+ // --- Initialization Function ---
207
+ /**
208
+ * Loads the General English vocabulary.
209
+ * expects a simple array of strings: ["about", "above", "actually", ...]
210
+ */
211
+ export var initL3Vocabulary = function initL3Vocabulary(l3Words) {
212
+ var _iterator5 = _createForOfIteratorHelper(l3Words),
213
+ _step5;
214
+ try {
215
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
216
+ var word = _step5.value;
217
+ // Insert with a tiny baseline frequency so it mathematically
218
+ // loses to any domain word in Stage 1, but still scores above 0.
219
+ l3Trie.insert(word, L3_BASELINE_FREQ, 0, 0);
220
+ }
221
+ } catch (err) {
222
+ _iterator5.e(err);
223
+ } finally {
224
+ _iterator5.f();
225
+ }
226
+ if (debugMode) {
227
+ // eslint-disable-next-line no-console
228
+ console.log("[text-predictor] L3 General English loaded: ".concat(l3Words.length, " words"));
229
+ }
230
+ };
231
+
232
+ // const bigramMap: Map<string, Record<string, number>> = new Map(
233
+ // Object.entries(bigramsData as Record<string, Record<string, number>>),
234
+ // );
235
+
236
+ var isInitialized = false;
237
+ var vectorStore = null;
238
+ var vectorsLoadStarted = false;
239
+ var debugMode = true;
240
+ var lastPredictionDebug = null;
241
+ var hasLoggedSemanticActive = false;
242
+
243
+ /** Get vector for a word from the store. */
244
+ var getWordVector = function getWordVector(word) {
245
+ if (!vectorStore) {
246
+ return null;
247
+ }
248
+ var idx = vectorStore.wordIndex[word.toLowerCase()];
249
+ if (idx === undefined) {
250
+ return null;
251
+ }
252
+ var start = idx * vectorStore.dim;
253
+ return vectorStore.float32.subarray(start, start + vectorStore.dim);
254
+ };
255
+
256
+ /**
257
+ * Compute context vector by averaging vectors of last N words in text.
258
+ * Falls back to null (cold mode) if no words have vectors.
259
+ */
260
+ var computeContextVectorLocal = function computeContextVectorLocal(textBefore) {
261
+ if (!vectorStore) {
262
+ return null;
263
+ }
264
+ var tokens = tokenize(textBefore);
265
+ var words = tokens.slice(-CONTEXT_WORDS);
266
+ var vectors = [];
267
+ var _iterator6 = _createForOfIteratorHelper(words),
268
+ _step6;
269
+ try {
270
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
271
+ var word = _step6.value;
272
+ var _v = getWordVector(word);
273
+ if (_v) {
274
+ vectors.push(_v);
275
+ }
276
+ }
277
+ } catch (err) {
278
+ _iterator6.e(err);
279
+ } finally {
280
+ _iterator6.f();
281
+ }
282
+ if (vectors.length === 0) {
283
+ return null;
284
+ }
285
+ var dim = vectorStore.dim;
286
+ var avg = new Float32Array(dim);
287
+ for (var _i = 0, _vectors = vectors; _i < _vectors.length; _i++) {
288
+ var v = _vectors[_i];
289
+ for (var i = 0; i < dim; i++) {
290
+ avg[i] += v[i];
291
+ }
292
+ }
293
+ for (var _i2 = 0; _i2 < dim; _i2++) {
294
+ avg[_i2] /= vectors.length;
295
+ }
296
+ return avg;
297
+ };
298
+
299
+ /**
300
+ * Get context vector for scoring. Prefers Slow Lane (BE) context when available
301
+ * and dimension matches; otherwise falls back to local averaging.
302
+ */
303
+ var getContextVectorForScoring = function getContextVectorForScoring(textBefore) {
304
+ var slowLaneVector = getStoredContextVector();
305
+ if (slowLaneVector && vectorStore && slowLaneVector.length === vectorStore.dim) {
306
+ return slowLaneVector;
307
+ }
308
+ return computeContextVectorLocal(textBefore);
309
+ };
310
+ var tokenize = function tokenize(text) {
311
+ var tokens = [];
312
+ // eslint-disable-next-line require-unicode-regexp, @atlassian/perf-linting/no-expensive-split-replace
313
+ var _iterator7 = _createForOfIteratorHelper(text.toLowerCase().split(/\s+/)),
314
+ _step7;
315
+ try {
316
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
317
+ var raw = _step7.value;
318
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
319
+ var clean = raw.replace(PUNCTUATION_BOUNDARY_REGEX, '');
320
+ if (clean.length >= 2) {
321
+ tokens.push(clean);
322
+ }
323
+ }
324
+ } catch (err) {
325
+ _iterator7.e(err);
326
+ } finally {
327
+ _iterator7.f();
328
+ }
329
+ return tokens;
330
+ };
331
+ var extractPreviousWord = function extractPreviousWord(text) {
332
+ // 1. Split the text by newlines or punctuation (. ? !)
333
+ // eslint-disable-next-line require-unicode-regexp
334
+ var sentences = text.split(/[\n.?!]+/);
335
+
336
+ // 2. Only look at the current sentence/line the user is typing in
337
+ var currentSentence = sentences[sentences.length - 1];
338
+
339
+ // 3. Extract the previous word as normal
340
+ // eslint-disable-next-line require-unicode-regexp
341
+ var words = currentSentence.trimEnd().split(/\s+/);
342
+ return words.length >= 2 ? words[words.length - 2] : '';
343
+ };
344
+
345
+ // ─── Debug Helpers ───────────────────────────────────────────────────────────
346
+
347
+ /** Enable or disable debug logging. Also checks localStorage key `autocomplete-debug`. */
348
+ export var setDebugMode = function setDebugMode(enabled) {
349
+ debugMode = enabled;
350
+ };
351
+
352
+ /** Check localStorage for autocomplete-debug on first access. */
353
+ var ensureDebugModeFromStorage = function ensureDebugModeFromStorage() {
354
+ if (typeof localStorage !== 'undefined' && localStorage.getItem('autocomplete-debug') === '1') {
355
+ debugMode = true;
356
+ }
357
+ };
358
+
359
+ /**
360
+ * Get predictor status for debugging.
361
+ * vectorsLoaded: true when semantic scoring is active
362
+ * wordCount: number of words in vector store (0 if not loaded)
363
+ */
364
+ export var getPredictorStatus = function getPredictorStatus() {
365
+ ensureDebugModeFromStorage();
366
+ return {
367
+ vectorsLoaded: vectorStore !== null,
368
+ wordCount: vectorStore ? Object.keys(vectorStore.wordIndex).length : 0,
369
+ vectorsLoadStarted: vectorsLoadStarted,
370
+ isInitialized: isInitialized
371
+ };
372
+ };
373
+
374
+ /**
375
+ * Get details of the last prediction (for debugging).
376
+ * Returns null if no prediction has run yet or debug was off.
377
+ */
378
+ export var getLastPredictionDebug = function getLastPredictionDebug() {
379
+ ensureDebugModeFromStorage();
380
+ return lastPredictionDebug;
381
+ };
382
+ export var initVocabulary = function initVocabulary(vocabulary) {
383
+ var _iterator8 = _createForOfIteratorHelper(vocabulary.terms),
384
+ _step8;
385
+ try {
386
+ for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
387
+ var term = _step8.value;
388
+ wordTrie.insert(term.word, term.freq, term.docFreq, term.authorFreq);
389
+ }
390
+ } catch (err) {
391
+ _iterator8.e(err);
392
+ } finally {
393
+ _iterator8.f();
394
+ }
395
+ isInitialized = true;
396
+ };
397
+
398
+ /**
399
+ * Increment L1 session frequency for a single word.
400
+ * Called from the plugin on word boundaries for efficient incremental boosting.
401
+ */
402
+ export var incrementSessionFreq = function incrementSessionFreq(word) {
403
+ wordTrie.incrementSessionFreq(word);
404
+ };
405
+
406
+ /**
407
+ * Prime session frequencies from a document page string.
408
+ *
409
+ * Iterates through every token in `pageContent` and increments its session
410
+ * frequency so that words already present on the page receive an L1 boost
411
+ * before the user starts typing.
412
+ *
413
+ * Pass `undefined` (or omit the argument) to skip priming — useful when the
414
+ * calling context does not yet have a page value available.
415
+ */
416
+ // NOTE: We ingest full page context here
417
+ export var ingestDocumentPage = function ingestDocumentPage(pageContent) {
418
+ if (!pageContent) {
419
+ return;
420
+ }
421
+ var words = tokenize(pageContent);
422
+ var validBoostedWords = new Set();
423
+ var _iterator9 = _createForOfIteratorHelper(words),
424
+ _step9;
425
+ try {
426
+ for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
427
+ var word = _step9.value;
428
+ var didBoost = wordTrie.incrementSessionFreq(word);
429
+ if (didBoost) {
430
+ validBoostedWords.add(word);
431
+ }
432
+ }
433
+ } catch (err) {
434
+ _iterator9.e(err);
435
+ } finally {
436
+ _iterator9.f();
437
+ }
438
+ if (debugMode && validBoostedWords.size > 0) {
439
+ // eslint-disable-next-line no-console
440
+ console.groupCollapsed("%c[L1 Session] %cPrimed ".concat(validBoostedWords.size, " valid dictionary words from page"), 'color: #00b8d9; font-weight: bold;', 'color: inherit; font-style: italic;');
441
+ // eslint-disable-next-line no-console
442
+ console.dir(Array.from(validBoostedWords).sort());
443
+ // eslint-disable-next-line no-console
444
+ console.groupEnd();
445
+ }
446
+ };
447
+ export var predict = function predict(textBefore) {
448
+ ensureDebugModeFromStorage();
449
+ if (!isInitialized) {
450
+ loadDefaultVocabulary();
451
+ }
452
+ var t0 = performance.now();
453
+
454
+ // ── Step 1: Bigram-based next-word suggestion at word boundary ───────────
455
+ // if (textBefore.length > 0 && /\s$/u.test(textBefore)) {
456
+ // const words = textBefore.toLowerCase().trimEnd().split(/\s+/u);
457
+ // const prevWord = words[words.length - 1];
458
+ // const nextWords = bigramMap.get(prevWord);
459
+ // if (nextWords) {
460
+ // const sorted = Object.entries(nextWords).sort((a, b) => b[1] - a[1]);
461
+ // let bestWord = '';
462
+ // for (const [word] of sorted) {
463
+ // if (!isGrammarAllowed(prevWord, word)) {
464
+ // continue;
465
+ // }
466
+ // bestWord = word;
467
+ // break;
468
+ // }
469
+ // if (bestWord) {
470
+ // if (debugMode) {
471
+ // const latencyMs = performance.now() - t0;
472
+ // console.log(
473
+ // '%c[autocomplete] BIGRAM',
474
+ // 'color:cyan',
475
+ // '| "' + prevWord + '" -> "' + bestWord + '" | ' + latencyMs.toFixed(1) + 'ms',
476
+ // );
477
+ // }
478
+ // return bestWord;
479
+ // }
480
+ // }
481
+ // if (debugMode) {
482
+ // console.log(
483
+ // '%c[autocomplete] BIGRAM-MISS',
484
+ // 'color:gray',
485
+ // '| no bigram for "' + words[words.length - 1] + '", skipping prefix completion',
486
+ // );
487
+ // }
488
+ // return null;
489
+ // }
490
+
491
+ // ── Step 2: Prefix completion (≥3 chars typed) ──────────────────────────
492
+ // eslint-disable-next-line require-unicode-regexp
493
+ if (textBefore.length > 0 && /\s$/.test(textBefore)) {
494
+ return null;
495
+ }
496
+ var trimmed = textBefore.trimEnd();
497
+ var lastSpaceIdx = trimmed.lastIndexOf(' ');
498
+ var currentWord = lastSpaceIdx === -1 ? trimmed : trimmed.slice(lastSpaceIdx + 1);
499
+ if (currentWord.length < MIN_PREFIX_LENGTH) {
500
+ return null;
501
+ }
502
+
503
+ // 1. Primary Query: Ask the L2 Domain Trie
504
+ var candidates = wordTrie.getCandidates(currentWord, MAX_CANDIDATES);
505
+
506
+ // 2. Fallback Query: Gap-fill with the L3 General English Trie
507
+ if (candidates.length < MAX_CANDIDATES) {
508
+ // Ask L3 for MAX_CANDIDATES to guarantee we have enough buffer
509
+ // to survive the deduplication process.
510
+ var l3Candidates = l3Trie.getCandidates(currentWord, MAX_CANDIDATES);
511
+ var existingWords = new Set(candidates.map(function (c) {
512
+ return c.word;
513
+ }));
514
+ var _iterator0 = _createForOfIteratorHelper(l3Candidates),
515
+ _step0;
516
+ try {
517
+ for (_iterator0.s(); !(_step0 = _iterator0.n()).done;) {
518
+ var l3c = _step0.value;
519
+ if (candidates.length >= MAX_CANDIDATES) break; // Stop exactly at the limit
520
+
521
+ if (!existingWords.has(l3c.word)) {
522
+ candidates.push(l3c);
523
+ }
524
+ }
525
+ } catch (err) {
526
+ _iterator0.e(err);
527
+ } finally {
528
+ _iterator0.f();
529
+ }
530
+ }
531
+
532
+ // If both Tries are completely empty for this prefix
533
+ if (candidates.length === 0) {
534
+ return null;
535
+ }
536
+ var previousWord = extractPreviousWord(trimmed);
537
+ var contextVector = getContextVectorForScoring(trimmed);
538
+ var lmLogits = getStoredLmLogits();
539
+
540
+ // Raw LM Output Logger
541
+ if (debugMode && lmLogits && Object.keys(lmLogits).length > 0) {
542
+ var rawLmTop = Object.entries(lmLogits).sort(function (a, b) {
543
+ return b[1] - a[1];
544
+ }).slice(0, 5).map(function (_ref) {
545
+ var _ref2 = _slicedToArray(_ref, 2),
546
+ word = _ref2[0],
547
+ score = _ref2[1];
548
+ return {
549
+ Word: word,
550
+ Prob: Number(score.toFixed(5))
551
+ };
552
+ });
553
+ // eslint-disable-next-line no-console
554
+ console.log('%c[Raw LM Prediction]🧠', 'color: #e83e8c; font-weight: bold;', rawLmTop);
555
+ }
556
+ var mode = contextVector ? 'warm' : 'cold';
557
+ if (debugMode && contextVector && !hasLoggedSemanticActive) {
558
+ hasLoggedSemanticActive = true;
559
+ }
560
+
561
+ // Build ScoringCandidate array from TrieNodes
562
+ var scoringCandidates = candidates.map(function (_ref3) {
563
+ var word = _ref3.word,
564
+ node = _ref3.node;
565
+ return {
566
+ word: word,
567
+ tenantFreq: node.tenantFreq,
568
+ docFreq: node.docFreq,
569
+ authorFreq: node.authorFreq,
570
+ sessionFreq: node.sessionFreq
571
+ };
572
+ });
573
+ var _rankCandidates = rankCandidates(scoringCandidates, contextVector, function (w) {
574
+ return getWordVector(w);
575
+ }, lmLogits, wordTrie.maxTenantFreq, previousWord),
576
+ ranked = _rankCandidates.candidates,
577
+ grammarMeta = _rankCandidates.grammarMeta;
578
+ var best = ranked[0];
579
+ var suggestion = best && best.finalScore >= MIN_SCORE_THRESHOLD ? best.word.slice(currentWord.length) : null;
580
+ if (debugMode) {
581
+ var latencyMs = performance.now() - t0;
582
+ var tokens = tokenize(trimmed);
583
+ var contextWords = tokens.slice(-CONTEXT_WORDS);
584
+ var belowThreshold = best && best.finalScore < MIN_SCORE_THRESHOLD;
585
+ var suggestionLabel = belowThreshold ? '🚫 (below threshold)' : suggestion && suggestion.length > 0 ? "\u2728 \"".concat(suggestion, "\"") : '🚫 (no match)';
586
+ lastPredictionDebug = {
587
+ textBefore: trimmed,
588
+ currentWord: currentWord,
589
+ mode: mode,
590
+ contextWords: contextWords,
591
+ topCandidates: ranked.slice(0, 5).map(function (r) {
592
+ return {
593
+ word: r.word,
594
+ finalScore: r.finalScore,
595
+ semanticScore: r.semanticScore,
596
+ freqScore: r.freqScore,
597
+ lmScore: r.lmScore
598
+ };
599
+ }),
600
+ suggestion: suggestion && suggestion.length > 0 ? suggestion : null
601
+ };
602
+
603
+ // ── Mode label: COLD / WARM(local) / WARM(BE) ───────────────────────
604
+ var slowLaneVec = getStoredContextVector();
605
+ var isUsingSlowLaneVector = slowLaneVec !== null && vectorStore !== null && slowLaneVec.length === vectorStore.dim;
606
+ var modeLabel = !contextVector ? 'COLD' : isUsingSlowLaneVector ? 'WARM(BE)' : 'WARM(local)';
607
+ var modeColor = modeLabel === 'WARM(BE)' ? 'color: #ff9800; font-weight: bold;' : modeLabel === 'WARM(local)' ? 'color: #4caf50; font-weight: bold;' : 'color: #9e9e9e; font-weight: bold;';
608
+
609
+ // 1. Collapsible group header
610
+ // eslint-disable-next-line no-console
611
+ console.groupCollapsed("%c[Autocomplete] %c".concat(modeLabel, " %c| \"").concat(currentWord, "\" \u2794 ").concat(suggestionLabel, " | \u23F1 ").concat(latencyMs.toFixed(1), "ms"), 'color: #00b8d9; font-weight: bold;', modeColor, 'color: inherit; font-weight: normal;');
612
+
613
+ // 2. Context window (what local vector averaging sees)
614
+ // eslint-disable-next-line no-console
615
+ console.log('%cContext Window:', 'color: #888; font-style: italic;', contextWords.length ? contextWords.join(' ') : '(none)');
616
+
617
+ // 3. Slow-lane status
618
+ var vectorStatus = isUsingSlowLaneVector ? "\u2705 BE semantic vector (dim=".concat(slowLaneVec === null || slowLaneVec === void 0 ? void 0 : slowLaneVec.length, ")") : vectorStore ? '⚠️ Local vector average (slow-lane not yet returned)' : '❌ No vectors (cold)';
619
+ var logitsStatus = lmLogits && Object.keys(lmLogits).length > 0 ? "\u2705 LM logits active (".concat(Object.keys(lmLogits).length, " tokens)") : '⏳ No LM logits (slow-lane pending or failed)';
620
+ // eslint-disable-next-line no-console
621
+ console.log('%cSlow Lane:', 'color: #888; font-style: italic;', vectorStatus, '|', logitsStatus);
622
+
623
+ // 4. Scoring formula active this prediction
624
+ var formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? 'Stage1(×0.6) + LM(×0.4)' : 'Stage1 only (no LM logits)';
625
+ // eslint-disable-next-line no-console
626
+ console.log('%cFormula:', 'color: #888; font-style: italic;', formulaLabel);
627
+
628
+ // 5. Grammar filter result (collected inside rankCandidates, logged here)
629
+ if (grammarMeta) {
630
+ // eslint-disable-next-line no-console
631
+ console.log("%c[Grammar] \"".concat(grammarMeta.prevWord, "\" [").concat(grammarMeta.prevTags.join('|'), "] \u2192 ").concat(grammarMeta.before, " candidates \u2192 ").concat(grammarMeta.after, " after filter"), 'color: #4caf50; font-weight: bold;');
632
+ if (grammarMeta.dropped.length > 0) {
633
+ // eslint-disable-next-line no-console
634
+ console.log("%c\uD83D\uDEAB Dropped: ".concat(grammarMeta.dropped.join(', ')), 'color: #f44336; font-style: italic;');
635
+ }
636
+ }
637
+
638
+ // 6. Candidate table
639
+ if (ranked.length > 0) {
640
+ var lmCoverage = ranked.slice(0, 10).filter(function (r) {
641
+ return r.lmScore > 0.05;
642
+ }).length;
643
+ // eslint-disable-next-line no-console
644
+ console.log("%cLM coverage: ".concat(lmCoverage, "/").concat(Math.min(ranked.length, 10), " candidates had real logit scores"), 'color: #888; font-style: italic;');
645
+ var tableData = ranked.slice(0, 10).map(function (r) {
646
+ var rawLogit = 'Not in Payload';
647
+ if (lmLogits) {
648
+ var val = lmLogits[r.word.toLowerCase()];
649
+ if (val !== undefined) {
650
+ rawLogit = Number(val.toFixed(5));
651
+ }
652
+ }
653
+ var original = scoringCandidates.find(function (sc) {
654
+ return sc.word === r.word;
655
+ });
656
+ var source = 'Unknown';
657
+ if (original) {
658
+ if (original.docFreq === 0 && original.tenantFreq === L3_BASELINE_FREQ) {
659
+ source = '🌍 L3 (Generic)';
660
+ } else if (original.sessionFreq > 0 && original.tenantFreq === 0) {
661
+ source = '👤 L1 (Session Only)';
662
+ } else {
663
+ source = '🏢 L2 (Domain)';
664
+ }
665
+ }
666
+ return {
667
+ Candidate: r.word,
668
+ Source: source,
669
+ 'Final Score': Number(r.finalScore.toFixed(4)),
670
+ Semantics: Number(r.semanticScore.toFixed(4)),
671
+ Freq: Number(r.freqScore.toFixed(4)),
672
+ 'LM Score': Number(r.lmScore.toFixed(4)),
673
+ 'Raw Logit': rawLogit,
674
+ 'Session Freq': (original === null || original === void 0 ? void 0 : original.sessionFreq) || 0
675
+ };
676
+ });
677
+
678
+ // eslint-disable-next-line no-console
679
+ console.table(tableData);
680
+ } else {
681
+ // eslint-disable-next-line no-console
682
+ console.log('No candidates found.');
683
+ }
684
+
685
+ // eslint-disable-next-line no-console
686
+ console.groupEnd();
687
+ }
688
+ return suggestion && suggestion.length > 0 ? suggestion : null;
689
+ };
690
+
691
+ // ─── Data Loading ────────────────────────────────────────────────────────────
692
+
693
+ export var loadVectorsAsync = /*#__PURE__*/function () {
694
+ var _ref4 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(options) {
695
+ var url, res, buffer, float32, wordIndex, nWords, dim;
696
+ return _regeneratorRuntime.wrap(function _callee$(_context) {
697
+ while (1) switch (_context.prev = _context.next) {
698
+ case 0:
699
+ if (!(vectorStore || vectorsLoadStarted)) {
700
+ _context.next = 2;
701
+ break;
702
+ }
703
+ return _context.abrupt("return");
704
+ case 2:
705
+ if (options !== null && options !== void 0 && options.vectorsUrl) {
706
+ _context.next = 5;
707
+ break;
708
+ }
709
+ // eslint-disable-next-line no-console
710
+ console.warn('[text-predictor] loadVectorsAsync called without a vectorsUrl — vectors will not load. Pass vectorsUrl via plugin options.');
711
+ return _context.abrupt("return");
712
+ case 5:
713
+ vectorsLoadStarted = true;
714
+ url = options.vectorsUrl;
715
+ _context.prev = 7;
716
+ _context.next = 10;
717
+ return fetch(url);
718
+ case 10:
719
+ res = _context.sent;
720
+ if (res.ok) {
721
+ _context.next = 15;
722
+ break;
723
+ }
724
+ vectorsLoadStarted = false;
725
+ // eslint-disable-next-line no-console
726
+ console.warn("[text-predictor] Failed to load vectors: ".concat(res.status));
727
+ return _context.abrupt("return");
728
+ case 15:
729
+ _context.next = 17;
730
+ return res.arrayBuffer();
731
+ case 17:
732
+ buffer = _context.sent;
733
+ float32 = new Float32Array(buffer);
734
+ wordIndex = wordIndexData;
735
+ nWords = Object.keys(wordIndex).length;
736
+ dim = float32.length / nWords;
737
+ vectorStore = {
738
+ float32: float32,
739
+ wordIndex: wordIndex,
740
+ dim: dim
741
+ };
742
+ ensureDebugModeFromStorage();
743
+ if (debugMode) {
744
+ // eslint-disable-next-line no-console
745
+ console.log('[text-predictor] Vectors loaded:', {
746
+ wordCount: nWords,
747
+ dim: dim,
748
+ sizeBytes: float32.byteLength
749
+ });
750
+ }
751
+ _context.next = 31;
752
+ break;
753
+ case 27:
754
+ _context.prev = 27;
755
+ _context.t0 = _context["catch"](7);
756
+ vectorsLoadStarted = false;
757
+ // eslint-disable-next-line no-console
758
+ console.warn('[text-predictor] Failed to load vectors:', _context.t0);
759
+ case 31:
760
+ case "end":
761
+ return _context.stop();
762
+ }
763
+ }, _callee, null, [[7, 27]]);
764
+ }));
765
+ return function loadVectorsAsync(_x) {
766
+ return _ref4.apply(this, arguments);
767
+ };
768
+ }();
769
+ export var initVectors = function initVectors(store) {
770
+ vectorStore = store;
771
+ };
772
+ export var loadDefaultVocabulary = function loadDefaultVocabulary() {
773
+ // 1. Load the Atlassian Domain (L2)
774
+ var data = vocabularyData;
775
+ var terms = Object.entries(data.words).map(function (_ref5) {
776
+ var _ref6 = _slicedToArray(_ref5, 2),
777
+ word = _ref6[0],
778
+ stats = _ref6[1];
779
+ return {
780
+ word: word,
781
+ freq: stats.freq,
782
+ docFreq: stats.doc_freq,
783
+ authorFreq: stats.author_freq
784
+ };
785
+ });
786
+ initVocabulary({
787
+ terms: terms
788
+ });
789
+
790
+ // 2. Load General English (L3)
791
+ var l3Words = l3VocabularyData;
792
+ initL3Vocabulary(l3Words);
793
+ };