@atlaskit/editor-plugin-autocomplete 9.0.0 → 9.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.
@@ -4,7 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
4
4
  Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
- exports.predict = exports.noteSuggestionAccepted = exports.loadVectorsAsync = exports.loadPhraseArtifacts = exports.loadDefaultVocabulary = exports.initVocabulary = exports.initVectors = exports.initPhrases = exports.initL3Vocabulary = exports.ingestDocumentPage = exports.incrementSessionFreq = exports.getPredictorStatus = exports.getLastPredictionDebug = void 0;
7
+ exports.resetSessionBoosts = exports.predict = exports.noteSuggestionAccepted = exports.lookupVocabularySource = exports.loadVectorsAsync = exports.loadPhraseArtifacts = exports.loadDefaultVocabulary = exports.isSurfaceInAcceptCooldown = exports.inspectSessionBoosts = exports.initVocabulary = exports.initVectors = exports.initPhrases = exports.initL3Vocabulary = exports.ingestDocumentPage = exports.incrementSessionFreq = exports.getPredictorStatus = exports.getLastPredictionOutcome = exports.getLastPredictionDebug = void 0;
8
8
  var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
9
9
  var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
10
10
  var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
@@ -38,7 +38,10 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
38
38
  * Falls back to cold mode (freq-only) when vectors not yet loaded.
39
39
  *
40
40
  * Session personalization (L1): words the user types are incrementally boosted
41
- * via incrementSessionFreq(), called on word boundaries from the plugin.
41
+ * via incrementSessionFreq(), called on word boundaries from the plugin, and
42
+ * words in ingested context text via ingestDocumentPage(). What the session has
43
+ * boosted is visible at any time from the console: `__atlCtcDebug__.session()`,
44
+ * or `__atlCtcDebug__.session('poll')` for one family — see inspectSessionBoosts.
42
45
  */
43
46
  // ─── Constants ───────────────────────────────────────────────────────────────
44
47
 
@@ -188,6 +191,18 @@ var DEBUG_TEXT_TAIL_CHARS = 120;
188
191
  * is vocabulary coverage.
189
192
  */
190
193
 
194
+ /**
195
+ * What the scored path concluded, recorded on every evaluation whether or not
196
+ * debug is on.
197
+ *
198
+ * This exists for the inline-code harvester, which may only offer a harvested
199
+ * surface once the scored path has finished and come away empty. The reason is
200
+ * the load-bearing part: `no-candidate` means no vocabulary reaches this prefix
201
+ * at all, while `winner-margin` means two known words the model cannot yet
202
+ * separate — the first is a gap worth filling and the second is a prefix
203
+ * ambiguous enough that filling it would be a guess.
204
+ */
205
+
191
206
  /**
192
207
  * A candidate paired with the length of the already-typed prefix it completes.
193
208
  * For a single word this is the current partial token length; for a phrase it
@@ -215,6 +230,17 @@ var WeightedWordTrie = /*#__PURE__*/function () {
215
230
  function WeightedWordTrie() {
216
231
  (0, _classCallCheck2.default)(this, WeightedWordTrie);
217
232
  (0, _defineProperty2.default)(this, "root", new TrieNode());
233
+ /**
234
+ * Every node a session boost has been written to.
235
+ *
236
+ * Kept because dropping the boosts is no longer a rare event — it happens
237
+ * each time the reader changes page or conversation — and walking a vocabulary
238
+ * of tens of thousands of words to find the few hundred that were touched
239
+ * costs about 10ms of main thread every time. The two writers below are the
240
+ * only way a `sessionFreq` moves, so keeping this in step costs one Set
241
+ * insertion on a path that is already descending the trie.
242
+ */
243
+ (0, _defineProperty2.default)(this, "boostedNodes", new Set());
218
244
  /** Highest tenantFreq seen — used to normalize freq scores at query time */
219
245
  (0, _defineProperty2.default)(this, "maxTenantFreq", 1);
220
246
  }
@@ -331,6 +357,13 @@ var WeightedWordTrie = /*#__PURE__*/function () {
331
357
  return node.word !== null ? node : null;
332
358
  }
333
359
 
360
+ /** Whether this exact surface is stored as a terminal word. */
361
+ }, {
362
+ key: "hasWord",
363
+ value: function hasWord(word) {
364
+ return this.findNode(word) !== null;
365
+ }
366
+
334
367
  /**
335
368
  * Set the session frequency for a word.
336
369
  * Returns true if the word exists in the trie.
@@ -343,6 +376,7 @@ var WeightedWordTrie = /*#__PURE__*/function () {
343
376
  return false;
344
377
  }
345
378
  node.sessionFreq = count;
379
+ this.boostedNodes.add(node);
346
380
  return true;
347
381
  }
348
382
 
@@ -358,8 +392,90 @@ var WeightedWordTrie = /*#__PURE__*/function () {
358
392
  return false;
359
393
  }
360
394
  node.sessionFreq += 1;
395
+ this.boostedNodes.add(node);
361
396
  return true;
362
397
  }
398
+
399
+ /**
400
+ * Zero every session boost, in the number of words boosted rather than the
401
+ * number of words known.
402
+ */
403
+ }, {
404
+ key: "clearSessionBoosts",
405
+ value: function clearSessionBoosts() {
406
+ var _iterator5 = _createForOfIteratorHelper(this.boostedNodes),
407
+ _step5;
408
+ try {
409
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
410
+ var node = _step5.value;
411
+ node.sessionFreq = 0;
412
+ }
413
+ } catch (err) {
414
+ _iterator5.e(err);
415
+ } finally {
416
+ _iterator5.f();
417
+ }
418
+ this.boostedNodes.clear();
419
+ }
420
+
421
+ /**
422
+ * Every word carrying a session boost, optionally limited to one prefix's
423
+ * subtree. Unlike `getCandidates` a word equal to the prefix is included,
424
+ * since the question here is what the session holds rather than what could
425
+ * still be typed.
426
+ *
427
+ * Walks the trie rather than reading `boostedNodes`, since a prefix answer is
428
+ * a subtree question and this only runs when a human asks it.
429
+ */
430
+ }, {
431
+ key: "collectSessionBoosted",
432
+ value: function collectSessionBoosted() {
433
+ var prefix = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
434
+ var node = this.root;
435
+ var _iterator6 = _createForOfIteratorHelper(prefix.toLowerCase()),
436
+ _step6;
437
+ try {
438
+ for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
439
+ var char = _step6.value;
440
+ var next = node.children.get(char);
441
+ if (!next) {
442
+ return [];
443
+ }
444
+ node = next;
445
+ }
446
+ } catch (err) {
447
+ _iterator6.e(err);
448
+ } finally {
449
+ _iterator6.f();
450
+ }
451
+ var boosted = [];
452
+ var stack = [node];
453
+ while (stack.length > 0) {
454
+ var current = stack.pop();
455
+ if (!current) {
456
+ continue;
457
+ }
458
+ if (current.word !== null && current.sessionFreq > 0) {
459
+ boosted.push({
460
+ word: current.word,
461
+ node: current
462
+ });
463
+ }
464
+ var _iterator7 = _createForOfIteratorHelper(current.children.values()),
465
+ _step7;
466
+ try {
467
+ for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
468
+ var child = _step7.value;
469
+ stack.push(child);
470
+ }
471
+ } catch (err) {
472
+ _iterator7.e(err);
473
+ } finally {
474
+ _iterator7.f();
475
+ }
476
+ }
477
+ return boosted;
478
+ }
363
479
  }]);
364
480
  }(); // L1/L2 Trie (Session + Atlassian Domain)
365
481
  var wordTrie = new WeightedWordTrie();
@@ -381,19 +497,19 @@ var phraseTrie = new WeightedWordTrie();
381
497
  * expects a simple array of strings: ["about", "above", "actually", ...]
382
498
  */
