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 7d1a953bf0..e6d5c1217d 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 @@ -10,10 +10,11 @@ import { LexicalModelTypes } from "@keymanapp/common-types"; import { SearchNode, SearchResult } from "./distance-modeler.js"; +import { SearchQuotientSpur } from "./search-quotient-spur.js"; +import { SearchQuotientRoot } from "./search-quotient-root.js"; import Distribution = LexicalModelTypes.Distribution; import Transform = LexicalModelTypes.Transform; -import { SearchQuotientSpur } from "./search-quotient-spur.js"; let SPACE_ID_SEED = 0; @@ -62,20 +63,20 @@ export interface InputSegment { } /** - * Models the properties and portion of an input event applied by a SearchSpace for - * correction-search purposes. + * Models the properties and portion of an input event applied by a + * SearchQuotientNode for correction-search purposes. */ export interface PathInputProperties { /** * Denotes the portion of the ongoing input stream represented by the corresponding - * input distribution(s) of a SearchSpace. + * input distribution(s) of a SearchQuotientNode. */ segment: InputSegment; /** * Notes the highest probability found in the input event's transform * distribution, regardless of whether or not that specific corresponding - * input is included within the SearchSpace's correction space. + * input is included within the SearchQuotientNode's correction space. */ bestProbFromSet: number; @@ -103,8 +104,8 @@ export interface SearchQuotientNode { readonly spaceId: number; /** - * Notes the SearchQuotientNode(s) whose correction-search paths are extended - * by this SearchQuotientNode. + * Notes the SearchQuotientNode(s) whose correction-search paths are extended by this + * SearchQuotientNode. */ readonly parents: SearchQuotientNode[]; @@ -163,7 +164,9 @@ export interface SearchQuotientNode { /** * Retrieves the sequence of inputs that led to this SearchSpace. * - * THIS WILL BE REMOVED SHORTLY. (Once SearchQuotientNode takes on merging & + * THIS WILL BE REMOVED SHORTLY in favor of `constituentPaths` below, which + * provides an improved view into the data and models multiple paths to the + * space when they exist. (Once SearchQuotientNode takes on merging & * splitting) */ readonly inputSequence: Distribution[]; @@ -207,7 +210,7 @@ export interface SearchQuotientNode { * @param keystrokeDistributions * @internal */ -export function quotientPathHasInputs(node: SearchQuotientNode, keystrokeDistributions: Distribution[]): boolean { +function quotientPathHasInputs(node: SearchQuotientNode, keystrokeDistributions: Distribution[]): boolean { if(!(node instanceof SearchQuotientSpur)) { for(const p of node.parents) { if(quotientPathHasInputs(p, keystrokeDistributions)) { @@ -263,4 +266,33 @@ export function quotientPathHasInputs(node: SearchQuotientNode, keystrokeDistrib return parentHasInput(); } +} + +/** + * Enumerates the different potential SearchQuotientSpur sequences that lead + * to a SearchQuotientNode. + * + * Intended only for use during unit testing. Does not include the root node. + */ +function constituentPaths(node: SearchQuotientNode): SearchQuotientSpur[][] { + if(node instanceof SearchQuotientRoot) { + return []; + } else if(node instanceof SearchQuotientSpur) { + const parentPaths = constituentPaths(node.parents[0]); + if(parentPaths.length > 0) { + return parentPaths.map(p => { + p.push(node); + return p; + }); + } else { + return [[node]]; + } + } else { + throw new Error("constituentPaths is unable to handle a new, unexpected SearchQuotientNode type"); + } +} + +export const unitTestEndpoints = { + quotientPathHasInputs, + constituentPaths } \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-thread/src/main/test-index.ts b/web/src/engine/predictive-text/worker-thread/src/main/test-index.ts index 291e9296b7..c6a05e7694 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/test-index.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/test-index.ts @@ -20,4 +20,10 @@ export { ModelCompositor } from './model-compositor.js'; export * from './predict-helpers.js'; export { default as TransformUtils } from './transformUtils.js' export { default as LMLayerWorker } from './index.js' -export * from './transform-subsets.js'; \ No newline at end of file +export * from './transform-subsets.js'; + +// unitTestEndpoints +import { unitTestEndpoints as SearchQuotientNodeEndpoints } from './correction/search-quotient-node.js'; +export const unitTestEndpoints: typeof SearchQuotientNodeEndpoints = { + ...SearchQuotientNodeEndpoints +} \ No newline at end of file diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-token.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-token.tests.ts index e242b79c54..07caadbe78 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-token.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/context/context-token.tests.ts @@ -13,14 +13,16 @@ import { assert } from 'chai'; import { default as defaultBreaker } from '@keymanapp/models-wordbreakers'; import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'; import { LexicalModelTypes } from '@keymanapp/common-types'; +import { KMWString } from '@keymanapp/web-utils'; -import { ContextToken, correction, generateSubsetId, getBestMatches, models, preprocessInputSources, quotientPathHasInputs, SearchQuotientSpur } from '@keymanapp/lm-worker/test-index'; +import { ContextToken, correction, generateSubsetId, getBestMatches, models, preprocessInputSources, SearchQuotientSpur, unitTestEndpoints } from '@keymanapp/lm-worker/test-index'; import Distribution = LexicalModelTypes.Distribution; import ExecutionTimer = correction.ExecutionTimer; import Transform = LexicalModelTypes.Transform; import TrieModel = models.TrieModel; -import { KMWString } from '@keymanapp/web-utils'; + +const { quotientPathHasInputs } = unitTestEndpoints; var plainModel = new TrieModel(jsonFixture('models/tries/english-1000'), {wordBreaker: defaultBreaker}); diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-node.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-node.tests.ts index 85681a944e..7e50825b57 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-node.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-node.tests.ts @@ -1,10 +1,12 @@ import { assert } from 'chai'; import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'; -import { LegacyQuotientRoot, models, quotientPathHasInputs } from '@keymanapp/lm-worker/test-index'; +import { LegacyQuotientRoot, models, unitTestEndpoints } from '@keymanapp/lm-worker/test-index'; import { buildSimplePathSplitFixture } from './search-quotient-spur.tests.js'; +const { quotientPathHasInputs } = unitTestEndpoints; + import TrieModel = models.TrieModel; const testModel = new TrieModel(jsonFixture('models/tries/english-1000')); diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts index 92fb31f35e..8c01201c6c 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts @@ -10,10 +10,12 @@ import { assert } from 'chai'; import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'; -import { LegacyQuotientSpur, models, LegacyQuotientRoot, quotientPathHasInputs } from '@keymanapp/lm-worker/test-index'; +import { LegacyQuotientSpur, models, LegacyQuotientRoot, unitTestEndpoints } from '@keymanapp/lm-worker/test-index'; import TrieModel = models.TrieModel; +const { constituentPaths, quotientPathHasInputs } = unitTestEndpoints; + const testModel = new TrieModel(jsonFixture('models/tries/english-1000')); export function buildSimplePathSplitFixture() { @@ -44,7 +46,7 @@ export function buildSimplePathSplitFixture() { const path4 = new LegacyQuotientSpur(path3, distrib4, distrib4[0]); return { - paths: [rootPath, path1, path2, path3, path4], + paths: [null, path1, path2, path3, path4], distributions: [distrib1, distrib2, distrib3, distrib4] }; } @@ -270,6 +272,23 @@ describe('SearchQuotientSpur', () => { {text: '', p: 1}) ); assert.isTrue(quotientPathHasInputs(pathToSplit, distributions)); + assert.equal(constituentPaths(pathToSplit).length, 1); }); }); + + describe('constituentPaths', () => { + it('includes a single entry array when all parents are SearchQuotientSpurs', () => { + const { paths } = buildSimplePathSplitFixture(); + const finalPath = paths[4]; + + assert.equal(constituentPaths(finalPath).length, 1); + + const pathSequence = constituentPaths(finalPath)[0]; + assert.equal(pathSequence.length, 4); // 4 inputs; does not include root node + + assert.sameOrderedMembers(pathSequence, paths.slice(1)); + }); + + // TODO: add a test for mixed SearchPath / SearchCluster cases. + }); }); \ No newline at end of file