@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
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
* Falls back to cold mode (freq-only) when vectors not yet loaded.
|
|
13
13
|
*
|
|
14
14
|
* Session personalization (L1): words the user types are incrementally boosted
|
|
15
|
-
* via incrementSessionFreq(), called on word boundaries from the plugin
|
|
15
|
+
* via incrementSessionFreq(), called on word boundaries from the plugin, and
|
|
16
|
+
* words in ingested context text via ingestDocumentPage(). What the session has
|
|
17
|
+
* boosted is visible at any time from the console: `__atlCtcDebug__.session()`,
|
|
18
|
+
* or `__atlCtcDebug__.session('poll')` for one family — see inspectSessionBoosts.
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
import { EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
|
|
@@ -35,6 +38,7 @@ import {
|
|
|
35
38
|
ctcTag,
|
|
36
39
|
isAutocompleteDebugEnabled,
|
|
37
40
|
isAutocompleteDebugVerbose,
|
|
41
|
+
registerCtcSessionInspector,
|
|
38
42
|
} from './debug-mode';
|
|
39
43
|
import {
|
|
40
44
|
loadGrammarDataAsync,
|
|
@@ -208,7 +212,7 @@ const DEBUG_TEXT_TAIL_CHARS = 120;
|
|
|
208
212
|
* `cold-competitor` and `unresolved-rival` are scheduling, and `no-candidate`
|
|
209
213
|
* is vocabulary coverage.
|
|
210
214
|
*/
|
|
211
|
-
type CtcAbstainReason =
|
|
215
|
+
export type CtcAbstainReason =
|
|
212
216
|
| 'below-posterior-gate'
|
|
213
217
|
| 'cold-competitor'
|
|
214
218
|
| 'empty-completion'
|
|
@@ -217,11 +221,34 @@ type CtcAbstainReason =
|
|
|
217
221
|
| 'missing-artifact'
|
|
218
222
|
| 'no-candidate'
|
|
219
223
|
| 'no-evidence'
|
|
224
|
+
| 'no-surface-token'
|
|
225
|
+
| 'not-initialized'
|
|
220
226
|
| 'prefetch'
|
|
221
227
|
| 'short-completion'
|
|
222
228
|
| 'unresolved-rival'
|
|
223
229
|
| 'winner-margin';
|
|
224
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
|
+
|
|
225
252
|
interface Candidate {
|
|
226
253
|
node: TrieNode;
|
|
227
254
|
word: string;
|
|
@@ -284,6 +311,17 @@ class TrieNode {
|
|
|
284
311
|
|
|
285
312
|
class WeightedWordTrie {
|
|
286
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>();
|
|
287
325
|
/** Highest tenantFreq seen — used to normalize freq scores at query time */
|
|
288
326
|
maxTenantFreq: number = 1;
|
|
289
327
|
|
|
@@ -363,6 +401,11 @@ class WeightedWordTrie {
|
|
|
363
401
|
return node.word !== null ? node : null;
|
|
364
402
|
}
|
|
365
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
|
+
|
|
366
409
|
/**
|
|
367
410
|
* Set the session frequency for a word.
|
|
368
411
|
* Returns true if the word exists in the trie.
|
|
@@ -373,6 +416,7 @@ class WeightedWordTrie {
|
|
|
373
416
|
return false;
|
|
374
417
|
}
|
|
375
418
|
node.sessionFreq = count;
|
|
419
|
+
this.boostedNodes.add(node);
|
|
376
420
|
return true;
|
|
377
421
|
}
|
|
378
422
|
|
|
@@ -386,8 +430,58 @@ class WeightedWordTrie {
|
|
|
386
430
|
return false;
|
|
387
431
|
}
|
|
388
432
|
node.sessionFreq += 1;
|
|
433
|
+
this.boostedNodes.add(node);
|
|
389
434
|
return true;
|
|
390
435
|
}
|
|
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
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Every word carrying a session boost, optionally limited to one prefix's
|
|
450
|
+
* subtree. Unlike `getCandidates` a word equal to the prefix is included,
|
|
451
|
+
* since the question here is what the session holds rather than what could
|
|
452
|
+
* still be typed.
|
|
453
|
+
*
|
|
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.
|
|
456
|
+
*/
|
|
457
|
+
collectSessionBoosted(prefix: string = ''): Candidate[] {
|
|
458
|
+
let node = this.root;
|
|
459
|
+
for (const char of prefix.toLowerCase()) {
|
|
460
|
+
const next = node.children.get(char);
|
|
461
|
+
if (!next) {
|
|
462
|
+
return [];
|
|
463
|
+
}
|
|
464
|
+
node = next;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const boosted: Candidate[] = [];
|
|
468
|
+
const stack: TrieNode[] = [node];
|
|
469
|
+
|
|
470
|
+
while (stack.length > 0) {
|
|
471
|
+
const current = stack.pop();
|
|
472
|
+
if (!current) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (current.word !== null && current.sessionFreq > 0) {
|
|
476
|
+
boosted.push({ word: current.word, node: current });
|
|
477
|
+
}
|
|
478
|
+
for (const child of current.children.values()) {
|
|
479
|
+
stack.push(child);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return boosted;
|
|
484
|
+
}
|
|
391
485
|
}
|
|
392
486
|
|
|
393
487
|
// L1/L2 Trie (Session + Atlassian Domain)
|
|
@@ -458,6 +552,15 @@ let lastPredictionDebug: {
|
|
|
458
552
|
}>;
|
|
459
553
|
} | null = null;
|
|
460
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
|
+
|
|
461
564
|
// ── Stabilization (QI-2): post-accept cooldown + whole-surface repetition ────
|
|
462
565
|
// Two guards that stop the accept→echo (`end to end` → `end to end to end`) and
|
|
463
566
|
// generally keep a just-accepted unit from being re-offered:
|
|
@@ -488,6 +591,25 @@ export const noteSuggestionAccepted = (surface: string): void => {
|
|
|
488
591
|
}
|
|
489
592
|
};
|
|
490
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
|
+
|
|
491
613
|
/** Get vector for a word from the store. */
|
|
492
614
|
const getWordVector = (word: string): Float32Array | null => {
|
|
493
615
|
if (!vectorStore) {
|
|
@@ -797,6 +919,36 @@ export const incrementSessionFreq = (word: string): void => {
|
|
|
797
919
|
wordTrie.incrementSessionFreq(word);
|
|
798
920
|
};
|
|
799
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
|
+
|
|
800
952
|
/**
|
|
801
953
|
* Prime session frequencies from a document page string.
|
|
802
954
|
*
|
|
@@ -823,7 +975,11 @@ export const ingestDocumentPage = (pageContent: string | undefined): void => {
|
|
|
823
975
|
}
|
|
824
976
|
|
|
825
977
|
if (isAutocompleteDebugEnabled() && validBoostedWords.size > 0) {
|
|
826
|
-
ctcTag(
|
|
978
|
+
ctcTag(
|
|
979
|
+
'init',
|
|
980
|
+
`L1 session primed ${validBoostedWords.size} words from page · __atlCtcDebug__.session() to inspect`,
|
|
981
|
+
CTC_STYLES.brand,
|
|
982
|
+
);
|
|
827
983
|
if (isAutocompleteDebugVerbose()) {
|
|
828
984
|
// eslint-disable-next-line no-console
|
|
829
985
|
console.dir(Array.from(validBoostedWords).sort());
|
|
@@ -831,6 +987,75 @@ export const ingestDocumentPage = (pageContent: string | undefined): void => {
|
|
|
831
987
|
}
|
|
832
988
|
};
|
|
833
989
|
|
|
990
|
+
/**
|
|
991
|
+
* How many boosted words `inspectSessionBoosts` lists.
|
|
992
|
+
*
|
|
993
|
+
* A page ingest can boost thousands, and a list that long is not read. The
|
|
994
|
+
* strongest boosts are the ones that change an ordering, and `boosted` still
|
|
995
|
+
* reports the full size, so the cap loses nothing but volume.
|
|
996
|
+
*/
|
|
997
|
+
const MAX_LISTED_SESSION_WORDS = 100;
|
|
998
|
+
|
|
999
|
+
interface SessionWordSnapshot {
|
|
1000
|
+
/** Times this session has seen it: words typed plus words in ingested text. */
|
|
1001
|
+
sessionFreq: number;
|
|
1002
|
+
/** No corpus frequency behind it, so L1 is the whole of its standing. */
|
|
1003
|
+
sessionOnly: boolean;
|
|
1004
|
+
surface: string;
|
|
1005
|
+
/** Corpus frequency shipped with the vocabulary, for scale against the boost. */
|
|
1006
|
+
tenantFreq: number;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
export interface SessionSnapshot {
|
|
1010
|
+
/** How many words hold a boost, whether or not they are listed below. */
|
|
1011
|
+
boosted: number;
|
|
1012
|
+
/** Ceiling on `words`; beyond it the weakest boosts are left out of the listing. */
|
|
1013
|
+
limit: number;
|
|
1014
|
+
/** The prefix asked about, when one was passed. */
|
|
1015
|
+
prefix?: string;
|
|
1016
|
+
/** Strongest boost first, then alphabetically. */
|
|
1017
|
+
words: SessionWordSnapshot[];
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Read the session's L1 boosts, optionally narrowed to a prefix.
|
|
1022
|
+
*
|
|
1023
|
+
* Installed as `__atlCtcDebug__.session()`, with `__atlCtcDebug__.session('poll')`
|
|
1024
|
+
* to ask about one family. Returned rather than logged, so the console renders it
|
|
1025
|
+
* as an inspectable object and a caller can assert on it.
|
|
1026
|
+
*
|
|
1027
|
+
* Only words the vocabulary already holds can carry a boost, because both writers
|
|
1028
|
+
* go through `incrementSessionFreq` and it only finds existing nodes. An ingested
|
|
1029
|
+
* word absent from the vocabulary is therefore missing from here and always will
|
|
1030
|
+
* be — that gap is what the inline-code harvester covers, and those surfaces show
|
|
1031
|
+
* up under `__atlCtcDebug__.harvest()` instead.
|
|
1032
|
+
*/
|
|
1033
|
+
export const inspectSessionBoosts = (prefix?: string): SessionSnapshot => {
|
|
1034
|
+
const boosted = wordTrie.collectSessionBoosted(prefix ?? '');
|
|
1035
|
+
return {
|
|
1036
|
+
boosted: boosted.length,
|
|
1037
|
+
limit: MAX_LISTED_SESSION_WORDS,
|
|
1038
|
+
...(prefix === undefined ? {} : { prefix }),
|
|
1039
|
+
words: boosted
|
|
1040
|
+
.sort(
|
|
1041
|
+
(a, b) =>
|
|
1042
|
+
b.node.sessionFreq - a.node.sessionFreq ||
|
|
1043
|
+
a.word.localeCompare(b.word, 'en', { numeric: true, sensitivity: 'base' }),
|
|
1044
|
+
)
|
|
1045
|
+
.slice(0, MAX_LISTED_SESSION_WORDS)
|
|
1046
|
+
.map(({ node, word }) => ({
|
|
1047
|
+
sessionFreq: node.sessionFreq,
|
|
1048
|
+
sessionOnly: node.tenantFreq === 0,
|
|
1049
|
+
surface: word,
|
|
1050
|
+
tenantFreq: node.tenantFreq,
|
|
1051
|
+
})),
|
|
1052
|
+
};
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
// At module scope so the console answers before the first keystroke, which is
|
|
1056
|
+
// when someone reaching for it usually asks.
|
|
1057
|
+
registerCtcSessionInspector(inspectSessionBoosts);
|
|
1058
|
+
|
|
834
1059
|
/**
|
|
835
1060
|
* Result of a prediction: the ghost tail to insert plus an immutable record of
|
|
836
1061
|
* the evidence that authorized the UI commitment.
|
|
@@ -843,8 +1068,15 @@ export interface PredictionResult {
|
|
|
843
1068
|
verifiedChars: number;
|
|
844
1069
|
verifiedTokens: number;
|
|
845
1070
|
};
|
|
846
|
-
/**
|
|
847
|
-
|
|
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';
|
|
848
1080
|
/**
|
|
849
1081
|
* Mean per-token log-probability of the verified prefix.
|
|
850
1082
|
*
|
|
@@ -1013,6 +1245,14 @@ export const predict = (textBefore: string): PredictionResult | null => {
|
|
|
1013
1245
|
// Kick off the load and skip this keystroke; the plugin also primes it on
|
|
1014
1246
|
// focus, so the tries are usually ready before the user types.
|
|
1015
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
|
+
});
|
|
1016
1256
|
return null;
|
|
1017
1257
|
}
|
|
1018
1258
|
|
|
@@ -1043,12 +1283,24 @@ export const predict = (textBefore: string): PredictionResult | null => {
|
|
|
1043
1283
|
priority: 0,
|
|
1044
1284
|
});
|
|
1045
1285
|
}
|
|
1286
|
+
recordPredictionOutcome({
|
|
1287
|
+
abstainReason: 'prefetch',
|
|
1288
|
+
awaitingAsyncEvidence: true,
|
|
1289
|
+
scoredCandidateCount: 0,
|
|
1290
|
+
textBefore,
|
|
1291
|
+
});
|
|
1046
1292
|
return null;
|
|
1047
1293
|
}
|
|
1048
1294
|
|
|
1049
1295
|
const trimmed = textBefore.trimEnd();
|
|
1050
1296
|
const trailingSurfaceToken = trimmed.match(TRAILING_SURFACE_TOKEN_REGEX)?.[0] ?? '';
|
|
1051
1297
|
if (trailingSurfaceToken.length === 0) {
|
|
1298
|
+
recordPredictionOutcome({
|
|
1299
|
+
abstainReason: 'no-surface-token',
|
|
1300
|
+
awaitingAsyncEvidence: false,
|
|
1301
|
+
scoredCandidateCount: 0,
|
|
1302
|
+
textBefore,
|
|
1303
|
+
});
|
|
1052
1304
|
return null;
|
|
1053
1305
|
}
|
|
1054
1306
|
const currentWord = trailingSurfaceToken;
|
|
@@ -1062,6 +1314,14 @@ export const predict = (textBefore: string): PredictionResult | null => {
|
|
|
1062
1314
|
|
|
1063
1315
|
// If every trie was empty for this prefix
|
|
1064
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
|
+
});
|
|
1065
1325
|
if (isAutocompleteDebugEnabled()) {
|
|
1066
1326
|
// eslint-disable-next-line no-console
|
|
1067
1327
|
console.log(
|
|
@@ -1821,6 +2081,12 @@ export const predict = (textBefore: string): PredictionResult | null => {
|
|
|
1821
2081
|
return ranked.some(hasJudgeableEvidence) ? 'below-posterior-gate' : 'no-evidence';
|
|
1822
2082
|
};
|
|
1823
2083
|
const abstainReason: CtcAbstainReason | null = resolveAbstainReason();
|
|
2084
|
+
recordPredictionOutcome({
|
|
2085
|
+
abstainReason,
|
|
2086
|
+
awaitingAsyncEvidence: hasUnresolvedPotential && !clearsWinnerMargin,
|
|
2087
|
+
scoredCandidateCount: ranked.length,
|
|
2088
|
+
textBefore,
|
|
2089
|
+
});
|
|
1824
2090
|
|
|
1825
2091
|
// The leader is the best-supported candidate, not the selected one: an
|
|
1826
2092
|
// evaluation that showed nothing is exactly the one whose posterior needs
|
|
@@ -1874,6 +2140,9 @@ export const predict = (textBefore: string): PredictionResult | null => {
|
|
|
1874
2140
|
'missing-artifact': 'canonical artifact coverage missing',
|
|
1875
2141
|
'no-candidate': 'nothing cleared scoring',
|
|
1876
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',
|
|
1877
2146
|
prefetch: `prefetch: ${currentWord.length}/${DISPLAY_MIN_PREFIX_LENGTH} chars`,
|
|
1878
2147
|
'short-completion': `completion shorter than ${MIN_SUGGESTION_LENGTH} chars`,
|
|
1879
2148
|
'unresolved-rival': 'expanding plausible token-prefix groups',
|