383
499
  var initL3Vocabulary = exports.initL3Vocabulary = function initL3Vocabulary(l3Words) {
384
- var _iterator5 = _createForOfIteratorHelper(l3Words),
385
- _step5;
500
+ var _iterator8 = _createForOfIteratorHelper(l3Words),
501
+ _step8;
386
502
  try {
387
- for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
388
- var word = _step5.value;
503
+ for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
504
+ var word = _step8.value;
389
505
  // Insert with a tiny baseline frequency so it mathematically
390
506
  // loses to any domain word in Stage 1, but still scores above 0.
391
507
  l3Trie.insert(word, L3_BASELINE_FREQ, 0, 0);
392
508
  }
393
509
  } catch (err) {
394
- _iterator5.e(err);
510
+ _iterator8.e(err);
395
511
  } finally {
396
- _iterator5.f();
512
+ _iterator8.f();
397
513
  }
398
514
  recallGeneration++;
399
515
  (0, _debugMode.ctcTag)('init', "L3 general English loaded: ".concat(l3Words.length, " words"));
@@ -419,6 +535,15 @@ var phraseTermCount = 0;
419
535
  var maxBigramFreq = 1;
420
536
  var maxPhraseFreq = 1;
421
537
  var lastPredictionDebug = null;
538
+ var lastPredictionOutcome = null;
539
+ var recordPredictionOutcome = function recordPredictionOutcome(outcome) {
540
+ lastPredictionOutcome = outcome;
541
+ };
542
+
543
+ /** The verdict from the most recent `predict` call. Always populated. */
544
+ var getLastPredictionOutcome = exports.getLastPredictionOutcome = function getLastPredictionOutcome() {
545
+ return lastPredictionOutcome;
546
+ };
422
547
 
423
548
  // ── Stabilization (QI-2): post-accept cooldown + whole-surface repetition ────
424
549
  // Two guards that stop the accept→echo (`end to end` → `end to end to end`) and
@@ -450,6 +575,22 @@ var noteSuggestionAccepted = exports.noteSuggestionAccepted = function noteSugge
450
575
  }
451
576
  };
452
577
 
578
+ /**
579
+ * Whether `surface` is the one the user just accepted and is still inside its
580
+ * cooldown window.
581
+ *
582
+ * Read-only, unlike the advance inside `predict`: the cooldown is measured in
583
+ * predictions, and a caller asking whether it is active must not consume one of
584
+ * them. Exported for the harvest path, which displays without going through
585
+ * arbitration and so would otherwise re-offer what was just accepted.
586
+ */
587
+ var isSurfaceInAcceptCooldown = exports.isSurfaceInAcceptCooldown = function isSurfaceInAcceptCooldown(surface) {
588
+ if (!acceptCooldown || acceptCooldown.surface !== surface.trim().toLowerCase()) {
589
+ return false;
590
+ }
591
+ return acceptCooldown.predictionsSince <= COOLDOWN_KEYSTROKES || performance.now() - acceptCooldown.ts < COOLDOWN_MS;
592
+ };
593
+
453
594
  /** Get vector for a word from the store. */
454
595
  var getWordVector = function getWordVector(word) {
455
596
  if (!vectorStore) {
@@ -474,20 +615,20 @@ var computeContextVectorLocal = function computeContextVectorLocal(textBefore) {
474
615
  var tokens = tokenize(textBefore);
475
616
  var words = tokens.slice(-CONTEXT_WORDS);
476
617
  var vectors = [];
477
- var _iterator6 = _createForOfIteratorHelper(words),
478
- _step6;
618
+ var _iterator9 = _createForOfIteratorHelper(words),
619
+ _step9;
479
620
  try {
480
- for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
481
- var word = _step6.value;
621
+ for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
622
+ var word = _step9.value;
482
623
  var _v = getWordVector(word);
483
624
  if (_v) {
484
625
  vectors.push(_v);
485
626
  }
486
627
  }
487
628
  } catch (err) {
488
- _iterator6.e(err);
629
+ _iterator9.e(err);
489
630
  } finally {
490
- _iterator6.f();
631
+ _iterator9.f();
491
632
  }
492
633
  if (vectors.length === 0) {
493
634
  return null;
@@ -520,11 +661,11 @@ var getContextVectorForScoring = function getContextVectorForScoring(textBefore)
520
661
  var tokenize = function tokenize(text) {
521
662
  var tokens = [];
522
663
  // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
523
- var _iterator7 = _createForOfIteratorHelper(text.toLowerCase().split(WHITESPACE_SPLIT_REGEX)),
524
- _step7;
664
+ var _iterator0 = _createForOfIteratorHelper(text.toLowerCase().split(WHITESPACE_SPLIT_REGEX)),
665
+ _step0;
525
666
  try {
526
- for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
527
- var raw = _step7.value;
667
+ for (_iterator0.s(); !(_step0 = _iterator0.n()).done;) {
668
+ var raw = _step0.value;
528
669
  // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
529
670
  var clean = raw.replace(PUNCTUATION_BOUNDARY_REGEX, '');
530
671
  if (clean.length >= 2) {
@@ -532,9 +673,9 @@ var tokenize = function tokenize(text) {
532
673
  }
533
674
  }
534
675
  } catch (err) {
535
- _iterator7.e(err);
676
+ _iterator0.e(err);
536
677
  } finally {
537
- _iterator7.f();
678
+ _iterator0.f();
538
679
  }
539
680
  return tokens;
540
681
  };
@@ -638,11 +779,11 @@ var getPhraseCandidates = function getPhraseCandidates(trimmed) {
638
779
  window: windowPrefix,
639
780
  matches: matches.length
640
781
  });
641
- var _iterator8 = _createForOfIteratorHelper(matches),
642
- _step8;
782
+ var _iterator1 = _createForOfIteratorHelper(matches),
783
+ _step1;
643
784
  try {
644
- for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
645
- var _match = _step8.value;
785
+ for (_iterator1.s(); !(_step1 = _iterator1.n()).done;) {
786
+ var _match = _step1.value;
646
787
  if (seen.has(_match.word)) {
647
788
  continue;
648
789
  }
@@ -655,9 +796,9 @@ var getPhraseCandidates = function getPhraseCandidates(trimmed) {
655
796
  });
656
797
  }
657
798
  } catch (err) {
658
- _iterator8.e(err);
799
+ _iterator1.e(err);
659
800
  } finally {
660
- _iterator8.f();
801
+ _iterator1.f();
661
802
  }
662
803
  }
663
804
  logPhrasePath();
@@ -702,17 +843,17 @@ var getLastPredictionDebug = exports.getLastPredictionDebug = function getLastPr
702
843
  return lastPredictionDebug;
703
844
  };
704
845
  var initVocabulary = exports.initVocabulary = function initVocabulary(vocabulary) {
705
- var _iterator9 = _createForOfIteratorHelper(vocabulary.terms),
706
- _step9;
846
+ var _iterator10 = _createForOfIteratorHelper(vocabulary.terms),
847
+ _step10;
707
848
  try {
708
- for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
709
- var term = _step9.value;
849
+ for (_iterator10.s(); !(_step10 = _iterator10.n()).done;) {
850
+ var term = _step10.value;
710
851
  wordTrie.insert(term.word, term.freq, term.docFreq, term.authorFreq);
711
852
  }
712
853
  } catch (err) {
713
- _iterator9.e(err);
854
+ _iterator10.e(err);
714
855
  } finally {
715
- _iterator9.f();
856
+ _iterator10.f();
716
857
  }
717
858
  isInitialized = true;
718
859
  recallGeneration++;
@@ -763,6 +904,36 @@ var incrementSessionFreq = exports.incrementSessionFreq = function incrementSess
763
904
  wordTrie.incrementSessionFreq(word);
764
905
  };
765
906
 
