@atlaskit/editor-plugin-autocomplete 0.1.0 → 0.2.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 (52) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/afm-cc/tsconfig.json +5 -1
  3. package/afm-jira/tsconfig.json +5 -1
  4. package/afm-products/tsconfig.json +5 -1
  5. package/build/tsconfig.json +24 -0
  6. package/dist/cjs/autocompletePlugin.js +18 -5
  7. package/dist/cjs/autocompletePluginType.js +5 -1
  8. package/dist/cjs/pm-plugins/autocomplete-plugin.js +361 -0
  9. package/dist/cjs/pm-plugins/ghost-text-decoration.js +39 -0
  10. package/dist/cjs/pm-plugins/scoring-pipeline.js +258 -0
  11. package/dist/cjs/pm-plugins/slow-lane-client.js +197 -0
  12. package/dist/cjs/pm-plugins/text-predictor.js +786 -0
  13. package/dist/es2019/autocompletePlugin.js +20 -6
  14. package/dist/es2019/autocompletePluginType.js +1 -0
  15. package/dist/es2019/pm-plugins/autocomplete-plugin.js +360 -0
  16. package/dist/es2019/pm-plugins/ghost-text-decoration.js +33 -0
  17. package/dist/es2019/pm-plugins/scoring-pipeline.js +224 -0
  18. package/dist/es2019/pm-plugins/slow-lane-client.js +154 -0
  19. package/dist/es2019/pm-plugins/text-predictor.js +624 -0
  20. package/dist/esm/autocompletePlugin.js +18 -5
  21. package/dist/esm/autocompletePluginType.js +1 -0
  22. package/dist/esm/pm-plugins/autocomplete-plugin.js +354 -0
  23. package/dist/esm/pm-plugins/ghost-text-decoration.js +33 -0
  24. package/dist/esm/pm-plugins/scoring-pipeline.js +255 -0
  25. package/dist/esm/pm-plugins/slow-lane-client.js +190 -0
  26. package/dist/esm/pm-plugins/text-predictor.js +783 -0
  27. package/dist/types/autocompletePluginType.d.ts +6 -3
  28. package/dist/types/pm-plugins/autocomplete-plugin.d.ts +36 -0
  29. package/dist/types/pm-plugins/ghost-text-decoration.d.ts +7 -0
  30. package/dist/types/pm-plugins/scoring-pipeline.d.ts +33 -0
  31. package/dist/types/pm-plugins/slow-lane-client.d.ts +46 -0
  32. package/dist/types/pm-plugins/text-predictor.d.ts +90 -0
  33. package/dist/types-ts4.5/autocompletePluginType.d.ts +6 -3
  34. package/dist/types-ts4.5/pm-plugins/autocomplete-plugin.d.ts +36 -0
  35. package/dist/types-ts4.5/pm-plugins/ghost-text-decoration.d.ts +7 -0
  36. package/dist/types-ts4.5/pm-plugins/scoring-pipeline.d.ts +33 -0
  37. package/dist/types-ts4.5/pm-plugins/slow-lane-client.d.ts +46 -0
  38. package/dist/types-ts4.5/pm-plugins/text-predictor.d.ts +90 -0
  39. package/package.json +2 -2
  40. package/src/autocompletePlugin.tsx +25 -5
  41. package/src/autocompletePluginType.ts +14 -3
  42. package/src/pm-plugins/autocomplete-plugin/package.json +15 -0
  43. package/src/pm-plugins/autocomplete-plugin.ts +429 -0
  44. package/src/pm-plugins/ghost-text-decoration.ts +44 -0
  45. package/src/pm-plugins/scoring-pipeline.ts +297 -0
  46. package/src/pm-plugins/slow-lane-client/package.json +15 -0
  47. package/src/pm-plugins/slow-lane-client.ts +220 -0
  48. package/src/pm-plugins/text-predictor/package.json +15 -0
  49. package/src/pm-plugins/text-predictor.ts +771 -0
  50. package/src/pm-plugins/typings.d.ts +12 -0
  51. package/tsconfig.app.json +10 -2
  52. package/tsconfig.json +3 -1
