From 013def7d9c917e767c4304b94f613996280205c6 Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Thu, 17 Sep 2026 16:30:50 -0500 Subject: [PATCH] change(web): add lexical weighting to prediction search Fixes: #16577 From 14.0 up until now, our correction-search focused on the minimum "correction distance" to a possible text _correction_, then performed prediction _on top of that_. This PR changes the search to integrate word-frequency weighting from models within the search, allowing us to optimize for the most likely predictions. There is no additional performace cost impact of note here - the probability associated with a correction's best prediction is already accessible O(1) with operations we've already been doing. It is likely that this will help further mitigate issues with transposition handling noted in the user tests for #16450. Build-bot: skip release:web,android,ios --- .../correction/correction-result-mapping.ts | 10 +- .../main/correction/correction-searchable.ts | 6 +- .../src/main/correction/distance-modeler.ts | 20 ++- .../main/correction/legacy-quotient-root.ts | 4 +- .../main/correction/legacy-quotient-spur.ts | 4 +- .../correction/search-quotient-cluster.ts | 8 +- .../main/correction/search-quotient-spur.ts | 8 +- .../main/correction/token-result-mapping.ts | 12 +- .../worker-thread/src/main/predict-helpers.ts | 33 +++-- .../distance-modeler.tests.ts | 83 ++++++------- .../correction-search/getBestMatches.tests.ts | 114 +++++------------- .../legacy-quotient-spur.tests.ts | 4 +- .../quotient-node-finalizer.tests.ts | 8 +- .../build-and-map-predictions.tests.ts | 8 +- 14 files changed, 142 insertions(+), 180 deletions(-) diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-result-mapping.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-result-mapping.ts index 67e8fa62b6..d1903b6caf 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-result-mapping.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-result-mapping.ts @@ -27,10 +27,16 @@ export interface CorrectionResultMapping { readonly matchedResult: Readonly; /** - * Gets the "total cost" of the edge, which should be considered as the + * Gets the "correction cost" of the edge, which should be considered as the * negative log-likelihood of the input path taken to reach the node * multiplied by the 'probability' induced by needed Damerau-Levenshtein edits * to the resulting output. */ - readonly totalCost: number; + readonly correctionCost: number; + + /** + * The "total cost" of the edge - comprised of both the correction cost and the + * prediction cost based on the model's frequency data for the word. + */ + readonly currentCost: number; } \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-searchable.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-searchable.ts index 74ca206e1e..648734c3ef 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-searchable.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/correction-searchable.ts @@ -29,10 +29,14 @@ type CompleteSearchPath = { export type PathResult = NullPath | IntermediateSearchPath | CompleteSearchPath; -export function CORRECTION_QUEUE_COMPARATOR(a: T, b: T) { +export function PREDICTION_QUEUE_COMPARATOR(a: T, b: T) { return a.currentCost - b.currentCost; } +export function CORRECTION_QUEUE_COMPARATOR(a: T, b: T) { + return a.correctionCost - b.correctionCost; +} + /** * Represents objects that support correction search via the `getBestMatches` * method, providing metadata relative to optimizing the search process for 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 b85f6b98c0..d3ffc2e0a4 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 @@ -4,7 +4,7 @@ import { PriorityQueue } from 'keyman/common/web-utils'; import { LexicalModelTypes } from '@keymanapp/common-types'; import { ClassicalDistanceCalculation } from './classical-calculation.js'; -import { CORRECTION_QUEUE_COMPARATOR, CorrectionSearchable } from './correction-searchable.js'; +import { PREDICTION_QUEUE_COMPARATOR, CorrectionSearchable } from './correction-searchable.js'; import { CorrectionResultMapping } from './correction-result-mapping.js'; import { ExecutionTimer, STANDARD_TIME_BETWEEN_DEFERS } from './execution-timer.js'; import { SearchQuotientNode } from './search-quotient-node.js'; @@ -262,7 +262,7 @@ export class SearchNode { * The correction search evaluates Nodes in cost-ascending order based on this property's * return value. */ - get currentCost(): number { + get correctionCost(): number { // - We reintrepret 'known cost' as a psuedo-probability. // - Noting that 1/e = 0.367879441, an edit-distance cost of 1 may be intepreted as -ln(1/e) - a log-space 'likelihood'. // - Not exactly normalized, though. @@ -278,6 +278,14 @@ export class SearchNode { return EDIT_DISTANCE_COST_SCALE * this.editCount + this.inputSamplingCost; } + get predictionCost(): number { + return -Math.log(this.currentTraversal.p); + } + + get currentCost(): number { + return this.correctionCost + this.predictionCost; + } + addEdit() { this.addedEditCost++; } @@ -625,7 +633,7 @@ export async function *getBestMatches< // If no filter function is provided, default to one that always returns true. filter ??= () => true; - let spaceQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR); + let spaceQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR); // Stage 1 - if we already have extracted results, build a queue just for them // and iterate over it first. @@ -633,7 +641,7 @@ export async function *getBestMatches< // Does not get any results that another iterator pulls up after this is // created - and those results won't come up later in stage 2, either. Only // intended for restarting a search, not searching twice in parallel. - const priorResultsQueue = new PriorityQueue((a, b) => a.totalCost - b.totalCost); + const priorResultsQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR); priorResultsQueue.enqueueAll(searchModules.map((space) => space.previousResults).flat()); // With potential prior results re-queued, NOW enqueue. (Not before - the heap may reheapify!) @@ -642,7 +650,7 @@ export async function *getBestMatches< // Stage 2: the fun part; actually searching! do { const entry: ResultMapping = timer.time(() => { - if((priorResultsQueue.peek()?.totalCost ?? Number.POSITIVE_INFINITY) <= spaceQueue.peek().currentCost) { + if((priorResultsQueue.peek()?.currentCost ?? Number.POSITIVE_INFINITY) <= spaceQueue.peek().currentCost) { const result = priorResultsQueue.dequeue(); // There's no guarantee that the filter closure is the same instance as @@ -669,7 +677,7 @@ export async function *getBestMatches< let lowestCostSource = spaceQueue.dequeue(); const newResult = lowestCostSource.handleNextNode(); spaceQueue.enqueue(lowestCostSource); - spaceQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, spaceQueue.toArray()); + spaceQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, spaceQueue.toArray()); if(newResult.type == 'none') { return null; 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 7b381890be..dc73a02be8 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 @@ -1,7 +1,7 @@ import { PriorityQueue } from 'keyman/common/web-utils'; import { LexicalModelTypes } from '@keymanapp/common-types'; -import { CORRECTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; +import { PREDICTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; import { SearchQuotientNode } from './search-quotient-node.js'; import { SearchQuotientRoot } from './search-quotient-root.js'; import { SearchNode } from './distance-modeler.js'; @@ -10,7 +10,7 @@ import LexicalModel = LexicalModelTypes.LexicalModel; import { TokenResultMapping } from './token-result-mapping.js'; export class LegacyQuotientRoot extends SearchQuotientRoot { - private selectionQueue: PriorityQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR); + private selectionQueue: PriorityQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR); private processed: SearchNode[] = []; constructor(model: LexicalModel) { diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-spur.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-spur.ts index a1134ddd3e..99e97b7f84 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-spur.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/legacy-quotient-spur.ts @@ -11,7 +11,7 @@ import { LexicalModelTypes } from '@keymanapp/common-types'; import { KMWString, PriorityQueue } from 'keyman/common/web-utils'; -import { CORRECTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; +import { PREDICTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; import { SearchNode } from './distance-modeler.js'; import { SearchQuotientNode, PathInputProperties } from './search-quotient-node.js'; import { SearchQuotientSpur } from './search-quotient-spur.js'; @@ -24,7 +24,7 @@ import Transform = LexicalModelTypes.Transform; // 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 LegacyQuotientSpur extends SearchQuotientSpur { - private transposeQueue: PriorityQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR); + private transposeQueue: PriorityQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR); private incomingTransposeRootNodes: TokenResultMapping[] = []; public readonly insertLength: number; 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 4a5ee0723f..ba625b5d81 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 @@ -11,7 +11,7 @@ import { PriorityQueue } from 'keyman/common/web-utils'; import { LexicalModelTypes } from '@keymanapp/common-types'; -import { CORRECTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; +import { PREDICTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; import { LegacyQuotientRoot } from './legacy-quotient-root.js'; import { generateSpaceSeed, InputSegment, SearchQuotientNode } from './search-quotient-node.js'; import { SearchQuotientSpur } from './search-quotient-spur.js'; @@ -20,7 +20,7 @@ import { TokenResultMapping } from './token-result-mapping.js'; // 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 extends SearchQuotientNode { - private selectionQueue: PriorityQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR); + private selectionQueue: PriorityQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR); readonly spaceId: number; // We use an array and not a PriorityQueue b/c batch-heapifying at a single @@ -102,7 +102,7 @@ export class SearchQuotientCluster extends SearchQuotientNode { entries.forEach((path) => path.increaseMaxEditDistance()); // Since we just modified the stored instances, and the costs may have shifted, we need to re-heapify. - this.selectionQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, entries.slice()); + this.selectionQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, entries.slice()); } /** @@ -131,7 +131,7 @@ export class SearchQuotientCluster extends SearchQuotientNode { const bestPath = this.selectionQueue.dequeue(); const baseResult = bestPath.handleNextNode(); this.selectionQueue.enqueue(bestPath); - this.selectionQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, this.selectionQueue.toArray()); + this.selectionQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, this.selectionQueue.toArray()); let finalResult = baseResult; if(baseResult.type == 'complete') { 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 dbc0742287..78fa68eb6e 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 @@ -12,7 +12,7 @@ import { KMWString, PriorityQueue } from 'keyman/common/web-utils'; import { LexicalModelTypes } from '@keymanapp/common-types'; import { buildMergedTransform } from '@keymanapp/models-templates'; -import { CORRECTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; +import { PREDICTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js'; import { EDIT_DISTANCE_COST_SCALE, SearchNode } from './distance-modeler.js'; import { generateSpaceSeed, InputSegment, PathInputProperties, SearchQuotientNode } from './search-quotient-node.js'; import { generateSubsetId } from './tokenization-subsets.js'; @@ -34,7 +34,7 @@ export const MAX_EDIT_THRESHOLD_FACTOR = 2.5; // 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 abstract class SearchQuotientSpur extends SearchQuotientNode { - private selectionQueue: PriorityQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR); + private selectionQueue: PriorityQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR); /** * Holds all incoming Nodes generated from a parent `SearchSpace` that have not yet been @@ -152,7 +152,7 @@ export abstract class SearchQuotientSpur extends SearchQuotientNode { entries.forEach(function(edge) { edge.calculation = edge.calculation.increaseMaxDistance(); }); // Since we just modified the stored instances, and the costs may have shifted, we need to re-heapify. - this.selectionQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, entries); + this.selectionQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, entries); } /** @@ -419,7 +419,7 @@ export abstract class SearchQuotientSpur extends SearchQuotientNode { // Allows a little 'wiggle room' + 2 "hard" edits. // Can be important if needed characters don't actually exist on the keyboard // ... or even just not the then-current layer of the keyboard. - if(currentNode.currentCost > this.lowestPossibleSingleCost + MAX_EDIT_THRESHOLD_FACTOR * EDIT_DISTANCE_COST_SCALE) { + if(currentNode.correctionCost > this.lowestPossibleSingleCost + MAX_EDIT_THRESHOLD_FACTOR * EDIT_DISTANCE_COST_SCALE) { return unmatchedResult; } 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 37e9d1d00b..d37f02ce37 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 @@ -33,8 +33,8 @@ export function initTokenResultFilterer() { return false; } - if((priorReturnCosts.get(searchResult.matchString) ?? Number.MAX_VALUE) > searchResult.totalCost) { - priorReturnCosts.set(searchResult.matchString, searchResult.totalCost); + if((priorReturnCosts.get(searchResult.matchString) ?? Number.MAX_VALUE) > searchResult.correctionCost) { + priorReturnCosts.set(searchResult.matchString, searchResult.correctionCost); return true; } else { @@ -118,6 +118,14 @@ export class TokenResultMapping implements CorrectionResultMapping { * multiplied by the 'probability' induced by needed Damerau-Levenshtein edits * to the resulting output. */ + get correctionCost(): number { + return this.node.correctionCost; + } + + get currentCost(): number { + return this.node.currentCost; + } + get totalCost(): number { return this.node.currentCost; } 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 2ba308fdea..dd1146793c 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 @@ -532,7 +532,7 @@ export function buildAndMapPredictions( transition: ContextTransition, tokenization: ContextTokenization, // Originally, Readonly - but we only need these three components here. - match: Readonly<{matchString: string, totalCost: number, editCount: number}>, + match: Readonly<{matchString: string, correctionCost: number, editCount: number}>, costFactor: number ): CorrectionPredictionTuple[] { const model = transition.final.model; @@ -545,7 +545,7 @@ export function buildAndMapPredictions( // --- to move into predictFromCorrections --- let correction = match.matchString; - let rootCost = match.totalCost; + let rootCost = match.correctionCost; // Replace the existing context with the correction. const correctionTransform: Transform = { @@ -656,7 +656,7 @@ export async function correctAndEnumerate( // Only run the correction search when corrections are enabled. let rawPredictions: CorrectionPredictionTuple[] = []; - let bestCorrectionCost: number; + let bestTotalCost: number; const correctionPredictionMap: Record> = {}; for await(const match of getBestTokenMatches(searchModules, timer)) { // Corrections obtained: now to predict from them! @@ -700,8 +700,8 @@ export async function correctAndEnumerate( const predictions = buildAndMapPredictions(transition, tokenization, match, costFactor); // Only set 'best correction' cost when a correction ACTUALLY YIELDS predictions. - if(predictions.length > 0 && bestCorrectionCost === undefined) { - bestCorrectionCost = match.totalCost * costFactor; + if(predictions.length > 0 && bestTotalCost === undefined) { + bestTotalCost = match.totalCost * costFactor; } // If we're getting the same prediction again, it's lower-cost. Update! @@ -714,7 +714,7 @@ export async function correctAndEnumerate( rawPredictions = rawPredictions.concat(predictions); - if(shouldStopSearchingEarly(bestCorrectionCost, match.totalCost, rawPredictions)) { + if(shouldStopSearchingEarly(bestTotalCost, match.totalCost, rawPredictions)) { break; } } @@ -738,20 +738,15 @@ export function shouldStopSearchingEarly( return true; // If enough have been found, we're safe to terminate earlier. } else if(rawPredictions.length >= ModelCompositor.MAX_SUGGESTIONS) { - if(currentCorrectionCost >= bestCorrectionCost + CORRECTION_SEARCH_THRESHOLDS.REPLACEMENT_SEARCH_THRESHOLD) { - // Very useful for stopping 'sooner' when words reach a sufficient length. - return true; - } else { - // Sort the prediction list; we need them in descending probability order - // for the next check. - rawPredictions.sort((a, b) => b.totalProb - a.totalProb); + // Sort the prediction list; we need them in descending probability order + // for the next check. + rawPredictions.sort((a, b) => b.totalProb - a.totalProb); - // If the best result at the current state of the search fails to beat the worst - // pending suggestion from previous tiers, assume all further corrections will - // similarly fail to win; terminate the search-loop. - if(rawPredictions[ModelCompositor.MAX_SUGGESTIONS-1].totalProb > Math.exp(-currentCorrectionCost)) { - return true; - } + // If the best result at the current state of the search fails to beat the worst + // pending suggestion from previous tiers, assume all further corrections will + // similarly fail to win; terminate the search-loop. + if(rawPredictions[ModelCompositor.MAX_SUGGESTIONS-1].totalProb > Math.exp(-currentCorrectionCost)) { + return true; } } diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/distance-modeler.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/distance-modeler.tests.ts index dbd9f54a1d..97260c24ee 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/distance-modeler.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/distance-modeler.tests.ts @@ -13,7 +13,7 @@ import { PriorityQueue } from 'keyman/common/web-utils'; import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'; import { LexicalModelTypes } from '@keymanapp/common-types'; -import { CORRECTION_QUEUE_COMPARATOR, models, SearchNode } from '@keymanapp/lm-worker/test-index'; +import { CORRECTION_QUEUE_COMPARATOR, models, PREDICTION_QUEUE_COMPARATOR, SearchNode } from '@keymanapp/lm-worker/test-index'; import SENTINEL_CODE_UNIT = models.SENTINEL_CODE_UNIT; import Distribution = LexicalModelTypes.Distribution; @@ -50,15 +50,6 @@ function edgeHasChars(edge: SearchNode, input: string, match: string) { return lastEntry(edge.calculation.matchSequence) == match; } -function findEdgesWithChars(edgeArray: SearchNode[], match: string) { - let results = edgeArray.filter(function(value) { - return lastEntry(value.calculation.matchSequence) == match; - }); - - assert.isAtLeast(results.length, 1); - return results; -} - function fetchCommonTENode() { const rootSeed = SEARCH_EDGE_SEED++; const rootNode = new SearchNode(testModel.traverseFromRoot(), rootSeed, toKey); @@ -130,7 +121,9 @@ describe('Correction Distance Modeler', () => { assert.equal(rootNode.editCount, 0); assert.equal(rootNode.inputSamplingCost, 0); - assert.equal(rootNode.currentCost, 0); + assert.equal(rootNode.correctionCost, 0); + assert.isAbove(rootNode.predictionCost, 0); + assert.isAbove(rootNode.currentCost, 0); assert.equal((rootNode.currentTraversal as TrieTraversal).prefix, ''); assert.isFalse(rootNode.hasPartialInput); @@ -151,7 +144,7 @@ describe('Correction Distance Modeler', () => { assert.equal(clonedNode.editCount, 0); assert.equal(clonedNode.inputSamplingCost, 0); - assert.equal(clonedNode.currentCost, 0); + assert.equal(clonedNode.correctionCost, 0); assert.equal((clonedNode.currentTraversal as TrieTraversal).prefix, ''); assert.isFalse(clonedNode.hasPartialInput); @@ -167,6 +160,9 @@ describe('Correction Distance Modeler', () => { // Verify aliasing for properties holding immutable objects assert.equal(clonedNode.calculation, originalNode.calculation); assert.equal(clonedNode.currentTraversal, originalNode.currentTraversal); + + // Verify local values are properly copied. + assert.equal(clonedNode.currentCost, originalNode.currentCost); }); it('properly deep-copies fully-processed nodes later in the search path', () => { @@ -222,12 +218,12 @@ describe('Correction Distance Modeler', () => { // ***** - function assertSourceNodeProps(node: SearchNode) { + function assertExpectedNodeProps(node: SearchNode) { assert.equal(node.resultKey, 'te'); assert.equal(node.editCount, 0); assert.equal(node.inputSamplingCost, -Math.log(firstLayerTransforms[0].p) - Math.log(secondLayerTransforms[0].p)); - assert.equal(node.currentCost, node.inputSamplingCost); + assert.equal(node.correctionCost, node.inputSamplingCost); assert.isFalse(node.hasPartialInput); assert.isFalse(node.isFullReplacement) @@ -238,12 +234,12 @@ describe('Correction Distance Modeler', () => { assert.equal(node.spaceId, secondSpaceId); } - assertSourceNodeProps(teNode); + assertExpectedNodeProps(teNode); const clonedNode = new SearchNode(teNode); // Root node properties; may as well re-assert 'em. - assertSourceNodeProps(clonedNode); + assertExpectedNodeProps(clonedNode); // Avoid aliasing for properties holding mutable objects assert.notEqual(clonedNode.priorInput, teNode.priorInput); @@ -308,12 +304,12 @@ describe('Correction Distance Modeler', () => { // ***** - function assertSourceNodeProps(node: SearchNode) { + function assertExpectedNodeProbs(node: SearchNode) { assert.equal(node.resultKey, 'te'); assert.equal(node.editCount, 0); assert.equal(node.inputSamplingCost, -Math.log(firstLayerTransforms[0].p) - Math.log(secondLayerTransforms[0].p)); - assert.equal(node.currentCost, node.inputSamplingCost); + assert.equal(node.correctionCost, node.inputSamplingCost); assert.isTrue(node.hasPartialInput); assert.isFalse(node.isFullReplacement) @@ -324,12 +320,12 @@ describe('Correction Distance Modeler', () => { assert.equal(node.spaceId, secondLayerId); } - assertSourceNodeProps(teNode); + assertExpectedNodeProbs(teNode); const clonedNode = new SearchNode(teNode); // Root node properties; may as well re-assert 'em. - assertSourceNodeProps(clonedNode); + assertExpectedNodeProbs(clonedNode); // Avoid aliasing for properties holding mutable objects assert.notEqual(clonedNode.priorInput, teNode.priorInput); @@ -577,7 +573,7 @@ describe('Correction Distance Modeler', () => { // Allow a little value wiggle due to double-precision limitations. assert.approximately(subsetNodes[i].inputSamplingCost, expectedCosts[i], 1e-8); // No actual edit-tracking is done yet, so these should also match. - assert.approximately(subsetNodes[i].currentCost, expectedCosts[i], 1e-8); + assert.approximately(subsetNodes[i].correctionCost, expectedCosts[i], 1e-8); } }); @@ -605,7 +601,7 @@ describe('Correction Distance Modeler', () => { assert.equal(lastEntry(ins1_dl0[1].calculation.matchSequence), 'h'); assert.equal(ins1_dl0[1].editCount, 0); assert.isBelow(ins1_dl0[0].inputSamplingCost, ins1_dl0[1].inputSamplingCost); - assert.isBelow(ins1_dl0[0].currentCost, ins1_dl0[1].currentCost); + assert.isBelow(ins1_dl0[0].correctionCost, ins1_dl0[1].correctionCost); // Correction of _other_ input characters to the 't' and the 'h' come // after ALL other corrections - these don't get both 't' and 'h' input @@ -616,13 +612,13 @@ describe('Correction Distance Modeler', () => { assert.equal(lastEntry(ins1_dl0[FIRST_CHAR_VARIANTS].calculation.matchSequence), 'h'); assert.equal(ins1_dl0[FIRST_CHAR_VARIANTS].editCount, 1); assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS-1].inputSamplingCost, ins1_dl0[FIRST_CHAR_VARIANTS].inputSamplingCost); - assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS-1].currentCost, ins1_dl0[FIRST_CHAR_VARIANTS].currentCost); + assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS-1].correctionCost, ins1_dl0[FIRST_CHAR_VARIANTS].correctionCost); assert.equal(lastEntry(ins1_dl0[FIRST_CHAR_VARIANTS+1].calculation.inputSequence), SENTINEL_CODE_UNIT); assert.equal(lastEntry(ins1_dl0[FIRST_CHAR_VARIANTS+1].calculation.matchSequence), 't'); assert.equal(ins1_dl0[FIRST_CHAR_VARIANTS+1].editCount, 1); assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS].inputSamplingCost, ins1_dl0[FIRST_CHAR_VARIANTS+1].inputSamplingCost); - assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS].currentCost, ins1_dl0[FIRST_CHAR_VARIANTS+1].currentCost); + assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS].correctionCost, ins1_dl0[FIRST_CHAR_VARIANTS+1].correctionCost); // For everything in between... well, the input-sampling weight is uniform, and // all require a full edit. @@ -649,7 +645,7 @@ describe('Correction Distance Modeler', () => { assert.equal(ins0_dl1[0].editCount, 0); assert.isUndefined(lastEntry(ins0_dl1[0].calculation.inputSequence)); assert.equal(ins0_dl1[0].inputSamplingCost, subsetNodes[3].inputSamplingCost); - assert.equal(ins0_dl1[0].currentCost, subsetNodes[3].currentCost); + assert.equal(ins0_dl1[0].correctionCost, subsetNodes[3].correctionCost); // ************ // Set 1: set for ins 2, dl 1 - 'tr' + 'th'. @@ -667,8 +663,8 @@ describe('Correction Distance Modeler', () => { assert.equal(lastEntry(ins2_dl1[0].calculation.matchSequence), 't'); assert.equal(ins2_dl1[0].editCount, 0); // The subset hasn't yet split! - assert.equal(ins2_dl1[0].currentCost, subsetNodes[1].currentCost); - assert.isBelow(ins2_dl1[0].currentCost, ins2_dl1[1].currentCost); + assert.equal(ins2_dl1[0].correctionCost, subsetNodes[1].correctionCost); + assert.isBelow(ins2_dl1[0].correctionCost, ins2_dl1[1].correctionCost); // All other (non-'t') entries get full subset probability with edit count 1; // they're all substitutions, as they fail to match against a non-'t' path. @@ -699,8 +695,8 @@ describe('Correction Distance Modeler', () => { assert.equal(lastEntry(ins2_dl0[0].calculation.matchSequence), 'c'); assert.equal(ins2_dl0[0].editCount, 0); // The subset won't split. - assert.equal(ins2_dl0[0].currentCost, subsetNodes[2].currentCost); - assert.isBelow(ins2_dl0[0].currentCost, ins2_dl0[1].currentCost); + assert.equal(ins2_dl0[0].correctionCost, subsetNodes[2].correctionCost); + assert.isBelow(ins2_dl0[0].correctionCost, ins2_dl0[1].correctionCost); // All other (non-'c') entries get full subset probability with edit count 1; // they're all substitutions, as they fail to match against a non-'t' path. @@ -816,7 +812,7 @@ describe('Correction Distance Modeler', () => { const subsetNodes = teNode.buildSubstitutionEdges(synthDistribution, SEARCH_EDGE_SEED++); assert.equal(subsetNodes.length, 4); subsetNodes.sort(CORRECTION_QUEUE_COMPARATOR); - const expectedCosts = [0.5, .25, 0.15, 0.1].map(x => -Math.log(x) + teNode.currentCost); + const expectedCosts = [0.5, .25, 0.15, 0.1].map(x => -Math.log(x) + teNode.correctionCost); // The known subs for the subsets defined above. for(let i=0; i < expectedCosts.length; i++) { assert.isTrue(subsetNodes[i].hasPartialInput); @@ -826,7 +822,7 @@ describe('Correction Distance Modeler', () => { // Allow a little value wiggle due to double-precision limitations. assert.approximately(subsetNodes[i].inputSamplingCost, expectedCosts[i], 1e-8); // No actual edit-tracking is done yet, so these should also match. - assert.approximately(subsetNodes[i].currentCost, expectedCosts[i], 1e-8); + assert.approximately(subsetNodes[i].correctionCost, expectedCosts[i], 1e-8); } }); @@ -862,13 +858,13 @@ describe('Correction Distance Modeler', () => { assert.equal(lastEntry(ins1_dl0[TE_CHILD_PATH_COUNT].calculation.matchSequence), 'l'); assert.equal(ins1_dl0[TE_CHILD_PATH_COUNT].editCount, 1); assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT-1].inputSamplingCost, ins1_dl0[TE_CHILD_PATH_COUNT].inputSamplingCost); - assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT-1].currentCost, ins1_dl0[TE_CHILD_PATH_COUNT].currentCost); + assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT-1].correctionCost, ins1_dl0[TE_CHILD_PATH_COUNT].correctionCost); assert.equal(lastEntry(ins1_dl0[TE_CHILD_PATH_COUNT+1].calculation.inputSequence), SENTINEL_CODE_UNIT); assert.equal(lastEntry(ins1_dl0[TE_CHILD_PATH_COUNT+1].calculation.matchSequence), 'r'); assert.equal(ins1_dl0[TE_CHILD_PATH_COUNT+1].editCount, 1); assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT].inputSamplingCost, ins1_dl0[TE_CHILD_PATH_COUNT+1].inputSamplingCost); - assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT].currentCost, ins1_dl0[TE_CHILD_PATH_COUNT+1].currentCost); + assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT].correctionCost, ins1_dl0[TE_CHILD_PATH_COUNT+1].correctionCost); // For everything in between... well, the input-sampling weight is uniform, and // all require a full edit. @@ -912,8 +908,8 @@ describe('Correction Distance Modeler', () => { assert.equal(lastEntry(ins2_dl1[0].calculation.matchSequence), 'a'); assert.equal(ins2_dl1[0].editCount, 0); // The subset hasn't yet split! - assert.equal(ins2_dl1[0].currentCost, subsetNodes[1].currentCost); - assert.isBelow(ins2_dl1[0].currentCost, ins2_dl1[1].currentCost); + assert.equal(ins2_dl1[0].correctionCost, subsetNodes[1].correctionCost); + assert.isBelow(ins2_dl1[0].correctionCost, ins2_dl1[1].correctionCost); // All other (non-'t') entries get full subset probability with edit count 1; // they're all substitutions, as they fail to match against a non-'t' path. @@ -943,8 +939,8 @@ describe('Correction Distance Modeler', () => { assert.equal(lastEntry(ins2_dl0[0].calculation.matchSequence), 'c'); assert.equal(ins2_dl0[0].editCount, 0); // The subset won't split. - assert.equal(ins2_dl0[0].currentCost, subsetNodes[2].currentCost); - assert.isBelow(ins2_dl0[0].currentCost, ins2_dl0[1].currentCost); + assert.equal(ins2_dl0[0].correctionCost, subsetNodes[2].correctionCost); + assert.isBelow(ins2_dl0[0].correctionCost, ins2_dl0[1].correctionCost); // All other (non-'c') entries get full subset probability with edit count 1; // they're all substitutions, as they fail to match against a non-'t' path. @@ -1058,7 +1054,7 @@ describe('Correction Distance Modeler', () => { const layer1Edges = rootNode.buildSubstitutionEdges(synthDistribution1, layer1Id) // No 2+ inserts here; we're fine with just one call. .flatMap(e => e.processSubsetEdge()); - const layer1Queue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, layer1Edges); + const layer1Queue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, layer1Edges); const tEdge = layer1Queue.dequeue(); assertEdgeChars(tEdge, 't', 't'); @@ -1068,7 +1064,7 @@ describe('Correction Distance Modeler', () => { const layer2Edges = tEdge.buildSubstitutionEdges(synthDistribution2, layer2Id) // No 2+ inserts here; we're fine with just one call. .flatMap(e => e.processSubsetEdge()); - const layer2Queue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, layer2Edges); + const layer2Queue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, layer2Edges); const eEdge = layer2Queue.dequeue(); assertEdgeChars(eEdge, 'e', 'e'); @@ -1078,21 +1074,16 @@ describe('Correction Distance Modeler', () => { assertEdgeChars(hEdge, 'h', 'h'); assert.equal(hEdge.spaceId, layer2Id); - // Needed for a proper e <-> h transposition. - const ehEdge = findEdgesWithChars(layer2Edges, 'h')[0]; - - assert.isOk(ehEdge); // Final round: we'll use three nodes and throw all of their results into the same priority queue. + // Note: as we're constructing these directly, we're not modeling transpositions. const layer3Id = SEARCH_EDGE_SEED++; const layer3eEdges = eEdge.buildSubstitutionEdges(synthDistribution3, layer3Id) // No 2+ inserts here; we're fine with just one call. .flatMap(e => e.processSubsetEdge()); const layer3hEdges = hEdge.buildSubstitutionEdges(synthDistribution3, layer3Id) .flatMap(e => e.processSubsetEdge()); - const layer3ehEdges = ehEdge.buildSubstitutionEdges(synthDistribution3, layer3Id) - .flatMap(e => e.processSubsetEdge()); - const layer3Queue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, layer3eEdges.concat(layer3hEdges).concat(layer3ehEdges)); + const layer3Queue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, layer3eEdges.concat(layer3hEdges)); // Find the first result with an actual word directly represented. let bestEdge; diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts index 4bccef2ce9..dcc3cf3f0c 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/getBestMatches.tests.ts @@ -18,7 +18,8 @@ import { models, LegacyQuotientRoot, SearchQuotientCluster, - TokenResultMapping + TokenResultMapping, + CORRECTION_QUEUE_COMPARATOR } from '@keymanapp/lm-worker/test-index'; import TrieModel = models.TrieModel; @@ -32,74 +33,31 @@ function buildTestTimer() { describe('Correction Searching', () => { describe('without multi-tokenization; using a single SearchPath sequence', () => { const checkRepeatableResults_teh = async (iter: AsyncGenerator, any, any>) => { - const firstIterResult = await iter.next(); // {value: , done: } - assert.isFalse(firstIterResult.done); - - const firstResult: TokenResultMapping = firstIterResult.value; // Retrieves - // No checks on the first set's cost. - assert.equal(firstResult.matchString, "ten"); - - // All start with 'te' but one, and invoke one edit of the same cost. - // 'th' has an 'h' at the same cost (input 3) of the 'e' (input 2). - const secondBatch = [ - 'tec', 'tel', 'tem', - 'ter', 'tes', 'th', - 'te' + const expectedFirstTwenty = [ + 'ten', // no edits required whatsoever + 'th', // one edit (deletion), but gets to ignore the cost of a keystroke and still prefixes 'the' + 'the', // one edit (transposition), incurs the cost of all three keystrokes + 'te', // one edit (deletion), but gets to ignore the cost of a keystroke + 'tel', 'beh', // both cost one edit (hard character replacement: n/h -> l vs t -> b) + // Other edits, generally of one edit cost, predicting words of varying frequency. + 'ter', 'tha', 'thi', 'thr', 'tho', 'tem', 'thu', 'then', 'men', 'wen', 'gen', 'en', 'sen', 'tec' ]; - async function checkBatch(batch: string[], prevCost: number) { - let cost; - while(batch.length > 0) { - const iter_result = await iter.next(); - assert.isFalse(iter_result.done); - - const result = iter_result.value; - assert.isAbove(result.totalCost, prevCost); - if(cost !== undefined) { - assert.equal(result.totalCost, cost); - } else { - cost = result.totalCost; - } - - const matchIndex = batch.findIndex((entry) => entry == result.matchString); - assert.notEqual(matchIndex, -1, `'${result.matchString}' received as prediction too early`); - batch.splice(matchIndex, 1); - } - - return cost; + let results: TokenResultMapping[] = []; + for(let i = 0; i < expectedFirstTwenty.length; i++) { + const iterResult = await iter.next(); + results.push(iterResult.value as TokenResultMapping); } - const secondCost = await checkBatch(secondBatch, firstResult.totalCost); + assert.sameOrderedMembers(results.map((r) => r.matchString), expectedFirstTwenty); + for(let i=0; i < expectedFirstTwenty.length - 1; i++) { + assert.isAtLeast(results[i+1].totalCost, results[i].totalCost); + } - // Single hard edit, all other input probability aspects are equal - const thirdBatch = [ - // 't' -> 'b' (sub) - 'beh', - // '' -> 'c' (insertion) - 'tech', - // 'eh' -> 'he' (transposition) - 'the' - ]; - - await checkBatch(thirdBatch, secondCost); - - // All replace the low-likelihood case for the third input. - const fourthBatch = [ - 'thi', 'tho', 'thr', - 'thu', 'tha' - ]; - - await checkBatch(fourthBatch, secondCost); - - // Replace the _first_ input's char OR insert an extra char, - // also matching the low-likelihood third-char option. - const fifthBatch = [ - 'cen', 'en', 'gen', - 'ken', 'len', 'men', - 'sen', 'then', 'wen' - ]; - - await checkBatch(fifthBatch, secondCost); + // The results will not be in the order as raw correction likelihood because some words + // are more frequent than others. + results.sort(CORRECTION_QUEUE_COMPARATOR); + assert.notSameOrderedMembers(results.map((r) => r.matchString), expectedFirstTwenty); } it('Empty search root, loaded model', async () => { @@ -113,12 +71,13 @@ describe('Correction Searching', () => { // While there's no input, insertion operations can produce suggestions. const resultState = await iter.next(); - const result = resultState.value; + const result = resultState.value as TokenResultMapping; // Just one suggestion root should be returned as the first result. - assert.equal(result.totalCost, 0); // Gives a perfect match + assert.equal(result.correctionCost, 0); // Gives a perfect match assert.equal(result.matchString, ''); // an empty match string. assert.isFalse(resultState.done); + assert.isAbove(result.totalCost, 0); }); // Hmm... how best to update this... @@ -423,9 +382,9 @@ describe('Correction Searching', () => { // paths of lower total cost. pathsResults.push(nextFromPaths); - assert.isAtLeast(nextFromCluster.totalCost, baseCost); - assert.isAtLeast(nextFromPaths.totalCost, baseCost); - baseCost = Math.max(baseCost, nextFromCluster.totalCost, nextFromPaths.totalCost); + assert.isAtLeast(nextFromCluster.correctionCost, baseCost); + assert.isAtLeast(nextFromPaths.correctionCost, baseCost); + baseCost = Math.max(baseCost, nextFromCluster.correctionCost, nextFromPaths.correctionCost); } assert.deepEqual(genResults.map(r => r.matchString), pathsResults.map(r => r.matchString)); @@ -434,22 +393,13 @@ describe('Correction Searching', () => { assert.sameDeepMembers(pathsResults.slice(0, 3).map(r => r.matchString), ['th', 'to', 'tr']); // These involve likely-enough corrections that should show, given the model fixture. assert.includeDeepMembers(pathsResults.map(r => r.matchString), [ - 'ty', // 'type' is quite frequent according to the text fixture. 't', // Deleting the second keystroke outright lands here. 'oth', // What if we insert an 'o' early on? 'other' is a very common English word - 'ti' // 'time' is pretty common too. + 'ti', // 'time' is pretty common too. + 'thi', // 'this' is common enough to show up early despite inserting the 'i'. + 'wh', // "which" is a super-frequent English word, worthy of being a forced correction + 'sh' // "she" is also strong enough to force an early appearance. ]); - - // NOTE: this level of corrections does not yet consider the word likelihood - only - // the raw correction cost. No ordering of "likely word" to "unlikely word" should - // occur yet. - - // 'time': weight 934 - // 'type': weight 540 - const timeResult = pathsResults.find(r => r.matchString == 'ti'); - const typeResult = pathsResults.find(r => r.matchString == 'ty'); - // Correction to either should be equally likely. - assert.equal(timeResult.totalCost, typeResult.totalCost); }); }); }); diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/legacy-quotient-spur.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/legacy-quotient-spur.tests.ts index ccc38467a5..74b2af3e43 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/legacy-quotient-spur.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/legacy-quotient-spur.tests.ts @@ -427,7 +427,7 @@ describe('LegacyQuotientSpur', () => { const entry_the = thirdResults.find((entry) => entry.matchString == 'the' && entry.editCount == 1); assert.isOk(entry_the); - thirdResults.sort((a, b) => a.totalCost - b.totalCost); + thirdResults.sort((a, b) => a.correctionCost - b.correctionCost); const the_index = thirdResults.findIndex((entry) => entry.matchString == 'the' && entry.editCount == 1); // `teh` should appear fairly early as a viable correction. assert.isBelow(the_index, 10); @@ -438,7 +438,7 @@ describe('LegacyQuotientSpur', () => { // We want to make sure we don't auto-ignore transposition cases by // accident by failing that conditional. const the_entry = thirdResults[the_index]; - assert.isBelow(the_entry.totalCost - thirdResults[0].totalCost, CORRECTION_SEARCH_THRESHOLDS.REPLACEMENT_SEARCH_THRESHOLD); + assert.isBelow(the_entry.correctionCost - thirdResults[0].correctionCost, CORRECTION_SEARCH_THRESHOLDS.REPLACEMENT_SEARCH_THRESHOLD); }); }); }); \ No newline at end of file diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/quotient-node-finalizer.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/quotient-node-finalizer.tests.ts index ee6ee088e6..1a89a68321 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/quotient-node-finalizer.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/quotient-node-finalizer.tests.ts @@ -99,9 +99,9 @@ describe('QuotientNodeFinalizer', () => { assert.equal(searchResult.type, 'complete'); if(searchResult.type == 'complete') { - assert.equal(searchResult.mapping.totalCost, -Math.log(therefo.bestExample.p)); + assert.equal(searchResult.mapping.correctionCost, -Math.log(therefo.bestExample.p)); assert.isNotNaN(searchResult.cost); - assert.equal(searchResult.cost, searchResult.mapping.totalCost); + assert.isAtLeast(searchResult.cost, searchResult.mapping.totalCost); } else { return; } @@ -129,9 +129,9 @@ describe('QuotientNodeFinalizer', () => { assert.equal(searchResult.type, 'complete'); if(searchResult.type == 'complete') { - assert.isAbove(searchResult.mapping.totalCost, -Math.log(therefo.bestExample.p)); + assert.isAbove(searchResult.mapping.correctionCost, -Math.log(therefo.bestExample.p)); assert.isNotNaN(searchResult.cost); - assert.equal(searchResult.cost, searchResult.mapping.totalCost); + assert.isAtLeast(searchResult.cost, searchResult.mapping.totalCost); } else { return; } diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/build-and-map-predictions.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/build-and-map-predictions.tests.ts index 5051c2acbf..72876e4ca1 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/build-and-map-predictions.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/prediction-helpers/build-and-map-predictions.tests.ts @@ -58,7 +58,7 @@ describe('buildAndMapPredictions', () => { const mappedPredictions = buildAndMapPredictions( transition, transition.base.displayTokenization, - {matchString: 'the', totalCost: 0, editCount: 0}, + {matchString: 'the', correctionCost: 0, editCount: 0}, 1 ); @@ -104,7 +104,7 @@ describe('buildAndMapPredictions', () => { const mappedPredictions = buildAndMapPredictions( transition, transition.base.displayTokenization, - {matchString: '', totalCost: 0, editCount: 0}, + {matchString: '', correctionCost: 0, editCount: 0}, 1 ); @@ -148,7 +148,7 @@ describe('buildAndMapPredictions', () => { const mappedPredictions = buildAndMapPredictions( transition, transition.base.displayTokenization, - {matchString: '', totalCost: 0, editCount: 0}, + {matchString: '', correctionCost: 0, editCount: 0}, 1 ); @@ -210,7 +210,7 @@ describe('buildAndMapPredictions', () => { const mappedPredictions = buildAndMapPredictions( transition, transition.final.displayTokenization, - {matchString: '', totalCost: 0, editCount: 0}, + {matchString: '', correctionCost: 0, editCount: 0}, 1 );