907
+ /**
908
+ * Drop every L1 boost this session has accumulated.
909
+ *
910
+ * The vocabulary itself is left alone: only `sessionFreq` is cleared, so the
911
+ * tenant and generic frequencies a boost was sitting on top of survive. Called
912
+ * when the plugin decides the session it was learning for has ended — a new
913
+ * conversation, or a different page — since a boost is a claim about what is
914
+ * being discussed and that claim does not carry over.
915
+ */
916
+ var resetSessionBoosts = exports.resetSessionBoosts = function resetSessionBoosts() {
917
+ wordTrie.clearSessionBoosts();
918
+ // Anything memoized against the old boosts is now describing a session that
919
+ // no longer exists.
920
+ recallGeneration++;
921
+ };
922
+
923
+ /**
924
+ * Which vocabulary already holds this surface, if any.
925
+ *
926
+ * Used by the inline-code harvester to drop terms the scored path can already
927
+ * serve, so that harvesting stays limited to words with no route to a
928
+ * suggestion today.
929
+ */
930
+ var lookupVocabularySource = exports.lookupVocabularySource = function lookupVocabularySource(word) {
931
+ if (wordTrie.hasWord(word)) {
932
+ return 'l2';
933
+ }
934
+ return l3Trie.hasWord(word) ? 'l3' : null;
935
+ };
936
+
766
937
  /**
767
938
  * Prime session frequencies from a document page string.
768
939
  *
@@ -779,23 +950,23 @@ var ingestDocumentPage = exports.ingestDocumentPage = function ingestDocumentPag
779
950
  }
780
951
  var words = tokenize(pageContent);
781
952
  var validBoostedWords = new Set();
782
- var _iterator0 = _createForOfIteratorHelper(words),
783
- _step0;
953
+ var _iterator11 = _createForOfIteratorHelper(words),
954
+ _step11;
784
955
  try {
785
- for (_iterator0.s(); !(_step0 = _iterator0.n()).done;) {
786
- var word = _step0.value;
956
+ for (_iterator11.s(); !(_step11 = _iterator11.n()).done;) {
957
+ var word = _step11.value;
787
958
  var didBoost = wordTrie.incrementSessionFreq(word);
788
959
  if (didBoost) {
789
960
  validBoostedWords.add(word);
790
961
  }
791
962
  }
792
963
  } catch (err) {
793
- _iterator0.e(err);
964
+ _iterator11.e(err);
794
965
  } finally {
795
- _iterator0.f();
966
+ _iterator11.f();
796
967
  }
797
968
  if ((0, _debugMode.isAutocompleteDebugEnabled)() && validBoostedWords.size > 0) {
798
- (0, _debugMode.ctcTag)('init', "L1 session primed ".concat(validBoostedWords.size, " words from page"), _debugMode.CTC_STYLES.brand);
969
+ (0, _debugMode.ctcTag)('init', "L1 session primed ".concat(validBoostedWords.size, " words from page \xB7 __atlCtcDebug__.session() to inspect"), _debugMode.CTC_STYLES.brand);
799
970
  if ((0, _debugMode.isAutocompleteDebugVerbose)()) {
800
971
  // eslint-disable-next-line no-console
801
972
  console.dir(Array.from(validBoostedWords).sort());
@@ -803,6 +974,57 @@ var ingestDocumentPage = exports.ingestDocumentPage = function ingestDocumentPag
803
974
  }
804
975
  };
805
976
 
977
+ /**
978
+ * How many boosted words `inspectSessionBoosts` lists.
979
+ *
980
+ * A page ingest can boost thousands, and a list that long is not read. The
981
+ * strongest boosts are the ones that change an ordering, and `boosted` still
982
+ * reports the full size, so the cap loses nothing but volume.
983
+ */
984
+ var MAX_LISTED_SESSION_WORDS = 100;
985
+ /**
986
+ * Read the session's L1 boosts, optionally narrowed to a prefix.
987
+ *
988
+ * Installed as `__atlCtcDebug__.session()`, with `__atlCtcDebug__.session('poll')`
989
+ * to ask about one family. Returned rather than logged, so the console renders it
990
+ * as an inspectable object and a caller can assert on it.
991
+ *
992
+ * Only words the vocabulary already holds can carry a boost, because both writers
993
+ * go through `incrementSessionFreq` and it only finds existing nodes. An ingested
994
+ * word absent from the vocabulary is therefore missing from here and always will
995
+ * be — that gap is what the inline-code harvester covers, and those surfaces show
996
+ * up under `__atlCtcDebug__.harvest()` instead.
997
+ */
998
+ var inspectSessionBoosts = exports.inspectSessionBoosts = function inspectSessionBoosts(prefix) {
999
+ var boosted = wordTrie.collectSessionBoosted(prefix !== null && prefix !== void 0 ? prefix : '');
1000
+ return _objectSpread(_objectSpread({
1001
+ boosted: boosted.length,
1002
+ limit: MAX_LISTED_SESSION_WORDS
1003
+ }, prefix === undefined ? {} : {
1004
+ prefix: prefix
1005
+ }), {}, {
1006
+ words: boosted.sort(function (a, b) {
1007
+ return b.node.sessionFreq - a.node.sessionFreq || a.word.localeCompare(b.word, 'en', {
1008
+ numeric: true,
1009
+ sensitivity: 'base'
1010
+ });
1011
+ }).slice(0, MAX_LISTED_SESSION_WORDS).map(function (_ref) {
1012
+ var node = _ref.node,
1013
+ word = _ref.word;
1014
+ return {
1015
+ sessionFreq: node.sessionFreq,
1016
+ sessionOnly: node.tenantFreq === 0,
1017
+ surface: word,
1018
+ tenantFreq: node.tenantFreq
1019
+ };
1020
+ })
1021
+ });
1022
+ };
1023
+
1024
+ // At module scope so the console answers before the first keystroke, which is
1025
+ // when someone reaching for it usually asks.
1026
+ (0, _debugMode.registerCtcSessionInspector)(inspectSessionBoosts);
1027
+
806
1028
  /**
807
1029
  * Result of a prediction: the ghost tail to insert plus an immutable record of
808
1030
  * the evidence that authorized the UI commitment.
@@ -834,20 +1056,20 @@ var computeCanonicalRecall = function computeCanonicalRecall(trimmed, currentWor
834
1056
  var existingWords = new Set(wordCandidates.map(function (c) {
835
1057
  return c.word;
836
1058
  }));
837
- var _iterator1 = _createForOfIteratorHelper(l3Candidates),
838
- _step1;
1059
+ var _iterator12 = _createForOfIteratorHelper(l3Candidates),
1060
+ _step12;
839
1061
  try {
840
- for (_iterator1.s(); !(_step1 = _iterator1.n()).done;) {
841
- var l3c = _step1.value;
1062
+ for (_iterator12.s(); !(_step12 = _iterator12.n()).done;) {
1063
+ var l3c = _step12.value;
842
1064
  if (wordCandidates.length >= MAX_CANDIDATES) break;
843
1065
  if (!existingWords.has(l3c.word)) {
844
1066
  wordCandidates.push(l3c);
845
1067
  }
846
1068
  }
847
1069
  } catch (err) {
848
- _iterator1.e(err);
1070
+ _iterator12.e(err);
849
1071
  } finally {
850
- _iterator1.f();
1072
+ _iterator12.f();
851
1073
  }
852
1074
  }
853
1075
 
@@ -859,9 +1081,9 @@ var computeCanonicalRecall = function computeCanonicalRecall(trimmed, currentWor
859
1081
  // Unify: a word completes the current partial token; a phrase completes its
860
1082
  // matched multi-word window. Track the prefix length per term so the ghost
861
1083
  // tail is sliced correctly regardless of term type.
862
- var matched = [].concat((0, _toConsumableArray2.default)(wordCandidates.map(function (_ref) {
863
- var word = _ref.word,
864
- node = _ref.node;
1084
+ var matched = [].concat((0, _toConsumableArray2.default)(wordCandidates.map(function (_ref2) {
1085
+ var word = _ref2.word,
1086
+ node = _ref2.node;
865
1087
  return {
866
1088
  word: word,
867
1089
  node: node,
@@ -874,19 +1096,19 @@ var computeCanonicalRecall = function computeCanonicalRecall(trimmed, currentWor
874
1096
  return _objectSpread(_objectSpread({}, candidate), (0, _canonicalLmScoring.deriveCanonicalCandidateContext)(trimmed, candidate.matchedPrefixLen, candidate.word, _slowLaneClient.getCanonicalSurfaceTokenIds, candidate.surfaceStart, positionCache));
875
1097
  });
876
1098
  var prefixLenByWord = new Map();
877
- var _iterator10 = _createForOfIteratorHelper(canonicalMatched),
878
- _step10;
1099
+ var _iterator13 = _createForOfIteratorHelper(canonicalMatched),
1100
+ _step13;
879
1101
  try {
880
- for (_iterator10.s(); !(_step10 = _iterator10.n()).done;) {
881
- var m = _step10.value;
1102
+ for (_iterator13.s(); !(_step13 = _iterator13.n()).done;) {
1103
+ var m = _step13.value;
882
1104
  if (!prefixLenByWord.has(m.word)) {
883
1105
  prefixLenByWord.set(m.word, m.matchedPrefixLen);
884
1106
  }
885
1107
  }
886
1108
  } catch (err) {
887
- _iterator10.e(err);
1109
+ _iterator13.e(err);
888
1110
  } finally {
889
- _iterator10.f();
1111
+ _iterator13.f();
890
1112
  }
891
1113
  return {
892
1114
  canonicalMatched: canonicalMatched,
@@ -925,6 +1147,14 @@ var predict = exports.predict = function predict(textBefore) {
925
1147
  void loadDefaultVocabulary({
926
1148
  source: 'predict'
927
1149
  }).catch(function () {});
1150
+ // Awaiting, not empty-handed: with no vocabulary loaded the harvester's own
1151
+ // intake filter has not been applied to anything either.
1152
+ recordPredictionOutcome({
1153
+ abstainReason: 'not-initialized',
1154
+ awaitingAsyncEvidence: true,
1155
+ scoredCandidateCount: 0,
1156
+ textBefore: textBefore
1157
+ });
928
1158
  return null;
929
1159
  }
930
1160
  var t0 = performance.now();
@@ -953,11 +1183,23 @@ var predict = exports.predict = function predict(textBefore) {
953
1183
  priority: 0
954
1184
  }));
955
1185
  }
1186
+ recordPredictionOutcome({
1187
+ abstainReason: 'prefetch',
1188
+ awaitingAsyncEvidence: true,
1189
+ scoredCandidateCount: 0,
1190
+ textBefore: textBefore
1191
+ });
956
1192
  return null;
957
1193
  }
958
1194
  var trimmed = textBefore.trimEnd();
959
1195
  var trailingSurfaceToken = (_trimmed$match$ = (_trimmed$match = trimmed.match(TRAILING_SURFACE_TOKEN_REGEX)) === null || _trimmed$match === void 0 ? void 0 : _trimmed$match[0]) !== null && _trimmed$match$ !== void 0 ? _trimmed$match$ : '';
960
1196
  if (trailingSurfaceToken.length === 0) {
1197
+ recordPredictionOutcome({
1198
+ abstainReason: 'no-surface-token',
1199
+ awaitingAsyncEvidence: false,
1200
+ scoredCandidateCount: 0,
1201
+ textBefore: textBefore
1202
+ });
961
1203
  return null;
962
1204
  }
963
1205
  var currentWord = trailingSurfaceToken;
@@ -968,6 +1210,14 @@ var predict = exports.predict = function predict(textBefore) {
968
1210
 
969
1211
  // If every trie was empty for this prefix
970
1212
  if (canonicalMatched.length === 0) {
1213
+ // Terminal, and the only verdict that says the vocabulary has no claim on
1214
+ // this prefix at all — which is what makes it the harvester's cue.
1215
+ recordPredictionOutcome({
1216
+ abstainReason: 'no-candidate',
1217
+ awaitingAsyncEvidence: false,
1218
+ scoredCandidateCount: 0,
1219
+ textBefore: textBefore
1220
+ });
971
1221
  if ((0, _debugMode.isAutocompleteDebugEnabled)()) {
972
1222
  // eslint-disable-next-line no-console
973
1223
  console.log("%c[CTC]%c \u2014 abstain: no matches for \"".concat(currentWord, "\""), _debugMode.CTC_STYLES.brand, _debugMode.CTC_STYLES.body);
@@ -980,9 +1230,9 @@ var predict = exports.predict = function predict(textBefore) {
980
1230
  var mode = contextVector ? 'warm' : 'cold';
981
1231
 
982
1232
  // Build ScoringCandidate array from matched terms (words + phrases)
983
- var scoringCandidates = canonicalMatched.map(function (_ref2) {
984
- var word = _ref2.word,
985
- node = _ref2.node;
1233
+ var scoringCandidates = canonicalMatched.map(function (_ref3) {
1234
+ var word = _ref3.word,
1235
+ node = _ref3.node;
986
1236
  return {
987
1237
  word: word,
988
1238
  tenantFreq: node.tenantFreq,
@@ -1002,9 +1252,9 @@ var predict = exports.predict = function predict(textBefore) {
1002
1252
  var currentWordSeparator = (_canonicalMatched$fin = canonicalMatched.find(function (candidate) {
1003
1253
  return candidate.node.termType === 'word';
1004
1254
  })) === null || _canonicalMatched$fin === void 0 ? void 0 : _canonicalMatched$fin.separatorKind;
1005
- var prefixLmLogits = lmLogits && currentWordSeparator === 'whitespace' ? Object.fromEntries(Object.entries(lmLogits).filter(function (_ref3) {
1006
- var _ref4 = (0, _slicedToArray2.default)(_ref3, 1),
1007
- word = _ref4[0];
1255
+ var prefixLmLogits = lmLogits && currentWordSeparator === 'whitespace' ? Object.fromEntries(Object.entries(lmLogits).filter(function (_ref4) {
1256
+ var _ref5 = (0, _slicedToArray2.default)(_ref4, 1),
1257
+ word = _ref5[0];
1008
1258
  return word.startsWith(prefix);
1009
1259
  })) : null;
1010
1260
  var canonicalScoringSupported = (0, _slowLaneClient.isCanonicalSurfaceScoringSupported)();
@@ -1015,29 +1265,29 @@ var predict = exports.predict = function predict(textBefore) {
1015
1265
  }))).sort();
1016
1266
  var familyKey = eligibleContextKeys.join("\x01");
1017
1267
  var primeRequests = (0, _canonicalLmScoring.selectBoundaryPrimeRequests)(familyKey, canonicalMatched, PHRASE_MAX_WORDS);
1018
- var _iterator11 = _createForOfIteratorHelper(primeRequests),
1019
- _step11;
1268
+ var _iterator14 = _createForOfIteratorHelper(primeRequests),
1269
+ _step14;
1020
1270
  try {
1021
- for (_iterator11.s(); !(_step11 = _iterator11.n()).done;) {
1022
- var request = _step11.value;
1271
+ for (_iterator14.s(); !(_step14 = _iterator14.n()).done;) {
1272
+ var request = _step14.value;
1023
1273
  (0, _slowLaneClient.primeBoundaryLm)(request);
1024
1274
  }
1025
1275
  } catch (err) {
1026
- _iterator11.e(err);
1276
+ _iterator14.e(err);
1027
1277
  } finally {
1028
- _iterator11.f();
1278
+ _iterator14.f();
1029
1279
  }
1030
1280
  var runtimeBySurface = new Map(canonicalMatched.map(function (candidate) {
1031
1281
  return [candidate.word, candidate];
1032
1282
  }));
1033
1283
  var canonicalEvidence = new Map();
1034
1284
  var firstTokenGroups = new Map();
1035
- var _iterator12 = _createForOfIteratorHelper(canonicalMatched),
1036
- _step12;
1285
+ var _iterator15 = _createForOfIteratorHelper(canonicalMatched),
1286
+ _step15;
1037
1287
  try {
1038
- for (_iterator12.s(); !(_step12 = _iterator12.n()).done;) {
1288
+ for (_iterator15.s(); !(_step15 = _iterator15.n()).done;) {
1039
1289
  var _firstTokenGroups$get;
1040
- var _candidate = _step12.value;
1290
+ var _candidate = _step15.value;
1041
1291
  if (_candidate.canonicalTokenIds === null) {
1042
1292
  continue;
1043
1293
  }
@@ -1065,28 +1315,28 @@ var predict = exports.predict = function predict(textBefore) {
1065
1315
  firstTokenGroups.set(_candidate.contextKey, _group2);
1066
1316
  }
1067
1317
  } catch (err) {
1068
- _iterator12.e(err);
1318
+ _iterator15.e(err);
1069
1319
  } finally {
1070
- _iterator12.f();
1320
+ _iterator15.f();
1071
1321
  }
1072
- var _iterator13 = _createForOfIteratorHelper(firstTokenGroups),
1073
- _step13;
1322
+ var _iterator16 = _createForOfIteratorHelper(firstTokenGroups),
1323
+ _step16;
1074
1324
  try {
1075
- for (_iterator13.s(); !(_step13 = _iterator13.n()).done;) {
1076
- var _step13$value = (0, _slicedToArray2.default)(_step13.value, 2),
1077
- _contextKey = _step13$value[0],
1078
- _group3 = _step13$value[1];
1325
+ for (_iterator16.s(); !(_step16 = _iterator16.n()).done;) {
1326
+ var _step16$value = (0, _slicedToArray2.default)(_step16.value, 2),
1327
+ _contextKey = _step16$value[0],
1328
+ _group3 = _step16$value[1];
1079
1329
  var boundary = (0, _slowLaneClient.getBoundaryLmState)(_contextKey);
1080
1330
  if (!boundary) {
1081
1331
  continue;
1082
1332
  }
1083
1333
  var maxLogit = -Infinity;
1084
- var _iterator20 = _createForOfIteratorHelper(_group3),
1085
- _step20;
1334
+ var _iterator23 = _createForOfIteratorHelper(_group3),
1335
+ _step23;
1086
1336
  try {
1087
- for (_iterator20.s(); !(_step20 = _iterator20.n()).done;) {
1337
+ for (_iterator23.s(); !(_step23 = _iterator23.n()).done;) {
1088
1338
  var _candidate2$canonical;
1089
- var _candidate2 = _step20.value;
1339
+ var _candidate2 = _step23.value;
1090
1340
  var tokenId = (_candidate2$canonical = _candidate2.canonicalTokenIds) === null || _candidate2$canonical === void 0 ? void 0 : _candidate2$canonical[0];
1091
1341
  var rawLogit = tokenId === undefined ? undefined : boundary.rawLogits[tokenId];
1092
1342
  if (rawLogit !== undefined && Number.isFinite(rawLogit) && rawLogit > maxLogit) {
@@ -1094,19 +1344,19 @@ var predict = exports.predict = function predict(textBefore) {
1094
1344
  }
1095
1345
  }
1096
1346
  } catch (err) {
1097
- _iterator20.e(err);
1347
+ _iterator23.e(err);
1098
1348
  } finally {
1099
- _iterator20.f();
1349
+ _iterator23.f();
1100
1350
  }
1101
1351
  if (!Number.isFinite(maxLogit)) {
1102
1352
  continue;
1103
1353
  }
1104
- var _iterator21 = _createForOfIteratorHelper(_group3),
1105
- _step21;
1354
+ var _iterator24 = _createForOfIteratorHelper(_group3),
1355
+ _step24;
1106
1356
  try {
1107
- for (_iterator21.s(); !(_step21 = _iterator21.n()).done;) {
1357
+ for (_iterator24.s(); !(_step24 = _iterator24.n()).done;) {
1108
1358
  var _candidate3$canonical, _candidate3$canonical2, _candidate3$canonical3;
1109
- var _candidate3 = _step21.value;
1359
+ var _candidate3 = _step24.value;
1110
1360
  var _tokenId = (_candidate3$canonical = _candidate3.canonicalTokenIds) === null || _candidate3$canonical === void 0 ? void 0 : _candidate3$canonical[0];
1111
1361
  var _rawLogit = _tokenId === undefined ? undefined : boundary.rawLogits[_tokenId];
1112
1362
  if (_rawLogit === undefined || !Number.isFinite(_rawLogit)) {
@@ -1145,15 +1395,15 @@ var predict = exports.predict = function predict(textBefore) {
1145
1395
  });
1146
1396
  }
1147
1397
  } catch (err) {
1148
- _iterator21.e(err);
1398
+ _iterator24.e(err);
1149
1399
  } finally {
1150
- _iterator21.f();
1400
+ _iterator24.f();
1151
1401
  }
1152
1402
  }
1153
1403
  } catch (err) {
1154
- _iterator13.e(err);
1404
+ _iterator16.e(err);
1155
1405
  } finally {
1156
- _iterator13.f();
1406
+ _iterator16.f();
1157
1407
  }
1158
1408
  var _rankCandidates = (0, _scoringPipeline.rankCandidates)(scoringCandidates, contextVector, function (w) {
1159
1409
  return getWordVector(w);
@@ -1174,12 +1424,12 @@ var predict = exports.predict = function predict(textBefore) {
1174
1424
  });
1175
1425
  if (canonicalScoringSupported && progressiveEligible.length > 0) {
1176
1426
  var byContext = new Map();
1177
- var _iterator14 = _createForOfIteratorHelper(progressiveEligible),
1178
- _step14;
1427
+ var _iterator17 = _createForOfIteratorHelper(progressiveEligible),
1428
+ _step17;
1179
1429
  try {
1180
- for (_iterator14.s(); !(_step14 = _iterator14.n()).done;) {
1430
+ for (_iterator17.s(); !(_step17 = _iterator17.n()).done;) {
1181
1431
  var _byContext$get;
1182
- var candidate = _step14.value;
1432
+ var candidate = _step17.value;
1183
1433
  var runtime = runtimeBySurface.get(candidate.word);
1184
1434
  if (!runtime || runtime.canonicalTokenIds === null) {
1185
1435
  continue;
@@ -1192,25 +1442,25 @@ var predict = exports.predict = function predict(textBefore) {
1192
1442
  byContext.set(runtime.contextKey, group);
1193
1443
  }
1194
1444
  } catch (err) {
1195
- _iterator14.e(err);
1445
+ _iterator17.e(err);
1196
1446
  } finally {
1197
- _iterator14.f();
1447
+ _iterator17.f();
1198
1448
  }
1199
- var _iterator15 = _createForOfIteratorHelper(byContext),
1200
- _step15;
1449
+ var _iterator18 = _createForOfIteratorHelper(byContext),
1450
+ _step18;
1201
1451
  try {
1202
- for (_iterator15.s(); !(_step15 = _iterator15.n()).done;) {
1203
- var _step15$value = (0, _slicedToArray2.default)(_step15.value, 2),
1204
- contextKey = _step15$value[0],
1205
- _group = _step15$value[1];
1452
+ for (_iterator18.s(); !(_step18 = _iterator18.n()).done;) {
1453
+ var _step18$value = (0, _slicedToArray2.default)(_step18.value, 2),
1454
+ contextKey = _step18$value[0],
1455
+ _group = _step18$value[1];
1206
1456
  (0, _slowLaneClient.requestProgressiveSurfaceScores)({
1207
1457
  familyKey: familyKey,
1208
1458
  contextKey: contextKey,
1209
1459
  prompt: _group[0].runtime.contextBeforeSurface,
1210
- candidates: _group.map(function (_ref5) {
1460
+ candidates: _group.map(function (_ref6) {
1211
1461
  var _runtime$canonicalTok;
1212
- var runtime = _ref5.runtime,
1213
- rankHint = _ref5.rankHint;
1462
+ var runtime = _ref6.runtime,
1463
+ rankHint = _ref6.rankHint;
1214
1464
  return {
1215
1465
  surface: runtime.word,
1216
1466
  tokenIds: (_runtime$canonicalTok = runtime.canonicalTokenIds) !== null && _runtime$canonicalTok !== void 0 ? _runtime$canonicalTok : [],
@@ -1220,9 +1470,9 @@ var predict = exports.predict = function predict(textBefore) {
1220
1470
  });
1221
1471
  }
1222
1472
  } catch (err) {
1223
- _iterator15.e(err);
1473
+ _iterator18.e(err);
1224
1474
  } finally {
1225
- _iterator15.f();
1475
+ _iterator18.f();
1226
1476
  }
1227
1477
  }
1228
1478
 
@@ -1311,12 +1561,12 @@ var predict = exports.predict = function predict(textBefore) {
1311
1561
  var contextRequestedCount = new Map();
1312
1562
  var bestTotalByContext = new Map();
1313
1563
  var bestCandidateByContext = new Map();
1314
- var _iterator16 = _createForOfIteratorHelper(ranked),
1315
- _step16;
1564
+ var _iterator19 = _createForOfIteratorHelper(ranked),
1565
+ _step19;
1316
1566
  try {
1317
- for (_iterator16.s(); !(_step16 = _iterator16.n()).done;) {
1567
+ for (_iterator19.s(); !(_step19 = _iterator19.n()).done;) {
1318
1568
  var _contextRequestedCoun2, _judged$total;
1319
- var _candidate4 = _step16.value;
1569
+ var _candidate4 = _step19.value;
1320
1570
  var _runtime = runtimeBySurface.get(_candidate4.word);
1321
1571
  if (!_runtime || _runtime.canonicalTokenIds === null) {
1322
1572
  continue;
@@ -1355,9 +1605,9 @@ var predict = exports.predict = function predict(textBefore) {
1355
1605
  * costs one lookup per space rather than a comparison against every member.
1356
1606
  */
