@atlaskit/editor-plugin-autocomplete 2.0.0 → 2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @atlaskit/editor-plugin-autocomplete
2
2
 
3
+ ## 2.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`6eb5747f5ba37`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/6eb5747f5ba37) -
8
+ Add SAR analytics for autocomplete plugin
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies
13
+
14
+ ## 2.1.0
15
+
16
+ ### Minor Changes
17
+
18
+ - [`6b36a63af0057`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/6b36a63af0057) -
19
+ Updated scoring math for contextual typeahead autocomplete
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies
24
+
3
25
  ## 2.0.0
4
26
 
5
27
  ### Patch Changes
@@ -25,6 +25,9 @@
25
25
  },
26
26
  {
27
27
  "path": "../../editor-common/afm-cc/tsconfig.json"
28
+ },
29
+ {
30
+ "path": "../../editor-plugin-analytics/afm-cc/tsconfig.json"
28
31
  }
29
32
  ]
30
33
  }
@@ -25,6 +25,9 @@
25
25
  },
26
26
  {
27
27
  "path": "../../editor-common/afm-products/tsconfig.json"
28
+ },
29
+ {
30
+ "path": "../../editor-plugin-analytics/afm-products/tsconfig.json"
28
31
  }
29
32
  ]
30
33
  }
@@ -6,7 +6,8 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.autocompletePlugin = void 0;
7
7
  var _autocompletePlugin = require("./pm-plugins/autocomplete-plugin");
8
8
  var autocompletePlugin = exports.autocompletePlugin = function autocompletePlugin(_ref) {
9
- var options = _ref.config;
9
+ var options = _ref.config,
10
+ api = _ref.api;
10
11
  return {
11
12
  name: 'autocomplete',
12
13
  getSharedState: function getSharedState(editorState) {
@@ -19,7 +20,7 @@ var autocompletePlugin = exports.autocompletePlugin = function autocompletePlugi
19
20
  return [{
20
21
  name: 'autocomplete',
21
22
  plugin: function plugin() {
22
- return (0, _autocompletePlugin.createAutocompletePlugin)(options);
23
+ return (0, _autocompletePlugin.createAutocompletePlugin)(options, api);
23
24
  }
24
25
  }];
25
26
  }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  });
7
7
  exports.createAutocompletePlugin = exports.autocompletePluginKey = void 0;
8
8
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
+ var _analytics = require("@atlaskit/editor-common/analytics");
9
10
  var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
10
11
  var _keymap = require("@atlaskit/editor-prosemirror/keymap");
11
12
  var _state = require("@atlaskit/editor-prosemirror/state");
