@atlaskit/editor-plugin-autocomplete 8.0.0 → 9.1.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +108 -0
  2. package/dist/cjs/analytics/ufo.js +2 -1
  3. package/dist/cjs/pm-plugins/artifact-loader.js +135 -0
  4. package/dist/cjs/pm-plugins/artifacts-manifest.js +3 -0
  5. package/dist/cjs/pm-plugins/autocomplete-plugin.js +498 -160
  6. package/dist/cjs/pm-plugins/canonical-lm-scoring.js +371 -0
  7. package/dist/cjs/pm-plugins/debug-mode.js +105 -7
  8. package/dist/cjs/pm-plugins/ghost-text-decoration.js +12 -3
  9. package/dist/cjs/pm-plugins/local-slow-lane-client.js +1202 -453
  10. package/dist/cjs/pm-plugins/scoring-pipeline.js +164 -19
  11. package/dist/cjs/pm-plugins/slow-lane-client.js +100 -37
  12. package/dist/cjs/pm-plugins/text-predictor.js +2099 -309
  13. package/dist/es2019/analytics/ufo.js +2 -0
  14. package/dist/es2019/pm-plugins/artifact-loader.js +70 -0
  15. package/dist/es2019/pm-plugins/artifacts-manifest.js +3 -0
  16. package/dist/es2019/pm-plugins/autocomplete-plugin.js +489 -152
  17. package/dist/es2019/pm-plugins/canonical-lm-scoring.js +270 -0
  18. package/dist/es2019/pm-plugins/debug-mode.js +102 -7
  19. package/dist/es2019/pm-plugins/ghost-text-decoration.js +12 -3
  20. package/dist/es2019/pm-plugins/local-slow-lane-client.js +940 -326
  21. package/dist/es2019/pm-plugins/scoring-pipeline.js +137 -13
  22. package/dist/es2019/pm-plugins/slow-lane-client.js +94 -37
  23. package/dist/es2019/pm-plugins/text-predictor.js +1692 -200
  24. package/dist/esm/analytics/ufo.js +2 -1
  25. package/dist/esm/pm-plugins/artifact-loader.js +128 -0
  26. package/dist/esm/pm-plugins/artifacts-manifest.js +3 -0
  27. package/dist/esm/pm-plugins/autocomplete-plugin.js +501 -163
  28. package/dist/esm/pm-plugins/canonical-lm-scoring.js +364 -0
  29. package/dist/esm/pm-plugins/debug-mode.js +104 -6
  30. package/dist/esm/pm-plugins/ghost-text-decoration.js +12 -3
  31. package/dist/esm/pm-plugins/local-slow-lane-client.js +1200 -451
  32. package/dist/esm/pm-plugins/scoring-pipeline.js +163 -18
  33. package/dist/esm/pm-plugins/slow-lane-client.js +100 -37
  34. package/dist/esm/pm-plugins/text-predictor.js +2101 -313
  35. package/dist/types/analytics/ufo.d.ts +1 -0
  36. package/dist/types/pm-plugins/artifact-loader.d.ts +44 -0
  37. package/dist/types/pm-plugins/artifacts-manifest.d.ts +3 -0
  38. package/dist/types/pm-plugins/autocomplete-plugin.d.ts +39 -0
  39. package/dist/types/pm-plugins/canonical-lm-scoring.d.ts +158 -0
  40. package/dist/types/pm-plugins/debug-mode.d.ts +55 -1
  41. package/dist/types/pm-plugins/ghost-text-decoration.d.ts +5 -2
  42. package/dist/types/pm-plugins/local-slow-lane-client.d.ts +87 -54
  43. package/dist/types/pm-plugins/scoring-pipeline.d.ts +33 -1
  44. package/dist/types/pm-plugins/slow-lane-client.d.ts +51 -3
  45. package/dist/types/pm-plugins/text-predictor.d.ts +156 -3
  46. package/package.json +2 -2
  47. package/src/analytics/ufo.ts +5 -0
  48. package/src/pm-plugins/artifact-loader.ts +92 -0
  49. package/src/pm-plugins/artifacts-manifest.ts +3 -0
  50. package/src/pm-plugins/autocomplete-plugin.ts +584 -154
  51. package/src/pm-plugins/canonical-lm-scoring.ts +412 -0
  52. package/src/pm-plugins/debug-mode.ts +134 -7
  53. package/src/pm-plugins/ghost-text-decoration.ts +13 -4
  54. package/src/pm-plugins/local-slow-lane-client.ts +1302 -386
  55. package/src/pm-plugins/scoring-pipeline.ts +167 -17
  56. package/src/pm-plugins/slow-lane-client.ts +147 -62
  57. package/src/pm-plugins/text-predictor.ts +2119 -254
  58. package/CANONICAL_FIX__DO_NOT_USE_ME_A/package.json +0 -8
  59. package/CANONICAL_FIX__DO_NOT_USE_ME_B/package.json +0 -8
  60. package/CANONICAL_FIX__DO_NOT_USE_ME_C/package.json +0 -8
  61. package/dist/cjs/entry-points/src-pm-plugins-autocomplete-plugin.js +0 -18
  62. package/dist/cjs/entry-points/src-pm-plugins-slow-lane-client.js +0 -36
  63. package/dist/cjs/entry-points/src-pm-plugins-text-predictor.js +0 -66
  64. package/dist/es2019/entry-points/src-pm-plugins-autocomplete-plugin.js +0 -2
  65. package/dist/es2019/entry-points/src-pm-plugins-slow-lane-client.js +0 -2
  66. package/dist/es2019/entry-points/src-pm-plugins-text-predictor.js +0 -2
  67. package/dist/esm/entry-points/src-pm-plugins-autocomplete-plugin.js +0 -2
  68. package/dist/esm/entry-points/src-pm-plugins-slow-lane-client.js +0 -2
  69. package/dist/esm/entry-points/src-pm-plugins-text-predictor.js +0 -2
  70. package/dist/types/entry-points/src-pm-plugins-autocomplete-plugin.d.ts +0 -2
  71. package/dist/types/entry-points/src-pm-plugins-slow-lane-client.d.ts +0 -2
  72. package/dist/types/entry-points/src-pm-plugins-text-predictor.d.ts +0 -2
  73. package/scripts/gen_first_token_to_words.py +0 -170
  74. package/src/entry-points/src-pm-plugins-autocomplete-plugin.ts +0 -7
  75. package/src/entry-points/src-pm-plugins-slow-lane-client.ts +0 -13
  76. package/src/entry-points/src-pm-plugins-text-predictor.ts +0 -14
  77. package/src/pm-plugins/autocomplete-plugin/package.json +0 -8
  78. package/src/pm-plugins/slow-lane-client/package.json +0 -8
  79. package/src/pm-plugins/text-predictor/package.json +0 -8
