From 6326210a1739e88b3270e8f1d6ef7c5324a9a1e1 Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Mon, 16 Feb 2026 09:22:35 +0700 Subject: [PATCH 1/4] change(web): incorporate feedback from PR review re alias, comments, and unfixed use of prior name --- .../correction/search-quotient-cluster.ts | 31 +++++++++---------- .../main/correction/search-quotient-spur.ts | 4 +-- .../context/context-state.tests.ts | 2 +- .../context/context-tokenization.tests.ts | 2 +- .../search-quotient-cluster.tests.ts | 2 +- 5 files changed, 19 insertions(+), 22 deletions(-) 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 88202f5dbd..0a86139655 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 @@ -3,26 +3,24 @@ * * Created by jahorton on 2025-10-20 * - * This file defines the predictive-text engine's SearchSpace class, which is used to - * manage the search-space(s) for text corrections within the engine. + * This file defines the predictive-text engine's SearchQuotientCluster class, + * which is used to manage the search-space(s) for text corrections within the + * engine. */ -import { QueueComparator as Comparator, PriorityQueue } from '@keymanapp/web-utils'; +import { QueueComparator, PriorityQueue } from '@keymanapp/web-utils'; import { LexicalModelTypes } from '@keymanapp/common-types'; import { SearchNode, SearchResult } from './distance-modeler.js'; import { generateSpaceSeed, InputSegment, PathResult, SearchQuotientNode } from './search-quotient-node.js'; -const PATH_QUEUE_COMPARATOR: Comparator = (a, b) => { +const PATH_QUEUE_COMPARATOR: QueueComparator = (a, b) => { return a.currentCost - b.currentCost; } // The set of search spaces corresponding to the same 'context' for search. // Whenever a wordbreak boundary is crossed, a new instance should be made. export class SearchQuotientCluster implements SearchQuotientNode { - // While most functions can be done directly from SearchSpace, merging and splitting will need access - // to SearchPath-specific members. It's also cleaner to not allow nested SearchClusters while we - // haven't worked out support for such a scenario. private selectionQueue: PriorityQueue = new PriorityQueue(PATH_QUEUE_COMPARATOR); readonly spaceId: number; @@ -48,20 +46,19 @@ export class SearchQuotientCluster implements SearchQuotientNode { private _processedEdgeSet?: {[pathKey: string]: boolean} = {}; /** - * Provides a heuristic for the base cost at each depth if the best - * individual input were taken at that level. + * Provides a heuristic for the base cost at each depth if the best individual + * input were taken at that level. */ readonly lowestPossibleSingleCost: number; /** - * Constructs a fresh SearchSpace instance for used in predictive-text correction - * and suggestion searches. - * @param baseSpaceId - * @param model + * Constructs a fresh SearchQuotientCluster instance for used in + * predictive-text correction and suggestion searches. + * @param inboundPaths */ constructor(inboundPaths: SearchQuotientNode[]) { if(inboundPaths.length == 0) { - throw new Error("SearchCluster requires an array with at least one SearchPath"); + throw new Error("SearchQuotientCluster requires an array with at least one SearchQuotientNode"); } let lowestPossibleSingleCost = Number.POSITIVE_INFINITY; @@ -72,12 +69,12 @@ export class SearchQuotientCluster implements SearchQuotientNode { for(let path of inboundPaths) { if(path.inputCount != inputCount || path.codepointLength != codepointLength) { - throw new Error(`SearchPath does not share same properties as others in the cluster: inputCount ${path.inputCount} vs ${inputCount}, codepointLength ${path.codepointLength} vs ${codepointLength}`); + throw new Error(`SearchQuotientNode does not share same properties as others in the cluster: inputCount ${path.inputCount} vs ${inputCount}, codepointLength ${path.codepointLength} vs ${codepointLength}`); } // If there's a source-range key mismatch - via mismatch in count or in actual ID, we have an error. if(path.sourceRangeKey != sourceRangeKey) { - throw new Error(`SearchPath does not share the same source identifiers as others in the cluster`); + throw new Error(`SearchQuotientNode does not share the same source identifiers as others in the cluster`); } lowestPossibleSingleCost = Math.min(lowestPossibleSingleCost, path.lowestPossibleSingleCost); @@ -121,7 +118,7 @@ export class SearchQuotientCluster implements SearchQuotientNode { * has fat-finger data available, which itself indicates that the user has * corrections enabled. */ - get correctionsEnabled(): boolean { + get correctionsEnabled(): boolean { const paths = this.selectionQueue.toArray(); // When corrections are disabled, the Web engine will only provide individual Transforms // for an input, not a distribution. No distributions means we shouldn't do corrections. 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 7a69c2bd99..bc90a8fa86 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 @@ -8,7 +8,7 @@ * engine. */ -import { QueueComparator as Comparator, KMWString, PriorityQueue } from '@keymanapp/web-utils'; +import { QueueComparator, KMWString, PriorityQueue } from '@keymanapp/web-utils'; import { LexicalModelTypes } from '@keymanapp/common-types'; import { buildMergedTransform } from '@keymanapp/models-templates'; @@ -23,7 +23,7 @@ import LexicalModel = LexicalModelTypes.LexicalModel; import ProbabilityMass = LexicalModelTypes.ProbabilityMass; import Transform = LexicalModelTypes.Transform; -export const QUEUE_NODE_COMPARATOR: Comparator = function(arg1, arg2) { +export const QUEUE_NODE_COMPARATOR: QueueComparator = function(arg1, arg2) { return arg1.currentCost - arg2.currentCost; } diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-state.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-state.tests.ts index 56be76c59b..5e57900477 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-state.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-state.tests.ts @@ -409,7 +409,7 @@ describe('ContextState', () => { assert.equal(state.tokenization.tokens[state.tokenization.tokens.length - 1].searchModule.inputCount, 1); }); - it.skip('handles case where tail token is split into three rather than two', function() { + it('handles case where tail token is split into three rather than two', function() { let baseContext = models.tokenize(defaultBreaker, { left: "text'", startOfBuffer: true, endOfBuffer: true }); diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-tokenization.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-tokenization.tests.ts index bcc1441fc0..925f229a40 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-tokenization.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-tokenization.tests.ts @@ -561,7 +561,7 @@ describe('ContextTokenization', function() { } }); - it.skip('handles case that triggers a token merge: can+\'+t', () => { + it('handles case that triggers a token merge: can+\'+t', () => { const baseTokens = ['an', ' ', 'apple', ' ', 'a', ' ', 'day', ' ', 'can', '\'']; const baseTokenization = new ContextTokenization(baseTokens.map(t => toToken(t))); diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-cluster.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-cluster.tests.ts index e7a0d785bd..84ef01f016 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-cluster.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-cluster.tests.ts @@ -3,7 +3,7 @@ * * Created by jahorton on 2025-10-29 * - * This file defines tests for the SearchSpace class of the + * This file defines tests for the SearchQuotientCluster class of the * predictive-text correction-search engine. */ From 80bcc0d5028104e75521abbb6796ffdbb4ad404f Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Mon, 16 Feb 2026 09:24:49 +0700 Subject: [PATCH 2/4] docs(web): missed a typo in prior commit's fixes --- .../src/main/correction/search-quotient-cluster.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0a86139655..bd7a7893eb 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 @@ -52,7 +52,7 @@ export class SearchQuotientCluster implements SearchQuotientNode { readonly lowestPossibleSingleCost: number; /** - * Constructs a fresh SearchQuotientCluster instance for used in + * Constructs a fresh SearchQuotientCluster instance for use in * predictive-text correction and suggestion searches. * @param inboundPaths */ From c6e492313beedbd6ea8abfc55ebaa99d684c44c0 Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Mon, 16 Feb 2026 09:34:00 +0700 Subject: [PATCH 3/4] fix(web): add code, comment fixes from PR review --- .../src/main/correction/search-quotient-cluster.ts | 12 +++++++++--- .../src/main/correction/search-quotient-spur.ts | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) 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 54b285f69c..33b025e3a6 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 @@ -195,9 +195,15 @@ export class SearchQuotientCluster implements SearchQuotientNode { return false; } - // We need to check if the parents match. Done naively in the manner below, this is O(N^2). - // Granted, we shouldn't have _that_ many incoming paths. - if(this.parents.find((path) => !space.parents.find((path2) => path.isSameNode(path2)))) { + // We need to check if the parents match. + // First, is the parent count the same? + if(this.parents.length != space.parents.length) { + return false; + } else if (this.parents.find((path) => !space.parents.find((path2) => path.isSameNode(path2)))) { + // Done naively in the manner above, checking each pair of nodes, to + // ensure a match is found for each, is O(N^2). + // + // Granted, we shouldn't have _that_ many incoming paths. return false; } 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 034aab1ddd..e30e99ff75 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 @@ -497,7 +497,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode { } isSameNode(space: SearchQuotientNode): boolean { - // Easiest cases: when the instances or their ' `spaceId` matches, we have + // Easiest cases: when the instances or their `spaceId` matches, we have // a perfect match. if(this == space || this.spaceId == space.spaceId) { return true; From ca0a3756325c7587704c94c83adc158cb2847245 Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Mon, 16 Feb 2026 09:43:09 +0700 Subject: [PATCH 4/4] change(web): Apply suggestions from code review Co-authored-by: Eberhard Beilharz --- .../src/main/correction/search-quotient-cluster.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 117cd9687d..6a648275da 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 @@ -193,7 +193,7 @@ export class SearchQuotientCluster implements SearchQuotientNode { const spaceHeadInputSource = space.inputSegments[0]; const isOnSplitInput = - thisTailSpaceIds.find((entry) => entry == space.inputSource.subsetId) + thisTailSpaceIds.some((entry) => entry == space.inputSource.subsetId) && thisTailInputSource.end == spaceHeadInputSource.start; // In this case, we only rebuild the single path; an outer stack frame will reconstitute @@ -209,7 +209,7 @@ export class SearchQuotientCluster implements SearchQuotientNode { // a prior split; if we'd split, it'd be a SearchQuotientCluster on both // ends. if(space instanceof SearchQuotientSpur) { - const parentMerge = this.merge(space.parents[0]) as SearchQuotientSpur; + const parentMerge = this.merge(space.parents[0]); return space.construct(parentMerge, space.inputs, space.inputSource); }