@atlaskit/editor-plugin-autocomplete 0.1.0 → 0.2.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 (52) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/afm-cc/tsconfig.json +5 -1
  3. package/afm-jira/tsconfig.json +5 -1
  4. package/afm-products/tsconfig.json +5 -1
  5. package/build/tsconfig.json +24 -0
  6. package/dist/cjs/autocompletePlugin.js +18 -5
  7. package/dist/cjs/autocompletePluginType.js +5 -1
  8. package/dist/cjs/pm-plugins/autocomplete-plugin.js +361 -0
  9. package/dist/cjs/pm-plugins/ghost-text-decoration.js +39 -0
  10. package/dist/cjs/pm-plugins/scoring-pipeline.js +258 -0
  11. package/dist/cjs/pm-plugins/slow-lane-client.js +197 -0
  12. package/dist/cjs/pm-plugins/text-predictor.js +786 -0
  13. package/dist/es2019/autocompletePlugin.js +20 -6
  14. package/dist/es2019/autocompletePluginType.js +1 -0
  15. package/dist/es2019/pm-plugins/autocomplete-plugin.js +360 -0
  16. package/dist/es2019/pm-plugins/ghost-text-decoration.js +33 -0
  17. package/dist/es2019/pm-plugins/scoring-pipeline.js +224 -0
  18. package/dist/es2019/pm-plugins/slow-lane-client.js +154 -0
  19. package/dist/es2019/pm-plugins/text-predictor.js +624 -0
  20. package/dist/esm/autocompletePlugin.js +18 -5
  21. package/dist/esm/autocompletePluginType.js +1 -0
  22. package/dist/esm/pm-plugins/autocomplete-plugin.js +354 -0
  23. package/dist/esm/pm-plugins/ghost-text-decoration.js +33 -0
  24. package/dist/esm/pm-plugins/scoring-pipeline.js +255 -0
  25. package/dist/esm/pm-plugins/slow-lane-client.js +190 -0
  26. package/dist/esm/pm-plugins/text-predictor.js +783 -0
  27. package/dist/types/autocompletePluginType.d.ts +6 -3
  28. package/dist/types/pm-plugins/autocomplete-plugin.d.ts +36 -0
  29. package/dist/types/pm-plugins/ghost-text-decoration.d.ts +7 -0
  30. package/dist/types/pm-plugins/scoring-pipeline.d.ts +33 -0
  31. package/dist/types/pm-plugins/slow-lane-client.d.ts +46 -0
  32. package/dist/types/pm-plugins/text-predictor.d.ts +90 -0
  33. package/dist/types-ts4.5/autocompletePluginType.d.ts +6 -3
  34. package/dist/types-ts4.5/pm-plugins/autocomplete-plugin.d.ts +36 -0
  35. package/dist/types-ts4.5/pm-plugins/ghost-text-decoration.d.ts +7 -0
  36. package/dist/types-ts4.5/pm-plugins/scoring-pipeline.d.ts +33 -0
  37. package/dist/types-ts4.5/pm-plugins/slow-lane-client.d.ts +46 -0
  38. package/dist/types-ts4.5/pm-plugins/text-predictor.d.ts +90 -0
  39. package/package.json +2 -2
  40. package/src/autocompletePlugin.tsx +25 -5
  41. package/src/autocompletePluginType.ts +14 -3
  42. package/src/pm-plugins/autocomplete-plugin/package.json +15 -0
  43. package/src/pm-plugins/autocomplete-plugin.ts +429 -0
  44. package/src/pm-plugins/ghost-text-decoration.ts +44 -0
  45. package/src/pm-plugins/scoring-pipeline.ts +297 -0
  46. package/src/pm-plugins/slow-lane-client/package.json +15 -0
  47. package/src/pm-plugins/slow-lane-client.ts +220 -0
  48. package/src/pm-plugins/text-predictor/package.json +15 -0
  49. package/src/pm-plugins/text-predictor.ts +771 -0
  50. package/src/pm-plugins/typings.d.ts +12 -0
  51. package/tsconfig.app.json +10 -2
  52. package/tsconfig.json +3 -1
