@atlaskit/editor-plugin-autocomplete 9.1.0 → 9.2.1

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.
@@ -175,6 +175,18 @@ const DEBUG_TEXT_TAIL_CHARS = 120;
175
175
  * is vocabulary coverage.
176
176
  */
177
177
 
178
+ /**
179
+ * What the scored path concluded, recorded on every evaluation whether or not
180
+ * debug is on.
181
+ *
182
+ * This exists for the inline-code harvester, which may only offer a harvested
183
+ * surface once the scored path has finished and come away empty. The reason is
184
+ * the load-bearing part: `no-candidate` means no vocabulary reaches this prefix
185
+ * at all, while `winner-margin` means two known words the model cannot yet
186
+ * separate — the first is a gap worth filling and the second is a prefix
187
+ * ambiguous enough that filling it would be a guess.
188
+ */
189
+
178
190
  /**
179
191
  * A candidate paired with the length of the already-typed prefix it completes.
180
192
  * For a single word this is the current partial token length; for a phrase it
@@ -203,6 +215,17 @@ class TrieNode {
203
215
  class WeightedWordTrie {
204
216
  constructor() {
205
217
  _defineProperty(this, "root", new TrieNode());
218
+ /**
219
+ * Every node a session boost has been written to.
220
+ *
221
+ * Kept because dropping the boosts is no longer a rare event — it happens
222
+ * each time the reader changes page or conversation — and walking a vocabulary
223
+ * of tens of thousands of words to find the few hundred that were touched
224
+ * costs about 10ms of main thread every time. The two writers below are the
225
+ * only way a `sessionFreq` moves, so keeping this in step costs one Set
226
+ * insertion on a path that is already descending the trie.
227
+ */
228
+ _defineProperty(this, "boostedNodes", new Set());
206
229
  /** Highest tenantFreq seen — used to normalize freq scores at query time */
207
230
  _defineProperty(this, "maxTenantFreq", 1);
208
231
  }
@@ -276,6 +299,11 @@ class WeightedWordTrie {
276
299
  return node.word !== null ? node : null;
277
300
  }
278
301
 
302
+ /** Whether this exact surface is stored as a terminal word. */
303
+ hasWord(word) {
304
+ return this.findNode(word) !== null;
305
+ }
306
+
279
307
  /**
280
308
  * Set the session frequency for a word.
281
309
  * Returns true if the word exists in the trie.
@@ -286,6 +314,7 @@ class WeightedWordTrie {
286
314
  return false;
287
315
  }
288
316
  node.sessionFreq = count;
317
+ this.boostedNodes.add(node);
289
318
  return true;
290
319
  }
291
320
 
@@ -299,17 +328,29 @@ class WeightedWordTrie {
299
328
  return false;
300
329
  }
301
330
  node.sessionFreq += 1;
331
+ this.boostedNodes.add(node);
302
332
  return true;
303
333
  }
304
334
 
335
+ /**
336
+ * Zero every session boost, in the number of words boosted rather than the
337
+ * number of words known.
338
+ */
339
+ clearSessionBoosts() {
340
+ for (const node of this.boostedNodes) {
341
+ node.sessionFreq = 0;
342
+ }
343
+ this.boostedNodes.clear();
344
+ }
345
+
305
346
  /**
306
347
  * Every word carrying a session boost, optionally limited to one prefix's
307
348
  * subtree. Unlike `getCandidates` a word equal to the prefix is included,
308
349
  * since the question here is what the session holds rather than what could
309
350
  * still be typed.
310
351
  *
311
- * Walks the trie instead of reading an index, so nothing has to be kept in
312
- * step on the write path for the sake of being able to ask.
352
+ * Walks the trie rather than reading `boostedNodes`, since a prefix answer is
353
+ * a subtree question and this only runs when a human asks it.
313
354
  */
314
355
  collectSessionBoosted(prefix = '') {
315
356
  let node = this.root;
@@ -390,6 +431,13 @@ let phraseTermCount = 0;
390
431
  let maxBigramFreq = 1;
391
432
  let maxPhraseFreq = 1;
392
433
  let lastPredictionDebug = null;
434
+ let lastPredictionOutcome = null;
435
+ const recordPredictionOutcome = outcome => {
436
+ lastPredictionOutcome = outcome;
437
+ };
438
+
439
+ /** The verdict from the most recent `predict` call. Always populated. */
440
+ export const getLastPredictionOutcome = () => lastPredictionOutcome;
393
441
 
394
442
  // ── Stabilization (QI-2): post-accept cooldown + whole-surface repetition ────
395
443
  // Two guards that stop the accept→echo (`end to end` → `end to end to end`) and
@@ -421,6 +469,22 @@ export const noteSuggestionAccepted = surface => {
421
469
  }
422
470
  };
