@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.
- package/CHANGELOG.md +133 -0
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +564 -67
- package/dist/cjs/pm-plugins/debug-mode.js +28 -2
- package/dist/cjs/pm-plugins/inline-code-harvester.js +576 -0
- package/dist/cjs/pm-plugins/text-predictor.js +437 -178
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +515 -52
- package/dist/es2019/pm-plugins/debug-mode.js +27 -1
- package/dist/es2019/pm-plugins/inline-code-harvester.js +455 -0
- package/dist/es2019/pm-plugins/text-predictor.js +223 -3
- package/dist/esm/pm-plugins/autocomplete-plugin.js +562 -67
- package/dist/esm/pm-plugins/debug-mode.js +27 -1
- package/dist/esm/pm-plugins/inline-code-harvester.js +572 -0
- package/dist/esm/pm-plugins/text-predictor.js +437 -178
- package/dist/types/pm-plugins/autocomplete-plugin.d.ts +37 -0
- package/dist/types/pm-plugins/debug-mode.d.ts +34 -0
- package/dist/types/pm-plugins/inline-code-harvester.d.ts +146 -0
- package/dist/types/pm-plugins/text-predictor.d.ts +105 -3
- package/package.json +2 -2
- package/src/pm-plugins/autocomplete-plugin.ts +589 -48
- package/src/pm-plugins/debug-mode.ts +47 -1
- package/src/pm-plugins/inline-code-harvester.ts +561 -0
- package/src/pm-plugins/text-predictor.ts +274 -5
|
@@ -13,14 +13,17 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
|
13
13
|
* Falls back to cold mode (freq-only) when vectors not yet loaded.
|
|
14
14
|
*
|
|
15
15
|
* Session personalization (L1): words the user types are incrementally boosted
|
|
16
|
-
* via incrementSessionFreq(), called on word boundaries from the plugin
|
|
16
|
+
* via incrementSessionFreq(), called on word boundaries from the plugin, and
|
|
17
|
+
* words in ingested context text via ingestDocumentPage(). What the session has
|
|
18
|
+
* boosted is visible at any time from the console: `__atlCtcDebug__.session()`,
|
|
19
|
+
* or `__atlCtcDebug__.session('poll')` for one family — see inspectSessionBoosts.
|
|
17
20
|
*/
|
|
18
21
|
|
|
19
22
|
import { EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
|
|
20
23
|
import { fetchAutocompleteArtifactBinary, fetchAutocompleteArtifactJson } from './artifact-loader';
|
|
21
24
|
import { ARTIFACT_NAME } from './artifacts-manifest';
|
|
22
25
|
import { createCanonicalContextPositionCache, deriveCanonicalCandidateContext, deriveWhitespaceBoundaryContext, logSoftmaxAt, logSumExp, selectBoundaryPrimeRequests } from './canonical-lm-scoring';
|
|
23
|
-
import { CTC_STYLES, ctcSection, ctcTag, isAutocompleteDebugEnabled, isAutocompleteDebugVerbose } from './debug-mode';
|
|
26
|
+
import { CTC_STYLES, ctcSection, ctcTag, isAutocompleteDebugEnabled, isAutocompleteDebugVerbose, registerCtcSessionInspector } from './debug-mode';
|
|
24
27
|
import { loadGrammarDataAsync, rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE, MIN_WINNER_MARGIN } from './scoring-pipeline';
|
|
25
28
|
import { getBoundaryLmState, getCanonicalSurfaceCount, getCanonicalSurfaceTokenIds, getDefaultSlowLaneClientStatus, getProgressiveSurfaceEvidence, getStoredContextInput, getStoredContextVector, getStoredLmLogits, getSurfaceScore, isCanonicalSurfaceScoringSupported, primeBoundaryLm, requestProgressiveSurfaceScores } from './slow-lane-client';
|
|
26
29
|
|
|
@@ -172,6 +175,18 @@ const DEBUG_TEXT_TAIL_CHARS = 120;
|
|
|
172
175
|
* is vocabulary coverage.
|
|
173
176
|
*/
|
|
174
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
|
+
|
|
175
190
|
/**
|
|
176
191
|
* A candidate paired with the length of the already-typed prefix it completes.
|
|
177
192
|
* For a single word this is the current partial token length; for a phrase it
|
|
@@ -200,6 +215,17 @@ class TrieNode {
|
|
|
200
215
|
class WeightedWordTrie {
|
|
201
216
|
constructor() {
|
|
202
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());
|
|
203
229
|
/** Highest tenantFreq seen — used to normalize freq scores at query time */
|
|
204
230
|
_defineProperty(this, "maxTenantFreq", 1);
|
|
205
231
|
}
|
|
@@ -273,6 +299,11 @@ class WeightedWordTrie {
|
|
|
273
299
|
return node.word !== null ? node : null;
|
|
274
300
|
}
|
|
275
301
|
|
|
302
|
+
/** Whether this exact surface is stored as a terminal word. */
|
|
303
|
+
hasWord(word) {
|
|
304
|
+
return this.findNode(word) !== null;
|
|
305
|
+
}
|
|
306
|
+
|
|
276
307
|
/**
|
|
277
308
|
* Set the session frequency for a word.
|
|
278
309
|
* Returns true if the word exists in the trie.
|
|
@@ -283,6 +314,7 @@ class WeightedWordTrie {
|
|
|
283
314
|
return false;
|
|
284
315
|
}
|
|
285
316
|
node.sessionFreq = count;
|
|
317
|
+
this.boostedNodes.add(node);
|
|
286
318
|
return true;
|
|
287
319
|
}
|
|
288
320
|
|
|
@@ -296,8 +328,58 @@ class WeightedWordTrie {
|
|
|
296
328
|
return false;
|
|
297
329
|
}
|
|
298
330
|
node.sessionFreq += 1;
|
|
331
|
+
this.boostedNodes.add(node);
|
|
299
332
|
return true;
|
|
300
333
|
}
|
|
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
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Every word carrying a session boost, optionally limited to one prefix's
|
|
348
|
+
* subtree. Unlike `getCandidates` a word equal to the prefix is included,
|
|
349
|
+
* since the question here is what the session holds rather than what could
|
|
350
|
+
* still be typed.
|
|
351
|
+
*
|
|
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.
|
|
354
|
+
*/
|
|
355
|
+
collectSessionBoosted(prefix = '') {
|
|
356
|
+
let node = this.root;
|
|
357
|
+
for (const char of prefix.toLowerCase()) {
|
|
358
|
+
const next = node.children.get(char);
|
|
359
|
+
if (!next) {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
node = next;
|
|
363
|
+
}
|
|
364
|
+
const boosted = [];
|
|
365
|
+
const stack = [node];
|
|
366
|
+
while (stack.length > 0) {
|
|
367
|
+
const current = stack.pop();
|
|
368
|
+
if (!current) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (current.word !== null && current.sessionFreq > 0) {
|
|
372
|
+
boosted.push({
|
|
373
|
+
word: current.word,
|
|
374
|
+
node: current
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
for (const child of current.children.values()) {
|
|
378
|
+
stack.push(child);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return boosted;
|
|
382
|
+
}
|
|
301
383
|
}
|
|
302
384
|
|
|
303
385
|
// L1/L2 Trie (Session + Atlassian Domain)
|
|
@@ -349,6 +431,13 @@ let phraseTermCount = 0;
|
|
|
349
431
|
let maxBigramFreq = 1;
|
|
350
432
|
let maxPhraseFreq = 1;
|
|
351
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;
|
|
352
441
|
|
|
353
442
|
// ── Stabilization (QI-2): post-accept cooldown + whole-surface repetition ────
|
|
354
443
|
// Two guards that stop the accept→echo (`end to end` → `end to end to end`) and
|
|
@@ -380,6 +469,22 @@ export const noteSuggestionAccepted = surface => {
|
|
|
380
469
|
}
|
|
381
470
|
};
|
|
382
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
|
+
|
|
383
488
|
/** Get vector for a word from the store. */
|
|
384
489
|
const getWordVector = word => {
|
|
385
490
|
if (!vectorStore) {
|
|
@@ -649,6 +754,36 @@ export const incrementSessionFreq = word => {
|
|
|
649
754
|
wordTrie.incrementSessionFreq(word);
|
|
650
755
|
};
|
|
651
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
|
+
|
|
652
787
|
/**
|
|
653
788
|
* Prime session frequencies from a document page string.
|
|
654
789
|
*
|
|
@@ -672,7 +807,7 @@ export const ingestDocumentPage = pageContent => {
|
|
|
672
807
|
}
|
|
673
808
|
}
|
|
674
809
|
if (isAutocompleteDebugEnabled() && validBoostedWords.size > 0) {
|
|
675
|
-
ctcTag('init', `L1 session primed ${validBoostedWords.size} words from page`, CTC_STYLES.brand);
|
|
810
|
+
ctcTag('init', `L1 session primed ${validBoostedWords.size} words from page · __atlCtcDebug__.session() to inspect`, CTC_STYLES.brand);
|
|
676
811
|
if (isAutocompleteDebugVerbose()) {
|
|
677
812
|
// eslint-disable-next-line no-console
|
|
678
813
|
console.dir(Array.from(validBoostedWords).sort());
|
|
@@ -680,6 +815,54 @@ export const ingestDocumentPage = pageContent => {
|
|
|
680
815
|
}
|
|
681
816
|
};
|
|
682
817
|
|
|
818
|
+
/**
|
|
819
|
+
* How many boosted words `inspectSessionBoosts` lists.
|
|
820
|
+
*
|
|
821
|
+
* A page ingest can boost thousands, and a list that long is not read. The
|
|
822
|
+
* strongest boosts are the ones that change an ordering, and `boosted` still
|
|
823
|
+
* reports the full size, so the cap loses nothing but volume.
|
|
824
|
+
*/
|
|
825
|
+
const MAX_LISTED_SESSION_WORDS = 100;
|
|
826
|
+
/**
|
|
827
|
+
* Read the session's L1 boosts, optionally narrowed to a prefix.
|
|
828
|
+
*
|
|
829
|
+
* Installed as `__atlCtcDebug__.session()`, with `__atlCtcDebug__.session('poll')`
|
|
830
|
+
* to ask about one family. Returned rather than logged, so the console renders it
|
|
831
|
+
* as an inspectable object and a caller can assert on it.
|
|
832
|
+
*
|
|
833
|
+
* Only words the vocabulary already holds can carry a boost, because both writers
|
|
834
|
+
* go through `incrementSessionFreq` and it only finds existing nodes. An ingested
|
|
835
|
+
* word absent from the vocabulary is therefore missing from here and always will
|
|
836
|
+
* be — that gap is what the inline-code harvester covers, and those surfaces show
|
|
837
|
+
* up under `__atlCtcDebug__.harvest()` instead.
|
|
838
|
+
*/
|
|
839
|
+
export const inspectSessionBoosts = prefix => {
|
|
840
|
+
const boosted = wordTrie.collectSessionBoosted(prefix !== null && prefix !== void 0 ? prefix : '');
|
|
841
|
+
return {
|
|
842
|
+
boosted: boosted.length,
|
|
843
|
+
limit: MAX_LISTED_SESSION_WORDS,
|
|
844
|
+
...(prefix === undefined ? {} : {
|
|
845
|
+
prefix
|
|
846
|
+
}),
|
|
847
|
+
words: boosted.sort((a, b) => b.node.sessionFreq - a.node.sessionFreq || a.word.localeCompare(b.word, 'en', {
|
|
848
|
+
numeric: true,
|
|
849
|
+
sensitivity: 'base'
|
|
850
|
+
})).slice(0, MAX_LISTED_SESSION_WORDS).map(({
|
|
851
|
+
node,
|
|
852
|
+
word
|
|
853
|
+
}) => ({
|
|
854
|
+
sessionFreq: node.sessionFreq,
|
|
855
|
+
sessionOnly: node.tenantFreq === 0,
|
|
856
|
+
surface: word,
|
|
857
|
+
tenantFreq: node.tenantFreq
|
|
858
|
+
}))
|
|
859
|
+
};
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
// At module scope so the console answers before the first keystroke, which is
|
|
863
|
+
// when someone reaching for it usually asks.
|
|
864
|
+
registerCtcSessionInspector(inspectSessionBoosts);
|
|
865
|
+
|
|
683
866
|
/**
|
|
684
867
|
* Result of a prediction: the ghost tail to insert plus an immutable record of
|
|
685
868
|
* the evidence that authorized the UI commitment.
|
|
@@ -783,6 +966,14 @@ export const predict = textBefore => {
|
|
|
783
966
|
void loadDefaultVocabulary({
|
|
784
967
|
source: 'predict'
|
|
785
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
|
+
});
|
|
786
977
|
return null;
|
|
787
978
|
}
|
|
788
979
|
const t0 = performance.now();
|
|
@@ -812,11 +1003,23 @@ export const predict = textBefore => {
|
|
|
812
1003
|
priority: 0
|
|
813
1004
|
});
|
|
814
1005
|
}
|
|
1006
|
+
recordPredictionOutcome({
|
|
1007
|
+
abstainReason: 'prefetch',
|
|
1008
|
+
awaitingAsyncEvidence: true,
|
|
1009
|
+
scoredCandidateCount: 0,
|
|
1010
|
+
textBefore
|
|
1011
|
+
});
|
|
815
1012
|
return null;
|
|
816
1013
|
}
|
|
817
1014
|
const trimmed = textBefore.trimEnd();
|
|
818
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$ : '';
|
|
819
1016
|
if (trailingSurfaceToken.length === 0) {
|
|
1017
|
+
recordPredictionOutcome({
|
|
1018
|
+
abstainReason: 'no-surface-token',
|
|
1019
|
+
awaitingAsyncEvidence: false,
|
|
1020
|
+
scoredCandidateCount: 0,
|
|
1021
|
+
textBefore
|
|
1022
|
+
});
|
|
820
1023
|
return null;
|
|
821
1024
|
}
|
|
822
1025
|
const currentWord = trailingSurfaceToken;
|
|
@@ -828,6 +1031,14 @@ export const predict = textBefore => {
|
|
|
828
1031
|
|
|
829
1032
|
// If every trie was empty for this prefix
|
|
830
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
|
+
});
|
|
831
1042
|
if (isAutocompleteDebugEnabled()) {
|
|
832
1043
|
// eslint-disable-next-line no-console
|
|
833
1044
|
console.log(`%c[CTC]%c — abstain: no matches for "${currentWord}"`, CTC_STYLES.brand, CTC_STYLES.body);
|
|
@@ -1511,6 +1722,12 @@ export const predict = textBefore => {
|
|
|
1511
1722
|
return ranked.some(hasJudgeableEvidence) ? 'below-posterior-gate' : 'no-evidence';
|
|
1512
1723
|
};
|
|
1513
1724
|
const abstainReason = resolveAbstainReason();
|
|
1725
|
+
recordPredictionOutcome({
|
|
1726
|
+
abstainReason,
|
|
1727
|
+
awaitingAsyncEvidence: hasUnresolvedPotential && !clearsWinnerMargin,
|
|
1728
|
+
scoredCandidateCount: ranked.length,
|
|
1729
|
+
textBefore
|
|
1730
|
+
});
|
|
1514
1731
|
|
|
1515
1732
|
// The leader is the best-supported candidate, not the selected one: an
|
|
1516
1733
|
// evaluation that showed nothing is exactly the one whose posterior needs
|
|
@@ -1561,6 +1778,9 @@ export const predict = textBefore => {
|
|
|
1561
1778
|
'missing-artifact': 'canonical artifact coverage missing',
|
|
1562
1779
|
'no-candidate': 'nothing cleared scoring',
|
|
1563
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',
|
|
1564
1784
|
prefetch: `prefetch: ${currentWord.length}/${DISPLAY_MIN_PREFIX_LENGTH} chars`,
|
|
1565
1785
|
'short-completion': `completion shorter than ${MIN_SUGGESTION_LENGTH} chars`,
|
|
1566
1786
|
'unresolved-rival': 'expanding plausible token-prefix groups',
|