feat(web): extend search-graph test-helper funcs to handle specialized spur types
Some checks failed
Keyman Build Summary / Summarize build status checks (push) Has been cancelled

Build-bot: skip build:web
Test-bot: skip
This commit is contained in:
Joshua Horton 2026-03-10 16:23:57 -05:00
parent 09db6a4766
commit 9165e8fc65
3 changed files with 95 additions and 2 deletions

View file

@ -9,9 +9,13 @@
import { assert } from 'chai';
import { DeletionQuotientSpur, InsertionQuotientSpur } from '@keymanapp/lm-worker/test-index';
import { constituentPaths } from "./constituentPaths.js";
import { toSpurTypeSequence } from './toSpurTypeSequence.js';
import { buildCantLinearFixture } from './buildCantLinearFixture.js';
import { buildAlphabeticClusterFixtures } from './buildAlphabeticClusteredFixture.js';
import { buildQuotientDocFixture } from './buildQuotientDocFixture.js';
describe('constituentPaths', () => {
it('includes a single entry array when all parents are SearchQuotientSpurs', () => {
@ -42,4 +46,50 @@ describe('constituentPaths', () => {
return p;
}));
});
describe('for the final quotient-graph doc example', () => {
it('handles insertion-only quotient-graph paths', () => {
const { sc2 } = buildQuotientDocFixture().nodes;
const sc2Constituents = constituentPaths(sc2);
assert.equal(sc2Constituents.length, 1);
sc2Constituents.forEach(s => s.forEach(p => assert.isTrue(p instanceof InsertionQuotientSpur)));
});
it('handles deletion-only quotient-graph paths', () => {
const { k2c0 } = buildQuotientDocFixture().nodes;
const k2c0Constituents = constituentPaths(k2c0);
assert.equal(k2c0Constituents.length, 1);
k2c0Constituents.forEach(s => s.forEach(p => assert.isTrue(p instanceof DeletionQuotientSpur)));
});
it('does not emit sequences with inserts immediately following deletes', () => {
const { k2c3 } = buildQuotientDocFixture().nodes;
const k2c3Constituents = constituentPaths(k2c3);
const shouldNotOccur = k2c3Constituents.find((seq) => {
const typeSeq = toSpurTypeSequence(seq);
return typeSeq.find((type, index) => {
return type == 'delete' && typeSeq[index+1] == 'insert';
});
});
assert.isNotOk(shouldNotOccur);
});
it('does emit sequences with deletes immediately following inserts', () => {
const { k2c3 } = buildQuotientDocFixture().nodes;
const k2c3Constituents = constituentPaths(k2c3);
const shouldOccur = k2c3Constituents.find((seq) => {
const typeSeq = toSpurTypeSequence(seq);
return typeSeq.find((type, index) => {
return type == 'insert' && typeSeq[index+1] == 'delete';
});
});
assert.isNotOk(shouldOccur);
});
});
});

View file

@ -8,6 +8,8 @@
*/
import {
DeletionQuotientSpur,
InsertionQuotientSpur,
SearchQuotientCluster,
SearchQuotientNode,
SearchQuotientRoot,
@ -26,8 +28,29 @@ export function constituentPaths(node: SearchQuotientNode): SearchQuotientSpur[]
} else if(node instanceof SearchQuotientCluster) {
return node.parents.flatMap((p) => constituentPaths(p));
} else if(node instanceof SearchQuotientSpur) {
const parentPaths = constituentPaths(node.parents[0]);
let pathsToExtend = parentPaths;
const parentPaths = constituentPaths(node.parents[0]); let pathsToExtend = parentPaths;
if(node instanceof InsertionQuotientSpur) {
pathsToExtend = pathsToExtend.filter(s => {
const tail = s[s.length - 1];
// Deletion nodes and modules should always be ordered after those for
// insertion in order to avoid duplicating search paths. (Insertions may
// stick to the right of a root, while deletions always process inputs; they
// may thus precede deletions.)
//
// Also, internally, insertion edges are not built after deletion (or empty) edges.
if(tail instanceof DeletionQuotientSpur) {
return false;
} else if(tail.insertLength == 0 && tail.leftDeleteLength == 0) {
// Insertions should also not appear after empty nodes; there's no net
// difference between inserting before and inserting after.
return false;
}
return true;
});
}
if(parentPaths.length > 0) {
return pathsToExtend.map(p => {

View file

@ -0,0 +1,20 @@
import {
DeletionQuotientSpur,
InsertionQuotientSpur,
SearchQuotientNode,
SubstitutionQuotientSpur
} from "@keymanapp/lm-worker/test-index";
export function toSpurTypeSequence(spurs: SearchQuotientNode[]): ('insert' | 'delete' | 'substitute' | 'legacy')[] {
return spurs.map(s => {
if(s instanceof InsertionQuotientSpur) {
return 'insert';
} else if(s instanceof DeletionQuotientSpur) {
return 'delete';
} else if(s instanceof SubstitutionQuotientSpur) {
return 'substitute';
} else {
return 'legacy';
}
})
}