Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
123 commits
Select commit Hold shift + click to select a range
6552f2b
feat(web): implement dedicated substitution-spur class, tests
jahorton Mar 4, 2026
856466e
change(web): update TokenizationCorrector, QuotientNodeFinalizer unit…
jahorton Apr 20, 2026
77b22a2
change(web): adds temp constructRoot() method to handle legacy vs non…
jahorton Apr 23, 2026
8ba4d0b
change(web): safeguard clusters from clustering roots
jahorton Apr 23, 2026
e641f5f
feat(web): add dedicated quotient-spur type for insertion edits
jahorton Mar 9, 2026
fb644ff
fix(web): prevent null deref when merging on an insertion node
jahorton Apr 23, 2026
30be0fe
change(web): remove unit test that expected root-based cluster
jahorton Apr 23, 2026
4816729
feat(web): add dedicated DeletionQuotientSpur type for handling delet…
jahorton Mar 10, 2026
feba7cd
feat(web): extend search-graph test-helper funcs to handle specialize…
jahorton Mar 10, 2026
599044b
fix(web): link and patch up predictive-text test helper function unit…
jahorton Apr 23, 2026
8fb9786
feat(web): add test for split operations on quotient-graph doc example
jahorton Mar 10, 2026
7445486
Merge branch 'epic/autocorrect' into feat/web/substitution-spurs
jahorton May 5, 2026
3f768dd
change(web): drop unit test targeting now-invalid case
jahorton May 5, 2026
6abdee4
fix(web): block use of predictable "fallback" result to cases where n…
jahorton May 5, 2026
3cc6775
Merge branch 'fix/web/block-default-predictable-unless-needed' into f…
jahorton May 5, 2026
218aba0
Merge branch 'feat/web/insertion-spurs' into feat/web/deletion-spurs
jahorton May 7, 2026
53bc228
Merge branch 'feat/web/deletion-spurs' into feat/web/extend-graph-tes…
jahorton May 7, 2026
f7b974a
Merge branch 'feat/web/extend-graph-test-helpers' into fix/web/link-a…
jahorton May 7, 2026
558d989
Merge branch 'fix/web/link-and-fix-pred-text-helpers' into feat/web/t…
jahorton May 7, 2026
6ca785c
change(web): address PR suggestion of local var reuse
jahorton May 8, 2026
5ced0d0
fix(web): apply logic fix from suggestion
jahorton May 14, 2026
e9346de
change(web): use enum val for deletion edge check
jahorton May 14, 2026
9816529
docs(web): fix header comment
jahorton May 14, 2026
30f2f36
change(web): Apply EB suggestions from code review
jahorton May 14, 2026
9696ca8
chore: establish epic/boundary-correction 🔪
mcdurdin May 28, 2026
3a37e74
Merge pull request #16024 from keymanapp/chore/establish-epic-boundar…
mcdurdin May 28, 2026
1a1c93d
docs(web): add test file header
jahorton May 28, 2026
da12b14
change(web): disables one unit test temporarily (to be restored in fo…
jahorton May 28, 2026
cdf2031
Merge branch 'feat/web/substitution-spurs' into fix/web/block-default…
jahorton May 28, 2026
b432c91
change(web): Revert "change(web): disables one unit test temporarily …
jahorton May 28, 2026
2c9d550
Merge pull request #15688 from keymanapp/feat/web/substitution-spurs
jahorton May 29, 2026
1415cec
Merge pull request #15911 from keymanapp/fix/web/block-default-predic…
jahorton May 29, 2026
1ee482e
Merge pull request #15714 from keymanapp/feat/web/insertion-spurs
jahorton May 29, 2026
7d36c4a
Merge pull request #15720 from keymanapp/feat/web/deletion-spurs
jahorton May 29, 2026
f841b32
Merge pull request #15721 from keymanapp/feat/web/extend-graph-test-h…
jahorton May 29, 2026
b2e629f
Merge pull request #15872 from keymanapp/fix/web/link-and-fix-pred-te…
jahorton May 29, 2026
8b659b9
Merge pull request #15722 from keymanapp/feat/web/test-quotient-speci…
jahorton May 29, 2026
8bf43ec
change(web): Merge branch 'epic/autocorrect' into change/web/A19S30-m…
jahorton Jun 10, 2026
08eda10
change(web): track ContextState tokenization in array to prep for whi…
jahorton Apr 24, 2026
cc1aca6
refactor(web): replace ContextToken.addInput with constructor taking …
jahorton Apr 24, 2026
9d35d3d
change(web): generalize determineSuggestionRange
jahorton May 21, 2026
beb2a55
change(web): simplify model.predict() calls
jahorton Mar 26, 2026
4c929e1
change(web): remove determineSuggestionAlignment method in favor of d…
jahorton Apr 27, 2026
cdfc2db
change(web): predict from per-token correction sequence
jahorton May 18, 2026
9336ea2
feat(web): add new unit tests for generation of suggestions from mult…
jahorton May 18, 2026
ec458c5
fix(web): propagate transitionID correctly from corrections to predic…
jahorton May 28, 2026
84d00f7
fix(web): transitionId undefined check, extra unit test
jahorton Jun 2, 2026
fe6acfd
refactor(web): expose suggestion-root parameters for use in unit tests
jahorton May 19, 2026
624e53c
Merge pull request #16092 from keymanapp/change/web/A19S30-merge-auto…
jahorton Jun 11, 2026
329b514
fix(web): filter out corrections to whitespace/backspace inputs from …
jahorton Jun 11, 2026
091367c
Merge branch 'change/web/simplify-predict-calls' into refactor/web/ex…
jahorton Jun 11, 2026
8d52ba4
change(web): simplify mapWhitespacedTokenization requirements
jahorton May 6, 2026
e76b338
refactor(web): define common buildCorrectionSequence method used for …
jahorton May 28, 2026
4cfac7b
change(web): rework traversalless prediction, add mild whitespace-cor…
jahorton May 6, 2026
ef8593b
fix(web): adjust tokenization unit test expectations to match
jahorton May 8, 2026
ba6536d
refactor(web): DRY out & spin-off correction-sequence construction
jahorton May 21, 2026
3d355f0
fix(web): add codepointLength prop to match new base-branch reqts
jahorton May 22, 2026
808c598
feat(web): add unit tests for multi-token traversalless-model correct…
jahorton May 22, 2026
0a85adc
refactor(web): rework application of single-char correction penalty
jahorton May 27, 2026
1a7bb31
feat(web): tokenize input corrections and provide for multi-token pre…
jahorton Apr 30, 2026
9a26be5
fix(web): fix bugs in createDefaultKeep, extend unit testing
jahorton May 12, 2026
dedcf02
Merge pull request #15885 from keymanapp/change/web/state-tokenizatio…
jahorton Jun 12, 2026
5faafc0
Merge pull request #15886 from keymanapp/refactor/web/drop-token-addi…
jahorton Jun 12, 2026
389586f
change(web): cover .deleteLeft in unit testing
jahorton Jun 12, 2026
415e003
refactor(web): refactor intermediate composited prediction type
jahorton May 12, 2026
618e6bd
change(web): support multi-token suggestion similarity
jahorton May 12, 2026
b12c912
change(web): add tokenized prediction intermediate type for whitespac…
jahorton May 4, 2026
6b27134
fix(web): adjusts existing unit tests to match new intermediate-predi…
jahorton May 11, 2026
fb0883e
fix(web): apply original casing-application logic on a per-token basis
jahorton May 13, 2026
dbd060a
change(web): clean up application of transitionIDs and applyInPost funcs
jahorton Jun 2, 2026
acec50b
fix(web): unit test patchup, dummy model updates for changed dummied-…
jahorton Jun 2, 2026
15080a6
fix(web): address issues caught by AI review
jahorton Jun 12, 2026
245f43f
change(web): adjust TokenizationCorrector spec
jahorton May 13, 2026
c2816da
change(web): remove unused transition-edit field
jahorton Jun 4, 2026
bbce66b
change(web): remove .preservationTransform, .taillessTrueKeystroke
jahorton Jun 4, 2026
4b8c37e
feat(web): add prepareTokenizationSearch helper method
jahorton Apr 14, 2026
2faebb4
change(web): convert primary correction-search loop for white-space c…
jahorton May 7, 2026
095ee32
docs(web): adjust comments per review
jahorton Jun 22, 2026
fe769f3
Merge pull request #15992 from keymanapp/change/web/rework-suggestion…
jahorton Jun 22, 2026
ecccdd0
Merge pull request #15851 from keymanapp/change/web/simplify-predict-…
jahorton Jun 22, 2026
7441df0
Merge pull request #15977 from keymanapp/refactor/web/expose-suggesti…
jahorton Jun 22, 2026
191fc5b
Merge pull request #15919 from keymanapp/change/web/abstract-whitespa…
jahorton Jun 22, 2026
1ce1695
Merge pull request #16028 from keymanapp/refactor/web/correction-sequ…
jahorton Jun 22, 2026
433641d
Merge pull request #15920 from keymanapp/change/web/rework-traversall…
jahorton Jun 22, 2026
1ff249a
Merge pull request #16023 from keymanapp/refactor/web/single-char-cor…
jahorton Jun 22, 2026
c949dcf
Merge pull request #15897 from keymanapp/feat/web/multi-token-predict…
jahorton Jun 22, 2026
4e30937
Merge pull request #15950 from keymanapp/fix/web/create-default-keep
jahorton Jun 22, 2026
ed15bf8
docs(web): document TokenizationCorrector fields, rearrange into prop…
jahorton Jun 22, 2026
ed42281
change(web): apply suggestions from PR review
jahorton Jun 22, 2026
bdc824a
change(web): rename compositeIntermdiatePredictions to composeInterme…
jahorton Jun 22, 2026
cda2d40
Merge branch 'change/web/multi-token-prediction-intermediates' into c…
jahorton Jun 22, 2026
03d20f1
change(web): rework type nomenclature
jahorton Jun 29, 2026
39d5550
Merge branch 'refactor/web/intermediate-composited-prediction' into c…
jahorton Jun 29, 2026
473ca85
Merge branch 'change/web/multi-token-suggestion-similarity' into chan…
jahorton Jun 29, 2026
874cd33
Merge branch 'change/web/multi-token-prediction-intermediates' into c…
jahorton Jun 29, 2026
8bdebac
Merge branch 'change/web/adjust-tokenization-corrector-spec' into cha…
jahorton Jun 29, 2026
9a0af09
Merge branch 'change/web/remove-unused-transition-edit-field' into ch…
jahorton Jun 29, 2026
7ea42fb
Merge pull request #15951 from keymanapp/refactor/web/intermediate-co…
jahorton Jun 29, 2026
f439696
Merge pull request #15952 from keymanapp/change/web/multi-token-sugge…
jahorton Jun 29, 2026
66c07a5
Merge branch 'epic/boundary-correction' into change/web/multi-token-p…
jahorton Jun 29, 2026
eb93ac7
Merge pull request #15907 from keymanapp/change/web/multi-token-predi…
jahorton Jun 29, 2026
28b3385
Merge pull request #15955 from keymanapp/change/web/adjust-tokenizati…
jahorton Jun 30, 2026
6265f02
Merge pull request #16071 from keymanapp/change/web/remove-unused-tra…
jahorton Jun 30, 2026
8689e20
Merge pull request #16072 from keymanapp/change/web/remove-preservati…
jahorton Jun 30, 2026
2f0f8ca
Merge branch 'epic/boundary-correction' into auto/A19S32-merge-base-i…
jahorton Jul 6, 2026
ed16d94
Merge pull request #16195 from keymanapp/auto/A19S32-merge-base-into-…
mcdurdin Jul 9, 2026
a92bd79
Merge branch 'epic/boundary-correction' into auto/A19S33-merge-master…
keyman-server Jul 20, 2026
20c05b9
Merge pull request #16256 from keymanapp/auto/A19S33-merge-master-int…
keyman-server Jul 20, 2026
b9d0201
feat(web): add prepareTokenizationSearch helper method
jahorton Apr 14, 2026
6896981
Merge branch 'epic/autocorrect' into change/merge-autocorrect-into-bo…
jahorton Jul 30, 2026
388ecac
Merge branch 'feat/web/prep-tokenization-search' into change/web/mult…
jahorton Jul 30, 2026
d2b5ac5
fix(web): fix broken unit test
jahorton Jul 31, 2026
0a8ae9a
Merge pull request #16294 from keymanapp/change/merge-autocorrect-int…
jahorton Aug 3, 2026
d5c3cc2
Merge branch 'epic/boundary-correction' into feat/web/prep-tokenizati…
jahorton Aug 3, 2026
39996fe
Merge branch 'feat/web/prep-tokenization-search' into change/web/mult…
jahorton Aug 3, 2026
487ce1c
change(web): address PR review comments and corrections
jahorton Aug 5, 2026
069610e
Merge branch 'feat/web/prep-tokenization-search' into change/web/mult…
jahorton Aug 5, 2026
ecc25c1
Merge pull request #15852 from keymanapp/feat/web/prep-tokenization-s…
jahorton Aug 6, 2026
018ef45
Merge pull request #15927 from keymanapp/change/web/multi-token-corre…
jahorton Aug 6, 2026
8da3fcf
Merge branch 'epic/boundary-correction' into auto/A19S34-merge-base-i…
jahorton Aug 12, 2026
ecc908f
Merge pull request #16322 from keymanapp/auto/A19S34-merge-base-into-…
mcdurdin Aug 13, 2026
f86e5fa
Merge branch 'epic/boundary-correction' into auto/A19S35-merge-base-i…
jahorton Aug 14, 2026
8e90b41
Merge pull request #16373 from keymanapp/auto/A19S35-merge-base-into-…
mcdurdin Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/epics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Epics

This document lists epics in the Keyman project.

Name | Lead | Target release | Spec
-----------------------|-----------|----------------|--------------------
epic/embed-osk-in-kmx | @mcdurdin | 19.0 | [embed-osk-in-kmx]
epic/autocorrect | @jahorton | 19.0 |
epic/boundary-correction | @jahorton | 20.0 |


Note: when establishing a new epic branch, add the details of the epic to this
file, as a clean way to start an epic without unreviewed code changes in the
base epic PR.

---

[embed-osk-in-kmx]: https://docs.google.com/document/d/15EjtSH7NAsGrdapfB3E7SlS6CqI-2zPy3PNyfUiAv54/edit?tab=t.0
2 changes: 1 addition & 1 deletion web/src/engine/predictive-text/templates/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function buildMergedTransform(first: Transform, second: Transform): Trans
deleteLeft: first.deleteLeft + mergedSecondDelete
}

