@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
@@ -1,5 +1,5 @@
1
- import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
2
1
  import _typeof from "@babel/runtime/helpers/typeof";
2
+ import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
3
3
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
4
4
  import _createClass from "@babel/runtime/helpers/createClass";
5
5
  import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
@@ -29,9 +29,10 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
29
29
 
30
30
  import { EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
31
31
 
32
- // The vocabulary, L3 and word-index JSON payloads are dynamically imported in
33
- // loadDefaultVocabulary / loadVectorsAsync so their (large) contents stay out of
34
- // the editor's main chunk and only load when autocomplete is initialised.
32
+ // The vocabulary, L3 and word-index payloads are fetched from the CDN in
33
+ // loadDefaultVocabulary / loadVectorsAsync so their (large) contents never enter
34
+ // the editor's bundle and only load when autocomplete is initialised.
35
+ import { ARTIFACT_NAME, fetchArtifactJson, getArtifactUrl } from './artifacts-manifest';
35
36
  import { isAutocompleteDebugEnabled } from './debug-mode';
36
37
  import { rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE } from './scoring-pipeline';
37
38
  import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
@@ -685,56 +686,10 @@ export var predict = function predict(textBefore) {
685
686
 
686
687
  // ─── Data Loading ────────────────────────────────────────────────────────────
687
688
 
688
- /**
689
- * Unwrap a dynamically imported JSON module to its parsed value, handling both
690
- * interop modes AFM's bundler chain emits: a `.default`-wrapped namespace
691
- * (classic webpack) and a named-exports namespace (webpack 5 / atlaspack JSON
692
- * modules, where `default` can be a misleading scalar). Named exports are
693
- * preferred when present. The caller declares the JSON `shape` because a dense
694
- * array and a sparse numeric-keyed object are emitted identically as named
695
- * exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
696
- */
697
- function unwrapJsonModule(mod, shape) {
698
- if (mod == null || _typeof(mod) !== 'object') {
699
- return null;
700
- }
701
- var namespace = mod;
702
- var ownKeys = Object.keys(namespace).filter(function (k) {
703
- return k !== 'default' && k !== '__esModule';
704
- });
705
- if (ownKeys.length > 0) {
706
- if (shape === 'array') {
707
- var len = ownKeys.length;
708
- var arr = new Array(len);
709
- for (var i = 0; i < len; i++) {
710
- arr[i] = namespace[String(i)];
711
- }
712
- return arr;
713
- }
714
- var obj = {};
715
- var _iterator1 = _createForOfIteratorHelper(ownKeys),
716
- _step1;
717
- try {
718
- for (_iterator1.s(); !(_step1 = _iterator1.n()).done;) {
719
- var k = _step1.value;
720
- obj[k] = namespace[k];
721
- }
722
- } catch (err) {
723
- _iterator1.e(err);
724
- } finally {
725
- _iterator1.f();
726
- }
727
- return obj;
728
- }
729
- if ('default' in namespace && namespace.default != null) {
730
- return namespace.default;
731
- }
732
- return null;
733
- }
734
689
  export var loadVectorsAsync = /*#__PURE__*/function () {
735
690
  var _ref6 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(options) {
736
691
  var _options$isLocalLLM;
737
- var isLocalLLM, surface, url, _wordIndexOuter$index, res, buffer, float32, wordIndexModule, wordIndexOuter, wordIndex, nWords, dim, _t, _t2;
692
+ var isLocalLLM, surface, url, res, buffer, float32, wordIndexPayload, wrappedIndex, wordIndex, nWords, dim, _t, _t2;
738
693
  return _regeneratorRuntime.wrap(function (_context) {
739
694
  while (1) switch (_context.prev = _context.next) {
740
695
  case 0:
@@ -744,14 +699,6 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
744
699
  }
745
700
  return _context.abrupt("return");
746
701
  case 1:
747
- if (options !== null && options !== void 0 && options.getBinaryUrl) {
748
- _context.next = 2;
749
- break;
750
- }
751
- // eslint-disable-next-line no-console
752
- console.warn('[text-predictor] loadVectorsAsync called without a getBinaryUrl — vectors will not load. Pass getVectorsBinaryUrl via plugin options.');
753
- return _context.abrupt("return");
754
- case 2:
755
702
  isLocalLLM = (_options$isLocalLLM = options === null || options === void 0 ? void 0 : options.isLocalLLM) !== null && _options$isLocalLLM !== void 0 ? _options$isLocalLLM : false;
756
703
  surface = options === null || options === void 0 ? void 0 : options.surface;
757
704
  vectorsLoadStarted = true;
@@ -760,16 +707,16 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
760
707
  }, surface ? {
761
708
  surface: surface
762
709
  } : {}));
763
- _context.prev = 3;
764
- _context.next = 4;
765
- return options.getBinaryUrl();
766
- case 4:
710
+ _context.prev = 2;
711
+ _context.next = 3;
712
+ return getArtifactUrl(ARTIFACT_NAME.WORD_VECTORS);
713
+ case 3:
767
714
  url = _context.sent;
768
- _context.next = 6;
715
+ _context.next = 5;
769
716
  break;
770
- case 5:
771
- _context.prev = 5;
772
- _t = _context["catch"](3);
717
+ case 4:
718
+ _context.prev = 4;
719
+ _t = _context["catch"](2);
773
720
  vectorsLoadStarted = false;
774
721
  failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', _objectSpread({
775
722
  isLocalLLM: isLocalLLM,
@@ -780,14 +727,14 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
780
727
  // eslint-disable-next-line no-console
781
728
  console.warn('[text-predictor] Failed to resolve vectors URL:', _t);
782
729
  return _context.abrupt("return");
783
- case 6:
784
- _context.prev = 6;
785
- _context.next = 7;
730
+ case 5:
731
+ _context.prev = 5;
732
+ _context.next = 6;
786
733
  return fetch(url);
787
- case 7:
734
+ case 6:
788
735
  res = _context.sent;
789
736
  if (res.ok) {
790
- _context.next = 8;
737
+ _context.next = 7;
791
738
  break;
792
739
  }
793
740
  vectorsLoadStarted = false;
@@ -801,24 +748,26 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
801
748
  // eslint-disable-next-line no-console
802
749
  console.warn("[text-predictor] Failed to load vectors: ".concat(res.status));
803
750
  return _context.abrupt("return");
751
+ case 7:
752
+ _context.next = 8;
753
+ return res.arrayBuffer();
804
754
  case 8:
755
+ buffer = _context.sent;
756
+ float32 = new Float32Array(buffer); // The word index is published either bare or wrapped as `{ "index": {…} }`
757
+ // (the wrapper exists so no real entry, e.g. the word "default", can shadow
758
+ // a bundler-synthesised ESM export). Detect the wrapper by value type, not
759
+ // by key presence — "index" is itself a plausible vocabulary word, and in a
760
+ // bare index every value is a number.
805
761
  _context.next = 9;
806
- return res.arrayBuffer();
762
+ return fetchArtifactJson(ARTIFACT_NAME.WORD_INDEX);
807
763
  case 9:
808
- buffer = _context.sent;
809
- float32 = new Float32Array(buffer); // word_index_10k.json is wrapped as `{ "index": {…} }` so no real entry
810
- // (e.g. the word "default") can shadow the synthetic ESM `default` export
811
- // the bundler creates for dynamically-imported JSON.
812
- _context.next = 10;
813
- return import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-word-index-10k" */'./data/word_index_10k.json');
814
- case 10:
815
- wordIndexModule = _context.sent;
816
- wordIndexOuter = unwrapJsonModule(wordIndexModule, 'object');
817
- wordIndex = (_wordIndexOuter$index = wordIndexOuter === null || wordIndexOuter === void 0 ? void 0 : wordIndexOuter.index) !== null && _wordIndexOuter$index !== void 0 ? _wordIndexOuter$index : {};
764
+ wordIndexPayload = _context.sent;
765
+ wrappedIndex = wordIndexPayload === null || wordIndexPayload === void 0 ? void 0 : wordIndexPayload.index;
766
+ wordIndex = _typeof(wrappedIndex) === 'object' && wrappedIndex !== null ? wrappedIndex : wordIndexPayload !== null && wordIndexPayload !== void 0 ? wordIndexPayload : {};
818
767
  nWords = Object.keys(wordIndex).length;
819
768
  if (nWords === 0) {
820
769
  // eslint-disable-next-line no-console
821
- console.warn('[text-predictor] word_index_10k.json missing its `index` wrapper — wordIndex is empty, semantic scoring will be a no-op.');
770
+ console.warn("[text-predictor] ".concat(ARTIFACT_NAME.WORD_INDEX, " was empty \u2014 semantic scoring will be a no-op."));
822
771
  }
823
772
  dim = float32.length / nWords;
824
773
  vectorStore = {
@@ -842,11 +791,11 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
842
791
  sizeBytes: float32.byteLength
843
792
  });
844
793
  }
845
- _context.next = 12;
794
+ _context.next = 11;
846
795
  break;
847
- case 11:
848
- _context.prev = 11;
849
- _t2 = _context["catch"](6);
796
+ case 10:
797
+ _context.prev = 10;
798
+ _t2 = _context["catch"](5);
850
799
  vectorsLoadStarted = false;
851
800
  failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', _objectSpread({
852
801
  isLocalLLM: isLocalLLM,
@@ -856,11 +805,11 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
856
805
  } : {}));