423
471
 
472
+ /**
473
+ * Whether `surface` is the one the user just accepted and is still inside its
474
+ * cooldown window.
475
+ *
476
+ * Read-only, unlike the advance inside `predict`: the cooldown is measured in
477
+ * predictions, and a caller asking whether it is active must not consume one of
478
+ * them. Exported for the harvest path, which displays without going through
479
+ * arbitration and so would otherwise re-offer what was just accepted.
480
+ */
481
+ export const isSurfaceInAcceptCooldown = surface => {
482
+ if (!acceptCooldown || acceptCooldown.surface !== surface.trim().toLowerCase()) {
483
+ return false;
484
+ }
485
+ return acceptCooldown.predictionsSince <= COOLDOWN_KEYSTROKES || performance.now() - acceptCooldown.ts < COOLDOWN_MS;
486
+ };
487
+
424
488
  /** Get vector for a word from the store. */
425
489
  const getWordVector = word => {
426
490
  if (!vectorStore) {
@@ -690,6 +754,36 @@ export const incrementSessionFreq = word => {
690
754
  wordTrie.incrementSessionFreq(word);
691
755
  };
692
756
 
757
+ /**
758
+ * Drop every L1 boost this session has accumulated.
759
+ *
760
+ * The vocabulary itself is left alone: only `sessionFreq` is cleared, so the
761
+ * tenant and generic frequencies a boost was sitting on top of survive. Called
762
+ * when the plugin decides the session it was learning for has ended — a new
763
+ * conversation, or a different page — since a boost is a claim about what is
764
+ * being discussed and that claim does not carry over.
765
+ */
766
+ export const resetSessionBoosts = () => {
767
+ wordTrie.clearSessionBoosts();
768
+ // Anything memoized against the old boosts is now describing a session that
769
+ // no longer exists.
770
+ recallGeneration++;
771
+ };
772
+
773
+ /**
774
+ * Which vocabulary already holds this surface, if any.
775
+ *
776
+ * Used by the inline-code harvester to drop terms the scored path can already
777
+ * serve, so that harvesting stays limited to words with no route to a
778
+ * suggestion today.
779
+ */
780
+ export const lookupVocabularySource = word => {
781
+ if (wordTrie.hasWord(word)) {
782
+ return 'l2';
783
+ }
784
+ return l3Trie.hasWord(word) ? 'l3' : null;
785
+ };
786
+
693
787
  /**
694
788
  * Prime session frequencies from a document page string.
695
789
  *
@@ -728,7 +822,7 @@ export const ingestDocumentPage = pageContent => {
728
822
  * strongest boosts are the ones that change an ordering, and `boosted` still
729
823
  * reports the full size, so the cap loses nothing but volume.
730
824
  */
731
- const MAX_LISTED_SESSION_WORDS = 50;
825
+ const MAX_LISTED_SESSION_WORDS = 100;
732
826
  /**
733
827
  * Read the session's L1 boosts, optionally narrowed to a prefix.
734
828
  *
@@ -739,7 +833,8 @@ const MAX_LISTED_SESSION_WORDS = 50;
739
833
  * Only words the vocabulary already holds can carry a boost, because both writers
740
834
  * go through `incrementSessionFreq` and it only finds existing nodes. An ingested
741
835
  * word absent from the vocabulary is therefore missing from here and always will
742
- * be.
836
+ * be — that gap is what the inline-code harvester covers, and those surfaces show
837
+ * up under `__atlCtcDebug__.harvest()` instead.
743
838
  */
744
839
  export const inspectSessionBoosts = prefix => {
745
840
  const boosted = wordTrie.collectSessionBoosted(prefix !== null && prefix !== void 0 ? prefix : '');
@@ -871,6 +966,14 @@ export const predict = textBefore => {
871
966
  void loadDefaultVocabulary({
872
967
  source: 'predict'
873
968
  }).catch(() => {});
969
+ // Awaiting, not empty-handed: with no vocabulary loaded the harvester's own
970
+ // intake filter has not been applied to anything either.
971
+ recordPredictionOutcome({
972
+ abstainReason: 'not-initialized',
973
+ awaitingAsyncEvidence: true,
974
+ scoredCandidateCount: 0,
975
+ textBefore
976
+ });
874
977
  return null;
875
978
  }
876
979
  const t0 = performance.now();
@@ -900,11 +1003,23 @@ export const predict = textBefore => {
900
1003
  priority: 0
901
1004
  });
902
1005
  }
1006
+ recordPredictionOutcome({
1007
+ abstainReason: 'prefetch',
1008
+ awaitingAsyncEvidence: true,
1009
+ scoredCandidateCount: 0,
1010
+ textBefore
1011
+ });
903
1012
  return null;
904
1013
  }
