@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
@@ -6,6 +6,7 @@
6
6
  * Request: { text, session_id }
7
7
  * Response: { semantic_vector: number[], lm_logits: Record<string, number> }
8
8
  */
9
+ import type { BoundaryLmState, BoundaryPrimeRequest, ProgressiveSurfaceEvidence, SurfaceScore, SurfaceScoreRequest } from './canonical-lm-scoring';
9
10
  /** Request payload for typeahead-encodings endpoint. */
10
11
  export interface TypeaheadEncodingsRequest {
11
12
  session_id: string;
@@ -34,14 +35,61 @@ export interface SlowLaneClientConfig {
34
35
  sessionId?: string;
35
36
  surface?: string;
36
37
  }
37
- export declare const createSlowLaneClient: (config: SlowLaneClientConfig) => {
38
+ export interface SlowLaneClient {
39
+ getBoundaryLmState?: (contextKey: string) => BoundaryLmState | null;
40
+ getCanonicalSurfaceCount?: () => number;
41
+ getCanonicalSurfaceTokenIds?: (surface: string) => number[] | null;
42
+ getContextInput?: () => string | null;
38
43
  getContextVector: () => Float32Array | null;
39
44
  getLmLogits: () => Record<string, number> | null;
45
+ getProgressiveSurfaceEvidence?: (contextKey: string, surface: string) => ProgressiveSurfaceEvidence | null;
46
+ getSurfaceScore?: (contextKey: string, surface: string) => SurfaceScore | null;
47
+ isReady?: () => boolean;
40
48
  isWordBoundary: (text: string) => boolean;
49
+ primeBoundaryLm?: (input: BoundaryPrimeRequest) => void;
50
+ requestProgressiveSurfaceScores?: (input: SurfaceScoreRequest) => void;
41
51
  setContextVector: (vector: Float32Array | null) => void;
42
52
  setLmLogits: (logits: Record<string, number> | null) => void;
43
53
  updateContext: (text: string) => void;
44
- };
45
- export declare const setDefaultSlowLaneClient: (client: ReturnType<typeof createSlowLaneClient> | null) => void;
54
+ }
55
+ export declare const createSlowLaneClient: (config: SlowLaneClientConfig) => SlowLaneClient;
56
+ export type SlowLaneClientKind = 'localLlm' | 'server';
57
+ export interface SlowLaneClientRegistration {
58
+ clientId: string;
59
+ kind: SlowLaneClientKind;
60
+ }
61
+ export interface SlowLaneClientStatus {
62
+ canonicalScoringSupported: boolean;
63
+ canonicalSurfaceCount: number | null;
64
+ clientId: string | null;
65
+ clientKind: SlowLaneClientKind | null;
66
+ localModelReady: boolean | null;
67
+ registered: boolean;
68
+ }
69
+ export declare const setDefaultSlowLaneClient: (client: SlowLaneClient | null, registration?: SlowLaneClientRegistration) => void;
70
+ /**
71
+ * Clear the shared client only when the caller still owns the registration.
72
+ *
73
+ * Multiple editor instances can briefly overlap during a React/editor remount.
74
+ * An older instance must not disconnect the newer instance when its delayed
75
+ * teardown runs after the newer client has registered.
76
+ */
77
+ export declare const clearDefaultSlowLaneClient: (client: SlowLaneClient) => boolean;
78
+ export declare const getDefaultSlowLaneClientStatus: () => SlowLaneClientStatus;
46
79
  export declare const getStoredContextVector: () => Float32Array | null;
80
+ export declare const getStoredContextInput: () => string | null;
47
81
  export declare const getStoredLmLogits: () => Record<string, number> | null;
82
+ export declare const getBoundaryLmState: (contextKey: string) => BoundaryLmState | null;
83
+ export declare const getCanonicalSurfaceTokenIds: (surface: string) => number[] | null;
84
+ export declare const getSurfaceScore: (contextKey: string, surface: string) => SurfaceScore | null;
85
+ export declare const getProgressiveSurfaceEvidence: (contextKey: string, surface: string) => ProgressiveSurfaceEvidence | null;
86
+ export declare const primeBoundaryLm: (input: BoundaryPrimeRequest) => void;
87
+ export declare const requestProgressiveSurfaceScores: (input: SurfaceScoreRequest) => void;
88
+ export declare const isCanonicalSurfaceScoringSupported: () => boolean;
89
+ /**
90
+ * How many surfaces the canonical token map currently holds. Goes from zero to
91
+ * its final size once the artifact lands, which is the only point at which
92
+ * `getCanonicalSurfaceTokenIds` starts answering — so callers memoising over
93
+ * that lookup can use this to detect the transition.
94
+ */
95
+ export declare const getCanonicalSurfaceCount: () => number;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Two prediction modes:
5
5
  * 1. Word boundary → bigram-based next-word suggestion (grammar-filtered)
6
- * 2. Mid-word (≥3 chars) → trie prefix search → scoring pipeline → top result
6
+ * 2. Mid-word (≥1 char) → grouped prefetch; ≥3 chars + exact evidence → ghost result
7
7
  *
8
8
  * Scoring is delegated to scoring-pipeline.ts which handles:
9
9
  * Stage 1 (semantic + frequency), grammar filter, Stage 2 (optional LM re-ranking).
@@ -12,8 +12,12 @@
12
12
  * Falls back to cold mode (freq-only) when vectors not yet loaded.
13
13
  *
14
14
  * Session personalization (L1): words the user types are incrementally boosted
15
- * via incrementSessionFreq(), called on word boundaries from the plugin.
15
+ * via incrementSessionFreq(), called on word boundaries from the plugin, and
16
+ * words in ingested context text via ingestDocumentPage(). What the session has
17
+ * boosted is visible at any time from the console: `__atlCtcDebug__.session()`,
18
+ * or `__atlCtcDebug__.session('poll')` for one family — see inspectSessionBoosts.
16
19
  */
20
+ import type { TermType } from './scoring-pipeline';
17
21
  export interface WeightedTerm {
18
22
  authorFreq: number;
19
23
  docFreq: number;
@@ -33,13 +37,32 @@ interface VectorStore {
33
37
  * expects a simple array of strings: ["about", "above", "actually", ...]
34
38
  */
35
39
  export declare const initL3Vocabulary: (l3Words: string[]) => void;
40
+ /**
41
+ * Start a short cooldown for the exact surface the editor inserted. The caller
42
+ * passes the committed snapshot's surface so background re-ranking can never
43
+ * move cooldown bookkeeping away from what the user actually accepted.
44
+ */
45
+ export declare const noteSuggestionAccepted: (surface: string) => void;
36
46
  /**
37
47
  * Get predictor status for debugging.
38
48
  * vectorsLoaded: true when semantic scoring is active
39
49
  * wordCount: number of words in vector store (0 if not loaded)
40
50
  */
41
51
  export declare const getPredictorStatus: () => {
52
+ canonicalScoringSupported: boolean;
53
+ canonicalSurfaceCount: number | null;
54
+ clientId: string | null;
55
+ clientKind: 'localLlm' | 'server' | null;
42
56
  isInitialized: boolean;
57
+ localModelReady: boolean | null;
58
+ maxFreqByType: {
59
+ bigram: number;
60
+ phrase: number;
61
+ word: number;
62
+ };
63
+ phraseCount: number;
64
+ phrasesLoaded: boolean;
65
+ slowLaneRegistered: boolean;
43
66
  vectorsLoaded: boolean;
44
67
  vectorsLoadStarted: boolean;
45
68
  wordCount: number;
@@ -49,8 +72,10 @@ export declare const getPredictorStatus: () => {
49
72
  * Returns null if no prediction has run yet or debug was off.
50
73
  */
51
74
  export declare const getLastPredictionDebug: () => {
75
+ awaitingAsyncEvidence: boolean;
52
76
  contextWords: string[];
53
77
  currentWord: string;
78
+ decision: string;
54
79
  mode: 'cold' | 'warm';
55
80
  suggestion: string | null;
56
81
  textBefore: string;
@@ -63,6 +88,16 @@ export declare const getLastPredictionDebug: () => {
63
88
  }>;
64
89
  } | null;
65
90
  export declare const initVocabulary: (vocabulary: TenantVocabulary) => void;
91
+ /**
92
+ * Insert a producer-shipped bigram/phrase artifact into the phrase trie.
93
+ *
94
+ * :params:
95
+ * artifact: Normalized `{term: {freq, doc_freq, author_freq, pos_seq, head_pos, association}}` map
96
+ * termType: Whether these terms are `bigram` (2 tokens) or `phrase` (3 tokens)
97
+ * :returns:
98
+ * The number of terms inserted
99
+ */
100
+ export declare const initPhrases: (artifact: PhraseArtifactJson, termType: TermType) => number;
66
101
  /**
67
102
  * Increment L1 session frequency for a single word.
68
103
  * Called from the plugin on word boundaries for efficient incremental boosting.
@@ -79,14 +114,132 @@ export declare const incrementSessionFreq: (word: string) => void;
79
114
  * calling context does not yet have a page value available.
80
115
  */
81
116
  export declare const ingestDocumentPage: (pageContent: string | undefined) => void;
82
- export declare const predict: (textBefore: string) => string | null;
117
+ interface SessionWordSnapshot {
118
+ /** Times this session has seen it: words typed plus words in ingested text. */
119
+ sessionFreq: number;
120
+ /** No corpus frequency behind it, so L1 is the whole of its standing. */
121
+ sessionOnly: boolean;
122
+ surface: string;
123
+ /** Corpus frequency shipped with the vocabulary, for scale against the boost. */
124
+ tenantFreq: number;
125
+ }
126
+ export interface SessionSnapshot {
127
+ /** How many words hold a boost, whether or not they are listed below. */
128
+ boosted: number;
129
+ /** Ceiling on `words`; beyond it the weakest boosts are left out of the listing. */
130
+ limit: number;
131
+ /** The prefix asked about, when one was passed. */
132
+ prefix?: string;
133
+ /** Strongest boost first, then alphabetically. */
134
+ words: SessionWordSnapshot[];
135
+ }
136
+ /**
137
+ * Read the session's L1 boosts, optionally narrowed to a prefix.
138
+ *
139
+ * Installed as `__atlCtcDebug__.session()`, with `__atlCtcDebug__.session('poll')`
140
+ * to ask about one family. Returned rather than logged, so the console renders it
141
+ * as an inspectable object and a caller can assert on it.
142
+ *
143
+ * Only words the vocabulary already holds can carry a boost, because both writers
144
+ * go through `incrementSessionFreq` and it only finds existing nodes. An ingested
145
+ * word absent from the vocabulary is therefore missing from here and always will
146
+ * be.
147
+ */
148
+ export declare const inspectSessionBoosts: (prefix?: string) => SessionSnapshot;
149
+ /**
150
+ * Result of a prediction: the ghost tail to insert plus an immutable record of
151
+ * the evidence that authorized the UI commitment.
152
+ */
153
+ export interface PredictionResult {
154
+ /** Canonical evidence depth for the selected full surface. */
155
+ evidenceDepth: {
156
+ totalChars: number;
157
+ totalTokens: number;
158
+ verifiedChars: number;
159
+ verifiedTokens: number;
160
+ };
161
+ /** Evidence tier that authorized display. Tier A is never display-eligible. */
162
+ evidenceTier: 'canonical-full-surface' | 'network-logit';
163
+ /**
164
+ * Mean per-token log-probability of the verified prefix.
165
+ *
166
+ * Carried alongside the posterior because the two answer different
167
+ * questions and a plausibility floor is placed on this one. A surface alone
168
+ * in its normaliser holds the whole pool whatever the model thinks of it,
169
+ * so its posterior cannot say whether it was worth showing and this can.
170
+ */
171
+ meanTokenLogProbability: number;
172
+ /**
173
+ * Whether a longer candidate in the same pool extends the selected surface.
174
+ *
175
+ * The chain rule leaves such a surface holding its pool's mass undivided, so
176
+ * its posterior is high for a structural reason rather than a modelled one —
177
+ * whichever continuation was meant, the prefix was right. Carried so the top
178
+ * posterior bucket can be split on it, since a lifted nested leader and a
179
+ * genuinely preferred surface are otherwise the same number.
180
+ */
181
+ poolHeldExtension: boolean;
182
+ /** Share of its shortlist's mass the model put on the selected surface. */
183
+ posterior: number;
184
+ /** Final Stage-1 + LM ranking score. */
185
+ rankScore: number;
186
+ /** How many scored candidates the selected surface's normaliser divided between. */
187
+ shortlistSize: number;
188
+ /** Full surface of the selected candidate (identity for keep-vs-swap checks). */
189
+ surface: string;
190
+ /** Whether the selected surface is a single word, a bigram, or a phrase. */
191
+ termType: TermType;
192
+ /** Ghost tail actually shown/inserted (surface minus the already-typed prefix). */
193
+ text: string;
194
+ /** Final-score lead over the strongest evidence-backed runner-up. */
195
+ winnerMargin: number;
196
+ }
197
+ export declare const predict: (textBefore: string) => PredictionResult | null;
198
+ /**
199
+ * Per-term stats shape for `bigrams.json` / `phrases.json`. Field names/types
200
+ * confirmed against the producer (`l2_vocabulary_creation._build_ngram_stat_payload`
201
+ * + `artifact_release._validate_ngram_payload`): `freq`/`doc_freq`/`author_freq`
202
+ * are ints, `pos_seq` a string array, `head_pos` a string or null, `association`
203
+ * a number.
204
+ */
205
+ interface PhraseStatsJson {
206
+ association?: number;
207
+ author_freq: number;
208
+ doc_freq: number;
209
+ freq: number;
210
+ head_pos?: string | null;
211
+ pos_seq?: string[];
212
+ }
213
+ /** Normalized `{term: stats}` map after unwrapping any producer wrapper key. */
214
+ type PhraseArtifactJson = Record<string, PhraseStatsJson>;
83
215
  export declare const loadVectorsAsync: (options?: {
84
216
  isLocalLLM?: boolean;
85
217
  surface?: string;
86
218
  }) => Promise<void>;
87
219
  export declare const initVectors: (store: VectorStore) => void;
220
+ /**
221
+ * Load the producer's `bigrams.json` + `phrases.json` completion-term artifacts
222
+ * over the TDP-OS gateway and insert them into the phrase trie.
223
+ *
224
+ * Each file is fetched independently (`Promise.allSettled`) so a missing or
225
+ * late-published artifact only skips that term type — word completion and the
226
+ * other term type are unaffected. Phrase VECTORS are not fetched here: per the
227
+ * producer's `extend` decision they are appended into the existing
228
+ * `word-index-10k.json` / `word-vectors-10k.bin`, so `loadVectorsAsync` already
229
+ * covers them.
230
+ *
231
+ * :params:
232
+ * options.isLocalLLM: Tags the UFO experience so latency/success feeds the same SLO
233
+ * :returns:
234
+ * A promise that resolves once both fetches have settled
235
+ */
236
+ export declare const loadPhraseArtifacts: (options?: {
237
+ isLocalLLM?: boolean;
238
+ }) => Promise<void>;
239
+ type VocabularyLoadSource = 'focus' | 'predict';
88
240
  export declare const loadDefaultVocabulary: (options?: {
89
241
  isLocalLLM?: boolean;
242
+ source?: VocabularyLoadSource;
90
243
  surface?: string;
91
244
  }) => Promise<void>;
92
245
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-autocomplete",
3
- "version": "8.0.0",
3
+ "version": "9.1.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",
@@ -27,7 +27,7 @@
27
27
  "wink-nlp": "^2.4.0"
28
28
  },
29
29
  "peerDependencies": {
30
- "@atlaskit/editor-common": "^119.0.0",
30
+ "@atlaskit/editor-common": "^119.9.0",
31
31
  "@atlaskit/editor-plugin-analytics": "^15.0.0",
32
32
  "react": "^18.2.0 || ^19.2.0"
33
33
  },
@@ -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_PHRASES: 'load-phrases',
40
41
  LOAD_GRAMMAR: 'load-grammar',
41
42
  } as const;
42
43
 
@@ -70,6 +71,10 @@ const experiences: Record<AutocompleteExperienceName, ConcurrentExperience> = {
70
71
  EXPERIENCE_NAME.LOAD_VECTORS,
71
72
  operationConfig,
72
73
  ),
74
+ [EXPERIENCE_NAME.LOAD_PHRASES]: new ConcurrentExperience(
75
+ EXPERIENCE_NAME.LOAD_PHRASES,
76
+ operationConfig,
77
+ ),
73
78
  [EXPERIENCE_NAME.LOAD_GRAMMAR]: new ConcurrentExperience(
74
79
  EXPERIENCE_NAME.LOAD_GRAMMAR,
75
80
  operationConfig,
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Validation and diagnostics layer over the artifacts manifest.
3
+ *
4
+ * `artifacts-manifest` owns *where* a payload comes from (one manifest request,
5
+ * then a presigned CDN download). This module owns *whether the payload is
6
+ * usable*: every loader declares the shape it expects, so a mis-published
7
+ * artifact fails at the boundary with the artifact's name rather than surfacing
8
+ * later as an empty trie or a silent no-op in scoring.
9
+ */
10
+
11
+ import { type ArtifactName, fetchArtifactJson, getArtifactUrl } from './artifacts-manifest';
12
+ import { ctcTag } from './debug-mode';
13
+
14
+ const TRAILING_SLASH_REGEX = /\/$/u;
15
+ const DEFAULT_BASE_URL = '';
16
+ const AUTOCOMPLETE_GATEWAY_BASE = '/gateway/api/v1/autocomplete';
17
+
18
+ type ArtifactLoaderOptions<T> = {
19
+ label?: string;
20
+ summarize?: (payload: T) => string;
21
+ validate?: (payload: unknown) => payload is T;
22
+ };
23
+
24
+ /**
25
+ * Build a URL for one of the autocomplete gateway's own endpoints (e.g. the
26
+ * network slow lane). Model artifacts do NOT go through here — they are served
27
+ * from the CDN via the manifest.
28
+ */
29
+ export const buildAutocompleteGatewayUrl = (
30
+ path: string,
31
+ baseUrl: string = DEFAULT_BASE_URL,
32
+ ): string => {
33
+ const normalizedBase = baseUrl.replace(TRAILING_SLASH_REGEX, '');
34
+ return `${normalizedBase}${AUTOCOMPLETE_GATEWAY_BASE}${path}`;
35
+ };
36
+
37
+ /**
38
+ * Download a JSON artifact and assert its shape before handing it to a loader.
39
+ *
40
+ * :params:
41
+ * artifactName: Manifest key for the artifact
42
+ * options.label: Name used in diagnostics; defaults to the manifest key
43
+ * options.summarize: Renders a short size/count summary for the debug log
44
+ * options.validate: Type guard the payload must satisfy
45
+ * :returns:
46
+ * The parsed, validated payload
47
+ */
48
+ export const fetchAutocompleteArtifactJson = async <T>(
49
+ artifactName: ArtifactName,
50
+ options: ArtifactLoaderOptions<T> = {},
51
+ ): Promise<T> => {
52
+ const label = options.label ?? artifactName;
53
+ // Presigned CDN URLs carry an auth token, so only the artifact name is logged.
54
+ ctcTag('init', `↓ loading ${label}`);
55
+
56
+ const payload = await fetchArtifactJson<unknown>(artifactName);
57
+ if (options.validate && !options.validate(payload)) {
58
+ throw new Error(`[autocomplete-artifacts] ${label} payload shape was invalid`);
59
+ }
60
+
61
+ const summary = options.summarize?.(payload as T);
62
+ ctcTag('init', `✓ loaded ${label}${summary ? ` (${summary})` : ''}`);
63
+
64
+ return payload as T;
65
+ };
66
+
67
+ /**
68
+ * Download a binary artifact (the word-vectors blob) as an ArrayBuffer.
69
+ *
70
+ * :params:
71
+ * artifactName: Manifest key for the artifact
72
+ * label: Name used in diagnostics; defaults to the manifest key
73
+ * :returns:
74
+ * The raw bytes of the artifact
75
+ */
76
+ export const fetchAutocompleteArtifactBinary = async (
77
+ artifactName: ArtifactName,
78
+ label: string = artifactName,
79
+ ): Promise<ArrayBuffer> => {
80
+ ctcTag('init', `↓ loading ${label}`);
81
+
82
+ const url = await getArtifactUrl(artifactName);
83
+ const response = await fetch(url);
84
+ if (!response.ok) {
85
+ throw new Error(`[autocomplete-artifacts] Failed to fetch ${label}: ${response.status}`);
86
+ }
87
+
88
+ const payload = await response.arrayBuffer();
89
+ ctcTag('init', `✓ loaded ${label} (${payload.byteLength} bytes)`);
90
+
91
+ return payload;
92
+ };
@@ -20,10 +20,13 @@ import { isAutocompleteDebugEnabled } from './debug-mode';
20
20
  * hyphenated, unlike the underscored filenames the artifacts are generated from.
21
21
  */
22
22
  export const ARTIFACT_NAME = {
23
+ BIGRAMS: 'bigrams.json',
23
24
  FIRST_TOKEN_TO_WORDS: 'first-token-to-words.json',
24
25
  GHOST_POS_TAGS: 'ghost-pos-tags.json',
25
26
  GRAMMAR_TRANSITIONS: 'grammar-transitions-10k.json',
26
27
  L3_VOCABULARY: 'l3-vocabulary.json',
28
+ PHRASE_CONTINUATION_TOKENS: 'phrase-continuation-tokens.json',
29
+ PHRASES: 'phrases.json',
27
30
  POS_TAGS: 'combined-l2-l3-pos-tags.json',
28
31
  VOCABULARY: 'vocabulary-10k.json',
29
32
  WORD_INDEX: 'word-index-10k.json',