@@ -14,6 +14,11 @@ import { isAutocompleteDebugEnabled } from './debug-mode';
14
14
 
15
15
  // ─── Types ──────────────────────────────────────────────────
16
16
 
17
+ /**
18
+ * A completion term is `(term, termType)`. `word` is the v1 default; `bigram`
19
+ * (2 tokens) and `phrase` (3 tokens) are multi-word units mined by the producer.
20
+ */
21
+
17
22
  /** Metadata returned by the grammar filter for debug logging in the caller. */
18
23
 
19
24
  // ─── Scoring Constants ──────────────────────────────────────
@@ -21,9 +26,25 @@ import { isAutocompleteDebugEnabled } from './debug-mode';
21
26
  const ALPHA = 0.5;
22
27
  const BETA = 0.5;
23
28
  const NEUTRAL_SCORE = 0.5;
29
+ // Multiplicative Stage-1 boost applied to a multi-word term proportional to its
30
+ // (clamped [0,1]) NPMI association, so a strongly-collocated phrase outranks a
31
+ // coincidental word sequence of comparable freq/semantics. Tunable.
32
+ const ASSOCIATION_BOOST = 0.5;
24
33
  export const STAGE1_WEIGHT = 0.35;
25
34
  export const STAGE2_WEIGHT = 0.65;
26
35
  export const MIN_STAGE1_SCORE = 0.35;
36
+ /**
37
+ * How far the leading candidate must sit above the runner-up on the blended
38
+ * score before a ghost is committed.
39
+ *
40
+ * Lives beside the weights it is measured in, because it is only meaningful on
41
+ * that scale — and because both the arbitration that applies it and the
42
+ * scheduler that decides when reading more tokens can no longer change the
43
+ * outcome have to use the same number. When those two drifted apart, the
44
+ * scheduler stopped early on a margin the arbitration did not recognise and the
45
+ * abstention that followed was invisible from either side.
46
+ */
47
+ export const MIN_WINNER_MARGIN = 0.08;
27
48
  const L1_SESSION_CAP = 1.2;
28
49
  // Minimum prefix-payload max LM probability before Stage 2 activates.
29
50
  // Below this threshold the LM signal is too weak to suppress Stage 1 — finalScore
@@ -42,13 +63,32 @@ let posTags = new Map();
42
63
  * without suggesting these words to the user.
43
64
  */
44
65
  let ghostPosTags = {};
66
+ let grammarDataPromise;
45
67
 
46
68
  /**
47
69
  * Precomputed map from each POS tag to the set of allowed next POS tags, so
48
70
  * applyGrammarFilter never re-iterates the transition rules per call.
49
71
  */
50
72
  let precomputedAllowedByPos = new Map();