905
1014
  const trimmed = textBefore.trimEnd();
906
1015
  const 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$ : '';
907
1016
  if (trailingSurfaceToken.length === 0) {
1017
+ recordPredictionOutcome({
1018
+ abstainReason: 'no-surface-token',
1019
+ awaitingAsyncEvidence: false,
1020
+ scoredCandidateCount: 0,
1021
+ textBefore
1022
+ });
908
1023
  return null;
909
1024
  }
910
1025
  const currentWord = trailingSurfaceToken;
@@ -916,6 +1031,14 @@ export const predict = textBefore => {
916
1031
 
917
1032
  // If every trie was empty for this prefix
918
1033
  if (canonicalMatched.length === 0) {
1034
+ // Terminal, and the only verdict that says the vocabulary has no claim on
1035
+ // this prefix at all — which is what makes it the harvester's cue.
1036
+ recordPredictionOutcome({
1037
+ abstainReason: 'no-candidate',
1038
+ awaitingAsyncEvidence: false,
1039
+ scoredCandidateCount: 0,
1040
+ textBefore
1041
+ });
919
1042
  if (isAutocompleteDebugEnabled()) {
920
1043
  // eslint-disable-next-line no-console
921
1044
  console.log(`%c[CTC]%c — abstain: no matches for "${currentWord}"`, CTC_STYLES.brand, CTC_STYLES.body);
@@ -1599,6 +1722,12 @@ export const predict = textBefore => {
1599
1722
  return ranked.some(hasJudgeableEvidence) ? 'below-posterior-gate' : 'no-evidence';
1600
1723
  };
1601
1724
  const abstainReason = resolveAbstainReason();
1725
+ recordPredictionOutcome({
1726
+ abstainReason,
1727
+ awaitingAsyncEvidence: hasUnresolvedPotential && !clearsWinnerMargin,
1728
+ scoredCandidateCount: ranked.length,
1729
+ textBefore
1730
+ });
1602
1731
 
1603
1732
  // The leader is the best-supported candidate, not the selected one: an
1604
1733
  // evaluation that showed nothing is exactly the one whose posterior needs
@@ -1649,6 +1778,9 @@ export const predict = textBefore => {
1649
1778
  'missing-artifact': 'canonical artifact coverage missing',
1650
1779
  'no-candidate': 'nothing cleared scoring',
1651
1780
  'no-evidence': 'full-surface evidence absent',
1781
+ // Recorded at the two exits above this block, so they never print here.
1782
+ 'no-surface-token': 'no trailing surface token',
1783
+ 'not-initialized': 'vocabulary not loaded',
1652
1784
  prefetch: `prefetch: ${currentWord.length}/${DISPLAY_MIN_PREFIX_LENGTH} chars`,
1653
1785
  'short-completion': `completion shorter than ${MIN_SUGGESTION_LENGTH} chars`,
1654
1786
  'unresolved-rival': 'expanding plausible token-prefix groups',
@@ -14,7 +14,8 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
14
14
  * https://hello.atlassian.net/wiki/spaces/AA6/pages/3961393753
15
15
  */
16
16
 
17
- import { ConcurrentExperience, ExperiencePerformanceTypes, ExperienceTypes } from '@atlaskit/ufo';
17
+ import { ConcurrentExperience } from '@atlaskit/ufo/concurrent-experience';
18
+ import { ExperiencePerformanceTypes, ExperienceTypes } from '@atlaskit/ufo/experience-types';
18
19
 
19
20
  /**
20
21
  * Experience name strings are surfaced downstream by the UFO pipeline as