Merge pull request #15441 from keymanapp/change/web/module-constituent-paths

change(web): determine all module-paths leading to a SearchQuotientNode 🚂
This commit is contained in:
Joshua Horton 2026-01-29 23:05:51 +07:00 • committed by GitHub
commit 072209af26
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 76 additions and 15 deletions

View file

@ -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<Transform>[];
@ -207,7 +210,7 @@ export interface SearchQuotientNode {
* @param keystrokeDistributions
* @internal
*/
export function quotientPathHasInputs(node: SearchQuotientNode, keystrokeDistributions: Distribution<Transform>[]): boolean {
function quotientPathHasInputs(node: SearchQuotientNode, keystrokeDistributions: Distribution<Transform>[]): 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
}

View file

@ -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';
export * from './transform-subsets.js';
// unitTestEndpoints
import { unitTestEndpoints as SearchQuotientNodeEndpoints } from './correction/search-quotient-node.js';
export const unitTestEndpoints: typeof SearchQuotientNodeEndpoints = {
...SearchQuotientNodeEndpoints
}

View file

@ -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});

View file

@ -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'));

View file

@ -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.
});
});