@atlaskit/editor-plugin-autocomplete 1.0.0 → 2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @atlaskit/editor-plugin-autocomplete
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`6b36a63af0057`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/6b36a63af0057) -
8
+ Updated scoring math for contextual typeahead autocomplete
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies
13
+
14
+ ## 2.0.0
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies
19
+
3
20
  ## 1.0.0
4
21
 
5
22
  ### Patch Changes
@@ -4,6 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
+ exports.STAGE2_WEIGHT = exports.STAGE1_WEIGHT = exports.MIN_STAGE1_SCORE = void 0;
7
8
  exports.rankCandidates = rankCandidates;
8
9
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
9
10
  var _combined_l2_l3_pos_tags = _interopRequireDefault(require("./data/combined_l2_l3_pos_tags.json"));
@@ -26,10 +27,15 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
26
27
  var ALPHA = 0.5;
27
28
  var BETA = 0.5;
28
29
  var NEUTRAL_SCORE = 0.5;
29
- var STAGE1_WEIGHT = 0.6;
30
- var STAGE2_WEIGHT = 0.4;
31
- var MIN_STAGE1_SCORE = 0.35;
30
+ var STAGE1_WEIGHT = exports.STAGE1_WEIGHT = 0.35;
31
+ var STAGE2_WEIGHT = exports.STAGE2_WEIGHT = 0.65;
32
+ var MIN_STAGE1_SCORE = exports.MIN_STAGE1_SCORE = 0.35;
32
33
  var L1_SESSION_CAP = 1.2;
34
+ // Minimum prefix-payload max LM probability before Stage 2 activates.
35
+ // Below this threshold the LM signal is too weak to suppress Stage 1 — finalScore
36
+ // falls back to stage1Score directly. Prevents weak prefixes (e.g. "ins" → "instances"
37
+ // at 0.00024) from triggering re-ranking.
38
+ var LM_GATE_THRESHOLD = 0.0005;
33
39
 
34
40
  // ─── Grammar Data (loaded once on import) ───────────────────
35
41
 
@@ -193,6 +199,7 @@ function getLmScore(word, lmLogits) {
193
199
  // ─── Public API ─────────────────────────────────────────────
194
200
 
195
201
  function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxTenantFreq, previousWord) {
202
+ var _grammarMeta$dropped;
196
203
  // Stage 1
197
204
  var stage1Results = candidates.map(function (candidate) {
198
205
  var _scoreStage = scoreStage1(candidate, contextVector, getWordVector, maxTenantFreq),
@@ -206,33 +213,48 @@ function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxT
206
213
  stage1Score: stage1Score
207
214
  };
208
215
  });
209
- var stage1Survivors = stage1Results.filter(function (entry) {
210
- return entry.stage1Score >= MIN_STAGE1_SCORE;
211
- });
216
+ var stage1Survivors = [];
217
+ var stage1Rejected = [];
218
+ var _iterator3 = _createForOfIteratorHelper(stage1Results),
219
+ _step3;
220
+ try {
221
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
222
+ var entry = _step3.value;
223
+ if (entry.stage1Score >= MIN_STAGE1_SCORE) {
224
+ stage1Survivors.push(entry);
225
+ } else {
226
+ stage1Rejected.push(entry.candidate.word);
227
+ }
228
+ }
212
229
 
213
- // Grammar Filter
230
+ // Grammar Filter
231
+ } catch (err) {
232
+ _iterator3.e(err);
233
+ } finally {
234
+ _iterator3.f();
235
+ }
214
236
  var _applyGrammarFilter = applyGrammarFilter(stage1Survivors, previousWord),
215
237
  filtered = _applyGrammarFilter.filtered,
216
238
  grammarMeta = _applyGrammarFilter.grammarMeta;
217
239
 
218
240
  // Stage 2 + final assembly
219
241
  var lmMax = 0;
220
- if (lmLogits && Object.keys(lmLogits).length > 0) {
242
+ if (lmLogits) {
221
243
  var values = Object.values(lmLogits);
222
- lmMax = Math.max.apply(Math, values);
244
+ if (values.length > 0) lmMax = Math.max.apply(Math, values);
223
245
  }
224
246
  var scored = filtered.map(function (entry) {
225
247
  var lmScore = 0;
226
248
  var finalScore = entry.stage1Score;
227
- if (lmLogits && Object.keys(lmLogits).length > 0) {
249
+ if (lmLogits && lmMax >= LM_GATE_THRESHOLD) {
228
250
  var rawLm = getLmScore(entry.candidate.word, lmLogits);
229
251
  if (rawLm !== 0) {
230
- // The word was in the top_k! Score it normally.
231
252
  var logitDiff = Math.log(rawLm) - Math.log(lmMax);
232
253
  lmScore = Math.exp(logitDiff);
233
- } else {
234
- lmScore = 0.05;
235
254
  }
255
+ // Words absent from the prefix-filtered payload get lmScore = 0,
256
+ // not 0.05, so they don't outrank genuine LM predictions.
257
+
236
258
  finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
237
259
  }
238
260
  return {
@@ -244,13 +266,17 @@ function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxT
244
266
  };
245
267
  });
246
268
  scored.sort(function (a, b) {
247
- if (b.finalScore !== a.finalScore) {
248
- return b.finalScore - a.finalScore;
249
- }
269
+ if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
250
270
  return a.word.length - b.word.length;
251
271
  });
252
272
  return {
253
273
  candidates: scored,
254
- grammarMeta: grammarMeta
274
+ grammarMeta: grammarMeta,
275
+ pipelineDebug: {
276
+ initial: candidates.length,
277
+ stage1Rejected: stage1Rejected,
278
+ grammarRejected: (_grammarMeta$dropped = grammarMeta === null || grammarMeta === void 0 ? void 0 : grammarMeta.dropped) !== null && _grammarMeta$dropped !== void 0 ? _grammarMeta$dropped : [],
279
+ final: scored.length
280
+ }
255
281
  };
256
282
  }
@@ -573,11 +573,21 @@ var predict = exports.predict = function predict(textBefore) {
573
573
  sessionFreq: node.sessionFreq
574
574
  };
575
575
  });