51
- let grammarDataPromise;
73
+ const isPosTagsRecord = payload => {
74
+ if (payload == null || typeof payload !== 'object') {
75
+ return false;
76
+ }
77
+ return Object.values(payload).every(value => Array.isArray(value) && value.every(entry => typeof entry === 'string'));
78
+ };
79
+ const isGrammarTransitions = payload => {
80
+ if (payload == null || typeof payload !== 'object') {
81
+ return false;
82
+ }
83
+ const transitions = payload.transitions;
84
+ if (transitions == null || typeof transitions !== 'object') {
85
+ return false;
86
+ }
87
+ return Object.values(transitions).every(value => {
88
+ const allowed = value === null || value === void 0 ? void 0 : value.allowed;
89
+ return Array.isArray(allowed) && allowed.every(entry => typeof entry === 'string');
90
+ });
91
+ };
52
92
  const setGrammarData = data => {
53
93
  var _data$grammarTransiti;
54
94
  posTags = new Map(Object.entries(data.posTags));
@@ -79,12 +119,24 @@ export const loadGrammarDataAsync = options => {
79
119
  });
80
120
  try {
81
121
  const [posTagsData, ghostPosTagsData, grammarTransitionsData] = await Promise.all([fetchArtifactJson(ARTIFACT_NAME.POS_TAGS), fetchArtifactJson(ARTIFACT_NAME.GHOST_POS_TAGS), fetchArtifactJson(ARTIFACT_NAME.GRAMMAR_TRANSITIONS)]);
122
+
123
+ // A missing wrapper and a malformed ruleset are reported separately: the
124
+ // first points at the publishing step, the second at the payload itself.
82
125
  if ((grammarTransitionsData === null || grammarTransitionsData === void 0 ? void 0 : grammarTransitionsData.transitions) == null) {
83
126
  throw new Error(`[scoring-pipeline] ${ARTIFACT_NAME.GRAMMAR_TRANSITIONS} is missing its \`transitions\` wrapper`);
84
127
  }
128
+ if (!isGrammarTransitions(grammarTransitionsData)) {
129
+ throw new Error(`[scoring-pipeline] ${ARTIFACT_NAME.GRAMMAR_TRANSITIONS} payload shape was invalid`);
130
+ }
131
+ if (!isPosTagsRecord(posTagsData)) {
132
+ throw new Error(`[scoring-pipeline] ${ARTIFACT_NAME.POS_TAGS} payload shape was invalid`);
133
+ }
134
+ if (!isPosTagsRecord(ghostPosTagsData)) {
135
+ throw new Error(`[scoring-pipeline] ${ARTIFACT_NAME.GHOST_POS_TAGS} payload shape was invalid`);
136
+ }
85
137
  setGrammarData({
86
- posTags: posTagsData !== null && posTagsData !== void 0 ? posTagsData : {},
87
- ghostPosTags: ghostPosTagsData !== null && ghostPosTagsData !== void 0 ? ghostPosTagsData : {},
138
+ posTags: posTagsData,
139
+ ghostPosTags: ghostPosTagsData,
88
140
  grammarTransitions: grammarTransitionsData
89
141
  });
90
142
  succeedExp(EXPERIENCE_NAME.LOAD_GRAMMAR, 'singleton', {
@@ -198,7 +250,16 @@ function applyGrammarFilter(candidates, previousWord) {
198
250
  const filtered = [];
199
251
  const dropped = [];
200
252
  for (const entry of candidates) {
201
- const candidateTags = posTags.get(entry.candidate.word.toLowerCase());
253
+ // The producer validates a multi-word term's *internal* grammar offline
254
+ // (boundary-clean, noun-headed, AUP-passed — spec §9), but nothing judges
255
+ // the transition into it. Exempting multi-word terms therefore let a noun
256
+ // this filter had just rejected re-enter wrapped in a bigram — "be" [AUX]
257
+ // dropped "quality" and then displayed "quality assurance". Only the term's
258
+ // first word takes part in the transition, so gate every term on that.
259
+ const surface = entry.candidate.word;
260
+ const firstSpace = surface.indexOf(' ');
261
+ const transitionWord = firstSpace === -1 ? surface : surface.slice(0, firstSpace);
262
+ const candidateTags = posTags.get(transitionWord.toLowerCase());
202
263
 
203
264
  // If candidate has no tags (unknown word), let it pass to be safe
204
265
  if (!candidateTags || candidateTags.length === 0) {
@@ -239,20 +300,35 @@ function getLmScore(word, lmLogits) {
239
300
 
240
301
  // ─── Public API ─────────────────────────────────────────────
241
302
 
242
- export function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxTenantFreq, previousWord) {
303
+ /**
304
+ * Apply Stage 1, grammar, and nullable context-local LM evidence to candidates.
305
+ */
306
+ export function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxFreqByType, previousWord, getCanonicalLmEvidence) {
243
307
  var _grammarMeta$dropped;
244
308
  // Stage 1
245
309
  const stage1Results = candidates.map(candidate => {
310
+ var _candidate$termType;
311
+ const termType = (_candidate$termType = candidate.termType) !== null && _candidate$termType !== void 0 ? _candidate$termType : 'word';
312
+ // Normalize freq within the candidate's own term-type population so a
313
+ // phrase (whose absolute counts are far below top unigrams) isn't crushed
314
+ // by the unigram max.
315
+ const maxFreq = maxFreqByType[termType] || 1;
246
316
  const {
247
317
  semanticScore,
248
318
  freqScore,
249
319
  stage1Score
250
- } = scoreStage1(candidate, contextVector, getWordVector, maxTenantFreq);
320
+ } = scoreStage1(candidate, contextVector, getWordVector, maxFreq);
321
+ // Multi-word units get an NPMI association boost (Stage-1 only).
322
+ let adjustedStage1 = stage1Score;
323
+ if (termType !== 'word' && typeof candidate.association === 'number') {
324
+ const assoc = Math.max(0, Math.min(1, candidate.association));
325
+ adjustedStage1 = stage1Score * (1 + ASSOCIATION_BOOST * assoc);
326
+ }
251
327
  return {
252
328
  candidate,
253
329
  semanticScore,
254
330
  freqScore,
255
- stage1Score
331
+ stage1Score: adjustedStage1
256
332
  };
257
333
  });
258
334
  const stage1Survivors = [];
@@ -277,26 +353,74 @@ export function rankCandidates(candidates, contextVector, getWordVector, lmLogit
277
353
  const values = Object.values(lmLogits);
278
354
  if (values.length > 0) lmMax = Math.max(...values);
279
355
  }
356
+ const canonicalEvidenceBySurface = new Map();
357
+ const maxLmRankingValueByTierAndContext = new Map();
358
+ for (const entry of filtered) {
359
+ var _getCanonicalLmEviden;
360
+ const evidence = (_getCanonicalLmEviden = getCanonicalLmEvidence === null || getCanonicalLmEvidence === void 0 ? void 0 : getCanonicalLmEvidence(entry.candidate.word)) !== null && _getCanonicalLmEviden !== void 0 ? _getCanonicalLmEviden : null;
361
+ if (!evidence) {
362
+ continue;
363
+ }
364
+ canonicalEvidenceBySurface.set(entry.candidate.word, evidence);
365
+ const rankingValue = evidence.source === 'canonical-full-surface' ? evidence.meanTokenLogProbability : evidence.rawLogit;
366
+ if (rankingValue !== undefined && Number.isFinite(rankingValue)) {
367
+ var _maxLmRankingValueByT;
368
+ const key = `${evidence.source}\u0000${evidence.contextKey}`;
369
+ const current = (_maxLmRankingValueByT = maxLmRankingValueByTierAndContext.get(key)) !== null && _maxLmRankingValueByT !== void 0 ? _maxLmRankingValueByT : -Infinity;
370
+ if (rankingValue > current) {
371
+ maxLmRankingValueByTierAndContext.set(key, rankingValue);
372
+ }
373
+ }
374
+ }
280
375
  const scored = filtered.map(entry => {
376
+ var _entry$candidate$term, _canonicalEvidenceByS;
377
+ const termType = (_entry$candidate$term = entry.candidate.termType) !== null && _entry$candidate$term !== void 0 ? _entry$candidate$term : 'word';
378
+ const isMultiWord = termType !== 'word';
281
379
  let lmScore = 0;
282
380
  let finalScore = entry.stage1Score;
283
- if (lmLogits && lmMax >= LM_GATE_THRESHOLD) {
381
+ let lmEvidence = (_canonicalEvidenceByS = canonicalEvidenceBySurface.get(entry.candidate.word)) !== null && _canonicalEvidenceByS !== void 0 ? _canonicalEvidenceByS : null;
382
+ if (lmEvidence) {
383
+ const rankingValue = lmEvidence.source === 'canonical-full-surface' ? lmEvidence.meanTokenLogProbability : lmEvidence.rawLogit;
384
+ const maxRankingValue = maxLmRankingValueByTierAndContext.get(`${lmEvidence.source}\u0000${lmEvidence.contextKey}`);
385
+ if (rankingValue !== undefined && maxRankingValue !== undefined) {
386
+ lmEvidence = {
387
+ ...lmEvidence,
388
+ score: Math.exp(rankingValue - maxRankingValue)
389
+ };
390
+ }
391
+ }
392
+ if (lmEvidence) {
393
+ lmScore = lmEvidence.score;
394
+ finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
395
+ } else if (!isMultiWord && lmLogits && lmMax >= LM_GATE_THRESHOLD) {
396
+ // Backward-compatible network slow-lane path.
284
397
  const rawLm = getLmScore(entry.candidate.word, lmLogits);
285
398
  if (rawLm !== 0) {
286
399
  const logitDiff = Math.log(rawLm) - Math.log(lmMax);
287
400
  lmScore = Math.exp(logitDiff);
401
+ lmEvidence = {
402
+ contextKey: 'network',
403
+ score: lmScore,
404
+ separatorKind: 'whitespace',
405
+ source: 'network-logit',
406
+ totalSurfaceCharCount: entry.candidate.word.length,
407
+ // The legacy BE word map does not report canonical token depth.
408
+ totalTokenCount: 0,
409
+ verifiedCharCount: 0,
410
+ verifiedTokenCount: 0
411
+ };
412
+ finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
288
413
  }
289
- // Words absent from the prefix-filtered payload get lmScore = 0,
290
- // not 0.05, so they don't outrank genuine LM predictions.
291
-
292
- finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
293
414
  }
294
415
  return {
295
416
  word: entry.candidate.word,
296
417
  freqScore: entry.freqScore,
297
418
  semanticScore: entry.semanticScore,
298
419
  lmScore,
299
- finalScore
420
+ lmEvidence,
421
+ finalScore,
422
+ stage1Score: entry.stage1Score,
423
+ termType
300
424
  };
301
425
  });
302
426
  scored.sort((a, b) => {
@@ -8,7 +8,8 @@
8
8
  */
9
9
 
10
10
  import { abortExp, EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
11
- import { isAutocompleteDebugEnabled } from './debug-mode';
11
+ import { buildAutocompleteGatewayUrl } from './artifact-loader';
12
+ import { CTC_STYLES, ctcTag } from './debug-mode';
12
13
 
13
14
  // ─── Types ───────────────────────────────────────────────────────────────────
14
15
 
@@ -20,8 +21,6 @@ import { isAutocompleteDebugEnabled } from './debug-mode';
20
21
 
21
22
  // eslint-disable-next-line require-unicode-regexp
22
23
  const WORD_BOUNDARY_CHARS = /[\s.,;:!?]/;
23
- // eslint-disable-next-line require-unicode-regexp
24
- const TRAILING_SLASH_REGEX = /\/$/;
25
24
  const DEFAULT_DEBOUNCE_MS = 300;
26
25
 
27
26
  /**
@@ -56,6 +55,7 @@ export const createSlowLaneClient = config => {
56
55
  };
57
56
  let debounceTimer = null;
58
57
  let lastRequestedText = '';
58
+ let storedContextInput = null;
59
59
  let storedContextVector = null;
60
60
  let storedLmLogits = null;
61
61
  let requestSeq = 0;
@@ -64,7 +64,7 @@ export const createSlowLaneClient = config => {
64
64
  if (!text || text.trim().length === 0) {
65
65
  return;
66
66
  }
67
- const url = `${baseUrl.replace(TRAILING_SLASH_REGEX, '')}${endpoint}`;
67
+ const url = buildAutocompleteGatewayUrl(endpoint, baseUrl);
68
68
  const payload = {
69
69
  text,
70
70
  session_id: sessionId
@@ -76,16 +76,7 @@ export const createSlowLaneClient = config => {
76
76
  surface
77
77
  } : {})
78
78
  });
79
- if (isAutocompleteDebugEnabled()) {
80
- // eslint-disable-next-line no-console
81
- console.groupCollapsed(`%c[SlowLane] %c📤 Sending context | ${text.length} chars`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
82
- text.split('\n').forEach((line, i, arr) => {
83
- // eslint-disable-next-line no-console
84
- console.log(` ${i === arr.length - 1 ? '▶' : ' '} ${line}`);
85
- });
86
- // eslint-disable-next-line no-console
87
- console.groupEnd();
88
- }
79
+ ctcTag('signal', `📤 network context sent · ${text.length} chars`, CTC_STYLES.section);
89
80
  try {
90
81
  const res = await fetchFn(url, {
91
82
  method: 'POST',
@@ -93,6 +84,7 @@ export const createSlowLaneClient = config => {
93
84
  body: JSON.stringify(payload)
94
85
  });
95
86
  if (!res.ok) {
87
+ storedContextInput = null;
96
88
  storedContextVector = null;
97
89
  storedLmLogits = null;
98
90
  failExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
@@ -103,33 +95,23 @@ export const createSlowLaneClient = config => {
103
95
  surface
104
96
  } : {})
105
97
  });
106
- if (isAutocompleteDebugEnabled()) {
107
- // eslint-disable-next-line no-console
108
- console.log(`%c[SlowLane] %c❌ Request failed (${res.status})`, 'color: #9c27b0; font-weight: bold;', 'color: #f44336;');
109
- }
98
+ ctcTag('signal', `❌ network request failed (${res.status})`, CTC_STYLES.bad);
110
99
  return;
111
100
  }
112
101
  const data = await res.json();
113
102
  if (data.semantic_vector && Array.isArray(data.semantic_vector)) {
114
103
  storedContextVector = new Float32Array(data.semantic_vector);
104
+ storedContextInput = text;
115
105
  } else {
116
106
  storedContextVector = null;
107
+ storedContextInput = null;
117
108
  }
118
109
  if (data.lm_logits && typeof data.lm_logits === 'object') {
119
110
  storedLmLogits = data.lm_logits;
120
111
  } else {
121
112
  storedLmLogits = null;
122
113
  }
123
- if (isAutocompleteDebugEnabled()) {
124
- // eslint-disable-next-line no-console
125
- console.groupCollapsed(`%c[SlowLane] %c📥 Response received`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
126
- // eslint-disable-next-line no-console
127
- console.log(storedContextVector ? `✅ semantic_vector: ${storedContextVector.length} dims` : '❌ No semantic_vector');
128
- // eslint-disable-next-line no-console
129
- console.log(storedLmLogits ? `✅ lm_logits: ${Object.keys(storedLmLogits).length} tokens` : '❌ No lm_logits');
130
- // eslint-disable-next-line no-console
131
- console.groupEnd();
132
- }
114
+ ctcTag('signal', `📥 network response · ${storedContextVector ? `semantic ✅ ${storedContextVector.length}d` : 'semantic ❌'} · ${storedLmLogits ? `logits ✅ ${Object.keys(storedLmLogits).length}tok` : 'logits ❌'}`, CTC_STYLES.section);
133
115
  succeedExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
134
116
  textLength: text.length,
135
117
  hasVector: storedContextVector !== null,
@@ -146,6 +128,7 @@ export const createSlowLaneClient = config => {
146
128
  });
147
129
  // eslint-disable-next-line no-unused-vars
148
130
  } catch (e) {
131
+ storedContextInput = null;
149
132
  storedContextVector = null;
150
133
  storedLmLogits = null;
151
134
  failExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
@@ -155,10 +138,7 @@ export const createSlowLaneClient = config => {
155
138
  surface
156
139
  } : {})
157
140
  });
158
- if (isAutocompleteDebugEnabled()) {
159
- // eslint-disable-next-line no-console
160
- console.log('%c[SlowLane] %c❌ Network error — context cleared', 'color: #9c27b0; font-weight: bold;', 'color: #f44336;');
161
- }
141
+ ctcTag('signal', '❌ network error — context cleared', CTC_STYLES.bad);
162
142
  } finally {
163
143
  if (inflightRequestId === requestId) {
164
144
  inflightRequestId = null;
@@ -187,9 +167,11 @@ export const createSlowLaneClient = config => {
187
167
  };
188
168
  return {
189
169
  updateContext: updateContextDebounced,
170
+ getContextInput: () => storedContextInput,
190
171
  getContextVector: () => storedContextVector,
191
172
  getLmLogits: () => storedLmLogits,
192
173
  setContextVector: vector => {
174
+ storedContextInput = null;
193
175
  storedContextVector = vector;
194
176
  },
195
177
  setLmLogits: logits => {
@@ -199,14 +181,89 @@ export const createSlowLaneClient = config => {
199
181
  };
200
182
  };
201
183
  let defaultSlowLaneClient = null;
202
- export const setDefaultSlowLaneClient = client => {
184
+ let defaultSlowLaneClientRegistration = null;
185
+ const supportsCanonicalSurfaceScoring = client => typeof (client === null || client === void 0 ? void 0 : client.primeBoundaryLm) === 'function' && typeof (client === null || client === void 0 ? void 0 : client.requestProgressiveSurfaceScores) === 'function';
186
+ export const setDefaultSlowLaneClient = (client, registration) => {
203
187
  defaultSlowLaneClient = client;
188
+ defaultSlowLaneClientRegistration = client ? registration !== null && registration !== void 0 ? registration : {
189
+ clientId: 'untracked',
190
+ kind: supportsCanonicalSurfaceScoring(client) ? 'localLlm' : 'server'
191
+ } : null;
192
+ };
193
+
194
+ /**
195
+ * Clear the shared client only when the caller still owns the registration.
196
+ *
197
+ * Multiple editor instances can briefly overlap during a React/editor remount.
198
+ * An older instance must not disconnect the newer instance when its delayed
199
+ * teardown runs after the newer client has registered.
200
+ */
201
+ export const clearDefaultSlowLaneClient = client => {
202
+ if (defaultSlowLaneClient !== client) {
203
+ return false;
204
+ }
205
+ defaultSlowLaneClient = null;
206
+ defaultSlowLaneClientRegistration = null;
207
+ return true;
208
+ };
209
+ export const getDefaultSlowLaneClientStatus = () => {
210
+ var _defaultSlowLaneClien, _defaultSlowLaneClien2, _defaultSlowLaneClien3, _defaultSlowLaneClien4, _defaultSlowLaneClien5, _defaultSlowLaneClien6, _defaultSlowLaneClien7;
211
+ const canonicalScoringSupported = supportsCanonicalSurfaceScoring(defaultSlowLaneClient);
212
+ const isLocalClient = ((_defaultSlowLaneClien = defaultSlowLaneClientRegistration) === null || _defaultSlowLaneClien === void 0 ? void 0 : _defaultSlowLaneClien.kind) === 'localLlm';
213
+ return {
214
+ registered: defaultSlowLaneClient !== null,
215
+ clientId: (_defaultSlowLaneClien2 = (_defaultSlowLaneClien3 = defaultSlowLaneClientRegistration) === null || _defaultSlowLaneClien3 === void 0 ? void 0 : _defaultSlowLaneClien3.clientId) !== null && _defaultSlowLaneClien2 !== void 0 ? _defaultSlowLaneClien2 : null,
216
+ clientKind: (_defaultSlowLaneClien4 = (_defaultSlowLaneClien5 = defaultSlowLaneClientRegistration) === null || _defaultSlowLaneClien5 === void 0 ? void 0 : _defaultSlowLaneClien5.kind) !== null && _defaultSlowLaneClien4 !== void 0 ? _defaultSlowLaneClien4 : null,
217
+ canonicalScoringSupported,
218
+ localModelReady: isLocalClient && typeof ((_defaultSlowLaneClien6 = defaultSlowLaneClient) === null || _defaultSlowLaneClien6 === void 0 ? void 0 : _defaultSlowLaneClien6.isReady) === 'function' ? defaultSlowLaneClient.isReady() : null,
219
+ canonicalSurfaceCount: canonicalScoringSupported && typeof ((_defaultSlowLaneClien7 = defaultSlowLaneClient) === null || _defaultSlowLaneClien7 === void 0 ? void 0 : _defaultSlowLaneClien7.getCanonicalSurfaceCount) === 'function' ? defaultSlowLaneClient.getCanonicalSurfaceCount() : null
220
+ };
204
221
  };
205
222
  export const getStoredContextVector = () => {
206
- var _defaultSlowLaneClien, _defaultSlowLaneClien2;
207
- return (_defaultSlowLaneClien = (_defaultSlowLaneClien2 = defaultSlowLaneClient) === null || _defaultSlowLaneClien2 === void 0 ? void 0 : _defaultSlowLaneClien2.getContextVector()) !== null && _defaultSlowLaneClien !== void 0 ? _defaultSlowLaneClien : null;
223
+ var _defaultSlowLaneClien8, _defaultSlowLaneClien9;
224
+ return (_defaultSlowLaneClien8 = (_defaultSlowLaneClien9 = defaultSlowLaneClient) === null || _defaultSlowLaneClien9 === void 0 ? void 0 : _defaultSlowLaneClien9.getContextVector()) !== null && _defaultSlowLaneClien8 !== void 0 ? _defaultSlowLaneClien8 : null;
225
+ };
226
+ export const getStoredContextInput = () => {
227
+ var _defaultSlowLaneClien0, _defaultSlowLaneClien1, _defaultSlowLaneClien10;
228
+ return (_defaultSlowLaneClien0 = (_defaultSlowLaneClien1 = defaultSlowLaneClient) === null || _defaultSlowLaneClien1 === void 0 ? void 0 : (_defaultSlowLaneClien10 = _defaultSlowLaneClien1.getContextInput) === null || _defaultSlowLaneClien10 === void 0 ? void 0 : _defaultSlowLaneClien10.call(_defaultSlowLaneClien1)) !== null && _defaultSlowLaneClien0 !== void 0 ? _defaultSlowLaneClien0 : null;
208
229
  };
209
230
  export const getStoredLmLogits = () => {
210
- var _defaultSlowLaneClien3, _defaultSlowLaneClien4;
211
- return (_defaultSlowLaneClien3 = (_defaultSlowLaneClien4 = defaultSlowLaneClient) === null || _defaultSlowLaneClien4 === void 0 ? void 0 : _defaultSlowLaneClien4.getLmLogits()) !== null && _defaultSlowLaneClien3 !== void 0 ? _defaultSlowLaneClien3 : null;
231
+ var _defaultSlowLaneClien11, _defaultSlowLaneClien12;
232
+ return (_defaultSlowLaneClien11 = (_defaultSlowLaneClien12 = defaultSlowLaneClient) === null || _defaultSlowLaneClien12 === void 0 ? void 0 : _defaultSlowLaneClien12.getLmLogits()) !== null && _defaultSlowLaneClien11 !== void 0 ? _defaultSlowLaneClien11 : null;
233
+ };
234
+ export const getBoundaryLmState = contextKey => {
235
+ var _defaultSlowLaneClien13, _defaultSlowLaneClien14, _defaultSlowLaneClien15;
236
+ return (_defaultSlowLaneClien13 = (_defaultSlowLaneClien14 = defaultSlowLaneClient) === null || _defaultSlowLaneClien14 === void 0 ? void 0 : (_defaultSlowLaneClien15 = _defaultSlowLaneClien14.getBoundaryLmState) === null || _defaultSlowLaneClien15 === void 0 ? void 0 : _defaultSlowLaneClien15.call(_defaultSlowLaneClien14, contextKey)) !== null && _defaultSlowLaneClien13 !== void 0 ? _defaultSlowLaneClien13 : null;
237
+ };
238
+ export const getCanonicalSurfaceTokenIds = surface => {
239
+ var _defaultSlowLaneClien16, _defaultSlowLaneClien17, _defaultSlowLaneClien18;
240
+ return (_defaultSlowLaneClien16 = (_defaultSlowLaneClien17 = defaultSlowLaneClient) === null || _defaultSlowLaneClien17 === void 0 ? void 0 : (_defaultSlowLaneClien18 = _defaultSlowLaneClien17.getCanonicalSurfaceTokenIds) === null || _defaultSlowLaneClien18 === void 0 ? void 0 : _defaultSlowLaneClien18.call(_defaultSlowLaneClien17, surface)) !== null && _defaultSlowLaneClien16 !== void 0 ? _defaultSlowLaneClien16 : null;
241
+ };
242
+ export const getSurfaceScore = (contextKey, surface) => {
243
+ var _defaultSlowLaneClien19, _defaultSlowLaneClien20, _defaultSlowLaneClien21;
244
+ return (_defaultSlowLaneClien19 = (_defaultSlowLaneClien20 = defaultSlowLaneClient) === null || _defaultSlowLaneClien20 === void 0 ? void 0 : (_defaultSlowLaneClien21 = _defaultSlowLaneClien20.getSurfaceScore) === null || _defaultSlowLaneClien21 === void 0 ? void 0 : _defaultSlowLaneClien21.call(_defaultSlowLaneClien20, contextKey, surface)) !== null && _defaultSlowLaneClien19 !== void 0 ? _defaultSlowLaneClien19 : null;
245
+ };
246
+ export const getProgressiveSurfaceEvidence = (contextKey, surface) => {
247
+ var _defaultSlowLaneClien22, _defaultSlowLaneClien23, _defaultSlowLaneClien24;
248
+ return (_defaultSlowLaneClien22 = (_defaultSlowLaneClien23 = defaultSlowLaneClient) === null || _defaultSlowLaneClien23 === void 0 ? void 0 : (_defaultSlowLaneClien24 = _defaultSlowLaneClien23.getProgressiveSurfaceEvidence) === null || _defaultSlowLaneClien24 === void 0 ? void 0 : _defaultSlowLaneClien24.call(_defaultSlowLaneClien23, contextKey, surface)) !== null && _defaultSlowLaneClien22 !== void 0 ? _defaultSlowLaneClien22 : null;
249
+ };
250
+ export const primeBoundaryLm = input => {
251
+ var _defaultSlowLaneClien25, _defaultSlowLaneClien26;
252
+ (_defaultSlowLaneClien25 = defaultSlowLaneClient) === null || _defaultSlowLaneClien25 === void 0 ? void 0 : (_defaultSlowLaneClien26 = _defaultSlowLaneClien25.primeBoundaryLm) === null || _defaultSlowLaneClien26 === void 0 ? void 0 : _defaultSlowLaneClien26.call(_defaultSlowLaneClien25, input);
253
+ };
254
+ export const requestProgressiveSurfaceScores = input => {
255
+ var _defaultSlowLaneClien27, _defaultSlowLaneClien28;
256
+ (_defaultSlowLaneClien27 = defaultSlowLaneClient) === null || _defaultSlowLaneClien27 === void 0 ? void 0 : (_defaultSlowLaneClien28 = _defaultSlowLaneClien27.requestProgressiveSurfaceScores) === null || _defaultSlowLaneClien28 === void 0 ? void 0 : _defaultSlowLaneClien28.call(_defaultSlowLaneClien27, input);
257
+ };
258
+ export const isCanonicalSurfaceScoringSupported = () => supportsCanonicalSurfaceScoring(defaultSlowLaneClient);
259
+
260
+ /**
261
+ * How many surfaces the canonical token map currently holds. Goes from zero to
262
+ * its final size once the artifact lands, which is the only point at which
263
+ * `getCanonicalSurfaceTokenIds` starts answering — so callers memoising over
264
+ * that lookup can use this to detect the transition.
265
+ */
266
+ export const getCanonicalSurfaceCount = () => {
267
+ var _defaultSlowLaneClien29, _defaultSlowLaneClien30, _defaultSlowLaneClien31;
268
+ return (_defaultSlowLaneClien29 = (_defaultSlowLaneClien30 = defaultSlowLaneClient) === null || _defaultSlowLaneClien30 === void 0 ? void 0 : (_defaultSlowLaneClien31 = _defaultSlowLaneClien30.getCanonicalSurfaceCount) === null || _defaultSlowLaneClien31 === void 0 ? void 0 : _defaultSlowLaneClien31.call(_defaultSlowLaneClien30)) !== null && _defaultSlowLaneClien29 !== void 0 ? _defaultSlowLaneClien29 : 0;
212
269
  };