@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
@@ -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_PHRASES: 'load-phrases';
33
34
  readonly LOAD_GRAMMAR: 'load-grammar';
34
35
  };
35
36
  export type AutocompleteExperienceName = (typeof EXPERIENCE_NAME)[keyof typeof EXPERIENCE_NAME];
@@ -0,0 +1,44 @@
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
+ import { type ArtifactName } from './artifacts-manifest';
11
+ type ArtifactLoaderOptions<T> = {
12
+ label?: string;
13
+ summarize?: (payload: T) => string;
14
+ validate?: (payload: unknown) => payload is T;
15
+ };
16
+ /**
17
+ * Build a URL for one of the autocomplete gateway's own endpoints (e.g. the
18
+ * network slow lane). Model artifacts do NOT go through here — they are served
19
+ * from the CDN via the manifest.
20
+ */
21
+ export declare const buildAutocompleteGatewayUrl: (path: string, baseUrl?: string) => string;
22
+ /**
23
+ * Download a JSON artifact and assert its shape before handing it to a loader.
24
+ *
25
+ * :params:
26
+ * artifactName: Manifest key for the artifact
27
+ * options.label: Name used in diagnostics; defaults to the manifest key
28
+ * options.summarize: Renders a short size/count summary for the debug log
29
+ * options.validate: Type guard the payload must satisfy
30
+ * :returns:
31
+ * The parsed, validated payload
32
+ */
33
+ export declare const fetchAutocompleteArtifactJson: <T>(artifactName: ArtifactName, options?: ArtifactLoaderOptions<T>) => Promise<T>;
34
+ /**
35
+ * Download a binary artifact (the word-vectors blob) as an ArrayBuffer.
36
+ *
37
+ * :params:
38
+ * artifactName: Manifest key for the artifact
39
+ * label: Name used in diagnostics; defaults to the manifest key
40
+ * :returns:
41
+ * The raw bytes of the artifact
42
+ */
43
+ export declare const fetchAutocompleteArtifactBinary: (artifactName: ArtifactName, label?: string) => Promise<ArrayBuffer>;
44
+ export {};
@@ -15,10 +15,13 @@
15
15
  * hyphenated, unlike the underscored filenames the artifacts are generated from.
16
16
  */