@@ -0,0 +1,354 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
+ import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
5
+ import { keydownHandler } from '@atlaskit/editor-prosemirror/keymap';
6
+ import { PluginKey } from '@atlaskit/editor-prosemirror/state';
7
+ import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
8
+ import { createGhostTextDecorationSet } from './ghost-text-decoration';
9
+ import { createSlowLaneClient, setDefaultSlowLaneClient, isWordBoundary } from './slow-lane-client';
10
+ import { predict, loadDefaultVocabulary, loadVectorsAsync, incrementSessionFreq, ingestDocumentPage } from './text-predictor';
11
+ var SLOW_LANE_ENDPOINT = '/gateway/api/v1/autocomplete/typeahead-encodings';
12
+ export var autocompletePluginKey = new PluginKey('autocomplete');
13
+ var DEBOUNCE_MS = 150;
14
+ var createInitialState = function createInitialState() {
15
+ return {
16
+ ghostText: '',
17
+ ghostPosition: -1,
18
+ decorationSet: DecorationSet.empty
19
+ };
20
+ };
21
+
22
+ /**
23
+ * Extract text content before the cursor from the current document.
24
+ * Returns the last ~200 characters for context.
25
+ */
26
+ var getTextBeforeCursor = function getTextBeforeCursor(state) {
27
+ var $from = state.selection.$from;
28
+ var maxChars = 200;
29
+
30
+ // 1. Get the perfectly flattened text of the current block up to the cursor
31
+ var blockNode = $from.parent;
32
+ var offsetInBlock = $from.parentOffset;
33
+ var blockText = blockNode.textContent.slice(0, offsetInBlock);
34
+ if (blockText.length >= maxChars) {
35
+ return blockText.slice(-maxChars);
36
+ }
37
+ var fullText = blockText;
38
+
39
+ // 2. Walk backwards through previous blocks
40
+ var depth = $from.depth - 1;
41
+ while (fullText.length < maxChars && depth >= 0) {
42
+ var parentNode = $from.node(depth);
43
+ var indexInParent = $from.index(depth);
44
+ for (var i = indexInParent - 1; i >= 0 && fullText.length < maxChars; i--) {
45
+ var sibling = parentNode.child(i);
46
+ var siblingText = sibling.textContent;
47
+ fullText = siblingText + '\n' + fullText;
48
+ }
49
+ depth--;
50
+ }
51
+ return fullText.slice(-maxChars);
52
+ };
53
+
54
+ /**
55
+ * Set the autocomplete state via a transaction metadata.
56
+ */
57
+ var setAutocompleteMeta = function setAutocompleteMeta(tr, meta) {
58
+ return tr.setMeta(autocompletePluginKey, meta);
59
+ };
60
+
61
+ /**
62
+ * Apply a ghost text suggestion to the editor state.
63
+ */
64
+ var showGhostText = function showGhostText(view, text, position) {
65
+ var state = view.state,
66
+ dispatch = view.dispatch;
67
+ var decorationSet = createGhostTextDecorationSet(state, position, text);
68
+ var tr = setAutocompleteMeta(state.tr, {
69
+ ghostText: text,
70
+ ghostPosition: position,
71
+ decorationSet: decorationSet
72
+ });
73
+ dispatch(tr);
74
+ };
75
+
76
+ /**
77
+ * Clear the current ghost text from the editor.
78
+ */
79
+ var clearGhostText = function clearGhostText(state, dispatch) {
80
+ var pluginState = autocompletePluginKey.getState(state);
81
+ if (!pluginState || !pluginState.ghostText) {
82
+ return false;
83
+ }
84
+ if (dispatch) {
85
+ var tr = setAutocompleteMeta(state.tr, {
86
+ ghostText: '',
87
+ ghostPosition: -1,
88
+ decorationSet: DecorationSet.empty
89
+ });
90
+ dispatch(tr);
91
+ }
92
+ return true;
93
+ };
94
+
95
+ /**
96
+ * Accept the current ghost text suggestion and insert it into the document.
97
+ */
98
+ var acceptGhostText = function acceptGhostText(state, dispatch) {
99
+ var pluginState = autocompletePluginKey.getState(state);
100
+ if (!pluginState || !pluginState.ghostText) {
101
+ return false;
102
+ }
103
+ if (dispatch) {
104
+ var ghostText = pluginState.ghostText,
105
+ ghostPosition = pluginState.ghostPosition;
106
+ var tr = state.tr.insertText(ghostText, ghostPosition);
107
+ tr = setAutocompleteMeta(tr, {
108
+ ghostText: '',
109
+ ghostPosition: -1,
110
+ decorationSet: DecorationSet.empty
111
+ });
112
+ dispatch(tr);
113
+ }
114
+ return true;
115
+ };
116
+
117
+ /**
118
+ * Context provided to the autocomplete plugin on first editor focus.
119
+ * Text fields are selectively ingested to boost word-frequency scoring for
120
+ * predictions, giving words already present in the document/thread an L1
121
+ * priority boost.
122
+ */
123
+
124
+ /**
125
+ * Build the text payload for the slow-lane request by prepending any available
126
+ * comment context ahead of the live document text. This gives the backend
127
+ * model richer context about the thread the user is writing in.
128
+ */
129
+ var buildSlowLaneText = function buildSlowLaneText(docText, context) {
130
+ var _context$siblingComme, _context$siblingComme2, _context$siblingComme3;
131
+ var lines = [];
132
+ if (context !== null && context !== void 0 && context.parentCommentContent) {
133
+ lines.push("comment: ".concat(context.parentCommentContent));
134
+ }
135
+ context === null || context === void 0 || (_context$siblingComme = context.siblingCommentsContents) === null || _context$siblingComme === void 0 || _context$siblingComme.forEach(function (sibling, index) {
136
+ lines.push("reply ".concat(index + 1, ": ").concat(sibling));
137
+ });
138
+ var nextReplyNumber = ((_context$siblingComme2 = context === null || context === void 0 || (_context$siblingComme3 = context.siblingCommentsContents) === null || _context$siblingComme3 === void 0 ? void 0 : _context$siblingComme3.length) !== null && _context$siblingComme2 !== void 0 ? _context$siblingComme2 : 0) + 1;
139
+ lines.push("reply ".concat(nextReplyNumber, ": ").concat(docText));
140
+ return lines.join('\n');
141
+ };
142
+ export var createAutocompletePlugin = function createAutocompletePlugin(options) {
143
+ var debounceTimer = null;
144
+ var hasIngestedPage = false;
145
+ var resolvedContext;
146
+ /**
147
+ * Set after accepting a suggestion so the next doc-change update
148
+ * skips scheduling a new prediction for the just-inserted text.
149
+ * Scoped to the factory so multiple editor instances don't share state.
150
+ */
151
+ var justAccepted = false;
152
+
153
+ /**
154
+ * Stores the text-before-cursor snapshot at the moment the user dismissed
155
+ * a suggestion via Escape. While the context remains identical, we suppress
156
+ * re-showing the same suggestion. Resets to null as soon as the text changes.
157
+ */
158
+ var dismissedContext = null;
159
+ var slowLaneClient = createSlowLaneClient({
160
+ baseUrl: '',
161
+ endpoint: SLOW_LANE_ENDPOINT
162
+ });
163
+ setDefaultSlowLaneClient(slowLaneClient);
164
+
165
+ /**
166
+ * Schedule a prediction after a short debounce.
167
+ * Tier 1 predictions are synchronous (<0.1ms) but we still debounce
168
+ * to avoid unnecessary work on rapid keystrokes.
169
+ */
170
+ var schedulePrediction = function schedulePrediction(view) {
171
+ if (debounceTimer) {
172
+ clearTimeout(debounceTimer);
173
+ }
174
+ debounceTimer = setTimeout(function () {
175
+ var state = view.state;
176
+ var selection = state.selection;
177
+
178
+ // Only predict for cursor selections (not range selections)
179
+ if (!selection.empty) {
180
+ return;
181
+ }
182
+ var textBefore = getTextBeforeCursor(state);
183
+
184
+ // Suppress re-showing the same suggestion the user just dismissed.
185
+ // Once the text context changes (user types or deletes), this clears automatically.
186
+ if (textBefore === dismissedContext) {
187
+ return;
188
+ }
189
+ dismissedContext = null;
190
+
191
+ // Don't predict if there's not enough context
192
+ if (textBefore.trim().length < 3) {
193
+ return;
194
+ }
195
+
196
+ // Tier 1 prediction is synchronous -- no async needed
197
+ var prediction = predict(textBefore);
198
+ if (prediction && prediction.length > 0) {
199
+ showGhostText(view, prediction, selection.from);
200
+ }
201
+ }, DEBOUNCE_MS);
202
+ };
203
+ var maybeUpdateSessionFrequency = function maybeUpdateSessionFrequency(view, prevState) {
204
+ var newText = getTextBeforeCursor(view.state);
205
+ var prevText = getTextBeforeCursor(prevState);
206
+ if (newText.length <= prevText.length) {
207
+ return;
208
+ }
209
+ var lastChar = newText[newText.length - 1];
210
+ if (!/[\t-\r !,\.:;\?\xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]/.test(lastChar)) {
211
+ return;
212
+ }
213
+
214
+ // Only fire if the previous state did not already end on a boundary,
215
+ // so we don't double-count when multiple boundary chars are inserted.
216
+ var prevLastChar = prevText[prevText.length - 1];
217
+ if (prevLastChar && /[\t-\r !,\.:;\?\xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF]/.test(prevLastChar)) {
218
+ return;
219
+ }
220
+ var beforeBoundary = newText.slice(0, -1).trimEnd();
221
+ var lastSpaceIdx = beforeBoundary.lastIndexOf(' ');
222
+ var completedWord = beforeBoundary.slice(lastSpaceIdx + 1).toLowerCase();
223
+ if (completedWord.length >= 2) {
224
+ incrementSessionFreq(completedWord);
225
+ }
226
+ };
227
+ return new SafePlugin({
228
+ key: autocompletePluginKey,
229
+ state: {
230
+ init: function init() {
231
+ return createInitialState();
232
+ },
233
+ apply: function apply(tr, pluginState) {
234
+ var meta = tr.getMeta(autocompletePluginKey);
235
+ if (meta) {
236
+ return _objectSpread(_objectSpread({}, pluginState), meta);
237
+ }
238
+
239
+ // If the document changed, clear the ghost text
240
+ // (new prediction will be scheduled from view.update)
241
+ if (tr.docChanged) {
242
+ return _objectSpread(_objectSpread({}, pluginState), {}, {
243
+ ghostText: '',
244
+ ghostPosition: -1,
245
+ decorationSet: DecorationSet.empty
246
+ });
247
+ }
248
+
249
+ // If selection changed without doc change, clear ghost text
250
+ if (tr.selectionSet && pluginState.ghostText) {
251
+ return _objectSpread(_objectSpread({}, pluginState), {}, {
252
+ ghostText: '',
253
+ ghostPosition: -1,
254
+ decorationSet: DecorationSet.empty
255
+ });
256
+ }
257
+ return pluginState;
258
+ }
259
+ },
260
+ props: {
261
+ decorations: function decorations(state) {
262
+ var _pluginState$decorati;
263
+ var pluginState = autocompletePluginKey.getState(state);
264
+ return (_pluginState$decorati = pluginState === null || pluginState === void 0 ? void 0 : pluginState.decorationSet) !== null && _pluginState$decorati !== void 0 ? _pluginState$decorati : DecorationSet.empty;
265
+ },
266
+ handleKeyDown: keydownHandler({
267
+ Tab: function Tab(state, dispatch) {
268
+ var accepted = acceptGhostText(state, dispatch);
269
+ if (accepted) justAccepted = true;
270
+ return accepted;
271
+ },
272
+ ArrowRight: function ArrowRight(state, dispatch) {
273
+ var accepted = acceptGhostText(state, dispatch);
274
+ if (accepted) justAccepted = true;
275
+ return accepted;
276
+ },
277
+ Escape: function Escape(state, dispatch) {
278
+ var didClear = clearGhostText(state, dispatch);
279
+ if (didClear) {
280
+ dismissedContext = getTextBeforeCursor(state);
281
+ }
282
+ return didClear;
283
+ }
284
+ }),
285
+ handleDOMEvents: {
286
+ blur: function blur(view) {
287
+ var pluginState = autocompletePluginKey.getState(view.state);
288
+ if (pluginState !== null && pluginState !== void 0 && pluginState.ghostText) {
289
+ clearGhostText(view.state, view.dispatch);
290
+ }
291
+ return false;
292
+ },
293
+ focus: function focus() {
294
+ loadDefaultVocabulary();
295
+ loadVectorsAsync().catch(function () {});
296
+ if (!hasIngestedPage) {
297
+ hasIngestedPage = true;
298
+ if (options !== null && options !== void 0 && options.getContext) {
299
+ options.getContext().then(function (context) {
300
+ var _context$siblingComme4;
301
+ if (!context) {
302
+ return;
303
+ }
304
+ resolvedContext = context;
305
+ if (context.fullPageContent) {
306
+ ingestDocumentPage(context.fullPageContent);
307
+ }
308
+ if (context.parentCommentContent) {
309
+ ingestDocumentPage(context.parentCommentContent);
310
+ }
311
+ (_context$siblingComme4 = context.siblingCommentsContents) === null || _context$siblingComme4 === void 0 || _context$siblingComme4.forEach(ingestDocumentPage);
312
+ }).catch(function () {});
313
+ }
314
+ }
315
+ return false;
316
+ }
317
+ }
318
+ },
319
+ view: function view() {
320
+ return {
321
+ update: function update(view, prevState) {
322
+ if (!prevState.doc.eq(view.state.doc)) {
323
+ if (justAccepted) {
324
+ justAccepted = false;
325
+
326
+ // ✨ THE FIX: Memorize the text state right after acceptance.
327
+ // Any follow-up transactions will hit the 'dismissedContext'
328
+ // block and abort until the user actually types a new character!
329
+ dismissedContext = getTextBeforeCursor(view.state);
330
+
331
+ // Also clear any pending debounce timers from before the acceptance
332
+ if (debounceTimer) {
333
+ clearTimeout(debounceTimer);
334
+ }
335
+ return;
336
+ }
337
+ maybeUpdateSessionFrequency(view, prevState);
338
+ var textBefore = getTextBeforeCursor(view.state);
339
+ if (isWordBoundary(textBefore)) {
340
+ slowLaneClient.updateContext(buildSlowLaneText(view.state.doc.textContent, resolvedContext));
341
+ }
342
+ schedulePrediction(view);
343
+ }
344
+ },
345
+ destroy: function destroy() {
346
+ if (debounceTimer) {
347
+ clearTimeout(debounceTimer);
348
+ }
349
+ setDefaultSlowLaneClient(null);
350
+ }
351
+ };
352
+ }
353
+ });
354
+ };
@@ -0,0 +1,33 @@
1
+ import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
2
+ var GHOST_TEXT_CLASS = 'autocomplete-ghost-text';
3
+
4
+ /**
5
+ * Creates a DecorationSet containing a ghost text widget at the given position.
6
+ * The ghost text is rendered as a styled <span> that appears after the cursor.
7
+ */
8
+ export var createGhostTextDecorationSet = function createGhostTextDecorationSet(state, position, text) {
9
+ if (!text) {
10
+ return DecorationSet.empty;
11
+ }
12
+ var decoration = Decoration.widget(position, function () {
13
+ var container = document.createElement('span');
14
+ container.className = GHOST_TEXT_CLASS;
15
+ container.setAttribute('data-autocomplete-ghost', 'true');
16
+ container.style.color = '#999';
17
+ container.style.opacity = '0.6';
18
+ container.style.pointerEvents = 'none';
19
+ container.style.userSelect = 'none';
20
+ container.style.fontStyle = 'italic';
21
+ // U+200B (Zero Width Space) gives the browser a line-break opportunity
22
+ // immediately before the ghost text. This ensures the typed text before
23
+ // the span is never pushed to the next line by the ghost text's width —
24
+ // only the ghost text itself will wrap if it doesn't fit.
25
+ container.textContent = "\u200B" + text;
26
+ return container;
27
+ }, {
28
+ side: 1,
29
+ // Render after content at this position
30
+ key: 'autocomplete-ghost-text'
31
+ });
32
+ return DecorationSet.create(state.doc, [decoration]);
33
+ };
@@ -0,0 +1,255 @@
1
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
2
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
3
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
4
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
5
+ /**
6
+ * Scoring Pipeline: Stage 1 (Semantic + Frequency), Grammar Filter, Stage 2 (LM Re-ranking).
7
+ *
8
+ * Operates synchronously on pre-loaded data. Each stage gracefully degrades
9
+ * when its required data isn't available (cold → warm → full warm).
10
+ */
11
+
12
+ // resolveJsonModule is disabled for this package (see tsconfig.json) to prevent
13
+ // TypeScript from parsing LFS pointer files during CI typecheck. JSON imports
14
+ // are typed via the '*.json' declaration in typings.d.ts.
15
+ import posTagsData from './data/combined_l2_l3_pos_tags.json';
16
+ import ghostPosTagsData from './data/ghost_pos_tags.json';
17
+ import grammarTransitionsData from './data/grammar_transitions_10k.json';
18
+
19
+ // ─── Types ──────────────────────────────────────────────────
20
+
21
+ /** Metadata returned by the grammar filter for debug logging in the caller. */
22
+
23
+ // ─── Scoring Constants ──────────────────────────────────────
24
+
25
+ var ALPHA = 0.5;
26
+ var BETA = 0.5;
27
+ var NEUTRAL_SCORE = 0.5;
28
+ var STAGE1_WEIGHT = 0.6;
29
+ var STAGE2_WEIGHT = 0.4;
30
+ var MIN_STAGE1_SCORE = 0.35;
31
+ var L1_SESSION_CAP = 1.2;
32
+
33
+ // ─── Grammar Data (loaded once on import) ───────────────────
34
+
35
+ var posTags = new Map(Object.entries(posTagsData));
36
+ var grammarTransitions = grammarTransitionsData;
37
+
38
+ /**
39
+ * Precomputed map from each POS tag to the set of allowed next POS tags.
40
+ * Built once at module load from grammarTransitions so applyGrammarFilter
41
+ * never re-iterates the transition rules per call.
42
+ */
43
+ var precomputedAllowedByPos = new Map(Object.entries(grammarTransitions.transitions).map(function (_ref) {
44
+ var _ref2 = _slicedToArray(_ref, 2),
45
+ pos = _ref2[0],
46
+ rule = _ref2[1];
47
+ return [pos, new Set(rule.allowed)];
48
+ }));
49
+
50
+ // ─── Math ───────────────────────────────────────────────────
51
+
52
+ function cosineSimilarity(a, b) {
53
+ var dot = 0;
54
+ var normA = 0;
55
+ var normB = 0;
56
+ for (var i = 0; i < a.length; i++) {
57
+ dot += a[i] * b[i];
58
+ normA += a[i] * a[i];
59
+ normB += b[i] * b[i];
60
+ }
61
+ var dNormA = Math.sqrt(normA);
62
+ var dNormB = Math.sqrt(normB);
63
+ if (dNormA === 0 || dNormB === 0) {
64
+ return NEUTRAL_SCORE;
65
+ }
66
+ return (1 + dot / (dNormA * dNormB)) / 2;
67
+ }
68
+
69
+ // ─── Stage 1: Semantic + Frequency ──────────────────────────
70
+
71
+ function scoreStage1(candidate, contextVector, getWordVector, maxTenantFreq) {
72
+ // 1. Calculate Base Global Score (Normalized Log)
73
+ var maxPossibleLog = Math.log10(maxTenantFreq + 1);
74
+
75
+ // Diversity Adjustment
76
+ var diversityRaw = (Math.log10(candidate.tenantFreq + 1) * 0.50 + Math.log10(candidate.docFreq + 1) * 0.25 + Math.log10(candidate.authorFreq + 1) * 0.25) / maxPossibleLog;
77
+ var sessionMultiplier = candidate.sessionFreq > 0 ? 1 + Math.log10(candidate.sessionFreq + 1) * 2.5 : 1;
78
+
79
+ // Apply multiplier; capped at L1_SESSION_CAP (default 1.2) to prevent excessive over-indexing
80
+ var freqScore = Math.min(diversityRaw * sessionMultiplier, L1_SESSION_CAP);
81
+
82
+ // 3. Semantic Scoring
83
+ var semanticScore = NEUTRAL_SCORE;
84
+ if (contextVector) {
85
+ var wordVec = getWordVector(candidate.word);
86
+ semanticScore = wordVec ? cosineSimilarity(contextVector, wordVec) : NEUTRAL_SCORE;
87
+ }
88
+ return {
89
+ semanticScore: semanticScore,
90
+ freqScore: freqScore,
91
+ stage1Score: ALPHA * semanticScore + BETA * freqScore
92
+ };
93
+ }
94
+
95
+ // ─── Grammar Filter ─────────────────────────────────────────
96
+
97
+ /**
98
+ * GHOST POS DICTIONARY
99
+ * A hardcoded mapping of common structural English words that were stripped
100
+ * from the main domain vocabulary. This allows the grammar filter to understand
101
+ * context without suggesting these words to the user.
102
+ */
103
+ var ghostPosTags = ghostPosTagsData;
104
+ function applyGrammarFilter(candidates, previousWord) {
105
+ if (!previousWord) return {
106
+ filtered: candidates,
107
+ grammarMeta: null
108
+ };
109
+ var lowerPrev = previousWord.toLowerCase();
110
+ var prevTags = ghostPosTags[lowerPrev] || posTags.get(lowerPrev);
111
+ if (!prevTags || prevTags.length === 0) {
112
+ return {
113
+ filtered: candidates,
114
+ grammarMeta: null
115
+ };
116
+ }
117
+ var allowedNextTags;
118
+ if (prevTags.length === 1) {
119
+ var _precomputedAllowedBy;
120
+ // Common case: single POS tag — reuse the precomputed Set directly (no allocation)
121
+ allowedNextTags = (_precomputedAllowedBy = precomputedAllowedByPos.get(prevTags[0])) !== null && _precomputedAllowedBy !== void 0 ? _precomputedAllowedBy : new Set();
122
+ } else {
123
+ allowedNextTags = new Set();
124
+ var _iterator = _createForOfIteratorHelper(prevTags),
125
+ _step;
126
+ try {
127
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
128
+ var pt = _step.value;
129
+ var allowed = precomputedAllowedByPos.get(pt);
130
+ if (allowed) allowed.forEach(function (tag) {
131
+ return allowedNextTags.add(tag);
132
+ });
133
+ }
134
+ } catch (err) {
135
+ _iterator.e(err);
136
+ } finally {
137
+ _iterator.f();
138
+ }
139
+ }
140
+ var filtered = [];
141
+ var dropped = [];
142
+ var _iterator2 = _createForOfIteratorHelper(candidates),
143
+ _step2;
144
+ try {
145
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
146
+ var entry = _step2.value;
147
+ var candidateTags = posTags.get(entry.candidate.word.toLowerCase());
148
+
149
+ // If candidate has no tags (unknown word), let it pass to be safe
150
+ if (!candidateTags || candidateTags.length === 0) {
151
+ filtered.push(entry);
152
+ continue;
153
+ }
154
+ if (candidateTags.some(function (ct) {
155
+ return allowedNextTags.has(ct);
156
+ })) {
157
+ filtered.push(entry);
158
+ } else {
159
+ dropped.push(entry.candidate.word);
160
+ }
161
+ }
162
+ } catch (err) {
163
+ _iterator2.e(err);
164
+ } finally {
165
+ _iterator2.f();
166
+ }
167
+ var finalFiltered = filtered.length > 0 ? filtered : candidates;
168
+ return {
169
+ filtered: finalFiltered,
170
+ grammarMeta: {
171
+ prevWord: lowerPrev,
172
+ prevTags: prevTags,
173
+ before: candidates.length,
174
+ after: finalFiltered.length,
175
+ dropped: filtered.length > 0 ? dropped : []
176
+ }
177
+ };
178
+ }
179
+
180
+ // ─── Stage 2: LM Re-ranking ────────────────────────────────
181
+ function getLmScore(word, lmLogits) {
182
+ if (!lmLogits) return 0;
183
+
184
+ // Look up the word directly! No more tokens.
185
+ var val = lmLogits[word.toLowerCase()];
186
+ if (typeof val === 'number') {
187
+ return val;
188
+ }
189
+ return 0;
190
+ }
191
+
192
+ // ─── Public API ─────────────────────────────────────────────
193
+
194
+ export function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxTenantFreq, previousWord) {
195
+ // Stage 1
196
+ var stage1Results = candidates.map(function (candidate) {
197
+ var _scoreStage = scoreStage1(candidate, contextVector, getWordVector, maxTenantFreq),
198
+ semanticScore = _scoreStage.semanticScore,
199
+ freqScore = _scoreStage.freqScore,
200
+ stage1Score = _scoreStage.stage1Score;
201
+ return {
202
+ candidate: candidate,
203
+ semanticScore: semanticScore,
204
+ freqScore: freqScore,
205
+ stage1Score: stage1Score
206
+ };
207
+ });
208
+ var stage1Survivors = stage1Results.filter(function (entry) {
209
+ return entry.stage1Score >= MIN_STAGE1_SCORE;
210
+ });
211
+
212
+ // Grammar Filter
213
+ var _applyGrammarFilter = applyGrammarFilter(stage1Survivors, previousWord),
214
+ filtered = _applyGrammarFilter.filtered,
215
+ grammarMeta = _applyGrammarFilter.grammarMeta;
216
+
217
+ // Stage 2 + final assembly
218
+ var lmMax = 0;
219
+ if (lmLogits && Object.keys(lmLogits).length > 0) {
220
+ var values = Object.values(lmLogits);
221
+ lmMax = Math.max.apply(Math, values);
222
+ }
223
+ var scored = filtered.map(function (entry) {
224
+ var lmScore = 0;
225
+ var finalScore = entry.stage1Score;
226
+ if (lmLogits && Object.keys(lmLogits).length > 0) {
227
+ var rawLm = getLmScore(entry.candidate.word, lmLogits);
228
+ if (rawLm !== 0) {
229
+ // The word was in the top_k! Score it normally.
230
+ var logitDiff = Math.log(rawLm) - Math.log(lmMax);
231
+ lmScore = Math.exp(logitDiff);
232
+ } else {
233
+ lmScore = 0.05;
234
+ }
235
+ finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
236
+ }
237
+ return {
238
+ word: entry.candidate.word,
239
+ freqScore: entry.freqScore,
240
+ semanticScore: entry.semanticScore,
241
+ lmScore: lmScore,
242
+ finalScore: finalScore
243
+ };
244
+ });
245
+ scored.sort(function (a, b) {
246
+ if (b.finalScore !== a.finalScore) {
247
+ return b.finalScore - a.finalScore;
248
+ }
249
+ return a.word.length - b.word.length;
250
+ });
251
+ return {
252
+ candidates: scored,
253
+ grammarMeta: grammarMeta
254
+ };
255
+ }