@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,113 @@
1
1
  # @atlaskit/editor-plugin-autocomplete
2
2
 
3
+ ## 9.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`a164c6d8e7de6`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/a164c6d8e7de6) -
8
+ Offer the Confluence page the chat is open on to autocomplete as context, so words already written
9
+ on it carry an L1 session boost in the chat input. This reads the editor context the page editor
10
+ already publishes, so no host wiring is needed, and covers a page being edited; reaching a page
11
+ that is being viewed needs a host bridge that comes separately. The page text is held together
12
+ with the page it was read from, so navigating invalidates it in the same render that changes the
13
+ page rather than just after — the context store keeps the last published document until another
14
+ publisher replaces it, and that document does not say which page it came from.
15
+
16
+ Both the page and the chat transcript sit behind
17
+ `platform_editor_ai_autocomplete_rovo_chat_editor`, and the editor context subscription that reads
18
+ the page lives in a component the chat input only mounts once enrolled. An unenrolled session
19
+ therefore neither parses a page document for context it cannot use nor re-renders the input on a
20
+ publish, which the editor does on blur and on selection change.
21
+
22
+ Context text now waits for the vocabulary load to settle before it is applied. Session boosts were
23
+ previously spent against an empty trie whenever the page resolved first, which silently dropped
24
+ every word in it. What the session holds is readable from the console with
25
+ `__atlCtcDebug__.session()`.
26
+
27
+ ### Patch Changes
28
+
29
+ - Updated dependencies
30
+
31
+ ## 9.0.0
32
+
33
+ ### Major Changes
34
+
35
+ - [`94ddcfaca6b5d`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/94ddcfaca6b5d) -
36
+ Improve client-only contextual typeahead confidence and responsiveness, and extend it to bigram
37
+ and phrase completions.
38
+
39
+ All of this ships inside the autocomplete plugin, which is only ever added to a preset behind an
40
+ experiment that defaults to off: `platform_editor_ai_autocomplete_rovo_chat_editor` for Rovo chat,
41
+ with a Confluence `productKey` check ahead of the read, and
42
+ `platform_editor_ai_autocomplete_conf_comments` for Confluence comments. The on-device model path
43
+ these changes concentrate on is additionally behind
44
+ `platform_editor_ai_autocomplete_conf_local_setup`, which selects the slow-lane client. With those
45
+ experiments off the plugin is never constructed, so nothing here runs.
46
+
47
+ **Confidence.** Candidates competing at the same word boundary are normalised into a posterior
48
+ over that shortlist, so a suggestion's confidence is the share of probability mass the model puts
49
+ on it rather than its distance from whichever candidate happens to lead. The comparison is made on
50
+ each surface's sequence log-likelihood rather than its per-token mean, which is not comparable
51
+ across surfaces of different token counts. A shortlist is normalised over its minimal members
52
+ only, so a surface and its own extension are no longer double-counted. A surface must additionally
53
+ clear an absolute plausibility floor, anchored at the uniform distribution over the model's
54
+ 49,152-token vocabulary (about -10.8 nats) rather than read off a histogram, hold a clear lead
55
+ over the runner-up, and come from a shortlist that offered a real alternative. Completions shorter
56
+ than three characters are no longer offered, and multi-word terms are held to the same
57
+ previous-word grammar transition as single words.
58
+
59
+ **Responsiveness.** Candidate work starts at two typed characters while ghost text stays gated at
60
+ three and must arrive within a 100 ms decision window. Queued scoring work is drained in an order
61
+ that keeps the engine's decoded sequence alive, so continuing a prefix costs a single decode step
62
+ instead of a full prompt prefill, and work no live decision would accept is discarded rather than
63
+ run. Contexts keep their prefilled model state until 128 are resident rather than 32, which stops
64
+ contexts being evicted while still in use. Once displayed, a ghost is an immutable snapshot shared
65
+ by rendering, Tab insertion, analytics and cooldown, so a later model result cannot make the
66
+ visible completion disagree with the inserted one.
67
+
68
+ **Per-keystroke cost.** One keystroke runs `predict()` several times — once when the decision
69
+ opens and again for each async evidence signal that lands inside the budget — and only the
70
+ evidence differs between those runs. Trie recall and the canonical context derived per candidate
71
+ are computed once and reused across them, invalidated when an artifact load changes what the tries
72
+ can return. Within one derivation the position-dependent half of a candidate's context is shared
73
+ by every candidate starting at the same offset, rather than being rebuilt a couple of hundred
74
+ times. A distribution's log-partition is memoised per logits buffer, since it does not depend on
75
+ the token being scored and the two full-vocabulary passes behind it previously ran once per
76
+ candidate for an answer that could not change. None of this changes which suggestion is chosen.
77
+
78
+ **Multi-word completions.** Bigrams and phrases are surfaced from a first-word or in-progress
79
+ window, frequency is normalised per term type, and selection runs a per-type precision floor
80
+ followed by expected-value arbitration, so the longest confident unit wins and a shaky long unit
81
+ self-demotes to a safer shorter one. Phrases surface only once the on-device continuation LM has
82
+ vouched for them. A whole-surface repetition guard and a post-accept cooldown together prevent
83
+ echoes such as `end to end` becoming `end to end to end`.
84
+
85
+ **Artifacts.** `bigrams.json`, `phrases.json` and `phrase-continuation-tokens.json` are resolved
86
+ from the artifacts manifest alongside the vocabulary and grammar payloads, so they are served from
87
+ the CDN rather than bundled. Each is fetched independently, so an unpublished payload disables
88
+ only its own term type. Every artifact is shape-checked as it downloads, failing at the boundary
89
+ naming the artifact rather than degrading later into an empty trie.
90
+
91
+ **Diagnostics.** Debug output is unified under a single `[CTC]` namespace behind
92
+ `__atlCtcDebug__.enable()`, with one collapsed group per keystroke and an opt-in verbose level for
93
+ the scored candidate table. All informational console output is routed behind the flag.
94
+
95
+ **Breaking change.** `@atlaskit/editor-plugin-autocomplete` no longer exposes its pm-plugin
96
+ modules as subpaths. The `./src/pm-plugins/autocomplete-plugin`,
97
+ `./src/pm-plugins/slow-lane-client` and `./src/pm-plugins/text-predictor` entry points are
98
+ removed, along with the `CANONICAL_FIX__DO_NOT_USE_ME_A`, `_B` and `_C` subpaths that exposed the
99
+ same three modules in raw form. The matching `./autocomplete/*` re-export wrappers in
100
+ `@atlaskit/editor-plugins` are removed too. No product consumed any of them; they were reachable
101
+ only from this plugin's own test package and the generated aggregator.
102
+
103
+ Removing both sets together is deliberate. The curated subpaths were named after real file paths,
104
+ so each one shadowed the module it wrapped and left two files claiming the same bundler canonical
105
+ id — which is what the `CANONICAL_FIX` subpaths existed to disambiguate. Keeping either set alone
106
+ reintroduces the collision. With both gone, nothing shadows a source path, and because the
107
+ wrappers mirrored the raw modules under an exact export-parity test, `@atlaskit/editor-plugins` is
108
+ no longer coupled to this plugin's internal module layout. `./autocompletePlugin` and
109
+ `./autocompletePluginType` are unaffected.
110
+
3
111
  ## 8.0.0
