Merge branch 'change/web/fix-result-spaceId-remapping' into change/web/return-search-token-mapping

This commit is contained in:
Joshua Horton 2026-04-20 16:39:17 -05:00
commit 6e1cb03bc4
8 changed files with 29 additions and 23 deletions

View file

@ -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<TokenResultMapping> {
export async function *getBestMatches(searchModules: SearchQuotientNode[], timer: ExecutionTimer): AsyncGenerator<Readonly<TokenResultMapping>> {
const spaceQueue = new PriorityQueue<SearchQuotientNode>((a, b) => a.currentCost - b.currentCost);
// Stage 1 - if we already have extracted results, build a queue just for them

View file

@ -47,8 +47,7 @@ export class LegacyQuotientRoot extends SearchQuotientRoot {
return {
type: 'complete',
cost: node.currentCost,
mapping: new TokenResultMapping(node),
spaceId: this.spaceId
mapping: new TokenResultMapping(node)
};
}

View file

@ -87,6 +87,7 @@ export class SearchQuotientCluster implements SearchQuotientNode {
this.lowestPossibleSingleCost = lowestPossibleSingleCost;
this.completedPaths = inboundPaths.flatMap(p => p.previousResults).map(r => r.node);
this.completedPaths.forEach((p) => p.spaceId = this.spaceId);
this.selectionQueue.enqueueAll(inboundPaths);
return;
@ -144,15 +145,16 @@ export class SearchQuotientCluster implements SearchQuotientNode {
this.selectionQueue.enqueue(bestPath);
if(currentResult.type == 'complete') {
this.completedPaths?.push(currentResult.mapping.node);
currentResult.spaceId = this.spaceId;
const node = currentResult.mapping.node;
node.spaceId = this.spaceId;
this.completedPaths?.push(node);
}
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 {

View file

@ -31,8 +31,7 @@ type IntermediateSearchPath = {
type CompleteSearchPath = {
type: 'complete',
cost: number,
mapping: TokenResultMapping,
spaceId: number
mapping: TokenResultMapping
}
export type PathResult = NullPath | IntermediateSearchPath | CompleteSearchPath;

View file

@ -77,8 +77,7 @@ export class SearchQuotientRoot implements SearchQuotientNode {
return {
type: 'complete',
cost: 0,
mapping: new TokenResultMapping(this.rootNode),
spaceId: this.spaceId
mapping: new TokenResultMapping(this.rootNode)
};
}

View file

@ -404,8 +404,7 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode {
return {
type: 'complete',
cost: currentNode.currentCost,
mapping: new TokenResultMapping(currentNode),
spaceId: this.spaceId
mapping: new TokenResultMapping(currentNode)
};
}

View file

@ -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";
@ -9,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<Transform>[] {

View file

@ -536,7 +536,7 @@ export async function correctAndEnumerate(
const correctionPredictionMap: Record<string, Distribution<Suggestion>> = {};
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) {