17
17
  export declare const ARTIFACT_NAME: {
18
+ readonly BIGRAMS: 'bigrams.json';
18
19
  readonly FIRST_TOKEN_TO_WORDS: 'first-token-to-words.json';
19
20
  readonly GHOST_POS_TAGS: 'ghost-pos-tags.json';
20
21
  readonly GRAMMAR_TRANSITIONS: 'grammar-transitions-10k.json';
21
22
  readonly L3_VOCABULARY: 'l3-vocabulary.json';
23
+ readonly PHRASE_CONTINUATION_TOKENS: 'phrase-continuation-tokens.json';
24
+ readonly PHRASES: 'phrases.json';
22
25
  readonly POS_TAGS: 'combined-l2-l3-pos-tags.json';
23
26
  readonly VOCABULARY: 'vocabulary-10k.json';
24
27
  readonly WORD_INDEX: 'word-index-10k.json';
@@ -3,7 +3,44 @@ import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
3
3
  import { PluginKey } from '@atlaskit/editor-prosemirror/state';
4
4
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
5
5
  import type { AutocompletePlugin } from '../autocompletePluginType';
6
+ import { type PredictionResult } from './text-predictor';
6
7
  export declare const autocompletePluginKey: PluginKey;
8
+ export interface CommittedSuggestion {
9
+ /** Wall-clock latency from the triggering editor update to the committed ghost. */
10
+ decisionLatencyMs: number;
11
+ /** Evidence tier that authorized this suggestion for display. */
12
+ evidenceTier: PredictionResult['evidenceTier'];
13
+ /** Ghost tail actually rendered and inserted. */
14
+ ghostText: string;
15
+ /**
16
+ * Mean per-token log-probability of the verified prefix, carried so an
17
+ * acceptance can be attributed to the evidence that authorized the display
18
+ * rather than to whatever the scorer holds by the time Tab arrives.
19
+ */
20
+ meanTokenLogProbability: number;
21
+ /**
22
+ * Whether a longer candidate in the same pool extended this surface, which
23
+ * hands it that pool's mass undivided under the chain rule and puts it near
24
+ * the top of the posterior range for a structural reason.
25
+ */
26
+ poolHeldExtension: boolean;
27
+ /** ProseMirror position at which the ghost is anchored. */
28
+ position: number;
29
+ /** Share of its shortlist's mass the model put on this surface. */
30
+ posterior: number;
31
+ /** Final confidence-v2 ranking score. */
32
+ rankScore: number;
33
+ /** Monotonic editor decision revision that owns this suggestion. */
34
+ revision: number;
35
+ /** How many scored candidates this surface's normaliser divided between. */
36
+ shortlistSize: number;
37
+ /** Full candidate surface, used by analytics and cooldown. */
38
+ surface: string;
39
+ /** Whether the surface is a single word, a bigram, or a phrase. */
40
+ termType: PredictionResult['termType'];
41
+ /** Difference between the selected rank score and its closest eligible rival. */
42
+ winnerMargin: number;
43
+ }
7
44
  export interface AutocompletePluginState {
8
45
  /** The decoration set containing the ghost text widget */
9
46
  decorationSet: DecorationSet;
@@ -11,6 +48,8 @@ export interface AutocompletePluginState {
11
48
  ghostPosition: number;
12
49
  /** The predicted ghost text to display */
13
50
  ghostText: string;
51
+ /** Single authoritative snapshot for rendering, insertion, analytics, and cooldown. */
52
+ suggestion: CommittedSuggestion | null;
14
53
  }
15
54
  /**
16
55
  * Context provided to the autocomplete plugin on first editor focus.
@@ -0,0 +1,158 @@
1
+ import type { LogitProcessor } from '@mlc-ai/web-llm';
2
+ export type SeparatorKind = 'whitespace' | 'document-start' | 'non-space';
3
+ export type CanonicalLmEvidenceSource = 'canonical-first-token' | 'canonical-full-surface' | 'network-logit';
4
+ export interface BoundaryLmState {
5
+ contextKey: string;
6
+ prompt: string;
7
+ rawLogits: Float32Array;
8
+ }
9
+ export interface SurfaceScore {
10
+ contextKey: string;
11
+ meanTokenLogProbability: number;
12
+ surface: string;
13
+ tokenCount: number;
14
+ totalLogProbability: number;
15
+ }
16
+ export interface CanonicalCandidateContext {
17
+ canonicalTokenIds: number[] | null;
18
+ contextBeforeSurface: string;
19
+ contextKey: string;
20
+ separatorKind: SeparatorKind;
21
+ surfaceStart: number;
22
+ }
23
+ export interface CanonicalBoundaryContext {
24
+ contextKey: string;
25
+ prompt: string;
26
+ }
27
+ export interface CanonicalLmEvidence {
28
+ contextKey: string;
29
+ logProbability?: number;
30
+ meanTokenLogProbability?: number;
31
+ rawLogit?: number;
32
+ score: number;
33
+ separatorKind: 'whitespace';
34
+ source: CanonicalLmEvidenceSource;
35
+ totalLogProbability?: number;
36
+ totalSurfaceCharCount: number;
37
+ totalTokenCount: number;
38
+ verifiedCharCount: number;
39
+ verifiedTokenCount: number;
40
+ }
41
+ export interface BoundaryPrimeRequest {
42
+ contextKey: string;
43
+ familyKey: string;
44
+ priority: number;
45
+ prompt: string;
46
+ }
47
+ export interface SurfaceScoreRequest {
48
+ candidates: Array<{
49
+ rankHint?: number;
50
+ surface: string;
51
+ tokenIds: number[];
52
+ }>;
53
+ contextKey: string;
54
+ familyKey: string;
55
+ prompt: string;
56
+ }
57
+ export interface ProgressiveSurfaceEvidence {
58
+ meanTokenLogProbabilityUpperBound: number;
59
+ scoredTokenCount: number;
60
+ totalLogProbability: number;
61
+ totalTokenCount: number;
62
+ }
63
+ export interface TokenPrefixExpansion {
64
+ contextKey: string;
65
+ rawNextTokenLogits: Float32Array;
66
+ tokenPrefix: number[];
67
+ totalLogProbability: number;
68
+ }
69
+ export interface TokenPrefixGroup {
70
+ surfaces: string[];
71
+ tokenPrefix: number[];
72
+ }
73
+ /**
74
+ * Token trie for the producer's canonical leading-space token sequences.
75
+ * It lets the scorer expand a shared token prefix once for every surface below
76
+ * that node (`root cause`, `root directory`, ...), rather than guessing two
77
+ * arbitrary full surfaces from a broad one-character string prefix.
78
+ */
79
+ export declare class CanonicalSurfaceTokenTrie {
80
+ private readonly root;
81
+ private readonly tokenIdsBySurface;
82
+ constructor(entries?: Iterable<readonly [string, number[]]>);
83
+ insert(surface: string, tokenIds: number[]): void;
84
+ getTokenIds(surface: string): number[] | null;
85
+ groupByScoredPrefix(candidates: Array<{
86
+ scoredTokenCount: number;
87
+ surface: string;
88
+ tokenIds: number[];
89
+ }>): TokenPrefixGroup[];
90
+ }
91
+ /** The part of a candidate's context that depends only on where its surface starts. */
92
+ interface SurfacePositionContext {
93
+ contextBeforeSurface: string;
94
+ contextKey: string;
95
+ separatorKind: SeparatorKind;
96
+ surfaceStart: number;
97
+ }
98
+ /**
99
+ * Per-pass store for the position-derived half of a candidate's context.
100
+ *
101
+ * Every unigram candidate for a keystroke shares one surface start, and the
102
+ * phrase candidates share a handful more, so without this the same slice,
103
+ * `trimEnd` and context key are rebuilt for each of the couple of hundred
104
+ * candidates. Valid only for a single `textBeforeCursor`.
105
+ */
106
+ export type CanonicalContextPositionCache = Map<number, SurfacePositionContext>;
107
+ export declare const createCanonicalContextPositionCache: () => CanonicalContextPositionCache;
108
+ /**
109
+ * Derive the causal prompt and artifact eligibility from the exact, untrimmed
110
+ * pre-cursor text. The shipped artifact contains only tokenizations of
111
+ * `" " + surface`, so it is valid only when the surface is actually preceded
112
+ * by whitespace.
113
+ */
114
+ export declare const deriveCanonicalCandidateContext: (textBeforeCursor: string, matchedPrefixLen: number, surface: string, getTokenIds: (surface: string) => number[] | null, surfaceStartOverride?: number, positionCache?: CanonicalContextPositionCache) => CanonicalCandidateContext;
115
+ /**
116
+ * Derive the next surface's leading-space context at an actual word boundary.
117
+ * This lets the local LM start its one-token prime on the space keystroke,
118
+ * before the user has typed the first character of the next word.
119
+ */
120
+ export declare const deriveWhitespaceBoundaryContext: (textBeforeCursor: string) => CanonicalBoundaryContext | null;
121
+ /**
122
+ * Return one prime per exact context, ordered from the latest surface start
123
+ * (normally the unigram window) to wider phrase windows.
124
+ */
125
+ export declare const selectBoundaryPrimeRequests: (familyKey: string, candidates: Array<CanonicalCandidateContext>, maxPrimes: number) => BoundaryPrimeRequest[];
126
+ /**
127
+ * `log(sum(exp(values)))`, shifted by the maximum so the sum cannot overflow or
128
+ * underflow to zero for the sequence log-likelihoods this is called with.
129
+ *
130
+ * Returns `-Infinity` for an empty or wholly non-finite input, which makes
131
+ * `exp(value - logSumExp(values))` evaluate to zero rather than `NaN`.
132
+ */
133
+ export declare const logSumExp: (values: readonly number[]) => number;
134
+ export declare const logSoftmaxAt: (logits: Float32Array, target: number) => number;
135
+ /**
136
+ * One processor is registered for the causal model so the scheduler can read
137
+ * the next-token distribution off any forward pass.
138
+ *
139
+ * The scheduler drives decoding token by token — it prefills a prompt, then
140
+ * feeds each continuation token itself — so the processor never has to force
141
+ * the model onto a target path. It only has to hand back the distribution the
142
+ * forward pass produced, which it does without altering the logits.
143
+ */
144
+ export declare class CanonicalLogitProcessor implements LogitProcessor {
145
+ private captured;
146
+ processLogits: (logits: Float32Array) => Float32Array;
147
+ processSampledToken: () => void;
148
+ resetState: () => void;
149
+ startCapture: () => void;
150
+ /**
151
+ * The captured distribution, or null when the forward pass never ran.
152
+ *
153
+ * `processLogits` already copied it, and every capture allocates afresh, so
154
+ * the buffer handed out here is never written to again.
155
+ */
156
+ getCapturedLogits: () => Float32Array | null;
157
+ }
158
+ export {};
@@ -18,10 +18,64 @@
18
18
  declare global {
19
19
  interface Window {
20
20
  __atlCtcDebug__?: {
21
- enable: () => void;
22
21
  disable: () => void;
22
+ /**
23
+ * Enable CTC debug logging for this session. Pass `'verbose'` to also
24
+ * emit the per-prediction candidate table, canonical LM, and grammar detail:
25
+ * `__atlCtcDebug__.enable('verbose')`.
26
+ */
27
+ enable: (level?: 'verbose') => void;
23
28
  isEnabled: () => boolean;
29
+ /** Whether verbose logging (candidate tables + extra detail) is on. */
30
+ isVerbose: () => boolean;
31
+ /**
32
+ * Snapshot of the L1 session boosts — the vocabulary words this session
33
+ * has seen and how often — or one family of them:
34
+ * `__atlCtcDebug__.session('poll')`.
35
+ *
36
+ * Installed by the predictor rather than declared with the rest of the
37
+ * API, and typed as `unknown` so the return shape can live with the module
38
+ * that owns it instead of creating a cycle back to this one.
39
+ */
40
+ session?: (prefix?: string) => unknown;
24
41
  };
25
42
  }
26
43
  }
44
+ /**
45
+ * Shared `%c` styles for all CTC console output, so every log — lifecycle,
46
+ * async signals, and per-prediction groups — reads consistently.
47
+ */
48
+ export declare const CTC_STYLES: {
49
+ brand: string;
50
+ section: string;
51
+ dim: string;
52
+ good: string;
53
+ warn: string;
54
+ bad: string;
55
+ cold: string;
56
+ lm: string;
57
+ body: string;
58
+ };
59
+ /**
60
+ * Log one aligned section line inside a per-prediction group, e.g.
61
+ * `INPUT raw: "…"`. Label is padded so the bodies line up. No-ops unless
62
+ * debug is enabled, so callers don't need to guard.
63
+ */
64
+ export declare const ctcSection: (label: string, body: string) => void;
65
+ /**
66
+ * Log a lifecycle / async-signal line (outside the per-prediction groups),
67
+ * tagged `[CTC:<tag>]`. No-ops unless debug is enabled, so callers don't need to
68
+ * guard. Use tags like `init` (loads) and `signal` (slow-lane arrivals).
69
+ */
70
+ export declare const ctcTag: (tag: string, body: string, tagStyle?: string) => void;
27
71
  export declare const isAutocompleteDebugEnabled: () => boolean;
72
+ export declare const isAutocompleteDebugVerbose: () => boolean;
73
+ /**
74
+ * Hang the L1 session-boost snapshot off the console API.
75
+ *
76
+ * Unlike the log helpers this is available whether or not debug is enabled:
77
+ * inspecting state on demand is not logging, and asking someone to turn on
78
+ * logging and retype to find out what the session already holds defeats the
79
+ * point of being able to ask.
80
+ */
81
+ export declare const registerCtcSessionInspector: (inspect: (prefix?: string) => unknown) => void;
@@ -1,7 +1,10 @@
1
- import type { EditorState } from '@atlaskit/editor-prosemirror/state';
1
+ import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
2
2
  import { DecorationSet } from '@atlaskit/editor-prosemirror/view';
3
3
  /**
4
4
  * Creates a DecorationSet containing a ghost text widget at the given position.
5
5
  * The ghost text is rendered as a styled <span> that appears after the cursor.
6
+ *
7
+ * Takes the document rather than the whole state so it can also be called from
8
+ * `apply`, where only the post-transaction doc exists.
6
9
  */
7
- export declare const createGhostTextDecorationSet: (state: EditorState, position: number, text: string) => DecorationSet;
10
+ export declare const createGhostTextDecorationSet: (doc: PMNode, position: number, text: string) => DecorationSet;
@@ -5,11 +5,9 @@
5
5
  * a backend API, this client runs two models in the browser via WebGPU, in a
6
6
  * single MLCEngine, to reproduce the BE encoder's outputs on-device:
7
7
  *
8
- * - Causal LM (SmolLM2-135M-Instruct): one decode step per word boundary. A
9
- * registered LogitProcessor captures the raw next-token logits, which
10
- * `computeBePayload` turns into a whole-word `lm_logits` payload — a faithful
11
- * port of the BE `CausalLMEncoder._get_top_k_probs` (masked softmax over the
12
- * vocab's first-tokens, prefix expansion, L2 reservation, log-space pooling).
8
+ * - Causal LM (SmolLM2-135M-Instruct): context-keyed one-step boundary primes
9
+ * provide canonical first-token logits. A persistent token-prefix scheduler
10
+ * expands shared paths and exact-scores only plausible finalists.
13
11
  * - Semantic embedder (Snowflake Arctic Embed S): produces the real 384-d
14
12
  * `semantic_vector`. Inputs are wrapped as passages (see `wrapForArctic`) so
15
13
  * the runtime vector lands in the same space as the precomputed word bin.
@@ -19,7 +17,7 @@
19
17
  * the main thread is viable:
20
18
  *
21
19
  * - WebGPU GPU compute is inherently async (doesn't block the main thread)
22
- * - CPU overhead (BE-parity post-processing) is a few ms
20
+ * - CPU overhead for named-token reads and cache bookkeeping is small
23
21
  * - Per-inference latency is well within autocomplete expectations
24
22
  * (~250 ms between word boundaries)
25
23
  *
@@ -30,10 +28,11 @@
30
28
  * - Standard npm import — just works
31
29
  *
32
30
  * ── Interface ────────────────────────────────────────────────────────────
33
- * Same shape as createSlowLaneClient so text-predictor.ts needs zero changes.
34
- * The client exposes getContextVector() and getLmLogits() which are populated
35
- * asynchronously after each updateContext() call.
31
+ * Same base shape as createSlowLaneClient, plus on-device canonical-surface
32
+ * scoring. Semantic updates remain word-boundary timed; causal work is requested
33
+ * independently for exact pre-surface contexts.
36
34
  */
35
+ import { type BoundaryLmState, type BoundaryPrimeRequest, type ProgressiveSurfaceEvidence, type SurfaceScore, type SurfaceScoreRequest } from './canonical-lm-scoring';
37
36
  export interface LocalSlowLaneClientConfig {
38
37
  /**
39
38
  * Optional custom model registration for models not in web-llm's
@@ -60,12 +59,25 @@ export interface LocalSlowLaneClientConfig {
60
59
  * `customModelConfig` with the model URL and WASM library URL.
61
60
  */
62
61
  modelId?: string;
62
+ /** Callback fired when a context-keyed Tier-A raw-logit vector is available. */
63
+ onBoundaryLmUpdate?: (opts: {
64
+ contextKey: string;
65
+ familyKey: string;
66
+ latencyMs: number;
67
+ }) => void;
63
68
  /** Callback fired when the engine fails to load/start. */
64
69
  onLoadError?: (error: LocalSlowLaneLoadError) => void;
65
70
  /** Callback fired when the engine successfully loads and is ready. */
66
71
  onLoadSuccess?: (info: LocalSlowLaneLoadSuccess) => void;
67
72
  /** Callback fired with status messages (model loading progress, etc.). */
68
73
  onStatus?: (message: string) => void;
74
+ /** Callback fired when grouped progress or exact surface scores become available. */
75
+ onSurfaceScoreUpdate?: (opts: {
76
+ contextKey: string;
77
+ count: number;
78
+ familyKey: string;
79
+ latencyMs: number;
80
+ }) => void;
69
81
  /** Callback fired when inference returns new results. */
70
82
  onUpdate?: (opts: {
71
83
  hasLmLogits: boolean;
@@ -78,11 +90,19 @@ export interface LocalSlowLaneClientConfig {
78
90
  export interface LocalSlowLaneClient {
79
91
  /** Clean up resources. */
80
92
  destroy: () => void;
93
+ getBoundaryLmState: (contextKey: string) => BoundaryLmState | null;
94
+ getCanonicalSurfaceCount: () => number;
95
+ getCanonicalSurfaceTokenIds: (surface: string) => number[] | null;
96
+ getContextInput: () => string | null;
81
97
  getContextVector: () => Float32Array | null;
82
98
  getLmLogits: () => Record<string, number> | null;
99
+ getProgressiveSurfaceEvidence: (contextKey: string, surface: string) => ProgressiveSurfaceEvidence | null;
100
+ getSurfaceScore: (contextKey: string, surface: string) => SurfaceScore | null;
83
101
  /** Whether the model is loaded and ready for inference. */
84
102
  isReady: () => boolean;
85
103
  isWordBoundary: (text: string) => boolean;
104
+ primeBoundaryLm: (input: BoundaryPrimeRequest) => void;
105
+ requestProgressiveSurfaceScores: (input: SurfaceScoreRequest) => void;
86
106
  setContextVector: (vector: Float32Array | null) => void;
87
107
  setLmLogits: (logits: Record<string, number> | null) => void;
88
108
  updateContext: (text: string) => void;
@@ -160,21 +180,7 @@ export declare const LOCAL_MLC_EMBEDDING_MODEL_ID = "snowflake-arctic-embed-s-q0
160
180
  * the relationship. Encode both sides as passages. See implementation.md §4.3.
161
181
  */
162
182
  export declare const wrapForArctic: (text: string) => string;
163
- /**
164
- * BE-parity constants — must match `CausalLMEncoder` defaults in the Python
165
- * sidecar (`cc-smarts/python-sidecar/src/causal_lm_encoder.py`) and
166
- * `SlowLaneEngine` (`typeahead_context_encoding.py`) so local payloads behave
167
- * identically to the server-client setup.
168
- */
169
- export declare const BE_PARITY: {
170
- /** Final payload size cap (BE: `top_k_words`). */
171
- readonly TOP_K_WORDS: 2000;
172
- /** L2 (domain) words admitted unconditionally before pooling (BE: `reserved_l2_slots`). */
173
- readonly RESERVED_L2_SLOTS: 500;
174
- /** Log-space additive bias favouring L2 over L3 in the pool (BE: `l2_bias`). */
175
- readonly L2_BIAS: 1;
176
- /** Drop words below this probability from the final payload (BE: `> 0.00001`). */
177
- readonly MIN_PROB: 0.00001;
183
+ export declare const LOCAL_INFERENCE: {
178
184
  /**
179
185
  * Word-level approximation of the BE causal LM token limit.
180
186
  *
@@ -195,33 +201,61 @@ export declare const BE_PARITY: {
195
201
  */
196
202
  readonly MAX_CONTEXT_WORDS: 100;
197
203
  };
198
- /**
199
- * Convert a raw next-token logit vector into a whole-word probability payload,
200
- * faithfully porting the BE `CausalLMEncoder._get_top_k_probs`
201
- * (`cc-smarts/python-sidecar/src/causal_lm_encoder.py`).
202
- *
203
- * Steps: (1) numerically-stable masked softmax over only the token ids present
204
- * in the prefix-expansion map; (2) spread each token's probability to every
205
- * whole word sharing that first token, taking the max; (3) reserve the top L2
206
- * words unconditionally; (4) rank the remainder in a log-space pool with an
207
- * additive L2 bias; (5) emit raw probabilities for the survivors, lowercased
208
- * and trimmed at `MIN_PROB`.
209
- *
210
- * :params:
211
- * rawLogits: Full-vocabulary logits from the LM's single decode step
212
- * prefixMap: Map of first-token id to the words starting with that token
213
- * domainWords: Set of L2 (domain) words, for tier-aware ranking
214
- * :returns:
215
- * A record of lowercase word to probability — the BE `lm_logits` payload
216
- */
217
- export declare const computeBePayload: (rawLogits: Float32Array, prefixMap: Map<number, string[]>, domainWords: Set<string>,
218
- /**
219
- * Pre-derived token-ID array for the softmax mask. Defaults to the
220
- * module-level `prefixMapTokenIds` (zero allocation in production). Pass
221
- * `Array.from(prefixMap.keys())` in tests that supply a custom prefixMap so
222
- * the softmax mask stays consistent with the iteration in Step 2.
223
- */
224
- validTokenIds?: number[]) => Record<string, number>;
204
+ export declare const CANONICAL_SCORING: {
205
+ /**
206
+ * How many contexts keep their prefilled state before the oldest is dropped.
207
+ *
208
+ * Sizes every context-keyed cache together on purpose: an expansion needs
209
+ * both the boundary logits and the candidate list to still be resident, so
210
+ * bounding them separately would evict half of a context and strand the
211
+ * other half.
212
+ *
213
+ * The context of a keystroke is the text *before* the word being typed,
214
+ * which does not change while that word is typed, so one word should cost
215
+ * one prefill and then hit. Measured hit rate was 45% over the first 84s of
216
+ * a session and 24% over the following four minutes, well short of that, and
217
+ * prompt prefills per decision rose 0.80 → 1.25 across the same split while
218
+ * cost per prefill stayed flat. Contexts were being dropped while still live.
219
+ *
220
+ * A boundary entry holds a `Float32Array` over the 49,152-token vocabulary,
221
+ * so each one is ~192KB and this bound is the dominant term in the scorer's
222
+ * footprint: ~24MB resident here, against ~6MB at the 32 this replaced. That
223
+ * cost is what kept the bound low, not a hit rate anyone had measured.
224
+ */
225
+ readonly BOUNDARY_CACHE_MAX: 128;
226
+ readonly EXACT_MAX_TARGET_TOKENS: 8;
227
+ readonly PREFIX_CACHE_MAX: 256;
228
+ /**
229
+ * How many candidates compete for expansion in one context.
230
+ *
231
+ * Each distinct token prefix among them is a separate branch, and a branch
232
+ * the engine is not already standing on costs a prompt prefill before its
233
+ * first decode. A wide field therefore spreads a decision's round trips
234
+ * across candidates and finishes none of them inside the budget.
235
+ */
236
+ readonly PROGRESSIVE_CANDIDATES_MAX: 8;
237
+ /**
238
+ * Ceiling on distinct token prefixes explored per context.
239
+ *
240
+ * Raising this to 8 to give long surfaces more room did the opposite: model
241
+ * calls per decision went 2.35 → 4.32, the warm-KV extend share fell 40% →
242
+ * 31% as the extra branches displaced the live path, the prefix queue backed
243
+ * up to 29 deep, and acceptances per thousand decisions fell 9.3 → 6.7. No
244
+ * phrase was shown either way, so breadth was never the binding constraint.
245
+ *
246
+ * What blocked them then was normalisation against a short word's per-token
247
+ * mean, which has since been replaced by a posterior over sequence
248
+ * log-likelihoods. That removes the bias towards short surfaces but does not
249
+ * by itself make phrases reachable: a longer surface is strictly less likely
250
+ * than a shorter one, so a phrase sharing a shortlist with a unigram still
251
+ * holds little of its mass. Whether the remaining gap is the threshold or the
252
+ * comparison is still open.
253
+ */
254
+ readonly PROGRESSIVE_EXPANSIONS_PER_CONTEXT_MAX: 4;
255
+ readonly PROGRESSIVE_INPUT_MAX: 400;
256
+ readonly SURFACE_CACHE_MAX: 128;
257
+ readonly TIER_A_PRIMES_MAX: 3;
258
+ };
225
259
  /**
226
260
  * Create a local slow-lane client powered by MLC WebLLM.
227
261
  *
@@ -233,9 +267,8 @@ validTokenIds?: number[]) => Record<string, number>;
233
267
  * const client = createLocalSlowLaneClient({ debounceMs: 300 });
234
268
  * // On word boundaries:
235
269
  * client.updateContext(docText);
236
- * // In scoring pipeline:
237
- * const vec = client.getContextVector();
238
- * const logits = client.getLmLogits();
270
+ * // Candidate scoring independently calls
271
+ * // primeBoundaryLm/requestProgressiveSurfaceScores.
239
272
  * // On plugin teardown:
240
273
  * client.destroy();
241
274
  * ```
@@ -7,18 +7,35 @@
7
7
  * The grammar/POS payloads are fetched from the CDN by loadGrammarDataAsync, so
8
8
  * the grammar filter is a pass-through until that resolves.
9
9
  */
10
+ import type { CanonicalLmEvidence } from './canonical-lm-scoring';
11
+ /**
12
+ * A completion term is `(term, termType)`. `word` is the v1 default; `bigram`
13
+ * (2 tokens) and `phrase` (3 tokens) are multi-word units mined by the producer.
14
+ */
15
+ export type TermType = 'word' | 'bigram' | 'phrase';
10
16
  export interface ScoringCandidate {
17
+ /** NPMI-style association strength (bigram/phrase only). */
18
+ association?: number | null;
11
19
  authorFreq: number;
12
20
  docFreq: number;
21
+ /** Last-token POS of a multi-word term (bigram/phrase only). */
22
+ headPos?: string | null;
23
+ /** Per-token POS sequence of a multi-word term (bigram/phrase only). */
24
+ posSeq?: string[] | null;
13
25
  sessionFreq: number;
14
26
  tenantFreq: number;
27
+ /** Defaults to `'word'` when omitted (back-compat with word-only callers). */
28
+ termType?: TermType;
15
29
  word: string;
16
30
  }
17
31
  export interface ScoredCandidate {
18
32
  finalScore: number;
19
33
  freqScore: number;
34
+ lmEvidence: CanonicalLmEvidence | null;
20
35
  lmScore: number;
21
36
  semanticScore: number;
37
+ stage1Score: number;
38
+ termType: TermType;
22
39
  word: string;
23
40
  }
24
41
  /** Metadata returned by the grammar filter for debug logging in the caller. */
@@ -32,6 +49,18 @@ export interface GrammarFilterMeta {
32
49
  export declare const STAGE1_WEIGHT = 0.35;
33
50
  export declare const STAGE2_WEIGHT = 0.65;
34
51
  export declare const MIN_STAGE1_SCORE = 0.35;
52
+ /**
53
+ * How far the leading candidate must sit above the runner-up on the blended
54
+ * score before a ghost is committed.
55
+ *
56
+ * Lives beside the weights it is measured in, because it is only meaningful on
57
+ * that scale — and because both the arbitration that applies it and the
58
+ * scheduler that decides when reading more tokens can no longer change the
59
+ * outcome have to use the same number. When those two drifted apart, the
60
+ * scheduler stopped early on a margin the arbitration did not recognise and the
61
+ * abstention that followed was invisible from either side.
62
+ */
63
+ export declare const MIN_WINNER_MARGIN = 0.08;
35
64
  export declare const isGrammarDataLoaded: () => boolean;
36
65
  /**
37
66
  * Fetch the POS tag and grammar transition payloads from the CDN.
@@ -54,4 +83,7 @@ export interface RankCandidatesResult {
54
83
  grammarMeta: GrammarFilterMeta | null;
55
84
  pipelineDebug: PipelineDebug;
56
85
  }
57
- export declare function rankCandidates(candidates: ScoringCandidate[], contextVector: Float32Array | null, getWordVector: (word: string) => Float32Array | null, lmLogits: Record<string, number> | null, maxTenantFreq: number, previousWord: string): RankCandidatesResult;
86
+ /**
87
+ * Apply Stage 1, grammar, and nullable context-local LM evidence to candidates.
88
+ */
89
+ export declare function rankCandidates(candidates: ScoringCandidate[], contextVector: Float32Array | null, getWordVector: (word: string) => Float32Array | null, lmLogits: Record<string, number> | null, maxFreqByType: Record<TermType, number>, previousWord: string, getCanonicalLmEvidence?: (surface: string) => CanonicalLmEvidence | null): RankCandidatesResult;