@atlaskit/editor-plugin-autocomplete 6.0.0 → 8.0.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 (61) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/compass.yml +2 -2
  3. package/dist/cjs/analytics/ufo.js +3 -2
  4. package/dist/cjs/pm-plugins/artifacts-manifest.js +210 -0
  5. package/dist/cjs/pm-plugins/autocomplete-plugin.js +10 -2
  6. package/dist/cjs/pm-plugins/local-slow-lane-client.js +68 -153
  7. package/dist/cjs/pm-plugins/scoring-pipeline.js +130 -28
  8. package/dist/cjs/pm-plugins/text-predictor.js +45 -106
  9. package/dist/es2019/analytics/ufo.js +4 -2
  10. package/dist/es2019/pm-plugins/artifacts-manifest.js +126 -0
  11. package/dist/es2019/pm-plugins/autocomplete-plugin.js +10 -2
  12. package/dist/es2019/pm-plugins/local-slow-lane-client.js +8 -78
  13. package/dist/es2019/pm-plugins/scoring-pipeline.js +97 -21
  14. package/dist/es2019/pm-plugins/text-predictor.js +17 -59
  15. package/dist/esm/analytics/ufo.js +3 -2
  16. package/dist/esm/pm-plugins/artifacts-manifest.js +204 -0
  17. package/dist/esm/pm-plugins/autocomplete-plugin.js +10 -2
  18. package/dist/esm/pm-plugins/local-slow-lane-client.js +67 -148
  19. package/dist/esm/pm-plugins/scoring-pipeline.js +128 -26
  20. package/dist/esm/pm-plugins/text-predictor.js +45 -98
  21. package/dist/types/analytics/ufo.d.ts +1 -0
  22. package/dist/types/pm-plugins/artifacts-manifest.d.ts +55 -0
  23. package/dist/types/pm-plugins/autocomplete-plugin.d.ts +5 -11
  24. package/dist/types/pm-plugins/scoring-pipeline.d.ts +14 -0
  25. package/dist/types/pm-plugins/text-predictor.d.ts +0 -1
  26. package/package.json +3 -6
  27. package/src/analytics/ufo.ts +5 -0
  28. package/src/pm-plugins/artifacts-manifest.ts +151 -0
  29. package/src/pm-plugins/autocomplete-plugin.ts +14 -12
  30. package/src/pm-plugins/local-slow-lane-client.ts +10 -92
  31. package/src/pm-plugins/scoring-pipeline.ts +115 -23
  32. package/src/pm-plugins/text-predictor.ts +28 -76
  33. package/dist/cjs/pm-plugins/data/combined_l2_l3_pos_tags.json +0 -73571
  34. package/dist/cjs/pm-plugins/data/first_token_to_words.json +0 -1
  35. package/dist/cjs/pm-plugins/data/ghost_pos_tags.json +0 -43
  36. package/dist/cjs/pm-plugins/data/grammar_transitions_10k.json +0 -46
  37. package/dist/cjs/pm-plugins/data/l3_vocabulary.json +0 -20002
  38. package/dist/cjs/pm-plugins/data/vocabulary_10k.json +0 -38794
  39. package/dist/cjs/pm-plugins/data/word_index_10k.json +0 -7762
  40. package/dist/es2019/pm-plugins/data/combined_l2_l3_pos_tags.json +0 -73571
  41. package/dist/es2019/pm-plugins/data/first_token_to_words.json +0 -1
  42. package/dist/es2019/pm-plugins/data/ghost_pos_tags.json +0 -43
  43. package/dist/es2019/pm-plugins/data/grammar_transitions_10k.json +0 -46
  44. package/dist/es2019/pm-plugins/data/l3_vocabulary.json +0 -20002
  45. package/dist/es2019/pm-plugins/data/vocabulary_10k.json +0 -38794
  46. package/dist/es2019/pm-plugins/data/word_index_10k.json +0 -7762
  47. package/dist/esm/pm-plugins/data/combined_l2_l3_pos_tags.json +0 -73571
  48. package/dist/esm/pm-plugins/data/first_token_to_words.json +0 -1
  49. package/dist/esm/pm-plugins/data/ghost_pos_tags.json +0 -43
  50. package/dist/esm/pm-plugins/data/grammar_transitions_10k.json +0 -46
  51. package/dist/esm/pm-plugins/data/l3_vocabulary.json +0 -20002
  52. package/dist/esm/pm-plugins/data/vocabulary_10k.json +0 -38794
  53. package/dist/esm/pm-plugins/data/word_index_10k.json +0 -7762
  54. package/src/pm-plugins/data/combined_l2_l3_pos_tags.json +0 -73571
  55. package/src/pm-plugins/data/first_token_to_words.json +0 -1
  56. package/src/pm-plugins/data/ghost_pos_tags.json +0 -43
  57. package/src/pm-plugins/data/grammar_transitions_10k.json +0 -46
  58. package/src/pm-plugins/data/l3_vocabulary.json +0 -20002
  59. package/src/pm-plugins/data/vocabulary_10k.json +0 -38794
  60. package/src/pm-plugins/data/word-vectors_10k.bin +0 -3
  61. package/src/pm-plugins/data/word_index_10k.json +0 -7762