1357
1607
  } catch (err) {
1358
- _iterator16.e(err);
1608
+ _iterator19.e(err);
1359
1609
  } finally {
1360
- _iterator16.f();
1610
+ _iterator19.f();
1361
1611
  }
1362
1612
  var extendsAPoolMember = function extendsAPoolMember(surface, pool) {
1363
1613
  for (var space = surface.indexOf(' '); space !== -1; space = surface.indexOf(' ', space + 1)) {
@@ -1392,22 +1642,22 @@ var predict = exports.predict = function predict(textBefore) {
1392
1642
  // it continues, because the cap alone would leave it looking exactly as
1393
1643
  // certain as its prefix while saying nothing about its own tail.
1394
1644
  var underReadExtensions = new Set();
1395
- var _iterator17 = _createForOfIteratorHelper(contextSurfaces),
1396
- _step17;
1645
+ var _iterator20 = _createForOfIteratorHelper(contextSurfaces),
1646
+ _step20;
1397
1647
  try {
1398
- for (_iterator17.s(); !(_step17 = _iterator17.n()).done;) {
1399
- var _step17$value = (0, _slicedToArray2.default)(_step17.value, 2),
1400
- _contextKey2 = _step17$value[0],
1401
- _surfaces = _step17$value[1];
1648
+ for (_iterator20.s(); !(_step20 = _iterator20.n()).done;) {
1649
+ var _step20$value = (0, _slicedToArray2.default)(_step20.value, 2),
1650
+ _contextKey2 = _step20$value[0],
1651
+ _surfaces = _step20$value[1];
1402
1652
  var pool = new Set(_surfaces);
1403
- var _iterator22 = _createForOfIteratorHelper((0, _toConsumableArray2.default)(_surfaces).sort(function (a, b) {
1653
+ var _iterator25 = _createForOfIteratorHelper((0, _toConsumableArray2.default)(_surfaces).sort(function (a, b) {
1404
1654
  return a.length - b.length;
1405
1655
  })),
1406
- _step22;
1656
+ _step25;
1407
1657
  try {
1408
- for (_iterator22.s(); !(_step22 = _iterator22.n()).done;) {
1658
+ for (_iterator25.s(); !(_step25 = _iterator25.n()).done;) {
1409
1659
  var _getProgressiveSurfac, _getProgressiveSurfac2, _runtimeBySurface$get8, _runtimeBySurface$get9;
1410
- var surface = _step22.value;
1660
+ var surface = _step25.value;
1411
1661
  var own = optimisticTotalByWord.get(surface);
1412
1662
  if (own === undefined) {
1413
1663
  continue;
@@ -1439,9 +1689,9 @@ var predict = exports.predict = function predict(textBefore) {
1439
1689
  }
1440
1690
  }
1441
1691
  } catch (err) {
1442
- _iterator22.e(err);
1692
+ _iterator25.e(err);
1443
1693
  } finally {
1444
- _iterator22.f();
1694
+ _iterator25.f();
1445
1695
  }
1446
1696
  }
1447
1697
 
@@ -1454,9 +1704,9 @@ var predict = exports.predict = function predict(textBefore) {
1454
1704
  * depths.
1455
1705
  */
1456
1706
  } catch (err) {
1457
- _iterator17.e(err);
1707
+ _iterator20.e(err);
1458
1708
  } finally {
1459
- _iterator17.f();
1709
+ _iterator20.f();
1460
1710
  }
1461
1711
  var judgedEvidenceFor = function judgedEvidenceFor(candidate) {
1462
1712
  return underReadExtensions.has(candidate.word) ? null : readJudgedEvidence(candidate);
@@ -1483,20 +1733,20 @@ var predict = exports.predict = function predict(textBefore) {
1483
1733
  // `contextTotals`, so how contested a context is still counts every scored
1484
1734
  // candidate.
1485
1735
  var logSumExpByContext = new Map();
1486
- var _iterator18 = _createForOfIteratorHelper(contextSurfaces),
1487
- _step18;
1736
+ var _iterator21 = _createForOfIteratorHelper(contextSurfaces),
1737
+ _step21;
1488
1738
  try {
1489
- for (_iterator18.s(); !(_step18 = _iterator18.n()).done;) {
1490
- var _step18$value = (0, _slicedToArray2.default)(_step18.value, 2),
1491
- _contextKey3 = _step18$value[0],
1492
- _surfaces2 = _step18$value[1];
1739
+ for (_iterator21.s(); !(_step21 = _iterator21.n()).done;) {
1740
+ var _step21$value = (0, _slicedToArray2.default)(_step21.value, 2),
1741
+ _contextKey3 = _step21$value[0],
1742
+ _surfaces2 = _step21$value[1];
1493
1743
  var _pool = new Set(_surfaces2);
1494
1744
  var minimalTotals = [];
1495
- var _iterator23 = _createForOfIteratorHelper(_surfaces2),
1496
- _step23;
1745
+ var _iterator26 = _createForOfIteratorHelper(_surfaces2),
1746
+ _step26;
1497
1747
  try {
1498
- for (_iterator23.s(); !(_step23 = _iterator23.n()).done;) {
1499
- var _surface = _step23.value;
1748
+ for (_iterator26.s(); !(_step26 = _iterator26.n()).done;) {
1749
+ var _surface = _step26.value;
1500
1750
  var total = optimisticTotalByWord.get(_surface);
1501
1751
  if (total !== undefined && !extendsAPoolMember(_surface, _pool)) {
1502
1752
  minimalTotals.push(total);
@@ -1505,9 +1755,9 @@ var predict = exports.predict = function predict(textBefore) {
1505
1755
  // An extension is strictly longer than what it extends, so the shortest
1506
1756
  // member of any non-empty pool is always minimal and this is never empty.
1507
1757
  } catch (err) {
1508
- _iterator23.e(err);
1758
+ _iterator26.e(err);
1509
1759
  } finally {
1510
- _iterator23.f();
1760
+ _iterator26.f();
1511
1761
  }
1512
1762
  logSumExpByContext.set(_contextKey3, (0, _canonicalLmScoring.logSumExp)(minimalTotals));
1513
1763
  }
@@ -1519,9 +1769,9 @@ var predict = exports.predict = function predict(textBefore) {
1519
1769
  * eventual posterior, or a verified total for the posterior itself.
1520
1770
  */
1521
1771
  } catch (err) {
1522
- _iterator18.e(err);
1772
+ _iterator21.e(err);
1523
1773
  } finally {
1524
- _iterator18.f();
1774
+ _iterator21.f();
1525
1775
  }
1526
1776
  var posteriorFor = function posteriorFor(candidate, total) {
1527
1777
  var runtime = runtimeBySurface.get(candidate.word);
@@ -1651,9 +1901,9 @@ var predict = exports.predict = function predict(textBefore) {
1651
1901
  // The gate is the model's own confidence in the surface; the blended score
1652
1902
  // only orders what has already cleared it, so a strong corpus prior can no
1653
1903
  // longer carry a surface the model is unsure of onto the screen.
1654
- .filter(function (_ref6) {
1655
- var candidate = _ref6.candidate,
1656
- posterior = _ref6.posterior;
1904
+ .filter(function (_ref7) {
1905
+ var candidate = _ref7.candidate,
1906
+ posterior = _ref7.posterior;
1657
1907
  return posterior >= MIN_LM_POSTERIOR[candidate.termType];
1658
1908
  });
1659
1909
  /**
@@ -1681,15 +1931,15 @@ var predict = exports.predict = function predict(textBefore) {
1681
1931
  // cleared both. A candidate refused here stays in `ranked` and so still
1682
1932
  // counts as competition below — promoting the runner-up in place of an
1683
1933
  // implausible leader would show something worse, not something better.
1684
- var plausible = gateCleared.filter(function (_ref7) {
1685
- var candidate = _ref7.candidate;
1934
+ var plausible = gateCleared.filter(function (_ref8) {
1935
+ var candidate = _ref8.candidate;
1686
1936
  return isPlausibleSurface(candidate);
1687
1937
  });
1688
1938
  // Applied after the gate rather than folded into it, so the two populations
1689
1939
  // stay separable: a surface refused here cleared its threshold and was
1690
1940
  // refused for having had nothing to clear it against.
1691
- var eligible = plausible.filter(function (_ref8) {
1692
- var candidate = _ref8.candidate;
1941
+ var eligible = plausible.filter(function (_ref9) {
1942
+ var candidate = _ref9.candidate;
1693
1943
  return !canonicalLmSupported || scoredPoolSize(candidate) >= MIN_SCORED_POOL_SIZE || requestedPoolSize(candidate) <= 1;
1694
1944
  }).sort(function (a, b) {
1695
1945
  return b.score - a.score;
@@ -1811,6 +2061,12 @@ var predict = exports.predict = function predict(textBefore) {
1811
2061
  return ranked.some(hasJudgeableEvidence) ? 'below-posterior-gate' : 'no-evidence';
1812
2062
  };
1813
2063
  var abstainReason = resolveAbstainReason();
2064
+ recordPredictionOutcome({
2065
+ abstainReason: abstainReason,
2066
+ awaitingAsyncEvidence: hasUnresolvedPotential && !clearsWinnerMargin,
2067
+ scoredCandidateCount: ranked.length,
2068
+ textBefore: textBefore
2069
+ });
1814
2070
 
1815
2071
  // The leader is the best-supported candidate, not the selected one: an
1816
2072
  // evaluation that showed nothing is exactly the one whose posterior needs
@@ -1855,9 +2111,9 @@ var predict = exports.predict = function predict(textBefore) {
1855
2111
  'below-posterior-gate': "posterior below ".concat(MIN_LM_POSTERIOR[((_posteriorLeader$cand = posteriorLeader === null || posteriorLeader === void 0 ? void 0 : posteriorLeader.candidate) !== null && _posteriorLeader$cand !== void 0 ? _posteriorLeader$cand : ranked[0]).termType], " (best ").concat(((_posteriorLeader$post = posteriorLeader === null || posteriorLeader === void 0 ? void 0 : posteriorLeader.posterior) !== null && _posteriorLeader$post !== void 0 ? _posteriorLeader$post : 0).toFixed(2), ")"),
1856
2112
  'cold-competitor': 'competitor has no LM evidence yet',
1857
2113
  'empty-completion': 'empty completion',
1858
- 'implausible-surface': "mean per-token log-probability below ".concat(MIN_MEAN_TOKEN_LOG_PROBABILITY, " (best ").concat(gateCleared.map(function (_ref9) {
2114
+ 'implausible-surface': "mean per-token log-probability below ".concat(MIN_MEAN_TOKEN_LOG_PROBABILITY, " (best ").concat(gateCleared.map(function (_ref0) {
1859
2115
  var _judgedEvidenceFor$me, _judgedEvidenceFor4;
1860
- var candidate = _ref9.candidate;
2116
+ var candidate = _ref0.candidate;
1861
2117
  return (_judgedEvidenceFor$me = (_judgedEvidenceFor4 = judgedEvidenceFor(candidate)) === null || _judgedEvidenceFor4 === void 0 ? void 0 : _judgedEvidenceFor4.mean) !== null && _judgedEvidenceFor$me !== void 0 ? _judgedEvidenceFor$me : -Infinity;
1862
2118
  }).reduce(function (best, mean) {
1863
2119
  return Math.max(best, mean);
@@ -1866,6 +2122,9 @@ var predict = exports.predict = function predict(textBefore) {
1866
2122
  'missing-artifact': 'canonical artifact coverage missing',
1867
2123
  'no-candidate': 'nothing cleared scoring',
1868
2124
  'no-evidence': 'full-surface evidence absent',
2125
+ // Recorded at the two exits above this block, so they never print here.
2126
+ 'no-surface-token': 'no trailing surface token',
2127
+ 'not-initialized': 'vocabulary not loaded',
1869
2128
  prefetch: "prefetch: ".concat(currentWord.length, "/").concat(DISPLAY_MIN_PREFIX_LENGTH, " chars"),
1870
2129
  'short-completion': "completion shorter than ".concat(MIN_SUGGESTION_LENGTH, " chars"),
1871
2130
  'unresolved-rival': 'expanding plausible token-prefix groups',
@@ -1945,10 +2204,10 @@ var predict = exports.predict = function predict(textBefore) {
1945
2204
  if (verbose && logitCount > 0 && lmLogits) {
1946
2205
  var rawLmTop = Object.entries(lmLogits).sort(function (a, b) {
1947
2206
  return b[1] - a[1];
1948
- }).slice(0, 5).map(function (_ref0) {
1949
- var _ref1 = (0, _slicedToArray2.default)(_ref0, 2),
1950
- word = _ref1[0],
1951
- score = _ref1[1];
2207
+ }).slice(0, 5).map(function (_ref1) {
2208
+ var _ref10 = (0, _slicedToArray2.default)(_ref1, 2),
2209
+ word = _ref10[0],
2210
+ score = _ref10[1];
1952
2211
  return "".concat(word, ":").concat(score.toFixed(3));
1953
2212
  }).join(', ');
1954
2213
  (0, _debugMode.ctcSection)(' rawLM', "\uD83E\uDDE0 ".concat(rawLmTop));
@@ -1960,17 +2219,17 @@ var predict = exports.predict = function predict(textBefore) {
1960
2219
  bigram: 0,
1961
2220
  phrase: 0
1962
2221
  };
1963
- var _iterator19 = _createForOfIteratorHelper(canonicalMatched),
1964
- _step19;
2222
+ var _iterator22 = _createForOfIteratorHelper(canonicalMatched),
2223
+ _step22;
1965
2224
  try {
1966
- for (_iterator19.s(); !(_step19 = _iterator19.n()).done;) {
1967
- var m = _step19.value;
2225
+ for (_iterator22.s(); !(_step22 = _iterator22.n()).done;) {
2226
+ var m = _step22.value;
1968
2227
  genByType[m.node.termType] += 1;
1969
2228
  }
1970
2229
  } catch (err) {
1971
- _iterator19.e(err);
2230
+ _iterator22.e(err);
1972
2231
  } finally {
1973
- _iterator19.f();
2232
+ _iterator22.f();
1974
2233
  }
1975
2234
  (0, _debugMode.ctcSection)('GENERATE', "matched ".concat(canonicalMatched.length, " \u2192 word:").concat(genByType.word, " bigram:").concat(genByType.bigram, " phrase:").concat(genByType.phrase).concat(canonicalLmSupported ? ' · display needs exact surface evidence' : ''));
1976
2235
 
@@ -2029,8 +2288,8 @@ var predict = exports.predict = function predict(textBefore) {
2029
2288
  return judgedPosterior(candidate) >= MIN_LM_POSTERIOR[termType];
2030
2289
  });
2031
2290
  var plausiblePassed = floorPassed.filter(isPlausibleSurface);
2032
- return "".concat(termType, " m:").concat(genByType[termType], " r:").concat(typeRanked.length, " exact:").concat(exact.length, " abs:").concat(absolute.length, " stable:").concat(stabilized.length, " suffix:").concat(longEnough.length, " floor:").concat(floorPassed.length, " plausible:").concat(plausiblePassed.length, " eligible:").concat(eligible.filter(function (_ref10) {
2033
- var candidate = _ref10.candidate;
2291
+ return "".concat(termType, " m:").concat(genByType[termType], " r:").concat(typeRanked.length, " exact:").concat(exact.length, " abs:").concat(absolute.length, " stable:").concat(stabilized.length, " suffix:").concat(longEnough.length, " floor:").concat(floorPassed.length, " plausible:").concat(plausiblePassed.length, " eligible:").concat(eligible.filter(function (_ref11) {
2292
+ var candidate = _ref11.candidate;
2034
2293
  return candidate.termType === termType;
2035
2294
  }).length);
2036
2295
  };
@@ -2039,11 +2298,11 @@ var predict = exports.predict = function predict(textBefore) {
2039
2298
  // One line per context: how many candidates share the normaliser, and how
2040
2299
  // much of the mass the leader holds. A leader well under its threshold
2041
2300
  // means the context is contested, which is the abstention we want.
2042
- var contextLeaders = Array.from(bestCandidateByContext.entries()).slice(0, PHRASE_MAX_WORDS).map(function (_ref11) {
2301
+ var contextLeaders = Array.from(bestCandidateByContext.entries()).slice(0, PHRASE_MAX_WORDS).map(function (_ref12) {
2043
2302
  var _contextTotals$get$le2, _contextTotals$get2;
2044
- var _ref12 = (0, _slicedToArray2.default)(_ref11, 2),
2045
- contextKey = _ref12[0],
2046
- leader = _ref12[1];
2303
+ var _ref13 = (0, _slicedToArray2.default)(_ref12, 2),
2304
+ contextKey = _ref13[0],
2305
+ leader = _ref13[1];
2047
2306
  var shortlistSize = (_contextTotals$get$le2 = (_contextTotals$get2 = contextTotals.get(contextKey)) === null || _contextTotals$get2 === void 0 ? void 0 : _contextTotals$get2.length) !== null && _contextTotals$get$le2 !== void 0 ? _contextTotals$get$le2 : 0;
2048
2307
  var evidenceKind = hasExactEvidence(leader) ? 'exact' : 'upper';
2049
2308
  return "".concat(contextKey.slice(0, 32), " \u2192 n=").concat(shortlistSize, " ").concat(leader.termType, ":\"").concat(leader.word, "\" ").concat(evidenceKind, " p=").concat(optimisticPosterior(leader).toFixed(3), "/").concat(MIN_LM_POSTERIOR[leader.termType]);
@@ -2273,7 +2532,7 @@ var normalizePhraseArtifact = function normalizePhraseArtifact(payload) {
2273
2532
  return null;
2274
2533
  };
2275
2534
  var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
2276
- var _ref13 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(options) {
2535
+ var _ref14 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(options) {
2277
2536
  var _options$isLocalLLM;
2278
2537
  var isLocalLLM, surface, buffer, float32, wordIndexPayload, wordIndex, nWords, dim, _t;
2279
2538
  return _regenerator.default.wrap(function (_context) {
@@ -2348,7 +2607,7 @@ var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
2348
2607
  }, _callee, null, [[2, 5]]);
2349
2608
  }));
2350
2609
  return function loadVectorsAsync(_x) {
2351
- return _ref13.apply(this, arguments);
2610
+ return _ref14.apply(this, arguments);
2352
2611
  };
2353
2612
  }();
2354
2613
  var initVectors = exports.initVectors = function initVectors(store) {
@@ -2372,7 +2631,7 @@ var initVectors = exports.initVectors = function initVectors(store) {
2372
2631
  * A promise that resolves once both fetches have settled
2373
2632
  */
2374
2633
  var loadPhraseArtifacts = exports.loadPhraseArtifacts = /*#__PURE__*/function () {
2375
- var _ref14 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(options) {
2634
+ var _ref15 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee3(options) {
2376
2635
  var _options$isLocalLLM2;
2377
2636
  var isLocalLLM, loadOne, _yield$Promise$allSet, _yield$Promise$allSet2, bigramsResult, phrasesResult, bigramCount, phraseCount;
2378
2637
  return _regenerator.default.wrap(function (_context3) {
@@ -2390,7 +2649,7 @@ var loadPhraseArtifacts = exports.loadPhraseArtifacts = /*#__PURE__*/function ()
2390
2649
  isLocalLLM: isLocalLLM
2391
2650
  });
2392
2651
  loadOne = /*#__PURE__*/function () {
2393
- var _ref15 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(artifactName, termType, label) {
2652
+ var _ref16 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(artifactName, termType, label) {
2394
2653
  var payload, normalized;
2395
2654
  return _regenerator.default.wrap(function (_context2) {
2396
2655
  while (1) switch (_context2.prev = _context2.next) {
@@ -2419,7 +2678,7 @@ var loadPhraseArtifacts = exports.loadPhraseArtifacts = /*#__PURE__*/function ()
2419
2678
  }, _callee2);
2420
2679
  }));
2421
2680
  return function loadOne(_x3, _x4, _x5) {
2422
- return _ref15.apply(this, arguments);
2681
+ return _ref16.apply(this, arguments);
2423
2682
  };
2424
2683
  }();
2425
2684
  _context3.next = 2;
@@ -2461,7 +2720,7 @@ var loadPhraseArtifacts = exports.loadPhraseArtifacts = /*#__PURE__*/function ()
2461
2720
  }, _callee3);
2462
2721
  }));
2463
2722
  return function loadPhraseArtifacts(_x2) {
2464
- return _ref14.apply(this, arguments);
2723
+ return _ref15.apply(this, arguments);
2465
2724
  };
2466
2725
  }();
2467
2726
  var vocabularyLoadPromise;
@@ -2527,10 +2786,10 @@ var loadDefaultVocabulary = exports.loadDefaultVocabulary = function loadDefault
2527
2786
  _yield$Promise$all2 = (0, _slicedToArray2.default)(_yield$Promise$all, 2);
2528
2787
  vocabularyData = _yield$Promise$all2[0];
2529
2788
  l3VocabularyData = _yield$Promise$all2[1];
2530
- terms = Object.entries(vocabularyData.words).map(function (_ref17) {
2531
- var _ref18 = (0, _slicedToArray2.default)(_ref17, 2),
2532
- word = _ref18[0],
2533
- stats = _ref18[1];
2789
+ terms = Object.entries(vocabularyData.words).map(function (_ref18) {
2790
+ var _ref19 = (0, _slicedToArray2.default)(_ref18, 2),
2791
+ word = _ref19[0],
2792
+ stats = _ref19[1];
2534
2793
  return {
2535
2794
  word: word,
2536
2795
  freq: stats.freq,