576
+
577
+ // Filter the LM payload to only words matching the current prefix so that
578
+ // lmMax in rankCandidates reflects prefix-relevant signal, not the global distribution.
579
+ var prefix = currentWord.toLowerCase();
580
+ var prefixLmLogits = lmLogits ? Object.fromEntries(Object.entries(lmLogits).filter(function (_ref4) {
581
+ var _ref5 = (0, _slicedToArray2.default)(_ref4, 1),
582
+ word = _ref5[0];
583
+ return word.startsWith(prefix);
584
+ })) : null;
576
585
  var _rankCandidates = (0, _scoringPipeline.rankCandidates)(scoringCandidates, contextVector, function (w) {
577
586
  return getWordVector(w);
578
- }, lmLogits, wordTrie.maxTenantFreq, previousWord),
587
+ }, prefixLmLogits, wordTrie.maxTenantFreq, previousWord),
579
588
  ranked = _rankCandidates.candidates,
580
- grammarMeta = _rankCandidates.grammarMeta;
589
+ grammarMeta = _rankCandidates.grammarMeta,
590
+ pipelineDebug = _rankCandidates.pipelineDebug;
581
591
  var best = ranked[0];
582
592
  var suggestion = best && best.finalScore >= MIN_SCORE_THRESHOLD ? best.word.slice(currentWord.length) : null;