@@ -39,6 +39,7 @@ import type { MLCEngine, InitProgressReport, AppConfig, LogitProcessor } from '@
39
39
 
40
40
  import { abortExp, EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
41
41
 
42
+ import { ARTIFACT_NAME, fetchArtifactJson } from './artifacts-manifest';
42
43
  import { isAutocompleteDebugEnabled } from './debug-mode';
43
44
  import { isWordBoundary } from './slow-lane-client';
44
45
 
@@ -349,78 +350,9 @@ let prefixMapTokenIds: number[] = [];
349
350
  let bePayloadDataPromise: Promise<void> | undefined;
350
351
 
351
352
  /**
352
- * Unwrap a dynamically imported JSON module to the parsed JSON value, working
353
- * across the two interop modes AFM's bundler chain emits:
354
- *
355
- * 1. **`.default`-wrapped namespace** — classic webpack (and Jest) hang the
356
- * JSON value under the `default` export.
357
- * 2. **Named-exports namespace** — webpack 5 / atlaspack with JSON
358
- * named-exports (or native ESM JSON modules) expose each top-level key as
359
- * a named export and shadow `default`, so `mod.default` can be `undefined`
360
- * (or some unrelated value) even though `mod` itself holds the data.
361
- *
362
- * The caller MUST declare the underlying JSON shape via `shape` because, in
363
- * named-exports mode, a dense array `["a","b"]` and a sparse numeric-keyed
364
- * object `{"5":"a","12":"b"}` are emitted identically (`{"0":..}` / `{"5":..}`);
365
- * no runtime heuristic can tell them apart, so only the caller knows which:
366
- *
367
- * - `'object'` — the JSON is a `{...}` (including sparse maps keyed by integer
368
- * IDs). The named exports are rebuilt into a plain object so `Object.entries`
369
- * yields the real keys, not synthetic array indices.
370
- * - `'array'` — the JSON is a `[...]`, reconstructed from the `0..n-1` indices.
371
- *
372
- * :param mod: The raw module object returned by `await import('./*.json')`.
373
- * :param shape: `'object'` if the source JSON is `{...}`, `'array'` if `[...]`.
374
- * :returns: The parsed JSON value, or `null` if neither interop mode applies.
375
- */
376
- function unwrapJsonModule<T>(mod: unknown, shape: 'object' | 'array'): T | null {
377
- if (mod == null || typeof mod !== 'object') {
378
- return null;
379
- }
380
- const namespace = mod as Record<string, unknown> & { default?: unknown };
381
-
382
- // Compute the named-export own-keys (strip synthetic markers).
383
- const ownKeys = Object.keys(namespace).filter((k) => k !== 'default' && k !== '__esModule');
384
-
385
- // PREFER named exports when present — they always reflect the JSON's real
386
- // top-level keys / indices, regardless of what `default` happens to be.
387
- // Under JSON named-exports mode `default` is not necessarily the parsed
388
- // value (e.g. for `{"service": 0, ...}` it can be the number `0`, with the
389
- // real data in the named exports), so taking `default` first would corrupt it.
390
- if (ownKeys.length > 0) {
391
- if (shape === 'array') {
392
- // JSON arrays are dense; reconstruct from `0..length-1` indices.
393
- const len = ownKeys.length;
394
- const arr = new Array(len);
395
- for (let i = 0; i < len; i++) {
396
- arr[i] = namespace[String(i)];
397
- }
398
- return arr as T;
399
- }
400
- // shape === 'object'. Rebuild a plain object from the (stripped) own
401
- // keys so callers can `Object.entries()` it without iterating over
402
- // `default` / `__esModule`, and to detach from the module-namespace
403
- // object (which is sealed/non-extensible on some bundler outputs).
404
- const obj: Record<string, unknown> = {};
405
- for (const k of ownKeys) {
406
- obj[k] = namespace[k];
407
- }
408
- return obj as T;
409
- }
410
-
411
- // Fallback: no named exports — classic webpack JSON-module interop where
412
- // the whole parsed JSON value is hung under `default`. Trust it.
413
- if ('default' in namespace && namespace.default != null) {
414
- return namespace.default as T;
415
- }
416
-
417
- return null;
418
- }
419
-
420
- /**
421
- * Lazily load and build the BE-parity lookup tables from their JSON payloads.
422
- * The dynamic imports are split into their own async chunks so neither file is
423
- * bundled into the editor's main chunk unless local inference is initialised.
353
+ * Lazily load and build the BE-parity lookup tables from their CDN payloads.
354
+ * Both artifacts are fetched on demand so neither is downloaded unless local
355
+ * inference is initialised.
424
356
  *
425
357
  * :returns:
426
358
  * A promise that resolves once `firstTokenToWords`, `l2Words` and
@@ -429,31 +361,17 @@ function unwrapJsonModule<T>(mod: unknown, shape: 'object' | 'array'): T | null
429
361
  const loadBePayloadData = (): Promise<void> => {
430
362
  if (!bePayloadDataPromise) {
431
363
  bePayloadDataPromise = (async () => {
432
- const [firstTokenToWordsModule, vocabularyModule] = await Promise.all([
433
- import(
434
- /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-first-token-to-words" */ './data/first_token_to_words.json'
435
- ),
436
- import(
437
- /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-vocabulary-10k" */ './data/vocabulary_10k.json'
438
- ),
364
+ const [firstTokenToWordsData, vocabularyData] = await Promise.all([
365
+ fetchArtifactJson<Record<string, string[]>>(ARTIFACT_NAME.FIRST_TOKEN_TO_WORDS),
366
+ fetchArtifactJson<{ words: Record<string, unknown> }>(ARTIFACT_NAME.VOCABULARY),
439
367
  ]);