@@ -68,6 +69,7 @@ var setAutocompleteMeta = function setAutocompleteMeta(tr, meta) {
68
69
  /**
69
70
  * Apply a ghost text suggestion to the editor state.
70
71
  */
72
+ var lastShownGhostText = '';
71
73
  var showGhostText = function showGhostText(view, text, position) {
72
74
  var state = view.state,
73
75
  dispatch = view.dispatch;
@@ -146,7 +148,7 @@ var buildSlowLaneText = function buildSlowLaneText(docText, context) {
146
148
  lines.push("reply ".concat(nextReplyNumber, ": ").concat(docText));
147
149
  return lines.join('\n');
148
150
  };
149
- var createAutocompletePlugin = exports.createAutocompletePlugin = function createAutocompletePlugin(options) {
151
+ var createAutocompletePlugin = exports.createAutocompletePlugin = function createAutocompletePlugin(options, api) {
150
152
  var debounceTimer = null;
151
153
  var hasIngestedPage = false;
152
154
  var resolvedContext;
@@ -204,6 +206,15 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
204
206
  var prediction = (0, _textPredictor.predict)(textBefore);
205
207
  if (prediction && prediction.length > 0) {
206
208
  showGhostText(view, prediction, selection.from);
209
+ if (prediction !== lastShownGhostText) {
210
+ var _api$analytics;
211
+ lastShownGhostText = prediction;
212
+ api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.fireAnalyticsEvent({
213
+ action: _analytics.ACTION.SUGGESTION_VIEWED,
214
+ actionSubject: _analytics.ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
215
+ eventType: _analytics.EVENT_TYPE.TRACK
216
+ });
217
+ }
207
218
  }
208
219
  }, DEBOUNCE_MS);
209
220
  };
@@ -275,12 +286,30 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
275
286
  handleKeyDown: (0, _keymap.keydownHandler)({
276
287
  Tab: function Tab(state, dispatch) {
277
288
  var accepted = acceptGhostText(state, dispatch);
278
- if (accepted) justAccepted = true;
289
+ if (accepted) {
290
+ var _api$analytics2;
291
+ justAccepted = true;
292
+ lastShownGhostText = '';
293
+ api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 || _api$analytics2.actions.fireAnalyticsEvent({
294
+ action: _analytics.ACTION.SUGGESTION_INSERTED,
295
+ actionSubject: _analytics.ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
296
+ eventType: _analytics.EVENT_TYPE.TRACK
297
+ });
298
+ }
279
299
  return accepted;
280
300
  },
281
301
  ArrowRight: function ArrowRight(state, dispatch) {
282
302
  var accepted = acceptGhostText(state, dispatch);
283
- if (accepted) justAccepted = true;
303
+ if (accepted) {
304
+ var _api$analytics3;
305
+ justAccepted = true;
306
+ lastShownGhostText = '';
307
+ api === null || api === void 0 || (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 || _api$analytics3.actions.fireAnalyticsEvent({
308
+ action: _analytics.ACTION.SUGGESTION_INSERTED,
309
+ actionSubject: _analytics.ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
310
+ eventType: _analytics.EVENT_TYPE.TRACK
311
+ });
312
+ }
284
313
  return accepted;
285
314
  },
286
315
  Escape: function Escape(state, dispatch) {
@@ -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,
@@ -1,6 +1,7 @@
1
1
  import { autocompletePluginKey, createAutocompletePlugin } from './pm-plugins/autocomplete-plugin';
2
2
  export const autocompletePlugin = ({
3
- config: options
3
+ config: options,
4
+ api
4
5
  }) => {
5
6
  return {
6
7
  name: 'autocomplete',
@@ -13,7 +14,7 @@ export const autocompletePlugin = ({
13
14
  pmPlugins() {
14
15
  return [{
15
16
  name: 'autocomplete',
16
- plugin: () => createAutocompletePlugin(options)
17
+ plugin: () => createAutocompletePlugin(options, api)
17
18
  }];
18
19
  }
19
20
  };
@@ -1,3 +1,4 @@
1
+ import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
1
2
  import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
2
3
  import { keydownHandler } from '@atlaskit/editor-prosemirror/keymap';
3
4
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
@@ -58,6 +59,7 @@ const setAutocompleteMeta = (tr, meta) => {
58
59
  /**
59
60
  * Apply a ghost text suggestion to the editor state.
60
61
  */
62
+ let lastShownGhostText = '';
61
63
  const showGhostText = (view, text, position) => {
62
64
  const {
63
65
  state,
@@ -140,7 +142,7 @@ const buildSlowLaneText = (docText, context) => {
140
142
  lines.push(`reply ${nextReplyNumber}: ${docText}`);
141
143
  return lines.join('\n');
142
144
  };
143
- export const createAutocompletePlugin = options => {
145
+ export const createAutocompletePlugin = (options, api) => {
144
146
  let debounceTimer = null;
145
147
  let hasIngestedPage = false;
146
148
  let resolvedContext;
@@ -202,6 +204,15 @@ export const createAutocompletePlugin = options => {
202
204
  const prediction = predict(textBefore);
203
205
  if (prediction && prediction.length > 0) {
204
206
  showGhostText(view, prediction, selection.from);
207
+ if (prediction !== lastShownGhostText) {
208
+ var _api$analytics;
209
+ lastShownGhostText = prediction;
210
+ api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.fireAnalyticsEvent({
211
+ action: ACTION.SUGGESTION_VIEWED,
212
+ actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
213
+ eventType: EVENT_TYPE.TRACK
214
+ });
215
+ }
205
216
  }
206
217
  }, DEBOUNCE_MS);
207
218
  };
@@ -276,12 +287,30 @@ export const createAutocompletePlugin = options => {
276
287
  handleKeyDown: keydownHandler({
277
288
  Tab: (state, dispatch) => {
278
289
  const accepted = acceptGhostText(state, dispatch);
279
- if (accepted) justAccepted = true;
290
+ if (accepted) {
291
+ var _api$analytics2;
292
+ justAccepted = true;
293
+ lastShownGhostText = '';
294
+ api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions.fireAnalyticsEvent({
295
+ action: ACTION.SUGGESTION_INSERTED,
296
+ actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
297
+ eventType: EVENT_TYPE.TRACK
298
+ });
299
+ }
280
300
  return accepted;
281
301
  },
282
302
  ArrowRight: (state, dispatch) => {
283
303
  const accepted = acceptGhostText(state, dispatch);
284
- if (accepted) justAccepted = true;
304
+ if (accepted) {
305
+ var _api$analytics3;
306
+ justAccepted = true;
307
+ lastShownGhostText = '';
308
+ api === null || api === void 0 ? void 0 : (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : _api$analytics3.actions.fireAnalyticsEvent({
309
+ action: ACTION.SUGGESTION_INSERTED,
310
+ actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
311
+ eventType: EVENT_TYPE.TRACK
312
+ });
313
+ }
285
314
  return accepted;
286
315
  },
287
316
  Escape: (state, dispatch) => {
@@ -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
  }
@@ -1,6 +1,7 @@
1
1
  import { autocompletePluginKey, createAutocompletePlugin } from './pm-plugins/autocomplete-plugin';
2
2
  export var autocompletePlugin = function autocompletePlugin(_ref) {
3
- var options = _ref.config;
3
+ var options = _ref.config,
4
+ api = _ref.api;
4
5
  return {
5
6
  name: 'autocomplete',
6
7
  getSharedState: function getSharedState(editorState) {
@@ -13,7 +14,7 @@ export var autocompletePlugin = function autocompletePlugin(_ref) {
13
14
  return [{
14
15
  name: 'autocomplete',
15
16
  plugin: function plugin() {
16
- return createAutocompletePlugin(options);
17
+ return createAutocompletePlugin(options, api);
17
18
  }
18
19
  }];
19
20
  }