583
593
  if (debugMode) {
@@ -624,7 +634,7 @@ var predict = exports.predict = function predict(textBefore) {
624
634
  console.log('%cSlow Lane:', 'color: #888; font-style: italic;', vectorStatus, '|', logitsStatus);
625
635
 
626
636
  // 4. Scoring formula active this prediction
627
- var formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? 'Stage1(×0.6) + LM(×0.4)' : 'Stage1 only (no LM logits)';
637
+ var formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? "Stage1(\xD7".concat(_scoringPipeline.STAGE1_WEIGHT, ") + LM(\xD7").concat(_scoringPipeline.STAGE2_WEIGHT, ")") : 'Stage1 only (no LM logits)';
628
638
  // eslint-disable-next-line no-console
629
639
  console.log('%cFormula:', 'color: #888; font-style: italic;', formulaLabel);
630
640
 
@@ -638,17 +648,21 @@ var predict = exports.predict = function predict(textBefore) {
638
648
  }
639
649
  }
640
650
 
641
- // 6. Candidate table
651
+ // 6. Pipeline funnel
652
+ // eslint-disable-next-line no-console
653
+ console.log("%c[Pipeline Funnel] %c\uD83D\uDCE5 In: ".concat(pipelineDebug.initial, " | \u274C Stage 1 (< ").concat(_scoringPipeline.MIN_STAGE1_SCORE, "): -").concat(pipelineDebug.stage1Rejected.length, " | \u274C Grammar: -").concat(pipelineDebug.grammarRejected.length, " | \u2705 Final: ").concat(pipelineDebug.final), 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
654
+
655
+ // 7. Candidate table
642
656
  if (ranked.length > 0) {
643
657
  var lmCoverage = ranked.slice(0, 10).filter(function (r) {
644
- return r.lmScore > 0.05;
658
+ return r.lmScore > 0;
645
659
  }).length;
646
660
  // eslint-disable-next-line no-console
647
661
  console.log("%cLM coverage: ".concat(lmCoverage, "/").concat(Math.min(ranked.length, 10), " candidates had real logit scores"), 'color: #888; font-style: italic;');
648
662
  var tableData = ranked.slice(0, 10).map(function (r) {
649
663
  var rawLogit = 'Not in Payload';
650
- if (lmLogits) {
651
- var val = lmLogits[r.word.toLowerCase()];
664
+ if (prefixLmLogits) {
665
+ var val = prefixLmLogits[r.word.toLowerCase()];
652
666
  if (val !== undefined) {
653
667
  rawLogit = Number(val.toFixed(5));
654
668
  }
@@ -694,7 +708,7 @@ var predict = exports.predict = function predict(textBefore) {
694
708
  // ─── Data Loading ────────────────────────────────────────────────────────────
695
709
 
696
710
  var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
697
- var _ref4 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(options) {
711
+ var _ref6 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(options) {
698
712
  var url, res, buffer, float32, wordIndex, nWords, dim;
699
713
  return _regenerator.default.wrap(function _callee$(_context) {
700
714
  while (1) switch (_context.prev = _context.next) {
@@ -780,7 +794,7 @@ var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
780
794
  }, _callee, null, [[6, 12], [17, 37]]);
781
795
  }));
782
796
  return function loadVectorsAsync(_x) {
783
- return _ref4.apply(this, arguments);
797
+ return _ref6.apply(this, arguments);
784
798
  };
785
799
  }();
786
800
  var initVectors = exports.initVectors = function initVectors(store) {
@@ -789,10 +803,10 @@ var initVectors = exports.initVectors = function initVectors(store) {
789
803
  var loadDefaultVocabulary = exports.loadDefaultVocabulary = function loadDefaultVocabulary() {
790
804
  // 1. Load the Atlassian Domain (L2)
791
805
  var data = _vocabulary_10k.default;
792
- var terms = Object.entries(data.words).map(function (_ref5) {
793
- var _ref6 = (0, _slicedToArray2.default)(_ref5, 2),
794
- word = _ref6[0],
795
- stats = _ref6[1];
806
+ var terms = Object.entries(data.words).map(function (_ref7) {
807
+ var _ref8 = (0, _slicedToArray2.default)(_ref7, 2),
808
+ word = _ref8[0],
809
+ stats = _ref8[1];
796
810
  return {
797
811
  word: word,
798
812
  freq: stats.freq,
@@ -18,10 +18,15 @@ import grammarTransitionsData from './data/grammar_transitions_10k.json';
18
18
  const ALPHA = 0.5;
19
19
  const BETA = 0.5;
20
20
  const NEUTRAL_SCORE = 0.5;
21
- const STAGE1_WEIGHT = 0.6;
22
- const STAGE2_WEIGHT = 0.4;
23
- const MIN_STAGE1_SCORE = 0.35;
21
+ export const STAGE1_WEIGHT = 0.35;
22
+ export const STAGE2_WEIGHT = 0.65;
23
+ export const MIN_STAGE1_SCORE = 0.35;
24
24
  const L1_SESSION_CAP = 1.2;
25
+ // Minimum prefix-payload max LM probability before Stage 2 activates.
26
+ // Below this threshold the LM signal is too weak to suppress Stage 1 — finalScore
27
+ // falls back to stage1Score directly. Prevents weak prefixes (e.g. "ins" → "instances"
28
+ // at 0.00024) from triggering re-ranking.
29
+ const LM_GATE_THRESHOLD = 0.0005;
25
30
 
26
31
  // ─── Grammar Data (loaded once on import) ───────────────────
27
32
 
@@ -158,6 +163,7 @@ function getLmScore(word, lmLogits) {
158
163
  // ─── Public API ─────────────────────────────────────────────
159
164
 
160
165
  export function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxTenantFreq, previousWord) {
166
+ var _grammarMeta$dropped;
161
167
  // Stage 1
162
168
  const stage1Results = candidates.map(candidate => {
163
169
  const {
@@ -172,7 +178,15 @@ export function rankCandidates(candidates, contextVector, getWordVector, lmLogit
172
178
  stage1Score
173
179
  };
174
180
  });
175
- const stage1Survivors = stage1Results.filter(entry => entry.stage1Score >= MIN_STAGE1_SCORE);
181
+ const stage1Survivors = [];
182
+ const stage1Rejected = [];
183
+ for (const entry of stage1Results) {
184
+ if (entry.stage1Score >= MIN_STAGE1_SCORE) {
185
+ stage1Survivors.push(entry);
186
+ } else {
187
+ stage1Rejected.push(entry.candidate.word);
188
+ }
189
+ }
176
190
 
177
191
  // Grammar Filter
178
192
  const {
@@ -182,22 +196,22 @@ export function rankCandidates(candidates, contextVector, getWordVector, lmLogit
182
196
 
183
197
  // Stage 2 + final assembly
184
198
  let lmMax = 0;
185
- if (lmLogits && Object.keys(lmLogits).length > 0) {
199
+ if (lmLogits) {
186
200
  const values = Object.values(lmLogits);
187
- lmMax = Math.max(...values);
201
+ if (values.length > 0) lmMax = Math.max(...values);
188
202
  }
189
203
  const scored = filtered.map(entry => {
190
204
  let lmScore = 0;
191
205
  let finalScore = entry.stage1Score;
192
- if (lmLogits && Object.keys(lmLogits).length > 0) {
206
+ if (lmLogits && lmMax >= LM_GATE_THRESHOLD) {
193
207
  const rawLm = getLmScore(entry.candidate.word, lmLogits);
194
208
  if (rawLm !== 0) {
195
- // The word was in the top_k! Score it normally.
196
209
  const logitDiff = Math.log(rawLm) - Math.log(lmMax);
197
210
  lmScore = Math.exp(logitDiff);
198
- } else {
199
- lmScore = 0.05;
200
211
  }
212
+ // Words absent from the prefix-filtered payload get lmScore = 0,
213
+ // not 0.05, so they don't outrank genuine LM predictions.
214
+
201
215
  finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
202
216
  }
203
217
  return {
@@ -209,13 +223,17 @@ export function rankCandidates(candidates, contextVector, getWordVector, lmLogit
209
223
  };
210
224
  });
211
225
  scored.sort((a, b) => {
212
- if (b.finalScore !== a.finalScore) {
213
- return b.finalScore - a.finalScore;
214
- }
226
+ if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
215
227
  return a.word.length - b.word.length;
216
228
  });
217
229
  return {
218
230
  candidates: scored,
219
- grammarMeta
231
+ grammarMeta,
232
+ pipelineDebug: {
233
+ initial: candidates.length,
234
+ stage1Rejected,
235
+ grammarRejected: (_grammarMeta$dropped = grammarMeta === null || grammarMeta === void 0 ? void 0 : grammarMeta.dropped) !== null && _grammarMeta$dropped !== void 0 ? _grammarMeta$dropped : [],
236
+ final: scored.length
237
+ }
220
238
  };
221
239
  }
@@ -21,7 +21,7 @@ import l3VocabularyData from './data/l3_vocabulary.json';
21
21
  import vocabularyData from './data/vocabulary_10k.json';
22
22
  import wordIndexData from './data/word_index_10k.json';
23
23
  // import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
24
- import { rankCandidates } from './scoring-pipeline';
24
+ import { rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE } from './scoring-pipeline';
25
25
  import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
26
26
 
27
27
  // ─── Constants ───────────────────────────────────────────────────────────────
@@ -452,10 +452,16 @@ export const predict = textBefore => {
452
452
  authorFreq: node.authorFreq,
453
453
  sessionFreq: node.sessionFreq
454
454
  }));
455
+
456
+ // Filter the LM payload to only words matching the current prefix so that
457
+ // lmMax in rankCandidates reflects prefix-relevant signal, not the global distribution.
458
+ const prefix = currentWord.toLowerCase();
459
+ const prefixLmLogits = lmLogits ? Object.fromEntries(Object.entries(lmLogits).filter(([word]) => word.startsWith(prefix))) : null;
455
460
  const {
456
461
  candidates: ranked,
457
- grammarMeta
458
- } = rankCandidates(scoringCandidates, contextVector, w => getWordVector(w), lmLogits, wordTrie.maxTenantFreq, previousWord);
462
+ grammarMeta,
463
+ pipelineDebug
464
+ } = rankCandidates(scoringCandidates, contextVector, w => getWordVector(w), prefixLmLogits, wordTrie.maxTenantFreq, previousWord);
459
465
  const best = ranked[0];
460
466
  const suggestion = best && best.finalScore >= MIN_SCORE_THRESHOLD ? best.word.slice(currentWord.length) : null;
461
467
  if (debugMode) {
@@ -500,7 +506,7 @@ export const predict = textBefore => {
500
506
  console.log('%cSlow Lane:', 'color: #888; font-style: italic;', vectorStatus, '|', logitsStatus);
501
507
 
502
508
  // 4. Scoring formula active this prediction
503
- const formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? 'Stage1(×0.6) + LM(×0.4)' : 'Stage1 only (no LM logits)';
509
+ const formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? `Stage1(×${STAGE1_WEIGHT}) + LM(×${STAGE2_WEIGHT})` : 'Stage1 only (no LM logits)';
504
510
  // eslint-disable-next-line no-console
505
511
  console.log('%cFormula:', 'color: #888; font-style: italic;', formulaLabel);
506
512
 
@@ -514,15 +520,19 @@ export const predict = textBefore => {
514
520
  }
515
521
  }
516
522
 
517
- // 6. Candidate table
523
+ // 6. Pipeline funnel
524
+ // eslint-disable-next-line no-console
525
+ console.log(`%c[Pipeline Funnel] %c📥 In: ${pipelineDebug.initial} | ❌ Stage 1 (< ${MIN_STAGE1_SCORE}): -${pipelineDebug.stage1Rejected.length} | ❌ Grammar: -${pipelineDebug.grammarRejected.length} | ✅ Final: ${pipelineDebug.final}`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
526
+
527
+ // 7. Candidate table
518
528
  if (ranked.length > 0) {
519
- const lmCoverage = ranked.slice(0, 10).filter(r => r.lmScore > 0.05).length;
529
+ const lmCoverage = ranked.slice(0, 10).filter(r => r.lmScore > 0).length;
520
530
  // eslint-disable-next-line no-console
521
531
  console.log(`%cLM coverage: ${lmCoverage}/${Math.min(ranked.length, 10)} candidates had real logit scores`, 'color: #888; font-style: italic;');
522
532
  const tableData = ranked.slice(0, 10).map(r => {
523
533
  let rawLogit = 'Not in Payload';
524
- if (lmLogits) {
525
- const val = lmLogits[r.word.toLowerCase()];
534
+ if (prefixLmLogits) {
535
+ const val = prefixLmLogits[r.word.toLowerCase()];
526
536
  if (val !== undefined) {
527
537
  rawLogit = Number(val.toFixed(5));
528
538
  }
@@ -22,10 +22,15 @@ import grammarTransitionsData from './data/grammar_transitions_10k.json';
22
22
  var ALPHA = 0.5;
23
23
  var BETA = 0.5;
24
24
  var NEUTRAL_SCORE = 0.5;
25
- var STAGE1_WEIGHT = 0.6;
26
- var STAGE2_WEIGHT = 0.4;
27
- var MIN_STAGE1_SCORE = 0.35;
25
+ export var STAGE1_WEIGHT = 0.35;
26
+ export var STAGE2_WEIGHT = 0.65;
27
+ export var MIN_STAGE1_SCORE = 0.35;
28
28
  var L1_SESSION_CAP = 1.2;
29
+ // Minimum prefix-payload max LM probability before Stage 2 activates.
30
+ // Below this threshold the LM signal is too weak to suppress Stage 1 — finalScore
31
+ // falls back to stage1Score directly. Prevents weak prefixes (e.g. "ins" → "instances"
32
+ // at 0.00024) from triggering re-ranking.
33
+ var LM_GATE_THRESHOLD = 0.0005;
29
34
 
30
35
  // ─── Grammar Data (loaded once on import) ───────────────────
31
36
 
@@ -189,6 +194,7 @@ function getLmScore(word, lmLogits) {
189
194
  // ─── Public API ─────────────────────────────────────────────
190
195
 
191
196
  export function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxTenantFreq, previousWord) {
197
+ var _grammarMeta$dropped;
192
198
  // Stage 1
193
199
  var stage1Results = candidates.map(function (candidate) {
194
200
  var _scoreStage = scoreStage1(candidate, contextVector, getWordVector, maxTenantFreq),
@@ -202,33 +208,48 @@ export function rankCandidates(candidates, contextVector, getWordVector, lmLogit
202
208
  stage1Score: stage1Score
203
209
  };
204
210
  });
205
- var stage1Survivors = stage1Results.filter(function (entry) {
206
- return entry.stage1Score >= MIN_STAGE1_SCORE;
207
- });
211
+ var stage1Survivors = [];
212
+ var stage1Rejected = [];
213
+ var _iterator3 = _createForOfIteratorHelper(stage1Results),
214
+ _step3;
215
+ try {
216
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
217
+ var entry = _step3.value;
218
+ if (entry.stage1Score >= MIN_STAGE1_SCORE) {
219
+ stage1Survivors.push(entry);
220
+ } else {
221
+ stage1Rejected.push(entry.candidate.word);
222
+ }
223
+ }
208
224
 
209
- // Grammar Filter
225
+ // Grammar Filter
226
+ } catch (err) {
227
+ _iterator3.e(err);
228
+ } finally {
229
+ _iterator3.f();
230
+ }
210
231
  var _applyGrammarFilter = applyGrammarFilter(stage1Survivors, previousWord),
211
232
  filtered = _applyGrammarFilter.filtered,
212
233
  grammarMeta = _applyGrammarFilter.grammarMeta;
213
234
 
214
235
  // Stage 2 + final assembly
215
236
  var lmMax = 0;
216
- if (lmLogits && Object.keys(lmLogits).length > 0) {
237
+ if (lmLogits) {
217
238
  var values = Object.values(lmLogits);
218
- lmMax = Math.max.apply(Math, values);
239
+ if (values.length > 0) lmMax = Math.max.apply(Math, values);
219
240
  }
220
241
  var scored = filtered.map(function (entry) {
221
242
  var lmScore = 0;
222
243
  var finalScore = entry.stage1Score;
223
- if (lmLogits && Object.keys(lmLogits).length > 0) {
244
+ if (lmLogits && lmMax >= LM_GATE_THRESHOLD) {
224
245
  var rawLm = getLmScore(entry.candidate.word, lmLogits);
225
246
  if (rawLm !== 0) {
226
- // The word was in the top_k! Score it normally.
227
247
  var logitDiff = Math.log(rawLm) - Math.log(lmMax);
228
248
  lmScore = Math.exp(logitDiff);
229
- } else {
230
- lmScore = 0.05;
231
249
  }
250
+ // Words absent from the prefix-filtered payload get lmScore = 0,
251
+ // not 0.05, so they don't outrank genuine LM predictions.
252
+
232
253
  finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
233
254
  }
234
255
  return {
@@ -240,13 +261,17 @@ export function rankCandidates(candidates, contextVector, getWordVector, lmLogit
240
261
  };
241
262
  });
242
263
  scored.sort(function (a, b) {
243
- if (b.finalScore !== a.finalScore) {
244
- return b.finalScore - a.finalScore;
245
- }
264
+ if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
246
265
  return a.word.length - b.word.length;
247
266
  });
248
267
  return {
249
268
  candidates: scored,
250
- grammarMeta: grammarMeta
269
+ grammarMeta: grammarMeta,
270
+ pipelineDebug: {
271
+ initial: candidates.length,
272
+ stage1Rejected: stage1Rejected,
273
+ grammarRejected: (_grammarMeta$dropped = grammarMeta === null || grammarMeta === void 0 ? void 0 : grammarMeta.dropped) !== null && _grammarMeta$dropped !== void 0 ? _grammarMeta$dropped : [],
274
+ final: scored.length
275
+ }
251
276
  };
252
277
  }
@@ -29,7 +29,7 @@ import l3VocabularyData from './data/l3_vocabulary.json';
29
29
  import vocabularyData from './data/vocabulary_10k.json';
30
30
  import wordIndexData from './data/word_index_10k.json';
31
31
  // import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
32
- import { rankCandidates } from './scoring-pipeline';
32
+ import { rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE } from './scoring-pipeline';
33
33
  import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
34
34
 
35
35
  // ─── Constants ───────────────────────────────────────────────────────────────
@@ -570,11 +570,21 @@ export var predict = function predict(textBefore) {
570
570
  sessionFreq: node.sessionFreq
571
571
  };
572
572
  });
573
+
574
+ // Filter the LM payload to only words matching the current prefix so that
575
+ // lmMax in rankCandidates reflects prefix-relevant signal, not the global distribution.
576
+ var prefix = currentWord.toLowerCase();
577
+ var prefixLmLogits = lmLogits ? Object.fromEntries(Object.entries(lmLogits).filter(function (_ref4) {
578
+ var _ref5 = _slicedToArray(_ref4, 1),
579
+ word = _ref5[0];
580
+ return word.startsWith(prefix);
581
+ })) : null;
573
582
  var _rankCandidates = rankCandidates(scoringCandidates, contextVector, function (w) {
574
583
  return getWordVector(w);
575
- }, lmLogits, wordTrie.maxTenantFreq, previousWord),
584
+ }, prefixLmLogits, wordTrie.maxTenantFreq, previousWord),
576
585
  ranked = _rankCandidates.candidates,
577
- grammarMeta = _rankCandidates.grammarMeta;
586
+ grammarMeta = _rankCandidates.grammarMeta,
587
+ pipelineDebug = _rankCandidates.pipelineDebug;
578
588
  var best = ranked[0];
579
589
  var suggestion = best && best.finalScore >= MIN_SCORE_THRESHOLD ? best.word.slice(currentWord.length) : null;
580
590
  if (debugMode) {
@@ -621,7 +631,7 @@ export var predict = function predict(textBefore) {
621
631
  console.log('%cSlow Lane:', 'color: #888; font-style: italic;', vectorStatus, '|', logitsStatus);
622
632
 
623
633
  // 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)';
634
+ var formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? "Stage1(\xD7".concat(STAGE1_WEIGHT, ") + LM(\xD7").concat(STAGE2_WEIGHT, ")") : 'Stage1 only (no LM logits)';
625
635
  // eslint-disable-next-line no-console
626
636
  console.log('%cFormula:', 'color: #888; font-style: italic;', formulaLabel);
627
637
 
@@ -635,17 +645,21 @@ export var predict = function predict(textBefore) {
635
645
  }
636
646
  }
637
647
 
638
- // 6. Candidate table
648
+ // 6. Pipeline funnel
649
+ // eslint-disable-next-line no-console
650
+ console.log("%c[Pipeline Funnel] %c\uD83D\uDCE5 In: ".concat(pipelineDebug.initial, " | \u274C Stage 1 (< ").concat(MIN_STAGE1_SCORE, "): -").concat(pipelineDebug.stage1Rejected.length, " | \u274C Grammar: -").concat(pipelineDebug.grammarRejected.length, " | \u2705 Final: ").concat(pipelineDebug.final), 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
651
+
652
+ // 7. Candidate table
639
653
  if (ranked.length > 0) {
640
654
  var lmCoverage = ranked.slice(0, 10).filter(function (r) {
641
- return r.lmScore > 0.05;
655
+ return r.lmScore > 0;
642
656
  }).length;
643
657
  // eslint-disable-next-line no-console
644
658
  console.log("%cLM coverage: ".concat(lmCoverage, "/").concat(Math.min(ranked.length, 10), " candidates had real logit scores"), 'color: #888; font-style: italic;');
645
659
  var tableData = ranked.slice(0, 10).map(function (r) {
646
660
  var rawLogit = 'Not in Payload';
647
- if (lmLogits) {
648
- var val = lmLogits[r.word.toLowerCase()];
661
+ if (prefixLmLogits) {
662
+ var val = prefixLmLogits[r.word.toLowerCase()];
649
663
  if (val !== undefined) {
650
664
  rawLogit = Number(val.toFixed(5));
651
665
  }
@@ -691,7 +705,7 @@ export var predict = function predict(textBefore) {
691
705
  // ─── Data Loading ────────────────────────────────────────────────────────────
692
706
 
693
707
  export var loadVectorsAsync = /*#__PURE__*/function () {
694
- var _ref4 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(options) {
708
+ var _ref6 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(options) {
695
709
  var url, res, buffer, float32, wordIndex, nWords, dim;
696
710
  return _regeneratorRuntime.wrap(function _callee$(_context) {
697
711
  while (1) switch (_context.prev = _context.next) {
@@ -777,7 +791,7 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
777
791
  }, _callee, null, [[6, 12], [17, 37]]);
778
792
  }));
779
793
  return function loadVectorsAsync(_x) {
780
- return _ref4.apply(this, arguments);
794
+ return _ref6.apply(this, arguments);
781
795
  };
782
796
  }();
783
797
  export var initVectors = function initVectors(store) {
@@ -786,10 +800,10 @@ export var initVectors = function initVectors(store) {
786
800
  export var loadDefaultVocabulary = function loadDefaultVocabulary() {
787
801
  // 1. Load the Atlassian Domain (L2)
788
802
  var data = vocabularyData;
789
- var terms = Object.entries(data.words).map(function (_ref5) {
790
- var _ref6 = _slicedToArray(_ref5, 2),
791
- word = _ref6[0],
792
- stats = _ref6[1];
803
+ var terms = Object.entries(data.words).map(function (_ref7) {
804
+ var _ref8 = _slicedToArray(_ref7, 2),
805
+ word = _ref8[0],
806
+ stats = _ref8[1];
793
807
  return {
794
808
  word: word,
795
809
  freq: stats.freq,
@@ -26,8 +26,18 @@ export interface GrammarFilterMeta {
26
26
  prevTags: string[];
27
27
  prevWord: string;
28
28
  }
29
+ export declare const STAGE1_WEIGHT = 0.35;
30
+ export declare const STAGE2_WEIGHT = 0.65;
31
+ export declare const MIN_STAGE1_SCORE = 0.35;
32
+ export interface PipelineDebug {
33
+ final: number;
34
+ grammarRejected: string[];
35
+ initial: number;
36
+ stage1Rejected: string[];
37
+ }
29
38
  export interface RankCandidatesResult {
30
39
  candidates: ScoredCandidate[];
31
40
  grammarMeta: GrammarFilterMeta | null;
41
+ pipelineDebug: PipelineDebug;
32
42
  }
33
43
  export declare function rankCandidates(candidates: ScoringCandidate[], contextVector: Float32Array | null, getWordVector: (word: string) => Float32Array | null, lmLogits: Record<string, number> | null, maxTenantFreq: number, previousWord: string): RankCandidatesResult;
@@ -26,8 +26,18 @@ export interface GrammarFilterMeta {
26
26
  prevTags: string[];
27
27
  prevWord: string;
28
28
  }
29
+ export declare const STAGE1_WEIGHT = 0.35;
30
+ export declare const STAGE2_WEIGHT = 0.65;
31
+ export declare const MIN_STAGE1_SCORE = 0.35;
32
+ export interface PipelineDebug {
33
+ final: number;
34
+ grammarRejected: string[];
35
+ initial: number;
36
+ stage1Rejected: string[];
37
+ }
29
38
  export interface RankCandidatesResult {
30
39
  candidates: ScoredCandidate[];
31
40
  grammarMeta: GrammarFilterMeta | null;
41
+ pipelineDebug: PipelineDebug;
32
42
  }
33
43
  export declare function rankCandidates(candidates: ScoringCandidate[], contextVector: Float32Array | null, getWordVector: (word: string) => Float32Array | null, lmLogits: Record<string, number> | null, maxTenantFreq: number, previousWord: string): RankCandidatesResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-autocomplete",
3
- "version": "1.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Client-side text autocomplete plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -33,7 +33,7 @@
33
33
  "wink-nlp": "^2.4.0"
34
34
  },
35
35
  "peerDependencies": {
36
- "@atlaskit/editor-common": "^113.0.0",
36
+ "@atlaskit/editor-common": "^114.1.0",
37
37
  "react": "^18.2.0"
38
38
  },
39
39
  "techstack": {
@@ -49,10 +49,15 @@ interface GrammarTransitions {
49
49
  const ALPHA = 0.5;
50
50
  const BETA = 0.5;
51
51
  const NEUTRAL_SCORE = 0.5;
52
- const STAGE1_WEIGHT = 0.6;
53
- const STAGE2_WEIGHT = 0.4;
54
- const MIN_STAGE1_SCORE = 0.35;
52
+ export const STAGE1_WEIGHT = 0.35;
53
+ export const STAGE2_WEIGHT = 0.65;
54
+ export const MIN_STAGE1_SCORE = 0.35;
55
55
  const L1_SESSION_CAP = 1.2;
56
+ // Minimum prefix-payload max LM probability before Stage 2 activates.
57
+ // Below this threshold the LM signal is too weak to suppress Stage 1 — finalScore
58
+ // falls back to stage1Score directly. Prevents weak prefixes (e.g. "ins" → "instances"
59
+ // at 0.00024) from triggering re-ranking.
60
+ const LM_GATE_THRESHOLD = 0.0005;
56
61
 
57
62
  // ─── Grammar Data (loaded once on import) ───────────────────
58
63
 
@@ -217,9 +222,17 @@ function getLmScore(word: string, lmLogits: Record<string, number> | null): numb
217
222
 
218
223
  // ─── Public API ─────────────────────────────────────────────
219
224
 
225
+ export interface PipelineDebug {
226
+ final: number;
227
+ grammarRejected: string[];
228
+ initial: number;
229
+ stage1Rejected: string[];
230
+ }
231
+
220
232
  export interface RankCandidatesResult {
221
233
  candidates: ScoredCandidate[];
222
234
  grammarMeta: GrammarFilterMeta | null;
235
+ pipelineDebug: PipelineDebug;
223
236
  }
224
237
 
225
238
  export function rankCandidates(
@@ -240,33 +253,39 @@ export function rankCandidates(
240
253
  );
241
254
  return { candidate, semanticScore, freqScore, stage1Score };
242
255
  });
243
-
244
- const stage1Survivors = stage1Results.filter((entry) => entry.stage1Score >= MIN_STAGE1_SCORE);
256
+ const stage1Survivors: typeof stage1Results = [];
257
+ const stage1Rejected: string[] = [];
258
+ for (const entry of stage1Results) {
259
+ if (entry.stage1Score >= MIN_STAGE1_SCORE) {
260
+ stage1Survivors.push(entry);
261
+ } else {
262
+ stage1Rejected.push(entry.candidate.word);
263
+ }
264
+ }
245
265
 
246
266
  // Grammar Filter
247
267
  const { filtered, grammarMeta } = applyGrammarFilter(stage1Survivors, previousWord);
248
268
 
249
269
  // Stage 2 + final assembly
250
270
  let lmMax = 0;
251
- if (lmLogits && Object.keys(lmLogits).length > 0) {
271
+ if (lmLogits) {
252
272
  const values = Object.values(lmLogits);
253
- lmMax = Math.max(...values);
273
+ if (values.length > 0) lmMax = Math.max(...values);
254
274
  }
255
275
 
256
276
  const scored: ScoredCandidate[] = filtered.map((entry) => {
257
277
  let lmScore = 0;
258
278
  let finalScore = entry.stage1Score;
259
279
 
260
- if (lmLogits && Object.keys(lmLogits).length > 0) {
280
+ if (lmLogits && lmMax >= LM_GATE_THRESHOLD) {
261
281
  const rawLm = getLmScore(entry.candidate.word, lmLogits);
262
282
 
263
283
  if (rawLm !== 0) {
264
- // The word was in the top_k! Score it normally.
265
284
  const logitDiff = Math.log(rawLm) - Math.log(lmMax);
266
285
  lmScore = Math.exp(logitDiff);
267
- } else {
268
- lmScore = 0.05;
269
286
  }
287
+ // Words absent from the prefix-filtered payload get lmScore = 0,
288
+ // not 0.05, so they don't outrank genuine LM predictions.
270
289
 
271
290
  finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
272
291
  }
@@ -281,11 +300,18 @@ export function rankCandidates(
281
300
  });
282
301
 
283
302
  scored.sort((a, b) => {
284
- if (b.finalScore !== a.finalScore) {
285
- return b.finalScore - a.finalScore;
286
- }
303
+ if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
287
304
  return a.word.length - b.word.length;
288
305
  });
289
306
 
290
- return { candidates: scored, grammarMeta };
307
+ return {
308
+ candidates: scored,
309
+ grammarMeta,
310
+ pipelineDebug: {
311
+ initial: candidates.length,
312
+ stage1Rejected,
313
+ grammarRejected: grammarMeta?.dropped ?? [],
314
+ final: scored.length,
315
+ },
316
+ };
291
317
  }
@@ -20,7 +20,7 @@ import l3VocabularyData from './data/l3_vocabulary.json';
20
20
  import vocabularyData from './data/vocabulary_10k.json';
21
21
  import wordIndexData from './data/word_index_10k.json';
22
22
  // import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
23
- import { rankCandidates } from './scoring-pipeline';
23
+ import { rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE } from './scoring-pipeline';
24
24
  import type { ScoringCandidate } from './scoring-pipeline';
25
25
  import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
26
26
 
@@ -533,11 +533,19 @@ export const predict = (textBefore: string): string | null => {
533
533
  sessionFreq: node.sessionFreq,
534
534
  }));
535
535
 
536
- const { candidates: ranked, grammarMeta } = rankCandidates(
536
+ // Filter the LM payload to only words matching the current prefix so that
537
+ // lmMax in rankCandidates reflects prefix-relevant signal, not the global distribution.
538
+ const prefix = currentWord.toLowerCase();
539
+ const prefixLmLogits = lmLogits
540
+ ? Object.fromEntries(
541
+ Object.entries(lmLogits).filter(([word]) => word.startsWith(prefix))
542
+ )
543
+ : null;
544
+ const { candidates: ranked, grammarMeta, pipelineDebug } = rankCandidates(
537
545
  scoringCandidates,
538
546
  contextVector,
539
547
  (w: string) => getWordVector(w),
540
- lmLogits,
548
+ prefixLmLogits,
541
549
  wordTrie.maxTenantFreq,
542
550
  previousWord,
543
551
  );
@@ -624,7 +632,7 @@ export const predict = (textBefore: string): string | null => {
624
632
  // 4. Scoring formula active this prediction
625
633
  const formulaLabel =
626
634
  lmLogits && Object.keys(lmLogits).length > 0
627
- ? 'Stage1(×0.6) + LM(×0.4)'
635
+ ? `Stage1(×${STAGE1_WEIGHT}) + LM(×${STAGE2_WEIGHT})`
628
636
  : 'Stage1 only (no LM logits)';
629
637
  // eslint-disable-next-line no-console
630
638
  console.log('%cFormula:', 'color: #888; font-style: italic;', formulaLabel);
@@ -645,9 +653,17 @@ export const predict = (textBefore: string): string | null => {
645
653
  }
646
654
  }
647
655
 
648
- // 6. Candidate table
656
+ // 6. Pipeline funnel
657
+ // eslint-disable-next-line no-console
658
+ console.log(
659
+ `%c[Pipeline Funnel] %c📥 In: ${pipelineDebug.initial} | ❌ Stage 1 (< ${MIN_STAGE1_SCORE}): -${pipelineDebug.stage1Rejected.length} | ❌ Grammar: -${pipelineDebug.grammarRejected.length} | ✅ Final: ${pipelineDebug.final}`,
660
+ 'color: #9c27b0; font-weight: bold;',
661
+ 'color: inherit;',
662
+ );
663
+
664
+ // 7. Candidate table
649
665
  if (ranked.length > 0) {
650
- const lmCoverage = ranked.slice(0, 10).filter((r) => r.lmScore > 0.05).length;
666
+ const lmCoverage = ranked.slice(0, 10).filter((r) => r.lmScore > 0).length;
651
667
  // eslint-disable-next-line no-console
652
668
  console.log(
653
669
  `%cLM coverage: ${lmCoverage}/${Math.min(ranked.length, 10)} candidates had real logit scores`,
@@ -656,8 +672,8 @@ export const predict = (textBefore: string): string | null => {
656
672
 
657
673
  const tableData = ranked.slice(0, 10).map((r) => {
658
674
  let rawLogit: string | number = 'Not in Payload';
659
- if (lmLogits) {
660
- const val = lmLogits[r.word.toLowerCase()];
675
+ if (prefixLmLogits) {
676
+ const val = prefixLmLogits[r.word.toLowerCase()];
661
677
  if (val !== undefined) {
662
678
  rawLogit = Number(val.toFixed(5));
663
679
  }