From 6cd518bc69b0fb98a55dfabf5ddc4ed0be5a301a Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Thu, 9 Apr 2026 16:40:24 -0500 Subject: [PATCH 1/2] docs(web): add header comment to new file --- .../src/main/correction/token-result-mapping.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts index d177b54bb0..10f00de5fc 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts @@ -1,3 +1,13 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by jahorton on 2026-04-02 + * + * This file defines the type used for tracking critical graph-search properties + * utilized during correction-search by the `getBestMatches` algorithm when run + * against individual tokens / words. + */ + import { LexicalModelTypes } from '@keymanapp/common-types'; import { SearchNode, TraversableToken } from "./distance-modeler.js"; From f9ec8558734471a764b0cf6a92fc41e2d769f975 Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Mon, 20 Apr 2026 16:24:03 -0500 Subject: [PATCH 2/2] change(web): simplify search-result spaceId tagging + remapping After using devin.ai to check some of my PRs, it caught something on #15817 that I was able to trace back to some decisions already in place for `TokenResultMapping`. It appears best to simplify the type and its relation to spaceId-tagging now to simplify it, to prevent the bug, and to hopefully prevent re-implementing the bug in the future. I did consider constructing new instances of SearchNode, just with the changed spaceId... but that may be prone to causing memory churn that can be avoided by just letting spaceId be public. Build-bot: skip build:web Test-bot: skip --- .../src/main/correction/distance-modeler.ts | 14 ++++++++------ .../src/main/correction/legacy-quotient-root.ts | 3 +-- .../src/main/correction/search-quotient-cluster.ts | 5 +++-- .../src/main/correction/search-quotient-node.ts | 3 +-- .../src/main/correction/search-quotient-root.ts | 3 +-- .../src/main/correction/search-quotient-spur.ts | 3 +-- .../src/main/correction/token-result-mapping.ts | 6 +----- .../worker-thread/src/main/predict-helpers.ts | 2 +- 8 files changed, 17 insertions(+), 22 deletions(-) diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts index 8722e9cec9..159f3045d9 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts @@ -146,13 +146,15 @@ export class SearchNode { private readonly deleteAfterInsertEditPairs: number; /** - * A unique identifier corresponding to the earliest SearchPath containing - * the correction-search graph edge represented by this instance. + * A unique identifier corresponding to the SearchQuotientNode last passed + * through by the represented search path. * - * Corresponding search results will be tagged with this, which can be used - * to identify the result's original source tokenization. + * The correction-search results produced by this search path will be tagged + * accordingly to match the correction with its original ContextTokenization. + * This is necessary in order to properly construct suggestions that apply as + * the user expects should the tokenization pattern itself be corrected. */ - readonly spaceId: number; + public spaceId: number; /** * Notes the edit operation used for the most recent edge in the node's @@ -580,7 +582,7 @@ export class SearchNode { * @param timer * @returns */ -export async function *getBestMatches(searchModules: SearchQuotientNode[], timer: ExecutionTimer): AsyncGenerator { +export async function *getBestMatches(searchModules: SearchQuotientNode[], timer: ExecutionTimer): AsyncGenerator> { const spaceQueue = new PriorityQueue((a, b) => a.currentCost - b.currentCost); // Stage 1 - if we already have extracted results, build a queue just for them diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-root.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-root.ts index a4c39a1f58..8f7752f9d3 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-root.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-root.ts @@ -47,8 +47,7 @@ export class LegacyQuotientRoot extends SearchQuotientRoot { return { type: 'complete', cost: node.currentCost, - finalNode: node, - spaceId: this.spaceId + finalNode: node }; } diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts index 0ba4f725b7..d0eee7824d 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts @@ -144,15 +144,16 @@ export class SearchQuotientCluster implements SearchQuotientNode { this.selectionQueue.enqueue(bestPath); if(currentResult.type == 'complete') { + const node = currentResult.finalNode; + node.spaceId = this.spaceId; this.completedPaths?.push(currentResult.finalNode); - currentResult.spaceId = this.spaceId; } return currentResult; } public get previousResults(): TokenResultMapping[] { - return this.completedPaths?.map((n => new TokenResultMapping(n, this.spaceId))) ?? []; + return this.completedPaths?.map((n) => new TokenResultMapping(n)) ?? []; } get model(): LexicalModelTypes.LexicalModel { diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts index 3afc569618..13e696c123 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts @@ -32,8 +32,7 @@ type IntermediateSearchPath = { type CompleteSearchPath = { type: 'complete', cost: number, - finalNode: SearchNode, - spaceId: number + finalNode: SearchNode } export type PathResult = NullPath | IntermediateSearchPath | CompleteSearchPath; diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts index b37290ee36..94e562485c 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts @@ -77,8 +77,7 @@ export class SearchQuotientRoot implements SearchQuotientNode { return { type: 'complete', cost: 0, - finalNode: this.rootNode, - spaceId: this.spaceId + finalNode: this.rootNode }; } diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts index b9ee3792da..19d2ef6045 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts @@ -404,8 +404,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode { return { type: 'complete', cost: currentNode.currentCost, - finalNode: currentNode, - spaceId: this.spaceId + finalNode: currentNode }; } diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts index 10f00de5fc..7e048148b0 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/token-result-mapping.ts @@ -19,12 +19,8 @@ import Transform = LexicalModelTypes.Transform; export class TokenResultMapping { readonly node: SearchNode; - // Supports SearchPath -> SearchSpace remapping. - readonly spaceId: number; - - constructor(node: SearchNode, spaceId?: number) { + constructor(node: SearchNode) { this.node = node; - this.spaceId = spaceId ?? node.spaceId; } get inputSequence(): ProbabilityMass[] { diff --git a/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts b/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts index c8d1204ac6..bff12b04b6 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts @@ -536,7 +536,7 @@ export async function correctAndEnumerate( const correctionPredictionMap: Record> = {}; for await(const match of getBestMatches(searchModules, timer)) { // Corrections obtained: now to predict from them! - const tokenization = tokenizations.find(t => t.spaceId == match.spaceId); + const tokenization = tokenizations.find(t => t.spaceId == match.node.spaceId); // If our 'match' results in fully deleting the new token, reject it and try again. if(match.matchSequence.length == 0 && match.inputSequence.length != 0) {