if(first.id && first.id == second.id) {
if(first.id !== undefined && first.id == second.id) {
returnedObj.id = first.id;
}

Expand Down
4 changes: 4 additions & 0 deletions web/src/engine/predictive-text/templates/src/tokenization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ export function tokenize(
currentIndex = nextIndex;
}

if(tokenization.left.length == 0) {
tokenization.left.push({text: '', isWhitespace: false});
}

// New step 2: handle any rejoins needed.

// Handle any desired special handling for directly-pre-caret scenarios - where for this
Expand Down
2 changes: 2 additions & 0 deletions web/src/engine/predictive-text/worker-thread/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ SRCMAP_CLEANER="${KEYMAN_ROOT}/web/build/tools/building/sourcemap-root/index.js"
################################ Main script ################################

SUBPROJECT_NAME=engine/predictive-text/worker-thread
SUBPROJECT_HELPERS=engine/predictive-text/helpers

builder_describe \
"Compiles the Language Modeling Layer for common use in predictive text and autocorrective applications." \
Expand Down Expand Up @@ -100,6 +101,7 @@ function do_test() {
WTR_INSPECT=" --manual"
fi

test-headless-typescript $SUBPROJECT_HELPERS
test-headless-typescript $SUBPROJECT_NAME

web-test-runner --config ./src/tests/test-runner/web-test-runner${WTR_CONFIG}.config.mjs ${WTR_INSPECT}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@ export class ContextState {
/**
* Denotes the possible tokenization(s) for the represented Context.
*/
tokenization: ContextTokenization;
_tokenizations: ContextTokenization[];

/**
* Tracks the tokenization pattern best matching the word boundary
* patterns in the actual context.
*/
_displayTokenization: ContextTokenization;

/**
* Denotes the keystroke-sourced Transform that was last applied to a
Expand Down Expand Up @@ -97,7 +103,14 @@ export class ContextState {
* is visible to the user.
*/
get displayTokenization(): ContextTokenization {
return this.tokenization;
return this._displayTokenization;
}

/**
* Denotes the possible tokenization(s) for the represented Context.
*/
get tokenizations(): ContextTokenization[] {
return this._tokenizations;
}

/**
Expand Down Expand Up @@ -127,13 +140,16 @@ export class ContextState {
* @param tokenization Precomputed tokenization for the context, leveraging previous
* correction-search progress and results
*/
constructor(context: Context, model: LexicalModel, tokenization?: ContextTokenization);
constructor(param1: Context | ContextState, model?: LexicalModel, tokenization?: ContextTokenization) {
constructor(context: Context, model: LexicalModel, tokenization?: ContextTokenization, tokenizations?: ContextTokenization[]);
constructor(param1: Context | ContextState, model?: LexicalModel, tokenization?: ContextTokenization, tokenizations?: ContextTokenization[]) {
if(!(param1 instanceof ContextState)) {
this.context = param1;
this.model = model;
if(tokenization) {
this.tokenization = tokenization;
this._tokenizations = tokenizations ? tokenizations : [tokenization];
this._displayTokenization = tokenization;

this.inputTransforms = new Map();
} else {
this.initFromReset();
}
Expand All @@ -142,7 +158,8 @@ export class ContextState {

Object.assign(this, stateToClone);
this.inputTransforms = new Map(stateToClone.inputTransforms);
this.tokenization = new ContextTokenization(stateToClone.tokenization);
this._displayTokenization = new ContextTokenization(stateToClone._displayTokenization);
this._tokenizations = stateToClone.tokenizations.map((t) => new ContextTokenization(t));

// A shallow copy of the array is fine, but we'd be best off
// not aliasing the array itself.
Expand Down Expand Up @@ -173,10 +190,35 @@ export class ContextState {
if(baseTokens.length == 0) {
baseTokens.push(ContextToken.fromRawText(this.model, ''));
}
this.tokenization = new ContextTokenization(baseTokens);
this._displayTokenization = new ContextTokenization(baseTokens);
this._tokenizations = [this._displayTokenization];
this.inputTransforms = new Map();
}

/**
* Builds a variant of the ContextState with no input metadata, reiterating the resulting
* state of a recent transition but without any associated input mutation data.
* @param stateToClone
* @param context
* @returns
*/
transitionContextWindow(context: Context) {
// The context may have slid since the last ContextState observation. We check for slide effects
// and apply them here as well.
const slideUpdateTransform = determineContextSlideTransform(this.context, context);

const model = this.model;

// Should a context-reset or similar occur, there is no input to replace, nor is there
// pre-tail token transform data to preserve. It's important that part, in particular,
// be cleared so that Suggestions in this state are not adversely affected.
const displayTokenization = this._displayTokenization.applyContextSlide(model, slideUpdateTransform);
const tokenizations = this.tokenizations.map((t) => t.applyContextSlide(model, slideUpdateTransform));

const state = new ContextState(context, this.model, displayTokenization, tokenizations);
return state;
}

/**
* As written, this method attempts to determine the context state and tokenization(s)
* that result from applying an incoming transform distribution to the incoming context
Expand Down Expand Up @@ -209,8 +251,7 @@ export class ContextState {

const slideUpdateTransform = determineContextSlideTransform(this.context, context);

// Goal: allow multiple base tokenizations.
const startTokenizations = [this.tokenization].map((t) => {
const startTokenizations = this.tokenizations.map((t) => {
return t.applyContextSlide(lexicalModel, slideUpdateTransform);
});

Expand All @@ -222,14 +263,15 @@ export class ContextState {
// If the tokenizations match, clone the ContextState; we want to preserve a post-application
// context separately from pre-application contexts for predictions based on empty roots.
const state = new ContextState(this);
state.tokenization = [...startTokenizations.values()][0];
state._tokenizations = [...startTokenizations.values()];
state._displayTokenization = this._displayTokenization.applyContextSlide(lexicalModel, slideUpdateTransform);
transition.finalize(state, transformDistribution);
return transition;
}

const { subsets, keyMatchingUserContext: trueInputSubsetKey } = precomputeTransitions(startTokenizations, transformDistribution);
const resultTokenization = transitionTokenizations(subsets, transformDistribution).get(trueInputSubsetKey);

const possibleTokenizations = transitionTokenizations(subsets, transformDistribution);
const resultTokenization = possibleTokenizations.get(trueInputSubsetKey);
// ------------

// So, if we have a suggestion transition ID at the end and didn't just apply...
Expand All @@ -240,10 +282,11 @@ export class ContextState {
// 'any'.)

const state = new ContextState(applyTransform(trueInput, context), lexicalModel);
state.tokenization = resultTokenization;
state._tokenizations = [resultTokenization]; // TODO: [...possibleTokenizations.values()];
state._displayTokenization = resultTokenization;
state.appliedInput = transformDistribution?.[0].sample;
transition.finalize(state, transformDistribution);
transition.revertableTransitionId = state.tokenization.tail.appliedTransitionId;
transition.revertableTransitionId = state._displayTokenization.tail.appliedTransitionId;
return transition;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { LegacyQuotientSpur } from "./legacy-quotient-spur.js";
import { LegacyQuotientRoot } from "./legacy-quotient-root.js";
import { generateSubsetId } from './tokenization-subsets.js';

import Distribution = LexicalModelTypes.Distribution;
import LexicalModel = LexicalModelTypes.LexicalModel;
import ProbabilityMass = LexicalModelTypes.ProbabilityMass;
import Transform = LexicalModelTypes.Transform;
Expand All @@ -37,11 +36,42 @@ function textToCharTransforms(text: string, transitionId?: number): Transform[]
[...text].map(insert => ({insert, deleteLeft: 0}));
}


/**
* Defines an interface compatible with ContextToken that is useful for handling
* cases that should not be considered correctable.
*/
export interface ContextTokenLike {
/**
* Generates text corresponding to the net effects of the most likely inputs
* received that can correspond to the represented token.
*/
exampleInput: string;

/**
* Reports the length in codepoints of corrected text represented by the
* current token.
*/
codepointLength: number;

/**
* Whether or not the token is likely still being edited by the user (due to
* adjacency of the caret)
*/
isPartial?: boolean;

/**
* Gets a compact string-based representation of `inputRange` that
* maps compatible token source ranges to each other.
*/
sourceRangeKey?: string;
}

/**
* Represents cached data about one token (either a word or a unit of whitespace)
* in the context and associated correction-search progress and results.
*/
export class ContextToken {
export class ContextToken implements ContextTokenLike {
/**
* Indicates whether or not the token is considered whitespace.
*/
Expand All @@ -56,6 +86,10 @@ export class ContextToken {
}
private _searchModule: SearchQuotientNode;

/**
* Whether or not the token is likely still being edited by the user (due to
* adjacency of the caret)
*/
isPartial: boolean;

/**
Expand Down Expand Up @@ -125,11 +159,11 @@ export class ContextToken {
}

/**
* Call this to record the original keystroke Transforms for the context range
* corresponding to this token.
* Reports the length in codepoints of corrected text represented by the
* current token.
*/
addInput(inputSource: PathInputProperties, distribution: Distribution<Transform>) {
this._searchModule = new LegacyQuotientSpur(this._searchModule, distribution, inputSource);
get codepointLength() {
return this._searchModule.codepointLength;
}

get inputCount() {
Expand Down Expand Up @@ -169,7 +203,7 @@ export class ContextToken {

/**
* Generates text corresponding to the net effects of the most likely inputs
* received that can correspond to the current instance.
* received that can correspond to the represented token.
*/
get exampleInput(): string {
return this.searchModule.bestExample.text;
Expand Down
Loading