857
806
  // eslint-disable-next-line no-console
858
807
  console.warn('[text-predictor] Failed to load vectors:', _t2);
859
- case 12:
808
+ case 11:
860
809
  case "end":
861
810
  return _context.stop();
862
811
  }
863
- }, _callee, null, [[3, 5], [6, 11]]);
812
+ }, _callee, null, [[2, 4], [5, 10]]);
864
813
  }));
865
814
  return function loadVectorsAsync(_x) {
866
815
  return _ref6.apply(this, arguments);
@@ -881,7 +830,7 @@ export var loadDefaultVocabulary = function loadDefaultVocabulary(options) {
881
830
  var isLocalLLM = (_options$isLocalLLM2 = options === null || options === void 0 ? void 0 : options.isLocalLLM) !== null && _options$isLocalLLM2 !== void 0 ? _options$isLocalLLM2 : false;
882
831
  var surface = options === null || options === void 0 ? void 0 : options.surface;
883
832
  vocabularyLoadPromise = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2() {
884
- var _yield$Promise$all, _yield$Promise$all2, vocabularyModule, l3VocabularyModule, vocabularyData, l3VocabularyData, terms, _t3;
833
+ var _yield$Promise$all, _yield$Promise$all2, vocabularyData, l3VocabularyData, terms, _t3;
885
834
  return _regeneratorRuntime.wrap(function (_context2) {
886
835
  while (1) switch (_context2.prev = _context2.next) {
887
836
  case 0:
@@ -892,19 +841,17 @@ export var loadDefaultVocabulary = function loadDefaultVocabulary(options) {
892
841
  } : {}));
893
842
  _context2.prev = 1;
894
843
  _context2.next = 2;
895
- return Promise.all([import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-vocabulary-10k" */'./data/vocabulary_10k.json'), import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-l3-vocabulary" */'./data/l3_vocabulary.json')]);
844
+ return Promise.all([fetchArtifactJson(ARTIFACT_NAME.VOCABULARY), fetchArtifactJson(ARTIFACT_NAME.L3_VOCABULARY)]);
896
845
  case 2:
897
846
  _yield$Promise$all = _context2.sent;
898
847
  _yield$Promise$all2 = _slicedToArray(_yield$Promise$all, 2);
899
- vocabularyModule = _yield$Promise$all2[0];
900
- l3VocabularyModule = _yield$Promise$all2[1];
901
- vocabularyData = unwrapJsonModule(vocabularyModule, 'object');
902
- l3VocabularyData = unwrapJsonModule(l3VocabularyModule, 'array');
848
+ vocabularyData = _yield$Promise$all2[0];
849
+ l3VocabularyData = _yield$Promise$all2[1];
903
850
  if (!((vocabularyData === null || vocabularyData === void 0 ? void 0 : vocabularyData.words) == null || !Array.isArray(l3VocabularyData))) {
904
851
  _context2.next = 3;
905
852
  break;
906
853
  }
907
- throw new Error('[text-predictor] vocabulary JSON modules could not be unwrapped');
854
+ throw new Error("[text-predictor] unexpected vocabulary payload \u2014 " + "".concat(ARTIFACT_NAME.VOCABULARY, " ").concat((vocabularyData === null || vocabularyData === void 0 ? void 0 : vocabularyData.words) == null ? 'is missing its `words` wrapper' : 'is valid', ", ") + "".concat(ARTIFACT_NAME.L3_VOCABULARY, " ").concat(Array.isArray(l3VocabularyData) ? 'is valid' : 'is not an array'));
908
855
  case 3:
909
856
  terms = Object.entries(vocabularyData.words).map(function (_ref8) {
910
857
  var _ref9 = _slicedToArray(_ref8, 2),
@@ -937,7 +884,7 @@ export var loadDefaultVocabulary = function loadDefaultVocabulary(options) {
937
884
  _t3 = _context2["catch"](1);
938
885
  failExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', _objectSpread({
939
886
  isLocalLLM: isLocalLLM,
940
- errorType: 'parse_error'
887
+ errorType: 'load_error'
941
888
  }, surface ? {
942
889
  surface: surface
943
890
  } : {}));
@@ -30,6 +30,7 @@ export declare const EXPERIENCE_NAME: {
30
30
  readonly SLOW_LANE_FETCH: 'slow-lane-fetch';
31
31
  readonly LOAD_VOCABULARY: 'load-vocabulary';
32
32
  readonly LOAD_VECTORS: 'load-vectors';
33
+ readonly LOAD_GRAMMAR: 'load-grammar';
33
34
  };
34
35
  export type AutocompleteExperienceName = (typeof EXPERIENCE_NAME)[keyof typeof EXPERIENCE_NAME];
35
36
  export declare const startExp: (name: AutocompleteExperienceName, id: string, metadata?: CustomData) => void;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Artifacts Manifest Client: resolves the autocomplete model artifacts hosted on
3
+ * the CDN.
4
+ *
5
+ * `GET /gateway/api/v1/autocomplete/artifacts` returns one presigned CDN URL per
6
+ * artifact for the tenant's active model version:
7
+ *
8
+ * { activeVersion, artifacts: [{ name, url, contentType, sizeBytes, checksum }] }
9
+ *
10
+ * Every loader in the plugin resolves its payload through here, so no model data
11
+ * is bundled with the package.
12
+ */
13
+ /**
14
+ * Manifest artifact names. These are the manifest's lookup keys and are
15
+ * hyphenated, unlike the underscored filenames the artifacts are generated from.
16
+ */
17
+ export declare const ARTIFACT_NAME: {
18
+ readonly FIRST_TOKEN_TO_WORDS: 'first-token-to-words.json';
19
+ readonly GHOST_POS_TAGS: 'ghost-pos-tags.json';
20
+ readonly GRAMMAR_TRANSITIONS: 'grammar-transitions-10k.json';
21
+ readonly L3_VOCABULARY: 'l3-vocabulary.json';
22
+ readonly POS_TAGS: 'combined-l2-l3-pos-tags.json';
23
+ readonly VOCABULARY: 'vocabulary-10k.json';
24
+ readonly WORD_INDEX: 'word-index-10k.json';
25
+ readonly WORD_VECTORS: 'word-vectors-10k.bin';
26
+ };
27
+ export type ArtifactName = (typeof ARTIFACT_NAME)[keyof typeof ARTIFACT_NAME];
28
+ /** A single entry in the artifacts manifest. */
29
+ export interface AutocompleteArtifact {
30
+ checksum: string;
31
+ contentType: string;
32
+ name: string;
33
+ sizeBytes: number;
34
+ url: string;
35
+ }
36
+ /** Response body of the artifacts endpoint. */
37
+ export interface AutocompleteArtifactsResponse {
38
+ activeVersion: string;
39
+ artifacts: AutocompleteArtifact[];
40
+ }
41
+ /** Test-only: drops the memoised manifest so the next lookup re-requests it. */
42
+ export declare const resetArtifactsManifestCache: () => void;
43
+ /**
44
+ * Resolve the CDN URL for an artifact.
45
+ *
46
+ * :raises Error: when the manifest cannot be fetched or omits the artifact.
47
+ */
48
+ export declare const getArtifactUrl: (name: ArtifactName) => Promise<string>;
49
+ /**
50
+ * Download and parse a JSON artifact. The presigned URL carries its own auth
51
+ * token, so the request is sent without Atlassian headers or credentials.
52
+ *
53
+ * :raises Error: when the manifest lookup or the download fails.
54
+ */
55
+ export declare const fetchArtifactJson: <T>(name: ArtifactName) => Promise<T>;
@@ -29,22 +29,11 @@ export interface AutocompleteContext {
29
29
  siblingCommentsContents?: string[];
30
30
  }
31
31
  export interface AutocompletePluginOptions {
32
- /**
33
- * Product/editor surface where autocomplete runs (e.g. "comment", "chat").
34
- * Added to analytics and UFO metadata for cross-surface reporting.
35
- */
36
- surface?: string;
37
32
  /**
38
33
  * Async function called once on first editor focus to retrieve context for
39
34
  * word-frequency boosting. Called lazily so the preset can remain synchronous.
40
35
  */
41
36
  getContext?: () => Promise<AutocompleteContext | undefined>;
42
- /**
43
- * Async function that resolves to a URL for the word vectors binary file.
44
- * When provided, this takes precedence over the bundled asset URL.
45
- * Use this to serve vectors from a CDN or media service in production.
46
- */
47
- getVectorsBinaryUrl?: () => Promise<string>;
48
37
  /**
49
38
  * User locale used to determine whether autocomplete should run.
50
39
  * Defaults to browser locale when omitted.
@@ -56,6 +45,11 @@ export interface AutocompletePluginOptions {
56
45
  * Only meaningful alongside `getContext`; without it each notification is a no-op.
57
46
  */
58
47
  subscribeToContextUpdates?: (onContextUpdated: () => void) => () => void;
48
+ /**
49
+ * Product/editor surface where autocomplete runs (e.g. "comment", "chat").
50
+ * Added to analytics and UFO metadata for cross-surface reporting.
51
+ */
52
+ surface?: string;
59
53
  /**
60
54
  * When true, uses on-device inference via WebGPU (MLC WebLLM) instead of
61
55
  * the network-based slow-lane backend. Defaults to false (network client).
@@ -3,6 +3,9 @@
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
  export interface ScoringCandidate {
8
11
  authorFreq: number;
@@ -29,6 +32,17 @@ export interface GrammarFilterMeta {
29
32
  export declare const STAGE1_WEIGHT = 0.35;
30
33
  export declare const STAGE2_WEIGHT = 0.65;
31
34
  export declare const MIN_STAGE1_SCORE = 0.35;
35
+ export declare const isGrammarDataLoaded: () => boolean;
36
+ /**
37
+ * Fetch the POS tag and grammar transition payloads from the CDN.
38
+ *
39
+ * Safe to call repeatedly: the in-flight promise is shared, and a failure is not
40
+ * cached so a later call can retry.
41
+ */
42
+ export declare const loadGrammarDataAsync: (options?: {
43
+ isLocalLLM?: boolean;
44
+ surface?: string;
45
+ }) => Promise<void>;
32
46
  export interface PipelineDebug {
33
47
  final: number;
34
48
  grammarRejected: string[];
@@ -81,7 +81,6 @@ export declare const incrementSessionFreq: (word: string) => void;
81
81
  export declare const ingestDocumentPage: (pageContent: string | undefined) => void;
82
82
  export declare const predict: (textBefore: string) => string | null;
83
83
  export declare const loadVectorsAsync: (options?: {
84
- getBinaryUrl?: () => Promise<string>;
85
84
  isLocalLLM?: boolean;
86
85
  surface?: string;
87
86
  }) => Promise<void>;
package/package.json CHANGED
@@ -1,15 +1,12 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-autocomplete",
3
- "version": "6.0.0",
3
+ "version": "8.0.0",
4
4
  "description": "Client-side text autocomplete plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
7
7
  "publishConfig": {
8
8
  "registry": "https://registry.npmjs.org/"
9
9
  },
10
- "scripts": {
11
- "ak-postbuild": "ls -d dist/* | xargs -n 1 copyfiles -u 1 -V src/pm-plugins/data/*.json"
12
- },
13
10
  "atlassian": {
14
11
  "team": "Editor: AI",
15
12
  "singleton": true
@@ -30,8 +27,8 @@
30
27
  "wink-nlp": "^2.4.0"
31
28
  },
32
29
  "peerDependencies": {
33
- "@atlaskit/editor-common": "^118.0.0",
34
- "@atlaskit/editor-plugin-analytics": "^14.0.0",
30
+ "@atlaskit/editor-common": "^119.0.0",
31
+ "@atlaskit/editor-plugin-analytics": "^15.0.0",
35
32
  "react": "^18.2.0 || ^19.2.0"
36
33
  },
37
34
  "techstack": {
@@ -37,6 +37,7 @@ export const EXPERIENCE_NAME = {
37
37
  SLOW_LANE_FETCH: 'slow-lane-fetch',
38
38
  LOAD_VOCABULARY: 'load-vocabulary',
39
39
  LOAD_VECTORS: 'load-vectors',
40
+ LOAD_GRAMMAR: 'load-grammar',
40
41
  } as const;
41
42
 
42
43
  export type AutocompleteExperienceName = (typeof EXPERIENCE_NAME)[keyof typeof EXPERIENCE_NAME];
@@ -69,6 +70,10 @@ const experiences: Record<AutocompleteExperienceName, ConcurrentExperience> = {
69
70
  EXPERIENCE_NAME.LOAD_VECTORS,
70
71
  operationConfig,
71
72
  ),
73
+ [EXPERIENCE_NAME.LOAD_GRAMMAR]: new ConcurrentExperience(
74
+ EXPERIENCE_NAME.LOAD_GRAMMAR,
75
+ operationConfig,
76
+ ),
72
77
  };
73
78
 
74
79
  export const startExp = (
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Artifacts Manifest Client: resolves the autocomplete model artifacts hosted on
3
+ * the CDN.
4
+ *
5
+ * `GET /gateway/api/v1/autocomplete/artifacts` returns one presigned CDN URL per
6
+ * artifact for the tenant's active model version:
7
+ *
8
+ * { activeVersion, artifacts: [{ name, url, contentType, sizeBytes, checksum }] }
9
+ *
10
+ * Every loader in the plugin resolves its payload through here, so no model data
11
+ * is bundled with the package.
12
+ */
13
+
14
+ import { isAutocompleteDebugEnabled } from './debug-mode';
15
+
16
+ // ─── Types ───────────────────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Manifest artifact names. These are the manifest's lookup keys and are
20
+ * hyphenated, unlike the underscored filenames the artifacts are generated from.
21
+ */
22
+ export const ARTIFACT_NAME = {
23
+ FIRST_TOKEN_TO_WORDS: 'first-token-to-words.json',
24
+ GHOST_POS_TAGS: 'ghost-pos-tags.json',
25
+ GRAMMAR_TRANSITIONS: 'grammar-transitions-10k.json',
26
+ L3_VOCABULARY: 'l3-vocabulary.json',
27
+ POS_TAGS: 'combined-l2-l3-pos-tags.json',
28
+ VOCABULARY: 'vocabulary-10k.json',
29
+ WORD_INDEX: 'word-index-10k.json',
30
+ WORD_VECTORS: 'word-vectors-10k.bin',
31
+ } as const;
32
+
33
+ export type ArtifactName = (typeof ARTIFACT_NAME)[keyof typeof ARTIFACT_NAME];
34
+
35
+ /** A single entry in the artifacts manifest. */
36
+ export interface AutocompleteArtifact {
37
+ checksum: string;
38
+ contentType: string;
39
+ name: string;
40
+ sizeBytes: number;
41
+ url: string;
42
+ }
43
+
44
+ /** Response body of the artifacts endpoint. */
45
+ export interface AutocompleteArtifactsResponse {
46
+ activeVersion: string;
47
+ artifacts: AutocompleteArtifact[];
48
+ }
49
+
50
+ // ─── Constants ───────────────────────────────────────────────────────────────
51
+
52
+ const MANIFEST_ENDPOINT = '/gateway/api/v1/autocomplete/artifacts';
53
+
54
+ /**
55
+ * Presigned artifact URLs expire ~10 minutes after the manifest is issued, so
56
+ * the manifest is re-requested well inside that window instead of being cached
57
+ * for the session and handing out dead URLs to a late loader.
58
+ */
59
+ const MANIFEST_TTL_MS = 5 * 60 * 1000;
60
+
61
+ // ─── Manifest ────────────────────────────────────────────────────────────────
62
+
63
+ let manifestPromise: Promise<Map<string, AutocompleteArtifact>> | undefined;
64
+ let manifestRequestedAt = 0;
65
+
66
+ /** Test-only: drops the memoised manifest so the next lookup re-requests it. */
67
+ export const resetArtifactsManifestCache = (): void => {
68
+ manifestPromise = undefined;
69
+ manifestRequestedAt = 0;
70
+ };
71
+
72
+ const requestManifest = async (): Promise<Map<string, AutocompleteArtifact>> => {
73
+ const res = await fetch(MANIFEST_ENDPOINT, {
74
+ method: 'GET',
75
+ headers: {
76
+ 'x-experience-id': 'confluence-smart-typeahead-artifacts',
77
+ 'x-product': 'confluence',
78
+ },
79
+ });
80
+
81
+ if (!res.ok) {
82
+ throw new Error(`[autocomplete-artifacts] Manifest request failed: ${res.status}`);
83
+ }
84
+
85
+ const body = (await res.json()) as AutocompleteArtifactsResponse;
86
+
87
+ if (!Array.isArray(body?.artifacts) || body.artifacts.length === 0) {
88
+ throw new Error('[autocomplete-artifacts] Manifest response contained no artifacts');
89
+ }
90
+
91
+ if (isAutocompleteDebugEnabled()) {
92
+ // eslint-disable-next-line no-console
93
+ console.log('[autocomplete-artifacts] Manifest loaded:', {
94
+ activeVersion: body.activeVersion,
95
+ names: body.artifacts.map((artifact) => artifact.name),
96
+ });
97
+ }
98
+
99
+ return new Map(body.artifacts.map((artifact) => [artifact.name, artifact]));
100
+ };
101
+
102
+ const getManifest = (): Promise<Map<string, AutocompleteArtifact>> => {
103
+ if (manifestPromise && Date.now() - manifestRequestedAt < MANIFEST_TTL_MS) {
104
+ return manifestPromise;
105
+ }
106
+
107
+ manifestRequestedAt = Date.now();
108
+ manifestPromise = requestManifest().catch((e) => {
109
+ // Never cache a rejection — a transient failure would otherwise stop every
110
+ // later loader in the session from retrying.
111
+ resetArtifactsManifestCache();
112
+ throw e;
113
+ });
114
+
115
+ return manifestPromise;
116
+ };
117
+
118
+ // ─── Public API ──────────────────────────────────────────────────────────────
119
+
120
+ /**
121
+ * Resolve the CDN URL for an artifact.
122
+ *
123
+ * :raises Error: when the manifest cannot be fetched or omits the artifact.
124
+ */
125
+ export const getArtifactUrl = async (name: ArtifactName): Promise<string> => {
126
+ const manifest = await getManifest();
127
+ const url = manifest.get(name)?.url;
128
+
129
+ if (!url) {
130
+ throw new Error(`[autocomplete-artifacts] Manifest has no URL for "${name}"`);
131
+ }
132
+
133
+ return url;
134
+ };
135
+
136
+ /**
137
+ * Download and parse a JSON artifact. The presigned URL carries its own auth
138
+ * token, so the request is sent without Atlassian headers or credentials.
139
+ *
140
+ * :raises Error: when the manifest lookup or the download fails.
141
+ */
142
+ export const fetchArtifactJson = async <T>(name: ArtifactName): Promise<T> => {
143
+ const url = await getArtifactUrl(name);
144
+ const res = await fetch(url);
145
+
146
+ if (!res.ok) {
147
+ throw new Error(`[autocomplete-artifacts] Failed to download "${name}": ${res.status}`);
148
+ }
149
+
150
+ return (await res.json()) as T;
151
+ };
@@ -22,6 +22,7 @@ import {
22
22
  type LocalSlowLaneLoadError,
23
23
  type LocalSlowLaneLoadSuccess,
24
24
  } from './local-slow-lane-client';
25
+ import { loadGrammarDataAsync } from './scoring-pipeline';
25
26
  import { createSlowLaneClient, setDefaultSlowLaneClient, isWordBoundary } from './slow-lane-client';
26
27
  import {
27
28
  predict,
@@ -224,22 +225,11 @@ export interface AutocompleteContext {
224
225
  }
225
226
 
226
227
  export interface AutocompletePluginOptions {
227
- /**
228
- * Product/editor surface where autocomplete runs (e.g. "comment", "chat").
229
- * Added to analytics and UFO metadata for cross-surface reporting.
230
- */
231
- surface?: string;
232
228
  /**
233
229
  * Async function called once on first editor focus to retrieve context for
234
230
  * word-frequency boosting. Called lazily so the preset can remain synchronous.
235
231
  */
236
232
  getContext?: () => Promise<AutocompleteContext | undefined>;
237
- /**
238
- * Async function that resolves to a URL for the word vectors binary file.
239
- * When provided, this takes precedence over the bundled asset URL.
240
- * Use this to serve vectors from a CDN or media service in production.
241
- */
242
- getVectorsBinaryUrl?: () => Promise<string>;
243
233
  /**
244
234
  * User locale used to determine whether autocomplete should run.
245
235
  * Defaults to browser locale when omitted.
@@ -251,6 +241,11 @@ export interface AutocompletePluginOptions {
251
241
  * Only meaningful alongside `getContext`; without it each notification is a no-op.
252
242
  */
253
243
  subscribeToContextUpdates?: (onContextUpdated: () => void) => () => void;
244
+ /**
245
+ * Product/editor surface where autocomplete runs (e.g. "comment", "chat").
246
+ * Added to analytics and UFO metadata for cross-surface reporting.
247
+ */
248
+ surface?: string;
254
249
  /**
255
250
  * When true, uses on-device inference via WebGPU (MLC WebLLM) instead of
256
251
  * the network-based slow-lane backend. Defaults to false (network client).
@@ -831,7 +826,6 @@ export const createAutocompletePlugin = (
831
826
  });
832
827
  });
833
828
  loadVectorsAsync({
834
- getBinaryUrl: options?.getVectorsBinaryUrl,
835
829
  isLocalLLM: options?.useLocalModel ?? false,
836
830
  surface,
837
831
  }).catch((error) => {
@@ -839,6 +833,14 @@ export const createAutocompletePlugin = (
839
833
  location: 'editor-plugin-autocomplete/loadVectorsAsync',
840
834
  });
841
835
  });
836
+ loadGrammarDataAsync({
837
+ isLocalLLM: options?.useLocalModel ?? false,
838
+ surface,
839
+ }).catch((error) => {
840
+ logException(error as Error, {
841
+ location: 'editor-plugin-autocomplete/loadGrammarDataAsync',
842
+ });
843
+ });
842
844
  if (!hasIngestedPage) {
843
845
  hasIngestedPage = true;
844
846
  refreshContext({