4
112
 
5
113
  ### Patch Changes
@@ -39,6 +39,7 @@ var EXPERIENCE_NAME = exports.EXPERIENCE_NAME = {
39
39
  SLOW_LANE_FETCH: 'slow-lane-fetch',
40
40
  LOAD_VOCABULARY: 'load-vocabulary',
41
41
  LOAD_VECTORS: 'load-vectors',
42
+ LOAD_PHRASES: 'load-phrases',
42
43
  LOAD_GRAMMAR: 'load-grammar'
43
44
  };
44
45
  var isUfoEnabled = function isUfoEnabled() {
@@ -57,7 +58,7 @@ var operationConfig = {
57
58
  })
58
59
  }
59
60
  };
60
- var experiences = (0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)({}, EXPERIENCE_NAME.SLOW_LANE_FETCH, new _ufo.ConcurrentExperience(EXPERIENCE_NAME.SLOW_LANE_FETCH, operationConfig)), EXPERIENCE_NAME.LOAD_VOCABULARY, new _ufo.ConcurrentExperience(EXPERIENCE_NAME.LOAD_VOCABULARY, operationConfig)), EXPERIENCE_NAME.LOAD_VECTORS, new _ufo.ConcurrentExperience(EXPERIENCE_NAME.LOAD_VECTORS, operationConfig)), EXPERIENCE_NAME.LOAD_GRAMMAR, new _ufo.ConcurrentExperience(EXPERIENCE_NAME.LOAD_GRAMMAR, operationConfig));
61
+ var experiences = (0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)((0, _defineProperty2.default)({}, EXPERIENCE_NAME.SLOW_LANE_FETCH, new _ufo.ConcurrentExperience(EXPERIENCE_NAME.SLOW_LANE_FETCH, operationConfig)), EXPERIENCE_NAME.LOAD_VOCABULARY, new _ufo.ConcurrentExperience(EXPERIENCE_NAME.LOAD_VOCABULARY, operationConfig)), EXPERIENCE_NAME.LOAD_VECTORS, new _ufo.ConcurrentExperience(EXPERIENCE_NAME.LOAD_VECTORS, operationConfig)), EXPERIENCE_NAME.LOAD_PHRASES, new _ufo.ConcurrentExperience(EXPERIENCE_NAME.LOAD_PHRASES, operationConfig)), EXPERIENCE_NAME.LOAD_GRAMMAR, new _ufo.ConcurrentExperience(EXPERIENCE_NAME.LOAD_GRAMMAR, operationConfig));
61
62
  var startExp = exports.startExp = function startExp(name, id, metadata) {
62
63
  if (!isUfoEnabled()) {
63
64
  return;
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.fetchAutocompleteArtifactJson = exports.fetchAutocompleteArtifactBinary = exports.buildAutocompleteGatewayUrl = void 0;
8
+ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
9
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
10
+ var _artifactsManifest = require("./artifacts-manifest");
11
+ var _debugMode = require("./debug-mode");
12
+ /**
13
+ * Validation and diagnostics layer over the artifacts manifest.
14
+ *
15
+ * `artifacts-manifest` owns *where* a payload comes from (one manifest request,
16
+ * then a presigned CDN download). This module owns *whether the payload is
17
+ * usable*: every loader declares the shape it expects, so a mis-published
18
+ * artifact fails at the boundary with the artifact's name rather than surfacing
19
+ * later as an empty trie or a silent no-op in scoring.
20
+ */
21
+
22
+ var TRAILING_SLASH_REGEX = /\/$/;
23
+ var DEFAULT_BASE_URL = '';
24
+ var AUTOCOMPLETE_GATEWAY_BASE = '/gateway/api/v1/autocomplete';
25
+ /**
26
+ * Build a URL for one of the autocomplete gateway's own endpoints (e.g. the
27
+ * network slow lane). Model artifacts do NOT go through here — they are served
28
+ * from the CDN via the manifest.
29
+ */
30
+ var buildAutocompleteGatewayUrl = exports.buildAutocompleteGatewayUrl = function buildAutocompleteGatewayUrl(path) {
31
+ var baseUrl = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : DEFAULT_BASE_URL;
32
+ var normalizedBase = baseUrl.replace(TRAILING_SLASH_REGEX, '');
33
+ return "".concat(normalizedBase).concat(AUTOCOMPLETE_GATEWAY_BASE).concat(path);
34
+ };
35
+
36
+ /**
37
+ * Download a JSON artifact and assert its shape before handing it to a loader.
38
+ *
39
+ * :params:
40
+ * artifactName: Manifest key for the artifact
41
+ * options.label: Name used in diagnostics; defaults to the manifest key
42
+ * options.summarize: Renders a short size/count summary for the debug log
43
+ * options.validate: Type guard the payload must satisfy
44
+ * :returns:
45
+ * The parsed, validated payload
46
+ */
47
+ var fetchAutocompleteArtifactJson = exports.fetchAutocompleteArtifactJson = /*#__PURE__*/function () {
48
+ var _ref = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(artifactName) {
49
+ var _options$label, _options$summarize;
50
+ var options,
51
+ label,
52
+ payload,
53
+ summary,
54
+ _args = arguments;
55
+ return _regenerator.default.wrap(function (_context) {
56
+ while (1) switch (_context.prev = _context.next) {
57
+ case 0:
58
+ options = _args.length > 1 && _args[1] !== undefined ? _args[1] : {};
59
+ label = (_options$label = options.label) !== null && _options$label !== void 0 ? _options$label : artifactName; // Presigned CDN URLs carry an auth token, so only the artifact name is logged.
60
+ (0, _debugMode.ctcTag)('init', "\u2193 loading ".concat(label));
61
+ _context.next = 1;
62
+ return (0, _artifactsManifest.fetchArtifactJson)(artifactName);
63
+ case 1:
64
+ payload = _context.sent;
65
+ if (!(options.validate && !options.validate(payload))) {
66
+ _context.next = 2;
67
+ break;
68
+ }
69
+ throw new Error("[autocomplete-artifacts] ".concat(label, " payload shape was invalid"));
70
+ case 2:
71
+ summary = (_options$summarize = options.summarize) === null || _options$summarize === void 0 ? void 0 : _options$summarize.call(options, payload);
72
+ (0, _debugMode.ctcTag)('init', "\u2713 loaded ".concat(label).concat(summary ? " (".concat(summary, ")") : ''));
73
+ return _context.abrupt("return", payload);
74
+ case 3:
75
+ case "end":
76
+ return _context.stop();
77
+ }
78
+ }, _callee);
79
+ }));
80
+ return function fetchAutocompleteArtifactJson(_x) {
81
+ return _ref.apply(this, arguments);
82
+ };
83
+ }();
84
+
85
+ /**
86
+ * Download a binary artifact (the word-vectors blob) as an ArrayBuffer.
87
+ *
88
+ * :params:
89
+ * artifactName: Manifest key for the artifact
90
+ * label: Name used in diagnostics; defaults to the manifest key
91
+ * :returns:
92
+ * The raw bytes of the artifact
93
+ */
94
+ var fetchAutocompleteArtifactBinary = exports.fetchAutocompleteArtifactBinary = /*#__PURE__*/function () {
95
+ var _ref2 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee2(artifactName) {
96
+ var label,
97
+ url,
98
+ response,
99
+ payload,
100
+ _args2 = arguments;
101
+ return _regenerator.default.wrap(function (_context2) {
102
+ while (1) switch (_context2.prev = _context2.next) {
103
+ case 0:
104
+ label = _args2.length > 1 && _args2[1] !== undefined ? _args2[1] : artifactName;
105
+ (0, _debugMode.ctcTag)('init', "\u2193 loading ".concat(label));
106
+ _context2.next = 1;
107
+ return (0, _artifactsManifest.getArtifactUrl)(artifactName);
108
+ case 1:
109
+ url = _context2.sent;
110
+ _context2.next = 2;
111
+ return fetch(url);
112
+ case 2:
113
+ response = _context2.sent;
114
+ if (response.ok) {
115
+ _context2.next = 3;
116
+ break;
117
+ }
118
+ throw new Error("[autocomplete-artifacts] Failed to fetch ".concat(label, ": ").concat(response.status));
119
+ case 3:
120
+ _context2.next = 4;
121
+ return response.arrayBuffer();
122
+ case 4:
123
+ payload = _context2.sent;
124
+ (0, _debugMode.ctcTag)('init', "\u2713 loaded ".concat(label, " (").concat(payload.byteLength, " bytes)"));
125
+ return _context2.abrupt("return", payload);
126
+ case 5:
127
+ case "end":
128
+ return _context2.stop();
129
+ }
130
+ }, _callee2);
131
+ }));
132
+ return function fetchAutocompleteArtifactBinary(_x2) {
133
+ return _ref2.apply(this, arguments);
134
+ };
135
+ }();
@@ -28,10 +28,13 @@ var _debugMode = require("./debug-mode");
28
28
  * hyphenated, unlike the underscored filenames the artifacts are generated from.
29
29
  */
30
30
  var ARTIFACT_NAME = exports.ARTIFACT_NAME = {
31
+ BIGRAMS: 'bigrams.json',
31
32
  FIRST_TOKEN_TO_WORDS: 'first-token-to-words.json',
32
33
  GHOST_POS_TAGS: 'ghost-pos-tags.json',
33
34
  GRAMMAR_TRANSITIONS: 'grammar-transitions-10k.json',
34
35
  L3_VOCABULARY: 'l3-vocabulary.json',
36
+ PHRASE_CONTINUATION_TOKENS: 'phrase-continuation-tokens.json',
37
+ PHRASES: 'phrases.json',
35
38
  POS_TAGS: 'combined-l2-l3-pos-tags.json',
36
39
  VOCABULARY: 'vocabulary-10k.json',
37
40
  WORD_INDEX: 'word-index-10k.json',