440
368
 
441
- const firstTokenToWordsData = unwrapJsonModule<Record<string, string[]>>(
442
- firstTokenToWordsModule,
443
- 'object',
444
- );
445
- const vocabularyData = unwrapJsonModule<{ words: Record<string, unknown> }>(
446
- vocabularyModule,
447
- 'object',
448
- );
449
-
450
369
  if (firstTokenToWordsData == null || vocabularyData?.words == null) {
451
370
  // Hard-fail with a precise message so the catch() in initEngine logs
452
- // exactly which import couldn't be unwrapped, rather than the generic
453
- // V8 "Cannot convert undefined or null to object" we hit before the
454
- // helper was added.
371
+ // exactly which payload was malformed, rather than the generic V8
372
+ // "Cannot convert undefined or null to object".
455
373
  throw new Error(
456
- `[LocalSlowLane] JSON module could not be unwrapped — ` +
374
+ `[LocalSlowLane] unexpected artifact payload — ` +
457
375
  `firstTokenToWordsData=${firstTokenToWordsData == null ? 'null/undefined' : 'defined'}, ` +
458
376
  `vocabularyData=${vocabularyData == null ? 'null/undefined' : vocabularyData.words == null ? 'defined but missing .words' : 'defined'}`,
459
377
  );
@@ -3,11 +3,15 @@
3
3
  *
4
4
  * Operates synchronously on pre-loaded data. Each stage gracefully degrades
5
5
  * when its required data isn't available (cold → warm → full warm).
6
+ *
7
+ * The grammar/POS payloads are fetched from the CDN by loadGrammarDataAsync, so
8
+ * the grammar filter is a pass-through until that resolves.
6
9
  */
7
10
 
8
- import posTagsData from './data/combined_l2_l3_pos_tags.json';
9
- import ghostPosTagsData from './data/ghost_pos_tags.json';
10
- import grammarTransitionsData from './data/grammar_transitions_10k.json';
11
+ import { EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
12
+
13
+ import { ARTIFACT_NAME, fetchArtifactJson } from './artifacts-manifest';
14
+ import { isAutocompleteDebugEnabled } from './debug-mode';
11
15
 
12
16
  // ─── Types ──────────────────────────────────────────────────
13
17
 
@@ -59,22 +63,114 @@ const L1_SESSION_CAP = 1.2;
59
63
  // at 0.00024) from triggering re-ranking.
60
64
  const LM_GATE_THRESHOLD = 0.0005;
61
65
 
62
- // ─── Grammar Data (loaded once on import) ───────────────────
66
+ // ─── Grammar Data (fetched once from the CDN) ────────────────
63
67
 
64
- const posTags: Map<string, string[]> = new Map(
65
- Object.entries(posTagsData as Record<string, string[]>),
66
- );
68
+ let posTags: Map<string, string[]> = new Map();
67
69
 
68
- const grammarTransitions = grammarTransitionsData as GrammarTransitions;
70
+ /**
71
+ * GHOST POS DICTIONARY
72
+ * A mapping of common structural English words that were stripped from the main
73
+ * domain vocabulary. This allows the grammar filter to understand context
74
+ * without suggesting these words to the user.
75
+ */
76
+ let ghostPosTags: Record<string, string[]> = {};
69
77
 
70
78
  /**
71
- * Precomputed map from each POS tag to the set of allowed next POS tags.
72
- * Built once at module load from grammarTransitions so applyGrammarFilter
73
- * never re-iterates the transition rules per call.
79
+ * Precomputed map from each POS tag to the set of allowed next POS tags, so
80
+ * applyGrammarFilter never re-iterates the transition rules per call.
74
81
  */
75
- const precomputedAllowedByPos: Map<string, Set<string>> = new Map(
76
- Object.entries(grammarTransitions.transitions).map(([pos, rule]) => [pos, new Set(rule.allowed)]),
77
- );
82
+ let precomputedAllowedByPos: Map<string, Set<string>> = new Map();
83
+
84
+ let grammarDataPromise: Promise<void> | undefined;
85
+
86
+ const setGrammarData = (data: {
87
+ ghostPosTags: Record<string, string[]>;
88
+ grammarTransitions: GrammarTransitions;
89
+ posTags: Record<string, string[]>;
90
+ }): void => {
91
+ posTags = new Map(Object.entries(data.posTags));
92
+ ghostPosTags = data.ghostPosTags;
93
+ precomputedAllowedByPos = new Map(
94
+ Object.entries(data.grammarTransitions.transitions ?? {}).map(([pos, rule]) => [
95
+ pos,
96
+ new Set(rule.allowed),
97
+ ]),
98
+ );
99
+ };
100
+
101
+ export const isGrammarDataLoaded = (): boolean => precomputedAllowedByPos.size > 0;
102
+
103
+ /**
104
+ * Fetch the POS tag and grammar transition payloads from the CDN.
105
+ *
106
+ * Safe to call repeatedly: the in-flight promise is shared, and a failure is not
107
+ * cached so a later call can retry.
108
+ */
109
+ export const loadGrammarDataAsync = (options?: {
110
+ isLocalLLM?: boolean;
111
+ surface?: string;
112
+ }): Promise<void> => {
113
+ if (grammarDataPromise) {
114
+ return grammarDataPromise;
115
+ }
116
+
117
+ const isLocalLLM = options?.isLocalLLM ?? false;
118
+ const surface = options?.surface;
119
+
120
+ grammarDataPromise = (async () => {
121
+ startExp(EXPERIENCE_NAME.LOAD_GRAMMAR, 'singleton', {
122
+ isLocalLLM,
123
+ ...(surface ? { surface } : {}),
124
+ });
125
+
126
+ try {
127
+ const [posTagsData, ghostPosTagsData, grammarTransitionsData] = await Promise.all([
128
+ fetchArtifactJson<Record<string, string[]>>(ARTIFACT_NAME.POS_TAGS),
129
+ fetchArtifactJson<Record<string, string[]>>(ARTIFACT_NAME.GHOST_POS_TAGS),
130
+ fetchArtifactJson<GrammarTransitions>(ARTIFACT_NAME.GRAMMAR_TRANSITIONS),
131
+ ]);
132
+
133
+ if (grammarTransitionsData?.transitions == null) {
134
+ throw new Error(
135
+ `[scoring-pipeline] ${ARTIFACT_NAME.GRAMMAR_TRANSITIONS} is missing its \`transitions\` wrapper`,
136
+ );
137
+ }
138
+
139
+ setGrammarData({
140
+ posTags: posTagsData ?? {},
141
+ ghostPosTags: ghostPosTagsData ?? {},
142
+ grammarTransitions: grammarTransitionsData,
143
+ });
144
+
145
+ succeedExp(EXPERIENCE_NAME.LOAD_GRAMMAR, 'singleton', {
146
+ isLocalLLM,
147
+ posTagCount: posTags.size,
148
+ transitionCount: precomputedAllowedByPos.size,
149
+ ...(surface ? { surface } : {}),
150
+ });
151
+
152
+ if (isAutocompleteDebugEnabled()) {
153
+ // eslint-disable-next-line no-console
154
+ console.log('[scoring-pipeline] Grammar data loaded:', {
155
+ posTagCount: posTags.size,
156
+ ghostPosTagCount: Object.keys(ghostPosTags).length,
157
+ transitionCount: precomputedAllowedByPos.size,
158
+ });
159
+ }
160
+ } catch (e) {
161
+ failExp(EXPERIENCE_NAME.LOAD_GRAMMAR, 'singleton', {
162
+ isLocalLLM,
163
+ errorType: 'network',
164
+ ...(surface ? { surface } : {}),
165
+ });
166
+ // Allow a later call to retry the load rather than caching the failure.
167
+ grammarDataPromise = undefined;
168
+ throw e;
169
+ }
170
+ })();
171
+
172
+ return grammarDataPromise;
173
+ };
78
174
 
79
175
  // ─── Math ───────────────────────────────────────────────────
80
176
 
@@ -135,14 +231,6 @@ function scoreStage1(
135
231
 
136
232
  // ─── Grammar Filter ─────────────────────────────────────────
137
233
 
138
- /**
139
- * GHOST POS DICTIONARY
140
- * A hardcoded mapping of common structural English words that were stripped
141
- * from the main domain vocabulary. This allows the grammar filter to understand
142
- * context without suggesting these words to the user.
143
- */
144
- const ghostPosTags: Record<string, string[]> = ghostPosTagsData as Record<string, string[]>;
145
-
146
234
  type FilterEntry = {
147
235
  candidate: ScoringCandidate;
148
236
  freqScore: number;
@@ -154,7 +242,11 @@ function applyGrammarFilter(
154
242
  candidates: FilterEntry[],
155
243
  previousWord: string,
156
244
  ): { filtered: FilterEntry[]; grammarMeta: GrammarFilterMeta | null } {
157
- if (!previousWord) return { filtered: candidates, grammarMeta: null };
245
+ // Pass everything through until loadGrammarDataAsync resolves — filtering on a
246
+ // partially loaded ruleset would drop valid candidates.
247
+ if (!previousWord || !isGrammarDataLoaded()) {
248
+ return { filtered: candidates, grammarMeta: null };
249
+ }
158
250
 
159
251
  const lowerPrev = previousWord.toLowerCase();
160
252
  const prevTags = ghostPosTags[lowerPrev] || posTags.get(lowerPrev);
@@ -17,9 +17,10 @@
17
17
 
18
18
  import { EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
19
19
 
20
- // The vocabulary, L3 and word-index JSON payloads are dynamically imported in
21
- // loadDefaultVocabulary / loadVectorsAsync so their (large) contents stay out of
22
- // the editor's main chunk and only load when autocomplete is initialised.
20
+ // The vocabulary, L3 and word-index payloads are fetched from the CDN in
21
+ // loadDefaultVocabulary / loadVectorsAsync so their (large) contents never enter
22
+ // the editor's bundle and only load when autocomplete is initialised.
23
+ import { ARTIFACT_NAME, fetchArtifactJson, getArtifactUrl } from './artifacts-manifest';
23
24
  import { isAutocompleteDebugEnabled } from './debug-mode';
24
25
  import { rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE } from './scoring-pipeline';
25
26
  import type { ScoringCandidate } from './scoring-pipeline';
@@ -709,60 +710,13 @@ interface VocabularyJson {
709
710
  >;
710
711
  }
711
712
 
712
- /**
713
- * Unwrap a dynamically imported JSON module to its parsed value, handling both
714
- * interop modes AFM's bundler chain emits: a `.default`-wrapped namespace
715
- * (classic webpack) and a named-exports namespace (webpack 5 / atlaspack JSON
716
- * modules, where `default` can be a misleading scalar). Named exports are
717
- * preferred when present. The caller declares the JSON `shape` because a dense
718
- * array and a sparse numeric-keyed object are emitted identically as named
719
- * exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
720
- */
721
- function unwrapJsonModule<T>(mod: unknown, shape: 'object' | 'array'): T | null {
722
- if (mod == null || typeof mod !== 'object') {
723
- return null;
724
- }
725
- const namespace = mod as Record<string, unknown> & { default?: unknown };
726
- const ownKeys = Object.keys(namespace).filter((k) => k !== 'default' && k !== '__esModule');
727
-
728
- if (ownKeys.length > 0) {
729
- if (shape === 'array') {
730
- const len = ownKeys.length;
731
- const arr = new Array(len);
732
- for (let i = 0; i < len; i++) {
733
- arr[i] = namespace[String(i)];
734
- }
735
- return arr as T;
736
- }
737
- const obj: Record<string, unknown> = {};
738
- for (const k of ownKeys) {
739
- obj[k] = namespace[k];
740
- }
741
- return obj as T;
742
- }
743
-
744
- if ('default' in namespace && namespace.default != null) {
745
- return namespace.default as T;
746
- }
747
-
748
- return null;
749
- }
750
-
751
713
  export const loadVectorsAsync = async (options?: {
752
- getBinaryUrl?: () => Promise<string>;
753
714
  isLocalLLM?: boolean;
754
715
  surface?: string;
755
716
  }): Promise<void> => {
756
717
  if (vectorStore || vectorsLoadStarted) {
757
718
  return;
758
719
  }
759
- if (!options?.getBinaryUrl) {
760
- // eslint-disable-next-line no-console
761
- console.warn(
762
- '[text-predictor] loadVectorsAsync called without a getBinaryUrl — vectors will not load. Pass getVectorsBinaryUrl via plugin options.',
763
- );
764
- return;
765
- }
766
720
  const isLocalLLM = options?.isLocalLLM ?? false;
767
721
  const surface = options?.surface;
768
722
  vectorsLoadStarted = true;
@@ -773,7 +727,7 @@ export const loadVectorsAsync = async (options?: {
773
727
 
774
728
  let url: string;
775
729
  try {
776
- url = await options.getBinaryUrl();
730
+ url = await getArtifactUrl(ARTIFACT_NAME.WORD_VECTORS);
777
731
  } catch (e) {
778
732
  vectorsLoadStarted = false;
779
733
  failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
@@ -803,22 +757,25 @@ export const loadVectorsAsync = async (options?: {
803
757
  const buffer = await res.arrayBuffer();
804
758
  const float32 = new Float32Array(buffer);
805
759
 
806
- // word_index_10k.json is wrapped as `{ "index": {…} }` so no real entry
807
- // (e.g. the word "default") can shadow the synthetic ESM `default` export
808
- // the bundler creates for dynamically-imported JSON.
809
- const wordIndexModule = await import(
810
- /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-word-index-10k" */ './data/word_index_10k.json'
811
- );
812
- const wordIndexOuter = unwrapJsonModule<{ index?: Record<string, number> }>(
813
- wordIndexModule,
814
- 'object',
760
+ // The word index is published either bare or wrapped as `{ "index": {…} }`
761
+ // (the wrapper exists so no real entry, e.g. the word "default", can shadow
762
+ // a bundler-synthesised ESM export). Detect the wrapper by value type, not
763
+ // by key presence — "index" is itself a plausible vocabulary word, and in a
764
+ // bare index every value is a number.
765
+ const wordIndexPayload = await fetchArtifactJson<Record<string, unknown>>(
766
+ ARTIFACT_NAME.WORD_INDEX,
815
767
  );
816
- const wordIndex = wordIndexOuter?.index ?? {};
768
+ const wrappedIndex = wordIndexPayload?.index;
769
+ const wordIndex = (
770
+ typeof wrappedIndex === 'object' && wrappedIndex !== null
771
+ ? wrappedIndex
772
+ : (wordIndexPayload ?? {})
773
+ ) as Record<string, number>;
817
774
  const nWords = Object.keys(wordIndex).length;
818
775
  if (nWords === 0) {
819
776
  // eslint-disable-next-line no-console
820
777
  console.warn(
821
- '[text-predictor] word_index_10k.json missing its `index` wrapper — wordIndex is empty, semantic scoring will be a no-op.',
778
+ `[text-predictor] ${ARTIFACT_NAME.WORD_INDEX} was empty — semantic scoring will be a no-op.`,
822
779
  );
823
780
  }
824
781
  const dim = float32.length / nWords;
@@ -877,22 +834,17 @@ export const loadDefaultVocabulary = (options?: {
877
834
  });
878
835
 
879
836
  try {
880
- // The L2 vocabulary and L3 word list are code-split into their own async
881
- // chunks so they stay out of the editor's main bundle.
882
- const [vocabularyModule, l3VocabularyModule] = await Promise.all([
883
- import(
884
- /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-vocabulary-10k" */ './data/vocabulary_10k.json'
885
- ),
886
- import(
887
- /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-l3-vocabulary" */ './data/l3_vocabulary.json'
888
- ),
837
+ const [vocabularyData, l3VocabularyData] = await Promise.all([
838
+ fetchArtifactJson<VocabularyJson>(ARTIFACT_NAME.VOCABULARY),
839
+ fetchArtifactJson<string[]>(ARTIFACT_NAME.L3_VOCABULARY),
889
840
  ]);
890
841
 
891
- const vocabularyData = unwrapJsonModule<VocabularyJson>(vocabularyModule, 'object');
892
- const l3VocabularyData = unwrapJsonModule<string[]>(l3VocabularyModule, 'array');
893
-
894
842
  if (vocabularyData?.words == null || !Array.isArray(l3VocabularyData)) {
895
- throw new Error('[text-predictor] vocabulary JSON modules could not be unwrapped');
843
+ throw new Error(
844
+ `[text-predictor] unexpected vocabulary payload — ` +
845
+ `${ARTIFACT_NAME.VOCABULARY} ${vocabularyData?.words == null ? 'is missing its `words` wrapper' : 'is valid'}, ` +
846
+ `${ARTIFACT_NAME.L3_VOCABULARY} ${Array.isArray(l3VocabularyData) ? 'is valid' : 'is not an array'}`,
847
+ );
896
848
  }
897
849
 
898
850
  const terms = Object.entries(vocabularyData.words).map(([word, stats]) => ({
@@ -917,7 +869,7 @@ export const loadDefaultVocabulary = (options?: {
917
869
  } catch (e) {
918
870
  failExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
919
871
  isLocalLLM,
920
- errorType: 'parse_error',
872
+ errorType: 'load_error',
921
873
  ...(surface ? { surface } : {}),
922
874
  });
923
875
  // Allow a later call to retry the load rather than caching the failure.