@@ -0,0 +1,786 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.setDebugMode = exports.predict = exports.loadVectorsAsync = exports.loadDefaultVocabulary = exports.initVocabulary = exports.initVectors = exports.initL3Vocabulary = exports.ingestDocumentPage = exports.incrementSessionFreq = exports.getPredictorStatus = exports.getLastPredictionDebug = void 0;
8
+ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
9
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
10
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
11
+ var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
12
+ var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
13
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
14
+ var _l3_vocabulary = _interopRequireDefault(require("./data/l3_vocabulary.json"));
15
+ var _vocabulary_10k = _interopRequireDefault(require("./data/vocabulary_10k.json"));
16
+ var _word_index_10k = _interopRequireDefault(require("./data/word_index_10k.json"));
17
+ var _scoringPipeline = require("./scoring-pipeline");
18
+ var _slowLaneClient = require("./slow-lane-client");
19
+ 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; } } }; }
20
+ 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; } }
21
+ 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; } /**
22
+ * Fast Lane Predictor: Local autocomplete using weighted trie + frequency + semantic scoring.
23
+ *
24
+ * Two prediction modes:
25
+ * 1. Word boundary → bigram-based next-word suggestion (grammar-filtered)
26
+ * 2. Mid-word (≥3 chars) → trie prefix search → scoring pipeline → top result
27
+ *
28
+ * Scoring is delegated to scoring-pipeline.ts which handles:
29
+ * Stage 1 (semantic + frequency), grammar filter, Stage 2 (optional LM re-ranking).
30
+ *
31
+ * Context vector: average of word vectors from text before cursor (last N words).
32
+ * Falls back to cold mode (freq-only) when vectors not yet loaded.
33
+ *
34
+ * Session personalization (L1): words the user types are incrementally boosted
35
+ * via incrementSessionFreq(), called on word boundaries from the plugin.
36
+ */ // import bigramsData from './data/bigrams.json';
37
+ // import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
38
+ // ─── Constants ───────────────────────────────────────────────────────────────
39
+
40
+ var PUNCTUATION_BOUNDARY_REGEX = /^[!"'-\),\.:;\?\[\]`\{\}]+|[!"'-\),\.:;\?\[\]`\{\}]+$/g;
41
+ var MIN_PREFIX_LENGTH = 3;
42
+ var MAX_CANDIDATES = 200;
43
+ var CONTEXT_WORDS = 10;
44
+ var MIN_SCORE_THRESHOLD = 0.2;
45
+ var L3_BASELINE_FREQ = 0.001;
46
+
47
+ // ─── Types ───────────────────────────────────────────────────────────────────
48
+ var TrieNode = /*#__PURE__*/(0, _createClass2.default)(function TrieNode() {
49
+ (0, _classCallCheck2.default)(this, TrieNode);
50
+ (0, _defineProperty2.default)(this, "children", new Map());
51
+ (0, _defineProperty2.default)(this, "word", null);
52
+ (0, _defineProperty2.default)(this, "tenantFreq", 0);
53
+ (0, _defineProperty2.default)(this, "docFreq", 0);
54
+ (0, _defineProperty2.default)(this, "authorFreq", 0);
55
+ (0, _defineProperty2.default)(this, "sessionFreq", 0);
56
+ });
57
+ var WeightedWordTrie = /*#__PURE__*/function () {
58
+ function WeightedWordTrie() {
59
+ (0, _classCallCheck2.default)(this, WeightedWordTrie);
60
+ (0, _defineProperty2.default)(this, "root", new TrieNode());
61
+ /** Highest tenantFreq seen — used to normalize freq scores at query time */
62
+ (0, _defineProperty2.default)(this, "maxTenantFreq", 1);
63
+ }
64
+ return (0, _createClass2.default)(WeightedWordTrie, [{
65
+ key: "insert",
66
+ value: function insert(word, tenantFreq, docFreq, authorFreq) {
67
+ var node = this.root;
68
+ var _iterator = _createForOfIteratorHelper(word.toLowerCase()),
69
+ _step;
70
+ try {
71
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
72
+ var char = _step.value;
73
+ var next = node.children.get(char);
74
+ if (!next) {
75
+ next = new TrieNode();
76
+ node.children.set(char, next);
77
+ }
78
+ node = next;
79
+ }
80
+ } catch (err) {
81
+ _iterator.e(err);
82
+ } finally {
83
+ _iterator.f();
84
+ }
85
+ node.word = word;
86
+ node.tenantFreq = tenantFreq;
87
+ node.docFreq = docFreq;
88
+ node.authorFreq = authorFreq;
89
+ if (tenantFreq > this.maxTenantFreq) {
90
+ this.maxTenantFreq = tenantFreq;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Return all words matching this prefix, up to maxResults.
96
+ * O(prefix_length + results) — traverses to the prefix node then collects subtree.
97
+ */
98
+ }, {
99
+ key: "getCandidates",
100
+ value: function getCandidates(prefix) {
101
+ var maxResults = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : MAX_CANDIDATES;
102
+ var node = this.root;
103
+ var _iterator2 = _createForOfIteratorHelper(prefix.toLowerCase()),
104
+ _step2;
105
+ try {
106
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
107
+ var char = _step2.value;
108
+ var next = node.children.get(char);
109
+ if (!next) {
110
+ return [];
111
+ }
112
+ node = next;
113
+ }
114
+ } catch (err) {
115
+ _iterator2.e(err);
116
+ } finally {
117
+ _iterator2.f();
118
+ }
119
+ var candidates = [];
120
+ var stack = [node];
121
+ while (stack.length > 0 && candidates.length < maxResults) {
122
+ var current = stack.pop();
123
+ if (!current) {
124
+ continue;
125
+ }
126
+
127
+ // Only add candidates that are longer than the prefix
128
+ if (current.word && current.word.length > prefix.length) {
129
+ candidates.push({
130
+ word: current.word,
131
+ node: current
132
+ });
133
+ }
134
+ var _iterator3 = _createForOfIteratorHelper(current.children.values()),
135
+ _step3;
136
+ try {
137
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
138
+ var child = _step3.value;
139
+ stack.push(child);
140
+ }
141
+ } catch (err) {
142
+ _iterator3.e(err);
143
+ } finally {
144
+ _iterator3.f();
145
+ }
146
+ }
147
+ return candidates;
148
+ }
149
+ }, {
150
+ key: "findNode",
151
+ value: function findNode(word) {
152
+ var node = this.root;
153
+ var _iterator4 = _createForOfIteratorHelper(word.toLowerCase()),
154
+ _step4;
155
+ try {
156
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
157
+ var char = _step4.value;
158
+ var next = node.children.get(char);
159
+ if (!next) {
160
+ return null;
161
+ }
162
+ node = next;
163
+ }
164
+ } catch (err) {
165
+ _iterator4.e(err);
166
+ } finally {
167
+ _iterator4.f();
168
+ }
169
+ return node.word !== null ? node : null;
170
+ }
171
+
172
+ /**
173
+ * Set the session frequency for a word.
174
+ * Returns true if the word exists in the trie.
175
+ */
176
+ }, {
177
+ key: "updateSessionFreq",
178
+ value: function updateSessionFreq(word, count) {
179
+ var node = this.findNode(word);
180
+ if (!node) {
181
+ return false;
182
+ }
183
+ node.sessionFreq = count;
184
+ return true;
185
+ }
186
+
187
+ /**
188
+ * Increment the session frequency for a word by 1.
189
+ * Returns true if the word exists in the trie.
190
+ */
191
+ }, {
192
+ key: "incrementSessionFreq",
193
+ value: function incrementSessionFreq(word) {
194
+ var node = this.findNode(word);
195
+ if (!node) {
196
+ return false;
197
+ }
198
+ node.sessionFreq += 1;
199
+ return true;
200
+ }
201
+ }]);
202
+ }(); // L1/L2 Trie (Session + Atlassian Domain)
203
+ var wordTrie = new WeightedWordTrie();
204
+
205
+ // L3 Trie (General English Fallback)
206
+ var l3Trie = new WeightedWordTrie();
207
+
208
+ // --- Initialization Function ---
209
+ /**
210
+ * Loads the General English vocabulary.
211
+ * expects a simple array of strings: ["about", "above", "actually", ...]
212
+ */
213
+ var initL3Vocabulary = exports.initL3Vocabulary = function initL3Vocabulary(l3Words) {
214
+ var _iterator5 = _createForOfIteratorHelper(l3Words),
215
+ _step5;
216
+ try {
217
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
218
+ var word = _step5.value;
219
+ // Insert with a tiny baseline frequency so it mathematically
220
+ // loses to any domain word in Stage 1, but still scores above 0.
221
+ l3Trie.insert(word, L3_BASELINE_FREQ, 0, 0);
222
+ }
223
+ } catch (err) {
224
+ _iterator5.e(err);
225
+ } finally {
226
+ _iterator5.f();
227
+ }
228
+ if (debugMode) {
229
+ // eslint-disable-next-line no-console
230
+ console.log("[text-predictor] L3 General English loaded: ".concat(l3Words.length, " words"));
231
+ }
232
+ };
233
+
234
+ // const bigramMap: Map<string, Record<string, number>> = new Map(
235
+ // Object.entries(bigramsData as Record<string, Record<string, number>>),
236
+ // );
237
+
238
+ var isInitialized = false;
239
+ var vectorStore = null;
240
+ var vectorsLoadStarted = false;
241
+ var debugMode = true;
242
+ var lastPredictionDebug = null;
243
+ var hasLoggedSemanticActive = false;
244
+
245
+ /** Get vector for a word from the store. */
246
+ var getWordVector = function getWordVector(word) {
247
+ if (!vectorStore) {
248
+ return null;
249
+ }
250
+ var idx = vectorStore.wordIndex[word.toLowerCase()];
251
+ if (idx === undefined) {
252
+ return null;
253
+ }
254
+ var start = idx * vectorStore.dim;
255
+ return vectorStore.float32.subarray(start, start + vectorStore.dim);
256
+ };
257
+
258
+ /**
259
+ * Compute context vector by averaging vectors of last N words in text.
260
+ * Falls back to null (cold mode) if no words have vectors.
261
+ */
262
+ var computeContextVectorLocal = function computeContextVectorLocal(textBefore) {
263
+ if (!vectorStore) {
264
+ return null;
265
+ }
266
+ var tokens = tokenize(textBefore);
267
+ var words = tokens.slice(-CONTEXT_WORDS);
268
+ var vectors = [];
269
+ var _iterator6 = _createForOfIteratorHelper(words),
270
+ _step6;
271
+ try {
272
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
273
+ var word = _step6.value;
274
+ var _v = getWordVector(word);
275
+ if (_v) {
276
+ vectors.push(_v);
277
+ }
278
+ }
279
+ } catch (err) {
280
+ _iterator6.e(err);
281
+ } finally {
282
+ _iterator6.f();
283
+ }
284
+ if (vectors.length === 0) {
285
+ return null;
286
+ }
287
+ var dim = vectorStore.dim;
288
+ var avg = new Float32Array(dim);
289
+ for (var _i = 0, _vectors = vectors; _i < _vectors.length; _i++) {
290
+ var v = _vectors[_i];
291
+ for (var i = 0; i < dim; i++) {
292
+ avg[i] += v[i];
293
+ }
294
+ }
295
+ for (var _i2 = 0; _i2 < dim; _i2++) {
296
+ avg[_i2] /= vectors.length;
297
+ }
298
+ return avg;
299
+ };
300
+
301
+ /**
302
+ * Get context vector for scoring. Prefers Slow Lane (BE) context when available
303
+ * and dimension matches; otherwise falls back to local averaging.
304
+ */
305
+ var getContextVectorForScoring = function getContextVectorForScoring(textBefore) {
306
+ var slowLaneVector = (0, _slowLaneClient.getStoredContextVector)();
307
+ if (slowLaneVector && vectorStore && slowLaneVector.length === vectorStore.dim) {
308
+ return slowLaneVector;
309
+ }
310
+ return computeContextVectorLocal(textBefore);
311
+ };
312
+ var tokenize = function tokenize(text) {
313
+ var tokens = [];
314
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
315
+ var _iterator7 = _createForOfIteratorHelper(text.toLowerCase().split(/[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]+/)),
316
+ _step7;
317
+ try {
318
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
319
+ var raw = _step7.value;
320
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
321
+ var clean = raw.replace(PUNCTUATION_BOUNDARY_REGEX, '');
322
+ if (clean.length >= 2) {
323
+ tokens.push(clean);
324
+ }
325
+ }
326
+ } catch (err) {
327
+ _iterator7.e(err);
328
+ } finally {
329
+ _iterator7.f();
330
+ }
331
+ return tokens;
332
+ };
333
+ var extractPreviousWord = function extractPreviousWord(text) {
334
+ // 1. Split the text by newlines or punctuation (. ? !)
335
+ var sentences = text.split(/[\n!\.\?]+/);
336
+
337
+ // 2. Only look at the current sentence/line the user is typing in
338
+ var currentSentence = sentences[sentences.length - 1];
339
+
340
+ // 3. Extract the previous word as normal
341
+ var words = currentSentence.trimEnd().split(/[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]+/);
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
+ var setDebugMode = exports.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
+ var getPredictorStatus = exports.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
+ var getLastPredictionDebug = exports.getLastPredictionDebug = function getLastPredictionDebug() {
379
+ ensureDebugModeFromStorage();
380
+ return lastPredictionDebug;
381
+ };
382
+ var initVocabulary = exports.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
+ var incrementSessionFreq = exports.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
+ var ingestDocumentPage = exports.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
+ var predict = exports.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
+ if (textBefore.length > 0 && /[\t-\r \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]$/.test(textBefore)) {
493
+ return null;
494
+ }
495
+ var trimmed = textBefore.trimEnd();
496
+ var lastSpaceIdx = trimmed.lastIndexOf(' ');
497
+ var currentWord = lastSpaceIdx === -1 ? trimmed : trimmed.slice(lastSpaceIdx + 1);
498
+ if (currentWord.length < MIN_PREFIX_LENGTH) {
499
+ return null;
500
+ }
501
+
502
+ // 1. Primary Query: Ask the L2 Domain Trie
503
+ var candidates = wordTrie.getCandidates(currentWord, MAX_CANDIDATES);
504
+
505
+ // 2. Fallback Query: Gap-fill with the L3 General English Trie
506
+ if (candidates.length < MAX_CANDIDATES) {
507
+ // Ask L3 for MAX_CANDIDATES to guarantee we have enough buffer
508
+ // to survive the deduplication process.
509
+ var l3Candidates = l3Trie.getCandidates(currentWord, MAX_CANDIDATES);
510
+ var existingWords = new Set(candidates.map(function (c) {
511
+ return c.word;
512
+ }));
513
+ var _iterator0 = _createForOfIteratorHelper(l3Candidates),
514
+ _step0;
515
+ try {
516
+ for (_iterator0.s(); !(_step0 = _iterator0.n()).done;) {
517
+ var l3c = _step0.value;
518
+ if (candidates.length >= MAX_CANDIDATES) break; // Stop exactly at the limit
519
+
520
+ if (!existingWords.has(l3c.word)) {
521
+ candidates.push(l3c);
522
+ }
523
+ }
524
+ } catch (err) {
525
+ _iterator0.e(err);
526
+ } finally {
527
+ _iterator0.f();
528
+ }
529
+ }
530
+
531
+ // If both Tries are completely empty for this prefix
532
+ if (candidates.length === 0) {
533
+ return null;
534
+ }
535
+ var previousWord = extractPreviousWord(trimmed);
536
+ var contextVector = getContextVectorForScoring(trimmed);
537
+ var lmLogits = (0, _slowLaneClient.getStoredLmLogits)();
538
+
539
+ // Raw LM Output Logger
540
+ if (debugMode && lmLogits && Object.keys(lmLogits).length > 0) {
541
+ var rawLmTop = Object.entries(lmLogits).sort(function (a, b) {
542
+ return b[1] - a[1];
543
+ }).slice(0, 5).map(function (_ref) {
544
+ var _ref2 = (0, _slicedToArray2.default)(_ref, 2),
545
+ word = _ref2[0],
546
+ score = _ref2[1];
547
+ return {
548
+ Word: word,
549
+ Prob: Number(score.toFixed(5))
550
+ };
551
+ });
552
+ // eslint-disable-next-line no-console
553
+ console.log('%c[Raw LM Prediction]🧠', 'color: #e83e8c; font-weight: bold;', rawLmTop);
554
+ }
555
+ var mode = contextVector ? 'warm' : 'cold';
556
+ if (debugMode && contextVector && !hasLoggedSemanticActive) {
557
+ hasLoggedSemanticActive = true;
558
+ }
559
+
560
+ // Build ScoringCandidate array from TrieNodes
561
+ var scoringCandidates = candidates.map(function (_ref3) {
562
+ var word = _ref3.word,
563
+ node = _ref3.node;
564
+ return {
565
+ word: word,
566
+ tenantFreq: node.tenantFreq,
567
+ docFreq: node.docFreq,
568
+ authorFreq: node.authorFreq,
569
+ sessionFreq: node.sessionFreq
570
+ };
571
+ });
572
+ var _rankCandidates = (0, _scoringPipeline.rankCandidates)(scoringCandidates, contextVector, function (w) {
573
+ return getWordVector(w);
574
+ }, lmLogits, wordTrie.maxTenantFreq, previousWord),
575
+ ranked = _rankCandidates.candidates,
576
+ grammarMeta = _rankCandidates.grammarMeta;
577
+ var best = ranked[0];
578
+ var suggestion = best && best.finalScore >= MIN_SCORE_THRESHOLD ? best.word.slice(currentWord.length) : null;
579
+ if (debugMode) {
580
+ var latencyMs = performance.now() - t0;
581
+ var tokens = tokenize(trimmed);
582
+ var contextWords = tokens.slice(-CONTEXT_WORDS);
583
+ var belowThreshold = best && best.finalScore < MIN_SCORE_THRESHOLD;
584
+ var suggestionLabel = belowThreshold ? '🚫 (below threshold)' : suggestion && suggestion.length > 0 ? "\u2728 \"".concat(suggestion, "\"") : '🚫 (no match)';
585
+ lastPredictionDebug = {
586
+ textBefore: trimmed,
587
+ currentWord: currentWord,
588
+ mode: mode,
589
+ contextWords: contextWords,
590
+ topCandidates: ranked.slice(0, 5).map(function (r) {
591
+ return {
592
+ word: r.word,
593
+ finalScore: r.finalScore,
594
+ semanticScore: r.semanticScore,
595
+ freqScore: r.freqScore,
596
+ lmScore: r.lmScore
597
+ };
598
+ }),
599
+ suggestion: suggestion && suggestion.length > 0 ? suggestion : null
600
+ };
601
+
602
+ // ── Mode label: COLD / WARM(local) / WARM(BE) ───────────────────────
603
+ var slowLaneVec = (0, _slowLaneClient.getStoredContextVector)();
604
+ var isUsingSlowLaneVector = slowLaneVec !== null && vectorStore !== null && slowLaneVec.length === vectorStore.dim;
605
+ var modeLabel = !contextVector ? 'COLD' : isUsingSlowLaneVector ? 'WARM(BE)' : 'WARM(local)';
606
+ var modeColor = modeLabel === 'WARM(BE)' ? 'color: #ff9800; font-weight: bold;' : modeLabel === 'WARM(local)' ? 'color: #4caf50; font-weight: bold;' : 'color: #9e9e9e; font-weight: bold;';
607
+
608
+ // 1. Collapsible group header
609
+ // eslint-disable-next-line no-console
610
+ 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;');
611
+
612
+ // 2. Context window (what local vector averaging sees)
613
+ // eslint-disable-next-line no-console
614
+ console.log('%cContext Window:', 'color: #888; font-style: italic;', contextWords.length ? contextWords.join(' ') : '(none)');
615
+
616
+ // 3. Slow-lane status
617
+ 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)';
618
+ 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)';
619
+ // eslint-disable-next-line no-console
620
+ console.log('%cSlow Lane:', 'color: #888; font-style: italic;', vectorStatus, '|', logitsStatus);
621
+
622
+ // 4. Scoring formula active this prediction
623
+ var formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? 'Stage1(×0.6) + LM(×0.4)' : 'Stage1 only (no LM logits)';
624
+ // eslint-disable-next-line no-console
625
+ console.log('%cFormula:', 'color: #888; font-style: italic;', formulaLabel);
626
+
627
+ // 5. Grammar filter result (collected inside rankCandidates, logged here)
628
+ if (grammarMeta) {
629
+ // eslint-disable-next-line no-console
630
+ 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;');
631
+ if (grammarMeta.dropped.length > 0) {
632
+ // eslint-disable-next-line no-console
633
+ console.log("%c\uD83D\uDEAB Dropped: ".concat(grammarMeta.dropped.join(', ')), 'color: #f44336; font-style: italic;');
634
+ }
635
+ }
636
+
637
+ // 6. Candidate table
638
+ if (ranked.length > 0) {
639
+ var lmCoverage = ranked.slice(0, 10).filter(function (r) {
640
+ return r.lmScore > 0.05;
641
+ }).length;
642
+ // eslint-disable-next-line no-console
643
+ console.log("%cLM coverage: ".concat(lmCoverage, "/").concat(Math.min(ranked.length, 10), " candidates had real logit scores"), 'color: #888; font-style: italic;');
644
+ var tableData = ranked.slice(0, 10).map(function (r) {
645
+ var rawLogit = 'Not in Payload';
646
+ if (lmLogits) {
647
+ var val = lmLogits[r.word.toLowerCase()];
648
+ if (val !== undefined) {
649
+ rawLogit = Number(val.toFixed(5));
650
+ }
651
+ }
652
+ var original = scoringCandidates.find(function (sc) {
653
+ return sc.word === r.word;
654
+ });
655
+ var source = 'Unknown';
656
+ if (original) {
657
+ if (original.docFreq === 0 && original.tenantFreq === L3_BASELINE_FREQ) {
658
+ source = '🌍 L3 (Generic)';
659
+ } else if (original.sessionFreq > 0 && original.tenantFreq === 0) {
660
+ source = '👤 L1 (Session Only)';
661
+ } else {
662
+ source = '🏢 L2 (Domain)';
663
+ }
664
+ }
665
+ return {
666
+ Candidate: r.word,
667
+ Source: source,
668
+ 'Final Score': Number(r.finalScore.toFixed(4)),
669
+ Semantics: Number(r.semanticScore.toFixed(4)),
670
+ Freq: Number(r.freqScore.toFixed(4)),
671
+ 'LM Score': Number(r.lmScore.toFixed(4)),
672
+ 'Raw Logit': rawLogit,
673
+ 'Session Freq': (original === null || original === void 0 ? void 0 : original.sessionFreq) || 0
674
+ };
675
+ });
676
+
677
+ // eslint-disable-next-line no-console
678
+ console.table(tableData);
679
+ } else {
680
+ // eslint-disable-next-line no-console
681
+ console.log('No candidates found.');
682
+ }
683
+
684
+ // eslint-disable-next-line no-console
685
+ console.groupEnd();
686
+ }
687
+ return suggestion && suggestion.length > 0 ? suggestion : null;
688
+ };
689
+
690
+ // ─── Data Loading ────────────────────────────────────────────────────────────
691
+
692
+ var DEFAULT_VECTORS_URL = '/data/word-vectors_10k.bin';
693
+ var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
694
+ var _ref4 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(options) {
695
+ var _options$vectorsUrl;
696
+ var url, res, buffer, float32, wordIndex, nWords, dim;
697
+ return _regenerator.default.wrap(function _callee$(_context) {
698
+ while (1) switch (_context.prev = _context.next) {
699
+ case 0:
700
+ if (!(vectorStore || vectorsLoadStarted)) {
701
+ _context.next = 2;
702
+ break;
703
+ }
704
+ return _context.abrupt("return");
705
+ case 2:
706
+ vectorsLoadStarted = true;
707
+ url = (_options$vectorsUrl = options === null || options === void 0 ? void 0 : options.vectorsUrl) !== null && _options$vectorsUrl !== void 0 ? _options$vectorsUrl : DEFAULT_VECTORS_URL;
708
+ _context.prev = 4;
709
+ _context.next = 7;
710
+ return fetch(url);
711
+ case 7:
712
+ res = _context.sent;
713
+ if (res.ok) {
714
+ _context.next = 12;
715
+ break;
716
+ }
717
+ vectorsLoadStarted = false;
718
+ // eslint-disable-next-line no-console
719
+ console.warn("[text-predictor] Failed to load vectors: ".concat(res.status));
720
+ return _context.abrupt("return");
721
+ case 12:
722
+ _context.next = 14;
723
+ return res.arrayBuffer();
724
+ case 14:
725
+ buffer = _context.sent;
726
+ float32 = new Float32Array(buffer);
727
+ wordIndex = _word_index_10k.default;
728
+ nWords = Object.keys(wordIndex).length;
729
+ dim = float32.length / nWords;
730
+ vectorStore = {
731
+ float32: float32,
732
+ wordIndex: wordIndex,
733
+ dim: dim
734
+ };
735
+ ensureDebugModeFromStorage();
736
+ if (debugMode) {
737
+ // eslint-disable-next-line no-console
738
+ console.log('[text-predictor] Vectors loaded:', {
739
+ wordCount: nWords,
740
+ dim: dim,
741
+ sizeBytes: float32.byteLength
742
+ });
743
+ }
744
+ _context.next = 28;
745
+ break;
746
+ case 24:
747
+ _context.prev = 24;
748
+ _context.t0 = _context["catch"](4);
749
+ vectorsLoadStarted = false;
750
+ // eslint-disable-next-line no-console
751
+ console.warn('[text-predictor] Failed to load vectors:', _context.t0);
752
+ case 28:
753
+ case "end":
754
+ return _context.stop();
755
+ }
756
+ }, _callee, null, [[4, 24]]);
757
+ }));
758
+ return function loadVectorsAsync(_x) {
759
+ return _ref4.apply(this, arguments);
760
+ };
761
+ }();
762
+ var initVectors = exports.initVectors = function initVectors(store) {
763
+ vectorStore = store;
764
+ };
765
+ var loadDefaultVocabulary = exports.loadDefaultVocabulary = function loadDefaultVocabulary() {
766
+ // 1. Load the Atlassian Domain (L2)
767
+ var data = _vocabulary_10k.default;
768
+ var terms = Object.entries(data.words).map(function (_ref5) {
769
+ var _ref6 = (0, _slicedToArray2.default)(_ref5, 2),
770
+ word = _ref6[0],
771
+ stats = _ref6[1];
772
+ return {
773
+ word: word,
774
+ freq: stats.freq,
775
+ docFreq: stats.doc_freq,
776
+ authorFreq: stats.author_freq
777
+ };
778
+ });
779
+ initVocabulary({
780
+ terms: terms
781
+ });
782
+
783
+ // 2. Load General English (L3)
784
+ var l3Words = _l3_vocabulary.default;
785
+ initL3Vocabulary(l3Words);
786
+ };