@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.
- package/CHANGELOG.md +115 -0
- package/dist/cjs/analytics/ufo.js +6 -5
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +496 -50
- package/dist/cjs/pm-plugins/debug-mode.js +14 -3
- package/dist/cjs/pm-plugins/inline-code-harvester.js +576 -0
- package/dist/cjs/pm-plugins/text-predictor.js +298 -151
- package/dist/es2019/analytics/ufo.js +2 -1
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +457 -36
- package/dist/es2019/pm-plugins/debug-mode.js +13 -2
- package/dist/es2019/pm-plugins/inline-code-harvester.js +455 -0
- package/dist/es2019/pm-plugins/text-predictor.js +136 -4
- package/dist/esm/analytics/ufo.js +2 -1
- package/dist/esm/pm-plugins/autocomplete-plugin.js +494 -50
- package/dist/esm/pm-plugins/debug-mode.js +13 -2
- package/dist/esm/pm-plugins/inline-code-harvester.js +572 -0
- package/dist/esm/pm-plugins/text-predictor.js +297 -150
- package/dist/types/analytics/ufo.d.ts +1 -1
- package/dist/types/pm-plugins/autocomplete-plugin.d.ts +37 -0
- package/dist/types/pm-plugins/debug-mode.d.ts +19 -4
- package/dist/types/pm-plugins/inline-code-harvester.d.ts +146 -0
- package/dist/types/pm-plugins/text-predictor.d.ts +70 -3
- package/package.json +2 -2
- package/src/analytics/ufo.ts +3 -6
- package/src/pm-plugins/autocomplete-plugin.ts +526 -31
- package/src/pm-plugins/debug-mode.ts +26 -5
- package/src/pm-plugins/inline-code-harvester.ts +561 -0
- package/src/pm-plugins/text-predictor.ts +162 -7
|
@@ -212,7 +212,7 @@ const DEBUG_TEXT_TAIL_CHARS = 120;
|
|
|
212
212
|
* `cold-competitor` and `unresolved-rival` are scheduling, and `no-candidate`
|
|
213
213
|
* is vocabulary coverage.
|
|
214
214
|
*/
|
|
215
|
-
type CtcAbstainReason =
|
|
215
|
+
export type CtcAbstainReason =
|
|
216
216
|
| 'below-posterior-gate'
|
|
217
217
|
| 'cold-competitor'
|
|
218
218
|
| 'empty-completion'
|
|
@@ -221,11 +221,34 @@ type CtcAbstainReason =
|
|
|
221
221
|
| 'missing-artifact'
|
|
222
222
|
| 'no-candidate'
|
|
223
223
|
| 'no-evidence'
|
|
224
|
+
| 'no-surface-token'
|
|
225
|
+
| 'not-initialized'
|
|
224
226
|
| 'prefetch'
|
|
225
227
|
| 'short-completion'
|
|
226
228
|
| 'unresolved-rival'
|
|
227
229
|
| 'winner-margin';
|
|
228
230
|
|
|
231
|
+
/**
|
|
232
|
+
* What the scored path concluded, recorded on every evaluation whether or not
|
|
233
|
+
* debug is on.
|
|
234
|
+
*
|
|
235
|
+
* This exists for the inline-code harvester, which may only offer a harvested
|
|
236
|
+
* surface once the scored path has finished and come away empty. The reason is
|
|
237
|
+
* the load-bearing part: `no-candidate` means no vocabulary reaches this prefix
|
|
238
|
+
* at all, while `winner-margin` means two known words the model cannot yet
|
|
239
|
+
* separate — the first is a gap worth filling and the second is a prefix
|
|
240
|
+
* ambiguous enough that filling it would be a guess.
|
|
241
|
+
*/
|
|
242
|
+
export interface PredictionOutcome {
|
|
243
|
+
abstainReason: CtcAbstainReason | null;
|
|
244
|
+
/** True while a pending async signal could still change the verdict. */
|
|
245
|
+
awaitingAsyncEvidence: boolean;
|
|
246
|
+
/** How many vocabulary candidates were scored for this prefix. */
|
|
247
|
+
scoredCandidateCount: number;
|
|
248
|
+
/** The exact string passed to `predict`, so a caller can confirm identity. */
|
|
249
|
+
textBefore: string;
|
|
250
|
+
}
|
|
251
|
+
|
|
229
252
|
interface Candidate {
|
|
230
253
|
node: TrieNode;
|
|
231
254
|
word: string;
|
|
@@ -288,6 +311,17 @@ class TrieNode {
|
|
|
288
311
|
|
|
289
312
|
class WeightedWordTrie {
|
|
290
313
|
private root = new TrieNode();
|
|
314
|
+
/**
|
|
315
|
+
* Every node a session boost has been written to.
|
|
316
|
+
*
|
|
317
|
+
* Kept because dropping the boosts is no longer a rare event — it happens
|
|
318
|
+
* each time the reader changes page or conversation — and walking a vocabulary
|
|
319
|
+
* of tens of thousands of words to find the few hundred that were touched
|
|
320
|
+
* costs about 10ms of main thread every time. The two writers below are the
|
|
321
|
+
* only way a `sessionFreq` moves, so keeping this in step costs one Set
|
|
322
|
+
* insertion on a path that is already descending the trie.
|
|
323
|
+
*/
|
|
324
|
+
private boostedNodes = new Set<TrieNode>();
|
|
291
325
|
/** Highest tenantFreq seen — used to normalize freq scores at query time */
|
|
292
326
|
maxTenantFreq: number = 1;
|
|
293
327
|
|
|
@@ -367,6 +401,11 @@ class WeightedWordTrie {
|
|
|
367
401
|
return node.word !== null ? node : null;
|
|
368
402
|
}
|
|
369
403
|
|
|
404
|
+
/** Whether this exact surface is stored as a terminal word. */
|
|
405
|
+
hasWord(word: string): boolean {
|
|
406
|
+
return this.findNode(word) !== null;
|
|
407
|
+
}
|
|
408
|
+
|
|
370
409
|
/**
|
|
371
410
|
* Set the session frequency for a word.
|
|
372
411
|
* Returns true if the word exists in the trie.
|
|
@@ -377,6 +416,7 @@ class WeightedWordTrie {
|
|
|
377
416
|
return false;
|
|
378
417
|
}
|
|
379
418
|
node.sessionFreq = count;
|
|
419
|
+
this.boostedNodes.add(node);
|
|
380
420
|
return true;
|
|
381
421
|
}
|
|
382
422
|
|
|
@@ -390,17 +430,29 @@ class WeightedWordTrie {
|
|
|
390
430
|
return false;
|
|
391
431
|
}
|
|
392
432
|
node.sessionFreq += 1;
|
|
433
|
+
this.boostedNodes.add(node);
|
|
393
434
|
return true;
|
|
394
435
|
}
|
|
395
436
|
|
|
437
|
+
/**
|
|
438
|
+
* Zero every session boost, in the number of words boosted rather than the
|
|
439
|
+
* number of words known.
|
|
440
|
+
*/
|
|
441
|
+
clearSessionBoosts(): void {
|
|
442
|
+
for (const node of this.boostedNodes) {
|
|
443
|
+
node.sessionFreq = 0;
|
|
444
|
+
}
|
|
445
|
+
this.boostedNodes.clear();
|
|
446
|
+
}
|
|
447
|
+
|
|
396
448
|
/**
|
|
397
449
|
* Every word carrying a session boost, optionally limited to one prefix's
|
|
398
450
|
* subtree. Unlike `getCandidates` a word equal to the prefix is included,
|
|
399
451
|
* since the question here is what the session holds rather than what could
|
|
400
452
|
* still be typed.
|
|
401
453
|
*
|
|
402
|
-
* Walks the trie
|
|
403
|
-
*
|
|
454
|
+
* Walks the trie rather than reading `boostedNodes`, since a prefix answer is
|
|
455
|
+
* a subtree question and this only runs when a human asks it.
|
|
404
456
|
*/
|
|
405
457
|
collectSessionBoosted(prefix: string = ''): Candidate[] {
|
|
406
458
|
let node = this.root;
|
|
@@ -500,6 +552,15 @@ let lastPredictionDebug: {
|
|
|
500
552
|
}>;
|
|
501
553
|
} | null = null;
|
|
502
554
|
|
|
555
|
+
let lastPredictionOutcome: PredictionOutcome | null = null;
|
|
556
|
+
|
|
557
|
+
const recordPredictionOutcome = (outcome: PredictionOutcome): void => {
|
|
558
|
+
lastPredictionOutcome = outcome;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
/** The verdict from the most recent `predict` call. Always populated. */
|
|
562
|
+
export const getLastPredictionOutcome = (): PredictionOutcome | null => lastPredictionOutcome;
|
|
563
|
+
|
|
503
564
|
// ── Stabilization (QI-2): post-accept cooldown + whole-surface repetition ────
|
|
504
565
|
// Two guards that stop the accept→echo (`end to end` → `end to end to end`) and
|
|
505
566
|
// generally keep a just-accepted unit from being re-offered:
|
|
@@ -530,6 +591,25 @@ export const noteSuggestionAccepted = (surface: string): void => {
|
|
|
530
591
|
}
|
|
531
592
|
};
|
|
532
593
|
|
|
594
|
+
/**
|
|
595
|
+
* Whether `surface` is the one the user just accepted and is still inside its
|
|
596
|
+
* cooldown window.
|
|
597
|
+
*
|
|
598
|
+
* Read-only, unlike the advance inside `predict`: the cooldown is measured in
|
|
599
|
+
* predictions, and a caller asking whether it is active must not consume one of
|
|
600
|
+
* them. Exported for the harvest path, which displays without going through
|
|
601
|
+
* arbitration and so would otherwise re-offer what was just accepted.
|
|
602
|
+
*/
|
|
603
|
+
export const isSurfaceInAcceptCooldown = (surface: string): boolean => {
|
|
604
|
+
if (!acceptCooldown || acceptCooldown.surface !== surface.trim().toLowerCase()) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
return (
|
|
608
|
+
acceptCooldown.predictionsSince <= COOLDOWN_KEYSTROKES ||
|
|
609
|
+
performance.now() - acceptCooldown.ts < COOLDOWN_MS
|
|
610
|
+
);
|
|
611
|
+
};
|
|
612
|
+
|
|
533
613
|
/** Get vector for a word from the store. */
|
|
534
614
|
const getWordVector = (word: string): Float32Array | null => {
|
|
535
615
|
if (!vectorStore) {
|
|
@@ -839,6 +919,36 @@ export const incrementSessionFreq = (word: string): void => {
|
|
|
839
919
|
wordTrie.incrementSessionFreq(word);
|
|
840
920
|
};
|
|
841
921
|
|
|
922
|
+
/**
|
|
923
|
+
* Drop every L1 boost this session has accumulated.
|
|
924
|
+
*
|
|
925
|
+
* The vocabulary itself is left alone: only `sessionFreq` is cleared, so the
|
|
926
|
+
* tenant and generic frequencies a boost was sitting on top of survive. Called
|
|
927
|
+
* when the plugin decides the session it was learning for has ended — a new
|
|
928
|
+
* conversation, or a different page — since a boost is a claim about what is
|
|
929
|
+
* being discussed and that claim does not carry over.
|
|
930
|
+
*/
|
|
931
|
+
export const resetSessionBoosts = (): void => {
|
|
932
|
+
wordTrie.clearSessionBoosts();
|
|
933
|
+
// Anything memoized against the old boosts is now describing a session that
|
|
934
|
+
// no longer exists.
|
|
935
|
+
recallGeneration++;
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Which vocabulary already holds this surface, if any.
|
|
940
|
+
*
|
|
941
|
+
* Used by the inline-code harvester to drop terms the scored path can already
|
|
942
|
+
* serve, so that harvesting stays limited to words with no route to a
|
|
943
|
+
* suggestion today.
|
|
944
|
+
*/
|
|
945
|
+
export const lookupVocabularySource = (word: string): 'l2' | 'l3' | null => {
|
|
946
|
+
if (wordTrie.hasWord(word)) {
|
|
947
|
+
return 'l2';
|
|
948
|
+
}
|
|
949
|
+
return l3Trie.hasWord(word) ? 'l3' : null;
|
|
950
|
+
};
|
|
951
|
+
|
|
842
952
|
/**
|
|
843
953
|
* Prime session frequencies from a document page string.
|
|
844
954
|
*
|
|
@@ -884,7 +994,7 @@ export const ingestDocumentPage = (pageContent: string | undefined): void => {
|
|
|
884
994
|
* strongest boosts are the ones that change an ordering, and `boosted` still
|
|
885
995
|
* reports the full size, so the cap loses nothing but volume.
|
|
886
996
|
*/
|
|
887
|
-
const MAX_LISTED_SESSION_WORDS =
|
|
997
|
+
const MAX_LISTED_SESSION_WORDS = 100;
|
|
888
998
|
|
|
889
999
|
interface SessionWordSnapshot {
|
|
890
1000
|
/** Times this session has seen it: words typed plus words in ingested text. */
|
|
@@ -917,7 +1027,8 @@ export interface SessionSnapshot {
|
|
|
917
1027
|
* Only words the vocabulary already holds can carry a boost, because both writers
|
|
918
1028
|
* go through `incrementSessionFreq` and it only finds existing nodes. An ingested
|
|
919
1029
|
* word absent from the vocabulary is therefore missing from here and always will
|
|
920
|
-
* be
|
|
1030
|
+
* be — that gap is what the inline-code harvester covers, and those surfaces show
|
|
1031
|
+
* up under `__atlCtcDebug__.harvest()` instead.
|
|
921
1032
|
*/
|
|
922
1033
|
export const inspectSessionBoosts = (prefix?: string): SessionSnapshot => {
|
|
923
1034
|
const boosted = wordTrie.collectSessionBoosted(prefix ?? '');
|
|
@@ -957,8 +1068,15 @@ export interface PredictionResult {
|
|
|
957
1068
|
verifiedChars: number;
|
|
958
1069
|
verifiedTokens: number;
|
|
959
1070
|
};
|
|
960
|
-
/**
|
|
961
|
-
|
|
1071
|
+
/**
|
|
1072
|
+
* Evidence tier that authorized display. Tier A is never display-eligible.
|
|
1073
|
+
*
|
|
1074
|
+
* `session-harvest` never passes through this module: an inline-code surface
|
|
1075
|
+
* harvested from the session has no frequencies, vector or canonical token
|
|
1076
|
+
* ids, so it is authorized by being marked as code on a prefix the scored
|
|
1077
|
+
* path left unclaimed rather than by model evidence.
|
|
1078
|
+
*/
|
|
1079
|
+
evidenceTier: 'canonical-full-surface' | 'network-logit' | 'session-harvest';
|
|
962
1080
|
/**
|
|
963
1081
|
* Mean per-token log-probability of the verified prefix.
|
|
964
1082
|
*
|
|
@@ -1127,6 +1245,14 @@ export const predict = (textBefore: string): PredictionResult | null => {
|
|
|
1127
1245
|
// Kick off the load and skip this keystroke; the plugin also primes it on
|
|
1128
1246
|
// focus, so the tries are usually ready before the user types.
|
|
1129
1247
|
void loadDefaultVocabulary({ source: 'predict' }).catch(() => {});
|
|
1248
|
+
// Awaiting, not empty-handed: with no vocabulary loaded the harvester's own
|
|
1249
|
+
// intake filter has not been applied to anything either.
|
|
1250
|
+
recordPredictionOutcome({
|
|
1251
|
+
abstainReason: 'not-initialized',
|
|
1252
|
+
awaitingAsyncEvidence: true,
|
|
1253
|
+
scoredCandidateCount: 0,
|
|
1254
|
+
textBefore,
|
|
1255
|
+
});
|
|
1130
1256
|
return null;
|
|
1131
1257
|
}
|
|
1132
1258
|
|
|
@@ -1157,12 +1283,24 @@ export const predict = (textBefore: string): PredictionResult | null => {
|
|
|
1157
1283
|
priority: 0,
|
|
1158
1284
|
});
|
|
1159
1285
|
}
|
|
1286
|
+
recordPredictionOutcome({
|
|
1287
|
+
abstainReason: 'prefetch',
|
|
1288
|
+
awaitingAsyncEvidence: true,
|
|
1289
|
+
scoredCandidateCount: 0,
|
|
1290
|
+
textBefore,
|
|
1291
|
+
});
|
|
1160
1292
|
return null;
|
|
1161
1293
|
}
|
|
1162
1294
|
|
|
1163
1295
|
const trimmed = textBefore.trimEnd();
|
|
1164
1296
|
const trailingSurfaceToken = trimmed.match(TRAILING_SURFACE_TOKEN_REGEX)?.[0] ?? '';
|
|
1165
1297
|
if (trailingSurfaceToken.length === 0) {
|
|
1298
|
+
recordPredictionOutcome({
|
|
1299
|
+
abstainReason: 'no-surface-token',
|
|
1300
|
+
awaitingAsyncEvidence: false,
|
|
1301
|
+
scoredCandidateCount: 0,
|
|
1302
|
+
textBefore,
|
|
1303
|
+
});
|
|
1166
1304
|
return null;
|
|
1167
1305
|
}
|
|
1168
1306
|
const currentWord = trailingSurfaceToken;
|
|
@@ -1176,6 +1314,14 @@ export const predict = (textBefore: string): PredictionResult | null => {
|
|
|
1176
1314
|
|
|
1177
1315
|
// If every trie was empty for this prefix
|
|
1178
1316
|
if (canonicalMatched.length === 0) {
|
|
1317
|
+
// Terminal, and the only verdict that says the vocabulary has no claim on
|
|
1318
|
+
// this prefix at all — which is what makes it the harvester's cue.
|
|
1319
|
+
recordPredictionOutcome({
|
|
1320
|
+
abstainReason: 'no-candidate',
|
|
1321
|
+
awaitingAsyncEvidence: false,
|
|
1322
|
+
scoredCandidateCount: 0,
|
|
1323
|
+
textBefore,
|
|
1324
|
+
});
|
|
1179
1325
|
if (isAutocompleteDebugEnabled()) {
|
|
1180
1326
|
// eslint-disable-next-line no-console
|
|
1181
1327
|
console.log(
|
|
@@ -1935,6 +2081,12 @@ export const predict = (textBefore: string): PredictionResult | null => {
|
|
|
1935
2081
|
return ranked.some(hasJudgeableEvidence) ? 'below-posterior-gate' : 'no-evidence';
|
|
1936
2082
|
};
|
|
1937
2083
|
const abstainReason: CtcAbstainReason | null = resolveAbstainReason();
|
|
2084
|
+
recordPredictionOutcome({
|
|
2085
|
+
abstainReason,
|
|
2086
|
+
awaitingAsyncEvidence: hasUnresolvedPotential && !clearsWinnerMargin,
|
|
2087
|
+
scoredCandidateCount: ranked.length,
|
|
2088
|
+
textBefore,
|
|
2089
|
+
});
|
|
1938
2090
|
|
|
1939
2091
|
// The leader is the best-supported candidate, not the selected one: an
|
|
1940
2092
|
// evaluation that showed nothing is exactly the one whose posterior needs
|
|
@@ -1988,6 +2140,9 @@ export const predict = (textBefore: string): PredictionResult | null => {
|
|
|
1988
2140
|
'missing-artifact': 'canonical artifact coverage missing',
|
|
1989
2141
|
'no-candidate': 'nothing cleared scoring',
|
|
1990
2142
|
'no-evidence': 'full-surface evidence absent',
|
|
2143
|
+
// Recorded at the two exits above this block, so they never print here.
|
|
2144
|
+
'no-surface-token': 'no trailing surface token',
|
|
2145
|
+
'not-initialized': 'vocabulary not loaded',
|
|
1991
2146
|
prefetch: `prefetch: ${currentWord.length}/${DISPLAY_MIN_PREFIX_LENGTH} chars`,
|
|
1992
2147
|
'short-completion': `completion shorter than ${MIN_SUGGESTION_LENGTH} chars`,
|
|
1993
2148
|
'unresolved-rival': 'expanding plausible token-prefix groups',
|